Terraform实战01:第一个Terraform项目——VPC + EC2
Terraform实战01:第一个Terraform项目——VPC + EC2

本篇目标
通过创建一个完整的AWS网络环境(VPC + 子网 + 安全组 + EC2),掌握Terraform的核心工作流程和基本概念。
学完本篇你将掌握:
- Terraform的核心命令:
init/plan/apply/destroy - Terraform的文件结构:
main.tf/variables.tf/outputs.tf - AWS基础网络资源的关系和串联方式
- Terraform中资源依赖的表达方式
环境准备
1. 安装Terraform
Linux:
# 下载
wget https://releases.hashicorp.com/terraform/1.9.0/terraform_1.9.0_linux_amd64.zip
# 解压
unzip terraform_1.9.0_linux_amd64.zip
# 移动到系统路径
sudo mv terraform /usr/local/bin/
# 验证安装
terraform version
# 输出:Terraform v1.9.0
macOS:
brew install terraform
terraform version
Windows:
从 https://developer.hashicorp.com/terraform/install 下载对应版本,解压后将 terraform.exe 所在目录加入系统 PATH 环境变量。
2. 安装AWS CLI
Linux:
# 下载
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# 解压
unzip awscliv2.zip
# 安装
sudo ./aws/install
# 验证
aws --version
# 输出:aws-cli/2.x.x ...
macOS:
brew install awscli
aws --version
3. 获取AWS Access Key
- 登录AWS控制台:https://console.aws.amazon.com
- 右上角点击你的用户名 → Security credentials(安全凭证)
- 找到 Access keys 部分
- 点击 Create access key
- 记录 Access Key ID 和 Secret Access Key(Secret只显示一次)
4. 配置AWS凭证
aws configure
依次输入:
- AWS Access Key ID:你创建的Access Key
- AWS Secret Access Key:你创建的Secret Key
- Default region name:
us-east-1 - Default output format:
json(或直接回车跳过)
验证配置是否正确:
aws sts get-caller-identity
如果返回了你的Account ID和ARN信息,说明配置成功。
5. 创建项目目录
mkdir -p ~/terraform-practice/01-basic-vpc
cd ~/terraform-practice/01-basic-vpc
架构图

