如何使用 CircleCI 自动部署到 DigitalOcean Kubernetes

作者选择科技教育基金作为为国家写作计划的一部分接受捐赠。

简介

自动部署流程是可扩展和弹性应用程序的必备条件,而_GitOps_,或基于 Git 的 DevOps,已迅速成为一种以 Git 仓库作为 "单一真实源 "来组织 CI/CD 的流行方法。CircleCI](https://circleci.com/)等工具与 GitHub 仓库集成,让您每次对仓库进行修改时都能自动测试和部署代码。当这种 CI/CD 与 Kubernetes 基础架构的灵活性相结合时,您就可以构建一个能根据不断变化的需求轻松扩展的应用程序。

在本文中,你将使用 CircleCI 将一个示例应用程序部署到 DigitalOcean Kubernetes (DOKS) 集群。读完本教程后,您将能够应用这些相同的技术来部署其他可构建为 Docker 镜像的 CI/CD 工具。

先决条件

要学习本教程,您需要具备以下条件

https://andsky.com/tech/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)。

在本教程中,您将使用 Kubernetes 版本 1.22.7kubectl 版本 1.23.5

步骤 1 - 创建你的 DigitalOcean Kubernetes 集群

<$>[注] 注意: 如果你已经有一个正在运行的 DigitalOcean Kubernetes 集群,可以跳过本节。 <$>

在第一步,你将创建一个 DigitalOcean Kubernetes(DOKS)集群,并在其中部署你的示例应用程序。从本地机器执行的 "kubectl "命令将直接从 Kubernetes 集群更改或检索信息。

访问你的 DigitalOcean 账户上的 Kubernetes 页面

点击创建 Kubernetes 集群 ,或点击页面右上方的绿色** 创建** 按钮,然后从下拉菜单中选择** Kubernetes** 。

在 DigitalOcean 上创建 Kubernetes 集群](assets/68186/4.gif)

下一页是指定集群细节的地方。在选择Kubernetes版本 中,选择** 1.22.7-do.0** 版本。如果没有这个版本,请选择最新的推荐版本。

选择数据中心区域 中,选择离您最近的区域。本教程将使用** 旧金山** 。

然后,你就可以选择构建节点池 。在Kubernetes上,节点是一台工作机,其中包含运行pod所需的服务。在DigitalOcean上,每个节点就是一个Droplet。你的节点池将由一个** 基本节点** 组成。选择** 1GB/1vCPU** 配置,并在节点数上更改为** 1 Node** 。

你可以添加额外的标签;如果你计划使用DigitalOcean API,或者只是为了更好地组织你的节点池,这可能会很有用。

选择名称 上,本教程使用 "kubernetes-deployment-tutorial"。这将使您在阅读接下来的章节时更容易理解。最后,点击绿色的** 创建群集** 按钮创建群集。等待群集创建完成。

创建群集后,会有连接群集的说明。请按照自动(推荐) 选项卡上的说明操作,或在** 手动** 选项卡下下载 kubeconfig 文件。该文件将用于验证要对集群运行的 "kubectl "命令。将其下载到你的 kubectl 机器上。

使用该文件的默认方式是在使用 kubectl 运行的所有命令中始终传递 --kubeconfig 标记和该文件的路径。例如,如果你把配置文件下载到了 Desktop 中,就会像这样运行 kubectl get pods 命令:

1kubectl --kubeconfig ~/Desktop/kubernetes-deployment-tutorial-kubeconfig.yaml get pods

输出结果如下

1[secondary_label Output]
2No resources found.

这意味着您访问了群集。未找到资源 "消息是正确的,因为您的集群中没有任何 pod。

如果不维护任何其他 Kubernetes 集群,可以将 kubeconfig 文件复制到主目录中名为.kube的文件夹。如果该目录不存在,请创建它:

1mkdir -p ~/.kube

然后将配置文件复制到新建的 .kube 目录中,并重命名为 config

1cp current_kubernetes-deployment-tutorial-kubeconfig.yaml_file_path ~/.kube/config

现在配置文件的路径应该是 ~/.kube/config。运行任何命令时,kubectl默认读取该文件,因此无需再传递--kubeconfig。运行以下命令

1kubectl get pods

您将收到以下输出结果:

1[secondary_label Output]
2No resources found in default namespace.

现在用以下命令访问群集:

1kubectl get nodes

您将收到群集上的节点列表。输出结果与此类似:

1[secondary_label Output]
2NAME STATUS ROLES AGE VERSION
3pool-upkissrv3-uzm8z Ready    <none>   12m v1.22.7

在本教程中,你将使用 "默认 "命名空间来执行所有 "kubectl "命令和_manifest 文件,这些文件定义了 Kubernetes 中的工作负载和运行参数。命名空间就像是单个物理集群中的虚拟集群。你可以改用任何其他命名空间;只需确保始终使用"--命名空间 "标志传递给 "kubectl",和/或在 Kubernetes 清单元数据字段中指定它。命名空间是组织团队部署及其运行环境的好方法;有关命名空间的更多信息,请参阅Kubernetes 官方命名空间概述

完成这一步后,你就可以在集群中运行 kubectl了。下一步,您将创建用于存放示例应用程序的本地 Git 仓库。

第 2 步 - 创建本地 Git 仓库

您现在要在本地 Git 仓库中构建部署示例。您还将创建一些 Kubernetes 清单,这些清单将用于您在集群上进行的所有部署。

<$>[注] 注: 本教程已在 Ubuntu 20.04 上进行了测试,各个命令的样式也与该操作系统相匹配。不过,这里的大多数命令都可以应用于其他 Linux 发行版,几乎无需改动,而且像 kubectl 这样的命令与平台无关。 <$>

