编写高效的CI/CD管道是DevOps(或后端工程师)最重要的工作之一。这篇博客是绝佳的起点。我们在这里研究了适合完全初学者的CI/CD。
前置条件要理解这篇博客,你最好对 AWS EC2 有基本了解。
你可以从这里了解更多关于EC2的信息。
这篇博客的内容有- 什么是CI?
- 什么是CD?
- GitHub Actions入门
- CI工作流编写教程
- CD工作流编写教程
CI代表持续集成。
假设你们是一个五人组成的团队,一起建立一个项目。在这个场景中,你们有一个位于 GitHub 的中央代码仓库,每个人都会将他们的代码推送到那里。在技术英语中,这被称为 CI(持续集成)。
CI就是快速而流畅地将多个开发者的代码更改合并到一个项目中。
在普通的项目中,我们可以执行简单的 git push
。这段代码会和你的 GitHub 仓库合并,这叫 CI。不过,在公司项目或开源项目里,你推代码后,你的代码不会直接合并到主仓库。
参与开源项目的一般流程:
- 将项目fork到你的GitHub仓库。
- 创建一个新的分支(我们称之为branch-5)来解决特定问题。
- 在branch-5中进行开发并提交你的代码改动。
- 从branch-5向该开源项目的master分支(或说明中指定的分支)发起Pull Request。
假设有人写了糟糕的代码,有一些语法错误或无法正确编译,并提出了一个拉取请求。如果这个拉取请求被合并到你的主分支,你的应用程序可能会崩溃。作为项目维护者,解决方案是你可以手动检查每个拉取请求在合并到主分支之前。难道你不认为在每次合并之前检查每个人的代码是手动劳动吗?作为DevOps工程师,我们的工作是自动化任何手动任务。在这里面,我们将编写一系列检查,每当有人向主分支提出拉取请求时,这些检查将会自动运行,检查语法错误、代码格式的正确性等。我们只会合并那些通过所有这些检查的拉取请求到主分支。
持续集成(CI),顾名思义,意味着将一些代码与我们的主分支集成。但我们只会集成那些不会给项目带来任何问题并且确实解决了问题的代码。所以,当有人向主分支发起拉取请求时,自动检查会运行。如果所有检查都通过了,我们就合并这个拉取请求。如果任何检查失败,开发人员会收到通知,我们也不会合并这段代码。当有人向主分支发起拉取请求时,这些自动运行的检查集合称为 CI 工作流。编写这个 CI 工作流是这项工作的内容。
一些关于CI的常规检查在CI中可以检查任何东西,但这里列出一些常见的检查内容:
- 静态代码分析:工具(例如,JavaScript 的 ESLint,SonarQube 或 Python 的 Flake8)检查代码中的错误、风格问题或潜在的安全隐患。
- 代码规范检查:确保代码遵守编码标准和最佳实践,例如正确使用制表符内的空格。
- 构建验证:确保代码能够成功编译或构建(例如 Java、C++ 或 React 应用)。
- 单元测试:检查单个组件或函数是否按预期工作。
- 集成测试:验证应用程序的不同部分是否如预期协同工作。
- 测试覆盖率:测量由自动化测试覆盖的代码比例(例如,80% 以上的测试覆盖率是常见目标)。
如果你不知道上述内容也别担心,我们会在这篇博客里学到这些东西。
CD是什么?经过CI之后,你的最新代码合并到了主分支。下一步呢?
答:进行部署。
部署就是将你的代码从你的本地笔记本电脑或Github仓库传输到EC2实例,让用户使用你的应用程序。
如果此部署是人工完成的,CD 代表 持续交付。
如果此部署是自动进行且无需人工干预,CD 代表 持续部署,一旦你把代码合并到主分支,一个自动的工作流会运行并部署你的应用。
在这篇博客里,我们将制定一个持续集成和部署的工作流程。
CI在某种程度上是标准的,也就是说你大多数时候在所有项目中都会有类似的CI,但部署策略并不相同。有些人选择在AWS上部署代码,有些人则在Azure上部署,等等。即使是在AWS上,有些人直接在EC2实例上部署代码,有些人使用ECS服务,还有些人使用EKS(Kubernetes容器编排)。所以,部署策略并不相同。在这次博客里,我们将在一个EC2实例上进行简单的部署。每当有代码合并到master分支时,最新的代码库将被上传到EC2实例,实例上的旧代码将被移除。这一过程应该是自动化的,不需要人工干预。
GitHub Actions 入门GitHub Actions 是一款强大的 CI/CD 工具,可帮助你自动化任务,例如测试、构建和部署你的应用。
你是如何用你本地的笔记本电脑完成这些事情的?
答) 你可以在终端里敲一堆Linux命令。至于构建一个Node.js应用,你会运行npm run build
。要测试Node.js应用,你会运行npm run test
。要部署应用程序,你会用SSH登录到EC2实例,然后把你的代码放过去。这也要用到一些Linux命令。
简单来说,你需要一种方式在GitHub上每次有提交、拉取请求(pull request)或代码合并时执行一些Linux命令。持续集成/持续交付(CI/CD)工具如Jenkins和GitHub Actions提供了这样的功能。每当你的仓库发生变动时,就会自动执行一系列Linux命令。
要运行 Linux 命令,你需要一个 Linux 操作系统。当你使用 GitHub Actions 时,这些 Linux 命令将在 GitHub 的服务器上运行。你提供一个包含这些 Linux 命令的 YAML 文件,并设置触发条件(如提交、拉取请求、合并等),GitHub 将在触发条件满足时,在其服务器上运行这些 Linux 命令。如果你想在自己的笔记本电脑或 EC2 实例上运行这些命令,就得用 Jenkins。
对于初学者而言:YAML 是一种数据序列化语言,就像 JSON 一样。
如果你不理解 YAML 这个格式,可以访问 这里 并粘贴 YAML 并查看对应的 JSON。
下面是一张CI的图片。
当有人将 PR 合并到我们项目仓库的 master 分支,以下三个命令会自动触发:
- 切换到 master 分支上;然后运行
npm build
;最后运行npm test
。
如果上面的所有命令都成功了,我们才允许合并代码。
运行 Linux 命令需要一个操作系统,或直接说运行在服务器上。
如果我们使用 GitHub Actions,所有的 Linux 命令将在 GitHub 的服务器上执行。
自从我们了解了使用GitHub Actions进行CI/CD的基本概念之后,我们现在将学习如何实现它。
工作流程: 一组用于定义自动化流程的指令(指令可以是 Linux 命令)。我们将编写 CI/CD(持续集成和持续部署)工作流程。
首先,在项目的根目录下创建一个.github
文件夹,接着,在这个.github
文件夹里再创建一个叫做workflows
的文件夹。所有的YAML文件都要在这个文件夹里编写。
以下是一个基本的工作流配置文件。试着读一下。如果你不习惯阅读 YAML 格式的话,可以使用这个网站 这里 进行 YAML 到 JSON 的转换。
name: Node.js 持续集成
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 配置 Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: 安装依赖包
run: npm install
- name: 运行单元测试
run: npm test
我们现在来理解上述工作流程中的每一个术语和概念,并学习如何编写工作流程文件。
关键词a) 在...上面或在...时候等,具体取决于上下文。在
定义触发工作流的那些事件。
- 示例: 当代码被推送到
master
分支时,运行流程。
on:
push:
branches:
- master # 当推送 master 分支时触发
事件可以触发其他事件,例如上面的事件会在推送至主分支时触发。一些常见的触发器包括例如推送至主分支或定时触发,以更好地解释常见的触发器。
- 拉取请求(Pull Request)
- 例如,你可以设置一个触发器,让它每天凌晨3点自动运行。
- 当其他工作流完成或失败时,使用workflow_run触发器。
b) 任务
设定工作流程中要完成的任务。
- 例子:
jobs:
build:
运行于: 最新 Ubuntu
步骤:
- name: 安装依赖项
run: npm install
c) 运行在
指定运行 Linux 命令和作业的机器种类。
通常我们使用 Ubuntu。
- 例如: 使用最新版本的 Ubuntu 操作系统。
runs-on: ubuntu-latest
d) 步骤(如需操作的步骤)
定义工作中的各个具体任务。
- 例如: 安装依赖项。
步骤:
- 名称: 安装依赖项
运行: 执行 `npm install`
每个步骤都有一个名字。你可以给它起任何你喜欢的名字来说明你在做什么。
名字后面可以有两样东西。
- 跑
- 用
在 run
里面,我们写 Linux 命令。如上所述,我们写了 npm run install
。你可以写任何 Linux 命令,比如 cd src && touch file1.txt
这样的。这个 Linux 命令将在你指定的系统里运行,比如 Ubuntu。
吴) 用
运行一个从开源仓库获取的预写脚本。
uses: actions/setup-node@v3 # 使用actions/setup-node@v3
这会把nodejs安装到Ubuntu服务器上。你可能在想为什么我们不直接写 run: apt install nodejs
。当然,你可以这样做,但预写好的操作更简单。它们只需一行代码就能搞定任务。像登录到dockerhub这样的复杂任务可以用一串Linux命令来完成,但通过复制粘贴这些操作,你只需一行命令就能搞定这些任务。对于常见任务,你只需在谷歌上搜一搜,看看是否有适合你任务的现成操作。如果有,就使用那个操作;如果没有,那就直接写Linux命令。
许多操作需要一些参数,比如上面的 Node.js 安装需要指定为 Node 版本。你可以通过 with
子句来传递这些参数。
使用 actions/setup-node@v3 操作,并设置 node-version 为 16。
你可以在操作的文档中找到这些内容。只需在Google上搜索。
Node.js安装操作: https://github.com/actions/setup-node
正如我之前说过的,总是用 Google 搜索一下,看看是否有现成的脚本或动作可以完成你的任务。如果有,就用那个;否则,直接写 Linux 命令。
试着读下面的代码。
name: Node.js 应用的 CI/CD 流程
on:
push:
branches:
- main # 在向 main 分支推送时触发
jobs:
build:
name: 构建并部署 Node.js 应用程序
runs-on: ubuntu-latest
steps:
# 步骤 1:检出代码
- name: 检出代码
uses: actions/checkout@v3
# 步骤 2:安装并设置 Node.js
- name: 安装并设置 Node.js
uses: actions/setup-node@v3
with:
node-version: 16
# 步骤 3:安装依赖
- name: 安装依赖
run: npm install
# 步骤 4:构建应用
- name: 构建应用
run: npm run build
deploy:
name: 构建并部署 Node.js 应用程序
runs-on: ubuntu-latest
needs: build
steps:
# 步骤 1:部署到 EC2
- name: 部署到 EC2
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
port: 22
script: |
# 步骤 1.1:创建并进入应用目录
mkdir -p ~/nodejs-app && cd ~/nodejs-app
# 步骤 1.2:移除旧文件
rm -rf *
# 步骤 1.3:从 GitHub 拉取代码,安装依赖并启动应用
git pull http://githubProjectRepoLink
cd projectRepoDir
npm install
node app.js
我希望你现在能读并理解这个内容。有两个任务 build
和 deploy
,这两个任务分别表示构建和部署的过程。在 deploy
的配置里,可以看到第三行写着 needs: build
。这一行确保了 deploy
任务只有在 build
任务所有步骤都成功完成后才会运行。如果不写 needs: build
,build
和 deploy
任务将会并行执行。我们不希望这种情况发生,所以我们只希望在 build
任务的所有步骤完成后再运行 deploy
任务。
你可能看到了这些文字:secrets.EC2_HOST
。这些都是密钥值。如果你不想直接暴露你的密码或敏感信息,你可以这样写。在项目仓库的设置标签下,你可以将这些密钥值以键值对的形式设置。当你运行工作流时,这些键的值(如EC2_HOST、EC2_KEY等)将从那里读取。如果你之前写过后端应用程序,那么这和在.env
文件中写入敏感信息差不多。
我们编写工作,每个工作包含多个步骤。这些步骤会依次执行。
如果你有多个工作,那么每个工作将会并行执行。如果你想确保某些工作只能在其他工作完成后执行,你可以通过 needs
关键字来实现。
每个任务(job)都在单独的机器上运行。正如你所见,我们需要为每个任务指定机器类型(在我们的例子中,有两个任务:构建和部署这两个任务)。对于使用Ubuntu的场景,我们写 runs-on: ubuntu-latest
。
注: 如果你还没理解,可以再读一遍。再读一遍应该就能懂了。
CI工作流编写教程我建议你在看这个教程之前先自己试着写一下。我已经讲完了相关的所有预备理论。甚至我在前面的部分已经给出了一种更简单的实现方式。
我们将为一个Node.js和TypeScript项目编写CI和CD流程。
项目地址: https://github.com/shivam-bhadani/CI-CD-Tutorial
当客户将PR提升到主分支时,我们会运行如上文所述的命令如下。如果每个命令都能顺利完成的话。
在项目根目录下创建一个名为 .github
的文件夹。在 .github
文件夹内,创建一个名为 workflows
的文件夹。在 workflows
文件夹内,创建一个任意命名的 yaml 文件,并将以下代码复制进去。
name: 项目CI流程
on:
pull_request:
branches:
- master
jobs:
build:
name: 构建NodeJS应用
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout@v3
- name: 配置NodeJS
uses: actions/setup-node@v3
- name: 安装依赖
run: npm install
- name: 编译项目使用Typescript
run: 执行npm run build
我们现在试试做个演示。
- 我们的初始项目包含一个src目录;这个目录里有一个index.ts文件,内容如下:
我们将创建一个名为“product_feature”的分支,并在该分支的 index.ts 文件中添加一段代码,然后提交该分支的代码合并请求(PR)到 master 分支以便检查 CI。
- 创建一个分支。点击“查看分支”。
点击右上角的“新建分支”按钮,然后输入分支的名字,再创建新分支。
3. 切换到(或浏览到)product_feature 分支。
- 把下面这段代码加到/src/index.ts里。
app.get("/product", (req, res) => {
res.send("这是产品页");
})
保存更改
第4步:可以通过点击“比较并发起拉取请求”按钮,将 product_feature 分支合并到主分支。
5. 提高公关
6. 看到神奇之处。自动检查已经开始。
可以看到下面的图片,写着“有些检查还没有完成”。
7. 在1–3秒后,检查将自动完成,如果您的代码没有问题,它将显示绿色的合并标志。您可以看到下方显示的是“所有检查通过了”。
如果你有任何错误或语法错误,那么“npm run build”就会失败,这里也会显示一个叉。
如果你想查看任何错误日志,可以点击“详情”链接。在“成功耗时6秒”右边可以看到“更多详情”链接。
- 你的代码已经成功通过了 CI 工作流检查。维护者会点击“合并请求”按钮,将你的代码合并到主分支。
我已将代码合并到主线上
下面可以看到,master分支的index.ts已经被更新了。
编写 CD 工作流的教程这里我用 Docker。如果你是初学者,还不了解 Docker,那么就自己准备部署,不使用 Docker。直接将你的代码上传到 EC2 并运行 npm run start
。
我们不会直接把代码放到EC2上。相反,我们会制作一个应用的Docker镜像并上传到DockerHub。EC2实例会从DockerHub下载这个镜像并运行它。
请参见下面的图片;当代码被推送到主分支时,我们将按照图中所示的步骤进行。
大多数情况下,前三个步骤保持不变。只有第四个步骤会根据部署策略来调整。这里我们仅在EC2上进行部署。而有些人则是在ECS上部署,只调整最后一步。
Docker化应用程序在当前项目的根目录新建一个叫“Dockerfile”的文件,并将下面的代码粘贴进去。
# 基础镜像
FROM node:18-alpine AS builder
# 工作目录
WORKDIR /app
# 复制 package*.json 文件
COPY package*.json /app
# 安装依赖
RUN npm install
# 复制项目文件
COPY . .
# 构建应用
RUN npm run build
# 基础镜像
FROM node:18-alpine AS runner
# 从构建阶段复制 package*.json 文件
COPY --from=builder /app/package*.json ./
# 从构建阶段复制构建输出文件
COPY --from=builder /app/dist ./dist
# 安装依赖
RUN npm install
# 暴露端口
EXPOSE 8080
# 设置默认命令
CMD ["node", "dist/index.js"]
如果你想确认一切运行正常,就依次运行下面两条命令,这样就会构建 Docker 镜像并启动它。
# 构建 Docker 镜像并标记为 ci-cd-tutorial
docker build -t ci-cd-tutorial .
# 运行容器并将容器的 8080 端口映射到主机的 8080 端口
docker run -p 8080:8080 ci-cd-tutorial
在浏览器中访问 http://localhost:8080
,看看是否一切正常。
我们希望将这个 Docker 镜像上传到 DockerHub,以便未来我们的 EC2 实例可以从那里拉这个镜像。
我们会为我们的应用在DockerHub上建立一个仓库。
登录 DockerHub,点击进入仓库选项卡并新建仓库。
2. 把仓库命名为任何你喜欢的名字。我用的是“ci-cd-tutorial”这个名字,然后点击创建按钮即可。
这个仓库已经建好了。我们会把我们的应用推送到这里。
由于我们要编写 CD 工作流,所以我们不会从本地笔记本电脑将应用程序放入仓库。相反,每当有新的代码合并到主分支,GitHub 的服务器就会推送镜像到 DockerHub 仓库。当新的代码合并到主分支后,这个 CD 工作流会构建新的 Docker 镜像,并将这个最新的镜像推送到 DockerHub 仓库。然后这个最新的镜像会被拉取到 EC2 实例并运行,以反映最新的更新。
因为 GitHub 正在将镜像推送到我们的 DockerHub,我们必须让 GitHub 访问我们的 DockerHub。为此,从 DockerHub 生成 access_token。
3. 前往账户设置页面
4、点一下个人访问令牌按钮
5. 点击生成访问密钥按钮
6. 输入任意描述并选择“读写删”权限,然后点击“生成”按钮即可。
7. 你可以看到已经生成了一个密码,这个密码是通过CD工作流登录DockerHub时必需的。
有两种方式:直接将此密码写入CD工作流的yaml配置文件中,或者不直接写入,而是通过GitHub的密钥来访问它。
如果你直接将这个密码写入你的 yaml 文件中,那么所有人都能看到它,他们就可以搞砸你的程序。这就是为什么永远不要直接把密码暴露出来。总是把它放到 secrets 里。你在 yaml 文件中可以通过 secrets.DOCKER_PASSWORD
访问这些 secret 变量。
8. 进入 Github 仓库的设置页面,在“环境变量”下点击“操作”选项。然后,点击“新建仓库密钥”。
我们将添加两个密钥:
- DOCKER_USERNAME
- DOCKER_PASSWORD
我们在之前的博客里已经讲过很多次了,所以这里我不再解释步骤,我只放几张截图。
在安全组的入站规则中添加允许8080端口无限制访问。
GitHub服务器将会通过SSH登录到这个EC2实例,拉取最新的Docker镜像并运行它。
为了进行SSH连接,我们需要提供以下3项内容:
- EC2主机
- “.pem”密钥。我们称其为SSH密钥。
- EC2用户(默认用户通常是“ubuntu”)
把这些内容添加到 GitHub 秘密中。在 EC2_SSH_KEY 中,复制粘贴你之前下载的 ec2 实例的 .pem 文件的内容,就可以了。
现在,请从你的笔记本电脑用SSH连接到这个EC2实例,并在里面给它装上Docker。
要在Ubuntu机器上安装Docker,可以点击下面这个链接。
编写 CD 工作流程在 .github/workflows
文件夹里,创建任何名称的 yaml 文件,并写入以下代码部署工作流。
name: 使用 Docker 实现 Node.js + TypeScript 的 CI/CD
on:
push:
branches:
- master
jobs:
部署:
name: 将应用程序部署到 EC2
运行在: ubuntu-latest
步骤:
# 检出代码
- name: 检出代码
uses: actions/checkout@v3
# 设置 Docker Buildx 工具
- name: 设置 Docker Buildx 工具
uses: docker/setup-buildx-action@v2
# 登录 Docker Hub
- name: 登录 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# 构建并推送 Docker 镜像到仓库
- name: 构建并推送 Docker 镜像到仓库
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/ci-cd-tutorial:latest
# 通过 SSH 将 Docker 镜像部署到 EC2
- name: 通过 SSH 将 Docker 镜像部署到 EC2
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
port: 22
script内容: |
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/ci-cd-tutorial:latest
sudo docker stop ci-cd-tutorial || true
sudo docker rm ci-cd-tutorial || true
sudo docker run -d --name ci-cd-tutorial -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/ci-cd-tutorial:latest
# 注意:请将 ci-cd-tutorial 替换成您在 Docker Hub 上的应用程序名称
我们已经写下了我们之前讨论的具体步骤。
我们使用了预定义的动作来登录到DockerHub并通过SSH登录到EC2实例。
现在到了最激动人心的部分。我们还没有把代码部署到 EC2 实例上。当我们通过 EC2 的 IP 地址访问应用程序的 8080 端口时,会发现里面什么都没有。
我们现在将把一些代码推送到GitHub主分支上,然后这将触发自动部署(即CD)过程,把我们的代码部署上去。
打开 src/index.ts 然后粘贴下面的代码。
app.get("/order", (req, res) => {
res.send("订单页");
})
提交更改内容
可以看到有一个红点(指示灯),这表示我们的CD被触发了。
你可以点击那个红点看看。
你可以点击“详情”,看看日志。
大约1到2分钟后,我们会看到一个绿色的勾,这意味着CD的步骤都成功了,也就是说我们的最新代码已经部署好了。
我们可以通过访问我们的EC2实例的IP地址和8080端口来查看应用。
每次你推送你的代码到GitHub的主分支,最新的代码就会自动部署到EC2服务器上。
博客到此结束如果你想学习Linux的高级概念和技术,可以看看这篇博客:这篇博客。
在接下来的一系列博客文章中,我们将通过Jenkins来学习持续集成与持续部署。
如果你喜欢我的作品,喜欢的话可以在下面任意金额捐款,支持一下。
- 印度用户: 通过UPI支付 => shivambhadani@slc
- 非印度用户: https://buymeacoffee.com/shivambhadani_
你可以通过以下方式联系我。
微信
领英
邮箱
请根据实际情况填写联系方式。