Terraform解说

来自linuxsa wiki
跳转到导航 跳转到搜索


main.tf

terraform {
  required_version = ">= 1.5.0"

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

provider "aws" {
  region = var.aws_region
}

# --------------------------------------------------
# VPC
# --------------------------------------------------

resource "aws_vpc" "lab" {
  cidr_block           = "10.10.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "ssh-port-lab-vpc"
  }
}

# --------------------------------------------------
# Public Subnet
# --------------------------------------------------

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.10.1.0/24"
  availability_zone       = "${var.aws_region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "ssh-port-lab-public"
  }
}

# --------------------------------------------------
# Internet Gateway
# --------------------------------------------------

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id

  tags = {
    Name = "ssh-port-lab-igw"
  }
}

# --------------------------------------------------
# Route Table
# --------------------------------------------------

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.lab.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }

  tags = {
    Name = "ssh-port-lab-public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# --------------------------------------------------
# Security Group
# --------------------------------------------------

resource "aws_security_group" "ssh" {
  name        = "ssh-port-lab-sg"
  description = "Allow SSH for SSH port migration lab"
  vpc_id      = aws_vpc.lab.id

  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ssh-port-lab-sg"
  }
}

# --------------------------------------------------
# EC2 Key Pair
# --------------------------------------------------

resource "aws_key_pair" "lab" {
  key_name   = var.key_name
  public_key = file(var.public_key_path)
}

# --------------------------------------------------
# Amazon Linux 2023 AMI
# --------------------------------------------------

data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

# --------------------------------------------------
# Launch Template
# --------------------------------------------------

resource "aws_launch_template" "lab" {
  name_prefix = "ssh-port-lab-"

  image_id = data.aws_ssm_parameter.al2023.value

  instance_type = "t3.micro"

  key_name = aws_key_pair.lab.key_name

  vpc_security_group_ids = [
    aws_security_group.ssh.id
  ]

  tag_specifications {
    resource_type = "instance"

    tags = {
      Name = "ssh-port-lab-ec2"
    }
  }
}

# --------------------------------------------------
# Auto Scaling Group
# --------------------------------------------------

resource "aws_autoscaling_group" "lab" {
  name = "ssh-port-lab-asg"

  min_size         = 1
  max_size         = 1
  desired_capacity = 1

  vpc_zone_identifier = [
    aws_subnet.public.id
  ]

  launch_template {
    id      = aws_launch_template.lab.id
    version = "$Latest"
  }

  health_check_type = "EC2"

  tag {
    key                 = "Name"
    value               = "ssh-port-lab-asg-instance"
    propagate_at_launch = true
  }
}


这个 `main.tf` 很适合拿来理解 Terraform,因为它基本把 AWS Infra 里最常见的几个概念串起来了:

**VPC → Subnet → Internet Gateway → Route Table → Security Group → Key Pair → AMI → Launch Template → ASG**

我按你现在这个实验的实际运行路径,从上到下解释。

---

# 1. Terraform 本身的版本和 Provider

```hcl
terraform {
  required_version = ">= 1.5.0"

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

这里**不是创建 AWS 资源**,而是在告诉 Terraform:

> “这个项目需要什么 Terraform 版本,以及什么 Provider。”

---

### `required_version`

```hcl
required_version = ">= 1.5.0"
```

意思:

```text
Terraform >= 1.5.0
```

比如:

```text
Terraform 1.4  ❌
Terraform 1.5  ✅
Terraform 1.6  ✅
Terraform 1.13 ✅
```

---

### `required_providers`

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

Terraform 自己并不知道:

```hcl
resource "aws_instance" ...
```

是什么意思。

真正知道 AWS API 怎么调用的是:

**AWS Provider**

所以:

```text
Terraform
   │
   ▼
AWS Provider
   │
   ▼
AWS API
   │
   ▼
VPC / EC2 / ASG / SG ...
```

---

### `source`

```hcl
source = "hashicorp/aws"
```

就是:

> 使用 HashiCorp 官方的 AWS Provider。

---

### `version`

```hcl
version = "~> 6.0"
```

表示使用 6.x 系列的 provider,避免 Terraform 随便升级到不兼容的大版本。

---

# 2. Provider

```hcl
provider "aws" {
  region = var.aws_region
}
```

这个才是告诉 AWS Provider:

> “我们操作哪个 AWS Region?”

你在 `variables.tf` 中定义了:

```hcl
variable "aws_region" {
  default = "ap-east-1"
}
```

所以最终相当于:

```hcl
provider "aws" {
  region = "ap-east-1"
}
```

也就是:

```text
Hong Kong
ap-east-1
```

---

# 3. VPC

```hcl
resource "aws_vpc" "lab" {
  cidr_block           = "10.10.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "ssh-port-lab-vpc"
  }
}
```

这是我们创建的第一个 AWS 资源。

Terraform 的基本语法:

```hcl
resource "TYPE" "NAME" {
    ...
}
```

这里:

```hcl
resource "aws_vpc" "lab"
```

可以理解成:

```text
resource type = aws_vpc
resource name = lab
```

Terraform 内部引用它的时候:

```hcl
aws_vpc.lab
```

---

### CIDR

```hcl
cidr_block = "10.10.0.0/16"
```

我们的 VPC:

```text
10.10.0.0/16
```

范围比较大:

```text
10.10.0.0
     ↓
