1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWSで構築したWeb環境をTerraformで再現してみた

1
Last updated at Posted at 2026-05-29

1. 初めに

今回は、前回の投稿で作成したAWS構成を、Terraformを利用してコード化してみました。
Terraformの文法はまだ勉強中のため、今回はChatGPTも活用しながら構築を進めました。

ただコードをコピーするだけではなく、実際に手で入力しながら、ChatGPTが生成したコードの内容を分析し、TerraformやAWS構成への理解を深めることを意識しました。

2. 全体設計

全体構成図は下記の記事をご参照ください。

3. Terraform

Terraformでは、役割やサービスごとに設定ファイルを分割して管理することが一般的です。

実際にファイルを分けてみると、以下のようなメリットがあると感じました。

  1. コードの役割が分かりやすい
  2. どのAWSサービスを利用しているのか把握しやすい
  3. 一部のコードを再利用しやすい

今回は、下記の8つのファイルに分けて構築しました。

  1. provider.tf
  2. network.tf
  3. security.tf
  4. iam.tf
  5. ec2.tf
  6. alb.tf
  7. route53.tf
  8. 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値と同じですが、学習のため明示的に記載しました。

image.png

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 でどの設定に作成されるのか確認してみました。
image.png

ここで 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 が両方存在していたため、違いを確認してみました。
image.png
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で実行しました。
image.png

作成したファイルを保存したフォルダーを VSCode で開き、
上部メニューの Terminal → New Terminal をクリックします。

VSCode下部にターミナルが表示されたら、terraform init を実行し、初期設定を行います。

terraform init 完了後、terraform plan を実行して、Terraformがどのような構成を作成する予定か確認します。

問題なく実行できたら、terraform apply を実行して構築します。

image.png
image.png

作成が完了しました。

正常に作られたのか確認します。

  1. 作られたVPCのリソースマップを確認します。
    image.png

  2. SSM接続ができるか確認します。
    image.png

  3. ブラウザより接続します。
    image.png

image.png

image.png

正常に動作することが確認できました。
検証完了後は、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で再現しながら、少しずつ理解を深めていきたいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?