跳至主要内容

Terraform AWS 教程:在 EC2 上自动部署代理

学习如何用 Terraform 自动化 AWS 基础设施。创建带 SSM 代理的 EC2 实例,在 S3 中管理状态,并通过模块实现扩展。
· 11分钟 阅读

使用 AI 探索

我花了无数个小时在 AWS 控制台里点来点去,手动启动 EC2 实例、配置安全组,最后难免犯错,调试又要耗掉好几个小时。这听起来是否很熟悉?您会在错误的可用区创建实例,忘了给资源打标签,或者因为没有记录步骤,只能花一个下午把上个月搭好的配置一模一样重做一遍。 

手动配置基础设施不仅乏味,还容易出错、难以扩展、无法审计。Terraform 正是在这里改变一切。想象一下,您把整个 AWS 基础设施都定义在可版本化的文本文件中,可以审查并自动部署。再也不用点几十个页面,也不会和团队上演“但它在我机器上能跑”的对话。

在本教程中,我将演示如何使用 Terraform 自动化 AWS 基础设施,聚焦一个真实案例:预置一个带自动引导安装 SSM(Systems Manager)代理的 EC2 实例。读完后,您将理解基础设施即代码的基本原理,并拥有可立即在自己 AWS 账户中部署的可用代码。

如果您是 AWS 新手,建议先学习我们的课程,如 AWS 概念、Python 中的 AWS Boto 入门,或 AWS 安全与成本管理。 

什么是 Terraform?

Terraform 是由 HashiCorp 创建的开源基础设施即代码(IaC)工具,允许您使用 HashiCorp 配置语言(HCL)定义、预置和管理云资源。 

您无需在云控制台中点选或编写命令式 bash 脚本,而是在配置文件中声明所需的基础设施,Terraform 会处理实现所需的细节。从命令式到声明式的转变,代表了我们思考云资源方式的根本变化。 

命令式 vs. 声明式方式启动 AWS EC2 实例

命令式 vs. 声明式方式启动 AWS EC2 实例

使用命令式方法,您要写出逐步指令:“先创建这个安全组,然后等待其创建完成,再用该安全组启动这个实例,然后挂载这个卷,接着创建这些标签。”您需要对精确的执行顺序负责,并在每一步处理错误。 

采用 Terraform 的声明式方法,您只需定义最终状态:“我想要一个EC2 实例,具备这些规格,连接到这个安全组,并带有这些标签。”Terraform 会分析依赖关系、确定正确顺序并执行计划。如果某处失败,Terraform 确切知道进度停在哪一步。

为什么在 AWS 上首选 Terraform? 

  1. 首先,社区生态极为庞大,几乎所有 AWS 服务都能找到模块、示例和解决方案。 
  2. 其次,Terraform 与云服务商解耦。学会一次,跨 AWS、Azure、GCP 及其他平台都能复用同样的技能与模式。在保持一致工作流的同时,避免厂商锁定。 
  3. 第三,Terraform 的状态管理系统会跟踪每个资源,使更新和变更可预测。它知道您创建了什么,并能安全地修改或销毁。

在动手配置之前,您需要理解构成 Terraform 基石的三个关键概念:

  • Resources(资源)是您创建的基础设施组件——EC2 实例、S3 存储桶、安全组。 
  • Providers(提供商)是让 Terraform 与不同平台交互的插件;AWS 提供商会把您的 HCL 翻译为 AWS API 调用。 
  • HCL 是用于定义基础设施的人类可读语法,既具表达力又简洁。

理解这些基础后,我们来准备环境,开始构建真实的基础设施。 

设置 Terraform 的 AWS 环境

在编写任何 Terraform 代码前,您需要安装 CLI 并配置 AWS 凭证。我来带您一步步完成。

安装 Terraform

安装 Terraform 会因操作系统而异:

  • 在 macOS 上使用 Homebrew:先用 brew tap hashicorp/tap 安装 HashiCorp 源,再用 brew install hashicorp/tap/terraform 安装 Terraform。 

  • 在 Windows 上,从 hashicorp.com 下载二进制并加入 PATH,或使用 Chocolatey:choco install terraform。 

  • 在 Linux 上,下载相应软件包或使用发行版的包管理器。 

