TerraformでEC2 + ALB環境を構築してみた

# はじめに

今回は、Terraform を使って AWS 上に Application Load Balancer（ALB）と EC2 インスタンスを組み合わせた Web アプリケーション環境を構築してみます。
これまで Terraform にあまり触れてきませんでしたが、今回はEC2 + ALB環境を構築することを通じて、理解を深めたいと思います。

# Terraform とは

Terraform は、HashiCorp 社によって開発されたオープンソースの Infrastructure as Code (IaC) ツールです。JSON に似た独自言語である HCL（HashiCorp Configuration Language）という言語を用いて、クラウドインフラストラクチャ（サーバー、ストレージ、ネットワーク、データベースなど）の構成をコードとして記述し、プロビジョニングと管理を自動化します。

https://developer.hashicorp.com/terraform

Terraform を利用することで、以下のようなメリットがあります。

- **再現性の向上**: 同じ構成のインフラを何度でも正確に再現できる
- **バージョニング**: インフラ構成を Git などのバージョン管理システムで管理できる
- **自動化**: インフラの構築、変更、削除を自動化し、手作業によるミスを削減できる
- **マルチクラウド対応**: AWS、Azure、Google Cloud Platform など、多くのクラウドプロバイダーに対応できる

Terraform の基本的なワークフローは以下の通りです。

1.  **Write**: HCL を用いてインフラ構成を記述
2.  **Plan**: `terraform plan` コマンドを実行し、現在の構成と記述された構成の差分を確認
3.  **Apply**: `terraform apply` コマンドを実行し、記述された構成を実際のインフラに適用

# ファイル構成

ファイル構成は以下の通りです。

```text
root/
├── provider.tf           # AWSプロバイダー設定
├── variables.tf          # 変数定義
├── vpc.tf                # VPC、サブネット、IGW、ルートテーブル
├── security_groups.tf    # セキュリティグループ
├── ec2.tf                # EC2インスタンス、AMIデータソース
├── alb.tf                # ALB、ターゲットグループ、リスナー
├── locals.tf             # ローカル変数定義
└── outputs.tf            # 出力値定義
```

各`.tf`ファイルに役割を分割することで、コードの見通しを良くし、管理しやすくしています。

# 構築した環境の概要

今回構築したのは、以下のような構成です。

```mermaid
graph TD
    subgraph "AWS Cloud"
        Internet --> ALB[ALB];

        subgraph "ap-northeast-1a"
            EC2_1["EC2 Instance 1 <br> (Apache Web)"];
        end

        subgraph "ap-northeast-1c"
            EC2_2["EC2 Instance 2 <br> (Apache Web)"];
        end

        ALB --> EC2_1;
        ALB --> EC2_2;
    end
```

# 実装手順と設定内容
以下のドキュメントに沿って構築していきます。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

## プロバイダー設定（provider.tf）

まず、Terraform の基本設定からです。

```terraform:provider.tf
provider "aws" {
  region  = "ap-northeast-1"
  profile = "test"
}
```

東京リージョンを選択し、事前に設定済みの AWS プロファイルを指定しました。

## 変数定義（variables.tf）

設定値は変数として管理することで、再利用性を高めています。

```terraform:variables.tf
variable "prefix" {
  description = "Resource name prefix"
  default     = "test-terraform"
}

variable "instance_type" {
  description = "EC2 instance type"
  default     = "t3.micro"
}

variable "availability_zones" {
  description = "Availability zones"
  type        = list(string)
  default     = ["ap-northeast-1a", "ap-northeast-1c"]
}
```

2 つのアベイラビリティゾーンを指定しました。

## ネットワーク構築（vpc.tf）

### VPC の作成

```terraform:vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = merge(local.tags, {
    Name = "${var.prefix}-vpc"
  })
}
```

プライベート IP アドレス範囲（RFC1918 準拠）を使用しています。

### パブリックサブネットの作成

```terraform:vpc.tf
resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  tags = merge(local.tags, {
    Name = "${var.prefix}-public-subnet-${count.index + 1}"
  })
}

# インターネットゲートウェイ
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(local.tags, {
    Name = "${var.prefix}-igw"
  })
}

# ルートテーブル
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 = merge(local.tags, {
    Name = "${var.prefix}-public-rt"
  })
}

# ルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  count = length(aws_subnet.public)

  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}
```

`map_public_ip_on_launch = true`で EC2 インスタンスに自動的にパブリック IP を割り当てるようにしました。

## セキュリティグループ設定（security_groups.tf）

### ALB 用セキュリティグループ

```terraform:security_groups.tf
resource "aws_security_group" "alb" {
  name        = "${var.prefix}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(local.tags, {
    Name = "${var.prefix}-alb-sg"
  })
}
```

:::note warn
今回は検証のため、一時的に全開放（`0.0.0.0/0`）としていますが、実際の運用では環境に合わせて適切な IP アドレス範囲に限定してください。
:::

### EC2 用セキュリティグループ

