k8s 折腾记:使用 ArgoCD & Helm 把应用搬上集群并实现 GitOps

目标

如题

最后大概是一个这样的流程

1
2
graph 
	User -->|git push| Github -->|build| x[Github Packages] -->|pull| K8s

整体分为 CI 和 CD 两个部分:

CI:

  • 推送代码到仓库
  • 触发 workflow,进行测试,构建等步骤,最后把构建出的镜像推送到镜像仓库
  • 更新 manifest 仓库中的镜像版本号

CD:

  • ArgoCD 监控 manifest 仓库,维护线上环境与仓库一致
  • 检测到 manifest 仓库中镜像版本号更新,更新线上环境

接下来就记录一下搭建的流程。

将应用配置方式改为环境变量

由于我的应用最终是要传到公开的镜像仓库上的,所以配置信息就不方便打包在应用里面了,要在运行时再注入进来,这里使用的是环境变量的方式。

1
2
3
4
5
6
7
8
9
# application.yaml
spring:
  profiles:
    active: prod
  datasource:
    url: ${DATASOURCE_URL}
    username: ${DATASOURCE_USERNAME}
    password: ${DATASOURCE_PASSWORD}
    driver-class-name: ${DATASOURCE_DRIVER_CLASS_NAME}

把应用打成镜像

上集群的第一步,肯定是把应用以镜像的形式交付嘛,毕竟 pod 是 k8s 的最小调度单位。

我的应用使用 java17 写的,网上找了一圈,有一个打成分层镜像的方式,应该是最佳实践了吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FROM maven:3.9-eclipse-temurin-17-alpine AS build

WORKDIR /app

COPY pom.xml ./pom.xml

COPY src ./src

RUN --mount=type=cache,target=/root/.m2 \
    mvn package  -am -DskipTests

RUN mkdir -p /layers && \
    cp /app/target/SIMS-0.0.1-SNAPSHOT.jar /layers/target.jar && \
    cd /layers && \
    java -Djarmode=layertools -jar /layers/target.jar extract

FROM eclipse-temurin:17-jre AS runtime

WORKDIR /app

COPY --from=build /layers/dependencies/ .
COPY --from=build /layers/snapshot-dependencies/ .
COPY --from=build /layers/spring-boot-loader/ .
COPY --from=build /layers/application/ .

EXPOSE 8080

# 执行命令
ENTRYPOINT [ "java", "org.springframework.boot.loader.launch.JarLauncher" ]

写 manifest

创建 Chart

这里我打算一步到位,使用 helm。

可以先看看 Helm 的 术语表,了解一下基础概念。

先使用内置模板创建 Chart

1
helm create my-app

就可以得到像下面这样的目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
my-app
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

编写 template

这里选择从头开始,不使用给的模板。

把 templates 目录下的文件都删除,然后把 values.yaml 文件清空

deployment

首先,最重要的肯定是 deployment。

使用下面的命令就可以生成一个 deploy 的模板

1
k create deploy backend --image ghcr.io/suyiiyii/sims:main -o yaml --dry-run

可以得到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: backend
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: backend
    spec:
      containers:
      - image: ghcr.io/suyiiyii/sims:main
        name: sims
        resources: {}
status: {}

按照自己的需要继续自定义即可。

service

接着是 service,把应用的服务暴露出来。

1
k create svc clusterip backend --tcp=80:8080 -o yaml --dry-run

这里把 service 的名字设置为和 deploy 相同,可以自动绑定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: backend
  name: backend
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: backend
  type: ClusterIP
status:
  loadBalancer: {}

ingress

最后是 ingress,为 service 添加路由。

1
k create ingress backend --rule="backend.kl.suyiiyii.top/=backend:80" -o yaml --dry-run

这里要指定访问的路径和绑定的 service。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  creationTimestamp: null
  name: backend
spec:
  rules:
  - host: backend.kl.suyiiyii.top
    http:
      paths:
      - backend:
          service:
            name: backend
            port:
              number: 80
        path: /
        pathType: Exact
status:
  loadBalancer: {}

PS: 这个时候其实就可以部署试试看了。

配置环境变量

由于前面我们把配置信息改成由环境变量注入了,所以我们还要在 manifest 中添加环境变量。

这里的关系大概是这样子的:

1
2
graph LR
	Values.yaml -->|Helm 渲染 | Secret -->|Deployment 引用 | Env

为了方便后面的注入,我把 application-prod.yaml 直接复制到 values.yaml 中(前者是 springboot 的配置文件,后者是 helm 的参数文件)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# values.yaml
spring:
  datasource:
    url: ""
    username: ""
    password: ""
    driverClassName: ""

jwt:
  secret: ""

值全部填空字符串。

接着,再回到 templates 文件夹中,创建一个 secret.yaml 文件,写成下面这样子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
data:
  spring.datasource.url: {{ .Values.spring.datasource.url | b64enc | quote }}
  spring.datasource.username: {{ .Values.spring.datasource.username | b64enc | quote }}
  spring.datasource.password: {{ .Values.spring.datasource.password | b64enc | quote }}
  spring.datasource.driver-class-name: {{ .Values.spring.datasource.driverClassName | b64enc | quote }}
  jwt.secret: {{ .Values.jwt.secret | b64enc | quote }}
  
