用AWS EC2从零搭建Jenkins并实现GitOps CI/CD

用AWS EC2从零搭建Jenkins并实现GitOps CI/CD

上一篇:用AWS EC2从零搭建Kubernetes和ArgoCD

前一篇里,我们在 AWS EC2 上从无到有,搭建了 Kubernetes 集群和 ArgoCD。因为终究缺少 CI 环境以实现真正的实时持续集成,这一篇将手把手,在 AWS EC2 上搭建 Jenkins 实现一个 GitOps CI/CD 工作流。

预备 #

准备 EC2 实例 #

使用 Amazon Linux 2 AMI。确保

  1. 安全组(Security Group)开放 8080 端口。
  2. EC2 的 IAM 角色有对 ECR 的读和写权限。一般是使用 AmazonEC2ContainerRegistryFullAccess 策略。

SSH 登录 EC2 #

1ssh -i <your-key.pem> ec2-user@<EC2-Public-IP>

更新包管理器 dnf #

1sudo dnf update -y

安装 wget #

1sudo dnf install wget -y

安装 git #

1sudo dnf install git -y

安装 docker #

1sudo dnf install docker -y
2sudo systemctl start docker
3sudo systemctl enable docker
4sudo usermod -aG docker $USER

为了避免每次使用 docker 命令都需要 sudo,将当前用户添加到 docker 用户组。

参考:在AWS Linux EC2上准备Docker

安装 Java #

1sudo dnf install java-17-amazon-corretto -y
2sudo dnf install fontconfig -y

因为是在 AWS EC2 上安装,所以使用的是 Amazon Corretto JDK。

fontconfig 是 Java 运行时所需的字体配置。

安装 Jenkins #

添加 Jenkins 仓库 #

1sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
2sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key

安装 #

1sudo dnf install jenkins -y 

启动服务 #

1sudo systemctl start jenkins
2sudo systemctl enable jenkins

查看服务状态 #

1sudo systemctl status jenkins

应当看到类似如下输出,服务应当处于 active (running) 状态。

● jenkins.service - Jenkins Continuous Integration Server
     Loaded: loaded (/usr/lib/systemd/system/jenkins.service; disabled; preset: disabled)
     Active: active (running) since Thu 2025-01-16 09:13:46 UTC; 7s ago
   Main PID: 976120 (java)
      Tasks: 53 (limit: 4565)
     Memory: 591.4M
        CPU: 24.742s
     CGroup: /system.slice/jenkins.service
             └─976120 /usr/bin/java -Djava.awt.headless=true -jar /usr/share/java/jenkins.war --webroot=/var/cache/jenkins/war --httpPort=8080

添加 Jenkins 用户到 Docker 用户组 #

1sudo usermod -aG docker jenkins
2sudo systemctl restart jenkins
3sudo systemctl restart docker

对于使用 Docker 进行镜像构建的 Jenkins 任务,这一步很重要。

配置 Jenkins #

获取初始密码 #

1sudo cat /var/lib/jenkins/secrets/initialAdminPassword

访问并初始化 #

  1. 在浏览器中输入 http://<EC2-Public-IP>:8080,然后输入初始密码,即可进入 Jenkins UI。
  2. 选择安装推荐插件。
  3. 创建管理员账户。
  4. 配置 Jenkins URL。(默认即可)

完成后,进入 Jenkins 主界面。

Jenkins Setup

预备代码仓库 #

应用仓库 #

假设我们已经有一个可以被 Docker 构建的极简 hello-world HTTP 服务应用,代码仓库地址为以下。

https://github.com/username/hello-world.git

这个仓库只管理应用代码,不包含部署配置。

部署配置仓库 #

我们还需要一个仓库用来管理用于 Kubernetes/ArgoCD 消费的部署配置文件,这个仓库地址为

https://github.com/username/hello-world-deployment.git

仓库的文件结构为

./staging/deployment.yml
./production/deployment.yml

