Terraform自动化部署SSM就绪的EC2实例
1. 项目概述:为什么一个能自动装好SSM代理的EC2实例,值得你花两小时认真读完
我第一次在AWS控制台里点出第17个EC2实例时,手已经有点抖了。不是因为紧张,是因为重复劳动带来的生理疲惫——选AMI、挑区域、配安全组、贴标签、挂IAM角色、再手动连上去跑一遍yum install amazon-ssm-agent……结果发现安全组少开了一个端口,回退重来;或者忘了给实例打上Environment=dev标签,导致第二天运维同事在资源治理平台里找不到它;最绝的一次是,我在法兰克福区创建的实例,被另一个同事误以为在东京区,他直接在控制台里删掉了“不存在”的资源,而我的Terraform状态文件还躺在本地硬盘上,没人知道发生了什么。
这种场景,你肯定不陌生。它不是“偶尔手滑”,而是手工操作基础设施的必然宿命:不可复现、不可审计、不可协作、不可回滚。你写的每一条aws ec2 run-instances命令,都像在沙子上刻字;你点下的每一个“Launch Instance”按钮,都在为下一次故障埋雷。而Terraform要做的,不是让你多学一个工具,而是帮你把“基础设施”从一种临时的、易变的操作行为,变成一种可版本化、可审查、可测试、可交付的工程产物。
这篇教程聚焦一个非常具体、非常高频、也非常有代表性的任务:用Terraform自动化部署一台预装SSM(Systems Manager)代理的EC2实例。它看起来只是“多装一个agent”,但背后是一整套现代云基础设施交付范式的落地切口。你将亲手写出能立刻跑通的代码,而不是看一堆概念图;你会理解为什么user_data里那几行bash脚本必须这么写,而不是复制粘贴就完事;你会搞懂terraform.tfstate文件到底存了什么、为什么不能放本地、S3锁文件是怎么防止两人同时apply把环境搞崩的;你还会把硬编码的t2.micro和eu-central-1抽成变量,再进一步打包成模块,为后续部署10台、100台同类服务器铺平道路。
它适合三类人:刚考完AWS Cloud Practitioner、想把知识落地的新人;已经在用AWS但还在控制台里“点点点”的中级工程师;以及团队里正被“环境不一致”问题折磨得夜不能寐的技术负责人。不需要你精通Go或Python,只需要你愿意把鼠标从AWS控制台挪开5分钟,打开终端,敲下terraform init。接下来的内容,没有一句是“理论上可以”,全是我在生产环境踩过坑、改过三次配置、重试过七遍才确认下来的实操细节。
2. 整体设计思路:为什么是这个结构?为什么不是Ansible或CloudFormation?
2.1 核心目标拆解:我们到底要“自动化”什么?
很多人一上来就想“我要用Terraform管整个VPC”,结果三天后卡在子网路由表关联上,信心全无。真正的起点,永远是一个最小、可验证、有明确业务价值的闭环。本项目的目标非常聚焦:
- 交付一个EC2实例:这是AWS最基础、最通用的计算单元,所有上层服务(数据库、缓存、应用服务器)都跑在它上面。
- 该实例必须能被SSM管理:这意味着无需SSH密钥、无需开放22端口、无需维护跳板机,就能执行命令、推送脚本、查看日志、安装补丁——这是现代云运维的“黄金标准”。
- 整个过程必须100%代码化、零人工干预:从创建IAM角色到启动实例,全部由Terraform驱动,且每次
apply的结果完全一致。
这三个目标环环相扣。如果只部署EC2但没SSM,你还是得登录;如果有了SSM但依赖手动创建IAM角色,那“自动化”就打了折扣;如果每次部署都要改region、改instance_type,那它就无法复用。所以我们的架构设计,必须围绕这三点展开,而不是堆砌功能。
2.2 方案选型:为什么是Terraform,而不是其他工具?
市面上能干这事的工具不少,但选择Terraform不是因为它“流行”,而是它在几个关键维度上给出了最优解:
声明式 vs. 命令式:Ansible是命令式(“先做A,再做B,最后检查C”),它擅长配置操作系统,但对“创建一个带特定策略的IAM角色”这种云原生资源,语法冗长且易出错。Terraform是声明式(“我要一个东西,它长这样”),你只描述终态,它自己算依赖、排顺序、处理失败。比如,你定义了
aws_instance依赖aws_iam_role,Terraform会自动确保角色创建成功后才启动实例,失败时能精准告诉你卡在哪一步。我试过用Ansible的aws_ec2模块做同样事,光是IAM角色的信任策略JSON格式就调了40分钟。状态管理能力:CloudFormation也能声明式部署,但它把状态牢牢锁死在AWS账户里,你无法用Git管理变更历史,也无法做跨账户同步。Terraform的
tfstate文件是纯文本(JSON格式),你可以把它放进GitLab仓库,设置分支保护规则,要求所有apply前必须经过Code Review。去年我们一个客户就靠这个功能,在一次误删生产RDS的事故中,5分钟内从Git历史里找回了旧状态并terraform apply回滚。模块化与复用性:CloudFormation的嵌套栈(Nested Stack)配置复杂,调试困难;Ansible的Role虽然可复用,但缺乏强类型校验。Terraform的模块(Module)是语言级特性,支持输入变量类型约束(
type = string)、默认值、描述文档,还能在Registry里直接引用HashiCorp官方或社区维护的成熟模块(比如terraform-aws-modules/vpc/aws)。我们后面会把EC2+SSM打包成模块,你以后要部署K8s节点、CI/CD构建机、甚至GPU训练实例,只要换掉user_data里的安装命令,其余代码一行不用改。Provider生态与AWS深度集成:Terraform AWS Provider目前支持超过300个AWS服务资源,且更新速度紧跟AWS新发布(比如2023年推出的EC2 Image Builder,Provider在两周内就提供了
aws_imagebuilder_component资源)。更重要的是,它对AWS特有的概念有原生支持:data "aws_ami"能跨Region查官方AMI,aws_iam_policy_document能用HCL生成合规的JSON策略,aws_ssm_parameter能无缝对接Parameter Store——这些都不是“能用”,而是“用起来比AWS CLI还顺手”。
提示:不要陷入“工具之争”。Ansible在OS层配置上依然无敌,CloudFormation在纯AWS、强合规场景下有其优势。Terraform的核心价值,是成为你云基础设施的“统一API层”——上接CI/CD,下管多云,中间串起所有配置工具。把它当成你的“基础设施操作系统”,而不是一个“又一个CLI”。
2.3 架构分层:为什么坚持main/variables/outputs三分法?
新手常犯的错误,是把所有东西都塞进一个main.tf里。我见过最夸张的案例,一个500行的main.tf文件里混着provider配置、AMI查询、EC2定义、安全组、输出,还有硬编码的密码。这种代码,别说协作,连自己三天后都看不懂。
我们采用经典的三层分离,不是为了“看起来专业”,而是解决三个真实痛点:
variables.tf:隔离变化点
环境(dev/staging/prod)、区域(us-east-1/eu-west-1)、规格(t2.micro/t3.medium)这些参数,是系统中最容易变、也最需要被不同角色控制的部分。开发人员改terraform-dev.tfvars,运维人员管terraform-prod.tfvars,安全团队审核variables.tf里的description字段是否准确。把它们集中管理,等于给系统装上了“可调节旋钮”,而不是每次都要动手术刀。main.tf:专注资源编排逻辑
这里只放“怎么做”:哪些资源要创建、它们之间如何关联、数据源如何查询。它应该像一份清晰的乐谱,告诉Terraform“这段旋律需要小提琴(EC2)、大提琴(IAM Role)和定音鼓(SSM Policy)一起演奏”。去掉所有硬编码值,只留var.xxx占位符,让逻辑本身变得纯粹、可测试、可推理。outputs.tf:定义交付物契约terraform apply完成后,你真正需要的是什么?是实例ID?是公网IP?是SSM会话连接字符串?outputs.tf就是这份“交付清单”。它强制你思考:“我的这个模块,对外承诺提供哪些信息?” 这些输出值,会被上层模块或CI/CD流水线直接引用。比如,你的部署脚本可以用$(terraform output -raw instance_id)获取ID,然后调用aws ssm start-session建立连接——整个流程无需人工抄写。
这种分层,本质上是在模拟软件工程的最佳实践:高内聚、低耦合。它让代码具备了“可演进性”。当业务需要增加CloudWatch日志组时,你只需在main.tf里加一个aws_cloudwatch_log_group资源,在outputs.tf里加一个log_group_name输出,variables.tf里加一个log_retention_days变量——改动范围清晰,风险可控。
3. 核心细节解析:从AMI查询到SSM代理安装的每一处陷阱
3.1 AMI数据源:为什么不能写死ami-0abcdef1234567890?
在AWS里,AMI(Amazon Machine Image)ID是Region级的。同一个Amazon Linux 2 AMI,在us-east-1的ID是ami-0c55b159cbfafe1f0,在eu-central-1可能是ami-0a313d60987694e48。如果你在main.tf里硬编码一个ID,代码就彻底失去了跨Region能力,更别提复用。
Terraform的data "aws_ami"就是为解决这个问题而生。但它的配置远不止“查最新版”这么简单,这里有三个极易被忽略的关键点:
most_recent = true的真实含义
它不是“找发布时间最新的AMI”,而是“在满足所有filter条件的AMI中,选creation_date字段值最大的那个”。而Amazon官方AMI的creation_date,是镜像构建完成的时间,不是上传时间。这意味着,即使你昨天刚用Packer构建了一个自定义AMI,只要它的creation_date比官方AMI老,most_recent = true还是会选中官方镜像。所以,永远不要依赖most_recent来获取你自己的AMI。对于自定义AMI,必须用name或tags精确匹配。filter的组合逻辑是AND,不是OR
你写了两个filter块,Terraform会把它们当作“同时满足”的条件。比如:filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] } filter { name = "virtualization-type" values = ["hvm"] }这等价于SQL里的
WHERE name LIKE 'amzn2-ami-hvm%-x86_64-gp2' AND virtualization-type = 'hvm'。如果漏掉virtualization-type,你可能会意外匹配到旧的PV(Paravirtual)类型AMI,而t2.micro等现代实例类型只支持HVM,会导致启动失败。owners = ["amazon"]是安全底线
AMI可以由任何人创建并公开分享。如果不指定owners,data块可能返回社区用户上传的、包含挖矿木马的恶意AMI。["amazon"]强制限定只搜索AWS官方发布的镜像。生产环境必须加上这一行,这是基础设施安全的第一道门禁。
实操心得:我曾经在一个客户环境里,因为忘记加owners = ["amazon"],terraform plan返回了一个ID为ami-0123456789abcdef0的AMI,apply后实例启动失败。terraform show显示ami值是空的,但错误日志里只有一句模糊的InvalidAMIID.NotFound。花了两小时排查,才发现这个AMI属于一个叫public-images的第三方账户,早已被删除。从此,我的所有data "aws_ami"都加了owners,并养成了在plan输出里第一眼就核对ami值是否以ami-开头的习惯。
3.2 IAM角色与实例配置文件:权限最小化的实战推演
让EC2实例能被SSM管理,核心是赋予它AmazonSSMManagedInstanceCore托管策略。但直接把策略绑给实例,是严重违反最小权限原则的。正确的路径是:IAM Role → Instance Profile → EC2 Instance。这个链条里,每个环节都有讲究:
Role的
assume_role_policy必须精确匹配EC2服务主体
很多人复制网上的例子,把Principal.Service写成"ec2.amazonaws.com",这在大多数Region是OK的,但在cn-north-1(中国宁夏)或us-gov-west-1(AWS GovCloud)等特殊Region,服务主体名是"ec2.amazonaws.com.cn"或"ec2.us-gov.amazonaws.com"。硬编码会导致terraform apply失败。最佳实践是使用data "aws_partition"动态获取:data "aws_partition" "current" {} resource "aws_iam_role" "ssm_role" { name = "ec2-ssm-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.${data.aws_partition.current.dns_suffix}" } }] }) }aws_partition数据源会根据当前Provider配置的Region,自动返回"aws"、"aws-cn"或"aws-us-gov",dns_suffix则给出对应的域名后缀。这保证了代码在全球所有AWS Region都能开箱即用。Instance Profile是Role的“包装纸”,必须显式创建
你不能直接在aws_instance资源里写iam_role = aws_iam_role.ssm_role.name。AWS API要求,EC2实例绑定的必须是一个InstanceProfile,而InstanceProfile内部再关联Role。Terraform的aws_iam_instance_profile资源就是干这个的。漏掉这一步,apply会报错InvalidParameterValue: Value (ec2-ssm-role) for parameter iamInstanceProfile.name is invalid。iam_instance_profile字段名易混淆
在aws_instance资源里,绑定实例配置文件的字段叫iam_instance_profile,不是iam_role或instance_profile。这是一个历史遗留的命名,很多新手在这里栽跟头。记住口诀:“实例Profile,不是Role”。
注意:
AmazonSSMManagedInstanceCore策略本身已足够精简,它只授予SSM Agent连接、发送心跳、执行命令等必要权限。但如果你的应用还需要访问S3日志桶或DynamoDB配置表,不要把那些权限也加到这个Role里。应该为应用创建独立的Role,通过aws_iam_role_policy_attachment附加,并在user_data里用aws configure切换凭证——让SSM管理权和应用运行权彻底分离。
3.3 user_data脚本:为什么用sudo yum install -y而不是curl | bash?
user_data是EC2启动时执行的初始化脚本,它是整个自动化链条里最脆弱的一环。任何网络超时、包管理器锁、权限错误,都会导致SSM Agent安装失败,而实例本身却显示“Running”。你只能干瞪眼,等着aws ssm describe-instance-information返回空结果。
我们选择yum install而非curl | bash,基于三个硬核理由:
可审计性与确定性
https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm这个URL指向的是“latest”版本。今天下载的是3.2.1234.0,明天AWS发布新版,它就变成3.2.1235.0。curl | bash会静默升级,可能引入不兼容变更。而.rpm包是确定性产物,你可以用yum list installed | grep amazon-ssm-agent精确验证版本。生产环境必须锁定版本,latest是魔鬼。离线安装能力
yum install会把RPM包下载到/var/cache/yum,下次重装同一版本时直接复用,不依赖外网。而curl必须实时下载,一旦S3桶临时不可达(极罕见但可能),安装就失败。我们曾在一个金融客户内网环境里,因防火墙策略收紧,curl被拦截,yum install却畅通无阻。错误处理与日志沉淀
yum install -y的退出码是标准的:0成功,1失败。你可以在脚本末尾加echo $?记录到/var/log/user-data.log。而curl | bash的错误流完全不可控,bash可能在curl还没结束时就开始执行半截脚本,导致难以诊断。
实操心得:我建议你在user_data里加入完整的错误捕获和日志:
#!/bin/bash exec > /var/log/user-data.log 2>&1 set -xeu cd /tmp echo "[$(date)] Starting SSM Agent installation..." >> /var/log/user-data.log # 下载RPM包,带重试 for i in {1..3}; do if curl -f -s -o amazon-ssm-agent.rpm https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm; then echo "[$(date)] RPM download succeeded on attempt $i" >> /var/log/user-data.log break else echo "[$(date)] RPM download failed on attempt $i, retrying..." >> /var/log/user-data.log sleep 5 fi done # 安装并启用 yum install -y ./amazon-ssm-agent.rpm systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent echo "[$(date)] SSM Agent installation completed successfully." >> /var/log/user-data.logset -xeu确保任何命令失败立即退出;exec > /var/log/user-data.log把所有输出重定向到日志;for循环实现下载重试。这些细节,决定了你是在“部署一个实例”,还是在“部署一个可运维的实例”。
4. 实操全流程:从零开始,一步步敲出可运行的代码
4.1 环境准备:CLI安装与凭证配置的避坑指南
4.1.1 Terraform安装:版本选择的血泪教训
Terraform的版本迭代很快,但并非越新越好。截至2024年,强烈推荐使用v1.5.x系列(如1.5.7),原因如下:
v1.6+ 引入了新的
required_providers语法强化,要求所有Provider必须在required_providers块中明确定义source和version,否则init失败。而大量线上教程、模块、甚至HashiCorp官方文档,仍以v1.5为基准。如果你用v1.6+,会频繁遇到Provider "registry.terraform.io/hashicorp/aws" v6.0.0 is not compatible with Terraform 1.6.0这类报错。v1.4及更早版本缺少
use_lockfile = true的原生S3锁支持,你必须额外配置DynamoDB,徒增复杂度。
安装步骤(以macOS为例):
# 卸载旧版本(如果存在) brew uninstall terraform # 添加HashiCorp官方tap(避免用homebrew-core里可能滞后的版本) brew tap hashicorp/tap # 安装v1.5.7(指定版本号,杜绝意外升级) brew install hashicorp/tap/terraform@1.5 # 创建软链接,让`terraform`命令指向v1.5 brew link --force terraform@1.5 # 验证 terraform version # 输出应为:Terraform v1.5.7 # on darwin_arm64提示:Windows用户请务必从 HashiCorp官网下载v1.5.7的zip包 ,解压后将
terraform.exe所在目录加入PATH。不要用choco install terraform,Chocolatey默认安装最新版,且无法指定历史版本。
4.1.2 AWS凭证:三种方式的安全等级排序
凭证配置是安全红线,必须严格遵循最小权限。以下是三种方式的安全等级(从高到低):
IAM Role for EC2(最高安全)
适用于Terraform代码运行在EC2实例上的场景(如CI/CD Runner)。你为该EC2实例附加一个IAM Role,Role拥有AdministratorAccess或精细化权限。Terraform自动从实例元数据服务(IMDS)获取临时凭证,无需存储长期密钥。这是生产环境首选。AWS Credentials File(开发/测试推荐)
创建~/.aws/credentials文件:[default] aws_access_key_id = AKIAIOSFODNN7EXAMPLE aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY关键禁忌:这个文件绝对不能提交到Git!必须加入
.gitignore。且IAM用户必须是专门创建的,绝不使用Root账号。权限策略应限制为仅允许操作本项目所需资源(如ec2:*,iam:CreateRole,iam:AttachRolePolicy等),而非AdministratorAccess。Environment Variables(临时调试用)
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY export AWS_DEFAULT_REGION=eu-central-1优点是灵活,缺点是容易泄露(
ps aux可见进程环境变量)。严禁在生产脚本或CI/CD中使用。
实操心得:我给自己设了一条铁律——本地开发用Credentials File,CI/CD用IAM Role。并且,我创建了一个名为terraform-deployer的IAM用户,其策略精确到:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:RunInstances", "ec2:DescribeImages", "ec2:DescribeSecurityGroups", "ec2:CreateTags", "iam:CreateRole", "iam:AttachRolePolicy", "iam:CreateInstanceProfile", "iam:AddRoleToInstanceProfile", "s3:GetObject", "s3:ListBucket" ], "Resource": "*" } ] }这个策略只允许创建本教程所需的资源,连ec2:TerminateInstances都没有,最大程度降低误操作风险。
4.2 项目初始化:创建标准目录结构与基础文件
在终端中执行以下命令,创建项目骨架:
mkdir -p aws-terraform/modules/ec2-with-ssm cd aws-terraform # 创建根目录文件 touch main.tf variables.tf outputs.tf # 创建模块目录文件 touch modules/ec2-with-ssm/{main.tf,variables.tf,outputs.tf} # 初始化Git(强烈建议,状态文件必须版本化) git init echo "*.tfstate" >> .gitignore echo ".terraform/" >> .gitignore echo "terraform.tfstate.backup" >> .gitignore git add .gitignore git commit -m "chore: init git repo, ignore state files"现在,你的目录结构是:
aws-terraform/ ├── main.tf ├── variables.tf ├── outputs.tf └── modules/ └── ec2-with-ssm/ ├── main.tf ├── variables.tf └── outputs.tf4.2.1 编写variables.tf:定义可配置的“旋钮”
在variables.tf中,填入以下内容。注意description字段,它会在terraform plan时作为注释显示,是给协作者最好的文档:
# variables.tf variable "aws_region" { description = "AWS region where resources will be created. Must match the S3 backend region." type = string default = "eu-central-1" } variable "instance_type" { description = "EC2 instance type (e.g., t2.micro, t3.medium)." type = string default = "t2.micro" } variable "environment" { description = "Environment name (e.g., dev, staging, prod). Used for tagging." type = string default = "dev" } variable "key_name" { description = "Name of the EC2 Key Pair to enable SSH access (optional, leave empty if not needed)." type = string default = "" } variable "enable_ssm" { description = "Whether to install and configure SSM Agent on the instance." type = bool default = true }4.2.2 编写main.tf:根模块的Provider与Backend配置
main.tf是项目的入口。这里只放全局配置,不放具体资源:
# main.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" # 锁定5.x系列,兼容v1.5.x Terraform } } # S3 Backend配置 - 生产环境必须启用 backend "s3" { bucket = "your-unique-bucket-name-here" # 替换为你自己的S3桶名 key = "terraform.tfstate" region = "eu-central-1" # 必须与aws_region变量值一致 dynamodb_table = "terraform-state-lock" # 旧版锁表,v1.5+可删,但保留兼容 encrypt = true } } # AWS Provider配置 provider "aws" { region = var.aws_region } # 模块调用 module "web_server" { source = "./modules/ec2-with-ssm" aws_region = var.aws_region instance_type = var.instance_type environment = var.environment key_name = var.key_name enable_ssm = var.enable_ssm }注意:
bucket字段必须是你自己创建的S3桶名,且该桶必须存在、已启用版本控制。region必须与aws_region变量值严格一致,否则init会失败。
4.2.3 编写模块modules/ec2-with-ssm/main.tf:核心资源定义
这是真正的“干活”文件。将之前讨论的所有最佳实践融入其中:
# modules/ec2-with-ssm/main.tf # 输入变量(必须与根模块的output保持一致) variable "aws_region" { description = "AWS region" type = string } variable "instance_type" { description = "EC2 instance type" type = string } variable "environment" { description = "Environment tag" type = string } variable "key_name" { description = "EC2 Key Pair name" type = string default = "" } variable "enable_ssm" { description = "Enable SSM Agent installation" type = bool default = true } # 数据源:动态获取AMI data "aws_partition" "current" {} data "aws_ami" "amazon_linux" { most_recent = true filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] } filter { name = "virtualization-type" values = ["hvm"] } owners = ["amazon"] } # IAM Role for SSM resource "aws_iam_role" "ssm_role" { name = "ec2-ssm-role-${var.environment}" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.${data.aws_partition.current.dns_suffix}" } }] }) } # Attach SSM managed policy resource "aws_iam_role_policy_attachment" "ssm_policy" { role = aws_iam_role.ssm_role.name policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" } # Instance Profile resource "aws_iam_instance_profile" "ssm_profile" { name = "ec2-ssm-profile-${var.environment}" role = aws_iam_role.ssm_role.name } # EC2 Instance resource "aws_instance" "web_server" { ami = data.aws_ami.amazon_linux.id instance_type = var.instance_type key_name = var.key_name # 只有enable_ssm为true时才绑定SSM Profile dynamic "iam_instance_profile" { for_each = var.enable_ssm ? [1] : [] content { name = aws_iam_instance_profile.ssm_profile.name } } # user_data:条件化执行SSM安装 user_data = var.enable_ssm ? <<-EOF #!/bin/bash exec > /var/log/user-data.log 2>&1 set -xeu cd /tmp echo "[$(date)] Starting SSM Agent installation..." for i in {1..3}; do if curl -f -s -o amazon-ssm-agent.rpm https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm; then echo "[$(date)] RPM download succeeded on attempt $i" break else echo "[$(date)] RPM download failed on attempt $i, retrying..." sleep 5 fi done yum install -y ./amazon-ssm-agent.rpm systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent echo "[$(date)] SSM Agent installation completed successfully." EOF : "" tags = { Name = "TerraformWebServer-${var.environment}" Environment = var.environment } }4.2.4 编写modules/ec2-with-ssm/outputs.tf:定义模块输出契约
模块的输出,是它对外提供的“接口”。其他模块或脚本,只应该依赖这些输出,而不应深入main.tf内部:
# modules/ec2-with-ssm/outputs.tf output "instance_id" { description = "The ID of the EC2 instance" value = aws_instance.web_server.id } output "public_ip" { description = "The public IP address of the EC2 instance" value = aws_instance.web_server.public_ip } output "private_ip" { description = "The private IP address of the EC2 instance" value = aws_instance.web_server.private_ip } output "ssm_instance_id" { description = "The SSM-managed instance ID (same as instance_id)" value = aws_instance.web_server.id }4.2.5 编写根outputs.tf:汇总最终交付物
在根目录的outputs.tf中,暴露模块的输出,方便上层调用:
# outputs.tf output "web_server_instance_id" { description = "EC2 instance ID from the web_server module" value = module.web_server.instance_id } output "web_server_public_ip" { description = "Public IP of the web server instance" value = module.web_server.public_ip } output "web_server_ssm_id" { description = "SSM instance ID for session management" value = module.web_server.ssm_instance_id }4.3 执行Terraform工作流:init/plan/apply的完整命令链
一切就绪,进入激动人心的执行阶段。请严格按照顺序操作:
4.3.1 初始化:下载Provider并配置Backend
# 在aws-terraform/目录下执行 terraform init # 首次使用S3 Backend时,会提示: # Do you want to copy existing state to the new backend? # 输入 yes,Terraform会将本地的terraform.tfstate(如果存在)上传到S34.3.2 计划预览:在真实世界前,先看一眼蓝图
# 生成执行计划 terraform plan -var-file="terraform-dev.tfvars" # 如果你还没有terraform-dev.tfvars,先创建它: echo 'aws_region = "eu-central-1"' > terraform-dev.tfvars echo 'instance_type = "t2.micro"' >> terraform-dev.tfvars echo 'environment = "dev"' >> terraform-dev.tfvarsplan输出会详细列出所有将被创建的资源。重点检查:
aws_instance.web_server的ami值是否是一个有效的ami-开头的ID;aws_iam_role.ssm_role的name是否包含dev后缀;aws_iam_instance_profile.ssm_profile是否被创建;user_data是否非空(表示SSM安装逻辑已启用)。
4.3.3 应用部署:让代码变为现实
# 执行部署 terraform apply -var-file="terraform-dev.tfvars" -auto-approve # -auto-approve 跳过交互式确认,适合CI/CD。首次手动执行,可去掉此参数,看到确认提示后再输入yes部署过程约需2-3分钟。成功后,你会看到类似输出
