使用 Terraform 建立 AWS VPC
- 作業系統:Windows 11
- Terraform 版本:1.10.5
看完前一篇已經初步認識 Terraform的基本概念,這篇會透過實際動手做一次來加深印象。 為避免越級打怪,本篇會帶領讀者一步步簡單的使用 resource 來建立 AWS VPC,下一篇再以 Module 重構。 VPC 是 AWS 提供的虛擬網路服務,能夠在雲端中建立一個隔離的網路環境,有興趣深入的讀者可以參考官方文件。
本篇主要的目標是透過 Terraform 建立以下資源:
- 1 個 VPC
- 1 個 Public Subnet
- 1 個 Private Subnet
- 2 個 Route Table
- 2 個 Network ACL
- 1 個 Internet Gateway
前置準備
在正式開始撰寫 Terraform 組態前,我們需要先把本機環境準備好,這裡以 Windows 系統為主進行說明。
安裝 Terraform
非 Windows 的開發者可參考 Install Terraform。
首先,開啟 PowerShell,輸入以下指令建立 Terraform 的安裝資料夾:
mkdir "$env:LOCALAPPDATA\Programs\Terraform\bin"
根據你的 CPU 架構下載適用版本,這裡我們下載 Windows 的 AMD64 v1.10.5 的 Terraform zip 檔,然後解壓縮到剛剛建立的資料夾。
解壓完成後,前往「使用者環境變數」,在 Path
中新增以下路徑:
%LOCALAPPDATA%\Programs\Terraform\bin
完成後重啟 PowerShell,輸入以下指令確認是否安裝成功:
terraform -version
若出現 Terraform v1.10.5 on windows_amd64 則代表安裝成功。
AWS 設置
安裝 AWS CLI
根據作業系統,依照官方文件進行安裝。
設定 AWS Profile
AWS CLI 的設定資訊會儲存在 $env:UserProfile\.aws
下,這裡我們先建立一個預設的 profile,輸入以下指令:
aws configure
依序填入:
- Access Key
- Secret Access Key
- 預設區域(如 ap-northeast-1)
- 輸出格式(如 json)
撰寫 HCL
根據官方建議,我們先建立以下幾個檔案:main.tf、variables.tf、outputs.tf、terraform.tf、providers.tf、backend.tf 和 locals.tf。建立完成後的專案結構如下:
terraform-demo/
├── backend.tf # Backend 設定
├── locals.tf # Local 變數定義
├── main.tf # 主要資源定義
├── outputs.tf # 輸出結果定義
├── providers.tf # 所有 provider 的設定
├── terraform.tf # Terraform 核心設定(版本與使用的 provider)
└── variables.tf # Input 變數宣告
定義 Terraform 與 Provider 版本
在開始撰寫 VPC 等資源之前,我們需要先定義 Terraform 所需的版本與使用的 Provider。這裡採用 Terraform 1.0 以上版本,並使用 AWS Provider 6.3.X:
# terraform.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.3.0"
}
}
}
使用本地 Backend
預設情況下,Terraform 就會使用本地 Backend,也就是把狀態檔儲存在專案資料夾中,不過這裡我們還是手動寫出來,這樣比較清楚未來要換成遠端 Backend 時該修改哪裡:
# backend.tf
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
💡 如果你省略 backend 區塊,Terraform 還是會預設使用本地狀態檔,所以這個設定不是必須,但會讓架構更清楚。
VPC
我們先從最基本的 VPC 開始。首先,在變數檔中定義 VPC 的名稱前綴以及一個 CIDR 範圍:
# variables.tf
variable "name" {
description = "Name of the VPC"
type = string
default = "demo"
}
接著,在 locals.tf 中組合出完整的 VPC 名稱,並選擇一個可用區(Availability Zone)來簡化架構。我們這裡只使用一個 AZ:
# locals.tf
data "aws_availability_zones" "available" {}
locals {
vpc_name = "${var.name}-vpc"
azs = slice(data.aws_availability_zones.available.names, 0, 1)
}
最後,在 main.tf 中定義一個 VPC,並開啟 DNS 支援:
# main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = local.vpc_name
}
}
Subnet
接下來,我們要為 VPC 建立一個 Public Subnet 和一個 Private Subnet。為了讓每個 Subnet 的 CIDR 不重疊,我們會使用 Terraform 的 cidrsubnet 函式來自動計算每個 Subnet 的網段。
在 locals.tf 中加入以下內容,分別計算 Public 和 Private Subnet 的 CIDR:
# locals.tf
locals {
public_subnets = [for k, v in local.azs : cidrsubnet(aws_vpc.main.cidr_block, 4, k)]
private_subnets = [for k, v in local.azs : cidrsubnet(aws_vpc.main.cidr_block, 4, k + 8)]
}
💡 cidrsubnet(base, newbits, netnum) 是用來從現有的 CIDR 中切割出子網。 這裡我們把 /16 切成多個 /20 子網(因為 4 = 20 - 16),並用不同的 netnum 避免重複。
接著,在 main.tf 中建立 Public 和 Private Subnet:
# main.tf
resource "aws_subnet" "public" {
count = length(local.azs)
availability_zone = element(local.azs, count.index)
cidr_block = element(local.public_subnets, count.index)
map_public_ip_on_launch = true
vpc_id = aws_vpc.main.id
tags = {
Name = format("${var.name}-subnet-public%s-%s", count.index + 1, element(local.azs, count.index))
Type = "Public"
}
}
resource "aws_subnet" "private" {
count = length(local.azs)
availability_zone = element(local.azs, count.index)
cidr_block = element(local.private_subnets, count.index)
vpc_id = aws_vpc.main.id
tags = {
Name = format("${var.name}-subnet-private%s-%s", count.index + 1, element(local.azs, count.index))
Type = "Private"
}
}
Internet Gateway
因為我們建立了 Public Subnet,接下來就需要讓這些子網能夠連上 Internet,因此要為 VPC 加上一個 Internet Gateway。
# main.tf
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name}-igw"
}
}
💡 Internet Gateway 是連接 VPC 到公網(Internet)的出入口,只有有被正確路由指向且開啟 map_public_ip_on_launch 的 Subnet,才算真正的「Public」。
Route Table
接下來,我們要設定 Route Table,讓 Subnet 能夠對外通訊。Public Subnet 需要透過 Internet Gateway 來連接 Internet,而 Private Subnet 則維持內部網路通訊。
首先為 Public Subnet 建立一張 Route Table,並設定路由讓它能透過 IGW 對外通訊:
# main.tf
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name}-rtb-public"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = element(aws_subnet.public[*].id, count.index)
route_table_id = aws_route_table.public.id
}
resource "aws_route" "public_internet_gateway" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
timeouts {
create = "5m"
}
}
Private Subnet 不需要連接 IGW,但需要跟其他子網溝通:
# main.tf
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name}-rtb-private"
}
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = element(aws_subnet.private[*].id, count.index)
route_table_id = aws_route_table.private.id
}
NACL
接下來我們為 Public 和 Private Subnet 建立對應的 NACL(Network ACL)。NACL 是一種子網層級的網路存取控制機制,可以設定允許或拒絕的流量規則。目前我們先全部開放,方便測試與開發,之後可以再視情況進行收斂。
Public Subnet 的 NACL
# main.tf
resource "aws_network_acl" "public" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.public[*].id
tags = {
Name = "${var.name}-nacl-public"
}
}
resource "aws_network_acl_rule" "public_inbound" {
network_acl_id = aws_network_acl.public.id
egress = false
rule_number = 100
rule_action = "allow"
from_port = 0
to_port = 0
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
resource "aws_network_acl_rule" "public_outbound" {
network_acl_id = aws_network_acl.public.id
egress = true
rule_number = 100
rule_action = "allow"
from_port = 0
to_port = 0
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
Private Subnet 的 NACL
# main.tf
resource "aws_network_acl" "private" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.private[*].id
tags = {
Name = "${var.name}-nacl-private"
}
}
resource "aws_network_acl_rule" "private_inbound" {
network_acl_id = aws_network_acl.private.id
egress = false
rule_number = 100
rule_action = "allow"
from_port = 0
to_port = 0
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
resource "aws_network_acl_rule" "private_outbound" {
network_acl_id = aws_network_acl.private.id
egress = true
rule_number = 100
rule_action = "allow"
from_port = 0
to_port = 0
protocol = "-1"
cidr_block = "0.0.0.0/0"
}
部署
上述的檔案與設定都完成後,就可以開始部署了。
整理格式
先使用以下指令整理 .tf 檔案的排版,讓格式更一致、易於閱讀:
terraform fmt
初始化專案
初始化 Terraform 專案,下載 provider 並建立執行環境:
terraform init
執行後會看到新增了 .terraform/
資料夾和 .terraform.lock.hcl
檔案:
.terraform/
:裡面包含 provider 的執行檔、Terraform 所使用的版本資訊,以及 backend 的初始化狀態。.terraform.lock.hcl
:記錄目前使用的 provider 版本,確保多人協作或日後執行時版本一致,避免因升級而導致非預期的變更。
套用變更
接下來就可以將定義好的資源部署到 AWS:
terraform apply
照畫面提示輸入 yes
即可開始執行。完成後,就會看到 VPC、子網、NACL 等資源已經建立在 AWS 上。這時候專案根目錄下也會出現一個 terraform.tfstate
檔案,裡面記錄了 Terraform 管理的所有資源狀態。
銷毀資源
如果是測試用資源或想清空環境,可以使用以下指令銷毀所有由 Terraform 管理的資源:
terraform destroy
跟 apply 一樣,輸入 yes
確認即可開始銷毀流程。
銷毀完成後,再去查看根目錄下的 terraform.tfstate
檔案,會發現 resources
變成空陣列,除此之外,還多了一個 terraform.tfstate.backup
檔案,裡面記錄了上一次成功套用的狀態。