首先,在本地新建一个 Git 仓库,稍后推送到 GitHub。在主目录下创建一个名为 do-sample-app 的空文件夹,并将 cd 放入其中:

1mkdir ~/do-sample-app
2cd ~/do-sample-app

现在用以下命令在该文件夹下创建一个新的 Git 仓库:

1git init .

在该版本库中,创建一个名为 kube 的空文件夹:

1mkdir ~/do-sample-app/kube/

这将是您存储与将部署到集群的示例应用程序相关的 Kubernetes 资源清单的位置。

现在,再创建一个名为 "kube-general "的文件夹,但这次要在刚刚创建的 Git 仓库之外。把它放在你的主目录下:

1mkdir ~/kube-general/

该文件夹位于 Git 仓库之外,因为它将用于存储不是群集上某一部署特有的清单,而是多个部署通用的清单。这样,您就可以在不同的部署中重复使用这些通用清单。

创建好文件夹和示例应用程序的 Git 仓库后,就该安排 DOKS 集群的身份验证和授权了。

第 3 步 - 创建服务账户

一般不建议使用默认的admin 用户从其他Services验证进入 Kubernetes 集群。如果外部提供商的密钥泄露,整个集群都会受到影响。

相反,您将使用具有特定角色的单个服务账户,这都是RBAC Kubernetes 授权模型的一部分。

该授权模型基于_角色_和_资源_。首先创建一个 Service Account(基本上是群集中的一个用户),然后创建一个 Role(角色),并在其中指定它可以访问群集中的哪些资源。最后,创建一个 Role Binding(角色绑定),用于在角色和之前创建的服务账户之间建立连接,授予服务账户访问该角色可访问的所有资源的权限。

您要创建的第一个 Kubernetes 资源是 CI/CD 用户的服务账户,本教程将命名为 "cicd"。

~/kube-general 文件夹中创建文件 cicd-service-account.yml,并用你喜欢的文本编辑器打开它:

1nano ~/kube-general/cicd-service-account.yml

在上面写下以下内容:

1[label ~/kube-general/cicd-service-account.yml]
2apiVersion: v1
3kind: ServiceAccount
4metadata:
5  name: cicd
6  namespace: default

这是一个 YAML 文件;所有 Kubernetes 资源都用一个 YAML 文件来表示。在这种情况下,你是说这个资源来自 Kubernetes API 版本 v1(在内部,kubectl 通过调用 Kubernetes HTTP API 来创建资源),而且它是一个 ServiceAccount

metadata "字段用于添加有关该资源的更多信息。在本例中,您给这个 ServiceAccount 起名为 cicd,并在 default 命名空间上创建它。

现在,您可以像下面这样运行 kubectl apply 在群集上创建该服务帐户:

1kubectl apply -f ~/kube-general/

您将收到类似下面的输出结果:

1[secondary_label Output]
2serviceaccount/cicd created

为确保您的服务账户正常工作,请尝试使用该账户登录群集。为此,你首先需要获取它们各自的访问令牌,并将其存储在环境变量中。每个服务账户都有一个访问令牌,Kubernetes 将其存储为 Secret

您可以使用以下命令获取该秘密:

1TOKEN=$(kubectl get secret $(kubectl get secret | grep cicd-token | awk '{print $1}') -o jsonpath='{.data.token}' | base64 --decode)

解释一下该命令的作用:

1$(kubectl get secret | grep cicd-token | awk '{print $1}')

用于检索与我们的 cicd 服务帐户相关的秘密名称。kubectl get secret 返回默认命名空间的秘密列表,然后使用 grep 搜索与cicd 服务帐户相关的行。然后返回名称,因为它是 grep 返回的单行中的第一个内容。

1kubectl get secret preceding-command -o jsonpath='{.data.token}' | base64 --decode

这将只获取服务帐户令牌的密文。然后使用 jsonpath 访问令牌字段,并将结果传递给 base64 --decode 。这是必要的,因为令牌是以 Base64 字符串形式存储的。令牌本身是一个 JSON Web 令牌

现在,您可以尝试使用 cicd 服务账户检索 pod。运行以下命令,将 server-from-kubeconfig-file 替换为在 ~/.kube/config(您下载的群集配置文件)中 server: 后找到的服务器 URL。该命令会给出一个特定的错误,你将在本教程稍后部分了解到:

1kubectl --insecure-skip-tls-verify --kubeconfig="/dev/null" --server=server-from-kubeconfig-file --token=$TOKEN get pods

--insecure-skip-tls-verify 跳过验证服务器证书的步骤,因为你只是在测试,不需要验证。--kubeconfig="/dev/null" 是为了确保 kubectl 不会读取你的配置文件和证书,而是使用提供的令牌。

输出结果应与此类似:

1[secondary_label Output]
2Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:cicd" cannot list resource "pods" in API group "" in the namespace "default"

这是一个错误,但它表明令牌起作用了。您收到的错误是由于您的服务账户不具备列出 "secrets "资源的必要授权,但您能够访问服务器本身。如果您的令牌不起作用,则会出现以下错误:

1[secondary_label Output]
2error: You must be logged in to the server (Unauthorized)

既然身份验证已经成功,下一步就是修复服务账户的授权错误。为此,您需要创建一个具有必要权限的角色,并将其绑定到服务账户。

第 4 步 - 创建角色和角色绑定

Kubernetes 有两种定义角色的方法:使用 "Role "或 "ClusterRole "资源。前者和后者的区别在于,前者适用于单个命名空间,而后者则对整个集群有效。