安装完成后,运行 terraform version 验证安装。

配置 AWS 凭证

配置 AWS 凭证需要一个AWS 账户、一个访问密钥和 IAM 中的密钥。切勿使用根账户。请创建一个具有编程访问权限且具备适当权限(AdministratorAccess)的 IAM 用户。标准做法是在 ~/.aws/credentials 使用 AWS 凭证文件:

[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY

或者,设置环境变量:AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY。在生产环境中,应使用附加到 EC2 实例或 CI/CD 系统的 IAM 角色,而非长期有效的凭证。

如果您考虑使用亚马逊以外的生态,或希望全局了解“三大云”的对比,推荐阅读这份AWS、Azure 与 GCP 在数据科学与 AI 方面的服务对比。

在 AWS 上使用 Terraform 部署带代理的 EC2

基础打好后,我们来构建一个实战示例:一个 EC2 实例在启动时自动安装 SSM 代理。

设置项目结构

Terraform 项目受益于清晰的结构化组织。创建三个文件:main.tf 用于资源定义,variables.tf 用于输入参数,outputs.tf 用于部署后需要的值。这样的分离能保持代码有序、易维护。

 aws-terraform
├── main.tf
├── output.tf
└── variables.tf

原因如下:随着项目增长,把所有内容混在一个文件会难以维护。变量让您通过改变输入,把同一份代码复用于不同环境(开发、预发布、生产)。输出能提供实例 ID 或 IP 地址等值,而无需在 AWS 控制台中手动查找。

编写 EC2 资源配置

现在来创建基础设施:一个预装 SSM 代理的 EC2 实例。在 main.tf 文件中加入以下配置:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "eu-central-1"
}

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"]
}

resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
  
  iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
  
  user_data = <<-EOF
              #!/bin/bash
              cd /tmp
              sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
              sudo systemctl enable amazon-ssm-agent
              sudo systemctl start amazon-ssm-agent
              EOF
  
  tags = {
    Name = "TerraformWebServer"
  }
}

resource "aws_iam_instance_profile" "ssm_profile" {
  name = "ec2-ssm-profile"
  role = aws_iam_role.ssm_role.name
}

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.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_policy" {
  role       = aws_iam_role.ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

Terraform EC2 实例配置

Terraform EC2 实例配置

data 块会向 AWS 查询最新的 Amazon Linux 2 AMI。过滤器确保拿到正确的镜像类型,而 owners = ["amazon"] 则确保使用亚马逊官方 AMI。这种方式可在所有区域通用——Terraform 会自动找到正确的区域 AMI。

关键在于 user_data:一段在实例首次启动时运行的 shell 脚本。它会下载并安装 SSM 代理,并设置为开机自启。IAM 角色为 SSM 管理该实例提供所需权限。

同样的模式适用于任何代理。想要 CloudWatch 日志与指标?只需修改 user_data 脚本安装 CloudWatch 代理即可。原则不变:定义您想安装的内容,Terraform 确保每个实例都按要求完成。

在 output.tf 文件中,您可以添加例如实例 ID 的输出。随后,在下一章将介绍的所有 Terraform 命令运行后,该值会显示在终端中。

output "instance_id" {
  description = "EC2 instance ID for SSM connection"
  value       = aws_instance.web_server.id
}

初始化并应用配置

配置写好后,Terraform 的工作流包含三个命令。首先,下面的命令会下载 AWS 提供商插件并准备您的目录:

terraform init

Terraform 初始化

terraform init

其次,terraform plan 会准确展示 Terraform 将创建、修改或销毁的内容。这个预览能帮助您在真正变更之前发现错误:

terraform plan

Terraform 计划

terraform plan

请仔细审阅输出。您将看到 Terraform 将创建的 EC2 实例、IAM 角色、实例配置文件和策略关联。

最后,terraform apply 会执行变更。Terraform 在继续之前会要求确认。运行该命令后输入 yes:

terraform apply

Terraform 执行

terraform apply

若一切顺利,资源创建完成后您会看到如下信息。 

Terraform 已创建资源

确认:已创建 Terraform 资源

几分钟内,您就能获得一台已配置 SSM 的 EC2 实例,无需 SSH 密钥即可访问。请在 AWS 控制台中查看新创建的资源。

在 AWS S3 上管理 Terraform 状态

运行上述命令后,目录中会出现一个名为 terraform.tfstate 的新文件。Terraform 的状态文件将您的配置映射到真实的 AWS 资源。让我们一起了解该文件的内容。

理解状态文件

状态文件包含已部署基础设施的每个细节:资源 ID、IP 地址、依赖关系。Terraform 会把该状态与您的配置及 AWS 的实际状态进行比较,以决定采取何种操作。丢失状态文件,就等于 Terraform 失去了对已创建一切的跟踪。

把状态保存在本地电脑上很危险。电脑崩溃,状态就没了。多名成员协作时,本地状态冲突会导致损坏。解决方案:远程状态存储。

想深入了解 S3 与 EFS,请查看这篇AWS 存储教程。

配置远程 S3 后端

Terraform 的 S3 后端会把状态存储在一个集中、耐久的位置,团队所有成员都可访问。首先,创建一个 S3 存储桶:

aws s3api create-bucket \
  --bucket datacamp-terraform-state \
  --region eu-central-1 \ 
   --create-bucket-configuration LocationConstraint=eu-central-1

aws s3api put-bucket-versioning \
  --bucket datacamp-terraform-state \
  --versioning-configuration Status=Enabled

版本控制至关重要:它让您可以通过回滚到先前版本,从意外中恢复。

现在,把后端配置加入到 Terraform 代码中:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  backend "s3" {
    bucket = "datacamp-terraform-state"
    key    = "terraform.tfstate"
    region = "eu-central-1"
    use_lockfile = true
  }  
}

再次运行 terraform init 将本地状态迁移到 S3。Terraform 在移动状态文件前会要求确认。

Terraform S3 远程后端

Terraform S3 远程后端

运行命令后,S3 存储桶中应已包含新的状态文件。您可以在 AWS 控制台中查看。

使用原生 S3 锁文件锁定状态

您可能注意到我们在 main.tf 文件中添加了这行:use_lockfile = true。此设置启用 Terraform 的最新功能:原生 S3 状态锁。此前,您需要单独的 DynamoDB 表来实现锁定。

Terraform 状态锁

Terraform 状态锁

这种简化让您的基础设施搭建少用了一项 AWS 服务,同时仍能提供同样的竞态保护。多位工程师可以安全协作,确保同一时间只有一个人的变更会被应用。

用 Terraform 在 AWS 上扩展基础设施

随着基础设施增长,硬编码值与复制粘贴的代码会变成维护噩梦。Terraform 的变量系统与模块能解决这些问题。

用输入变量重构

之前我们创建了 variables.tf 文件。现在,您可以把此前在 main.tf 中硬编码的值移入这个用于存放变量的新文件:

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "eu-central-1"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

现在在 main.tf 中的资源里引用这些变量:

provider "aws" {
  region = var.aws_region
}

resource "aws_instance" "web_server" {
  instance_type = var.instance_type
  # ... rest of configuration
  
  tags = {
    Environment = var.environment
  }
}

注意,AWS S3 存储桶是全局唯一的,因此在 main.tf 的 backend 参数中,区域不能使用变量。这里必须保留硬编码的区域,比如 “eu-central-1”,而不能用 var.region_aws。

变量既然已经定义,您也可以定义不同环境(如开发或生产),其中变量值有所不同。这可以通过创建扩展名为 .tfvars 的新文件来完成。

例如,您可以创建用于开发的 terraform-dev.tfvars 和用于生产的 terraform-prod.tfvars:

开发环境:

aws_region    = "eu-central-1"
instance_type = "t2.micro"

生产环境:

aws_region    = "eu-central-1"
instance_type = "t3.medium"