文件结构
01-basic-vpc/
├── main.tf # 【你写的】主配置文件,定义所有资源
├── variables.tf # 【你写的】变量定义文件,抽取可配置的参数
├── outputs.tf # 【你写的】输出定义文件,apply后显示关键信息
├── .terraform/ # 【自动生成】terraform init下载的Provider插件
├── .terraform.lock.hcl # 【自动生成】锁文件,锁定Provider精确版本
├── terraform.tfstate # 【自动生成】状态文件,记录已创建资源的实际状态
└── terraform.tfstate.backup # 【自动生成】状态文件备份
你需要写的只有前3个文件,后面的由Terraform自动管理。
各文件职责:
| 文件 | 职责 | 一句话总结 |
|---|---|---|
main.tf | 定义资源 | “我要创建什么” |
variables.tf | 定义变量 | “哪些参数是可配置的” |
outputs.tf | 定义输出 | “创建完后告诉我什么信息” |
自动生成文件说明:
| 文件 | 作用 | 是否提交到Git |
|---|---|---|
.terraform/ | Provider插件目录 | ❌ 不提交(加入.gitignore) |
.terraform.lock.hcl | 锁定Provider版本 | ✅ 提交(保证团队用同一版本) |
terraform.tfstate | 资源状态记录 | ⚠️ 本地练习可以,生产环境应存到远程(S3) |
terraform.tfstate.backup | 状态备份 | 同上 |
完整代码
main.tf
# ============================================
# Terraform 配置块
# 声明使用的 Provider(插件)和版本约束
# 类比:package.json中声明依赖包版本
# terraform init 时根据这段下载插件到 .terraform/ 目录
# ============================================
terraform {
required_providers {
aws = {
source = "hashicorp/aws" # Provider来源:HashiCorp官方的AWS Provider
version = "~> 5.0" # 版本约束:5.x的任意版本(~>表示只允许最右侧版本号变化)
}
}
required_version = ">= 1.0" # Terraform CLI最低版本要求
}
# ============================================
# Provider 配置
# 告诉 Terraform 如何连接 AWS
# 凭证不写在代码里,自动读取 aws configure 配置的AK/SK
# ============================================
provider "aws" {
region = var.region # 引用 variables.tf 中定义的 region 变量
}
# ============================================
# 资源1:VPC(虚拟私有云)
# 你在AWS上的隔离网络空间,所有资源都必须在VPC里
# ============================================
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16" # VPC的IP地址范围(/16 = 65536个地址)
enable_dns_support = true # 启用VPC内DNS解析
enable_dns_hostnames = true # 为实例分配DNS主机名
tags = {
Name = "tf-practice-vpc" # 标签,在AWS控制台显示的名称
}
}
# ============================================
# 资源2:公有子网
# VPC内的一个子网段,EC2实例放在这里
# ============================================
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # 属于哪个VPC(引用上面VPC的ID)
cidr_block = "10.0.1.0/24" # 子网的IP范围(/24 = 256个地址,必须在VPC范围内)
availability_zone = "${var.region}a" # 可用区(字符串拼接:变量值 + "a")
map_public_ip_on_launch = true # 在此子网创建的实例自动分配公网IP
tags = {
Name = "tf-practice-public-subnet"
}
}
# ============================================
# 资源3:Internet Gateway(IGW)
# VPC的"大门",连接VPC和互联网
# 本身没有IP地址,只做流量转发和NAT映射
# ============================================
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id # 附加到哪个VPC
tags = {
Name = "tf-practice-igw"
}
}
# ============================================
# 资源4:路由表
# 定义流量的转发规则:"去哪里的流量走哪条路"
# ============================================
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id # 属于哪个VPC
route {
cidr_block = "0.0.0.0/0" # 目标:所有外部地址
gateway_id = aws_internet_gateway.main.id # 下一跳:走IGW出去
}
tags = {
Name = "tf-practice-public-rt"
}
}
# ============================================
# 资源5:路由表关联
# 把路由表绑定到子网,子网里的资源才会按此规则转发
# 没有这一步,子网使用VPC的默认路由表(没有IGW路由)
# ============================================
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id # 关联到哪个子网
route_table_id = aws_route_table.public.id # 使用哪个路由表
}
# ============================================
# 资源6:安全组
# 实例级别的防火墙,控制入站和出站流量
# ============================================
resource "aws_security_group" "allow_ssh" {
name = "tf-practice-allow-ssh"
description = "Allow SSH inbound traffic"
vpc_id = aws_vpc.main.id # 作用域:在哪个VPC内生效
# 入站规则:允许SSH(22端口)
ingress {
description = "SSH from anywhere"
from_port = 22 # 起始端口
to_port = 22 # 结束端口(22-22表示只开22端口)
protocol = "tcp" # 协议类型
cidr_blocks = ["0.0.0.0/0"] # 来源:任何IP(生产环境应限制为特定IP)
}
# 出站规则:允许所有出站流量
egress {
from_port = 0 # 0表示所有端口
to_port = 0
protocol = "-1" # -1表示所有协议
cidr_blocks = ["0.0.0.0/0"] # 目标:任何IP
}
tags = {
Name = "tf-practice-sg"
}
}
# ============================================
# 资源7:EC2实例
# 实际的虚拟服务器
# ============================================
resource "aws_instance" "web" {
ami = var.ami_id # 镜像ID(引用variables.tf中的变量)
instance_type = "t3.micro" # 实例规格(2vCPU, 1GB内存)
subnet_id = aws_subnet.public.id # 放在哪个子网
vpc_security_group_ids = [aws_security_group.allow_ssh.id] # 绑定哪些安全组(数组,可绑多个)
tags = {
Name = "tf-practice-ec2"
}
}
variables.tf
# ============================================
# 变量定义
# 把可变的参数抽出来,不硬编码在main.tf里
# 好处:同一套代码通过修改变量就能适配不同环境
# ============================================
variable "region" {
description = "AWS 区域" # 变量说明(给人看的)
type = string # 类型约束:只接受字符串
default = "us-east-1" # 默认值:不传参时使用这个
}
variable "ami_id" {
description = "EC2 使用的 AMI ID(Amazon Linux 2023,us-east-1)"
type = string
default = "ami-0bdc7d025135d7b49" # Amazon Linux 2023 in us-east-1
}
覆盖变量默认值的三种方式:
# 方式1:命令行传参
terraform apply -var="region=ap-southeast-1"
# 方式2:创建 terraform.tfvars 文件(推荐)
# 文件内容:
# region = "ap-southeast-1"
# ami_id = "ami-xxxxxxxx"
# 方式3:环境变量(TF_VAR_ 前缀)
export TF_VAR_region="ap-southeast-1"
outputs.tf
# ============================================
# 输出定义
# terraform apply 完成后在终端显示关键信息
# 也可以被其他模块引用(模块化时用到)
# ============================================
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id # 引用资源的属性
}
output "subnet_id" {
description = "公有子网 ID"
value = aws_subnet.public.id
}
output "instance_id" {
description = "EC2 实例 ID"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "EC2 公网 IP"
value = aws_instance.web.public_ip
}
Terraform 语法要点
resource 语法格式
resource "资源类型" "本地名称" {
参数 = 值
}
- 资源类型:由Provider定义,格式为
provider_资源,如aws_vpc、aws_subnet、aws_instance - 本地名称:你自己起的,在代码内引用时用
- 引用方式:
资源类型.本地名称.属性,如aws_vpc.main.id
variable 语法格式
variable "变量名" {
description = "说明"
type = 类型 # string、number、bool、list、map
default = 默认值 # 可选,没有default则apply时必须传入
}
在其他地方引用:var.变量名
output 语法格式
output "输出名" {
description = "说明"
value = 表达式 # 通常引用资源的某个属性
}
字符串拼接
availability_zone = "${var.region}a" # 结果:"us-east-1a"
用 ${ } 在字符串中插入变量或表达式。
关键字段说明
VPC
| 字段 | 含义 | 说明 |
|---|---|---|
cidr_block | IP地址范围 | /16 = 65536个IP,/24 = 256个IP |
enable_dns_support | 启用VPC内DNS解析 | 几乎都要开启 |
enable_dns_hostnames | 为实例分配DNS主机名 | 需要公网DNS名时开启 |
子网
| 字段 | 含义 | 说明 |
|---|---|---|
vpc_id | 所属VPC | 通过 aws_vpc.main.id 引用 |
cidr_block | 子网IP范围 | 必须是VPC CIDR的子集 |
availability_zone | 可用区 | 如 us-east-1a |
map_public_ip_on_launch | 自动分配公网IP | true = 此子网内的实例自动获得公网IP |
安全组
| 字段 | 含义 | 说明 |
|---|---|---|
ingress | 入站规则 | 控制谁能访问这个资源 |
egress | 出站规则 | 控制资源能访问哪里 |
from_port / to_port | 端口范围 | 22-22 = 只开SSH |
protocol | 协议 | tcp、udp、icmp、-1(所有) |
cidr_blocks | 来源/目标IP范围 | 0.0.0.0/0 = 任何地址 |
EC2
| 字段 | 含义 | 说明 |
|---|---|---|
ami | Amazon Machine Image | 决定装什么操作系统 |
instance_type | 实例规格 | t3.micro = 2vCPU + 1GB |
subnet_id | 放在哪个子网 | 决定了网络位置 |
vpc_security_group_ids | 安全组列表 | 是数组,可以绑多个 |
操作步骤
Step 1:初始化
cd ~/terraform-practice/01-basic-vpc
terraform init
输出关键信息:
Initializing provider plugins...
- Installing hashicorp/aws v5.100.0...
Terraform has been successfully initialized!
做了什么: 根据terraform { }配置块,下载AWS Provider插件到 .terraform/ 目录,生成 .terraform.lock.hcl 锁文件。
Step 2:预览变更
terraform plan
输出关键信息:
Plan: 7 to add, 0 to change, 0 to destroy.
做了什么: 对比代码和状态文件(第一次运行时状态为空),计算出需要创建7个资源。不会真正执行任何操作,只是预览。
Step 3:执行创建
terraform apply -auto-approve
注:
-auto-approve跳过确认提示。不加这个参数会让你输入yes确认。
输出关键信息:
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 16s [id=vpc-025bec669a1069631]
aws_internet_gateway.main: Creating...
aws_subnet.public: Creating...
aws_security_group.allow_ssh: Creating...
...
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0d2e355e5d3355e76"
instance_public_ip = "100.58.232.70"
subnet_id = "subnet-0075db46e8b2ebd0f"
vpc_id = "vpc-025bec669a1069631"
做了什么: 按照依赖顺序创建资源。Terraform自动分析引用关系,判断创建顺序。
Step 4:验证
去AWS控制台(us-east-1区域)确认资源已创建:
| 资源 | 控制台路径 | 查找名称 |
|---|---|---|
| VPC | VPC → Your VPCs | tf-practice-vpc |
| 子网 | VPC → Subnets | tf-practice-public-subnet |
| Internet Gateway | VPC → Internet Gateways | tf-practice-igw |
| 路由表 | VPC → Route Tables | tf-practice-public-rt |
| 安全组 | VPC → Security Groups | tf-practice-allow-ssh |
| EC2 | EC2 → Instances | tf-practice-ec2 |
或者用AWS CLI验证:
aws ec2 describe-instances --filters "Name=tag:Name,Values=tf-practice-ec2" --query "Reservations[].Instances[].{ID:InstanceId,IP:PublicIpAddress,State:State.Name}" --output table
Step 5:销毁(重要!省钱!)
terraform destroy -auto-approve
输出关键信息:
aws_instance.web: Destroying...
aws_route_table_association.public: Destroying...
...
aws_vpc.main: Destruction complete after 1s
Destroy complete! Resources: 7 destroyed.
做了什么: 按照创建的反序销毁所有资源(先EC2,最后VPC)。Terraform自动处理销毁顺序。
原理讲解
Terraform核心工作流程