由于本教程使用的是单一命名空间,因此将使用 "角色"。

创建文件 ~/kube-general/cicd-role.yml,并用你喜欢的文本编辑器打开它:

1nano ~/kube-general/cicd-role.yml

基本概念是授予访问权限,以便在 "默认 "命名空间中执行与大多数 Kubernetes 资源相关的所有操作。您的 "角色 "将如下所示:

 1[label ~/kube-general/cicd-role.yml]
 2kind: Role
 3apiVersion: rbac.authorization.k8s.io/v1
 4metadata:
 5  name: cicd
 6  namespace: default
 7rules:
 8  - apiGroups: ["", "apps", "batch", "extensions"]
 9    resources: ["deployments", "services", "replicasets", "pods", "jobs", "cronjobs"]
10    verbs: ["*"]

这个 YAML 与你之前创建的 YAML 有一些相似之处,但这里你说的资源是一个 "角色",它来自 Kubernetes API rbac.authorization.k8s.io/v1。你将角色命名为 "cicd",并在创建 "服务账户"(即 "默认 "账户)的同一命名空间中创建它。

然后是 "规则 "字段,它是该角色可访问的资源列表。在 Kubernetes 中,资源的定义基于其所属的 API 组、资源类型本身,以及你能对其进行的操作(由动词表示)。这些动词与 HTTP 中的动词类似

在这种情况下,您是说允许您的 Role 在以下资源上执行一切 * 操作:deploymentsservicesreplicasetspodsjobscronjobs。这也适用于属于以下 API 组的资源:""(空字符串)、appsbatchextensions。空字符串表示根 API 组。如果在创建资源时使用 apiVersion: v1` 表示该资源属于该 API 组。

角色 "本身不起任何作用;您还必须创建一个 `角色绑定',将 "角色 "绑定到某个东西上,在本例中就是 "服务帐户"。

创建文件 ~/kube-general/cicd-role-binding.yml 并打开:

1nano ~/kube-general/cicd-role-binding.yml

在文件中添加以下几行:

 1[label ~/kube-general/cicd-role-binding.yml]
 2kind: RoleBinding
 3apiVersion: rbac.authorization.k8s.io/v1
 4metadata:
 5  name: cicd
 6  namespace: default
 7subjects:
 8  - kind: ServiceAccount
 9    name: cicd
10    namespace: default
11roleRef:
12  kind: Role
13  name: cicd
14  apiGroup: rbac.authorization.k8s.io

您的 RoleBinding 有一些本教程尚未涉及的特定字段。roleRef 是您要绑定的 Role 角色;在本例中,它是您先前创建的 cicd 角色。subjects "是要将角色绑定到的资源列表;在本例中是一个名为 "cicd "的单一 "服务帐户"。

<$>[注] 注意: 如果您使用的是 ClusterRole,则必须创建 ClusterRoleBinding,而不是 RoleBinding。文件几乎是一样的。唯一的区别是,它的 metadata 中没有 namespace 字段。 <$>

创建了这些文件后,你就可以再次使用 kubectl apply 了。运行以下命令,在 Kubernetes 集群上创建这些新资源:

1kubectl apply -f ~/kube-general/

您将收到类似下面的输出结果:

1[secondary_label Output]
2rolebinding.rbac.authorization.k8s.io/cicd created
3role.rbac.authorization.k8s.io/cicd created
4serviceaccount/cicd unchanged

现在,试试之前运行的命令:

1kubectl --insecure-skip-tls-verify --kubeconfig="/dev/null" --server=server-from-kubeconfig-file --token=$TOKEN get pods

由于您没有 pod,因此输出结果如下:

1[secondary_label Output]
2No resources found in default namespace.

在此步骤中,您为将在 CircleCI 上使用的服务账户提供了必要的授权,使其可以在集群上执行有意义的操作,如列出、创建和更新资源。现在是创建示例应用程序的时候了。

第 5 步 - 创建示例应用程序

<$>[注] 注意: 从现在起创建的所有命令和文件都将从您先前创建的文件夹 ~/do-sample-app 开始。这是因为您现在要创建的文件是专门针对要部署到集群中的示例应用程序的。 <$>

您要创建的 Kubernetes 部署将使用 Nginx 镜像作为基础,而您的应用程序将是一个简单的静态 HTML 页面。这是一个很好的开始,因为它可以让你通过直接从 Nginx 服务 HTML 来测试你的部署是否有效。稍后您将看到,您可以将本地 "地址:端口 "的所有流量重定向到集群上的部署,以测试其是否正常工作。

在之前建立的版本库中,创建一个新的 Dockerfile 文件,并用你选择的文本编辑器打开它:

1nano ~/do-sample-app/Dockerfile

在上面写下以下内容:

1[label ~/do-sample-app/Dockerfile]
2FROM nginx:1.21
3
4COPY index.html /usr/share/nginx/html/index.html

这将告诉 Docker 从 nginx 映像构建应用程序容器。

现在创建一个新的 index.html 文件并打开:

1nano ~/do-sample-app/index.html

编写以下 HTML 内容:

1[label ~/do-sample-app/index.html]
2<!DOCTYPE html>
3<title>DigitalOcean</title>
4<body>
5  Kubernetes Sample Application
6</body>

该 HTML 将显示一条简单的信息,让您了解应用程序是否正常运行。

您可以通过构建并运行图像来测试图像是否正确。

首先,使用以下命令构建镜像,将 dockerhub-username 替换为你自己的 Docker Hub 用户名。你必须在此指定用户名,这样以后推送到 Docker Hub 时才能正常工作:

1docker build ~/do-sample-app/ -t dockerhub-username/do-kubernetes-sample-app

现在运行映像。使用以下命令启动映像,并将8080端口上的任何本地流量转发到映像中的80端口,即 Nginx 默认监听的端口:

1docker run --rm -it -p 8080:80 dockerhub-username/do-kubernetes-sample-app

命令运行时,命令提示符将停止交互。取而代之的是 Nginx 访问日志。在任何浏览器上打开 "localhost:8080",都会显示内容为"~/do-sample-app/index.html "的 HTML 页面。如果没有浏览器,可以打开一个新的终端窗口,使用下面的 curl 命令从网页中获取 HTML 内容:

1curl localhost:8080

您将收到以下输出结果:

1[secondary_label Output]
2<!DOCTYPE html>
3<title>DigitalOcean</title>
4<body>
5  Kubernetes Sample Application
6</body>

停止容器(在运行容器的终端上使用 CTRL + C),然后将此镜像提交到你的 Docker Hub 账户。为此,请先登录 Docker Hub:

1docker login

填写所需的 Docker Hub 账户信息,然后使用以下命令推送镜像(别忘了将 dockerhub-username 替换为您自己的用户名):

1docker push dockerhub-username/do-kubernetes-sample-app

现在,您已将示例应用程序镜像推送到 Docker Hub 账户。下一步,您将根据该镜像在 DOKS 集群上创建一个部署。

第 6 步 - 创建 Kubernetes 部署和服务

创建好 Docker 映像并投入使用后,您就可以创建一份清单,告诉 Kubernetes 如何在集群上创建 Deployment

创建 YAML 部署文件 ~/do-sample-app/kube/do-sample-deployment.yml 并用文本编辑器打开:

1nano ~/do-sample-app/kube/do-sample-deployment.yml

在文件中写入以下内容,确保将 dockerhub-username 替换为 Docker Hub 用户名:

 1[label ~/do-sample-app/kube/do-sample-deployment.yml]
 2apiVersion: apps/v1
 3kind: Deployment
 4metadata:
 5  name: do-kubernetes-sample-app
 6  namespace: default
 7  labels:
 8    app: do-kubernetes-sample-app
 9spec:
10  replicas: 1
11  selector:
12    matchLabels:
13      app: do-kubernetes-sample-app
14  template:
15    metadata:
16      labels:
17        app: do-kubernetes-sample-app
18    spec:
19      containers:
20        - name: do-kubernetes-sample-app
21          image: dockerhub-username/do-kubernetes-sample-app:latest
22          ports:
23            - containerPort: 80
24              name: http

Kubernetes 部署来自 API 组 "apps",因此清单的 "apiVersion "设置为 "apps/v1"。在 metadata 中,你添加了一个以前未使用过的新字段,名为 metadata.labels。这对组织部署非常有用。字段 spec 表示部署的行为规范。一个部署负责管理一个或多个 pod;在这种情况下,spec.replicas字段表示它只有一个副本。也就是说,它将创建并管理一个 pod。

要管理 pod,部署必须知道它负责哪些 pod。spec.selector "字段就是提供该信息的字段。在这种情况下,部署将负责所有标记为 app=do-kubernetes-sample-app 的 pod。spec.template "字段包含该部署将创建的 "Pod "的详细信息。模板中还有一个 spec.template.metadata 字段。该字段中的 labels 必须与 spec.selector 中使用的 labels 匹配。spec.template.spec 是 pod 本身的规范。在本例中,它包含一个容器,名为 do-kubernetes-sample-app。该容器的镜像是你之前构建并推送到 Docker Hub 的镜像。

该 YAML 文件还告诉 Kubernetes,该容器暴露了端口 80,并将该端口命名为 http

要访问 "部署 "暴露的端口,请创建一个服务。创建一个名为 ~/do-sample-app/kube/do-sample-service.yml 的文件,并用你喜欢的编辑器打开它:

1nano ~/do-sample-app/kube/do-sample-service.yml

接下来,在文件中添加以下几行:

 1[label ~/do-sample-app/kube/do-sample-service.yml]
 2apiVersion: v1
 3kind: Service
 4metadata:
 5  name: do-kubernetes-sample-app
 6  namespace: default
 7  labels:
 8    app: do-kubernetes-sample-app
 9spec:
10  type: ClusterIP
11  ports:
12    - port: 80
13      targetPort: http
14      name: http
15  selector:
16    app: do-kubernetes-sample-app

该文件将为您的 "服务 "提供与部署时相同的标签。这不是必需的,但有助于在 Kubernetes 上组织应用程序。

