跳到正文 Lab 06: Configuration Automation and Code Versioning Systems (Pulumi) · Jimmy Huang's Blog
← Week 6 Infrastructure-as-Code (IAC) + Cloud Platforms - AWS (1) 学习 › C6001 › 2026 S1 › FIT5225 Cloud computing and security › Week 6 Infrastructure-as-Code (IAC) + Cloud Platforms - AWS (1) Lab 06: Configuration Automation and Code Versioning Systems (Pulumi) Lab 06: Configuration Automation and Code Versioning Systems (Pulumi) 本页目录· 46 条 这一页有部分内容暂时无法显示,其余内容完整。
Lab学后总结 ›
Overview 这周 Lab 的主线是把前几周学过的“手动建 VM、手动装软件、手动部署应用”,改成可以重复执行的 Infrastructure as Code(IaC) 流程。
用 Ansible 自动准备基础设施,用 Kubernetes 运行容器应用,用 Pulumi 把应用部署过程代码化。
具体来说,这周分成三段:
先准备一台 控制节点(control VM) ,作为后续自动化操作的执行环境。
用 Ansible 自动在 Nectar 上创建两台新 VM,并继续用 Ansible 把它们配置成一个 2 节点 Kubernetes 集群 。
用 Pulumi 把上周的 ToDo Web Service 以声明式方式部署到 Kubernetes 中,同时自动完成镜像构建、推送和服务创建。
这一周真正想让你掌握的,不是单条命令,而是这一条完整链路:
控制节点准备 → Ansible 建资源 → Ansible 装集群 → Pulumi 部署应用 → Git 管理 IaC 文件版本
替换提醒:文档里的 <your-name>、 <your-key-name-in-nectar>、 <CONTROL_VM_PUBLIC_IP>、 <MASTER_PUBLIC_IP>、 <WORKER_PUBLIC_IP>、 <your-dockerhub-username> 都要换成你自己的值。 此处有一段内容暂不支持显示。
Goal 这周实验的目标可以概括成 4 件事:
本页目录 学会用 Ansible 批量创建云资源并批量配置多台机器
学会把两台 VM 自动化搭成一个最小可用的 Kubernetes 集群
学会用 Pulumi + Docker + Kubernetes 把应用从源码直接部署到集群里
Task 0: 创建控制节点 VM 并准备环境 创建一台后续专门拿来跑自动化脚本的 VM
准备这周需要用到的 OpenStack 凭据和基础软件
这台机器不是 K8s master,而是 控制中心
做完后,你应该能在这台 VM 上继续执行 Ansible / OpenStack / Git / Python 相关命令
Step 0.1:创建控制节点 VM 这台 VM 是本周所有自动化操作的执行入口,后面你会在它上面运行 Ansible、OpenStack CLI,并通过它去创建另外两台机器。
如果没有一台固定的控制节点,你后面的 playbook、inventory、openrc、私钥文件就没有一个统一的执行环境。
Step 0.2:准备 openrc 和 Nectar 密码 openrc 用来把 OpenStack 的认证变量加载进 shell,后面的 openstack server list 和 Ansible OpenStack 模块都要依赖它。
没有 openrc,CLI 和 playbook 都不知道该连哪个项目、用哪个账号认证。
Step 0.3:在控制节点安装基础工具 安装后面会用到的 Git、Curl、Python 和 Pip,并把用户级安装路径加到 PATH 里。
后面你会用 pip 安装部分工具,如果不把 ~/.local/bin 加进 PATH,有些命令可能安装了却找不到。
Task 1: 使用 Ansible 自动创建资源并配置 Web Server 把这周 Lab 提供的压缩包、openrc 和 SSH 私钥准备到控制节点上
安装 Ansible、OpenStack SDK 和 CLI
运行 playbook 自动在 Nectar 上创建两台新 VM
再用 Ansible 对这两台 VM 做连通性验证和 Apache 安装
做完后,你应该能在浏览器里访问这两台新机器的 Apache 默认页面
Step 1.1:把本周需要的文件放到控制节点 你需要把下面这些文件上传到控制节点 VM 的家目录 ~:
ansible-demo.zip 和 ansible-k8s-demo.zip:
Nectar -> Object Store -> Containers -> Ansible-K8s
openrc:
上一周使用过的 OpenStack 配置文件
SSH 私钥:
你用来登录 Nectar VM 的那个私钥
把后续自动化操作需要的所有输入材料集中到控制节点上。
Ansible playbook、inventory、OpenStack 认证、SSH 登录都依赖这些文件。
Step 1.2:准备 SSH 私钥路径
做法 A:你已经把私钥复制到控制节点
做法 B:你不想复制旧私钥,直接在控制节点新生成一对 key 然后把 ~/.ssh/id_ed25519.pub 的内容复制到 Nectar:
后面你要把这个 Key Pair Name 当成:
后面 Ansible 要通过 SSH 连接你创建出来的新 VM,这个环境变量就是统一保存私钥路径。
后面的 ansible ... --private-key=$PRI_KEY_PATH 都会复用它,不用每次手写完整路径。
No such file or directory:私钥路径写错
Permissions 0644 ... are too open:执行 chmod 600 <your-private-key>
Permission denied (publickey):通常是创建 VM 时注入的 key pair 名字不对,或者 inventory IP 写错
Step 1.3:安装 Ansible 和 OpenStack 相关依赖 安装 Ansible 本体、OpenStack SDK、OpenStack CLI,以及 Ansible 访问 OpenStack 所需的 collection。
后面的 nectar.yaml 不是在本地模拟创建,而是真的要调用 OpenStack API 去创建 VM。
Step 1.4:解压 ansible-demo.zip 并准备 openrc 如果你的 openrc 文件名不同,请把命令里的文件名替换掉。
把官方给的 Ansible demo 解压出来,并把 OpenStack 认证文件放进这个项目目录。
后面你会直接在这个目录里 source openrc 并执行 playbook,放在同一个目录最方便。
Step 1.5:修改 host_vars/nectar.yaml告诉 playbook:新 VM 要创建在哪个 zone、用什么规格、用哪一个镜像。
如果这些参数不改,playbook 可能会在错误的 zone 创建、用错配置、甚至直接创建失败。
Step 1.6:加载 OpenStack 环境变量并测试认证 执行 source 时会提示你输入 Nectar/OpenStack 密码。
把 openrc 中的认证变量加载到当前 shell,然后用 openstack server list 测试认证是否成功。
如果这里都认证不过,后面的 Ansible playbook 也不可能成功创建实例。
openstack server list 能列出项目里的实例列表。
Step 1.7:运行 playbook 自动创建两台新 VM 调用 nectar.yaml 这个 playbook,在 Nectar 上自动创建两台新实例。
这一步就是本周 IaC 的第一个核心点: 不用手动点 Dashboard,而是让配置文件帮你创建资源。
Step 1.8:创建 hosts.ini 作为 Web Server inventory 在 ~/ansible-demo 目录下新建 hosts.ini:
后面的 ping、装 Apache、启动 Apache 都会通过这个 inventory 找到目标机器。
Step 1.9:用 Ansible ping 模块测试 SSH 连通性 不是普通 ICMP ping,而是让 Ansible 通过 SSH 登录到目标主机,再运行 ping 模块返回 pong。
inventory IP 对不对
SSH 私钥对不对
远程用户名对不对
Step 1.10:批量安装 Apache Web Server 这是 Ansible 的第二个核心点: 同一条命令对多台机器批量生效。
两个目标主机都显示任务成功,Apache 被安装完成。
Step 1.11:启动 Apache 并验证网页访问 安装完成后再把 Apache 服务真正启动起来,并确认外部访问正常。
仅仅“包安装好了”还不够,真正会被老师检查的是: 服务有没有起来、浏览器能不能打开。
Task 2: 使用 Ansible 安装 Kubernetes 集群 复用 Task 1 创建出来的两台 VM
指定其中一台做 master,另一台做 worker
继续用 Ansible 自动化完成 K8s 依赖安装、初始化和节点加入
做完后,你应该能在 master 节点上执行 kubectl get nodes
Step 2.1:解压 ansible-k8s-demo.zip进入 Lab 提供的第二套 Ansible 脚本,这一套是专门用来部署 Kubernetes 的。
Task 1 的 playbook 负责“创建主机”,Task 2 的 playbook 负责“把主机配置成 K8s 集群”。
Step 2.2:创建 k8s-hosts.ini在 ~/ansible-k8s-demo 目录下新建文件:
给两台机器分角色:一台是 control plane/master,一台是 worker。
Kubernetes 节点角色不同,后面的 playbook 会根据分组执行不同操作。
确认 master 和 worker 的 IP 没写反。后面你会登录 master 执行 kubectl。
Step 2.3:先测试所有主机的连通性 先确认控制节点能通过 SSH 连到 master 和 worker。
如果这一步不通,后面的 4 个 playbook 连跑都没必要开始。
Step 2.4:执行用户权限设置 playbook Kubernetes 的安装和管理通常需要特定权限准备,这一步就是把基础环境先铺好。
Step 2.5:安装 Kubernetes 依赖 如果缺少 container runtime、kubelet、kubeadm、kubectl 等依赖,后面的集群初始化不可能成功。
Step 2.6:创建 master 节点 初始化 Kubernetes control plane,让第一台机器成为 master。
master 初始化成功,后续 worker 才能 join 进来。
Step 2.7:让 worker 节点加入集群 一个只有 master 的集群还不完整,这一步完成后才是一个最小的 2 节点集群。
Step 2.8:登录 master 节点验证集群 检查集群节点状态,以及系统命名空间里的核心组件是否正常运行。
kubectl get nodes 是这一部分最关键的验收命令之一,老师很可能会看这个。
Task 3: 使用 Pulumi 把 ToDo Web Service 部署到 Kubernetes 在本地先初始化一个 Pulumi Python 项目
写好 Pulumi IaC 代码,让它自动创建 namespace、build/push image、创建 Deployment 和 Service
准备应用的 Docker build context,也就是 src/ 目录
再把这些文件放到 K8s master 上执行 pulumi up
做完后,你应该能在集群中看到 namespace、deployment、pods、service,并能从集群内部访问应用
说明:如果你前面的 Task 2 实在没做完,原始 Lab 允许你改用类似 Week 4 的 K3s 集群继续 Task 3。
Step 3.1:在 K8s master 节点安装 Pulumi CLI 和 python3-venv
Step 3.2:在 K8s master 节点初始化 Pulumi Python 项目 Pulumi 会帮你生成一个 Python 项目骨架,包括 __main__.py、 requirements.txt 和本地虚拟环境。
你后面要做的不是“从零拼文件”,而是在 Pulumi 生成的骨架基础上替换成自己的 IaC 逻辑。
Pulumi.yaml
__main__.py
requirements.txt
venv/
Step 3.3:激活虚拟环境并安装 Pulumi 依赖
Step 3.4:把根目录的 __main__.py 替换成 Lab 给的 Pulumi 程序 把项目根目录下的 __main__.py 替换成下面内容,并把配置变量改成你自己的值:
把 Pulumi 项目从默认模板改成一个真正能部署你应用的 IaC 程序。
这个文件就是本周 Pulumi 的核心,它把“创建 namespace、构建镜像、部署 Deployment、暴露 Service”写成代码。
Namespace(...):创建一个独立命名空间
docker.Image(...):从 ./src build 镜像并推到 Docker Hub
Deployment(...):创建 2 个副本的应用 Pod
Service(...):把 Service 的 80 端口映射到容器的 8000 端口
pulumi.export(...):导出部署结果给你查看
docker_repo_name 必须换成你自己的 Docker Hub 用户名
k8s_namespace_name 可自定义,但后面验证命令也要跟着改
service_type 建议先保持 ClusterIP,和 Lab 保持一致
如果你的应用监听的不是 8000,要同步修改: containerPort
targetPort
Docker 启动命令里实际使用的端口
Step 3.5:准备 src/ 目录、应用源码、应用依赖和 Dockerfile 原始 Lab 写的是把 todo-app.py 放进 src/。
给 docker.Image(build={"context": "./src"}) 提供真正的构建上下文。
Pulumi 并不会凭空构建镜像,它要去 ./src 里找你的源码、Dockerfile 和应用依赖。
Step 3.6:在 K8s master 节点安装 Pulumi 并初始化运行目录 在真正连着 Kubernetes 的那台 master 上准备好 Pulumi 运行环境,并生成一个 Linux 下可用的 Python 项目骨架。
不要直接把你本地电脑生成的 venv/ 整个复制过来复用。
不同操作系统下的虚拟环境经常不能直接通用,最稳的做法是在 master 上重新初始化一份。
Step 3.7:在 master 上安装依赖、登录 Docker、执行 pulumi up执行 pulumi up 后,先检查 preview,没问题再输入 yes。
让 Pulumi 按你写的 IaC 代码自动完成这些事:
创建 namespace
build Docker image
push image 到 Docker Hub
创建 Deployment
创建 Service
这一步把 Week 3 的 Docker、Week 4/5 的 Kubernetes、以及本周的 IaC 串成了一条自动化流水线。
pulumi up 成功结束,并显示创建了若干资源
Step 3.8:验证 Kubernetes 资源并测试服务 然后自动取出 Service 的 ClusterIP:
检查 Pulumi 是否真的把 Deployment、Pod、Service 都创建出来了,并且服务能在集群内部访问。
Lab 这里默认用的是 ClusterIP,所以 不能直接从你本地浏览器访问 。
ClusterIP 的意义就是“只在集群内部可达”。
Summary 这周真正学到的不是某一条命令,而是 把基础设施和应用部署都写成代码 。
前半段的 Ansible 让你看到,VM 创建、批量装软件、批量配置节点,不一定要靠手工操作;
后半段的 Pulumi 则进一步把“构建镜像、推镜像、创建 Deployment、创建 Service”也写进了代码里。
再加上 Git,你就可以把这些 IaC 文件像普通项目代码一样版本化、比较、回滚和协作。
以后不是“我手动部署过一次”,而是“我把部署过程写成了可以重复执行的代码”。
本周必须理解的知识点 IaC(Infrastructure as Code) :基础设施不是手动点出来,而是写成可复现的配置或代码
控制节点(control VM):主要用来运行 Ansible 和 OpenStack CLI,负责创建 VM、批量配置机器;它不等于 K8s master。
K8s master:负责运行 kubectl,并在本次流程中执行 Pulumi 部署。
Ansible inventory :告诉 Ansible 要连哪些主机
Ansible playbook :把一系列配置操作写成文本化步骤,批量执行
Pulumi 项目根目录 vs src/:前者是 IaC 代码,后者是应用镜像构建上下文
ClusterIP :默认只能在集群内部访问,不能直接给外部浏览器打开
关键概念对比
Ansible vs Pulumi “我想要这些目标资源存在,工具会帮我创建或更新到目标状态。”
Ansible :偏主机配置自动化
Pulumi :偏云资源和应用资源声明式管理
Deployment vs Pod 你一般不是直接长期管理 Pod,而是让 Deployment 去管理 Pod 。
Service vs Pod Pod 负责跑应用,Service 负责把请求送到正确的 Pod。
ClusterIP vs LoadBalancer 本周自己搭的 K8s 集群里, ClusterIP 更符合 Lab 预期,也更容易成功。
项目根目录 requirements.txt vs src/requirements.txtavailability zone -> monash-02
instance flavour -> r3.small
instance image ID -> c0250c96-98a4-4bfa-b67c-51874808337f[web_servers]
<WEB_SERVER_1_PUBLIC_IP>
<WEB_SERVER_2_PUBLIC_IP>sudo apt update sudo apt install -y \ software-properties-common \ git \ curl \ python3 \ python3-pip \ unzip \ vim \ python3-shade export PATH=$PATH:/home/ubuntu/.local/bin echo ' export PATH=$PATH:/home/ubuntu/.local/bin ' >> ~/.bashrc git --version python3 --version pip3 --version export PRI_KEY_PATH=<path-of-your-private-key> export PRI_KEY_PATH=~/.ssh/mykey.pem ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 cat ~/.ssh/id_ed25519.pub export PRI_KEY_PATH=~/.ssh/id_ed25519 echo $PRI_KEY_PATH ls -l $PRI_KEY_PATH sudo apt update sudo apt install -y python3-pip unzip vim python3-shade sudo pip3 install -U ansible openstacksdk openstackclient sudo ansible-galaxy collection install openstack.cloud ansible --version openstack --version ansible-galaxy collection list | grep openstack.cloud unzip ~/ansible-demo.zip cp ~/FIT5225-Cloud-openrc.sh ~/ansible-demo/ cd ~/ansible-demo cd ~/ansible-demo vim host_vars/nectar.yaml source ./FIT5225-Cloud-openrc.sh openstack server list ansible-playbook -e " my_name=<your-name> " -e " key_name=<your-key-name-in-nectar> " nectar.yaml ansible -i hosts.ini -v web_servers -m ping --private-key= $ PRI_KEY_PATH -u ubuntu ansible -i hosts.ini -v web_servers -m apt -a " name=apache2 state=latest " --private-key= $ PRI_KEY_PATH -u ubuntu --become ansible -i hosts.ini -v web_servers -a " systemctl start apache2 " --private-key= $ PRI_KEY_PATH -u ubuntu --become curl http:// < WEB_SERVER_1_PUBLIC_I P> curl http:// < WEB_SERVER_2_PUBLIC_I P> cd ~ unzip ansible-k8s-demo.zip cd ansible-k8s-demo cat > k8s-hosts.ini << EOF [masters] 118.138.239.74 [workers] 118.138.239.25 EOF cat k8s-hosts.ini ansible -i k8s-hosts.ini all -m ping --private-key= $ PRI_KEY_PATH -u ubuntu ANSIBLE_HOST_KEY_CHECKING= False ansible-playbook -i k8s-hosts.ini --private-key= $ PRI_KEY_PATH 1_users.yml ANSIBLE_HOST_KEY_CHECKING= False ansible-playbook -i k8s-hosts.ini --private-key= $ PRI_KEY_PATH 2_install_k8s.yml ANSIBLE_HOST_KEY_CHECKING= False ansible-playbook -i k8s-hosts.ini --private-key= $ PRI_KEY_PATH 3_create_master.yml ANSIBLE_HOST_KEY_CHECKING= False ansible-playbook -i k8s-hosts.ini --private-key= $ PRI_KEY_PATH 4_join_worker.yml 本次实际遇到的问题: worker join 时报错: couldn ' t validate the identity of the API Server Get "https://118.138.239.74:6443/..." timeout 原因: worker 无法访问 master 的 Kubernetes API Server 端口 6443。 检查方法: 在控制节点执行: ANSIBLE_HOST_KEY_CHECKING=False ansible -i k8s-hosts.ini workers \ -m shell \ -a "curl -k -m 5 https://118.138.239.74:6443/healthz" \ --private-key=$PRI_KEY_PATH \ -u ubuntu 如果 timeout,需要给 kubernetes-cluster security group 添加 TCP 6443 ingress rule。 修复后重新运行 4_join_worker.yml。 ssh -i $PRI_KEY_PATH ubuntu@ < MASTER_PUBLIC_I P> kubectl get nodes kubectl get pods -A curl -fsSL https://get.pulumi.com | sh echo ' export PATH="$PATH:$HOME/.pulumi/bin" ' >> ~/.bashrc source ~/.bashrc sudo apt update sudo apt install -y python3-venv pulumi version python3 -m venv --help | head mkdir -p ~/pulumi-todoapp cd ~/pulumi-todoapp pulumi login --local pulumi new python project name: pulumi-todoapp project description: de stack name: dev cd ~/pulumi-todoapp cat > requirements.txt << EOF pulumi pulumi_docker pulumi_kubernetes EOF source venv/bin/activate pip install -r requirements.txt """ A Python Pulumi program """ import pulumi import pulumi_docker as docker import pulumi_kubernetes as k8s from pulumi_kubernetes.core.v1 import Namespace from pulumi_kubernetes.apps.v1 import Deployment from pulumi_kubernetes.core.v1 import Service # Configuration variables app_name = " todoapp " docker_repo_name = " <your-dockerhub-username> " docker_tag = " latest " k8s_namespace_name = " todo-app-ns " service_type = " ClusterIP " # Change to "LoadBalancer" if needed # 1. Create a Kubernetes Namespace namespace = Namespace( k8s_namespace_name, metadata={ " name " : k8s_namespace_name}, ) # 2. Build the Docker image docker_image = docker.Image( app_name, build={ " context " : " ./src " , " platform " : " linux/amd64 " , }, image_name= f"docker.io/ {docker_repo_name} / {app_name} : {docker_tag} " , ) # 3. Deploy to Kubernetes app_labels = { " app " : app_name} deployment = Deployment( app_name, metadata={ " namespace " : k8s_namespace_name, }, spec={ " selector " : { " matchLabels " : app_labels}, " replicas " : 2, " template " : { " metadata " : { " labels " : app_labels}, " spec " : { " containers " : [ { " name " : app_name, " image " : docker_image.image_name, " ports " : [{ " containerPort " : 8000}], " env " : [ # {"name": "VAR_NAME", "value": "value"}, ], } ] }, }, }, ) # 4. Create a Kubernetes Service service = Service( app_name, metadata={ " namespace " : k8s_namespace_name, }, spec={ " selector " : app_labels, " ports " : [{ " port " : 80, " targetPort " : 8000}], " type " : service_type, }, ) # 5. Export the Service's IP address (if applicable) if service_type == " LoadBalancer " : service_ip = service.status.apply( lambda status: status.load_balancer.ingress[0].ip if status.load_balancer and status.load_balancer.ingress else " pending " ) pulumi.export( " service_ip " , service_ip) else : pulumi.export( " service_name " , service.metadata[ " name " ]) cd ~/pulumi-todoapp mkdir -p src cat > src/todo_app.py << 'EOF' from fastapi import FastAPI from pydantic import BaseModel app = FastAPI(title="Lab 06 ToDo App") class TodoItem(BaseModel): id: int title: str completed: bool = False todos = [ TodoItem(id=1, title="Finish Lab 06 Task 3", completed=False), TodoItem(id=2, title="Deploy with Pulumi", completed=False), ] @app.get("/") def read_root(): return { "message": "Hello from the Lab 06 ToDo App", "docs": "/docs" } @app.get("/todos") def list_todos(): return todos @app.post("/todos") def create_todo(todo: TodoItem): todos.append(todo) return todo @app.get("/health") def health_check(): return {"status": "ok"} EOF cat > src/requirements.txt << 'EOF' fastapi uvicorn[standard] pydantic EOF cat > src/Dockerfile << 'EOF' FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "todo_app:app", "--host", "0.0.0.0", "--port", "8000"] EOF ls -lh src ssh -i < your-local-private-ke y> ubuntu@ < MASTER_PUBLIC_I P> sudo apt update sudo apt install -y docker.io sudo systemctl enable --now docker sudo usermod -aG docker ubuntu newgrp docker docker --version docker ps docker login -u username cd ~/pulumi-todoapp source venv/bin/activate docker info | grep Username kubectl get nodes pip install -r requirements.txt pulumi up kubectl get all -n todo-app-ns kubectl get svc -n todo-app-ns kubectl get pods -n todo-app-ns -o wide SERVICE_IP=$(kubectl get svc -n todo-app-ns -o jsonpath= ' {.items[0].spec.clusterIP} ' ) echo $SERVICE_IP curl http:// < CLUSTER_I P> / < api-endpoin t>