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

  1. 登录AWS控制台:https://console.aws.amazon.com
  2. 右上角点击你的用户名 → Security credentials(安全凭证)
  3. 找到 Access keys 部分
  4. 点击 Create access key
  5. 记录 Access Key IDSecret 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_vpcaws_subnetaws_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_blockIP地址范围/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自动分配公网IPtrue = 此子网内的实例自动获得公网IP

安全组

字段含义说明
ingress入站规则控制谁能访问这个资源
egress出站规则控制资源能访问哪里
from_port / to_port端口范围22-22 = 只开SSH
protocol协议tcp、udp、icmp、-1(所有)
cidr_blocks来源/目标IP范围0.0.0.0/0 = 任何地址

EC2

字段含义说明
amiAmazon 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区域)确认资源已创建:

资源控制台路径查找名称
VPCVPC → Your VPCstf-practice-vpc
子网VPC → Subnetstf-practice-public-subnet
Internet GatewayVPC → Internet Gatewaystf-practice-igw
路由表VPC → Route Tablestf-practice-public-rt
安全组VPC → Security Groupstf-practice-allow-ssh
EC2EC2 → Instancestf-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)自动判断依赖关系和创建顺序:

  1. 先创建VPC(不依赖任何资源)
  2. 并行创建子网、IGW、安全组(只依赖VPC)
  3. 创建路由表(依赖VPC和IGW)
  4. 创建路由表关联(依赖路由表和子网)
  5. 创建EC2(依赖子网和安全组)

EC2被外部访问的四个条件

条件对应资源缺少会怎样
1. 有公网IP子网 map_public_ip_on_launch = true外部不知道你的地址
2. VPC有IGWaws_internet_gateway流量进不了VPC
3. 路由表指向IGWaws_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的第一次完整实操,核心收获:

  1. Terraform的工作模式:声明式——你描述"想要什么",Terraform负责"怎么做"和"按什么顺序做"
  2. 核心命令:init(初始化)→ plan(预览)→ apply(执行)→ destroy(销毁)
  3. 文件组织:main.tf(资源定义)、variables.tf(变量)、outputs.tf(输出)
  4. 资源依赖:通过 资源类型.名称.属性 引用其他资源,Terraform自动处理创建和销毁顺序
  5. AWS网络基础:VPC → 子网 → IGW → 路由表 → 安全组 → EC2 的完整串联
  6. 状态管理:Terraform通过state文件追踪已创建的资源

下一篇预告

Terraform实战02:公有子网 + 私有子网 + NAT Gateway

下一篇我们会创建一个更贴近生产环境的网络架构:

  • 公有子网放对外服务(如ALB)
  • 私有子网放业务服务(不直接暴露)
  • NAT Gateway让私有子网的实例能访问外网但外网不能主动访问它们
  • 理解为什么生产环境要做公私网分离

参考链接

更多推荐