kind: Secret
metadata:
  creationTimestamp: null
  name: secret

这里用到了 helm 的模板语法,{{ .Values.jwt.secret | b64enc | quote }}的意思就是,从 values 中获取 jwt.secret 这个键值对的值,然后用 base64 编码,并用引号包裹。

PS:这里好像有个坑,helm 的模板语法用不了 - 这个字符,所以改了字段名。

最后,在 deploy 中添加环境变量即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
    spec:
      containers:
      - image: ghcr.io/suyiiyii/sims:main
        name: sims
        resources: {}
        env:
        - name: DATASOURCE_URL
          valueFrom:
            secretKeyRef:
              name: secret
              key: spring.datasource.url
        - name: DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: secret
              key: spring.datasource.username
        - name: DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: secret
              key: spring.datasource.password
        - name: DATASOURCE_DRIVER_CLASS_NAME
          valueFrom:
            secretKeyRef:
              name: secret
              key: spring.datasource.driver-class-name
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: secret
              key: jwt.secret
status: {}

创建 ArgoCD 应用

参考 官方文档 安装。

虽然我是在 ui 上面操作的,但是为了方便复现,所以这里还是给出 manifest。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sims
  namespace: argocd
  uid: 23be0eed-ed44-444c-8565-3fc53bdc5ef0
spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  revisionHistoryLimit: 3
  source:
    helm:
      values: |-
        spring:
          datasource:
            url: jdbc:mysql://example.com:9999/superms
            username: root
            password: password
            driverClassName: com.mysql.cj.jdbc.Driver
        jwt:
          secret: password        
    path: .
    repoURL: https://git.suyiiyii.top/ssyg/SIMS.git
    targetRevision: manifest
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true

重点在 spec

  • destination:就是应用最终会部署到哪里,我是当前集群,所以就保持默认。
  • source:定义了读取仓库的方式,因为我是用的 helm chart,所以选 helm。这里,在 values,字段里面提供一个 yaml,就可以让 argo 在部署的时候,自动替换掉仓库里面的 values 文件的字段,达到传递配置信息的作用。并且,配置信息被保存在集群上,仓库中的只是一个占位符,这样就保证了安全性。
  • path:定义了仓库中配置文件的位置
  • repoURL & targetRevision:仓库的地址和分支

如果 git 仓库需要认证的话,可以创建一个 secret,访问仓库就会自动使用 secret 里面的认证信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
data:
  password: passss
  project: ZGVmYXVsdA==
  type: Z2l0
  url: repoooo
kind: Secret
metadata:
  annotations:
    managed-by: argocd.argoproj.io
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque

PS:其实官方更加推荐使用 cli 去操作。

这个时候,再去访问 ui,应该就可以看到我们部署的应用了。

image.png

配置镜像自动更新

先把镜像的版本配置为可以被覆盖的

1
2
3
4
5
6
# values.yaml
image:
  repository: ghcr.io/suyiiyii/sims
  pullPolicy: IfNotPresent
  tag: "240825-8d10add"
  # 这里的值可以随便填,后面会覆盖的
1
2
3
4
5
6
7
8
9
# deployment.yaml
...
    spec:
      containers:
      - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        name: sims
        resources: {}
        env:
...

因为 CI 和 CD 是分离的,两边通过镜像仓库进行对接。CI 构建完镜像就推送到仓库,CD 检测到有新的镜像就尝试去部署。

但是,我们是 GitOps,线上环境应该要和 git 仓库保持一致的,所以修改镜像版本最好也要保存到仓库里面,这样整个流程更加完整。

Argo 是有一个 自动更新镜像版本的工具 的,但是这个工具需要回写仓库比较麻烦,所以还是使用了 CI 结束后用 Action 更新镜像版本的方式。

emmm,也没啥好说的,直接给 workflow 吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout manifest
        if: github.event_name == 'push'
        uses: actions/checkout@v4
        with:
          ref: manifest
      - name: Update manifest
        if: github.event_name == 'push'
        uses: mikefarah/yq@master
        with:
          cmd: yq eval '.image.tag = "${{ steps.meta.outputs.version }}"' -i values.yaml
      - name: Git Auto Commit
        uses: stefanzweifel/git-auto-commit-action@v5.0.1
        with:
          commit_message: "Update deployment image to ${{ steps.meta.outputs.version }}"
          branch: manifest
          commit_user_name: "github-actions[bot]"
          commit_user_email: "github-actions[bot]@users.noreply.github.com"
          commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"

这里我省略了很多无关的部分,主要逻辑就是,构建完镜像之后,再拿镜像标签去更新 values.yaml 里面写的版本号。

感觉,好像到这里就差不多了。

最后,我实操的仓库是 这个,由于网络原因,所以搞了个镜像的仓库,argo 绑定的是镜像的仓库。理论上 manifest 仓库应该是独立的,并且设置更严格的认证。但是因为本来就是开源项目,也没有那么强的审批要求,所以就直接做成主仓库的一个分支了。

总结

搞下了一套还是挺爽的。其实之前就搞过一次,不过没有记录。差不多看看,可以提出一个通用的模板出来,做成一个通用的框架吧,以后还有应用要上 k8s 就直接套用了。