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类型的实例中覆盖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应用程序的所有静态资源这次都已正确加载。

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