然后,通过指定相应文件来部署到不同环境:

terraform apply -var-file="terraform-dev.tfvars”

变量解决了环境特定值的问题,但如果要复用整套基础设施模式怎么办?这时模块就至关重要了。

创建可复用的资源模块

模块将相关资源封装为可复用组件。最佳实践是创建专门的文件夹。例如,您可以创建如下目录结构:

modules/
  ec2-with-ssm/
    main.tf
    variables.tf
    outputs.tf

您可以把先前创建的 variables.tf 文件移到模块文件夹(或新建一个)。关键变化在 main.tf 文件。模块中的 main.tf 应包含我们的资源,因此从 AMI 开始到文件末尾的内容都应移到这里:

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"]
}

resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  
  iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
  
  user_data = <<-EOF
              #!/bin/bash
              cd /tmp
              sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
              sudo systemctl enable amazon-ssm-agent
              sudo systemctl start amazon-ssm-agent
              EOF
  
  tags = {
    Name = "TerraformWebServer"
  }
}

resource "aws_iam_instance_profile" "ssm_profile" {
  name = "ec2-ssm-profile"
  role = aws_iam_role.ssm_role.name
}

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.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_policy" {
  role       = aws_iam_role.ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

为替代这些变更,您在主配置中保留其余内容,但新增一个指向特定模块的 module 参数。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  backend "s3" {
    bucket = "datacamp-terraform-state"
    key    = "terraform.tfstate"
    region = "eu-central-1"
    use_lockfile = true
  }
}

provider "aws" {
  region = var.aws_region
}

module "web_server" {
  source = "./modules/ec2-with-ssm"
  
  instance_type = var.instance_type
}

现在,按此前的方式运行 init、plan 和 apply,您会部署与之前相同的资源。不同之处在于,模块让您以可复用的相似配置快速启动基础设施,这是将 Terraform 扩展到更复杂项目的关键。

接下来,我们转入另一个重要主题:安全。

在 AWS 上使用 Terraform 的安全最佳实践

基础设施代码需要与应用代码同等的安全严格性。因此,保护您的 Terraform 部署至关重要。

管理机密与敏感数据

切勿把机密提交到 Git。绝对不要。请使用 AWS Secrets Manager 存储敏感值。您可以在 Terraform 中使用如下资源:

resource "aws_secretsmanager_secret" "example" {
  name = "example"
}

对于特定环境的机密,您可以使用带有 TF_VAR_ 前缀的环境变量。 

例如,您可以在 variables.tf 文件中使用 variable "aws_access_key" {},并在终端用 export TF_VAR_aws_access_key=<access_key_value> 导出。Terraform 会自动加载这些变量值,而无需在代码中暴露。

处理配置漂移

另一个重要的配置主题是漂移。当有人绕过 Terraform,直接通过 AWS 控制台或 CLI 修改基础设施时就会发生。请经常运行 terraform plan 检测漂移。计划输出会显示与配置不匹配的资源。

出现漂移时,您有两种选择:若手动更改是有意为之,则更新 Terraform 代码以匹配现实;否则运行 terraform apply 将基础设施强制回到代码定义的目标状态。正确选择取决于手动更改是否是出于目的。

清理资源

最后,在确定不再需要时移除所有资源也很重要,以避免不必要的成本。您可以使用以下命令清理资源:

terraform destroy

请仔细审阅销毁计划。Terraform 会删除它创建的每个资源。对于生产环境,考虑使用 terraform destroy -target 只移除特定资源,而非全部。

结论

您已经学会了 Terraform 的核心流程:用代码定义基础设施,用 plan 预览变更,用 apply 应用变更,并通过状态管理跟踪一切。我们涵盖了自动安装代理的 EC2 部署、将状态迁移到带原生锁定的 S3、通过变量与模块实现扩展,以及用机密管理保障安全。

请以对待应用代码的同等严谨来对待基础设施代码。使用版本控制,对变更进行代码审查,并先在非生产环境测试。CI/CD 流水线可自动执行 terraform apply,让基础设施更新如同应用发布一样顺畅。

