使用Helm将基于Postgres的Yesod网络应用程序部署到Kubernetes上的教程

89 阅读6分钟

Yesod是Haskell领域中最流行的网络框架之一。这篇文章将探讨创建一个基于Postgres的Yesod网络应用程序样本,然后将其部署到Kubernetes集群。我们将创建一个Helm图来做我们的kubernetes发布。请注意,整个源代码都在这里。

创建Yesod应用程序

第一步将是使用Stack build工具创建一个Yesod应用程序的样本

$ stack new yesod-demo yesod-postgres

现在,我将在现有的脚手架网站上添加一个额外的模型

Person json
    name Text
    age Int
    deriving Show

同时在应用程序中添加四个新的路由

/person PersonR GET POST

/health/liveness LivenessR GET
/health/readiness ReadinessR GET

基于/health 的处理程序是为了对你的应用程序进行基本的健康检查。工作节点中的每个kubelet将执行定期诊断检查,以确定你的Web应用程序的健康状况。根据结果,它将执行适当的行动(如重新启动你的失败容器)。

现在让我们为上述路由定义一些Yesod处理程序

postPersonR :: Handler RepJson
postPersonR = do
  person :: Person <- requireJsonBody
  runDB $ insert_ person
  return $ repJson $ toJSON person

getPersonR :: Handler RepJson
getPersonR = do
  persons :: [Entity Person] <- runDB $ selectList [] []
  return $ repJson $ toJSON persons

getLivenessR :: Handler ()
getLivenessR = return ()

getReadinessR :: Handler RepJson
getReadinessR = do
  person :: [Entity Person] <- runDB $ selectList [] [LimitTo 1]
  return $ repJson $ toJSON person

postPersonR 函数将在数据库中插入一个Person 对象,getPersonR 函数将返回数据库中所有Person 的列表。另外两个处理程序是为kubelet执行的探测检查而设的。我已经为它们提供了简化的实现。有效性探测是为了告诉kubelet容器是否在运行。如果它没有运行,那么容器就会被杀死,并根据重启策略,采取适当的行动。另一方面,准备性探针通知kubelet,容器是否准备好为请求提供服务。你可以在这里阅读更多关于它们的信息。

构建Docker镜像

现在,我将更新stack.yaml 文件,为我们的应用程序构建一个docker镜像。

image:
  container:
    name: psibi/yesod-demo:3.0
    base: fpco/stack-build
    add:
      static: /app
    
    entrypoints:
    - yesod-demo

docker:
  enable: true

注意,docker镜像的名称被指定为psibi/yesod-demo:3.0 。另外,我还会在容器内添加static 目录,这样它就能被服务器正常提供服务。现在,做stack image container ,将构建docker镜像。你可以用docker工具来验证它。

 $ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
psibi/yesod-demo           3.0-yesod-demo      8e32c1329557        46 hours ago        7.92GB

现在,我将把它推送到docker注册表。

$ docker push psibi/yesod-demo:3.0-yesod-demo

部署到Kubernetes

鉴于我们的应用程序现在已经在docker hub中可用,你可以继续创建Kubernetes清单,并准备部署我们的yesod应用程序。由于我们的应用程序有PostgreSQL作为数据库后端,我们将利用现有的Helm postgres图表。注意,对于关键任务的工作负载来说,像RDS这样的东西会是一个更好的选择。让我们为我们的应用程序创建一个Helm图表。

$ helm create yesod-postgres-chart
Creating yesod-postgres-chart
$ tree yesod-postgres-chart/
yesod-postgres-chart/
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── ingress.yaml
│   ├── NOTES.txt
│   └── service.yaml
└── values.yaml

2 directories, 7 files

现在我将在文件夹内创建一个requirements.txt 文件,并在那里定义我们的postgresql依赖关系。

dependencies:
  - name: postgresql
    version: 0.15.0
    repository: https://kubernetes-charts.storage.googleapis.com

现在,让我们从我们的values.yaml 文件中配置postgresql helm chart。

postgresql:
  postgresUser: postgres
  postgresPassword: your-postgresql-password
  postgresDatabase: yesod-test
  persistence:
    storageClass: ssd-slow

请注意,我们还在上述文件中提供了一个名为ssd-slow 的存储类。所以,你必须在k8s集群中配置它。使用StorageClass ,postgres helm chart将按需创建卷。所有的k8s清单都在templates 这个目录下。让我们首先创建上述storageClass资源。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: ssd-slow
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2

Kubernetes有一个ConfigMap和Secret的概念,用来存储特殊的配置和秘密(duh!)数据。让我们为我们的yesod应用程序定义ConfigMap 资源。