这个仓库只管理部署配置,不包含应用代码。

两个 manifest 文件分别用于 stagingproduction 环境。

部署配置文件 #

staging 环境的 deployment.yml 文件为例。一般来说,这个文件包含以下几个部分,即以下几个 Kind

Section Description
Deployment 管理应用生命周期的资源,它定义了应用的副本数、容器镜像、更新策略等。
Service 定义如何在集群内或外部访问 Pods。它充当了 Pods 的访问入口,能够提供负载均衡和端口映射。
Namespace 用于资源逻辑隔离

以下是一个基础的 deployment.yml 文件。

 1# Create a namespace
 2apiVersion: v1
 3kind: Namespace
 4metadata:
 5  name: YOUR_NAMESPACE
 6---
 7## define the deployment
 8apiVersion: apps/v1
 9kind: Deployment
10metadata:
11  name: YOUR_DEPLOYMENT_NAME
12  namespace: YOUR_NAMESPACE
13  labels:
14    app: YOUR_LABEL
15spec:
16  replicas: 2
17  selector:
18    matchLabels:
19      app: YOUR_LABEL
20  template:
21    metadata:
22      labels:
23        app: YOUR_LABEL
24    spec:
25      imagePullSecrets:
26        - name: ecr-pull-secret
27      affinity:
28        nodeAffinity:
29          requiredDuringSchedulingIgnoredDuringExecution:
30            nodeSelectorTerms:
31              - matchExpressions:
32                  - key: kubernetes.io/arch
33                    operator: In
34                    values:
35                      - arm64
36                      - amd64
37      containers:
38        - name: YOUR_CONTAINER_NAME
39          image: YOUR_AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/hello-world:latest
40          ports:
41            - name: http
42              containerPort: 8080
43          imagePullPolicy: Always
44      nodeSelector:
45        kubernetes.io/os: linux
46---
47## define the service
48apiVersion: v1
49kind: Service
50metadata:
51  name: YOUR_SERVICE_NAME
52  namespace: YOUR_NAMESPACE
53  labels:
54    app: YOUR_LABEL
55spec:
56  selector:
57    app: YOUR_LABEL
58  ports:
59    - port: 80
60      targetPort: 8080
61      nodePort: 30000
62  type: NodePort

重点字段说明:

请根据实际情况替换 YOUR_ 开头的字段。

replica: 副本数,根据实际情况调整。

imagePullPolicy: 镜像拉取策略,Always 表示每次都拉取最新镜像,IfNotPresent 表示只有本地没有时才拉取。

nodePort: 用于外部访问的端口,即可以用 http://<EC2-Public-IP>:30000 访问服务。

关于 imagePullSecrets #

配置中看到到 imagePullSecrets 字段,是用于从让 kubernetes 使用 dockerECR 拉取镜像时的校验,需要提前创建,这非常重要。

先让 Docker 登录到 ECR。

1aws ecr get-login-password --region <your_aws_region> | docker login --username AWS --password-stdin <your_aws_account>.dkr.ecr.ap-northeast-1.amazonaws.com

然后根据已有的 ~/.docker/config.json 文件为 kubernetes 创建 secret

1kubectl create secret generic regcred \
2    --from-file=.dockerconfigjson=<path/to/.docker/config.json> \
3    --type=kubernetes.io/dockerconfigjson \
4    --namespace <namespace_of_your_application>

其中 regcred 是一个自定义的名称,可以根据实际情况替换,但必须保证和 deployment.yml 中的一致。

--namespace 必须和 deployment.yml 中的一致。

参考:官方文档

检查创建好的 secret。

1kubectl get secret regcred -n <namespace_of_your_application>

删除现有的 secret 的命令:

1kubectl delete secret <secret_name> -n <namespace_of_your_application>

以上命令或需周期性地执行,以保证 kubernetes 能够正常拉取镜像。

配置 Pipeline #

Pipeline的关键是 Jenkinsfile,它定义了整个 CI/CD 流程。通常对于 staging(预览环境)和 production(生产环境)有不同的流程。