下一步:探索 GitHub Actions 或 GitLab CI,在您推送代码时自动运行 Terraform。基础设施即代码与自动化结合,能重塑团队管理云资源的方式,以可靠、可复现、受版本控制的基础设施替代手工控制台操作。

我的建议是在此基础上继续进阶,报名 AWS 云从业者(CLF-C02) 技能路径,它会教会您获得亚马逊 CLF-C02 认证所需的一切。

Terraform AWS 常见问答

如何高效地用 Terraform 管理多个 AWS 环境?

针对每个环境使用变量与独立的 .tfvars 文件。创建 terraform.dev.tfvars、terraform.staging.tfvars 和 terraform.prod.tfvars,在其中设置区域、实例类型、标签等环境特定值。通过 terraform apply -var-file="terraform.prod.tfvars" 部署到不同环境。此方法让您在不复制代码的情况下,用单一代码库为各环境定制配置。

在 AWS 中使用 Terraform 配置时,如何保护敏感数据的最佳实践是什么?

切勿把机密提交到 Git。使用 AWS Secrets Manager 存储数据库密码等敏感值,然后在 Terraform 中通过数据源引用。对于特定环境的机密,使用带 TF_VAR_ 前缀的环境变量——Terraform 会自动加载这些值,而不在代码中暴露。为 S3 状态桶启用加密,并将 IAM 权限限制为必要的用户与 CI/CD 系统。

Terraform 的 S3 后端仍然需要 DynamoDB 来进行状态加锁吗?

不需要。您可以在后端配置中设置 use_lockfile = true 使用原生 S3 状态锁。Terraform 会直接在 S3 中通过条件写入创建 .tflock 文件,无需使用单独的 DynamoDB 表。这样能简化配置、降低成本,并在多人协作时提供同等的并发修改保护。

当有人手动修改了基础设施时,如何处理配置漂移?

定期运行 terraform plan 以检测漂移。计划输出会显示由于通过 AWS 控制台或 CLI 的手动更改而与配置不匹配的资源。您有两种选择:若手动更改是有意的,则更新 Terraform 代码与之对齐;否则运行 terraform apply 将基础设施恢复到代码定义的目标状态。像对待应用代码那样对待基础设施——所有变更都应通过 Terraform 进行。

我能用同一套 Terraform 代码在不同 AWS 区域部署资源吗?

完全可以!在 provider 块中使用 aws_region 变量而非硬编码区域。使用数据源(如 data "aws_ami")替代硬编码 AMI ID,因为 AMI ID 在不同区域不同。在 .tfvars 文件中定义区域:某环境用 aws_region = "us-east-1",另一个用 aws_region = "eu-west-1"。在 provider 中引用:provider "aws" { region = var.aws_region }。这样您的代码就能真正跨区域移植。

Benito Martin

作者

Benito Martin

作为 Martin Data Solutions 的创始人兼自由职业数据科学家、ML 与 AI 工程师,我在回归、分类、NLP、LLM、RAG、神经网络、集成方法和计算机视觉等领域拥有多元项目经验。

  • 成功交付多项端到端机器学习项目,覆盖数据清洗、分析、建模与在 AWS 和 GCP 上的部署,提供具备影响力且可扩展的解决方案。
  • 使用 Streamlit 和 Gradio 构建交互式、可扩展的网页应用,服务于不同行业场景。
  • 教授并指导数据科学与数据分析学员,通过个性化学习路径促进其职业成长。
  • 为企业级需求设计检索增强生成(RAG)应用课程内容。
  • 撰写高影响力的 AI 与 ML 技术博客,涵盖 MLOps、向量数据库和 LLM 等主题,并获得显著关注。

在我参与的每个项目中,我都会应用最新的软件工程与 DevOps 实践,如 CI/CD、代码规范检查、格式化、模型监控、实验追踪以及健壮的错误处理。我致力于交付完整的解决方案,将数据洞察转化为切实可行的策略,帮助企业成长,并充分发挥数据科学、机器学习与 AI 的价值。

主题