apiVersion: v1
data:
  yesod_static_path: /app
  yesod_db_name: yesod-test
  yesod_db_port: "5432"
  yesod_host_name: "{{ .Release.Name  }}-postgresql"
kind: ConfigMap
metadata:
  name: {{ template "yesod-postgres-chart.full_name" . }}-configmap

我们之前在我们的stack.yaml ,我们的静态文件夹被添加到了/app ,所以相应的我们给出了正确的路径到键yesod_static_path 。我已经在文件_helpers.tpl 中定义了一些辅助函数,现在我用它来生成metadata.name 。我们将在整个模板中使用这些辅助函数。现在,让我们为你的应用程序定义Secret manifest。

apiVersion: v1
kind: Secret
metadata:
  name: {{ template "yesod-postgres-chart.full_name" . }}-secret
type: Opaque
data:
  yesod-db-password: {{ .Values.postgresql.postgresPassword | b64enc | quote }}
  yesod-db-user: {{ .Values.postgresql.postgresUser | b64enc | quote }}

现在是重要的部分--部署清单。他们基本上是一个控制器,将管理你的pod。我们的应用程序的这个清单稍微大一些。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{ template "yesod-postgres-chart.full_name" . }}
spec:
  replicas: 1
  template:
    metadata:
      labels:
        {{- include "yesod-postgres-chart.release_labels" . | indent 8 }}
    spec:
      containers:
        - name: yesod-demo
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          resources: 
{{ toYaml .Values.yesodapp.resources | indent 12 }}
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 3000
              scheme: HTTP
              initialDelaySeconds: 30
              periodSeconds: 15
              timeoutSeconds: 5              
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 3000
              scheme: HTTP
            initialDelaySeconds: 30
            timeoutSeconds: 1
          env:
            - name: YESOD_STATIC_DIR
              valueFrom:
                configMapKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-configmap
                  key: yesod_static_path
            - name: YESOD_PGPORT
              valueFrom:
                configMapKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-configmap
                  key: yesod_db_port
            - name: YESOD_PGHOST
              valueFrom:
                configMapKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-configmap
                  key: yesod_host_name
            - name: YESOD_PGDATABASE
              valueFrom:
                configMapKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-configmap
                  key: yesod_db_name
            - name: YESOD_PGUSER
              valueFrom:
                secretKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-secret
                  key: yesod-db-user
            - name: YESOD_PGPASS
              valueFrom:
                secretKeyRef:
                  name: {{ template "yesod-postgres-chart.full_name" . }}-secret
                  key: yesod-db-password

虽然上述文件看起来很复杂,但其实很简单。我们在spec.containers.image 中指定docker镜像,在spec.containers.resources 中指定计算资源。然后,我们用之前定义的适当的路径指定liveness和readiness probe。之后,我们定义环境变量,这是正常运行该应用程序所需要的。我们从之前定义的configmaps和secrets中获取这些变量。

最后一步是创建一个服务清单,它将作为部署后创建的pod的持久化端点。服务使用标签来选择一个特定的pod。这就是我们的服务定义。

kind: Service
apiVersion: v1
metadata:
  name: {{ template "yesod-postgres-chart.full_name" . }}
  labels:
    {{- include "yesod-postgres-chart.release_labels" . | indent 4 }}
spec:
  selector:
    app: {{ template "yesod-postgres-chart.full_name" . }}
  ports:
    - protocol: "TCP"
      port: 3100
      targetPort: 3000
  type: ClusterIP

在上述文件中,你已经将服务类型定义为ClusterIP 。这使得我们的服务只能在集群内到达。通过spec.ports.targetPort ,我们指明了我们的服务在POD上运行的实际端口。由于我们的yesod应用程序在容器上的3000端口运行,我们适当地指定它。spec.ports.port 表示它将对集群中的其他服务可用的端口。现在,我将在此基础上添加nginx部署,它将把请求路由到我们的yesod应用程序。我不会显示它的部署清单,因为它和我们之前看到的那个很相似,但它的服务对象很有趣。

kind: Service
apiVersion: v1
metadata:
  name: {{ template "yesod-nginx-resource.full_name" . }}
spec:
  selector:
    {{- include "yesod-nginx-resource.release_labels" . | indent 4 }}
  ports:
    - protocol: "TCP"
      port: 80
      targetPort: 80
  type: LoadBalancer

类型LoadBalancer 将使用底层云的负载均衡器来公开服务。一旦你准备好了整个东西,你就可以安装图表了。

helm install . --name="yesod-demo"

我们的pods需要一些时间来启动并开始为请求提供服务。我们指定发布名称为yesod-demo 。你可以通过使用kubectl查询来找到负载均衡器的DNS名称。