服务资源还有一个 spec 字段。spec.type "字段负责服务的行为。在本例中,它是一个 ClusterIP,这意味着该服务暴露在一个群集内部 IP 上,只能从群集内部到达。这是服务的默认 spec.type 类型。spec.selector是标签选择器标准,用于选择该服务要公开的 pod。由于您的 pod 有标签 app: do-kubernetes-sample-app,因此您在此处使用了它。spec.ports "是您希望通过此服务公开的 pod 容器所公开的端口。您的 pod 有一个暴露端口为 80的容器,名为http,因此您在此处将其用作 targetPort。该服务也在端口 80`上以相同的名称公开该端口,但您可以使用与容器不同的端口/名称组合。

创建了 "服务 "和 "部署 "清单文件后,现在就可以使用 "kubectl "在 Kubernetes 集群上创建这些资源了:

1kubectl apply -f ~/do-sample-app/kube/

您将收到以下输出结果:

1[secondary_label Output]
2deployment.apps/do-kubernetes-sample-app created
3service/do-kubernetes-sample-app created

将您机器上的一个端口转发到 Kubernetes 集群内该服务公开的端口,以测试这是否有效。您可以使用 kubectl port-forward进行测试:

1kubectl port-forward $(kubectl get pod --selector="app=do-kubernetes-sample-app" --output jsonpath='{.items[0].metadata.name}') 8080:80

子壳命令 $(kubectl get pod --selector="app=do-kubernetes-sample-app" --output jsonpath='{.items[0].metadata.name}')会检索与您使用的 app 标记相匹配的 pod 的名称。否则,您可以使用 kubectl get pods 从 pod 列表中检索。

运行 port-forward 后,shell 将停止交互,转而输出重定向到群集的请求:

1[secondary_label Output]
2Forwarding from 127.0.0.1:8080 -> 80
3Forwarding from [::1]:8080 -> 80

在任何浏览器上打开 "localhost:8080",都会呈现与本地运行容器时相同的页面,但现在它来自 Kubernetes 集群。和以前一样,你也可以在新的终端窗口中使用 curl 来检查它是否正常工作:

1curl localhost:8080

您将收到以下输出结果:

1[secondary_label Output]
2<!DOCTYPE html>
3<title>DigitalOcean</title>
4<body>
5  Kubernetes Sample Application
6</body>

接下来,就该把创建的所有文件推送到 GitHub 仓库了。为此,你必须先在 GitHub 上创建一个名为 "digital-ocean-kubernetes-deploy "的仓库

为了使该版本库在演示时保持简洁,当 GitHub UI 要求使用 READMElicense.gitignore 文件时,请不要使用它们来初始化新版本库。您可以稍后添加这些文件。

创建好仓库后,将本地仓库指向 GitHub 上的仓库。为此,按 CTRL + C 键停止 kubectl port-forward 并返回命令行,然后运行以下命令添加名为 origin 的新远程:

1cd ~/do-sample-app/
2git remote add origin https://github.com/your-github-account-username/digital-ocean-kubernetes-deploy.git

前面的命令应该没有输出。

接下来,将之前创建的所有文件提交到 GitHub 仓库。首先,添加文件:

1git add --all

接下来,将文件提交到版本库,并在提交信息中加上引号:

1git commit -m "initial commit"

输出结果如下

1[secondary_label Output]
2[master (root-commit) db321ad] initial commit
3 4 files changed, 47 insertions(+)
4 create mode 100644 Dockerfile
5 create mode 100644 index.html
6 create mode 100644 kube/do-sample-deployment.yml
7 create mode 100644 kube/do-sample-service.yml

最后,将文件推送到 GitHub:

1git push -u origin master

系统将提示您输入用户名和密码。输入后,您将看到如下输出:

1[secondary_label Output]
2Counting objects: 7, done.
3Delta compression using up to 8 threads.
4Compressing objects: 100% (7/7), done.
5Writing objects: 100% (7/7), 907 bytes | 0 bytes/s, done.
6Total 7 (delta 0), reused 0 (delta 0)
7To github.com:your-github-account-username/digital-ocean-kubernetes-deploy.git
8 * [new branch]      master -> master
9Branch master set up to track remote branch master from origin.

进入 GitHub 仓库页面,就能看到所有文件。项目在 GitHub 上发布后,现在就可以将 CircleCI 设置为 CI/CD 工具了。

第 7 步 - 配置 CircleCI

在本教程中,您将使用 CircleCI 在代码更新时自动部署应用程序,因此您需要使用 GitHub 账户登录 CircleCI 并设置您的仓库。

首先,访问他们的主页 https://circleci.com,然后按注册

circleci-主页

您正在使用 GitHub,请点击绿色的注册 GitHub 按钮。

CircleCI 将重定向到 GitHub 上的授权页面。CircleCI 需要您账户的一些权限才能开始构建您的项目。这样,CircleCI 就能获得你的电子邮件、部署密钥和在你的软件源上创建钩子的权限,并为你的账户添加 SSH 密钥。如果您需要更多关于 CircleCI 如何处理您的数据的信息,请查阅他们的关于 GitHub 集成的文档

circleci-github-authorization

授权 CircleCI 后,您将被重定向到项目 页面。在这里,你可以在CircleCI中设置你的GitHub仓库。在 "digital-ocean-kubernetes-deploy "版本库的条目中选择** 设置项目** 。然后选择** 更快:将启动 CI 管道提交到新分支** 选项。这将为你的项目创建一个新的** circleci-project-setup** 分支。

接下来,在 CircleCI 设置中指定一些环境变量。选择页面右上方的项目设置 按钮,然后选择** 环境变量** ,即可找到项目设置。按** 添加环境变量** 创建新的环境变量。

首先,添加两个名为 DOCKERHUB_USERNAMEDOCKERHUB_PASS 的环境变量,稍后将需要它们把镜像推送到 Docker Hub。将其分别设置为你的 Docker Hub 用户名和密码。

然后再添加三个:KUBERNETES_TOKEN"、"KUBERNETES_SERVER "和 "KUBERNETES_CLUSTER_CERTIFICATE"。

KUBERNETES_TOKEN "的值将是你之前用来使用服务账户用户在 Kubernetes 集群上进行身份验证的本地环境变量的值。如果关闭了终端,可以运行以下命令再次获取:

1kubectl get secret $(kubectl get secret | grep cicd-token | awk '{print $1}') -o jsonpath='{.data.token}' | base64 --decode

KUBERNETES_SERVER "将是您使用 "cicd "服务账户登录时作为"--server "标志传给 "kubectl "的字符串。你可以在"~/.kube/config "文件中的 "server: "后面找到这个字符串,或者在你初始设置 Kubernetes 集群时从 DigitalOcean 控制面板下载的 "kubernetes-deployment-tutorial-kubeconfig.yaml "文件中找到它。

KUBERNETES_CLUSTER_CERTIFICATE "也应出现在你的"~/.kube/config "文件中。它是与集群相关的 clusters 项目中的 certificate-authority-data 字段。它应该是一个长字符串,请务必全部复制。

必须在此定义这些环境变量,因为它们大多包含敏感信息,直接放在 CircleCI YAML 配置文件中并不安全。

在 CircleCI 监听版本库的更改并配置好环境变量后,就该创建配置文件了。

在示例应用程序存储库中创建一个名为 .circleci的目录:

1mkdir ~/do-sample-app/.circleci/

在该目录下创建一个名为 config.yml 的文件,并用你喜欢的编辑器打开它:

1nano ~/do-sample-app/.circleci/config.yml

在文件中添加以下内容,确保将 dockerhub-username 替换为 Docker Hub 用户名:

 1[label ~/do-sample-app/.circleci/config.yml]
 2version: 2.1
 3jobs:
 4  build:
 5    docker:
 6      - image: circleci/buildpack-deps:bullseye
 7    environment:
 8      IMAGE_NAME: dockerhub-username/do-kubernetes-sample-app
 9    working_directory: ~/app
10    steps:
11      - checkout
12      - setup_remote_docker
13      - run:
14          name: Build Docker image
15          command: |
16            docker build -t $IMAGE_NAME:latest .
17      - run:
18          name: Push Docker Image
19          command: |
20            echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
21            docker push $IMAGE_NAME:latest
22workflows:
23  version: 2
24  build-deploy-master:
25    jobs:
26      - build:
27          filters:
28            branches:
29              only: master

这样就建立了工作流程 build-deploy-master,它现在只有一个名为 build 的作业。该工作将在每次提交到 master 分支时运行。

build "任务使用 "circleci/buildpack-deps:bullseye "镜像运行其步骤,该镜像来自 CircleCI,基于官方的 "buildpack-deps "Docker 镜像,但安装了一些额外的工具,如 Docker 二进制文件本身。

工作流程有四个步骤:

  • checkout 从 GitHub 获取代码。
  • setup_remote_docker 为每次构建设置一个远程隔离环境。在工作步骤中使用任何 docker 命令之前,都需要这样做。这是必要的,因为当步骤在 docker 镜像中运行时,setup_remote_docker 会分配另一台机器在那里运行命令。
  • 第一个 "运行 "步骤是构建镜像,就像之前在本地环境中所做的那样。为此,你需要使用在 environment: 中声明的环境变量 IMAGE_NAME
  • 最后一个 run 步骤使用你在项目设置中配置的环境变量进行身份验证,将镜像推送到 Docker Hub。

<$>[信息] 除了 Docker Hub,你还可以使用其他注册表来存储容器镜像并与他人协作。例如,DigitalOcean 有自己的容器注册中心,你可以向其推送容器镜像。

要将镜像推送到DigitalOcean容器注册中心,可以用以下步骤替换第二个 "运行 "步骤:

1[label ~/do-sample-app/.circleci/config.yml]
2. . .
3      - run: |
4          docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD registry.digitalocean.com/your_registry
5          docker push registry.digitalocean.com/your_registry/do-kubernetes-sample-app
6. . .

这假定你已在文件中将"$DOCKER_USERNAME "和"$DOCKER_PASSWORD "安全设置为环境变量,因为这将避免在CircleCI作业输出中暴露它们。此外,请注意这两个值应设置为相同的 DigitalOcean API 令牌。

要了解有关 DigitalOcean 容器注册表的更多信息,请查阅我们的产品文档。 <$>

将新文件提交到你的版本库,并将更改推送到上游:

1cd ~/do-sample-app/
2git add .circleci/
3git commit -m "add CircleCI config"
4git push

这将触发 CircleCI 上的新构建。CircleCI 工作流程将正确构建并将你的镜像推送到 Docker Hub。

包含成功构建信息的 CircleCI 构建页面](assets/68186/3.png)

创建并测试了 CircleCI 工作流程后,您就可以设置 DOKS 群集,以便从 Docker Hub 获取最新映像,并在进行更改时自动部署。

第 8 步 - 更新 Kubernetes 集群上的部署

现在,您每次向 GitHub 上的 "master "分支推送变更时,应用程序镜像都会被构建并发送到 Docker Hub,是时候更新 Kubernetes 集群上的部署了,这样它就能检索到新镜像并将其作为部署的基础。

要做到这一点,首先要解决部署中的一个问题:它目前依赖于带有 latest 标记的图片。这个标签并没有告诉我们你使用的是哪个版本的镜像。每次向 Docker Hub 推送新镜像时,该标签都会被覆盖,因此无法轻松地将部署锁定在该标签上。

您可以在Vladislav Supalov 关于为什么依赖于 latest 标记是一种反模式的文章中阅读更多相关信息。

要纠正这一点,首先必须对~/do-sample-app/.circleci/config.yml文件中的 "推送 Docker Image "构建步骤进行一些更改。打开该文件:

1nano ~/do-sample-app/.circleci/config.yml

然后在 "推送 Docker 镜像 "步骤中添加高亮显示的行:

 1[label ~/do-sample-app/.circleci/config.yml]
 2...
 3      - run:
 4          name: Push Docker Image
 5          command: |
 6            echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
 7            docker tag $IMAGE_NAME:latest $IMAGE_NAME:$CIRCLE_SHA1
 8            docker push $IMAGE_NAME:latest
 9            docker push $IMAGE_NAME:$CIRCLE_SHA1
10...

CircleCI 默认设置了一些特殊的环境变量。其中一个是 CIRCLE_SHA1,它包含正在构建的提交的哈希值。你对`~/do-sample-app/.circleci/config.yml`所做的更改将使用该环境变量来标记你的图片,并标明它是根据哪个提交构建的,而且总是用最新的标签标记最新的构建。这样,当你向版本库推送新内容时,就能始终拥有可用的特定图片,而不会覆盖它们。

保存并退出文件。

接下来,更改部署清单文件,使其指向该文件。如果在~/do-sample-app/kube/do-sample-deployment.yml中将映像设置为dockerhub-username/do-kubernetes-sample-app:$COMMIT_SHA1,这只是一个小改动,但在使用kubectl apply时,kubectl不会在清单中进行变量替换。为了解决这个问题,可以使用 envsubstenvsubst 是一个 CLI 工具,是 GNU gettext 项目的一部分。它允许你向其传递一些文本,如果发现文本中的任何变量与环境变量相匹配,它就会用相应的值替换该变量。结果文本将作为输出返回。

为此,您需要创建一个负责部署的 bash 脚本。在 ~/do-sample-app/ 中新建一个名为 scripts 的文件夹:

1mkdir ~/do-sample-app/scripts/

在该文件夹中新建一个名为 ci-deploy.sh的 bash 脚本,并用你喜欢的文本编辑器打开它:

1nano ~/do-sample-app/scripts/ci-deploy.sh

在其中编写以下 bash 脚本:

 1[label ~/do-sample-app/scripts/ci-deploy.sh]
 2#! /bin/bash
 3# exit script when any command ran here returns with non-zero exit code
 4set -e
 5
 6COMMIT_SHA1=$CIRCLE_SHA1
 7
 8# Export it so it's available for envsubst
 9export COMMIT_SHA1=$COMMIT_SHA1
10
11#  Since the only way for envsubst to work on files is using input/output redirection,
12#  it's not possible to do in-place substitution, so you will save the output to another file
13#  and overwrite the original with that one.
14envsubst <./kube/do-sample-deployment.yml >./kube/do-sample-deployment.yml.out
15mv ./kube/do-sample-deployment.yml.out ./kube/do-sample-deployment.yml
16
17echo "$KUBERNETES_CLUSTER_CERTIFICATE" | base64 --decode > cert.crt
18
19./kubectl \
20  --kubeconfig=/dev/null \
21  --server=$KUBERNETES_SERVER \
22  --certificate-authority=cert.crt \
23  --token=$KUBERNETES_TOKEN \
24  apply -f ./kube/

让我们利用文件中的注释来查看一下这个脚本。首先是以下内容:

1set -e

这一行确保任何失败的命令都会停止 bash 脚本的执行。这样,如果一条命令失败,下一条命令就不会被执行。

1COMMIT_SHA1=$CIRCLE_SHA1
2export COMMIT_SHA1=$COMMIT_SHA1

这几行以一个新名称导出 CircleCI $CIRCLE_SHA1 环境变量。如果只是声明了变量,而没有使用 export 导出,那么 envsubst 命令就看不到它。

1envsubst <./kube/do-sample-deployment.yml >./kube/do-sample-deployment.yml.out
2mv ./kube/do-sample-deployment.yml.out ./kube/do-sample-deployment.yml

envsubst 不能进行就地替换。也就是说,它不能读取文件内容,用各自的值替换变量,然后将输出写回同一文件。因此,你需要将输出重定向到另一个文件,然后用新文件覆盖原文件。

1echo "$KUBERNETES_CLUSTER_CERTIFICATE" | base64 --decode > cert.crt

您之前在 CircleCI 项目设置中创建的环境变量 $KUBERNETES_CLUSTER_CERTIFICATE 实际上是一个 Base64 编码字符串。要在 kubectl 中使用它,必须解码其内容并将其保存到文件中。在本例中,您要将其保存到当前工作目录下名为 cert.crt 的文件中。

1./kubectl \
2  --kubeconfig=/dev/null \
3  --server=$KUBERNETES_SERVER \
4  --certificate-authority=cert.crt \
5  --token=$KUBERNETES_TOKEN \
6  apply -f ./kube/

最后,您正在运行 kubectl。该命令的参数与测试服务账户时运行的命令类似。你正在调用 apply -f ./kube/,因为在 CircleCI 上,当前工作目录就是项目的根目录。这里的./kube/就是你的~/do-sample-app/kube文件夹。

保存文件并确保其可执行:

1chmod +x ~/do-sample-app/scripts/ci-deploy.sh

现在,编辑 ~/do-sample-app/kube/do-sample-deployment.yml

1nano ~/do-sample-app/kube/do-sample-deployment.yml

将容器图像值的标记改为如下所示:

1[label ~/do-sample-app/kube/do-sample-deployment.yml]
2...
3      containers:
4        - name: do-kubernetes-sample-app
5          image: dockerhub-username/do-kubernetes-sample-app:$COMMIT_SHA1
6          ports:
7            - containerPort: 80
8              name: http

保存并关闭文件。现在,您必须在 CI 配置文件中添加一些新步骤,以更新 Kubernetes 上的部署。

用你喜欢的文本编辑器打开~/do-sample-app/.circleci/config.yml

1nano ~/do-sample-app/.circleci/config.yml

在之前创建的 "build "任务的右下方编写以下新任务:

 1[label ~/do-sample-app/.circleci/config.yml]
 2...
 3          command: |
 4            echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
 5            docker tag $IMAGE_NAME:latest $IMAGE_NAME:$CIRCLE_SHA1
 6            docker push $IMAGE_NAME:latest
 7            docker push $IMAGE_NAME:$CIRCLE_SHA1
 8  deploy:
 9    docker:
10      - image: circleci/buildpack-deps:bullseye
11    working_directory: ~/app
12    steps:
13      - checkout
14      - run:
15          name: Install envsubst
16          command: |
17            sudo apt-get update && sudo apt-get -y install gettext-base
18      - run:
19          name: Install kubectl
20          command: |
21            curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
22            chmod u+x ./kubectl
23      - run:
24          name: Deploy Code
25          command: ./scripts/ci-deploy.sh
26...

新的 deploy 作业的前两个步骤是安装一些依赖项,首先是 envsubst 然后是 kubectl。部署代码 "步骤负责运行我们的部署脚本。

现在,你要将此任务添加到之前创建的 build-deploy-master 工作流中。在 "build-deploy-master "工作流配置中,在 "build "任务的现有条目后写入以下新条目:

 1[label ~/do-sample-app/.circleci/config.yml]
 2...
 3workflows:
 4  version: 2
 5  build-deploy-master:
 6    jobs:
 7      - build:
 8          filters:
 9            branches:
10              only: master
11      - deploy:
12          requires:
13            - build
14          filters:
15            branches:
16              only: master

这将在 build-deploy-master 工作流中添加 deploy 作业。deploy "作业只会在 "build "作业完成后,针对提交到 "master "的内容运行。

现在,~/do-sample-app/.circleci/config.yml中的内容将如下所示:

 1[label ~/do-sample-app/.circleci/config.yml]
 2version: 2.1
 3jobs:
 4  build:
 5    docker:
 6      - image: circleci/buildpack-deps:bullseye
 7    environment:
 8      IMAGE_NAME: dockerhub-username/do-kubernetes-sample-app
 9    working_directory: ~/app
10    steps:
11      - checkout
12      - setup_remote_docker
13      - run:
14          name: Build Docker image
15          command: |
16            docker build -t $IMAGE_NAME:latest .
17      - run:
18          name: Push Docker Image
19          command: |
20            echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
21            docker tag $IMAGE_NAME:latest $IMAGE_NAME:$CIRCLE_SHA1
22            docker push $IMAGE_NAME:latest
23            docker push $IMAGE_NAME:$CIRCLE_SHA1
24  deploy:
25    docker:
26      - image: circleci/buildpack-deps:bullseye
27    working_directory: ~/app
28    steps:
29      - checkout
30      - run:
31          name: Install envsubst
32          command: |
33            sudo apt-get update && sudo apt-get -y install gettext-base
34      - run:
35          name: Install kubectl
36          command: |
37            curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
38            chmod u+x ./kubectl
39      - run:
40          name: Deploy Code
41          command: ./scripts/ci-deploy.sh
42workflows:
43  version: 2
44  build-deploy-master:
45    jobs:
46      - build:
47          filters:
48            branches:
49              only: master
50      - deploy:
51          requires:
52            - build
53          filters:
54            branches:
55              only: master

现在您可以保存并退出文件。

要确保 Kubernetes 部署中确实反映了这些更改,请编辑 index.html。将 HTML 改为其他内容,如

1[label ~/do-sample-app/index.html]
2<!DOCTYPE html>
3<title>DigitalOcean</title>
4<body>
5  Automatic Deployment is Working!
6</body>

保存上述更改后,将所有修改后的文件提交到版本库,并将更改推送到上游:

1cd ~/do-sample-app/
2git add --all
3git commit -m "add deploy script and add new steps to circleci config"
4git push

您将看到新的构建在 CircleCI 上运行,并成功将更改部署到 Kubernetes 集群。

等待构建完成,然后运行之前运行的命令:

1kubectl port-forward $(kubectl get pod --selector="app=do-kubernetes-sample-app" --output jsonpath='{.items[0].metadata.name}') 8080:80

在 URL localhost:8080 上打开浏览器或向其发出 curl 请求,确保一切正常。它应该会显示更新后的 HTML:

1curl localhost:8080

您将收到以下输出结果:

1[secondary_label Output]
2<!DOCTYPE html>
3<title>DigitalOcean</title>
4<body>
5  Automatic Deployment is Working!
6</body>

这意味着您已成功使用 CircleCI 设置了自动部署。

结论

这是关于如何使用 CircleCI 向 DigitalOcean Kubernetes 进行部署的基础教程。从这里开始,你可以通过多种方式改进你的管道。首先,你可以为多个部署创建一个 "构建 "作业,每个作业部署到不同的 Kubernetes 集群或不同的命名空间。当你有不同的 Git 分支用于开发/暂存/生产环境时,这将非常有用,可以确保部署始终是分开的。

你也可以构建自己的映像,以用于 CircleCI,而不是使用 buildpack-deps。这个镜像可以基于它,但可能已经安装了 kubectlenvsubst 依赖项。

如果您想了解有关 Kubernetes 上 CI/CD 的更多信息,请查看我们的Kubernetes 上的 CI/CD 网络系列讲座 的教程,或有关 Kubernetes 上应用程序的更多信息,请参阅Modernizing Applications for Kubernetes

Published At
Categories with 技术
comments powered by Disqus