はじめに
マルチクラウド対応のTerraformを生成AIを活用して学習してみました。
今回は以下の手順で学習をしました。
①学習のゴールを自身で設定
②ゴールの達成において必要な準備と手順を生成AIに質問
③準備や手順ごとに、最新の公式ドキュメントやチュートリアルを確認
ゴール
- シンプルなNext.jsアプリをAWS ECS Fargateにデプロイ(terraformの学習なのでアプリの内容は問わない)
- 特定IPのみアクセス可能なセキュリティ設定とアクセス確認(業務に生かしたいので社内のみの制限などを見越して)
- リソースの削除(節約)
システム構成図
今回のゴールを満たすシステム構成を生成AIに整理してもらいました
ローカルで構築する際も比較のため整理してもらいました
サイトにアクセスして画面が表示されるまでの流れ(ローカル環境)
①ブラウザにlocalhost:3000(127.0.0.1:3000)にアクセス
↓
②ブラウザがPCのOSにアクセスをリクエスト
↓
③OSがDockerへリクエストを渡す
↓
④Dockerが3000ポートでリクエストを受け取り、コンテナ内の3000ポートにリクエストを転送
↓
⑤コンテナ内のNext.jsサーバーがリクエストを受け取り、画面の情報をレスポンス
↓
⑥レスポンスが逆の経路で戻り、ブラウザに画面の情報が表示される:Next.js → Docker → OS → ブラウザ
システム構成を考える上での以下の5つのポイント
ポイント1: アクセス方法の違い
| 項目 | ローカル環境 | AWS環境 |
|---|---|---|
| アドレス |
localhost または 127.0.0.1
|
パブリックIPまたはドメイン |
| 意味 | 自分自身のPCを指す特別なアドレス | インターネット上の一意のアドレス |
| アクセス範囲 | PC内のみ(外部から完全に遮断) | 世界中からアクセス可能 |
| 必要なリソース | なし | インターネットゲートウェイ(IGW)、パブリックIPアドレス |
ポイント2: ネットワーク空間の違い
| 項目 | ローカル環境 | AWS環境 |
|---|---|---|
| ネットワーク | OSのネットワークスタック | VPC(Virtual Private Cloud) |
| IP範囲 |
127.0.0.1(固定) |
自分で設計(例: 10.0.0.0/16) |
| 分離 | PC内で閉じている | 他のAWSユーザーと物理的に完全分離 |
| 必要なリソース | なし | VPC、サブネット(複数) |
ポイント3: 通信経路制御の違い
| 項目 | ローカル環境 | AWS環境 |
|---|---|---|
| 経路制御 | OSが自動的に処理 | ルートテーブルで明示的に設定 |
| デフォルト動作 | ループバック(自分自身に返す) | 設定しないと通信不可 |
| 設定 | 不要 | 必須(どこに転送するか指定) |
| 必要なリソース | なし | ルートテーブル、IGWへのルート設定、サブネットとの関連付け |
ポイント4: セキュリティ制御の違い
| 項目 | ローカル環境 | AWS環境 |
|---|---|---|
| 保護 | OSレベルで自動的に外部から遮断 | セキュリティグループで明示的に制御 |
| デフォルト | 127.0.0.1は外部からアクセス不可 | すべて拒否(ホワイトリスト方式) |
| アクセス制御 | 不要 | 必須(IPアドレス、ポート単位で設定) |
| 必要なリソース | なし | セキュリティグループ(インバウンド/アウトバウンドルール) |
ポイント5: コンテナ実行環境の違い
| 項目 | ローカル環境 | AWS環境 |
|---|---|---|
| 実行基盤 | Docker Desktop | ECS(Elastic Container Service)+ Fargate |
| イメージ保管 | ローカルストレージ | ECR(Elastic Container Registry) |
| ネットワーク設定 | docker run -p 3000:3000 |
タスク定義で設定 |
| 管理 | 手動で起動・停止 | 自動管理(常時起動、障害時再起動) |
| 必要なリソース | なし | ECRリポジトリ、ECSクラスター、ECSタスク定義、ECSサービス |
サイトにアクセスして画面が表示されるまでの流れ(クラウド環境)
最終的に、以下の流れを実現するような構成をterraformで構築しました。
①ブラウザからパブリックIP:3000にアクセス
↓
②ブラウザがインターネット経由でAWSにリクエストを送信
↓
③インターネットゲートウェイ(IGW)がリクエストを受け取り、VPC内へ転送
↓
④ルートテーブルがリクエストの宛先を確認し、適切なサブネットへルーティング
↓
⑤セキュリティグループがリクエスト元のIPアドレスとポート番号をチェックし、許可されていれば通過させる
↓
⑥サブネット内のECSタスク(コンテナ)の3000ポートにリクエストが到達
↓
⑦ECSタスク内のNext.jsサーバーがリクエストを受け取り、画面の情報をレスポンス
↓
⑧レスポンスが逆の経路で戻り、ブラウザに画面の情報が表示される:
Next.js → ECSタスク → セキュリティグループ → サブネット → ルートテーブル → IGW → インターネット → ブラウザ
使用したAWSサービス
- ECS Fargate
- ECR
- VPC/Subnet/IGW
- IAM
- CloudWatch Logs
開発環境
- macOS(M2、ARM64)
- Docker Desktop
- Terraform v1.14.3
- AWS CLI
- Node.js 20
アプリの準備
mkdir app
npx create-next-app@latest .
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD HOSTNAME=0.0.0.0 node server.js
docker build -t terraform-study-app .
docker run -p 3000:3000 terraform-study-app
3000にアクセスして、「My First AWS App with Terraform」と表示されていればOK
terraformでawsリソースを構築
初期設定
インストール含め、まずは公式チュートリアルを行いました。
appディレクトリと同じ階層でterraformディレクトリを作成し、プロバイダー設定とterraformの初期化を行います
書き方は公式ドキュメントに記載されています
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
terraform init
ネットワーク基盤の構築
システム構成図におけるネットワーク基盤(VPC/Subnet/IGW)を構築します。
-
aws_vpc
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc -
aws_subnet
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet -
aws_internet_gateway
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway
main.tfに追記します:
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
# パブリックサブネット1(東京AZ-a)
resource "aws_subnet" "public_1" {
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 = "public-subnet-1"
}
}
# パブリックサブネット2(東京AZ-c)
resource "aws_subnet" "public_2" {
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 = "public-subnet-2"
}
}
# インターネットゲートウェイ
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
以下を実行し、AWSコンソールでVPC/Subnet/IGWが作成されていることを確認できればOK
terraform plan
terraform apply
通信経路の設定
システム構成図における通信経路(ルートテーブル、Subnetとの関連付け)を構築します。
-
aws_route_table
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table -
aws_route_table_association
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association
main.tfに追記します:
# ルートテーブル
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 # IGWへ送る
}
tags = {
Name = "public-route-table"
}
}
# サブネット1にルートテーブルを関連付け
resource "aws_route_table_association" "public_1" {
subnet_id = aws_subnet.public_1.id
route_table_id = aws_route_table.public.id
}
# サブネット2にルートテーブルを関連付け
resource "aws_route_table_association" "public_2" {
subnet_id = aws_subnet.public_2.id
route_table_id = aws_route_table.public.id
}
terraform apply
VPCのリソースマップからも関連付けを確認できます
セキュリティの設定
IP制限を行います。
以下のコマンドで表示されたIPアドレスに/32をつけてセキュリティグループに設定します。
curl checkip.amazonaws.com
- aws_security_group
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group
main.tfに追記します:
resource "aws_security_group" "ecs_tasks" {
name = "ecs-tasks-sg"
description = "Security group for ECS tasks"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
cidr_blocks = ["123.456.789.012/32"] # ← curl checkip.amazonaws.comの結果
description = "Allow access from my IP"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ecs-tasks-sg"
}
}
terraform apply
EC2のセキュリティグループにリソースが作成されていることを確認できればOK。
インバウンドルールに自身のIP/32:3000が設定されていて、アウトバウンドルールに0.0.0.0/0が設定されているはずです。
コンテナ基盤の構築
アプリケーションの実行基盤を構築します。
ECRリポジトリの作成
まずはDockerイメージのプッシュ先となるECRリポジトリを作成します。
main.tfに追記します:
# ECRリポジトリ
resource "aws_ecr_repository" "app" {
name = "nextjs-app"
image_tag_mutability = "MUTABLE"
force_delete = true
image_scanning_configuration {
scan_on_push = false
}
tags = {
Name = "nextjs-app-repo"
}
}
output "ecr_repository_url" {
description = "ECR repository URL"
value = aws_ecr_repository.app.repository_url
}
terraform apply
ECS/IAM/CloudWatch Logsの構築
続けてmain.tfに追記します:
# CloudWatch Logsグループ
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/nextjs-app"
retention_in_days = 7
tags = {
Name = "ecs-nextjs-logs"
}
}
# IAMロール(ECSタスク実行用)
resource "aws_iam_role" "ecs_task_execution_role" {
name = "ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = {
Name = "ecs-task-execution-role"
}
}
# AWS管理ポリシーをアタッチ(ECRからイメージ取得など)
resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# CloudWatch Logs用の権限
resource "aws_iam_role_policy" "ecs_logs" {
name = "ecs-logs-policy"
role = aws_iam_role.ecs_task_execution_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:ap-northeast-1:*:log-group:/ecs/nextjs-app:*"
}]
})
}
# ECSクラスター
resource "aws_ecs_cluster" "main" {
name = "nextjs-cluster"
tags = {
Name = "nextjs-cluster"
}
}
# ECSタスク定義
resource "aws_ecs_task_definition" "app" {
family = "nextjs-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256" # 0.25 vCPU
memory = "512" # 512 MB
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
container_definitions = jsonencode([{
name = "nextjs"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true
portMappings = [{
containerPort = 3000
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/nextjs-app"
"awslogs-region" = "ap-northeast-1"
"awslogs-stream-prefix" = "ecs"
}
}
}])
tags = {
Name = "nextjs-task"
}
}
# ECSサービス(タスクを常時起動)
resource "aws_ecs_service" "app" {
name = "nextjs-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id]
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = true
}
tags = {
Name = "nextjs-service"
}
}
output "ecs_task_public_ip" {
description = "ECS task public IP (check after task is running)"
value = "Check ECS console for the task's public IP"
}
この状態でapplyしてもECRにアプリケーションのイメージが存在しないため、ECSがタスクの起動に失敗します。
次のセクションでDockerイメージをプッシュします。
Dockerイメージのプッシュ
apply成功時に表示されたecr_repository_urlをコピーします。
以下のコマンドでECRにログインします:
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin あなたのECR_URL_のドメイン部分
Dockerfileが存在するディレクトリでビルドします。
注意:
AWS FargateはデフォルトでAMD64アーキテクチャを使用するため、M1/M2 Macの場合は--platform linux/amd64 を指定する必要があります。
docker build --platform linux/amd64 -t nextjs-app .
イメージのタグ付けし、ECRにpushします:
docker tag nextjs-app:latest あなたのECR_URL:latest
docker push あなたのECR_URL:latest
デプロイとアクセス確認
ECRにイメージがプッシュされると、ECSのタスクが起動し始めます。
タスクのステータスが実行中になったことを確認できたら、ネットワーキングタブからパブリックIPをコピーし、パブリックIP:3000でブラウザからアクセスしてみましょう!
画面が表示されるはずです!
同じアドレスでスマホからアクセスすると画面が表示されないことまで確認できればOK
(PCと同じWiFi使っている場合は接続を切りましょう)
お片付け
terraform destroy
terraform show
The state file is empty. No resources are represented.
おわりに
CloudFormationも少し触っていたことがありますが、terraformの方が書きやすい、見やすい印象でした。
今後はGitHub ActionsがAWSにアクセスするための設定や一連のCI/CDパイプラインの自動化にも活用していきたいです。
余談ですが生成AIは「どう書けばいいか」は教えてくれますが、「なぜそうなるのか」「正しいのか」の確認や理解は自分で行う必要があるので、これまで以上に頭を使う場面が増えたなと感じています。



