从零构建全自动容器化部署流水线:GitHub Actions + Azure ACI实战
1. 项目概述:从零构建一个全自动的容器化部署流水线
在云原生和DevOps的实践中,我经常被问到:“我该如何开始学习容器和CI/CD?” 这是一个非常好的问题。很多人一上来就想直接挑战复杂的Kubernetes集群和微服务架构,结果往往被海量的概念和配置劝退。我的建议是,先从最简单、最直接的路径走通整个闭环,感受自动化部署带来的“魔力”。今天分享的这个实战项目,就是为这个目的设计的:我们将构建一个极简的Node.js网页应用,用Docker将其容器化,然后通过GitHub Actions实现一个全自动的CI/CD流水线,最终部署到Azure Container Instances上。整个过程,你只需要一次推送代码到GitHub,剩下的构建、推送镜像、部署上线全部自动完成。这不仅是学习,更是建立一个可复用的工程实践模板。
为什么选择Azure Container Instances而不是更强大的AKS?这是个关键问题。对于严肃的生产环境,AKS无疑是更佳选择,它提供了完整的Kubernetes生态。但ACI的定位非常精准:它是无服务器的容器实例。你无需管理任何集群节点、控制平面或工作节点,只需提供一个容器镜像,Azure负责运行它。这种“零运维开销”的特性,使其成为演示、开发测试环境、内部工具部署的绝佳选择。它的简单性,能让你专注于理解CI/CD流程本身,而不是被复杂的集群管理分散精力。这个项目适合所有希望将应用现代化、并开始实践自动化部署的开发者,无论你是前端、后端还是全栈。
2. 核心思路与架构设计解析
2.1 技术栈选型与设计哲学
这个项目的核心设计哲学是“最小可行产品”和“关注点分离”。我们不过度设计应用本身,而是将全部精力集中在部署流水线的构建上。因此,技术栈的选择都服务于这个目标:
- 应用层 :一个纯静态的Node.js Web服务器。我们没有使用Express、Koa等框架,甚至没有使用任何前端框架。应用仅包含
index.html、style.css和一个极简的server.js。目的是消除一切与核心目标无关的复杂性,让应用本身简单到不会成为学习障碍。 - 容器化层 :Docker。它是实现环境一致性的基石。通过一个
Dockerfile,我们定义了应用运行所需的完整环境(Node.js运行时、依赖、代码)。这确保了应用在任何地方(你的笔记本、GitHub的构建服务器、Azure的生产环境)的行为都是一致的。 - 镜像仓库 :Azure Container Registry。你可以把它理解为Azure平台内的私有Docker Hub。它是我们流水线的中间站:GitHub Actions构建的镜像被推送到这里,然后ACI从这里拉取镜像并运行。使用ACR而非公共仓库,保证了镜像的安全性和部署速度(同在Azure网络内)。
- 计算平台 :Azure Container Instances。如前所述,它是我们的“服务器”。我们无需关心虚拟机、操作系统更新或运行时配置。ACI按秒计费,用完即删,成本极低,非常适合实验。
- 自动化引擎 :GitHub Actions。它是整个流水线的大脑和执行者。我们通过一个YAML文件定义了一系列步骤:检出代码、登录Azure、构建镜像、部署容器。GitHub提供了免费的额度,足以支撑个人项目的CI/CD需求。
这个架构的精妙之处在于,每一个环节都是云原生的标准组件,并且它们之间的集成是“原生”的。例如,GitHub Actions有官方的Azure登录Action,ACR支持通过Azure CLI直接进行安全的镜像构建。这种设计使得整个流水线既健壮又易于理解和维护。
2.2 安全与权限管理设计
在自动化流程中,安全是首要考虑。我们不能将敏感凭证(如Azure订阅的访问密钥)硬编码在代码或配置文件里,尤其是公开的仓库。本项目的安全设计遵循了最小权限原则和秘密管理最佳实践:
- 服务主体 :我们在Azure Active Directory中创建一个专门用于GitHub Actions的“服务主体”。你可以把它理解为一个机器人账户。我们只为这个账户授予对特定资源组(而非整个订阅)的“贡献者”角色。这样,即使凭证泄露,攻击者的权限也被限制在很小的范围内。
- GitHub Secrets :创建服务主体后,我们会得到一组密钥(Client ID, Client Secret, Tenant ID)。这些密钥被以加密形式存储在GitHub仓库的“Secrets”中。在GitHub Actions工作流运行时,这些秘密会被安全地注入到环境变量中,供登录步骤使用。你的工作流YAML文件里永远不会出现明文的密钥。
- ACR访问控制 :在部署到ACI时,ACI需要从ACR拉取镜像。我们同样使用服务主体的凭证进行认证,而不是为ACR单独创建管理员账号。这实现了统一的身份管理。
注意 :在真实项目中,对于生产环境,可以考虑使用Azure Key Vault来集中管理密钥,并通过GitHub Actions的
azure/get-keyvault-secretsAction来动态获取,实现更高级别的秘密轮换和管理。
3. 环境准备与项目初始化
3.1 本地开发环境配置
在开始编写任何代码之前,我们需要确保本地环境就绪。这里假设你使用的是macOS或Linux系统(Windows用户建议使用WSL2以获得最佳体验)。
- 安装Docker Desktop :这是容器化的基础。前往Docker官网下载并安装Docker Desktop。安装后启动它,确保右下角状态为“Running”。在终端运行
docker --version和docker run hello-world来验证安装成功。Docker Desktop包含了完整的Docker引擎、CLI以及一个轻量级的Kubernetes集群(本项目用不到),是我们进行本地构建和测试的利器。 - 安装Azure CLI :这是与Azure资源交互的命令行工具。你可以通过包管理器安装(如macOS的
brew install azure-cli),或从微软官方下载安装包。安装后,运行az --version验证。接下来需要进行登录,运行az login,这会打开浏览器让你用你的Azure账户进行认证。成功登录后,你的订阅信息就被CLI管理起来了。 - 准备GitHub仓库 :在GitHub上创建一个新的空仓库,例如命名为
azure-ci-cd-demo。然后将其克隆到本地:git clone https://github.com/你的用户名/azure-ci-cd-demo.git && cd azure-ci-cd-demo。
3.2 创建极简Node.js应用
在我们的项目根目录下,创建以下四个文件。记住,应用本身极其简单,重点在于理解文件结构和它们如何被容器化。
package.json : 这是Node.js项目的清单文件,定义了项目名称、版本和启动脚本。
{
"name": "cicd-demo-app",
"version": "1.0.0",
"description": "A minimal web app for CI/CD demo",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"keywords": ["demo", "azure", "ci-cd"],
"author": "Your Name",
"license": "MIT"
}
server.js : 一个不足20行的原生Node.js HTTP服务器,用于提供静态文件。
const http = require('http');
const fs = require('fs');
const path = require('path');
const port = process.env.PORT || 3000; // 关键:从环境变量读取端口
const server = http.createServer((req, res) => {
let filePath = '.' + req.url;
if (filePath === './') {
filePath = './index.html';
}
const extname = path.extname(filePath);
let contentType = 'text/html';
switch (extname) {
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'application/javascript';
break;
}
fs.readFile(filePath, (error, content) => {
if (error) {
if(error.code == 'ENOENT') {
// 文件不存在,返回404
fs.readFile('./404.html', (err, cont) => {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end(cont || 'Page not found');
});
} else {
// 服务器错误
res.writeHead(500);
res.end('Sorry, internal error: ' + error.code);
}
} else {
// 成功,返回文件内容
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
这个服务器脚本有几个细节值得注意:它通过 process.env.PORT 读取环境变量,这为我们在不同环境(本地3000端口,ACI的80端口)运行提供了灵活性。它包含了简单的错误处理,虽然基础,但比直接崩溃更友好。
index.html : 应用的前端界面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azure CI/CD 演示</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<header class="header">
<h1>🚀 自动化部署成功!</h1>
<p class="subtitle">你的代码已通过 GitHub Actions 自动部署到 Azure Container Instances。</p>
</header>
<main class="content">
<div class="card">
<h2>当前版本</h2>
<div class="version-badge">v1.0.0</div>
<p>这个版本号仅存在于 <code>index.html</code> 文件中。修改它并推送,即可触发一次全新的自动化部署。</p>
</div>
<div class="card">
<h2>技术栈</h2>
<ul class="tech-stack">
<li>Node.js 静态服务器</li>
<li>Docker 容器化</li>
<li>Azure Container Registry (ACR)</li>
<li>Azure Container Instances (ACI)</li>
<li>GitHub Actions (CI/CD)</li>
</ul>
</div>
<div class="card">
<h2>下一步尝试</h2>
<ol class="next-steps">
<li>打开 <code>index.html</code>,将版本号改为 <code>v2.0.0</code>。</li>
<li>执行 <code>git add .</code>, <code>git commit -m "Bump to v2.0.0"</code>, <code>git push</code>。</li>
<li>前往 GitHub 仓库的 <strong>Actions</strong> 标签页,观看流水线自动运行。</li>
<li>约2分钟后,刷新此页面,观察版本号是否已更新。</li>
</ol>
</div>
</main>
<footer class="footer">
<p>这是一个用于演示现代 CI/CD 工作流的教学项目。容器化与自动化让部署变得简单可靠。</p>
</footer>
</div>
</body>
</html>
style.css : 让页面看起来更专业的样式。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
background: linear-gradient(90deg, #0078d4, #00b4ff);
color: white;
padding: 3rem 2rem;
text-align: center;
}
.header h1 {
font-size: 2.8rem;
font-weight: 600;
margin-bottom: 0.8rem;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
font-weight: 300;
}
.content {
padding: 2.5rem;
}
.card {
background: #f8f9fa;
border-left: 5px solid #0078d4;
border-radius: 10px;
padding: 1.8rem;
margin-bottom: 2rem;
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 120, 212, 0.1);
}
.card h2 {
color: #005a9e;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.version-badge {
display: inline-block;
background: #0078d4;
color: white;
padding: 0.5rem 1.2rem;
border-radius: 50px;
font-weight: 600;
font-size: 1.3rem;
margin-bottom: 1rem;
letter-spacing: 0.5px;
}
.tech-stack {
list-style: none;
}
.tech-stack li {
padding: 0.5rem 0;
border-bottom: 1px dashed #dee2e6;
}
.tech-stack li:before {
content: "✓ ";
color: #28a745;
font-weight: bold;
}
.next-steps {
padding-left: 1.5rem;
color: #555;
}
.next-steps li {
margin-bottom: 0.7rem;
}
code {
background: #e9ecef;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #d63384;
}
.footer {
text-align: center;
padding: 1.5rem;
background: #f1f3f4;
color: #666;
font-size: 0.9rem;
border-top: 1px solid #dee2e6;
}
现在,你可以在本地运行 node server.js 并访问 http://localhost:3000 来预览这个应用。它应该显示一个美观的页面,展示了当前版本和技术栈。但这只是在你的本地机器上运行,下一步我们将把它装进“集装箱”。
4. 容器化应用:编写Dockerfile与本地测试
4.1 深入理解Dockerfile的每一行
容器化的核心是 Dockerfile ,它是一个文本文件,包含了一系列指令,告诉Docker如何构建我们的镜像。在项目根目录创建 Dockerfile (没有扩展名):
# 第一阶段:使用官方轻量级Node.js镜像作为构建和运行环境
FROM node:20-alpine AS builder
# 设置容器内的工作目录,后续命令都会在这个路径下执行
WORKDIR /usr/src/app
# 首先复制依赖定义文件
COPY package*.json ./
# 安装生产环境依赖(不安装devDependencies)
# 利用Docker的层缓存:如果package.json没变,这层会被复用,加速构建
RUN npm ci --only=production
# 第二阶段:复制应用代码
# 再次使用轻量级镜像,减小最终镜像体积
FROM node:20-alpine
# 设置运行时的工作目录
WORKDIR /usr/src/app
# 从上一阶段(builder)复制已安装的node_modules
COPY --from=builder /usr/src/app/node_modules ./node_modules
# 复制应用源代码
COPY server.js ./
COPY index.html ./
COPY style.css ./
# 声明容器运行时监听的端口
# 这只是一个文档说明,实际映射在运行`docker run`时指定
EXPOSE 3000
# 定义容器启动时执行的命令
# 使用数组格式(exec form)比字符串格式(shell form)更推荐
CMD ["node", "server.js"]
让我们拆解这个Dockerfile的设计考量:
- 多阶段构建 :我们使用了两个
FROM指令。第一阶段(builder)专门用于安装依赖。第二阶段是最终的运行时镜像。我们只从第一阶段复制了node_modules,而没有复制package.json等文件。这样做的好处是,最终的镜像不包含构建工具和源代码之外的任何文件,体积更小,安全性更高(减少了攻击面)。 - 基础镜像选择 :
node:20-alpine。Alpine Linux是一个极简的Linux发行版,镜像体积通常只有5MB左右,而Node.js官方基于Alpine的镜像也比基于Debian等发行版的镜像小很多。对于生产环境,小体积意味着更快的拉取速度和更少的安全漏洞。 - 依赖安装优化 :使用
npm ci而不是npm install。npm ci严格根据package-lock.json安装依赖,能确保依赖树的一致性,并且安装速度更快。--only=production参数确保不安装devDependencies,进一步减小镜像。 - 层缓存策略 :Docker构建是分层的,每一行指令都会产生一个层。我们将变化频率最低的指令(如
COPY package*.json ./)放在前面,变化频率最高的指令(如COPY应用代码)放在后面。这样,当我们只修改了应用代码而package.json未变时,Docker可以复用之前已构建好的node_modules层,极大加速构建过程。 -
EXPOSE与CMD:EXPOSE 3000是元数据,告诉用户这个容器打算监听3000端口。CMD定义了容器启动时的默认命令。我们使用数组格式["node", "server.js"],这能确保信号(如SIGTERM)能正确传递给Node.js进程。
4.2 本地构建与运行测试
在编写完Dockerfile后,务必在本地进行测试,这是保证后续CI/CD流程顺利的关键。
-
构建镜像 :在项目根目录(Dockerfile所在目录)打开终端,执行:
docker build -t cicd-demo-app:local .-t参数为镜像打上标签cicd-demo-app:local,.表示使用当前目录作为构建上下文。观察输出,你会看到Docker一步步执行Dockerfile中的指令。首次构建会下载基础镜像,需要一些时间。 -
运行容器 :镜像构建成功后,运行它:
docker run -d -p 8080:3000 --name my-demo-app cicd-demo-app:local-d: 后台运行(detached mode)。-p 8080:3000: 端口映射,将宿主机的8080端口映射到容器的3000端口。--name my-demo-app: 为容器指定一个名字,便于管理。cicd-demo-app:local: 要运行的镜像标签。
-
验证 :打开浏览器,访问
http://localhost:8080。你应该能看到和之前本地运行Node.js时一模一样的页面。这证明了容器化成功——应用在一个与宿主机隔离的、标准化的环境中正常运行了。 -
查看日志与清理 :
# 查看容器日志 docker logs my-demo-app # 停止容器 docker stop my-demo-app # 删除容器 docker rm my-demo-app # (可选)删除本地镜像 docker rmi cicd-demo-app:local
实操心得 :在本地成功运行容器是至关重要的一步。如果这里失败,CI/CD流程中必然失败。常见的本地问题包括:Docker Desktop未启动、端口被占用、Dockerfile语法错误、应用代码本身在容器内路径错误等。务必确保本地测试通过后再推送到远程。
5. 配置Azure云资源与服务主体
5.1 使用Azure CLI创建核心资源
所有Azure资源的创建都将通过命令行完成,这本身就是“基础设施即代码”的实践。打开终端,确保已通过 az login 登录。
首先,我们设置一些环境变量,避免在后续命令中反复输入重复信息,也减少出错几率。
# 定义资源组名称(可自定义,需在订阅内唯一)
RESOURCE_GROUP="rg-cicd-demo-$(date +%s)"
# 定义容器注册表名称(全局唯一,只能包含小写字母和数字)
ACR_NAME="acrdemo$(openssl rand -hex 3)"
# 选择区域,建议选择离你近的,如 eastus, westus2, westeurope
LOCATION="eastus"
# 获取当前订阅ID
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
echo "资源组: $RESOURCE_GROUP"
echo "容器注册表: $ACR_NAME"
echo "区域: $LOCATION"
echo "订阅ID: $SUBSCRIPTION_ID"
使用 $(date +%s) (时间戳)和 $(openssl rand -hex 3) (随机数)是为了确保名称唯一,避免因名称冲突导致创建失败。
接下来,创建资源组和容器注册表:
# 创建资源组(逻辑上的容器,用于管理一组相关的Azure资源)
az group create --name $RESOURCE_GROUP --location $LOCATION
# 创建Azure容器注册表(ACR),SKU选择Basic(适合开发测试,成本最低)
az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Basic \
--admin-enabled false # 禁用管理员账户,使用更安全的服务主体认证
创建ACR可能需要一两分钟。 --admin-enabled false 是一个安全最佳实践,它禁用了ACR自带的用户名/密码管理员账户,强制我们使用Azure Active Directory(AAD)进行身份验证,例如接下来要创建的服务主体。
5.2 创建并配置GitHub Actions服务主体
服务主体是自动化流程在Azure中的身份。我们需要创建一个,并赋予它恰好够用的权限。
# 创建服务主体,并将其作用域限定在我们刚创建的资源组
SP_JSON=$(az ad sp create-for-rbac \
--name "http://github-actions-${ACR_NAME}" \
--role contributor \
--scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP \
--sdk-auth)
echo $SP_JSON
执行这条命令后,会输出一段JSON, 请务必妥善保存这个输出 ,它包含了GitHub Actions连接Azure所需的全部凭证:
{
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
"resourceManagerEndpointUrl": "https://management.azure.com/",
...
}
clientId: 服务主体的应用程序ID,对应GitHub SecretAZURE_CLIENT_ID。clientSecret: 服务主体的密码/密钥,对应AZURE_CLIENT_SECRET。 这是最敏感的信息 。subscriptionId: 你的Azure订阅ID,对应AZURE_SUBSCRIPTION_ID。tenantId: 你的Azure AD租户ID,对应AZURE_TENANT_ID。
重要安全警告 :
clientSecret是明文密码,一旦泄露,他人可以以此身份操作你资源组内的所有资源。因此,绝对不要将其提交到代码仓库、日志或任何公开场合。我们下一步就把它存入GitHub Secrets。
5.3 在GitHub仓库中配置Secrets
- 打开你的GitHub仓库页面。
- 点击顶部导航栏的 Settings 。
- 在左侧边栏找到 Secrets and variables -> Actions 。
- 点击 New repository secret 。
- 分别创建四个secret,名称和值对应上面JSON中的字段:
- Name:
AZURE_CLIENT_ID, Value:clientId的值。 - Name:
AZURE_CLIENT_SECRET, Value:clientSecret的值。 - Name:
AZURE_TENANT_ID, Value:tenantId的值。 - Name:
AZURE_SUBSCRIPTION_ID, Value:subscriptionId的值。
- Name:
创建完成后,你的Secrets列表应该如下图所示。这些加密的变量将在工作流运行时被安全地注入到环境中。
6. 构建GitHub Actions自动化工作流
6.1 编写工作流YAML文件
GitHub Actions的工作流由YAML文件定义。在项目根目录创建 .github/workflows/deploy-to-aci.yml 文件。路径和文件名是固定的约定。
name: Build and Deploy to Azure Container Instances
# 定义触发条件:当代码推送到main分支时,或手动触发时
on:
push:
branches: [ "main" ]
# 允许在GitHub仓库的Actions页面手动触发工作流,方便测试
workflow_dispatch:
# 环境变量,方便后续步骤引用,避免硬编码
env:
RESOURCE_GROUP: rg-cicd-demo-${{ github.run_id }} # 使用运行ID确保唯一性
ACR_NAME: your_acr_name_here # 替换为你的ACR名称!
CONTAINER_NAME: cicd-demo-app
LOCATION: eastus
jobs:
build-and-deploy:
runs-on: ubuntu-latest # 工作流运行在GitHub托管的Ubuntu虚拟机上
steps:
# 步骤1:检出代码。这是所有CI/CD工作流的第一步。
- name: Checkout repository
uses: actions/checkout@v4
# 步骤2:登录到Azure。使用我们存储在Secrets中的服务主体凭证。
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }} # 推荐使用单个JSON secret,见下方说明
# 或者使用分开的四个secret:
# client-id: ${{ secrets.AZURE_CLIENT_ID }}
# client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
# tenant-id: ${{ secrets.AZURE_TENANT_ID }}
# subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# 步骤3:登录到Azure容器注册表(ACR)
- name: Log in to Azure Container Registry
run: |
az acr login --name ${{ env.ACR_NAME }}
# 步骤4:在Azure Cloud中构建并推送Docker镜像
- name: Build and push image to ACR
run: |
az acr build \
--registry ${{ env.ACR_NAME }} \
--image ${{ env.CONTAINER_NAME }}:${{ github.sha }} \
--image ${{ env.CONTAINER_NAME }}:latest \
--file Dockerfile \
.
# 步骤5:部署到Azure Container Instances (ACI)
- name: Deploy to Azure Container Instances
run: |
# 检查容器组是否已存在,如果存在则更新,否则创建
if az container show --resource-group ${{ env.RESOURCE_GROUP }} --name ${{ env.CONTAINER_NAME }} --output none 2>/dev/null; then
echo "Container instance exists, updating..."
az container update \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }} \
--image ${{ env.ACR_NAME }}.azurecr.io/${{ env.CONTAINER_NAME }}:latest
else
echo "Container instance does not exist, creating..."
az container create \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }} \
--image ${{ env.ACR_NAME }}.azurecr.io/${{ env.CONTAINER_NAME }}:latest \
--registry-login-server ${{ env.ACR_NAME }}.azurecr.io \
--registry-username ${{ secrets.AZURE_CLIENT_ID }} \
--registry-password ${{ secrets.AZURE_CLIENT_SECRET }} \
--dns-name-label ${{ env.CONTAINER_NAME }}-${{ github.run_number }} \
--ports 80 \
--os-type Linux \
--cpu 1 \
--memory 1.5 \
--environment-variables PORT=80 \
--restart-policy Always
fi
# 步骤6:(可选)输出容器访问信息
- name: Get container FQDN
run: |
az container show \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }} \
--query ipAddress.fqdn \
-o tsv
重要提示 :请务必将YAML文件中的 your_acr_name_here 替换为你实际创建的ACR名称(即前面 $ACR_NAME 变量的值)。
6.2 工作流步骤深度解析
- 触发机制 :
on指令定义了工作流何时运行。push到main分支是自动触发,workflow_dispatch提供了手动触发按钮,这在调试时非常有用。 - 环境变量 :在
env部分定义的变量可以在所有步骤中使用。这里我们动态生成了资源组名称rg-cicd-demo-${{ github.run_id }},利用GitHub Actions的运行ID确保每次运行创建的资源组都是唯一的,避免了冲突,也便于清理。github.sha和github.run_number是GitHub提供的上下文变量,分别代表触发工作流的提交SHA和当前工作流的运行编号。 - Azure登录 :我们使用了官方的
azure/login@v2Action。这里演示了两种传递凭证的方式。更简洁的方式是将之前服务主体输出的整个JSON对象保存为一个名为AZURE_CREDENTIALS的GitHub Secret,然后直接使用creds参数引用。这比管理四个单独的secret更方便。 - ACR构建 :
az acr build命令是 关键优化点 。它不是在GitHub的Runner上执行docker build,而是将构建上下文(你的代码)上传到Azure,在Azure云服务中执行构建。这样做的好处是:- 无需Docker in Docker :GitHub Runner不需要安装或运行Docker守护进程,简化了Runner环境。
- 性能与网络 :构建在Azure内部完成,镜像直接推送到同区域的ACR,速度极快,且不消耗GitHub Runner的出站带宽。
- 安全 :构建密钥和中间层不会暴露在Runner上。
- 我们为镜像打了两个标签:
${{ github.sha }}(唯一的提交哈希,用于精确版本追踪和回滚)和latest(指向最新构建的镜像)。
- ACI部署 :部署步骤使用了条件判断。它先尝试检查同名的ACI容器组是否存在。如果存在,则执行
az container update来更新其镜像(实现原地升级);如果不存在,则执行az container create创建新的容器组。这是一种更健壮的部署逻辑。--dns-name-label: 为容器实例生成一个公共可访问的FQDN(完全限定域名)。我们附加了run_number使其唯一。--cpu和--memory: 指定容器的计算资源。1个vCPU和1.5GB内存对于这个简单应用绰绰有余,也是ACI免费层(如果可用)或低成本层的典型配置。--environment-variables PORT=80: 将容器内的环境变量PORT设置为80,这样我们的Node.js应用(通过process.env.PORT || 3000)就会监听80端口,与ACI对外暴露的端口一致。--restart-policy Always: 确保容器在退出时总是重启,提高可用性。
7. 触发流水线并验证自动化部署
7.1 首次推送与流水线观察
现在,将我们所有的代码和配置文件推送到GitHub仓库的main分支。
# 添加所有文件到暂存区
git add .
# 提交更改
git commit -m "feat: 初始提交 - 包含应用代码、Dockerfile和GitHub Actions工作流"
# 推送到远程main分支
git push origin main
推送完成后,立即打开你的GitHub仓库页面,点击顶部的 Actions 标签页。你会看到一个新的工作流运行已经启动,名称就是“Build and Deploy to Azure Container Instances”。点击进入该次运行,可以实时观察每个步骤的执行情况。
- 黄色 表示步骤正在执行或排队。
- 绿色对勾 表示步骤成功。
- 红色叉号 表示步骤失败。如果失败,可以点击该步骤查看详细的日志输出,这是排查问题的关键。
整个流程通常需要2到4分钟。最耗时的步骤是“Build and push image to ACR”,因为它需要在云端拉取基础镜像并构建。
7.2 获取访问地址与验证部署
当工作流所有步骤都显示绿色对勾后,部署就成功了。如何访问我们的应用呢?
方法一:从工作流日志获取 在工作流运行的详情页面,找到“Deploy to Azure Container Instances”步骤,展开其日志。在最后部分,你应该能看到“Get container FQDN”步骤的输出,它打印了容器的完整域名(FQDN),格式类似 cicd-demo-app-12345678.eastus.azurecontainer.io 。复制这个地址。
方法二:使用Azure CLI查询 如果你本地Azure CLI已登录且配置了正确的订阅,可以运行:
# 请将资源组名称和容器名称替换为你的实际值
az container show \
--resource-group rg-cicd-demo-你的运行ID \
--name cicd-demo-app \
--query ipAddress.fqdn \
-o tsv
重要提示 :ACI默认通过HTTP在80端口提供服务。现代浏览器(如Chrome)可能会尝试将你重定向到HTTPS,导致连接失败。因此,在浏览器地址栏中访问时, 务必显式地加上 http:// 前缀 ,例如: http://cicd-demo-app-12345678.eastus.azurecontainer.io 。
打开后,你应该能看到和本地运行一模一样的页面,显示“v1.0.0”。恭喜,你的应用已经成功运行在Azure云上了!
7.3 体验真正的持续部署
现在,让我们来体验CI/CD的“魔法”。打开本地的 index.html 文件,找到显示版本号的地方(大约在第20行),将 v1.0.0 修改为 v2.0.0 。
保存文件后,再次提交并推送更改:
git add index.html
git commit -m "chore: 更新版本号至 v2.0.0"
git push origin main
再次回到GitHub仓库的 Actions 标签页。你会看到一个新的工作流运行被自动触发。等待它执行完毕(这次构建会更快,因为Docker层缓存可能生效)。
工作流完成后, 不要 手动重启容器。等待大约30秒到1分钟(给Azure负载均衡器和DNS一点时间),然后刷新你的浏览器页面。你会发现,页面上的版本号已经自动变成了 v2.0.0 !
这个过程完全自动化:代码推送触发了工作流,工作流构建了新的镜像并推送到ACR,然后更新了ACI中的容器组,使其使用新的镜像。整个过程中,你的应用服务没有中断(ACI会先启动新容器,健康检查通过后再停止旧容器,实现无缝更新)。
8. 进阶优化、问题排查与清理
8.1 工作流优化与进阶技巧
基础流水线已经跑通,但我们可以让它更健壮、更专业。
- 使用单个JSON Secret :如前所述,将服务主体的完整JSON输出保存为一个名为
AZURE_CREDENTIALS的Secret,然后在工作流中直接使用creds: ${{ secrets.AZURE_CREDENTIALS }},这样更简洁安全。 - 添加构建缓存 :虽然
az acr build在云端进行,但我们可以通过配置缓存来加速后续构建。修改构建步骤,添加--platform和缓存参数(如果适用)。 - 镜像扫描与安全 :在推送镜像前,可以集成安全扫描工具。Azure Defender for container registries(ACR高级版功能)或开源工具如Trivy可以集成到工作流中,在构建后对镜像进行漏洞扫描,如果发现高危漏洞则失败。
- 多环境部署 :为开发(dev)、预发布(staging)、生产(prod)设置不同的资源组和ACR,通过GitHub环境(Environments)和分支规则来管理。例如,推送到
develop分支部署到开发环境,打标签时部署到生产环境。 - 添加健康检查 :在
az container create命令中添加--protocol TCP --port 80的liveness probe和readiness probe,让ACI能够判断容器是否健康,并进行自动恢复。 - 使用Azure Key Vault管理密钥 :将数据库连接字符串等应用机密存储在Azure Key Vault中,让ACI在启动时从Key Vault拉取,而不是写在代码或环境变量里。
8.2 常见问题与排查实录
在实践过程中,你可能会遇到以下问题。这里记录了排查思路和解决方法:
问题1:GitHub Actions工作流在“Login to Azure”步骤失败。
- 错误信息 :
Error: Credentials could not be authenticated. - 排查 :
- 检查GitHub Secrets中的四个值(Client ID, Secret, Tenant ID, Subscription ID)是否与服务主体创建时的输出完全一致,尤其是Secret,确保没有多余的空格或换行。
- 服务主体可能已过期或被删除。使用
az ad sp list --display-name "你的服务主体名称"检查其状态。可以尝试重新创建服务主体并更新Secrets。 - 确保服务主体确实被赋予了目标资源组的“Contributor”角色。可以使用
az role assignment list --assignee <clientId> --resource-group <resourceGroup>来验证。
问题2:工作流在“Build and push image to ACR”步骤失败。
- 错误信息 :
ERROR: The resource with name 'xxx' and type 'Microsoft.ContainerRegistry/registries' could not be found in subscription. - 排查 :
- 检查YAML文件中
env.ACR_NAME的值是否正确,是否与你通过CLI创建的ACR名称完全一致(区分大小写)。 - 检查服务主体是否有权限访问该ACR。除了资源组的Contributor角色,可能还需要ACR特定的角色(如
AcrPush)。可以运行az role assignment create --assignee <clientId> --scope /subscriptions/<subId>/resourceGroups/<rgName>/providers/Microsoft.ContainerRegistry/registries/<acrName> --role AcrPush来授予推送权限。
- 检查YAML文件中
问题3:容器部署成功,但无法通过浏览器访问(连接被拒绝/超时)。
- 排查 :
- 确认协议 :ACI默认是HTTP。确保浏览器访问的是
http://开头的地址,而不是https://。 - 检查端口 :我们的应用在容器内监听
process.env.PORT || 3000,而我们在az container create中设置了环境变量PORT=80,并指定了--ports 80。所以容器内监听80,ACI对外暴露80,这是正确的。如果应用在容器内仍监听3000,则无法访问。确保环境变量传递成功。 - 查看容器日志 :使用Azure CLI
az container logs --resource-group <rgName> --name <containerName>查看容器内部的应用日志,确认Node.js服务器是否成功启动,是否有错误。 - 检查ACI状态 :
az container show --resource-group <rgName> --name <containerName>,查看provisioningState是否为Succeeded,ipAddress和fqdn字段是否正常。
- 确认协议 :ACI默认是HTTP。确保浏览器访问的是
问题4:更新镜像后,访问网站发现还是旧版本。
- 排查 :
- 浏览器缓存 :这是最常见的原因。使用Ctrl+F5强制刷新浏览器,或打开浏览器开发者工具的“网络”选项卡,勾选“禁用缓存”。
- ACI更新延迟 :
az container update命令发出后,ACI需要时间拉取新镜像并重启容器。等待1-2分钟再试。 - DNS缓存 :公共DNS记录可能有TTL(生存时间)。可以尝试使用其他设备或网络访问,或者通过容器的IP地址直接访问(从
az container show的输出中获取ipAddress.ip)。 - 确认镜像标签 :确保工作流中
az container update命令使用的镜像标签(如:latest)确实指向了新构建的镜像。可以登录Azure门户,进入ACR,查看镜像的“最近推送”时间。
8.3 资源清理
实验完成后,为了避免产生不必要的费用,请务必清理资源。最彻底的方式是删除整个资源组,这会删除组内所有资源(ACR, ACI等)。
# 请将资源组名称替换为你实际使用的名称
az group delete --name rg-cicd-demo-你的运行ID --yes --no-wait
--yes: 跳过确认提示。--no-wait: 不等待删除操作完成就返回命令提示符。
你也可以在Azure门户中手动删除资源组。进入Azure门户,导航到“资源组”,找到你的资源组,点击“删除资源组”,输入资源组名称确认即可。
9. 从实验到生产:技术路径演进思考
通过这个实验,你已经掌握了现代CI/CD流水线的核心模式:代码变更触发自动化工作流,工作流构建不可变的容器镜像,并将镜像部署到无服务器容器平台。这个模式是通用的,可以平滑地迁移到更强大的Azure服务上。
-
下一步:Azure Container Apps 如果你需要自动扩缩容(根据HTTP请求量、CPU使用率等自动调整容器实例数量)以及基于事件(如消息队列)的触发,ACA是完美的下一步。它将ACI的无服务器特性与Kubernetes的弹性结合了起来。你的工作流只需将部署命令从
az container create改为az containerapp update。 -
进阶:Azure Kubernetes Service 当你需要运行复杂的微服务、有严格的网络策略需求(如服务网格)、或需要蓝绿部署、金丝雀发布等高级部署策略时,AKS是终极选择。学习曲线较陡,但能力最强。你的CI/CD流水线最终会使用
kubectl apply -f deployment.yaml来更新Kubernetes集群中的部署。 -
简化:Azure App Service 如果你的应用只是一个传统的Web应用(如.NET Core, Java Spring Boot, Python Django),不想操心容器,那么Azure App Service提供了极简的部署体验(Git推送、ZIP部署等),并内置了自动扩缩容、流量管理等功能。它抽象了底层基础设施,让你更专注于代码。
无论选择哪条路径,你今天构建的GitHub Actions工作流的核心逻辑——认证、构建、推送、部署——都将保持不变。变化的只是最后一步部署的目标平台和命令。这才是这个实验最大的价值:它为你提供了一个坚实、可扩展的自动化部署基础模板。
更多推荐


所有评论(0)