将任何 Python 项目部署到 Kubernetes
随着项目规模的增长,单个虚拟机或简单的 SaaS 解决方案可能难以应对。您可以切换到更强大的解决方案,例如Kubernetes,来解决这个问题。但是,如果您不熟悉 Kubernetes 的概念或从未用过,这可能会比较复杂。因此,为了帮助您,本文将介绍您需要了解的所有内容,帮助您入门并将Python项目部署到集群上——包括集群设置、所有Kubernetes清单以及一些额外的自动化功能,让您日后的开发工作更加轻松!
这是之前关于“自动化 Python 项目的各个方面”的文章的后续,所以你可能想在阅读本文之前先看看那些文章。
简而言之:这是我的代码仓库,包含完整的源代码和文档:https://github.com/MartinHeinz/python-project-blueprint
舒适的开发环境
为了提高开发效率,你需要一个舒适的本地开发环境。在这种情况下,这意味着在本地部署一个易于使用的Kubernetes 集群,使其与你的真实生产集群高度相似。为此,我们将使用KinD:
KinD(Kubernetes-in-Docker)顾名思义,是在Docker容器中运行Kubernetes集群。它是Kubernetes维护者用于Kubernetes v1.11+ 一致性测试的官方工具。它支持多节点集群以及高可用性 (HA) 集群。由于KinD在Docker中运行Kubernetes,因此它可以在 Windows、Mac 和 Linux 上运行。也就是说,只要安装了 Docker,您就可以在任何地方运行它。
那么,让我们来安装KinD(在 Linux 系统上 - 如果您使用的是 Windows 系统,请点击此处查看安装信息):
~ $ curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-$(uname)-amd64
~ $ chmod +x ./kind
~ $ sudo mv ./kind /usr/local/bin/kind
~ $ kind --version
kind version 0.7.0
至此,我们就可以开始搭建集群了。为此,我们需要以下 YAML 文件:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
authorization-mode: "AlwaysAllow"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
- role: worker
此清单描述了我们的集群。它将包含 3 个节点:控制平面节点(role: control-plane)和 2 个工作节点role: worker。我们还添加了一些设置和参数,以便稍后配置 Ingress 控制器,从而在该集群上实现 HTTPS。关于这些设置,您只需了解它们的作用:extraPortMappings指示集群将主机上的端口转发到运行在某个节点上的 Ingress 控制器。
注意:集群和 Python 应用程序的所有清单文件都可以在我的仓库的此k8s目录中找到。
现在,我们需要运行几个命令来启动它:
$ ~ kind create cluster --config kind-config.yaml --name cluster
$ ~ kubectl cluster-info --context kind-cluster
Kubernetes master is running at https://127.0.0.1:32769
KubeDNS is running at https://127.0.0.1:32769/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
$ ~ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cluster-control-plane Ready master 2m39s v1.17.0
cluster-worker Ready <none> 2m3s v1.17.0
cluster-worker2 Ready <none> 2m3s v1.17.0
要创建集群,我们只需运行第一个命令。之后,我们可以通过运行 `git add`cluster-info和 ` get nodesgit checkout` 命令来检查集群是否正常运行。长时间输入这些命令会很麻烦,所以我们Make稍后会简化操作,但目前为止,集群已经启动并运行了。
接下来,我们需要为集群设置入口。为此,我们需要运行一些kubectl命令,使其与KinD协同工作:
~ $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
~ $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/service-nodeport.yaml
~ $ kubectl patch deployments -n ingress-nginx nginx-ingress-controller -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx-ingress-controller","ports":[{"containerPort":80,"hostPort":80},{"containerPort":443,"hostPort":443}]}],"nodeSelector":{"ingress-ready":"true"},"tolerations":[{"key":"node-role.kubernetes.io/master","operator":"Equal","effect":"NoSchedule"}]}}}}'
首先,我们部署必要的ingress-nginx组件。在此基础上,我们使用NodePort暴露nginx服务,这就是第二个命令的作用。最后一个命令为入口控制器应用了一些KinD特有的补丁。
定义清单
集群准备就绪后,就可以开始设置和部署我们的应用程序了。为此,我们将使用一个非常简单的Flask应用程序——echo server:
# __main__.py
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def echo():
return f"You said: {request.args.get('text', '')}\n"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
我选择使用Flask应用而不是命令行工具(或Python包),因为我们需要一个不会像某些Python包那样立即终止的应用。另外,请注意host参数已设置,如果没有此设置,当我们通过Kubernetes0.0.0.0服务和 Ingress暴露应用时,将无法访问该应用。
接下来我们需要的是该应用程序的 YAML 清单文件,让我们将其拆分成单独的对象:
- 命名空间:
apiVersion: v1
kind: Namespace
metadata:
name: blueprint
没什么特别要说的。我们通常不希望在default命名空间中部署应用程序,所以会使用这个命名空间。
- 配置映射:
apiVersion: v1
kind: ConfigMap
metadata:
name: env-config-blueprint
namespace: blueprint
data: # Example vars that will get picked up by Flask application
FLASK_ENV: development
FLASK_DEBUG: "1"
在这里我们可以定义应用程序的变量。这些变量data将作为环境变量注入到应用程序容器中。例如,我添加了 `${value1}`FLASK_ENV和`${value2}`, Flask会在应用程序启动时自动获取FLASK_DEBUG它们。
- 秘密:
apiVersion: v1
kind: Secret
metadata:
name: env-secrets-blueprint
namespace: blueprint
data:
VAR: VkFMVUU= # base64 of "VALUE"
就像我们指定纯文本变量一样,我们可以使用Secret向应用程序添加凭据和密钥等信息。但是,由于此对象包含敏感数据,因此不应将其推送到存储库。我们可以使用以下命令动态创建它:
~ $ kubectl create secret generic env-secrets-blueprint -n blueprint \
--from-literal=VAR=VALUE \
--from-literal=VAR2=VALUE2 \
--dry-run -o yaml >> app.yaml
注意:部署应用程序所需的此命令和其他命令已列在存储库的 README 文件中,以及此处清单文件的底部。
- 部署:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: blueprint
name: blueprint
namespace: blueprint
spec:
replicas: 1
template:
metadata:
labels:
app: blueprint
spec:
containers:
# - image: docker.pkg.github.com/martinheinz/python-project-blueprint/example:flask
# ^^^^^ https://github.com/containerd/containerd/issues/3291 https://github.com/kubernetes-sigs/kind/issues/870
- image: martinheinz/python-project-blueprint:flask
name: blueprint
imagePullPolicy: Always
ports:
- containerPort: 5000
protocol: TCP
envFrom:
- configMapRef:
name: env-config-blueprint
- secretRef:
name: env-secrets-blueprint
# imagePullSecrets: # In case you are using private registry (see kubectl command at the bottom on how to create regcred)
# - name: regcred
selector:
matchLabels:
app: blueprint
现在到了最重要的部分——部署。这里需要重点关注的是spec指定镜像、端口和环境变量的部分。image我们这里指定镜像来自Docker Hub。如果要使用像Artifactory这样的私有镜像仓库,则需要添加imagePullSecret集群凭据以拉取镜像。可以使用以下命令创建此密钥:
kubectl create secret docker-registry regcred \
--docker-server=docker.pkg.github.com \
--docker-username=<USERNAME> --docker-password=<GITHUB_REG_TOKEN> \
--dry-run -o yaml >> app.yaml
这展示了如何允许从GitHub Package Registry拉取您的图像,但遗憾的是,由于上述 YAML 中列出的问题,它目前无法与KinD一起使用,但它可以很好地与您的云中的生产集群一起使用(假设它不使用KinD)。
如果您不想每次重新部署应用程序时都将镜像推送到远程注册表,则可以使用以下命令将镜像加载kind load docker-image martinheinz/python-project-blueprint:flask到集群中。
之后image,我们还要指定端口ports。这些是我们的应用程序正在监听的端口,在本例中5000,因为我们的应用程序开始使用app.run(host='0.0.0.0', port=5000)。
最后一部分envFrom用于通过在相应的字段中指定名称,将上面显示的ConfigMap和SecretRef中的明文变量和密钥注入到该部分中。
- 服务:
apiVersion: v1
kind: Service
metadata:
name: blueprint
namespace: blueprint
labels:
app: blueprint
spec:
selector:
app: blueprint
ports:
- name: http
targetPort: 5000 # port the container accepts traffic on
port: 443 # port other pods use to access the Service
protocol: TCP
现在,我们已经有一个应用程序在监听端口port,我们需要一个服务来将其暴露出来。这个对象定义了这样一个事实:监听端口的应用程序5000应该暴露在集群节点的端口上443。
- Ingress:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: blueprint-ingress
namespace: blueprint
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
labels:
app: blueprint
spec:
tls:
- hosts:
- localhost
secretName: tls-secret
rules:
- host: localhost
http:
paths:
- path: "/"
backend:
serviceName: blueprint
servicePort: 443
最后一块重要的拼图——Ingress——一个管理集群中服务外部访问的对象。我们先来看这rules部分——在本例中,我们定义主机为localhost。我们还设置path了/,这意味着发送到的任何请求localhost/都属于之前显示的Servicebackend的名称及其关联的。port
这里还有一部分。这部分通过指定包含 ` <secret> ` 和 `<secret>` 的密钥tls,为列出的内容提供 HTTPS 连接。让我们创建这个密钥:hoststls.crttls.key
KEY_FILE="blueprint.key"
CERT_FILE="blueprint.crt"
HOST="localhost"
CERT_NAME=tls-secret
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ${KEY_FILE} -out ${CERT_FILE} -subj "/CN=${HOST}/O=${HOST}"
kubectl create secret tls ${CERT_NAME} --key ${KEY_FILE} --cert ${CERT_FILE} --dry-run -o yaml >> app.yaml
上面的代码片段首先设置了一些变量,然后使用这些变量生成用于 TLS 的证书和密钥文件openssl。最后一个命令创建包含这两个文件的Secret 。
部署应用程序
所有清单文件准备就绪后,我们终于可以部署应用程序了:
$ ~ kubectl config set-context kind-cluster --namespace blueprint
$ ~ KEY_FILE="blueprint.key"
$ ~ CERT_FILE="blueprint.crt"
$ ~ HOST="localhost"
$ ~ CERT_NAME=tls-secret
$ ~ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ${KEY_FILE} -out ${CERT_FILE} -subj "/CN=${HOST}/O=${HOST}"
$ ~ kubectl create secret generic env-secrets-blueprint --from-literal=VAR=VALUE --dry-run -o yaml >> app.yaml && echo "---" >> app.yaml
$ ~ kubectl create secret tls ${CERT_NAME} --key ${KEY_FILE} --cert ${CERT_FILE} --dry-run -o yaml >> app.yaml
$ ~ kubectl apply -f app.yaml
namespace/blueprint unchanged
deployment.apps/blueprint created
configmap/env-config-blueprint unchanged
service/blueprint created
ingress.extensions/blueprint-ingress unchanged
secret/env-secrets-blueprint configured
secret/tls-secret configured
$ ~ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/blueprint-5d86484b76-dkw7z 1/1 Running 0 13s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/blueprint ClusterIP 10.96.253.31 <none> 443/TCP 13s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/blueprint 1/1 1 1 13s
NAME DESIRED CURRENT READY AGE
replicaset.apps/blueprint-5d86484b76 1 1 1 13s
$ ~ curl -k https://localhost/?text=Hello
You said: Hello
上面的大部分命令我们之前已经在章节中见过了。新增的内容是,kubectl apply -f app.yaml它会在集群中创建所有必要的对象。创建完成后,我们可以使用 `constructor` 命令检查这些对象是否存在kubectl get all。最后,我们可以使用 `constructor` 命令检查应用程序是否可访问cURL,结果是可以的!至此,我们的应用程序已经在集群上运行了!
Make化繁为简
kind如果你对所有命令还不完全熟悉kubectl,或者像我一样懒,不想全部输入,那么我Make为你准备了几个目标——让你的生活更轻松:
- 启动集群:
cluster:
@if [ $$(kind get clusters | wc -l) = 0 ]; then \
kind create cluster --config ./k8s/cluster/kind-config.yaml --name kind; \
fi
@kubectl cluster-info --context kind-kind
@kubectl get nodes
@kubectl config set-context kind-kind --namespace $(MODULE)
make cluster如果集群尚未准备就绪,该命令将为您设置集群;如果已准备就绪,它将提供所有相关信息。如果您需要检查节点状态并切换到开发命名空间,这将非常有用。
- 重新部署/重启应用程序:
deploy-local:
@kubectl rollout restart deployment $(MODULE)
这个功能非常简单——它所做的只是推出新的部署——所以如果有新镜像,它就会部署它;否则,它只会重新启动你的应用程序。
- 调试:
cluster-debug:
@echo "\n${BLUE}Current Pods${NC}\n"
@kubectl describe pods
@echo "\n${BLUE}Recent Logs${NC}\n"
@kubectl logs --since=1h -lapp=$(MODULE)
如果您需要调试应用程序,您可能需要查看与应用程序 pod 相关的近期事件以及最近(过去一小时)的日志。这正是它能make cluster-debug为您提供的功能。
- 获取远程 shell:
cluster-rsh:
# if your container has bash available
@kubectl exec -it $$(kubectl get pod -l app=${MODULE} -o jsonpath="{.items[0].metadata.name}") -- /bin/bash
如果日志不足以解决您可能遇到的一些问题,并且您决定需要查看容器内部,那么您可以运行命令make cluster-rsh来获取远程 shell。
- 更新清单:
manifest-update:
@kubectl apply -f ./k8s/app.yaml
我们之前见过这个命令。它只是重新应用 YAML 清单,这在调整Kubernetes对象的某些属性时非常有用。
结论
本文并非Kubernetes教程,但我认为它足以帮助您快速入门并启动运行应用程序。要深入了解Kubernetes,我建议您亲自尝试,在清单文件中进行各种调整和修改,看看会发生什么。在我看来,这是了解其工作原理并熟悉kubectl命令的好方法。如果您有任何问题、建议或遇到任何问题,请随时与我联系或在我的代码仓库中创建 issue 。您还可以在该代码仓库中找到文档以及本文中展示的所有清单文件。