v1.0 / 作成日: 2026-06-07 / IaC入門(Terraform)/ VPC + パブリックサブネット × 2 + EC2 × 2(Nginx)/ AWS CloudShell で実行
HashiCorp社が開発したオープンソースのIaCツールです。AWS・Azure・GCP・Kubernetes など 100以上のプロバイダー に対応しており、同じ構文で複数クラウドのインフラを管理できます。
AWSだけでなく、Azure・GCP・Datadogなど100以上のプロバイダーに対応。同一ツールで管理できます。
「何を作りたいか」を記述するだけで、Terraformが差分を計算して必要な操作を実行します。
作成したリソースの現在状態をStateファイル(tfstate)で管理。差分適用が正確に行えます。
terraform plan で変更内容を事前プレビュー。CloudFormationの変更セットに相当します。
| 項目 | CloudFormation(CFn) | Terraform |
|---|---|---|
| 提供元 | AWS(マネージドサービス) | HashiCorp(OSSツール) |
| 対応クラウド | AWSのみ | AWS / Azure / GCP / 他100+ |
| 構文 | YAML / JSON | HCL(HashiCorp Configuration Language) |
| 状態管理 | AWSが自動管理(スタック) | Stateファイル(tfstate)で管理 |
| 変更プレビュー | 変更セット(Change Set) | terraform plan |
| 全削除 | スタック削除 | terraform destroy |
| ロールバック | 自動ロールバック(失敗時) | 手動対応が必要 |
| ツールインストール | 不要(AWSコンソールのみ) | Terraform CLIのインストールが必要 |
| 学習曲線 | 低〜中(YAMLに慣れれば) | 中(HCL + State管理の理解が必要) |
CloudFormationハンズオンと同じ構成(VPC + パブリックサブネット×2 + EC2×2)をTerraformで作成します。同じインフラをCFnとTerraformの両方で構築することで、それぞれのアプローチの違いを体感できます。
Terraformは同じディレクトリ内のすべての .tf ファイルをまとめて読み込みます。1つのファイルにすべてを書いてもよいですが、役割で分けるのが一般的です。
| ファイル名 | 役割 | CFnでの対応箇所 |
|---|---|---|
providers.tf | 使用するプロバイダー(AWS)とバージョン | AWSTemplateFormatVersion |
variables.tf | 入力変数の定義(型・デフォルト値・バリデーション) | Parameters セクション |
main.tf | 作成するリソースの定義 | Resources セクション |
outputs.tf | apply後に表示する値 | Outputs セクション |
terraform.tfstate | リソースの現在状態(自動生成・編集禁止) | CFnのスタック状態(AWSが内部管理) |
.terraform/ | プロバイダープラグイン(init時に自動生成) | — |
# プロバイダー設定 provider "aws" { region = "ap-northeast-1" } # 変数の定義 variable "instance_type" { type = string default = "t2.micro" } # リソースの定義: resource "リソースタイプ" "リソース名(ローカル名)" { ... } resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" tags = { Name = "handson-vpc" } } # 既存リソースを参照するデータソース data "aws_ssm_parameter" "ami" { name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" } # 出力値の定義 output "vpc_id" { value = aws_vpc.main.id # リソース参照: タイプ.名前.属性 }
| 参照の種類 | 構文 | CFnでの対応 |
|---|---|---|
| 他リソースのID | aws_vpc.main.id | !Ref VPC |
| 他リソースの属性 | aws_instance.web1.public_ip | !GetAtt WebServer1.PublicIp |
| 変数の値 | var.instance_type | !Ref InstanceType |
| データソース | data.aws_ssm_parameter.ami.value | {{resolve:ssm:/aws/service/...}} |
| 文字列に埋め込み | "${var.env_name}-vpc" | !Sub ${EnvironmentName}-vpc |
providers.tf / variables.tf / main.tf / outputs.tfsudo yum install -y yum-utils sudo yum-config-manager --add-repo \ https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo sudo yum -y install terraform
terraform version
Terraform v1.x.x
on linux_amd64
/home/cloudshell-user/ 以下のファイル(.tfファイル、terraform.tfstate)は セッションをまたいでも保持 されます。ただし、インストールしたパッケージ(terraform本体)はセッション再開時に消えます。CloudShellセッションを再開した場合は手順3-1のインストールをやり直してください。ローカルPC(Windows)で実行する場合は以下の手順でセットアップします:
# PowerShell(管理者不要)
winget install --id Hashicorp.Terraform
terraform version
# IAMユーザーのアクセスキーを設定 aws configure AWS Access Key ID: 【アクセスキーID】 AWS Secret Access Key: 【シークレットアクセスキー】 Default region name: ap-northeast-1 Default output format: json
aws configure で設定が必要です。アクセスキーはIAMコンソール → ユーザー → 「セキュリティ認証情報」タブから作成できます。mkdir ~/tf-handson && cd ~/tf-handson
cat > providers.tf <<'EOF'
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
EOF
cat > variables.tf <<'EOF'
variable "aws_region" {
description = "AWSリージョン"
type = string
default = "ap-northeast-1"
}
variable "environment_name" {
description = "リソース名のプレフィックス"
type = string
default = "handson"
}
variable "instance_type" {
description = "EC2インスタンスタイプ"
type = string
default = "t2.micro"
}
EOF
cat > main.tf <<'EOF'
# AMI自動取得(SSMパラメーターストア)
data "aws_ssm_parameter" "al2023_ami" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "${var.environment_name}-vpc" }
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.environment_name}-igw" }
}
# Subnets
resource "aws_subnet" "public_1a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = { Name = "${var.environment_name}-subnet-public-1a" }
}
resource "aws_subnet" "public_1c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
tags = { Name = "${var.environment_name}-subnet-public-1c" }
}
# Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "${var.environment_name}-rt-public" }
}
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_1c" {
subnet_id = aws_subnet.public_1c.id
route_table_id = aws_route_table.public.id
}
# Security Group
resource "aws_security_group" "web" {
name = "${var.environment_name}-sg-web"
description = "Web server security group"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from internet"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${var.environment_name}-sg-web" }
}
# IAM Role (Session Manager)
resource "aws_iam_role" "ec2_ssm" {
name = "${var.environment_name}-ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Principal = { Service = "ec2.amazonaws.com" }, Action = "sts:AssumeRole" }]
})
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ec2_ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_ssm" {
name = "${var.environment_name}-ec2-instance-profile"
role = aws_iam_role.ec2_ssm.name
}
# EC2 Instance 1
resource "aws_instance" "web1" {
ami = data.aws_ssm_parameter.al2023_ami.value
instance_type = var.instance_type
subnet_id = aws_subnet.public_1a.id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.ec2_ssm.name
user_data = <<-SCRIPT
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>${var.environment_name} - WebServer 1 (1a) - v1</h1>" \
> /usr/share/nginx/html/index.html
SCRIPT
tags = { Name = "${var.environment_name}-web-1a", Version = "v1" }
}
# EC2 Instance 2
resource "aws_instance" "web2" {
ami = data.aws_ssm_parameter.al2023_ami.value
instance_type = var.instance_type
subnet_id = aws_subnet.public_1c.id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.ec2_ssm.name
user_data = <<-SCRIPT
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>${var.environment_name} - WebServer 2 (1c) - v1</h1>" \
> /usr/share/nginx/html/index.html
SCRIPT
tags = { Name = "${var.environment_name}-web-1c", Version = "v1" }
}
EOF
cat > outputs.tf <<'EOF'
output "vpc_id" {
description = "作成されたVPCのID"
value = aws_vpc.main.id
}
output "web_server_1_url" {
description = "WebServer1のURL"
value = "http://${aws_instance.web1.public_ip}"
}
output "web_server_2_url" {
description = "WebServer2のURL"
value = "http://${aws_instance.web2.public_ip}"
}
EOF
ファイルが作成されたことを確認:
ls -la ~/tf-handson/
total 20
-rw-r--r-- 1 ... main.tf
-rw-r--r-- 1 ... outputs.tf
-rw-r--r-- 1 ... providers.tf
-rw-r--r-- 1 ... variables.tf
cd ~/tf-handson terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.x.x...
- Installed hashicorp/aws v5.x.x (signed by HashiCorp)
Terraform has been successfully initialized!
providers.tf で指定したAWSプロバイダープラグインをダウンロードして .terraform/ ディレクトリに保存します。新しいディレクトリでTerraformを使い始めるときや、providers.tfを変更したとき必ず実行します。terraform plan
Terraform will perform the following actions: # aws_iam_instance_profile.ec2_ssm will be created + resource "aws_iam_instance_profile" "ec2_ssm" { ... } # aws_iam_role.ec2_ssm will be created + resource "aws_iam_role" "ec2_ssm" { ... } # aws_instance.web1 will be created + resource "aws_instance" "web1" { + ami = "ami-xxxxxxxxxxxxxxxxx" + instance_type = "t2.micro" ... } # aws_vpc.main will be created + resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" ... } Plan: 12 to add, 0 to change, 0 to destroy.
| 記号 | 色 | 意味 | CFnの変更セットでの表記 |
|---|---|---|---|
+ | 緑 | 追加(新規作成) | Add |
~ | オレンジ | 変更(インプレース更新) | Modify(Replace: False) |
-/+ | 赤 | 置き換え(削除→再作成) | Modify(Replace: True)⚠️ |
- | 赤 | 削除 | Remove |
terraform plan はプレビューのみで実際のAWSリソースには何も変更を加えません。CFnの変更セット作成に相当します。Plan: 12 to add, 0 to change, 0 to destroy. と表示されれば次へ進めます。terraform apply
planの内容が再表示された後、確認プロンプトが表示されます:
Plan: 12 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes
aws_vpc.main: Creating... aws_vpc.main: Creation complete after 2s [id=vpc-xxxxxxxxxx] aws_internet_gateway.main: Creating... ... aws_instance.web1: Creation complete after 12s [id=i-xxxxxxxxxx] aws_instance.web2: Creation complete after 12s [id=i-xxxxxxxxxx] Apply complete! Resources: 12 added, 0 changed, 0 destroyed. Outputs: vpc_id = "vpc-xxxxxxxxxx" web_server_1_url = "http://xx.xx.xx.xx" web_server_2_url = "http://yy.yy.yy.yy"
terraform apply -auto-approve とすると確認プロンプトをスキップできます。CIパイプラインでの自動実行時に使用しますが、学習中は手動確認を推奨します。ls -la
-rw-r--r-- 1 ... main.tf
-rw-r--r-- 1 ... outputs.tf
-rw-r--r-- 1 ... providers.tf
-rw-r--r-- 1 ... terraform.tfstate ← apply後に自動生成
-rw-r--r-- 1 ... variables.tf
# Stateファイルの内容を確認(管理中のリソース一覧)
terraform state list
data.aws_ssm_parameter.al2023_ami
aws_iam_instance_profile.ec2_ssm
aws_iam_role.ec2_ssm
aws_iam_role_policy_attachment.ssm
aws_instance.web1
aws_instance.web2
aws_internet_gateway.main
aws_route_table.public
aws_route_table_association.public_1a
aws_route_table_association.public_1c
aws_security_group.web
aws_subnet.public_1a
aws_subnet.public_1c
aws_vpc.main
apply完了時のOutputsに表示された web_server_1_url の値(http://xx.xx.xx.xx)をブラウザで開きます。
Outputsを再表示したい場合は以下コマンドを使います:
terraform output
vpc_id = "vpc-xxxxxxxxxx"
web_server_1_url = "http://xx.xx.xx.xx"
web_server_2_url = "http://yy.yy.yy.yy"
既存のリソースを変更します。CFnのスタック更新に相当します。terraform plan で差分を確認してから terraform apply で適用します。
| 変更箇所 | 変更種別 | 内容 | インスタンス置き換え |
|---|---|---|---|
| aws_security_group.web | 追加 | HTTPS(TCP:443)のingressルールを追加 | なし(インプレース更新) |
| aws_instance.web1, web2 | 変更 | タグ Version = "v1" → "v2" |
なし(インプレース更新) |
CloudShellのエディタ(vi / nano)で main.tf を開き、以下の変更を加えます:
① セキュリティグループにHTTPS(443)を追加:
# main.tf の aws_security_group.web の ingress ブロックの後に追加 ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] description = "HTTP from internet" } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] description = "HTTPS from internet" }
② EC2タグのVersionをv1→v2に変更(web1とweb2の両方):
tags = { Name = "${var.environment_name}-web-1a", Version = "v1" } tags = { Name = "${var.environment_name}-web-1a", Version = "v2" }
または sed コマンドで一括置換:
# セキュリティグループにHTTPS ingress を追加 sed -i '/description = "HTTP from internet"/a\ }\n ingress {\n from_port = 443\n to_port = 443\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0\/0"]\n description = "HTTPS from internet"' main.tf # Version タグを v1 → v2 に変更 sed -i 's/Version = "v1"/Version = "v2"/g' main.tf
terraform plan
Terraform will perform the following actions: # aws_security_group.web will be updated in-place ~ resource "aws_security_group" "web" { + ingress { + cidr_blocks = ["0.0.0.0/0"] + description = "HTTPS from internet" + from_port = 443 + protocol = "tcp" + to_port = 443 } } # aws_instance.web1 will be updated in-place ~ resource "aws_instance" "web1" { ~ tags = { ~ "Version" = "v1" -> "v2" } } # aws_instance.web2 will be updated in-place ~ resource "aws_instance" "web2" { ... } Plan: 0 to add, 3 to change, 0 to destroy.
~(チルダ)はインプレース更新です。インスタンスの置き換えなし(-/+)でないことを確認してからapplyします。Plan: 0 to add, 3 to change, 0 to destroy. が正しい結果です。terraform apply
確認プロンプトに yes を入力します。
aws_security_group.web: Modifying...
aws_instance.web1: Modifying...
aws_instance.web2: Modifying...
Apply complete! Resources: 0 added, 3 changed, 0 destroyed.
AWSコンソール → VPC → セキュリティグループ → handson-sg-web のインバウンドルールに HTTPS(443) が追加されていることを確認します。
Stateで管理しているすべてのリソースを一括削除します。CFnのスタック削除に相当します。
terraform destroy
Terraform will perform the following actions: # aws_instance.web1 will be destroyed - resource "aws_instance" "web1" { ... } # aws_instance.web2 will be destroyed - resource "aws_instance" "web2" { ... } # aws_vpc.main will be destroyed - resource "aws_vpc" "main" { ... } ... Plan: 0 to add, 0 to change, 12 to destroy. Do you really want to destroy all resources? Only 'yes' will be accepted to approve. Enter a value: yes
aws_instance.web1: Destroying... aws_instance.web2: Destroying... ... aws_vpc.main: Destroying... aws_vpc.main: Destruction complete after 0s Destroy complete! Resources: 12 destroyed.
terraform state list
# 何も表示されなければ全削除完了
cat terraform.tfstate | python3 -c "import sys,json; d=json.load(sys.stdin); print('Resources:', len(d.get('resources', [])))"
Resources: 0
# providers.tf に以下を追加してリモートバックエンドを設定 terraform { backend "s3" { bucket = "my-terraform-state-bucket" key = "handson/terraform.tfstate" region = "ap-northeast-1" dynamodb_table = "terraform-state-lock" # 排他制御用 encrypt = true } }