10.10.255.255
```

然后我们后面从里面划一个 subnet:

```text
VPC
10.10.0.0/16
      │
      └── subnet
          10.10.1.0/24
```

---

### DNS

```hcl
enable_dns_hostnames = true
enable_dns_support   = true
```

让 VPC 支持 AWS DNS 功能以及实例 DNS hostname。

对于 EC2 / AWS Infra 环境,一般建议开启。

---

# 4. Public Subnet

```hcl
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.10.1.0/24"
  availability_zone       = "${var.aws_region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "ssh-port-lab-public"
  }
}
```

这里开始出现一个非常重要的 Terraform 概念:

```hcl
vpc_id = aws_vpc.lab.id
```

这不是手写:

```hcl
vpc_id = "vpc-0123456789"
```

而是:

> 把刚才创建的 VPC 的 ID 自动传给 Subnet。

---

## `aws_vpc.lab.id`

可以拆成:

```text
aws_vpc
   │
   └── lab
        │
        └── id
```

也就是:

```text
资源类型 → 资源名称 → 属性
```

Terraform 会自动建立依赖关系:

```text
aws_vpc.lab
     │
     ▼
aws_subnet.public
```

所以 Terraform 知道:

> 先创建 VPC,再创建 Subnet。

这就是 Terraform 非常核心的:

**Dependency Graph**

---

## Subnet CIDR

```hcl
cidr_block = "10.10.1.0/24"
```

这个 subnet 位于:

```text
10.10.0.0/16
```

里面。

所以:

```text
VPC
10.10.0.0/16

    └── Public Subnet
        10.10.1.0/24
```

---

## Availability Zone

```hcl
availability_zone = "${var.aws_region}a"
```

如果:

```hcl
var.aws_region = "ap-east-1"
```

Terraform 会得到:

```text
ap-east-1a
```

---

## `map_public_ip_on_launch`

```hcl
map_public_ip_on_launch = true
```

意思是:

> 在这个 subnet 中启动的 EC2,默认分配 Public IPv4。

所以我们后面才能:

```bash
ssh [email protected]
```

---

# 5. Internet Gateway

```hcl
resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id

  tags = {
    Name = "ssh-port-lab-igw"
  }
}
```

Internet Gateway:

**IGW**

它负责让 VPC 和 Internet 之间进行 Internet connectivity。

关系:

```text
Internet
   │
   ▼
Internet Gateway
   │
   ▼
VPC
   │
   ▼
Subnet
   │
   ▼
EC2
```

注意:

**仅仅创建 IGW 并不能让 EC2 上网。**

还需要 Route Table。

---

# 6. Route Table

```hcl
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.lab.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }

  tags = {
    Name = "ssh-port-lab-public-rt"
  }
}
```

这里非常重要。

```hcl
cidr_block = "0.0.0.0/0"
```

意思:

> 所有不是本地 VPC 的 IPv4 流量。

然后:

```hcl
gateway_id = aws_internet_gateway.lab.id
```

意思:

> 把这些流量送到 Internet Gateway。

所以:

```text
0.0.0.0/0
      │
      ▼
Internet Gateway
```

---

# 7. Route Table Association

前面只是创建了 Route Table。

现在要告诉 AWS:

> “这个 Route Table 应该用于哪个 Subnet?”

```hcl
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
```

最终:

```text
Public Subnet
10.10.1.0/24
       │
       │ association
       ▼
Public Route Table
       │
       └── 0.0.0.0/0 → IGW
