目标
如题
最后大概是一个这样的流程
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
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 中添加环境变量。
这里的关系大概是这样子的:
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,应该就可以看到我们部署的应用了。
配置镜像自动更新
先把镜像的版本配置为可以被覆盖的
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 就直接套用了。