环境 触发条件 镜像版本 部署方式
staging push 到目标分支 分支的 HEAD commit hash 由CI自动触发 ArgoCD 的同步命令
production tagrelease tag 人工手动在 ArgoCD 上触发同步

以下是针对 staging 环境的 Jenkinsfile

 1pipeline {
 2    agent any
 3    // 定义环境变量以便在整个流程中引用
 4    // 这里我们用 `AWS` 的 `ECR`(`Elastic Container Registry`)作为 `Docker` 镜像仓库。
 5    environment {
 6        ECR_REGISTRY = '733089366385.dkr.ecr.ap-northeast-1.amazonaws.com'
 7        ECR_REPOSITORY = 'hello-world'
 8        REGION = 'ap-northeast-1'
 9    }
10    stages {
11        stage('Checkout') {
12            steps {
13                // 从 GitHub 仓库拉取代码
14                // 目标分支是 `main`
15                git branch: 'main', url: 'https://github.com/username/hello-world.git',
16                        // 用 github 的 `Personal Access Token` 作为凭证,最佳实践,比直接用密码或者 SSH 更安全。
17                        credentialsId: 'fr-github'
18            }
19        }
20
21        stage('Build Docker Image') {
22            steps {
23                script {
24                    // 用 `git` 的 `HEAD commit hash` 作为镜像版本
25                    IMAGE_TAG = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
26                    // 贴到环境变量中以便在后续步骤中引用
27                    env.IMAGE_TAG = IMAGE_TAG
28                }
29                // docker 构建镜像
30                sh '''
31                    docker build -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG} --build-arg ENVIRONMENT=staging .
32                '''
33            }
34        }
35
36        stage('Push to AWS ECR') {
37            steps {
38                // 用 AWS CLI 获取 ECR 登录密码
39                // 并推送到 ECR 仓库
40                sh '''
41                    aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
42                    docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}
43                '''
44            }
45        }
46
47        stage('Update Deployment Config') {
48            steps {
49                // 更新另一个 GitHub 仓库中的 `deployment.yml` 文件,这个文件是 `ArgoCD` 读取的用于部署的配置文件。
50                // 将其中的 `image` 字段更新为最新的镜像版本。
51                // 然后提交并推送到 GitHub 仓库。
52                git branch: 'main', url: 'https://github.com/username/hello-world-deployment.git', credentialsId: 'fr-github'
53                withCredentials([usernamePassword(credentialsId: 'fr-github', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
54                    sh '''
55                        sed -i "s|image: .*|image: ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}|g" staging/deployment.yml
56                        git config user.name "Jenkins CI"
57                        git config user.email "jenkins@example.com"
58                        git add staging/deployment.yml
59                        if git diff-index --quiet HEAD; then
60                            echo "No changes to commit."
61                            exit 0
62                        fi
63                        git commit -m "Update staging deployment to ${IMAGE_TAG}"
64                        git push https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/username/hello-world-deployment.git main
65                    '''
66                }
67            }
68        }
69
70        // 最后触发 `ArgoCD` 同步命令。
71        stage('Trigger ArgoCD Sync') {
72            steps {
73                sh '''
74                    argocd login 13.231.229.5:32135 --username admin --password admin1234 --insecure
75                    argocd app sync poc-staging
76                '''
77            }
78        }
79    }
80}

配置好保存后,一个 staging 环墶的 CI/CD 流程就配置完成了。可以尝试手动 build 一次。预期结果是应用即刻用目标分支的最新代码构建并部署。

删除 Jenkins #

有时因操作错误需重新安装 Jenkins,以下命令用于删除 Jenkins 以备不时之需。

1sudo systemctl stop jenkins
2sudo dnf remove jenkins -y
3sudo rm -rf /var/lib/jenkins
4sudo rm -rf /usr/share/jenkins
5sudo rm -rf /etc/sysconfig/jenkins