```

于是这个 subnet 才真正成为 Public Subnet。

---

# 8. Security Group

```hcl
resource "aws_security_group" "ssh" {
  name        = "ssh-port-lab-sg"
  description = "Allow SSH for SSH port migration lab"
  vpc_id      = aws_vpc.lab.id
```

Security Group 可以理解成:

> EC2 的虚拟防火墙。

我们这个实验第一阶段只允许 SSH:

```text
Internet
   │
   │ TCP 22
   ▼
Security Group
   │
   ▼
EC2
```

---

## Inbound

```hcl
ingress {
  description = "SSH"
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = [var.my_ip]
}
```

这里非常重要。

```hcl
from_port = 22
to_port   = 22
```

允许:

```text
TCP/22
```

---

### `var.my_ip`

比如:

```hcl
my_ip = "1.2.3.4/32"
```

那么只有:

```text
1.2.3.4
```

可以访问:

```text
EC2:22
```

而不是:

```hcl
0.0.0.0/0
```

这样实验环境安全很多。

---

# 9. Egress

```hcl
egress {
  description = "Allow all outbound traffic"
  from_port   = 0
  to_port     = 0
  protocol    = "-1"
  cidr_blocks = ["0.0.0.0/0"]
}
```

这是:

> EC2 出去的流量全部允许。

例如:

```text
EC2
 │
 ├── yum
 ├── curl
 ├── AWS API
 └── Internet
```

都可以出去。

---

# 10. Key Pair

```hcl
resource "aws_key_pair" "lab" {
  key_name   = var.key_name
  public_key = file(var.public_key_path)
}
```

这个资源非常值得理解。

你本地:

```text
~/.ssh/id_ed25519
~/.ssh/id_ed25519.pub
```

Terraform:

```hcl
public_key = file(var.public_key_path)
```

读取:

```text
id_ed25519.pub
```

然后上传到 AWS:

```text
AWS EC2 Key Pair
```

---

## 注意一个非常重要的事情

Terraform **没有把你的 private key 上传 AWS**。

流程是:

```text
你的电脑

id_ed25519          ← Private Key
     │
     │ 不上传
     X

id_ed25519.pub      ← Public Key
     │
     ▼
Terraform
     │
     ▼
AWS Key Pair
```

所以你执行:

```bash
ssh -i ~/.ssh/id_ed25519 [email protected]
```

的时候:

```text
你的 private key
       │
       ▼
EC2 sshd
       │
       ▼
验证 AWS 中对应的 public key
```

---

# 11. AL2023 AMI

这里是整个代码里一个很典型的 **data source**:

```hcl
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
```

注意:

```text
resource
```

和:

```text
data
```

是不一样的。

---

## `resource`

例如:

```hcl
resource "aws_vpc" "lab"
```

意思:

> Terraform **创建/管理**这个 AWS 资源。

---

## `data`

```hcl
data "aws_ssm_parameter" "al2023"
```

意思:

> Terraform **查询已有的信息**。

这里 Terraform 查询 AWS Systems Manager Parameter Store:

```text
/aws/service/ami-amazon-linux-latest/
al2023-ami-kernel-default-x86_64
```

AWS 会告诉 Terraform 当前 Amazon Linux 2023 的 AMI ID。

例如可能类似:

```text
ami-xxxxxxxxxxxxxxxxx
```

这样我们就不用硬编码:

```hcl
image_id = "ami-123456789"
```

因为 Amazon Linux AMI 会更新。

---

# 12. Launch Template

```hcl
resource "aws_launch_template" "lab" {
  name_prefix = "ssh-port-lab-"

  image_id = data.aws_ssm_parameter.al2023.value

  instance_type = "t3.micro"

  key_name = aws_key_pair.lab.key_name

  vpc_security_group_ids = [
    aws_security_group.ssh.id
  ]
```

这个资源非常重要。

**Launch Template 可以理解成 EC2 的“启动模板”。**

它定义:

```text
EC2 应该长什么样?
```

包括:

```text
AMI
Instance Type
SSH Key
Security Group
User Data
Disk
Network
...
```

---

## AMI

```hcl
image_id = data.aws_ssm_parameter.al2023.value
```

刚才:

```text
data.aws_ssm_parameter.al2023
```

查到的是 SSM Parameter。

它的:

```text
.value
```

就是实际 AMI ID。

所以:

```text
SSM Parameter
      │
      ▼
AMI ID
      │
      ▼
Launch Template
      │
      ▼
EC2
```

---

# 13. Instance Type

```hcl
instance_type = "t3.micro"
```

就是:

```text
EC2 = t3.micro
```

我们的实验不需要很大的机器。

---

# 14. Key Name

```hcl
key_name = aws_key_pair.lab.key_name
```

又出现了 Terraform dependency。

它不是:

```hcl
key_name = "ssh-port-lab-key"
```

而是:

```text
aws_key_pair.lab
       │
       └── key_name
```

所以 Terraform 自动知道:

```text
Key Pair
   ↓
Launch Template
   ↓
EC2
```

---

# 15. Security Group

```hcl
vpc_security_group_ids = [
  aws_security_group.ssh.id
]
```

就是把刚才创建的:

```text
ssh-port-lab-sg
```

绑定到 EC2。

所以最终:

```text
EC2
 │
 └── Security Group
       │
       └── TCP 22
```

---

# 16. Tag Specification

```hcl
tag_specifications {
  resource_type = "instance"

  tags = {
    Name = "ssh-port-lab-ec2"
  }
}
```

给 EC2 打 Tag:

```text
Name = ssh-port-lab-ec2
```

这样 AWS Console 里面很容易找到。

---

# 17. ASG

最后是:

```hcl
resource "aws_autoscaling_group" "lab" {
  name = "ssh-port-lab-asg"

  min_size         = 1
  max_size         = 1
  desired_capacity = 1
```

这就是我们真正要实验的 **Auto Scaling Group**。

---

## 三个数字

```hcl
min_size         = 1
max_size         = 1
desired_capacity = 1
```

现在:

```text
最少 1
最多 1
期望 1
```

所以:

```text
ASG
 │
 └── EC2 × 1
```

如果这台 EC2 被人为 terminate:

```text
EC2
  ↓
terminated
  ↓
ASG 发现少了一台
  ↓
重新创建 EC2
```

这就是 ASG 的核心价值。

---

# 18. ASG 使用哪个 Subnet?

```hcl
vpc_zone_identifier = [
  aws_subnet.public.id
]
```

意思:

> ASG 创建的 EC2 放进这个 subnet。

所以:

```text
ASG
 │
 └── Public Subnet
       │
       └── EC2
```

---

# 19. ASG 使用哪个 Launch Template?

```hcl
launch_template {
  id      = aws_launch_template.lab.id
  version = "$Latest"
}
```

这个特别重要。

意思:

> ASG 创建 EC2 时,按照这个 Launch Template 来创建。

完整关系:

```text
                    ┌── Security Group
                    │
                    ├── Key Pair
                    │
                    └── AL2023 AMI
                            │
                            ▼
                    Launch Template
                            │
                            ▼
                           ASG
                            │
                            ▼
                           EC2
```

这就是为什么我们没有直接写:

```hcl
resource "aws_instance"
```

而是:

```text
Launch Template + ASG
```

因为我们的最终实验就是要研究:

**ASG 中 EC2 的 SSH 配置发生变化以后,如何保持 SFTP 用户侧连接不变。**

---

# 20. Health Check

```hcl
health_check_type = "EC2"
```

意思 ASG 使用 EC2 状态来判断实例是否健康。

如果 EC2:

```text
running → unhealthy/terminated
```

ASG 可以重新补一台。

---

# 21. ASG Tag

```hcl
tag {
  key                 = "Name"
  value               = "ssh-port-lab-asg-instance"
  propagate_at_launch = true
}
```

这里:

```hcl
propagate_at_launch = true
```

非常重要。

意思:

> ASG 创建 EC2 的时候,把这个 Tag 自动传给 EC2。

所以:

```text
ASG
 │
 └── Name=ssh-port-lab-asg-instance
           │
           ▼
       EC2 Instance
```

---

# 最后把整个 `main.tf` 串起来

你现在不要把这些 Terraform resource 当成孤立的代码。

真正应该理解成这个依赖图:

```text
                    AWS Provider
                         │
                         ▼
                       VPC
                    10.10.0.0/16
                         │
             ┌───────────┴───────────┐
             ▼                       ▼
       Internet Gateway          Subnet
             │                 10.10.1.0/24
             │                       │
             │                       ▼
             │                 Route Table
             │                       │
             │                 0.0.0.0/0
             │                       │
             └───────────────────────┘


        Security Group
             │
             │ TCP 22
             ▼

        Key Pair
             │
             │
             ▼

      AL2023 AMI
             │
             ▼
     Launch Template
             │
       ┌─────┴──────┐
       │            │
       ▼            ▼
    Key Pair    Security Group
       │            │
       └─────┬──────┘
             ▼
            ASG
             │
       min=1 max=1
             │
             ▼
          EC2 × 1
             │
             ▼
          SSH :22
```

### 你现在最应该记住的 5 个 Terraform 概念

结合你现在做 AWS Infra,我建议先把这五个吃透:

| Terraform   | 你这个实验中的例子                         | 含义                 |
| ----------- | --------------------------------- | ------------------ |
| `resource`  | `aws_vpc.lab`                     | **创建/管理** AWS 资源   |
| `data`      | `aws_ssm_parameter.al2023`        | **查询**已有信息         |
| `variable`  | `var.aws_region`                  | 输入参数               |
| `attribute` | `aws_vpc.lab.id`                  | 获取资源的属性            |
| dependency  | `aws_subnet.public → aws_vpc.lab` | Terraform 自动建立资源依赖 |

尤其是:

```hcl
vpc_id = aws_vpc.lab.id
```

```hcl
key_name = aws_key_pair.lab.key_name
```

```hcl
vpc_security_group_ids = [
  aws_security_group.ssh.id
]
```

```hcl
image_id = data.aws_ssm_parameter.al2023.value
```

这四种写法是你以后阅读公司 Terraform `.tf` 文件时**非常常见的模式**。

另外,你这次 `terraform plan` 显示 **9 to add**,本质上就是 Terraform 根据这些资源和依赖关系,计算出它需要创建的 AWS 对象。