はじめに
Terraformを使ってAWS上に3層構成の環境を作成しました。
これまでに、
- TerraformでAWSリソースを構築
- GitHub ActionsでCI/CD
- SSM Run Command経由でAnsibleを実行
- Spring BootアプリをEC2へ自動デプロイ
といった構成を作ってきました。
その中で、最初はなんとなく理解していたものの、実際に構築してみると以下のような疑問が出てきました。
- Public SubnetとPrivate Subnetは何が違うのか
- ALBはなぜPublic Subnetに置くのか
- EC2はPublic SubnetとPrivate Subnetのどちらに置くべきなのか
- RDSはなぜPrivate Subnetに置くのか
- サブネットはリソースごとに分ける必要があるのか
- CIDRはどう設計すればよいのか
この記事では、TerraformでAWSの3層構成を作りながら理解した、VPC・Subnet・ALB・EC2・RDSの関係を整理します。
初心者目線でつまずいたポイントも含めてまとめます。
作成した構成
今回作成した構成は、ざっくり以下のような形です。
Internet
↓
Internet Gateway
↓
Public Subnet
↓
ALB
↓
Private Subnet
↓
EC2
↓
Private Subnet
↓
RDS
Terraformでは、主に以下のリソースを作成しました。
- VPC
- Public Subnet
- Private Subnet
- Internet Gateway
- Route Table
- NAT Gateway
- Security Group
- ALB
- EC2
- RDS
- IAM Role
- CloudWatch Alarm
最初はEC2をPublic Subnetに置いていましたが、最終的にPrivate Subnetに配置して、ALB経由でのみアクセスするようにしました。
3層構成とは
3層構成とは、アプリケーションを役割ごとに分ける構成です。
代表的には以下の3つに分けます。
プレゼンテーション層
アプリケーション層
データベース層
今回のAWS構成に当てはめると、以下のようになります。
プレゼンテーション層:ALB
アプリケーション層:EC2
データベース層:RDS
ALBが外部からのHTTPリクエストを受け取り、EC2上のアプリケーションに転送します。
EC2上のアプリケーションは、必要に応じてRDSへ接続します。
ユーザー
↓
ALB
↓
EC2
↓
RDS
このように役割を分けることで、セキュリティや拡張性を考えやすくなります。
VPCとは
VPCは、AWS上に作成する自分専用のネットワーク空間です。
今回の構成では、以下のようなCIDRを使いました。
10.10.0.0/16
これは、10.10.0.0 から始まるプライベートIPアドレスの範囲を、AWS上のネットワークとして使うという意味です。
VPCの中に、Public SubnetやPrivate Subnetを作成します。
VPC 10.10.0.0/16
├── Public Subnet
├── Public Subnet
├── Private Subnet
└── Private Subnet
VPCはネットワーク全体の箱で、Subnetはその中を用途ごとに分けた小さなネットワーク、というイメージです。
Public SubnetとPrivate Subnetの違い
Public SubnetとPrivate Subnetの大きな違いは、インターネットから直接アクセスできる経路があるかどうかです。
Public Subnet
Public Subnetは、Internet Gatewayへのルートを持つサブネットです。
Public Subnet
↓
Route Table
↓
0.0.0.0/0 → Internet Gateway
0.0.0.0/0 は、すべての宛先を意味します。
つまり、Public Subnetのルートテーブルに以下のような設定があると、インターネットへ出ることができます。
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "AwsStudy-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
tags = {
Name = "AwsStudy-public-rt"
}
}
resource "aws_route" "public_default" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id
route_table_id = aws_route_table.public.id
}
Public Subnet用のRoute Tableでは、0.0.0.0/0 をInternet Gatewayへ向けています。
このルートがあることで、Public Subnetに配置したALBがインターネットと通信できるようになります。
また、Route Tableは作成しただけではSubnetに適用されないため、aws_route_table_association で対象のSubnetに関連付けする必要があります。
Private Subnet
Private Subnetは、Internet Gatewayへの直接ルートを持たないサブネットです。
そのため、インターネットから直接アクセスされることはありません。
RDSのように外部公開したくないリソースは、Private Subnetに配置します。
また、EC2をPrivate Subnetに置くこともできます。
Private Subnet
↓
EC2
↓
RDS
ただし、Private Subnet上のEC2がインターネットへ出る必要がある場合は、NAT GatewayやVPC Endpointを使う必要があります。
ALBをPublic Subnetに置く理由
ALBは、ユーザーからのHTTPリクエストを受け取る入口になります。
そのため、インターネットからアクセスできる場所に置く必要があります。
Internet
↓
ALB
↓
EC2
ALBをPublic Subnetに配置する理由は、外部からALBにアクセスしてもらうためです。
ただし、ALBをPublic Subnetに置くからといって、EC2やRDSまでPublic Subnetに置く必要はありません。
むしろ、よくある構成では以下のようにします。
Public Subnet
└── ALB
Private Subnet
└── EC2
Private Subnet
└── RDS
外部から直接アクセスできるのはALBだけにして、EC2やRDSはPrivate Subnetに置くことで、より安全な構成になります。
EC2はPublic SubnetとPrivate Subnetのどちらに置くべきか
最初は、EC2をPublic Subnetに置いていました。
理由は、SSH接続や動作確認がしやすかったからです。
Internet
↓
EC2
しかし、この構成だとEC2がインターネットから直接到達可能な場所にあるため、セキュリティ面では注意が必要です。
現在は、Session Managerを使えばSSHを開けずにEC2へ接続できます。
そのため、より実務に近い構成を考えるなら、EC2はPrivate Subnetに置く方が自然です。
Internet
↓
ALB
↓
Private SubnetのEC2
この場合、外部からEC2へ直接アクセスするのではなく、ALB経由でアプリケーションにアクセスします。
RDSをPrivate Subnetに置く理由
RDSは、基本的にインターネットへ公開する必要がありません。
アプリケーションであるEC2から接続できればよいからです。
そのため、RDSはPrivate Subnetに配置します。
EC2
↓
RDS
Security Groupも、EC2からのみ接続できるようにします。
例えば、RDSのSecurity Groupでは、EC2のSecurity Groupからの3306番ポートだけを許可します。
RDS Security Group
Inbound:
3306 from EC2 Security Group
このようにすることで、インターネットからRDSへ直接接続できない構成にできます。
Security Groupの考え方
今回の構成では、Security Groupを以下のように分けました。
ALB用 Security Group
EC2用 Security Group
RDS用 Security Group
通信の流れは以下のようになります。
Internet
↓ 80番
ALB
↓ 8080番
EC2
↓ 3306番
RDS
それぞれのSecurity Groupの役割は以下です。
| Security Group | 許可する通信 |
|---|---|
| ALB用 | インターネットから80番を許可 |
| EC2用 | ALBから8080番を許可 |
| RDS用 | EC2から3306番を許可 |
ポイントは、EC2やRDSを直接インターネットに公開しないことです。
特にRDSは、ALBやインターネットから直接アクセスされる必要はありません。
Security GroupのTerraform例
ALBはインターネットからHTTPアクセスを受け付けます。
resource "aws_security_group" "alb" {
name = "AwsStudy-alb-sg"
vpc_id = aws_vpc.this.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"]
}
}
EC2は、ALBからの8080番ポートのみ許可します。
resource "aws_security_group" "ec2" {
name = "AwsStudy-ec2-sg"
vpc_id = aws_vpc.this.id
ingress {
from_port = 8080
to_port = 8080
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"]
}
}
RDSは、EC2からの3306番ポートのみ許可します。
resource "aws_security_group" "rds" {
name = "AwsStudy-rds-sg"
vpc_id = aws_vpc.this.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.ec2.id]
}
}
このようにSecurity Group同士を参照することで、IPアドレスではなく「ALBからだけ」「EC2からだけ」という形で通信を制御できます。
サブネットはリソースごとに分ける必要があるのか
最初に勘違いしていたのが、サブネットとリソースの関係です。
私は最初、
EC2用のサブネット
RDS用のサブネット
冗長化用のEC2サブネット
冗長化用のRDSサブネット
のように、リソースごとにサブネットを分ける必要があるのではないかと思っていました。
しかし、サブネットは「リソース1台につき1つ必要」というものではありません。
1つのサブネットには、複数のリソースを配置できます。
Private Subnet
├── EC2
├── EC2
└── RDS
ただし、設計上の理由でサブネットを分けることはあります。
例えば、
- App用Private Subnet
- DB用Private Subnet
を分ける設計です。
VPC
├── Public Subnet
│ └── ALB
├── App Private Subnet
│ └── EC2
└── DB Private Subnet
└── RDS
このように分けると、役割ごとにルートテーブルやNetwork ACLを分けやすくなります。
ただし、学習用や小規模構成であれば、最初から細かく分けすぎる必要はないと感じました。
冗長化する場合のサブネット構成
AWSでは、可用性を高めるために複数のAZを使うことが多いです。
例えば、東京リージョンで以下のように分けます。
ap-northeast-1a
ap-northeast-1c
今回のような構成では、以下のようにPublic SubnetとPrivate Subnetを2つずつ作ることが多いです。
VPC
├── Public Subnet 1a
├── Public Subnet 1c
├── Private Subnet 1a
└── Private Subnet 1c
ALBは複数AZのPublic Subnetに配置します。
Public Subnet 1a ─┐
├── ALB
Public Subnet 1c ─┘
EC2も冗長化する場合は、複数AZのPrivate Subnetに配置します。
Private Subnet 1a
└── EC2
Private Subnet 1c
└── EC2
RDSもMulti-AZ構成にすると、複数AZを使って冗長化できます。
ここで重要なのは、RDS用に必ずEC2とは別のサブネットを作らなければいけない、というわけではないことです。
同じPrivate Subnetを使うこともできます。
ただし、よりきれいに分けるなら、以下のようにApp用とDB用で分ける構成もあります。
VPC
├── Public Subnet 1a
├── Public Subnet 1c
├── App Private Subnet 1a
├── App Private Subnet 1c
├── DB Private Subnet 1a
└── DB Private Subnet 1c
この場合、合計6つのサブネットになります。
一方で、シンプルにするなら以下でも構成できます。
VPC
├── Public Subnet 1a
├── Public Subnet 1c
├── Private Subnet 1a
└── Private Subnet 1c
学習用や小規模なポートフォリオでは、まずはこの形でも十分だと思いました。
CIDR設計で考えたこと
今回のVPCでは、以下のようなCIDRを使いました。
VPC: 10.10.0.0/16
サブネットは、例えば以下のように分けられます。
Public Subnet 1a : 10.10.1.0/24
Public Subnet 1c : 10.10.2.0/24
Private Subnet 1a: 10.10.10.0/24
Private Subnet 1c: 10.10.20.0/24
/24 のサブネットでは、約250個ほどのIPアドレスを使えます。
学習用としてはかなり余裕があります。
また、番号に少し余白を持たせることで、後からサブネットを追加しやすくなります。
例えば、将来的にDB用Private Subnetを分けるなら、以下のように追加できます。
Public Subnet 1a : 10.10.1.0/24
Public Subnet 1c : 10.10.2.0/24
App Private Subnet 1a: 10.10.10.0/24
App Private Subnet 1c: 10.10.20.0/24
DB Private Subnet 1a : 10.10.30.0/24
DB Private Subnet 1c : 10.10.40.0/24
最初から完璧なCIDR設計をするのは難しいですが、後から追加しやすいように、少し余裕を持たせておくとよいと感じました。
VPC Endpointを作ったのに、なぜNAT Gatewayも必要なのか?
EC2をPrivate Subnetに移動したあと、SSM Run CommandやSession Managerを使えるようにするために、VPC Endpointを作成しました。
最初は、VPC Endpointを作ればPrivate SubnetのEC2から必要な通信はすべてできると思っていました。
しかし、実際にAnsibleを実行する構成では、VPC Endpointだけでは足りませんでした。
理由は、VPC Endpointで通信できる先と、NAT Gatewayで通信できる先が違うからです。
VPC Endpointの役割
VPC Endpointは、VPC内のリソースからAWSサービスへプライベートに接続するための仕組みです。
今回の構成では、Private SubnetのEC2から以下のようなAWSサービスへ通信する必要がありました。
EC2 → SSM
EC2 → SSM Messages
EC2 → EC2 Messages
EC2 → S3
EC2 → CloudWatch Logs
例えば、Session ManagerやSSM Run Commandを使うには、EC2上のSSM AgentがSystems Managerと通信できる必要があります。
また、SSM Run Commandの実行ログをCloudWatch Logsへ出力する場合は、CloudWatch Logs用のVPC Endpointも必要になります。
そのため、SSM関連のVPC Endpointを作成しました。
com.amazonaws.ap-northeast-1.ssm
com.amazonaws.ap-northeast-1.ssmmessages
com.amazonaws.ap-northeast-1.ec2messages
また、AnsibleコードをS3に配置している場合、S3用のVPC Endpointを作成すれば、Private SubnetのEC2からS3へアクセスできます。
aws s3 cp s3://<bucket-name>/ansible/ansible.zip /tmp/ansible.zip
つまり、S3からAnsibleのzipファイルをダウンロードするところまでは、S3用のVPC Endpointで対応できます。
それでもVPC Endpointだけでは足りなかった理由
今回のデプロイ処理では、S3からAnsibleコードを取得するだけではありません。
EC2上で、Ansibleの実行に必要なパッケージもインストールしています。
例えば、SSM Run Commandでは以下のような処理を実行しています。
sudo dnf install -y unzip ansible-core
ansible-galaxy collection install community.mysql
ここが重要なポイントです。
S3からzipファイルを取得する通信は、AWSサービスであるS3への通信です。
そのため、S3用のVPC Endpointで対応できます。
一方で、dnf install や ansible-galaxy collection install は、インターネット上のパッケージリポジトリへアクセスします。
dnf install
→ OSのパッケージリポジトリへアクセス
ansible-galaxy collection install
→ Ansible Galaxyへアクセス
これらはS3やSSMのようなAWSサービスではありません。
そのため、VPC Endpointでは通信できません。
Private SubnetのEC2から外部のインターネットへ出る経路が必要になります。
NAT Gatewayの役割
NAT Gatewayは、Private Subnetのリソースからインターネットへ出るために使います。
今回の構成では、Private SubnetのEC2から外部のパッケージリポジトリへアクセスするためにNAT Gatewayを使いました。
通信の流れは以下のようになります。
Private SubnetのEC2
↓
NAT Gateway
↓
Internet Gateway
↓
Internet
NAT Gatewayを使うことで、EC2からインターネットへのアウトバウンド通信ができます。
一方で、インターネット側からPrivate SubnetのEC2へ直接アクセスすることはできません。
そのため、EC2をPrivate Subnetに置いたまま、必要な外部通信だけを行うことができます。
VPC EndpointとNAT Gatewayの使い分け
今回の構成では、通信先によってVPC EndpointとNAT Gatewayを使い分けています。
| 通信内容 | 通信先 | 使うもの |
|---|---|---|
| SSM AgentがSystems Managerと通信 | AWSサービス | VPC Endpoint |
| Session Managerを使う | AWSサービス | VPC Endpoint |
| SSM Run Commandを実行する | AWSサービス | VPC Endpoint |
| S3からAnsible zipを取得する | AWSサービス | S3 Gateway Endpoint |
| CloudWatch Logsへログを出力する | AWSサービス | VPC Endpoint |
dnf install でパッケージを取得する |
外部リポジトリ | NAT Gateway |
ansible-galaxy でcollectionを取得する |
外部リポジトリ | NAT Gateway |
ざっくり言うと、以下のような考え方です。
VPC Endpoint
→ AWSサービスへプライベートに接続するための経路
NAT Gateway
→ Private Subnetからインターネットへ出るための経路
S3 Gateway EndpointでAnsibleコードを取得できるようにした
今回、AnsibleコードはS3に配置しています。
そのため、S3 Gateway Endpointを作成し、Private Subnetに関連付いているRoute Tableへ関連付けました。
これにより、Private SubnetのEC2からS3上のAnsible zipを取得できることを確認しました。
aws s3 cp s3://<bucket-name>/ansible/ansible.zip /tmp/ansible.zip
ただし、それだけではAnsibleの実行に必要なパッケージまではそろいません。
例えば、EC2上で以下を実行する場合は、外部リポジトリへの通信が必要になります。
sudo dnf install -y unzip ansible-core
ansible-galaxy collection install community.mysql
そのため、S3 Endpointを作成しても、外部パッケージを取得する処理がある限り、NAT Gatewayが必要になります。
もしNAT Gatewayをなくしたい場合
NAT Gatewayを使わない構成にすることも可能です。
ただし、その場合はEC2がインターネットへ出なくてもよいように、必要なものを事前に用意しておく必要があります。
例えば、以下のような方法が考えられます。
Ansible実行に必要なファイルをすべてS3に置く
必要なcollectionも事前にダウンロードしてS3に置く
ansible-coreなどをインストール済みのAMIを作成する
dnf installが不要な構成にする
このようにすれば、S3 Endpointだけでデプロイに必要なファイルを取得できる可能性があります。
ただし、構成は少し複雑になります。
今回の学習用構成では、SSMやS3などのAWSサービスにはVPC Endpointを使い、外部リポジトリへの通信にはNAT Gatewayを使う形にしました。
TerraformでのNAT Gatewayへのルート作成例
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "AwsStudy-nat-eip"
}
}
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_1a.id
tags = {
Name = "AwsStudy-nat"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
tags = {
Name = "AwsStudy-private-rt"
}
}
resource "aws_route" "private_default" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this.id
}
resource "aws_route_table_association" "private_1a" {
subnet_id = aws_subnet.private_1a.id
route_table_id = aws_route_table.private.id
}
NAT Gateway自体はPublic Subnetに配置し、Private Subnet用のRoute TableからNAT Gatewayへ向けています。
これにより、Private SubnetのEC2からインターネット上のパッケージリポジトリへアウトバウンド通信できるようになります。
TerraformでのSSM用VPC Endpoint作成例
SSM Run CommandやSession Managerを利用するため、SSM関連のVPC Endpointを作成しました。
resource "aws_vpc_endpoint" "ssm" {
vpc_id = aws_vpc.this.id
service_name = "com.amazonaws.ap-northeast-1.ssm"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private_1a.id, aws_subnet.private_1c.id]
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ssmmessages" {
vpc_id = aws_vpc.this.id
service_name = "com.amazonaws.ap-northeast-1.ssmmessages"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private_1a.id, aws_subnet.private_1c.id]
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ec2messages" {
vpc_id = aws_vpc.this.id
service_name = "com.amazonaws.ap-northeast-1.ec2messages"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private_1a.id, aws_subnet.private_1c.id]
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.this.id
service_name = "com.amazonaws.ap-northeast-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
}
Interface EndpointにはSecurity Groupを関連付けます。
EC2からVPC EndpointへHTTPS通信できるように、EC2用Security Groupからの443番を許可しました。
resource "aws_security_group" "vpc_endpoint" {
name = "AwsStudy-vpc-endpoint-sg"
vpc_id = aws_vpc.this.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.ec2.id]
}
}
改善後の全体構成
改善後の構成は以下のようになります。
Internet
↓
Internet Gateway
↓
Public Subnet
↓
ALB
↓
Private Subnet
↓
EC2
↓
Private Subnet
↓
RDS
管理用の接続はSSHではなく、Session Managerを使います。
管理者
↓
Session Manager
↓
Private SubnetのEC2
この構成にすることで、外部公開するのはALBだけにできました。
EC2とRDSはPrivate Subnetに配置し、Security Groupでも必要な通信だけを許可することで、より安全な構成に近づけることができました。
今回の構成で学んだこと
今回の構成を作って、特に以下の点が理解しやすくなりました。
- 外部公開するのはALBだけでよい
- EC2はPrivate Subnetに置いても、ALB経由でアプリケーション公開できる
- RDSはEC2から接続できればよく、インターネットへ公開する必要はない
- Private SubnetのEC2には、通信先に応じてNAT GatewayやVPC Endpointが必要
- サブネットはリソース1台ごとではなく、役割やAZ単位で考える
まとめ
TerraformでAWSの3層構成を作る中で、VPC・Subnet・ALB・EC2・RDSの関係を整理しました。
今回学んだことは以下です。
- VPCはAWS上の自分専用ネットワーク
- SubnetはVPC内を用途やAZごとに分けたもの
- Public SubnetはInternet Gatewayへのルートを持つ
- Private Subnetはインターネットから直接アクセスされない
- ALBは外部からアクセスされるためPublic Subnetに置く
- EC2はPrivate Subnetに置くとより安全
- RDSは基本的にPrivate Subnetに置く
- Security Groupは通信の流れに沿って最小限にする
- サブネットはリソース1台ごとではなく、役割やAZ単位で考える
- Private SubnetのEC2が外部と通信するには、通信先に応じてNAT GatewayやVPC Endpointが必要
最初は、Public SubnetとPrivate Subnetの違いや、ALB・EC2・RDSをどこに置くべきかが曖昧でした。
しかし、実際にTerraformで作ってみることで、AWSのネットワーク構成をかなり具体的に理解できるようになりました。
単にリソースを作るだけでなく、「なぜそのリソースをその場所に置くのか」「そのリソースが外部と通信するにはどの経路が必要なのか」を考えることで、AWSのネットワーク設計をより具体的に理解できました。