はじめに
本記事は、AWS上に「Web・AP・DB」の3層レイヤーインフラストラクチャをTerraform(IaC)で自動構築し、セキュリティに配慮した状態でGitHubへ公開するまでの手順をまとめた備忘録です。
後から同じ構成を完全に再現できるように、ソースコード、検証時のエラー対策、Gitのトラブルシューティングを網羅しています。
1. 成果物のインフラ構成
個人開発環境における検証コスト(無料枠の適用)を最小限に抑えるため、あえてシングルAZ構成を採用しています。
- Web層: インターネットからのHTTP(80)アクセスを受付(Ubuntu / Apache)
- AP層: Web層からのみ通信を許可する隔離された領域(EC2 Ubuntu)
- DB層: AP層からのみ接続を許可するデータベース領域(RDS MySQL)
- 運用・保守: SSH秘密鍵の管理を不要にするため、AWS Systems Manager (Session Manager) を導入しブラウザから安全にリモートログイン可能。
2. ソースコードの準備
プロジェクトのルートディレクトリに、以下の3つのファイルを作成します。
① main.tf
データベースのパスワードを直書きせず、variable を用いて外部から安全に注入する設計にしています。
# 1. プロバイダーの設定
provider "aws" {
region = "ap-northeast-1"
}
# 2. VPCの定義
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "3tier-vpc" }
}
# 3. インターネットゲートウェイの定義
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = { Name = "3tier-igw" }
}
# 4. Web層用サブネット(パブリック)
resource "aws_subnet" "web_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 = "3tier-web-subnet-1a" }
}
# 5. AP層用サブネット(プライベート)
resource "aws_subnet" "app_1a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1a"
tags = { Name = "3tier-app-subnet-1a" }
}
# 6. DB層用サブネット(プライベート)
resource "aws_subnet" "db_1a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
availability_zone = "ap-northeast-1a"
tags = { Name = "3tier-db-subnet-1a" }
}
# 7. ルートテーブルの定義(パブリック用)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = { Name = "3tier-public-rt" }
}
# 8. ルートテーブルとWeb層サブネットの紐付け
resource "aws_route_table_association" "web_1a" {
subnet_id = aws_subnet.web_1a.id
route_table_id = aws_route_table.public.id
}
# 9. Webサーバー用セキュリティグループ
resource "aws_security_group" "web_sg" {
name = "3tier-web-sg"
description = "Allow HTTP traffic from internet"
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 = { Name = "3tier-web-sg" }
}
# 10. APサーバー用セキュリティグループ
resource "aws_security_group" "app_sg" {
name = "3tier-app-sg"
description = "Allow traffic from Web SG"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.web_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "3tier-app-sg" }
}
# 11. DB(RDS)用セキュリティグループ
resource "aws_security_group" "db_sg" {
name = "3tier-db-sg"
description = "Allow traffic from App SG"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "3tier-db-sg" }
}
# 12. Webサーバー(EC2インスタンス)
resource "aws_instance" "web" {
ami = "ami-0d52744d6551d851e"
instance_type = "t3.micro"
subnet_id = aws_subnet.web_1a.id
vpc_security_group_ids = [aws_security_group.web_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
user_data = <<-EOF
#!/bin/bash
apt update -y
apt install -y apache2
systemctl start apache2
systemctl enable apache2
echo "<h1>Hello from AWS 3-Tier Architecture (Web Layer)</h1>" > /var/www/html/index.html
EOF
tags = { Name = "3tier-web-server" }
}
# 13. APサーバー(EC2インスタンス)
resource "aws_instance" "app" {
ami = "ami-0d52744d6551d851e"
instance_type = "t3.micro"
subnet_id = aws_subnet.app_1a.id
vpc_security_group_ids = [aws_security_group.app_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
tags = { Name = "3tier-app-server" }
}
# 14. SSM用のIAMロールとプロファイル
resource "aws_iam_role" "ec2_ssm" {
name = "3tier-ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
}
]
})
}
resource "aws_iam_role_policy_attachment" "ssm_attach" {
role = aws_iam_role.ec2_ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_profile" {
name = "3tier-ec2-ssm-profile"
role = aws_iam_role.ec2_ssm.name
}
# 15. RDS配置用の追加サブネット(マルチAZ用要件を満たすため定義)
resource "aws_subnet" "db_1c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.4.0/24"
availability_zone = "ap-northeast-1c"
tags = { Name = "3tier-db-1c" }
}
# 16. DBサブネットグループ
resource "aws_db_subnet_group" "db_subnet_group" {
name = "dbsg"
subnet_ids = [aws_subnet.db_1a.id, aws_subnet.db_1c.id]
tags = { Name = "3tier-db-subnet-group" }
}
# 17. データベース(RDS MySQL)インスタンス
resource "aws_db_instance" "db" {
allocated_storage = 20
max_allocated_storage = 20
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
db_name = "mydb"
username = "admin"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.db_subnet_group.name
vpc_security_group_ids = [aws_security_group.db_sg.id]
skip_final_snapshot = true # 検証環境のためtrue
tags = { Name = "3tier-db-instance" }
}
# 18. 変数定義(パスワードマスク用)
variable "db_password" {
description = "RDS root password"
type = string
sensitive = true
}
② .gitignore
パスワードを含むメタデータ(tfstate)や、巨大なプロバイダバイナリ(.terraform/)をGit管理から確実に除外するためのファイル。ファイル名の先頭にドットが必要。
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
③ README.md
ポートフォリオとしての見栄え、設計思想を言語化したドキュメント。
# AWS 3-Tier Architecture with Terraform
Terraformを使用してAWS上に3層インフラストラクチャをコード化したリポジトリです。本環境は検証コストの最適化を優先して構成しています。
## 可用性とコスト最適化のトレードオフ
本インフラは個人検証用のため、あえて「シングルAZ」の最小構成を採用しており、単一障害点(SPOF)が存在します。
実務における本番環境への移行時は、以下の高可用性(HA)拡張を行えるよう設計しています。
1. **Web/AP層の冗長化**: マルチAZ配置(1a/1c)への拡張、および前段へのALB(Application Load Balancer)の配置。
2. **DB層の高可用性化**: 定義済みのサブネットグループ(1a/1c)を活かし、パラメータ `multi_az = true` を有効化することで即座にマルチAZ同期レプリケーション構成へ拡張可能です。
3. デプロイとエラーへの対処法
初期化と適用
terraform init
terraform apply
実行時に var.db_password を求められるので入力する。
🚨 遭遇したエラーと対応策
現象:RDSインスタンス作成時に以下のエラーが発生
api error InvalidParameterValue: The parameter MasterUserPassword is not a valid password because it is shorter than 8 characters.
- 原因: AWS側のセキュリティルールにより、MySQLのマスターパスワードは「8文字以上」である必要がある。プロンプトへの入力が不足していた。
-
対策:
terraform applyを再実行し、英数字を含む8文字以上の強力なパスワード(例:SuperSecure123)を入力することで解決。
4. GitHubへの安全な公開手順(トラブル事例含む)
Git管理を開始してGitHubへアップロードする。
git init
git add .
🚨 遭遇したエラーと対応策(大容量ファイルの拒否)
現象:git push 時にGitHub側から100MB制限で拒否される
remote: error: File .terraform/providers/.../terraform-provider-aws... is 857.66 MB; this exceeds GitHub's file size limit of 100.00 MB
-
原因:
.gitignoreファイルの作成漏れ(またはファイル名不備)により、Terraformが自動ダウンロードした800MB超のAWSプロバイダ本体ファイルまでGitが追跡してしまい、コミット履歴に刻まれてしまった。 -
対策: Gitの歴史を一度リセットし、
.gitignoreを正しく配置してやり直した。
# 1. 汚染されたGit履歴を一度完全に削除
rm -rf .git
# 2. .gitignore が正しく存在することを確認(なければ上記の内容で新規作成)
ls -a
# 3. 再度Gitを初期化して安全なファイルだけを登録
git init
git add .
# 4. 軽いテキストファイル(main.tf, README, .gitignore)だけが対象であることを確認
git status
# 5. コミットとGitHubへの送信
git commit -m "feat: AWS 3-tier architecture with Terraform"
git branch -M main
git remote add origin https://github.com/あなたのユーザー名/リポジトリ名.git
git push -u origin main
5. リソースのクリーンアップ(掃除)
検証が終了したら、AWSの不要な課金を防ぐためにリソースを即座に削除します。
terraform destroy
確認プロンプトが出たら、yes と入力して実行します。約5〜10分でAWS上のすべてのインフラ(VPC、EC2、RDSなど)が自動解体されます。
まとめ
Terraformを利用することで、複雑な3層インフラをコード1本で安全に管理・デプロイ・破壊できるサイクルを構築できました。
GitHubへの公開時は、インフラの実行キャッシュや機密情報を巻き込まないよう、最初に適切な .gitignore を配置することが実務上極めて重要です。
