1. 初めに
今回は、前回の投稿で作成したAWS構成を、Terraformを利用してコード化してみました。
Terraformの文法はまだ勉強中のため、今回はChatGPTも活用しながら構築を進めました。
ただコードをコピーするだけではなく、実際に手で入力しながら、ChatGPTが生成したコードの内容を分析し、TerraformやAWS構成への理解を深めることを意識しました。
2. 全体設計
全体構成図は下記の記事をご参照ください。
3. Terraform
Terraformでは、役割やサービスごとに設定ファイルを分割して管理することが一般的です。
実際にファイルを分けてみると、以下のようなメリットがあると感じました。
- コードの役割が分かりやすい
- どのAWSサービスを利用しているのか把握しやすい
- 一部のコードを再利用しやすい
今回は、下記の8つのファイルに分けて構築しました。
- provider.tf
- network.tf
- security.tf
- iam.tf
- ec2.tf
- alb.tf
- route53.tf
- acm.tf
1. provider.tf
provider.tfでは、Terraformで利用するプロバイダー情報を定義します。
今回はAWSを利用するため、利用するサービスとバージョン、リージョンを設定します。
terraform { # 利用するProviderを明示的に定義します。
required_providers {
aws = {
source = "hashicorp/aws" # Terraform公式のAWS Providerを利用する設定です。
version = "~> 5.0" # AWS Providerのバージョンを指定します。(5.x系)
}
}
}
provider "aws" {
region = "ap-northeast-1" # AWS regionを設定します。(Tokyo)
}
terraform {} は必須ではありませんが、Providerのバージョン固定や、利用するProviderを明示するために記載しています。
2. network.tf
network.tfでは、VPC、Subnet、Route Table など、
ネットワークに関するリソースを定義します。
VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16" # VPCの CIDRブロックを指定
enable_dns_support = true # DNS解決を有効化 (default : true)
enable_dns_hostnames = false # DNSホスト名を有効化 (default : false)
tags = {
Name = "seenew-vpc"
}
}
enable_dns_support と enable_dns_hostnames は、
AWSコンソール上の「DNS設定」に該当します。
default値と同じですが、学習のため明示的に記載しました。
default値は、実際にコードを書かずに実行し、Console上でどのような設定になるか確認しました。
Subnet
まずは `Public_1a`のサブネットを書きます。resource "aws_subnet" "public_1a" {
cidr_block = "10.0.0.0/24" # Subnet の CIDRブロックを指定
vpc_id = aws_vpc.main.id # Subnetが属する VPCのIDを指定
availability_zone = "ap-northeast-1a" # Subnetの AZを指定
tags = {
Name = "seenew-vpc"
}
}
Subnetは上記のようにコードを作成しました。
コンソールで設定した項目と、ほぼ同じ内容をコードで記述する形でした。
作成後、terraform plan でどの設定に作成されるのか確認してみました。

