
最近折腾了一套完整的 CI/CD 流水线,从 GitHub 代码托管开始,一路经过 Azure DevOps 构建、Docker 镜像推送、Helm 部署,最终跑在 AKS 上。踩了不少坑,这篇把整个过程和关键概念说清楚。
目标不是做个多复杂的应用,而是理解应用代码从源码到真实部署环境这个过程里,到底发生了什么。
最终要搞清楚的流程是这样的:
Developer | v
GitHub Repository | v
Azure DevOps | v
CI/CD Pipeline | +----------------------+
| |
v v
Build + Test Docker Build | v Azure Container Registry | v Helm | v AKS | v DEV | v Running Application
应用本身故意做得很简单:
学习的重点其实在基础设施这一块。
我想搞清楚这些东西:
这次只做 DEV 环境,UAT、PROD 和审批门以后再加。
DEV 环境大概长这样:
GitHub | v Azure DevOps | v Build + Test + Docker | v Azure Container Registry | v Helm | v Azure Kubernetes Service | +------------+------------+
| |
v v frontend namespace backend namespace
| |
v v Frontend Deployment Backend Deployment
| |
v v LoadBalancer Service ClusterIP Service
| |
v | Public IP / Browser | v PostgreSQL Flexible Server | v Private Network | v Private DNS Zone
一个重要的设计决策:PostgreSQL 没有装在 AKS 里面,而是用了 Azure Database for PostgreSQL Flexible Server。
也就是说 AKS 只负责跑应用负载,数据库服务由 Azure 来管。
项目仓库是:
k8app-azure-devops
仓库里包含前端、后端和 Kubernetes/Helm 配置。
简化后的结构:
k8app-azure-devops/
│
├── backend/
│ ├── app/
│ └── backend.dockerfile
│
├── frontend/
│ └── frontend.dockerfile
│
├── helm/
│ └── k8app/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── frontend-deployment.yaml
│ ├── frontend-service.yaml
│ ├── backend-deployment.yaml
│ ├── backend-service.yaml
│ ├── backend-configmap.yaml
│ └── backend-secret.yaml
│
└── azure-pipelines.yml
具体应用本身不重要,基础设施才是重点。
用的订阅是 Azure for Students。
区域选的是:
South Africa North
选这个区域是因为学生订阅对可用区域有限制,有的区域用不了。
创建资源之前最好先确认一下订阅允许哪些区域,可以从 Azure Portal 创建资源时查看,或者用命令:
az account list-locations -o table
第一个创建的资源是资源组:
Resource Group:
devops-aks-lab-rg-sa Region:
South Africa North
资源组是 Azure 资源的逻辑容器。
把所有相关资源放在一起而不是分散在 Azure 各处,管理起来方便多了:
devops-aks-lab-rg-sa | +-- ACR +-- AKS +-- PostgreSQL +-- Networking resources
权限管理、资源清理都更简单。
进入:
Azure Portal ↓
Resource groups ↓
Create
然后选择:
Subscription:
Azure for Students Resource group:
devops-aks-lab-rg-sa Region:
South Africa North
点击 Review + create,然后 Create。
也可以用命令行:
az login
然后:
az group create \ --name devops-aks-lab-rg-sa \ --location southafricanorth
验证:
az group show \ --name devops-aks-lab-rg-sa \ -o table
下一个组件是 ACR:
ACR name:
dessydevopsacr
仓库登录服务器是:
dessydevopsacr.azurecr.io
CI 流水线构建 Docker 镜像,这些镜像需要一个地方存放,Kubernetes 才能拉取部署。
ACR 就成了镜像的中央仓库:
Azure DevOps | | Docker image v
Azure Container Registry | | Docker image v
AKS
这个项目有两个镜像:
dessydevopsacr.azurecr.io/k8app-backend
dessydevopsacr.azurecr.io/k8app-frontend
进入 Azure Portal:
Azure Portal ↓
Create a resource ↓
Search:
Container Registry ↓
Create
在 Basics 页面配置:
Subscription:
Azure for Students Resource group:
devops-aks-lab-rg-sa Registry name:
dessydevopsacr Location:
South Africa North SKU:
Basic
这次禁用了 ACR 管理账号。
点击 Review + create,然后 Create。
az acr create \ --resource-group devops-aks-lab-rg-sa \ --name dessydevopsacr \ --location southafricanorth \ --sku Basic \ --admin-enabled false
验证:
az acr show \ --name dessydevopsacr \ --resource-group devops-aks-lab-rg-sa \ --query "{name:name,loginServer:loginServer,sku:sku.name}" \ -o table
预期登录服务器:
dessydevopsacr.azurecr.io
ACR 可以通过管理账号提供静态用户名密码,但不想让 CI/CD 依赖这种长期有效的凭证。
Azure DevOps 用服务连接来认证,模型更干净:
Azure DevOps | v
Service Connection | v
Azure / ACR
因为 PostgreSQL 要用私有网络,所以在创建数据库之前就得考虑网络。
基本思路是不同负载用不同的网络区域:
Virtual Network
|
+-- AKS subnet
|
+-- PostgreSQL subnet
PostgreSQL 子网用的是:
postgres-subnet
10.0.2.0/24
/24 给数据库子网分配了一个固定的私有地址段。
把数据库单独放在一个子网里是因为它有不同的用途和安全边界,跟应用负载不一样。
项目确定用了:
postgres-subnet:
10.0.2.0/24
AKS 集群还用了:
Pod CIDR:
10.244.0.0/16 Service CIDR:
10.240.0.0/16
这些跟 Azure VNet 子网不是一回事。
复现项目时网络设计要注意不要创建重叠的 CIDR 段,比如:
VNet
|
+-- AKS subnet
|
+-- PostgreSQL subnet | +-- 10.0.2.0/24
规则就是:不要让两个不同子网包含相同地址。
在 Azure Portal 里从虚拟网络创建子网:
Azure Portal ↓
Virtual networks ↓
Select your project VNet ↓
Subnets ↓
+ Subnet
创建:
Subnet name:
postgres-subnet Subnet address range:
10.0.2.0/24
保存即可,这样就给 PostgreSQL 预留了一个专用私有网段。
故意没有把 PostgreSQL 暴露到公网。
走的是:
AKS | | Private network v
PostgreSQL
而不是:
Internet | v
Public PostgreSQL endpoint
这更接近生产环境的架构设计,应用负载通过私有网络跟数据库通信。
这个概念花了一段时间才理解。
光给 PostgreSQL 创建私有 IP 不够,应用通常用主机名连接数据库,比如:
dessy-k8app-postgres.postgres.database.azure.com
主机名需要解析到私有 IP 地址,这就是私有 DNS 的作用。
创建了:
private.postgres.database.azure.com
把 DNS 想象成电话本。
应用知道:
dessy-k8app-postgres.postgres.database.azure.com
但网络最终需要的是 IP 地址。
私有 DNS 提供映射:
Hostname | v
Private DNS | v
Private IP
这个项目里 PostgreSQL 私有 IP 解析到:
10.0.2.4
所以流程是:
AKS Pod | | asks DNS: | "Where is | dessy-k8app-postgres.postgres.database.azure.com?" | v
Private DNS Zone | v
10.0.2.4 | v
PostgreSQL
没有 DNS 解析的话,应用知道主机名但不知道往哪发网络流量。
进入:
Azure Portal ↓
Create a resource ↓
Search:
Private DNS zones ↓
Create
配置:
Resource group:
devops-aks-lab-rg-sa Name:
private.postgres.database.azure.com
点击 Review + create,然后 Create。
创建完之后还需要把 DNS zone 链接到 VNet:
Private DNS zone ↓
Virtual network links ↓
+ Add
选择应用/数据库用的 VNet,这样 VNet 里的资源就能用这个私有 DNS zone。
创建了:
PostgreSQL server:
dessy-k8app-postgres Region:
South Africa North PostgreSQL:
16 SKU:
Standard_B1ms Database:
app
主机名是:
dessy-k8app-postgres.postgres.database.azure.com
服务器用的是私有网络。
没有在 Kubernetes 里面跑 PostgreSQL:
AKS | +-- PostgreSQL Pod
而是:
AKS | | private network v
Azure PostgreSQL Flexible Server
这样把应用计算和数据库基础设施分开了。
AKS 专注跑:
frontend
backend
数据库服务由 Azure 管理。
进入:
Azure Portal ↓
Create a resource ↓
Search:
Azure Database for PostgreSQL Flexible Server ↓
Create
在 Basics 页面配置:
Subscription:
Azure for Students Resource group:
devops-aks-lab-rg-sa Server name:
dessy-k8app-postgres Region:
South Africa North PostgreSQL version:
16 Workload type:
Development / appropriate low-cost option Compute:
Standard_B1ms
配置管理员:
Administrator username:
k8appadmin Administrator password:
<your password>
密码不要公开。
创建 PostgreSQL 时选择私有访问而不是公共访问。
数据库需要使用 VNet 和专用子网:
Virtual network:
<project VNet> Subnet:
postgres-subnet
私有 DNS 配置用:
private.postgres.database.azure.com
关键关系是:
PostgreSQL | v
postgres-subnet
10.0.2.0/24 | v
Private DNS
private.postgres.database.azure.com
Azure 就会建立私有网络连接。
服务器创建完之后,创建数据库:
app
从 PostgreSQL 资源进入:
PostgreSQL Flexible Server ↓
Databases ↓
+ Add
创建:
Database name:
app
应用后续连接用:
Server:
dessy-k8app-postgres.postgres.database.azure.com Database:
app User:
k8appadmin
密码保密。
创建完数据库还不够,更重要的问题是:
跑在 AKS 里的负载能不能真正连到 PostgreSQL?
从 Kubernetes 里面测试一下。
先检查 DNS 解析:
kubectl run psql-test \ --rm -it \ --image=postgres:16 \ --restart=Never \ -- \ getent hosts dessy-k8app-postgres.postgres.database.azure.com
主机名解析到了私有 PostgreSQL IP:
10.0.2.4
这证明私有 DNS 路径是通的。
用临时 PostgreSQL 客户端 pod 测试:
kubectl run psql-test \ --rm -it \ --image=postgres:16 \ --restart=Never \ -- \ psql "host=dessy-k8app-postgres.postgres.database.azure.com port=5432 dbname=app user=k8appadmin sslmode=require"
提示输入密码时输入数据库密码。
通过 TLS 连接成功。
这证明这条路是通的:
AKS Pod | v
Private DNS | v
Private IP | v
PostgreSQL Flexible Server
这是很重要的测试,把数据库/网络问题和应用问题分开了。
下一个主要组件是 Azure Kubernetes Service。
创建了:
AKS:
dessy-aks-cluster Resource Group:
devops-aks-lab-rg-sa Region:
South Africa North
集群用了:
Azure CNI Overlay Pod CIDR:
10.244.0.0/16 Service CIDR:
10.240.0.0/16 Load Balancer:
Standard
还开启了 Microsoft Entra 集成和 Kubernetes RBAC。
AKS 网络有不同配置方式,这次用了 Azure CNI Overlay。
一个重要区别是这些 CIDR:
10.244.0.0/16
10.240.0.0/16
是 Kubernetes 网络范围,跟 Azure VNet 子网范围不是一回事。
看到 10.244.x.x 这种 IP 很容易误以为是 Azure VNet IP,其实不是。
简化模型是:
Azure VNet | +-- Azure subnet | +-- AKS nodes | +-- Kubernetes pod networking | +-- Kubernetes service networking
进入:
Azure Portal ↓
Create a resource ↓
Search:
Kubernetes Service ↓
Create
在 Basics 页面配置:
Subscription:
Azure for Students Resource group:
devops-aks-lab-rg-sa Cluster preset:
appropriate development option Kubernetes cluster name:
dessy-aks-cluster Region:
South Africa North
节点池配一个节点用于学习环境。
身份/安全配置用:
Managed identity
而不是手动管理长期凭证,这样 Azure 资源可以用托管身份来工作。
开启了 Microsoft Entra 集成。
集群用 Microsoft Entra ID 做身份,Kubernetes RBAC 做授权。
认证和授权不是一回事:
Microsoft Entra ID | | Who are you? v
Authentication | v
Kubernetes RBAC | | What are you allowed to do? v
Authorization
集群网络配置:
Network plugin:
Azure CNI Network plugin mode:
Overlay Pod CIDR:
10.244.0.0/16 Service CIDR:
10.240.0.0/16
用了 Standard 负载均衡器。
这很重要,因为前端 Kubernetes Service 后面会配置成:
type: LoadBalancer
Azure 就会给那个 Service 配一个 Azure 负载均衡器/公网 IP。
开启了:
OIDC issuer
Workload identity
这些功能对 Kubernetes 负载和 Azure 服务之间的现代身份认证很有用,也能避免在应用里放长期 Azure 凭证。
集群创建完之后获取凭证:
az aks get-credentials \ --resource-group devops-aks-lab-rg-sa \ --name dessy-aks-cluster \ --overwrite-existing
然后:
kubectl get nodes
预期节点状态:
Ready
这确认了本地机器可以跟集群通信。
这个项目涉及多个身份,这是最重要的学习点之一。
AKS 集群有个 kubelet 托管身份,用命令查看:
az aks show \ --resource-group devops-aks-lab-rg-sa \ --name dessy-aks-cluster \ --query identityProfile.kubeletidentity.clientId \ -o tsv
kubelet 身份需要权限才能从 ACR 拉镜像。
需要的权限是:
AcrPull
流程是:
AKS Kubelet Identity | | AcrPull v
Azure Container Registry | v
Docker Image
在 Azure Portal 进入:
Container Registry ↓
dessydevopsacr ↓
Access control (IAM) ↓
Role assignments
添加角色分配,选择:
Role:
AcrPull
分配给 AKS kubelet 托管身份。
之后:
AKS | | AcrPull v
ACR | v
k8app-backend image
k8app-frontend image
重要区别是:
Azure DevOps 推送镜像,AKS 拉取镜像。
这是两个不同操作,可以用两个不同身份。
这个项目涉及三个不同概念。
回答:
Who are you?
对 Azure DevOps:
Azure DevOps | v
OIDC / Workload Identity Federation | v
Microsoft Entra ID
回答:
What can you do in Azure?
比如:
Azure DevOps identity | v
Contributor | v
Resource Group
回答:
What can you do inside Kubernetes?
比如:
Azure DevOps identity | v
Kubernetes RoleBinding | v
edit | +-- frontend namespace | +-- backend namespace
简化模型:
OIDC ↓
Who are you? Azure RBAC ↓
What can you do in Azure? Kubernetes RBAC ↓
What can you do in Kubernetes?
创建了 Azure DevOps 项目:
DessyTest-1
进入:
Azure DevOps ↓
New Project
创建:
Project name:
DessyTest-1
GitHub 仓库还是作为源码仓库,Azure DevOps 只负责 CI/CD 流水线。
关系是:
GitHub | v
Azure DevOps | v
Pipeline
在 Azure DevOps 里面配置 GitHub 连接:
Azure DevOps ↓
Project Settings ↓
Service connections
创建 GitHub 连接并授权仓库,目的是让 Azure DevOps 能拉取仓库源码。
Azure DevOps 需要认证到 ACR:
Azure DevOps ↓
Project Settings ↓
Service connections ↓
New service connection
选择:
Docker Registry
然后配置为 Azure Container Registry,创建服务连接:
dessydevopsacr
这个连接被流水线的 Docker 任务使用。
概念:
Azure DevOps | v
ACR Service Connection | v
Azure Container Registry
部署阶段需要 Azure 访问权限来获取 AKS 凭证,创建了:
dessy-aks-deploy
进入:
Azure DevOps ↓
Project Settings ↓
Service connections ↓
New service connection
选择 Azure Resource Manager 连接类型,用:
Workload Identity Federation
而不是长期客户端密码,这样得到:
Azure DevOps | v
OIDC / Workload Identity Federation | v
Microsoft Entra ID | v
Azure
部署服务连接用的服务主体需要对 Azure 资源有操作权限。
学习环境里分配了资源组级别的:
Contributor
给:
devops-aks-lab-rg-sa
在 Azure Portal:
Resource Group ↓
devops-aks-lab-rg-sa ↓
Access control (IAM) ↓
Add role assignment
选择 Contributor,然后选择 dessy-aks-deploy 用的服务主体。
生产环境应该用更严格的权限。
光有 Azure 权限不够,流水线能认证到 Azure 但 Kubernetes 还是拒绝了某些操作。
这教会了一个重要教训:
Azure 认证和 Kubernetes 授权是两个独立的问题。
Azure DevOps 服务主体是:
<AZURE_DEVOPS_SERVICE_PRINCIPAL_OBJECT_ID>
在 frontend 命名空间创建了 RoleBinding,在 backend 也创建了。
绑定给这个身份 Kubernetes edit ClusterRole。
示例:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: azure-devops-deployer namespace: backend
subjects: - kind: User name: <AZURE_DEVOPS_SERVICE_PRINCIPAL_OBJECT_ID> apiGroup: rbac.authorization.k8s.io
roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io
前端绑定一样,只是 namespace: frontend。
测试 Azure DevOps 身份是否能创建 Deployment:
kubectl auth can-i create deployments \ --as=<AZURE_DEVOPS_SERVICE_PRINCIPAL_OBJECT_ID> \ -n backend
kubectl auth can-i create deployments \ --as=<AZURE_DEVOPS_SERVICE_PRINCIPAL_OBJECT