```terraform:security_groups.tf
resource "aws_security_group" "ec2" {
  name        = "${var.prefix}-ec2-sg"
  description = "Security group for EC2"
  vpc_id      = aws_vpc.main.id
  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = merge(local.tags, {
    Name = "${var.prefix}-ec2-sg"
  })
}

```

EC2 への直接アクセスを防ぎ、ALB を経由したアクセスのみを許可します。

## EC2 インスタンス設定（ec2.tf）

### AMI 選択

```terraform:ec2.tf
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }
}
```

Amazon Linux 2023 x86_64 系のAMIの最新版を利用する設定です。

### EC2 インスタンスの作成

```terraform:ec2.tf
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}
resource "aws_instance" "web" {
  count = length(aws_subnet.public)

  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public[count.index].id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Hello from EC2 Instance ${count.index + 1}</h1>" > /var/www/html/index.html
    TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
    INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id)
    echo "<p>Instance ID: $INSTANCE_ID</p>" >> /var/www/html/index.html
  EOF
  tags = merge(local.tags, {
    Name = "${var.prefix}-web-${count.index + 1}"
  })
}

```

サブネット数分（2 台）の EC2 インスタンスを作成し、user_data スクリプトで起動時に自動的に Apache をインストール・設定します。各インスタンスで異なる内容を表示することで、ロードバランサーが正しく動作することを確認できるようにしています。

## Application Load Balancer 設定（alb.tf）

### ALB 本体の作成

```terraform:alb.tf
resource "aws_lb" "main" {
  name               = "${var.prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = aws_subnet.public[*].id

  enable_deletion_protection = false
  tags = merge(local.tags, {
    Name = "${var.prefix}-alb"
  })
}
```


### ターゲットグループの設定

```terraform:alb.tf
resource "aws_lb_target_group" "web" {
  name     = "${var.prefix}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id

  health_check {
    enabled             = true
    healthy_threshold   = 2
    interval            = 30
    matcher             = "200"
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    unhealthy_threshold = 2
  }
  tags = merge(local.tags, {
    Name = "${var.prefix}-tg"
  })
}

# ターゲットグループアタッチメント
resource "aws_lb_target_group_attachment" "web" {
  count = length(aws_instance.web)

  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

# リスナー設定
resource "aws_lb_listener" "web" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.web.arn
  }
}
```

30 秒間隔でヘルスチェックを実行する設定です。

## ローカル変数定義（locals.tf）

Terraform では `locals` を使って、繰り返し利用する値やタグなどを一元管理できます。

```terraform:locals.tf
locals {
  tags = {
    Project     = "terraform-ec2-alb"
    Environment = "dev"
    Owner       = "test"
  }
}
```

これにより、各リソースの `tags` に `merge(local.tags, {...})` で共通タグを簡単に付与できます。

## 出力設定（outputs.tf）

Terraform 実行後に必要な情報を確認できるよう、出力設定を追加しました。

```terraform:outputs.tf
output "alb_dns_name" {
  description = "DNS name of the load balancer"
  value       = aws_lb.main.dns_name
}

output "alb_zone_id" {
  description = "The canonical hosted zone ID of the load balancer"
  value       = aws_lb.main.zone_id
}

output "ec2_instance_ids" {
  description = "IDs of the EC2 instances"
  value       = aws_instance.web[*].id
}

output "ec2_public_ips" {
  description = "Public IP addresses of the EC2 instances"
  value       = aws_instance.web[*].public_ip
}

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}
```

構築完了後に ALB の DNS 名や EC2 インスタンスの IP アドレスなど、動作確認に必要な各種情報を出力させることができます。

# 構築コマンド

1. 初期化
   ```bash
   terraform init
   ```
2. 確認
   ```bash
   terraform plan
   ```
3. 適用
   ```bash
   terraform apply
   ```

Terraform の実行が完了すると、ALB の DNS などが出力されます。

# 動作確認

構築完了後、出力された ALB DNS 名（例：`test-terraform-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com`）にブラウザでアクセスすると、以下のように表示されます。

```html
Hello from EC2 Instance 1 Instance ID: i-xxxxxxxxxxxxxxxxx
```

ページをリロードするたびに、「Instance 1」と「Instance 2」が交互に表示され、正しく動作していることがわかります。

# リソースの削除

テスト完了後は、以下のコマンドでリソースを削除できます。

```bash
terraform destroy
```

# まとめ

今回、Terraform を使って EC2 + ALB 構成の Web アプリケーション環境を構築してみました。
インフラをコードで管理することで、構成の再現性や変更履歴の追跡が簡単になると感じました。
また、手作業による設定ミスも防げるので、運用の効率化や品質向上にも大きく貢献できると感じました。
次回は、Auto Scaling や RDS を追加して、より本格的な Web アプリケーション環境を構築してみたいと思います。

# 参考資料

https://note.com/fujimohige/n/n4d1b7ecea485

https://tech-blog.cloud-config.jp/2023-10-30-first-terraform

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

https://docs.aws.amazon.com/elasticloadbalancing/latest/application/