$ kubectl get services
NAME                              TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)        AGE
kubernetes                        ClusterIP      172.20.0.1                                                                           443/TCP        12d
nginx-yesod-demo-yesod-postgres-chart   LoadBalancer   172.20.155.194   a0d96c1a5a14a11e88a68065a88681d5-2013621451.us-west-2.elb.amazonaws.com   80:31578/TCP   3d
yesod-demo-postgresql                   ClusterIP      172.20.173.246                                                                       5432/TCP       3d
yesod-demo-yesod-postgres-chart         ClusterIP      172.20.125.142                                                                       3100/TCP       3d

要查看yesod应用程序的日志:

$ kubectl get pods
NAME                                            READY     STATUS    RESTARTS   AGE
nginx-yesod-demo-yesod-postgres-chart-f4f5979-2w6s9   1/1       Running   0          3d
yesod-demo-postgresql-786b4cc4-dwpph                  1/1       Running   0          3d
yesod-demo-yesod-postgres-chart-7bcb474796-xdbfd      1/1       Running   0          11h
$ kubectl logs -f yesod-demo-yesod-postgres-chart-7bcb474796-xdbfd
10.0.0.28 - - [19/Aug/2018:17:13:11 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:21 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:31 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:38 +0000] "GET /health/readiness HTTP/1.1" 200 33 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:41 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:48 +0000] "GET /health/readiness HTTP/1.1" 200 33 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:51 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:13:58 +0000] "GET /health/readiness HTTP/1.1" 200 33 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:14:01 +0000] "GET /health/liveness HTTP/1.1" 200 0 "" "kube-probe/1.10"
10.0.0.28 - - [19/Aug/2018:17:14:08 +0000] "GET /health/readiness HTTP/1.1" 200 33 "" "kube-probe/1.10"

你可以在我们的pod的日志中看到,网络应用正在不断地被诊断性检查所探测。让我们确认一下,我们的网络应用确实在工作。

$ curl --header "Content-Type: application/json" --request POST --data '{"name":"Sibi","age":"32"}' https://a0d96c1a5a14a11e88a68065a88681d5-2013621451.us-west-2.elb.amazonaws.com
$ curl --header "Content-Type: application/json" --request POST --data '{"name":"Sibi","age":32}' https://a0d96c1a5a14a11e88a68065a88681d5-2013621451.us-west-2.elb.amazonaws.com/person
{"age":32,"name":"Sibi"}

现在,如果你在索引页上做一个curl请求,那么你会发现网络应用中的url不正确。Yesod,默认情况下,根据settings.yml 文件中的配置,将主机名放在url中。你将会在你的链接的url中看到pod的主机名。由于这个原因,它在你的浏览器中会呈现不正常。

yesod render issue

有三种方法来解决这个问题:

  • 在Yesod类型的实例中覆盖Approot。这是最直接的方法,我只是将approot定义为ApprootRelative ,这样就没有根URL了。你可以在这里看到其他的选项。
  • 另一个选择是在kubernetes部署后找出负载均衡器的DNS名称,并根据你的settings.yml 文件为你的yesod应用程序传递新的环境变量。但这将意味着再次使用helm upgrade 进行部署。我不喜欢这种方法,因为它涉及到使用Helm的多次部署。目前Helm中还有一个与此相关的开放性问题
  • 第三种方法是作为初始化步骤的一部分,从你的Web应用中调用Kubernetes API,动态获取DNS名称并相应地更新你的应用设置。你必须使用服务API来实现它。

对于这个演示应用程序,让我们使用第一种方法,并部署一个带有标签4.0 的新图像。

$ docker push psibi/yesod-demo:4.0-yesod-demo

现在,让我们把values.yaml 文件更新为图像的最新标签,然后用helm升级部署。

$ helm upgrade yesod-demo .

现在,你确实可以验证pod已经更新到新的镜像上了

$ kubectl describe pod yesod-demo-yesod-postgres-chart-7bcb474796-xdbfd | grep Image
    Image:          psibi/yesod-demo:4.0-yesod-demo
    Image ID:       docker-pullable://psibi/yesod-demo@sha256:5d22e4dfd4ba4050f56f02f0b98eb7ae92d78ee4b5f2e6a0ec4fd9e6f9a068c7

试着访问主页,你可以观察到你的yesod应用程序的所有静态资源这次都已正确加载。

yesod proper render

正如你所看到的,你的应用程序现在可以正常加载了。至此,我们关于在Kubernetes上部署Yesod应用程序的文章结束了。如果你有任何问题/疑问,请通过评论区告诉我。