ここで map_public_ip_on_launch は Public IPv4アドレスを自動割り当てに該当する項目で default値はfalseになっていることが確認できました。
他のサブネットも同様に作成します。
Private_1a
resource "aws_subnet" "private_1a" {
cidr_block = "10.0.1.0/24"
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1a"
tags = {
Name = "seenew-private-1a"
}
}
Public_1c
resource "aws_subnet" "public_1c" {
cidr_block = "10.0.2.0/24"
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
tags = {
Name = "seenew-public-1c"
}
}
Private_1c
resource "aws_subnet" "private_1c" {
cidr_block = "10.0.3.0/24"
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
tags = {
Name = "seenew-private-1c"
}
}
Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id # VPCにアタッチ
tags = {
Name = "seenew-igw"
}
}
インターネットゲートウェイのコードは比較的に簡単でした。
Elastic IP
NATゲートウェイ作成のため、EIPも設定します。
resource "aws_eip" "nat" {
}
EIPはリソースを定義するだけで、
追加設定を書かなくても作成されます。
NAT Gateway
resource "aws_nat_gateway" "main" {
subnet_id = aws_subnet.public_1a.id # NAT Gatewayを配置するSubnet
allocation_id = aws_eip.nat.id # 利用するEIP
tags = {
Name = "seenew-nat"
}
}
コンソール設定と同様に、NAT Gatewayを配置するSubnetと、利用するEIPを指定します。
allocation_id を書かずに実行したところ、terraform plan ではエラーが出ませんでしたが、terraform apply 実行時にエラーが発生しました。
Route Table
Route Tableは以下の流れで設定します。
Route Table作成 → Route作成 → Route TableのSubnet関連付け
# 1. Route Table 作成
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = {
Name = "seenew-public-rtb"
}
}
# 2. Route 作成
resource "aws_route" "public_igw" {
route_table_id = aws_route_table.public.id # このRouteを使うRoute Table
destination_cidr_block = "0.0.0.0/0" # 送信元
gateway_id = aws_internet_gateway.main.id # ターゲット
}
#. 3. Route TableのSubnet関連付け
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id # 関連付けする Subnet
route_table_id = aws_route_table.public.id # 関連付けする Route Table
}
resource "aws_route_table_association" "public_1c" {
subnet_id = aws_subnet.public_1c.id
route_table_id = aws_route_table.public.id
}
Private Route
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "seenew-private-rtb"
}
}
resource "aws_route" "private_nat" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
resource "aws_route_table_association" "private_1a" {
subnet_id = aws_subnet.private_1a.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "private_1c" {
subnet_id = aws_subnet.private_1c.id
route_table_id = aws_route_table.private.id
}
3. security.tf
security.tfでは、Security Groupを定義します。
ALB用Security Group
resource "aws_security_group" "sg_alb" {
vpc_id = aws_vpc.main.id # VPCの指定
description = "Security Group for ALB" # 説明
name = "seenew-sg-alb" # セキュリティグループ名
tags = {
Name = "seenew-sg-alb" # Tags Name
}
}
resource "aws_vpc_security_group_ingress_rule" "sg_alb_http" { # インバウンド設定
security_group_id = aws_security_group.sg_alb.id # 設定するSecurity Group
from_port = 80
to_port = 80
ip_protocol = "TCP"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_vpc_security_group_ingress_rule" "sg_alb_https" {
security_group_id = aws_security_group.sg_alb.id
from_port = 443
to_port = 443
ip_protocol = "TCP"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_vpc_security_group_egress_rule" "sg_alb_all" { # アウトバウンド設定
security_group_id = aws_security_group.sg_alb.id
ip_protocol = "-1" # すべてのトラフィック
cidr_ipv4 = "0.0.0.0/0"
}
Security Group設定では、通常の name と tags の Name が両方存在していたため、違いを確認してみました。

name を削除して実行したところ、スクリーンショットのようにterraform-xxxxx のような自動生成名が設定されました。
実行自体には問題ありませんでしたが、見やすさのため、両方とも設定するほうがいいと思いました。
EC2用Security Group
resource "aws_security_group" "sg_ec2" {
vpc_id = aws_vpc.main.id
description = "Security Group for EC2"
name = "seenew-sg-ec2"
tags = {
Name = "seenew-sg-ec2"
}
}
resource "aws_vpc_security_group_ingress_rule" "sg_ec2_http" {
security_group_id = aws_security_group.sg_ec2.id
from_port = 80
to_port = 80
ip_protocol = "TCP"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_vpc_security_group_egress_rule" "sg_ec2_all" {
security_group_id = aws_security_group.sg_ec2.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
4. iam.tf
iam.tfでは Role、Policy、Userなど IAM関連のリソースを定義します。
resource "aws_iam_role" "ec2_ssm" {
name = "seenew_role_ec2_ssm" # IAMロール名
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { # EC2サービスにロール利用を許可
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole" # ロール引き受け権限
}
]
})
}
resource "aws_iam_role_policy_attachment" "ssmcore" {
# PolicyをアタッチするIAMロール
role = aws_iam_role.ec2_ssm.name
# SSM接続用AWS管理ポリシー
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_profile" {
# EC2に付与するIAMロール
role = aws_iam_role.ec2_ssm.name
name = "seenew-instance-profile-ec2"
}
コンソールで設定する時は、ロール作成後にポリシーを検索してアタッチするだけでしたが、Terraformでは assume_role_policyの内容までコードで定義する必要があり、少し難しく感じました。
ただ、コードで書くことで、EC2がどの権限を利用しているのか理解しやすくなったと思いました。
5. ec2.tf
ec2.tfでは EC2について定義します。
WEB1
# amazon linux 2023の最新バージョン
data "aws_ssm_parameter" "amazon_linux_2023" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
resource "aws_instance" "web_1" {
# 最新のamazon linux 2023の イメージを利用
ami = data.aws_ssm_parameter.amazon_linux_2023.value
instance_type = "t3.micro" # インスタンスタイプ
subnet_id = aws_subnet.private_1a.id # 配置する Subnet
vpc_security_group_ids = [aws_security_group.sg_ec2.id] # Security Group指定
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name # IAMロールアタッチ
# EC2起動時、下記のコマンドが実行され、nginxをインストールする
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Hello From Web1</h1>" > /usr/share/nginx/html/index.html
EOF
tags = {
Name = "seenew-ec2-web-1"
}
}
WEB2
resource "aws_instance" "web_2" {
# 最新のamazon linux 2023の イメージを利用
ami = data.aws_ssm_parameter.amazon_linux_2023.value
instance_type = "t3.micro" # インスタンスタイプ
subnet_id = aws_subnet.private_1c.id # 配置する Subnet
vpc_security_group_ids = [aws_security_group.sg_ec2.id] # Security Group指定
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name # IAMロールアタッチ
# EC2起動時、下記のコマンドが実行され、nginxをインストールする
user_data = <<-EOF
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Hello From Web2</h1>" > /usr/share/nginx/html/index.html
EOF
tags = {
Name = "seenew-ec2-web-2"
}
}
6. alb.tf
alb.tfではALB、リスナー、ターゲットグループなど、ALBに関するリソースを定義します。
ALB
resource "aws_lb" "main" {
name = "seenew-alb"
load_balancer_type = "application" # Application Load Balancerで作成
internal = false # スキーム : インターネット向け
ip_address_type = "ipv4"
security_groups = [aws_security_group.sg_alb.id] # seenew-sg-alb指定
subnets = [aws_subnet.public_1a.id, aws_subnet.public_1c.id] # Public Subnet指定
tags = {
Name = "seenew-alb"
}
}
ターゲットグループ
resource "aws_lb_target_group" "web_1" {
vpc_id = aws_vpc.main.id # インスタンスがあるVPCを指定
name = "seenew-alb-tg-web-1" # ターゲットグループ名
target_type = "instance" # ターゲット種類 : インスタンス
protocol = "HTTP" # ALB ⇔ TG 通信用Protocol
port = 80 # ターゲットの受信ポート
protocol_version = "HTTP1" # プロトコルバージョン
health_check {
protocol = "HTTP" # ヘルスチェックの通信に使うプロトコル
path = "/" # ヘルスチェックする位置
}
tags = {
Name = "seenew-alb-tg-web-1"
}
}
resource "aws_lb_target_group_attachment" "web_1" {
target_group_arn = aws_lb_target_group.web_1.arn # 対象ターゲットグループ
target_id = aws_instance.web_1.id # 対象インスタンス EC2_Web-1
port = 80
}
resource "aws_lb_target_group" "web_2" {
vpc_id = aws_vpc.main.id
name = "seenew-alb-tg-web-2"
target_type = "instance"
protocol = "HTTP"
port = 80
protocol_version = "HTTP1"
health_check {
protocol = "HTTP"
path = "/"
}
tags = {
Name = "seenew-alb-tg-web-2"
}
}
resource "aws_lb_target_group_attachment" "web_2" {
target_group_arn = aws_lb_target_group.web_2.arn
target_id = aws_instance.web_2.id
port = 80
}
リスナー
resource "aws_lb_listener" "http" { # リスナー 1
load_balancer_arn = aws_lb.main.arn # 対象ALB
port = 80 # 外部から80番ポートで受信した場合
protocol = "HTTP"
default_action { # アクション
type = "redirect" # リダイレクトする
redirect {
port = "443" # HTTPSへ
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "https" { # リスナー 2
load_balancer_arn = aws_lb.main.arn # 対象ALB
port = 443 # 外部から443番ポートで受信した場合
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09" # 暗号化アルゴリズム
certificate_arn = aws_acm_certificate_validation.main.certificate_arn # 証明書
# ターゲットグループ Web-1へ転送
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web_1.arn
}
}
resource "aws_lb_listener_rule" "https_sub" { # リスナー 2 ルール
listener_arn = aws_lb_listener.https.arn # 対象リスナー
priority = 10 # 優先度
condition { # 条件
host_header {
values = ["sub.seenewlab.site"] # sub.seenewlab.siteの場合
}
}
# ターゲットグループ Web-2へ転送
action {
type = "forward"
target_group_arn = aws_lb_target_group.web_2.arn
}
}
certificate_arnはまだ定義してないため、実行するとエラーが出ます。
acm.tfを作成するとエラーがなくなります。
7. route53.tf
route53.tfでは、route53に関するリソースを定義します。
# 既にホストゾーンは登録されているため、データで取得する
data "aws_route53_zone" "main" {
name = "seenewlab.site" # 対象 ホストゾーン
private_zone = false
}
# ルートレコードを作成
resource "aws_route53_record" "root" {
name = "seenewlab.site"
type = "A"
zone_id = data.aws_route53_zone.main.zone_id
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}
# サブレコードを作成
resource "aws_route53_record" "wildcard" {
name = "*.seenewlab.site"
type = "A"
zone_id = data.aws_route53_zone.main.zone_id
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}
8. acm.tf
acm.tfでは、ACMに関するリソースを定義します。
resource "aws_acm_certificate" "main" {
# 証明書を発行するドメイン
domain_name = "seenewlab.site"
# サブドメイン用証明書
subject_alternative_names = ["*.seenewlab.site"]
# DNS認証を利用
validation_method = "DNS"
# RSA 2048bit鍵を利用
key_algorithm = "RSA_2048"
lifecycle {
# 証明書更新時、新しい証明書作成後に古い証明書を削除
create_before_destroy = true
}
}
resource "aws_route53_record" "acm_validation" {
# 利用するHosted Zone
zone_id = data.aws_route53_zone.main.zone_id
# ACMで自動生成されたDNS検証レコード名
name = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_name
# DNSレコードタイプ(CNAME)
type = tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_type
# ACM検証用レコード値
records = [tolist(aws_acm_certificate.main.domain_validation_options)[0].resource_record_value]
# DNS TTL
ttl = 60
# 同名レコード存在時は上書き許可
allow_overwrite = true
}
resource "aws_acm_certificate_validation" "main" {
# 検証する証明書ARN
certificate_arn = aws_acm_certificate.main.arn
# Route53で作成したDNS検証レコード
validation_record_fqdns = [aws_route53_record.acm_validation.fqdn]
}
4. 動作確認
作成したファイルを保存したフォルダーを VSCode で開き、
上部メニューの Terminal → New Terminal をクリックします。
VSCode下部にターミナルが表示されたら、terraform init を実行し、初期設定を行います。
terraform init 完了後、terraform plan を実行して、Terraformがどのような構成を作成する予定か確認します。
問題なく実行できたら、terraform apply を実行して構築します。
作成が完了しました。
正常に作られたのか確認します。
正常に動作することが確認できました。
検証完了後は、Terminalで terraform destroy を実行し、作成したリソースを一括削除します。
5. トラブルシューティング
-
route53.tf : 最初は既存のHosted Zoneを利用するつもりでしたが、
data "aws_route53_zone"ではなく、resource "aws_route53_zone"を使用していました。
その結果、既存のHosted Zoneを参照するのではなく、同じドメインのHosted Zoneが新規作成されてしまいました。
既存のHosted Zoneを利用するため、data "aws_route53_zone"に修正して解決しました。 -
acm.tf : ACM証明書でルートドメイン
seenewlab.siteとワイルドカードドメイン*.seenewlab.siteを登録したところ、同じDNS検証レコードが生成されました。
その状態でTerraformからRoute53へレコードを登録すると、既に同名のレコードが存在するためエラーが発生しました。
今回はACMが生成した検証レコードのうち、1件のみを利用するように修正し、正常に証明書を発行できるようになりました。
6. 終わりに
前回のAWS実習をTerraformで作成してみると、確かにコンソール操作より事前知識が必要だと感じました。
どのような仕組みで動いているのか理解していないと、使いこなすのはなかなか難しいと思いました。
しかし、それ以上に便利さも感じました。
数十台以上のサーバを構築する際、一つずつコンソールで作成するよりも、コードを再利用しながら構築できるため、作業効率は圧倒的に高いと思いました。
まだTerraformは勉強中ですが、今後もコンソールで構築した環境をTerraformで再現しながら、少しずつ理解を深めていきたいと思います。








