AWS Terraform ハンズオン手順書

v1.0 / 作成日: 2026-06-07 / IaC入門(Terraform)/ VPC + パブリックサブネット × 2 + EC2 × 2(Nginx)/ AWS CloudShell で実行

目次

  1. Terraformとは(概念の整理)
  2. HCL構文とファイル構成の解説
  3. 前提条件・環境セットアップ
  4. ハンズオン① init / plan / apply(インフラ構築)
    1. 作業ディレクトリとファイルを作成
    2. terraform init(初期化)
    3. terraform plan(プレビュー)
    4. terraform apply(適用)
    5. 動作確認
  5. ハンズオン② 設定変更と terraform plan / apply
  6. ハンズオン③ terraform destroy(全削除)
  7. 完了チェックリスト

1. Terraformとは(概念の整理)

Terraformの特徴

HashiCorp社が開発したオープンソースのIaCツールです。AWS・Azure・GCP・Kubernetes など 100以上のプロバイダー に対応しており、同じ構文で複数クラウドのインフラを管理できます。

🌍 マルチクラウド

AWSだけでなく、Azure・GCP・Datadogなど100以上のプロバイダーに対応。同一ツールで管理できます。

📄 宣言的構文(HCL)

「何を作りたいか」を記述するだけで、Terraformが差分を計算して必要な操作を実行します。

🗂️ Stateによる管理

作成したリソースの現在状態をStateファイル(tfstate)で管理。差分適用が正確に行えます。

👁️ Plan で安全確認

terraform plan で変更内容を事前プレビュー。CloudFormationの変更セットに相当します。

Terraformのワークフロー

terraform init
初期化
プロバイダー取得
terraform plan
プレビュー
差分確認
terraform apply
適用
リソース作成/変更
(変更繰り返し)
plan → apply
を繰り返す
terraform destroy
全削除

CloudFormationとの比較

