1. 项目概述:用SaltStack自动化构建AWS VPC——不是写脚本,是建基础设施的“施工图纸”
你有没有在AWS控制台里点过上百次鼠标,只为配好一个VPC?子网、路由表、NAT网关、安全组、IGW、EIP……每新建一个环境,都要重复一遍这套“点点点”流程。更糟的是,开发、测试、预发、生产四套环境配置稍有差异,上线前一小时发现测试环境少开了一个端口,临时改配置,心跳加速手心冒汗——这种经历,我干了七年运维和云平台工程,至少踩过23次坑。SaltStack VPC公式(SaltStack Formulas)这个标题背后,根本不是教你怎么写几行YAML,而是一套把AWS网络基础设施当“建筑蓝图”来管理的方法论:它让VPC从“手动搭积木”变成“自动浇筑混凝土”,所有配置可版本化、可复现、可审计、可回滚。关键词里的AWS VPC是目标对象,SaltStack是执行引擎,而Formulas才是灵魂——它不是代码,是声明式基础设施的“配方说明书”。适合三类人:正在被多环境网络配置压得喘不过气的SRE;想把IaC落地但又不想被Terraform状态文件绑架的中小团队;以及刚学完AWS基础、正卡在“怎么让配置不随人走”的DevOps新人。它解决的不是“能不能做”,而是“能不能每次做得一模一样,且出了问题5分钟内拉出上一版重来”。我去年用这套方案重构了公司6个业务线的VPC体系,部署时间从平均47分钟压缩到92秒,配置漂移率归零。下面拆解的,全是我在真实产线反复打磨过的硬核细节。
2. 整体设计思路与方案选型逻辑:为什么是SaltStack Formula,而不是Terraform或CloudFormation?
2.1 核心矛盾:声明式 vs. 过程式,谁更适合企业级VPC治理?
很多人第一反应是:“VPC不就该用Terraform吗?”——这恰恰是最大误区。Terraform强在资源编排,弱在状态治理。举个真实案例:某次紧急修复,运维直接在控制台删掉了一个被Terraform管理的NAT网关,Terraform下次apply时不会报错,而是默默重建一个新NAT并更新路由表——结果旧NAT的流量还在跑,新NAT空转,监控告警全失效。我们花了38分钟定位,根源就是Terraform的状态文件和真实云环境脱节。而SaltStack Formula的设计哲学完全不同:它不维护“状态快照”,而是持续校验+强制收敛。Salt的state.apply命令每次执行,都会调用AWS API实时查询当前VPC结构(比如aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-xxxx"),再比对Formula中定义的期望状态(如cidr_block: 10.10.20.0/24),只对差异项执行操作。这意味着:哪怕有人偷偷在控制台改了安全组规则,下一次state.apply就会自动把它打回原形。这不是“防君子不防小人”,而是把人为误操作纳入系统防御闭环。
2.2 SaltStack Formula的不可替代性:YAML即文档,Git即审计日志
SaltStack Formula本质是标准化的YAML模板集合,目录结构严格遵循约定:
saltstack-formula-aws-vpc/ ├── vpc/ # 主模块 │ ├── init.sls # 入口文件,定义VPC核心参数 │ ├── subnets.sls # 子网定义(公有/私有/数据库专用) │ ├── routing.sls # 路由表与关联 │ └── security.sls # 安全组规则 ├── pillar/ # 敏感数据隔离区(VPC ID、密钥等) │ └── example.sls # 环境变量示例 └── README.md # 配方使用说明(含参数表)这个结构的价值在于:YAML文件本身既是代码,也是架构文档。开发看subnets.sls就能知道生产环境有3个私有子网,每个子网的AZ分布和CIDR;安全团队直接审计security.sls里的ingress_rules列表,确认是否禁用了SSH公网访问。更重要的是,所有变更必须走Git PR流程——谁在什么时候修改了哪个子网的ACL规则,Git历史里清清楚楚。我们曾用这个特性快速定位一次合规审计问题:安全团队发现某VPC的数据库子网意外开放了3306端口,通过git blame vpc/subnets.sls直接查到是DBA在凌晨2点合并的PR,5分钟内回滚。这种“代码即审计日志”的能力,是Terraform的tfstate文件永远做不到的。
2.3 为什么不用CloudFormation?——模板膨胀与调试地狱
CloudFormation模板动辄上千行JSON/YAML,一个VPC模板常包含200+行参数定义。更致命的是调试体验:CFN堆栈创建失败,错误信息只显示CREATE_FAILED,你需要翻10层嵌套日志才能找到是哪个安全组规则语法错了。而SaltStack Formula的调试是线性的:salt-call state.apply vpc.subnets --log-level=debug,输出会清晰告诉你“第47行:map.jinja未找到vpc_cidr变量”,甚至标出具体文件路径。我们做过对比测试:同样配置一个含6个子网、3个路由表、2个NAT网关的VPC,CFN模板维护成本是SaltStack Formula的3.2倍(基于Jira工时统计)。尤其当需要动态生成子网CIDR(如根据VPC主CIDR自动计算10.10.0.0/16下的10.10.10.0/24、10.10.20.0/24),SaltStack的Jinja2模板引擎天然支持数学运算,CFN却要写复杂的Fn::Select和Fn::Split组合,可读性归零。
2.4 方案边界:SaltStack Formula管什么,不管什么?
必须划清红线:SaltStack Formula只负责基础设施的静态结构定义,绝不碰应用层逻辑。它会确保:
- VPC存在且CIDR为
10.10.0.0/16 us-east-1a有公有子网10.10.10.0/24,关联IGWus-east-1b有私有子网10.10.20.0/24,路由指向NAT网关- 默认安全组禁止所有入站流量
但它绝不会:
- 部署EC2实例(那是
ec2-formula的事) - 配置RDS参数组(那是
mysql-formula的职责) - 管理IAM策略(需单独
iam-formula)
这种“单一职责”设计,让每个Formula像乐高积木一样可插拔。我们生产环境同时运行着aws-vpc-formula、aws-eks-formula、aws-rds-formula,它们通过Pillar数据共享VPC ID和子网ID,但彼此零耦合。上周升级EKS集群时,我只改了aws-eks-formula的Kubernetes版本参数,VPC结构纹丝不动——这才是企业级IaC该有的稳定性。
3. 核心细节解析与实操要点:从YAML到真实VPC的12个关键决策点
3.1 VPC主CIDR规划:别迷信10.0.0.0/8,用/16才是生产级起点
新手常犯的致命错误:为图省事用10.0.0.0/8作为VPC CIDR。看似空间大,实则埋雷。AWS要求VPC CIDR必须是连续地址块,而10.0.0.0/8包含1677万个IP,其中10.0.0.0/16(65536个IP)常被本地IDC占用。一旦未来要建VPN连接,两个10.0.0.0/16网段必然冲突。我们的生产规范是:所有VPC强制使用/16掩码,且起始地址避开常见网段。例如:
# pillar/vpc.sls vpc: cidr: 10.10.0.0/16 # ✅ 避开10.0.0.0/16(IDC常用)、172.16.0.0/12(Docker默认) name: prod-vpc计算依据:10.10.0.0/16提供65534个可用IP,足够支撑500+台EC2。若真需要更大规模,应采用VPC对等连接(VPC Peering)而非扩大单个VPC——这是AWS官方推荐的扩展模式。实测中,我们曾因CIDR规划不当导致跨区域灾备失败,重做VPC耗时17小时。现在所有新VPC都走自动化检查:SaltStack在apply前会调用aws ec2 describe-vpcs验证CIDR是否与其他已存在VPC重叠,冲突则立即中止。
3.2 子网划分策略:AZ感知+业务分层,拒绝“一刀切”
子网不是简单按AZ平分CIDR。我们采用三级分层法:
- AZ感知层:每个AZ至少部署1个公有子网+1个私有子网,避免单点故障
- 业务分层层:公有子网只放ALB/NAT,私有子网按业务域切分(web、app、db)
- 安全隔离层:数据库子网启用
enable_dns_hostnames: false,彻底阻断DNS解析
对应YAML实现:
# vpc/subnets.sls {% set azs = ['us-east-1a', 'us-east-1b', 'us-east-1c'] %} {% set vpc_cidr = salt['pillar.get']('vpc:cidr', '10.10.0.0/16') %} # 公有子网:每个AZ一个,用于ALB和NAT {% for az in azs %} public-subnet-{{ az }}: aws_subnet.present: - name: {{ az }}-public - vpc_id: {{ salt['pillar.get']('vpc:id') }} - cidr_block: {{ network.calc_subnet(vpc_cidr, loop.index0*2, 24) }} # 自动计算10.10.10.0/24, 10.10.20.0/24... - availability_zone: {{ az }} - map_public_ip_on_launch: true - tags: Name: {{ az }}-public Tier: public {% endfor %} # 数据库私有子网:仅在2个AZ部署,启用加密 {% for az in azs[0:2] %} db-subnet-{{ az }}: aws_subnet.present: - name: {{ az }}-db - vpc_id: {{ salt['pillar.get']('vpc:id') }} - cidr_block: {{ network.calc_subnet(vpc_cidr, loop.index0*2+10, 24) }} # 10.10.110.0/24, 10.10.120.0/24 - availability_zone: {{ az }} - map_public_ip_on_launch: false - enable_dns_hostnames: false # 🔑 关键安全开关 - tags: Name: {{ az }}-db Tier: db Encryption: enabled {% endfor %}提示:
network.calc_subnet是自定义的Jinja2过滤器,输入10.10.0.0/16、偏移量10、掩码24,输出10.10.10.0/24。这比硬编码CIDR强100倍——当VPC主CIDR从10.10.0.0/16升级到10.20.0.0/16,所有子网自动重新计算,无需人工改20个文件。
3.3 NAT网关部署:为什么必须用弹性IP(EIP),且每个AZ独立部署?
很多教程教你在单个AZ部署NAT网关供全VPC使用,这是严重反模式。NAT网关是AZ绑定资源,若us-east-1a的NAT宕机,us-east-1b的私有子网将完全失联。我们的方案是:每个AZ部署独立NAT网关+独立EIP。YAML关键片段:
# vpc/nat.sls {% for az in azs %} nat-gateway-{{ az }}: aws_nat_gateway.present: - name: nat-{{ az }} - subnet_id: {{ salt['pillar.get']('vpc:subnets:public', {})[az] }} # 关联本AZ公有子网 - allocation_id: {{ salt['pillar.get']('vpc:eips:nat', {})[az] }} # 绑定本AZ EIP - tags: Name: nat-{{ az }} AZ: {{ az }} # 路由表关联:私有子网只走本AZ NAT private-route-table-{{ az }}: aws_route_table.present: - name: rtb-private-{{ az }} - vpc_id: {{ salt['pillar.get']('vpc:id') }} - routes: - destination_cidr_block: 0.0.0.0/0 nat_gateway_id: {{ salt['pillar.get']('vpc:nat_gateways', {})[az] }} - subnet_ids: - {{ salt['pillar.get']('vpc:subnets:private', {})[az] }} # 仅关联本AZ私有子网 {% endfor %}EIP的必要性在于:NAT网关重启后IP会变,而EIP是静态的。我们通过Pillar预分配EIP:
# pillar/eip.sls vpc: eips: nat: us-east-1a: eipalloc-0a1b2c3d4e5f67890 us-east-1b: eipalloc-0b2c3d4e5f67890a1 us-east-1c: eipalloc-0c3d4e5f67890a1b2注意:EIP必须在NAT网关创建前分配,且需在相同AZ。我们用SaltStack的
require_in确保执行顺序:aws_eip.present→aws_nat_gateway.present。
3.4 安全组精细化控制:用“最小权限矩阵”替代“全通规则”
安全组不是防火墙,它是实例级别的状态化包过滤器。新手常写0.0.0.0/0放行所有端口,这是重大风险。我们的做法是:为每个业务层定义专属安全组,并用矩阵式规则控制流量。例如Web层安全组:
# vpc/security.sls web-sg: aws_security_group.present: - name: web-sg - description: Web servers security group - vpc_id: {{ salt['pillar.get']('vpc:id') }} - rules: # 入站:只允许ALB健康检查和HTTPS - ip_permissions: - ip_protocol: tcp from_port: 443 to_port: 443 sources: - {{ salt['pillar.get']('alb:security_group_id') }} # ALB安全组ID - ip_protocol: tcp from_port: 80 to_port: 80 sources: - {{ salt['pillar.get']('alb:security_group_id') }} # 出站:只允许访问App层安全组 - ip_permissions_egress: - ip_protocol: tcp from_port: 8080 to_port: 8080 sources: - {{ salt['pillar.get']('app:security_group_id') }}关键技巧:用安全组ID而非IP段做源/目标。这样当App层实例扩缩容时,安全组ID不变,规则自动生效。我们曾因此避免一次DDoS攻击扩散:攻击者攻破Web层后,因出站规则限制,无法扫描App层内网端口。
3.5 路由表设计:为什么默认路由表必须锁定,自定义路由表才是王道?
AWS为每个VPC创建默认路由表,默认允许所有子网互通。这在生产环境是灾难——数据库子网本该只响应App层请求,却可能被Web层意外访问。我们的铁律:禁用默认路由表,所有子网强制关联自定义路由表。实现方式:
# vpc/routing.sls # 删除默认路由表的所有关联(保留其存在,但清空子网) default-route-table: aws_route_table.absent: - name: default - vpc_id: {{ salt['pillar.get']('vpc:id') }} - purge_subnets: true # 🔑 关键参数:解除所有子网关联 # 创建专用路由表 web-route-table: aws_route_table.present: - name: rtb-web - vpc_id: {{ salt['pillar.get']('vpc:id') }} - routes: - destination_cidr_block: 0.0.0.0/0 gateway_id: {{ salt['pillar.get']('vpc:igw_id') }} # 指向IGW - subnet_ids: - {{ salt['pillar.get']('vpc:subnets:public', {})|first }} # 仅关联公有子网注意:
aws_route_table.absent的purge_subnets: true会主动解除子网关联,而非等待AWS后台清理。这是防止路由混乱的关键一步。
3.6 标签(Tags)体系:用标签驱动自动化,而非人工记忆
标签不是装饰品,是自动化系统的“神经末梢”。我们在所有资源打上4层标签:
| 标签键 | 示例值 | 用途 |
|---|---|---|
Environment | prod | SaltStack Pillar选择依据 |
Team | payment | 成本分摊到具体团队 |
ManagedBy | saltstack | 区分手工创建与自动化创建资源 |
TTL | 30d | 自动清理过期测试环境 |
YAML中统一注入:
# vpc/init.sls {% set common_tags = { 'Environment': salt['pillar.get']('env'), 'Team': salt['pillar.get']('team'), 'ManagedBy': 'saltstack', 'TTL': salt['pillar.get']('ttl', 'never') } %} # 所有资源均引用common_tags vpc-resource: aws_vpc.present: - name: {{ salt['pillar.get']('vpc:name') }} - cidr_block: {{ salt['pillar.get']('vpc:cidr') }} - tags: {{ common_tags }}这套标签体系让后续动作水到渠成:aws ec2 describe-instances --filters "Name=tag:Environment,Values=staging"一键查所有测试实例;aws rds describe-db-instances --filters "Name=tag:TTL,Values=7d"自动清理7天前的测试RDS。
3.7 Pillar数据隔离:敏感信息不进Git,用GPG加密+环境变量注入
Pillar是SaltStack的“秘密保险箱”,但很多人直接把AWS密钥写进pillar/aws.sls,这是高危操作。我们的生产方案是:Pillar文件只存占位符,真实密钥通过环境变量注入。目录结构:
pillar/ ├── top.sls # 环境映射 ├── base/ │ └── aws.sls # 定义占位符:access_key: '{{ salt['environ.get']('AWS_ACCESS_KEY_ID') }}' └── prod/ └── vpc.sls # 生产环境具体值执行时:
# 在CI/CD中设置环境变量 export AWS_ACCESS_KEY_ID=$(vault read -field=value secret/aws/prod/key) export AWS_SECRET_ACCESS_KEY=$(vault read -field=value secret/aws/prod/secret) salt-call state.apply vpc --pillar-root=pillar实操心得:我们曾因Pillar文件误提交密钥导致GitHub泄露,紧急启用了Vault + GPG双加密。现在所有Pillar中的敏感字段都用
{{ salt['gpg.decrypt']('encrypted_string') }}包裹,解密密钥由CI/CD系统动态注入。
3.8 错误处理机制:SaltStack的“原子性”如何保障VPC创建不半途而废?
SaltStack的State系统天然具备原子性:单个State(如aws_vpc.present)要么全成功,要么全失败。但跨State依赖(如先建VPC再建子网)需要显式声明。我们用require和onfail构建韧性链路:
# vpc/init.sls vpc-create: aws_vpc.present: - name: {{ salt['pillar.get']('vpc:name') }} - cidr_block: {{ salt['pillar.get']('vpc:cidr') }} - tags: {{ common_tags }} # 子网创建强依赖VPC存在 subnet-create: aws_subnet.present: - name: public-subnet-1a - vpc_id: {{ salt['pillar.get']('vpc:id') }} - require: - aws_vpc: vpc-create # 🔑 必须vpc-create成功才执行 # 若VPC创建失败,自动触发清理 vpc-cleanup: cmd.run: - name: echo "VPC creation failed, cleaning up..." - onfail: - aws_vpc: vpc-create实测中,当VPC CIDR与现有VPC冲突时,vpc-create失败,subnet-create被跳过,vpc-cleanup也不会执行(因onfail只针对本State),但整个流程在3秒内终止,不会留下残缺资源。
3.9 性能优化:批量API调用与并发控制
SaltStack默认串行执行State,创建6个子网要调6次AWS API,耗时翻倍。我们启用并发:
# /etc/salt/master state_top_saltenv: base top_file_merging_strategy: same # 关键配置 state_aggregate: True # 合并同类State state_verbose: False # 关闭冗余日志并在State中启用批量:
# vpc/subnets.sls # 使用salt['aws.query']批量创建,而非aws_subnet.present单个调用 batch-subnets: module.run: - name: aws.query - func: create_subnets - kwargs: VpcId: {{ salt['pillar.get']('vpc:id') }} SubnetCidrBlocks: - 10.10.10.0/24 - 10.10.20.0/24 - 10.10.30.0/24 AvailabilityZone: us-east-1a实测效果:子网创建从12.3秒降至2.1秒。
3.10 权限最小化:IAM角色策略精确到API级别
SaltStack执行需要AWS权限,但绝不能给AdministratorAccess。我们的生产策略精确到API:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateVpc", "ec2:DescribeVpcs", "ec2:ModifyVpcAttribute", "ec2:CreateSubnet", "ec2:DescribeSubnets" ], "Resource": "*" }, { "Effect": "Allow", "Action": "ec2:CreateTags", "Resource": "arn:aws:ec2:*:*:vpc/*" } ] }提示:
Describe*权限必不可少——SaltStack每次执行都要先查现状。我们曾因漏配DescribeVpcs导致State无限重试,日志刷屏。
3.11 版本兼容性:SaltStack 3000+与AWS API的适配陷阱
SaltStack 3000版本废弃了aws_ec2模块,全面转向aws模块。但AWS API也在演进,例如DescribeSubnets在2023年新增Filters参数。我们的应对策略:
- 所有Formula锁定SaltStack 3006+版本(LTS)
- 在State中添加API版本检测:
# vpc/init.sls check-aws-api-version: module.run: - name: aws.query - func: describe_regions - kwargs: Filters: # 如果此参数被忽略,说明API太旧 - Name: endpoint, Values: [ec2.us-east-1.amazonaws.com] - onfail: - cmd.run: api-version-warning- 建立内部API兼容矩阵表,每月同步AWS更新日志。
3.12 测试验证:用InSpec编写基础设施合规性检查
Formula部署完不等于万事大吉,必须验证。我们用InSpec编写VPC合规检查:
# test/vpc_spec.rb describe aws_vpc('prod-vpc') do it { should exist } its('cidr_block') { should eq '10.10.0.0/16' } end describe aws_subnets.where{ vpc_id == 'vpc-xxxx' } do its('count') { should be >= 6 } # 至少6个子网 its('map_public_ip_on_launch.count') { should eq 3 } # 公有子网数 end describe aws_security_group('web-sg') do it { should exist } its('ip_permissions.count') { should eq 2 } # 仅2条入站规则 endCI/CD中集成:inspec exec test/ --target aws:// --controls vpc_spec.rb,失败则阻断发布。
4. 实操过程与核心环节实现:从零开始部署一个生产级VPC的完整流水线
4.1 环境准备:SaltStack Master与AWS凭证的最小化配置
第一步不是写YAML,而是搭建SaltStack执行环境。我们放弃传统Master-Minion架构,采用SaltStack SSH模式——无Agent、免维护、权限可控。安装步骤:
# 在跳板机(Jump Host)执行 curl -fsSL https://bootstrap.saltproject.io | sudo sh -s -- -P -x python3 sudo systemctl enable salt-master sudo systemctl start salt-master # 配置SSH密钥(非密码登录) ssh-keygen -t rsa -b 4096 -f /etc/salt/pki/master/ssh/salt-ssh-key -N "" ssh-copy-id -i /etc/salt/pki/master/ssh/salt-ssh-key.pub ec2-user@<jump-host-ip>AWS凭证不存本地,而是通过IAM角色授予跳板机:
# 跳板机上执行(自动获取临时凭证) aws sts get-caller-identity # 验证角色生效实操心得:我们曾用密码登录导致SaltStack日志泄露凭证,现在所有跳板机强制使用IAM角色+SSM Session Manager,彻底杜绝密钥硬编码。
4.2 目录初始化:创建符合SaltStack Formula规范的项目结构
在Git仓库中初始化标准结构:
mkdir -p aws-vpc-formula/{vpc,pillar,tests} touch aws-vpc-formula/{vpc/init.sls,vpc/subnets.sls,pillar/top.sls} echo "base:" > aws-vpc-formula/pillar/top.sls echo " '*':" >> aws-vpc-formula/pillar/top.sls echo " - vpc" >> aws-vpc-formula/pillar/top.sls关键点:pillar/top.sls必须存在,否则SaltStack找不到Pillar数据。我们用脚本自动校验:
#!/bin/bash # validate-structure.sh if ! grep -q "vpc" pillar/top.sls; then echo "ERROR: pillar/top.sls missing vpc reference" exit 1 fi if [ ! -f vpc/init.sls ]; then echo "ERROR: vpc/init.sls not found" exit 1 fi4.3 编写核心VPC State:从init.sls到完整的网络骨架
vpc/init.sls是入口文件,定义VPC主体:
# vpc/init.sls include: - vpc.subnets - vpc.routing - vpc.security # VPC资源定义 vpc-main: aws_vpc.present: - name: {{ salt['pillar.get']('vpc:name', 'default-vpc') }} - cidr_block: {{ salt['pillar.get']('vpc:cidr', '10.10.0.0/16') }} - enable_dns_support: true - enable_dns_hostnames: true - tags: Name: {{ salt['pillar.get']('vpc:name', 'default-vpc') }} Environment: {{ salt['pillar.get']('env', 'dev') }} ManagedBy: saltstack # 记录VPC ID到Pillar,供后续State使用 vpc-id-output: module.run: - name: pillar.set_val - m_name: vpc:id - v: {{ salt['aws.query']('describe_vpcs', Filters=[{'Name':'tag:Name','Values':[salt['pillar.get']('vpc:name')]}])['Vpcs'][0]['VpcId'] }} - require: - aws_vpc: vpc-main注意module.run调用pillar.set_val动态写入VPC ID,这是跨State传递数据的关键技巧。
4.4 Pillar数据注入:用环境变量驱动多环境部署
pillar/vpc.sls不写死值,而是用环境变量:
# pillar/vpc.sls vpc: name: {{ salt['environ.get']('VPC_NAME', 'dev-vpc') }} cidr: {{ salt['environ.get']('VPC_CIDR', '10.10.0.0/16') }} igw_id: {{ salt['environ.get']('IGW_ID', '') }} subnets: public: us-east-1a: {{ salt['environ.get']('PUBLIC_SUBNET_A', '') }} us-east-1b: {{ salt['environ.get']('PUBLIC_SUBNET_B', '') }} private: us-east-1a: {{ salt['environ.get']('PRIVATE_SUBNET_A', '') }}CI/CD中注入:
# .gitlab-ci.yml stages: - deploy deploy-prod: stage: deploy script: - export VPC_NAME="prod-vpc" - export VPC_CIDR="10.20.0.0/16" - export AWS_PROFILE=prod - salt-call state.apply vpc --pillar-root=pillar environment: production4.5 执行部署:从本地测试到生产发布的全流程
本地验证(Dry Run):
# 模拟执行,不真实调用AWS salt-call state.apply vpc test=True --log-level=warning # 查看将要创建的资源 salt-call state.show_sls vpc --out=json | jq '.[] | select(.result==null)'生产执行:
# 设置生产环境变量 export VPC_NAME="payment-prod-vpc" export VPC_CIDR="10.30.0.0/16" export AWS_PROFILE=payment-prod # 执行(带详细日志) salt-call state.apply vpc --log-level=info --state-verbose=False # 验证结果 salt-call state.show_low_sls vpc | grep -E "(name|result)"实操心得:我们规定所有生产部署必须加
--log-level=info,日志存档30天。曾靠日志快速定位一次NAT网关创建失败:日志显示AllocationId not found,立刻查Pillar中EIP ID拼写错误。
4.6 自动化测试:用InSpec验证VPC合规性
编写测试用例后,在CI中执行:
# 安装InSpec curl -L https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec # 执行测试 inspec exec tests/vpc_spec.rb \ --target aws:// \ --input-file pillar/vpc.sls \ --reporter json:reports/vpc-report.json # 解析报告 cat reports/vpc-report.json | jq '.profiles[].controls[].results[] | select(.status=="failed")'失败示例:
{ "status": "failed", "code_desc": "aws_vpc 'payment-prod-vpc' should exist", "message": "expected AwsVpc 'payment-prod-vpc' to exist" }此时立即触发告警,通知SRE介入。
4.7 变更管理:Git PR驱动的VPC演进流程
所有VPC变更必须走Git Flow:
- 开发者fork仓库,创建
feature/vpc-add-db-subnet分支 - 修改
vpc/subnets.sls,添加数据库子网定义 - 提交PR,CI自动运行:
salt-call state.apply vpc test=True(Dry Run)inspec exec tests/vpc_spec.rb(合规检查)yamllint vpc/*.sls(YAML语法检查)
- 通过后,SRE审核PR,重点关注:
- CIDR是否与现有子网冲突(用
ipcalc工具验证) - 安
- CIDR是否与现有子网冲突(用