terraform plan 对比代码(期望状态)和 state 文件(实际状态),计算出需要的变更。
资源之间的关系
VPC
├── 子网(vpc_id → VPC)
├── Internet Gateway(vpc_id → VPC)
├── 安全组(vpc_id → VPC)
└── 路由表(vpc_id → VPC)
└── 路由表关联(subnet_id → 子网, route_table_id → 路由表)
EC2实例(subnet_id → 子网, vpc_security_group_ids → 安全组)
Terraform通过代码中的引用(如 aws_vpc.main.id)自动判断依赖关系和创建顺序:
- 先创建VPC(不依赖任何资源)
- 并行创建子网、IGW、安全组(只依赖VPC)
- 创建路由表(依赖VPC和IGW)
- 创建路由表关联(依赖路由表和子网)
- 创建EC2(依赖子网和安全组)
EC2被外部访问的四个条件
| 条件 | 对应资源 | 缺少会怎样 |
|---|---|---|
| 1. 有公网IP | 子网 map_public_ip_on_launch = true | 外部不知道你的地址 |
| 2. VPC有IGW | aws_internet_gateway | 流量进不了VPC |
| 3. 路由表指向IGW | aws_route_table 中 0.0.0.0/0 → IGW | 流量不知道往哪走 |
| 4. 安全组放行端口 | aws_security_group ingress规则 | 流量被拒绝 |
公有子网 vs 私有子网
不是创建时选择的"类型",本质区别在于路由表:
- 公有子网:关联的路由表有
0.0.0.0/0 → IGW的规则 - 私有子网:关联的路由表没有指向IGW的规则
terraform.tfstate 状态文件的作用
- Terraform每次apply/destroy都会更新状态文件
- 它记录了"已经创建了哪些资源、资源的ID和属性是什么"
- 下次执行plan时,Terraform对比代码和状态文件来决定需要做什么变更
- 本地练习可以存在本地,生产环境应存到远程(S3 + DynamoDB锁),后续文章会讲
踩坑记录
问题1:t2.micro不在Free Tier范围
报错: The specified instance type is not eligible for Free Tier
原因: 新的AWS账号Free Tier支持的可能是t3.micro,不是t2.micro。
解决: 用以下命令查看账号支持哪些Free Tier实例类型:
aws ec2 describe-instance-types --filters "Name=free-tier-eligible,Values=true" --query "InstanceTypes[].InstanceType" --output text
问题2:AMI ID不存在
报错: The image id does not exist
原因: AMI ID是特定区域+特定时间的,会过期或被新版本替代。
解决: 用以下命令查最新的Amazon Linux 2023 AMI:
aws ec2 describe-images --owners amazon \
--filters "Name=name,Values=al2023-ami-2023*-x86_64" "Name=state,Values=available" \
--query "Images | sort_by(@, &CreationDate) | [-1].ImageId" --output text
问题3:aws command not found
原因: AWS CLI没有安装。
解决: 参考本文"环境准备"部分安装AWS CLI。
费用说明
| 资源 | 费用 |
|---|---|
| VPC / 子网 / 安全组 / IGW / 路由表 | 免费 |
| EC2 t3.micro | ~$0.0104/小时 |
| 公网IP | ~$0.005/小时 |
| EBS 8GB gp3 | ~$0.80/月 |
本次练习总花费(创建到销毁约30分钟):< $0.01
省钱关键:练完立刻执行 terraform destroy!
延伸思考:面试常见问题
如果面试中被问到Terraform和AWS网络相关的问题,本篇的知识可以回答以下问题:
| 面试问题 | 答案要点 |
|---|---|
| Terraform的核心工作流程是什么? | init → plan → apply → destroy,声明式管理 |
| Terraform怎么知道要创建什么? | 对比 .tf代码(期望状态)和 .tfstate(实际状态) |
| EC2无法从外网访问,你怎么排查? | 检查4个条件:公网IP、IGW、路由表指向IGW、安全组放行 |
| 什么是公有子网? | 关联的路由表有 0.0.0.0/0 → IGW 的路由规则 |
| terraform.tfstate有什么用? | 记录已创建资源的实际状态,用于plan时的差异计算 |
| .terraform.lock.hcl要不要提交到Git? | 要,保证团队使用相同版本的Provider |
小结
本篇完成了Terraform的第一次完整实操,核心收获:
- Terraform的工作模式:声明式——你描述"想要什么",Terraform负责"怎么做"和"按什么顺序做"
- 核心命令:init(初始化)→ plan(预览)→ apply(执行)→ destroy(销毁)
- 文件组织:main.tf(资源定义)、variables.tf(变量)、outputs.tf(输出)
- 资源依赖:通过
资源类型.名称.属性引用其他资源,Terraform自动处理创建和销毁顺序 - AWS网络基础:VPC → 子网 → IGW → 路由表 → 安全组 → EC2 的完整串联
- 状态管理:Terraform通过state文件追踪已创建的资源
下一篇预告
Terraform实战02:公有子网 + 私有子网 + NAT Gateway
下一篇我们会创建一个更贴近生产环境的网络架构:
- 公有子网放对外服务(如ALB)
- 私有子网放业务服务(不直接暴露)
- NAT Gateway让私有子网的实例能访问外网但外网不能主动访问它们
- 理解为什么生产环境要做公私网分离
参考链接
更多推荐
所有评论(0)