項目CloudFormation(CFn)Terraform
提供元AWS(マネージドサービス)HashiCorp(OSSツール)
対応クラウドAWSのみAWS / Azure / GCP / 他100+
構文YAML / JSONHCL(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の両方で構築することで、それぞれのアプローチの違いを体感できます。

2. HCL構文とファイル構成の解説

ファイル構成

Terraformは同じディレクトリ内のすべての .tf ファイルをまとめて読み込みます。1つのファイルにすべてを書いてもよいですが、役割で分けるのが一般的です。

ファイル名役割CFnでの対応箇所
providers.tf使用するプロバイダー(AWS)とバージョンAWSTemplateFormatVersion
variables.tf入力変数の定義(型・デフォルト値・バリデーション)Parameters セクション
main.tf作成するリソースの定義Resources セクション
outputs.tfapply後に表示する値Outputs セクション
terraform.tfstateリソースの現在状態(自動生成・編集禁止)CFnのスタック状態(AWSが内部管理)
.terraform/プロバイダープラグイン(init時に自動生成)

HCLの基本構文

HCL構文の骨格
# プロバイダー設定
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での対応
他リソースのIDaws_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

今回使用するTerraformファイル

ファイル構成本手順書と同梱の以下4ファイルをCloudShell上に作成します(内容は手順4-1で詳述):
providers.tfvariables.tfmain.tfoutputs.tf

3. 前提条件・環境セットアップ

実行環境:AWS CloudShell(推奨)本手順書はAWS CloudShell上でのTerraform実行を基本とします。AWS認証情報が自動設定済みのため追加設定不要です。

CloudShellでTerraformをインストール

1
HashiCorpリポジトリを追加してTerraformをインストール
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo \
  https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform
2
インストール確認
terraform version
Terraform v1.x.x
on linux_amd64
注意:CloudShellとTerraformのインストールCloudShellの /home/cloudshell-user/ 以下のファイル(.tfファイル、terraform.tfstate)は セッションをまたいでも保持 されます。ただし、インストールしたパッケージ(terraform本体)はセッション再開時に消えます。CloudShellセッションを再開した場合は手順3-1のインストールをやり直してください。

ローカルPCへのインストール(補足)

ローカルPC(Windows)で実行する場合は以下の手順でセットアップします:

1
Terraform CLIのインストール(winget)
# PowerShell(管理者不要)
winget install --id Hashicorp.Terraform
terraform version
2
AWS認証情報の設定
# IAMユーザーのアクセスキーを設定
aws configure
AWS Access Key ID: 【アクセスキーID】
AWS Secret Access Key: 【シークレットアクセスキー】
Default region name: ap-northeast-1
Default output format: json
CloudShellとの違いCloudShellではAWS認証情報が自動設定されますが、ローカルPCでは aws configure で設定が必要です。アクセスキーはIAMコンソール → ユーザー → 「セキュリティ認証情報」タブから作成できます。

4. ハンズオン① init / plan / apply(インフラ構築)

Terraform

4-1. 作業ディレクトリとファイルを作成

1
作業ディレクトリを作成
mkdir ~/tf-handson && cd ~/tf-handson
2
providers.tf を作成
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
3
variables.tf を作成
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
4
main.tf を作成
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
5
outputs.tf を作成
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

4-2. terraform init(初期化)

1
初期化を実行
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!
terraform initとはproviders.tf で指定したAWSプロバイダープラグインをダウンロードして .terraform/ ディレクトリに保存します。新しいディレクトリでTerraformを使い始めるときや、providers.tfを変更したとき必ず実行します。

4-3. terraform plan(プレビュー)

1
変更内容をプレビュー
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
planは何も変更しないterraform plan はプレビューのみで実際のAWSリソースには何も変更を加えません。CFnの変更セット作成に相当します。Plan: 12 to add, 0 to change, 0 to destroy. と表示されれば次へ進めます。

4-4. terraform apply(適用)

1
applyを実行してリソースを作成
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"
自動承認(-auto-approveオプション)terraform apply -auto-approve とすると確認プロンプトをスキップできます。CIパイプラインでの自動実行時に使用しますが、学習中は手動確認を推奨します。
2
Stateファイルを確認
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
terraform.tfstateは編集禁止Stateファイルはリソースの現在状態を追跡するTerraformの内部ファイルです。手動で編集すると整合性が崩れます。チーム開発ではS3バケットに保存(リモートバックエンド)して共有するのが標準的です。

4-5. 動作確認

1
OutputsのURLにアクセス

apply完了時のOutputsに表示された web_server_1_url の値(http://xx.xx.xx.xx)をブラウザで開きます。

成功「handson - WebServer 1 (1a) - v1」と表示されればNginxが動作しています。
表示されない場合EC2起動後UserDataの実行に2〜3分かかります。しばらく待ってリロードしてください。

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"

5. ハンズオン② 設定変更と terraform plan / apply

Terraform

既存のリソースを変更します。CFnのスタック更新に相当します。terraform plan で差分を確認してから terraform apply で適用します。

変更内容(v1 → v2)

変更箇所変更種別内容インスタンス置き換え
aws_security_group.web 追加 HTTPS(TCP:443)のingressルールを追加 なし(インプレース更新)
aws_instance.web1, web2 変更 タグ Version = "v1""v2" なし(インプレース更新)
1
main.tf を編集(viまたはsed)

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
2
terraform plan で差分を確認
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. が正しい結果です。
3
terraform apply で差分を適用
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) が追加されていることを確認します。

6. ハンズオン③ terraform destroy(全削除)

Terraform

Stateで管理しているすべてのリソースを一括削除します。CFnのスタック削除に相当します。

1
terraform destroy を実行
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はリソース間の依存関係を自動解析し、正しい順序で削除します(EC2 → SG → Subnet → VPC の順など)。CFnのスタック削除と同様に、手動で順番を気にする必要はありません。
2
Stateが空になったことを確認
terraform state list
# 何も表示されなければ全削除完了
cat terraform.tfstate | python3 -c "import sys,json; d=json.load(sys.stdin); print('Resources:', len(d.get('resources', [])))"
Resources: 0

Stateのポイント(チーム開発での注意)

リモートバックエンド(本番推奨)今回はStateファイルがローカル(CloudShell上)に保存されます。チームで共同作業する場合は、Stateファイルを S3 + DynamoDB で管理する「リモートバックエンド」の設定が必要です。以下が設定例です:
# 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
  }
}

7. 完了チェックリスト

概念の理解

ハンズオン①:init / plan / apply

ハンズオン②:変更適用

ハンズオン③:全削除