今回作る環境
VPC: 10.0.0.0/16
AZ-a
├─ Public Subnet: 10.0.1.0/24
│ ├─ ALB
│ ├─ NAT Gateway
│ └─ Bastion EC2
│
├─ Private App Subnet: 10.0.11.0/24
│ └─ EC2 API Server
│
└─ Private DB Subnet: 10.0.21.0/24
└─ RDS Primary
AZ-c
├─ Public Subnet: 10.0.2.0/24
│ ├─ ALB
│ ├─ NAT Gateway
│ └─ Bastion EC2
│
├─ Private App Subnet: 10.0.12.0/24
│ └─ EC2 API Server
│
└─ Private DB Subnet: 10.0.22.0/24
└─ RDS Primary
本番向け Multi-AZ 3層Webシステム構成
ユーザーから見れば、ブラウザでアクセスしてページを見る
AWS内部では、2つのEC2/RDSを立て、ロードバランサで振り分けている
構成図は下記の通り。
Internet
│
│
┌──────────┴──────────┐
│ Internet Gateway │
│ (IGW) │
└──────────┬──────────┘
│
???????????????????????????????????????????????????????????
VPC : 10.0.0.0/16
???????????????????????????????????????????????????????????
┌──────────────────── AZ-a ────────────────────┐
Public Subnet
10.0.1.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
0.0.0.0/0 → IGW
ALB Node
10.0.1.10
NAT Gateway
10.0.1.20
Elastic IP
54.x.x.x
Bastion EC2
10.0.1.30
────────────────────────────────────
Private App Subnet
10.0.11.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
0.0.0.0/0 → NAT-A
EC2 API Server
10.0.11.10
────────────────────────────────────
Private DB Subnet
10.0.21.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
RDS Primary
10.0.21.10
└──────────────────────────────────────────────┘
┌──────────────────── AZ-c ────────────────────┐
Public Subnet
10.0.2.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
0.0.0.0/0 → IGW
ALB Node
10.0.2.10
NAT Gateway
10.0.2.20
Elastic IP
18.x.x.x
────────────────────────────────────
Private App Subnet
10.0.12.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
0.0.0.0/0 → NAT-C
EC2 API Server
10.0.12.10
────────────────────────────────────
Private DB Subnet
10.0.22.0/24
────────────────────────────────────
Route Table
10.0.0.0/16 → local
RDS Standby
10.0.22.10
└──────────────────────────────────────────────┘
通信例
通信① ユーザーアクセス
Browser
↓
ALB DNS
↓
ALB (10.0.1.10)
↓
EC2 (10.0.11.10)
↓
RDS Primary (10.0.21.10)
または
Browser
↓
ALB DNS
↓
ALB (10.0.2.10)
↓
EC2 (10.0.12.10)
↓
RDS Primary (10.0.21.10)
通信② EC2のインターネットアクセス
AZ-a
EC2
10.0.11.10
↓
NAT Gateway
10.0.1.20
↓
Elastic IP
54.x.x.x
↓
Internet
AZ-c
EC2
10.0.12.10
↓
NAT Gateway
10.0.2.20
↓
Elastic IP
18.x.x.x
↓
Internet
通信③ 管理者ログイン
自宅PC
203.0.113.10
↓
SSH(22)
↓
Bastion
10.0.1.30
↓
SSH(22)
↓
EC2
10.0.11.10
または
自宅PC
203.0.113.10
↓
SSH
↓
Bastion
10.0.1.30
↓
SSH
↓
EC2
10.0.12.10
障害時
AZ-a障害
RDS Primary
停止
↓
自動フェイルオーバー
↓
RDS Standby
10.0.22.10
↓
Primary昇格
アプリ側は同じRDSエンドポイントを利用するため、
アプリ設定変更不要
です。
サブネット一覧
| 用途 | CIDR |
|---|---|
| VPC | 10.0.0.0/16 |
| Public-A | 10.0.1.0/24 |
| Public-C | 10.0.2.0/24 |
| App-A | 10.0.11.0/24 |
| App-C | 10.0.12.0/24 |
| DB-A | 10.0.21.0/24 |
| DB-C | 10.0.22.0/24 |
※ ALB・NAT Gateway・RDS の実際のIPアドレスは AWS が自動管理するため、上記の IP は理解しやすくするための例です。
AWSネットワーク構成を理解するための全体像
「インターネット公開部分」「アプリ部分」「DB部分」を分離して安全に運用する構成」
です。
まず全体像をシンプルにするとこうなります。
利用者
↓
ALB
↓
EC2(API)
↓
RDS(DB)
ただし、
EC2やRDSを直接インターネットへ公開すると危険
なので、
Public
↓
Private App
↓
Private DB
の3層に分けています。
まずVPCとは何か
VPCはAWS上の自分専用ネットワークです。
AWS
┌────────────────────┐
│ あなた専用のLAN │
│ 10.0.0.0/16 │
└────────────────────┘
なぜSubnetを分けるのか
あなたの構成は
Public Subnet
Private App Subnet
Private DB Subnet
に分かれています。
理由は
見せてもいいもの
見せたくないもの
絶対見せたくないもの
を分離するためです。
Public Subnet
ここはインターネットと通信できます。
Internet
↓
Public Subnet
配置するもの
ALB
NAT Gateway
Bastion
ALB
利用者の入口です。
Browser
↓
ALB
例えば
https://example.com
へアクセスすると
まずALBに到達します。
ALBは
どのEC2へ転送するか
を決めます。
Bastion
踏み台サーバです。
管理者だけがログインします。
あなた
↓
SSH
↓
Bastion
一般ユーザーは使いません。
NAT Gateway
Private Subnetから外へ出るための出口です。
Private EC2
↓
NAT
↓
Internet
Private App Subnet
ここにアプリケーションを置きます。
EC2 API Server
重要なのは
インターネットから直接アクセス不可
です。
利用者は
Browser
↓
ALB
↓
EC2
の順で到達します。
つまり
○ Browser → ALB → EC2
× Browser → EC2
です。
なぜPrivateに置くのか
もしEC2をPublicに置くと
全世界
↓
EC2
になります。
攻撃対象になりやすいです。
そのため
ALBだけ公開
EC2は非公開
にします。
Private DB Subnet
ここは最も重要です。
RDS
を配置します。
通信経路は
EC2
↓
3306
↓
RDS
のみです。
つまり
Browser → RDS
Internet → RDS
は不可能です。
Route Tableの意味
通信の行き先を決めています。
Public RT
0.0.0.0/0 → IGW
意味
外へ出るならInternet Gateway
App RT
0.0.0.0/0 → NAT Gateway
意味
外へ出るならNAT経由
DB RT
localのみ
意味
VPC内しか通信できない
Security Groupの意味
Security Groupは
誰から来た通信を受けるか
を決めています。
ALB-SG
Internet
↓
TCP80
↓
ALB
誰でもアクセス可能
APP-SG
ALB
↓
TCP8080
↓
EC2
ALBからだけ許可
Bastion
↓
TCP22
↓
EC2
管理者だけSSH可能
DB-SG
EC2
↓
TCP3306
↓
RDS
EC2だけDB接続可能
実際の通信① Webアクセス
ユーザーがアクセスすると
Browser
↓
Internet
↓
IGW
↓
ALB
↓
EC2(API)
↓
RDS
処理後
RDS
↓
EC2
↓
ALB
↓
Browser
へ結果を返します。
実際の通信② pip install
EC2はPrivateなので
直接Internetへ出られない
です。
代わりに
EC2
↓
NAT Gateway
↓
IGW
↓
PyPI
となります。
例えば
pip install pymysql
すると
Private EC2
↓
NAT
↓
pypi.org
へアクセスします。
実際の通信③ 管理者ログイン
管理者は直接Private EC2へ入れません。
まず踏み台へ入ります。
あなた
↓
SSH
↓
Bastion
↓
SSH
↓
Private EC2
これにより
Internet
↓
Private EC2
を禁止できます。
この構成で学べること
この1つの環境だけで、
VPC
Subnet
Route Table
Internet Gateway
NAT Gateway
Elastic IP
Security Group
ALB
EC2
RDS
Multi-AZ
Bastion
というAWSネットワークの重要要素をほぼ一通り学べます。
AWSの業務では、この構成が 「教科書的な3層Webシステム構成」 として非常によく使われます。
Route Tableの役割
Route Table(ルートテーブル) は、
「通信先ごとに、どこへ転送するかを決めるルール表」
です。
Public RT
10.0.0.0/16 → local
0.0.0.0/0 → IGW
意味
VPC内の通信 → 直接送る
インターネット向け通信 → Internet Gatewayへ送る
そのため Public Subnet のEC2はインターネットと通信できます。
App RT
10.0.0.0/16 → local
0.0.0.0/0 → NAT Gateway
意味
VPC内の通信 → 直接送る
インターネット向け通信 → NAT Gatewayへ送る
そのため Private Subnet のEC2は、
外へアクセス可能
外部からアクセス不可
になります。
DB RT
10.0.0.0/16 → local
意味
VPC内との通信のみ許可
インターネットへの経路なし
そのため RDS などのDBは外部と通信できません。
一言でいうと
Public RT → Internet Gateway経由で外と通信
App RT → NAT Gateway経由で外へだけ通信
DB RT → VPC内だけ通信
Route Tableは 「通信の行き先を決める交通整理表」 です。
Security Groupの役割
Security Group(SG) は、
「EC2・RDS・ALBごとに設定するファイアウォール」
です。
どの通信を許可するかを制御します。
ALB-SG
許可
Internet
↓
TCP80
↓
ALB
意味
インターネットからALBへのHTTPアクセスを許可
APP-SG
許可①
ALB
↓
TCP8080
↓
EC2
意味
ALBからアプリサーバへのアクセスを許可
許可②
Bastion
↓
TCP22
↓
EC2
意味
踏み台サーバからSSH接続を許可
DB-SG
許可
EC2 API
↓
TCP3306
↓
RDS
意味
アプリサーバからDB(MySQL)への接続を許可
一言でいうと
ALB-SG → 利用者からALBへの通信を許可
APP-SG → ALBやBastionからEC2への通信を許可
DB-SG → EC2からRDSへの通信を許可
Security Group は 「サーバごとの通信許可リスト」 です。
NAT Gatewayの役割
NAT GatewayはPrivate EC2の代理人です。
可能
Private EC2
↓
NAT
↓
Internet
不可
Internet
↓
NAT
↓
Private EC2
ALBの役割
Internet
↓
ALB
↓
EC2-A
または
Internet
↓
ALB
↓
EC2-B
へ振り分けます。
実際に作っていく
VPC: 10.0.0.0/16
AZ-a
├─ Public Subnet: 10.0.1.0/24
│ ├─ ALB
│ ├─ NAT Gateway
│ └─ Bastion EC2
│
├─ Private App Subnet: 10.0.11.0/24
│ └─ EC2 API Server
│
└─ Private DB Subnet: 10.0.21.0/24
└─ RDS Primary
AZ-c
├─ Public Subnet: 10.0.2.0/24
│ ├─ ALB
│ ├─ NAT Gateway
│ └─ Bastion EC2
│
├─ Private App Subnet: 10.0.12.0/24
│ └─ EC2 API Server
│
└─ Private DB Subnet: 10.0.21.0/24
└─ RDS Primary
使う環境変数の定義
export AWS_REGION=ap-northeast-1
export AZ1=ap-northeast-1a
export AZ2=ap-northeast-1c
export DB_USER=admin
export DB_PASS='Password123456!'
export DB_NAME=inventorydb
VPCの作成
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --region $AWS_REGION --query 'Vpc.VpcId' --output text)
| 引数 | 意味 |
|---|---|
aws ec2 create-vpc |
VPCを作成するAWS CLIコマンド |
--cidr-block 10.0.0.0/16 |
VPCで使うIPアドレス範囲 |
--region $AWS_REGION |
作成先リージョン |
--query 'Vpc.VpcId' |
結果からVPC IDだけ抜き出す |
--output text |
出力をテキスト形式にする |
aws ec2 describe-vpcs
aws ec2 describe-vpcs --query 'Vpcs[*].[VpcId,CidrBlock,State]' --output table
10.0.0.0/16 は 10.0.0.0~10.0.255.255 まで使える大きなネットワーク
VPCのタグ付け
aws ec2 create-tags --resources $VPC_ID --tags Key=Name,Value=inventory-vpc --region $AWS_REGION
| 引数 | 意味 |
|---|---|
create-tags |
AWSリソースにタグを付ける |
--resources $VPC_ID |
タグを付ける対象 |
--tags Key=Name,Value=inventory-vpc |
名前を inventory-vpc にする |
SUBNETの作成
VPC
├─ Public Subnet
├─ Private App Subnet
└─ Private DB Subnet
PUBLIC_SUBNET_1=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone $AZ1 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
PUBLIC_SUBNET_2=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone $AZ2 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
APP_SUBNET_1=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.11.0/24 --availability-zone $AZ1 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
APP_SUBNET_2=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.12.0/24 --availability-zone $AZ2 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
DB_SUBNET_1=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.21.0/24 --availability-zone $AZ1 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
DB_SUBNET_2=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.22.0/24 --availability-zone $AZ2 --region $AWS_REGION --query 'Subnet.SubnetId' --output text)
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --output table
| 引数 | 意味 |
|---|---|
create-subnet |
Subnetを作成 |
--vpc-id $VPC_ID |
どのVPC内に作るか |
--cidr-block 10.0.1.0/24 |
SubnetのIP範囲 |
--availability-zone $AZ1 |
どのAZに作るか |
--query 'Subnet.SubnetId' |
Subnet IDだけ取得 |
--output text |
テキストで出力 |
| Subnet | CIDR | 役割 |
|---|---|---|
PUBLIC_SUBNET_1 |
10.0.1.0/24 |
ALB/NAT用 |
PUBLIC_SUBNET_2 |
10.0.2.0/24 |
ALB冗長化用 |
APP_SUBNET_1 |
10.0.11.0/24 |
EC2 API用 |
APP_SUBNET_2 |
10.0.12.0/24 |
EC2 API冗長化用 |
DB_SUBNET_1 |
10.0.21.0/24 |
RDS用 |
DB_SUBNET_2 |
10.0.22.0/24 |
RDS冗長化用 |
Internet Gateway作成
VPCをインターネットに接続するための部品です。
IGW_ID=$(aws ec2 create-internet-gateway --region $AWS_REGION --query 'InternetGateway.InternetGatewayId' --output text)
| 引数 | 意味 |
|---|---|
create-internet-gateway |
Internet Gatewayを作成 |
--query |
IGW IDだけ抜き出す |
--output text |
テキストで出力 |
Internet
↓
Internet Gateway
↓
VPC
作成したInternet GatewayをVPCに接続します。
aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $AWS_REGION
| 引数 | 意味 |
|---|---|
attach-internet-gateway |
IGWをVPCに接続 |
--internet-gateway-id $IGW_ID |
接続するIGW |
--vpc-id $VPC_ID |
接続先VPC |
public route table
PUBLIC_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID --region $AWS_REGION --query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PUBLIC_RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID --region $AWS_REGION
ルートテーブルを作成し、そのルートテーブルにインターネットへ出るための経路を追加している。
| 引数 | 意味 |
|---|---|
create-route |
ルートを追加 |
--route-table-id $PUBLIC_RT |
追加先のRoute Table |
--destination-cidr-block 0.0.0.0/0 |
全インターネット宛 |
--gateway-id $IGW_ID |
Internet Gatewayへ流す |
aws ec2 associate-route-table --route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNET_1 --region $AWS_REGION
aws ec2 associate-route-table --route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNET_2 --region $AWS_REGION
Route TableをSubnetに関連付けます。
| 引数 | 意味 |
|---|---|
associate-route-table |
Route TableをSubnetへ適用 |
--route-table-id |
適用するRoute Table |
--subnet-id |
適用先Subnet |
NAT Gateway作成
NAT Gateway用の固定パブリックIPを確保します。
EIP_ALLOC_1_ID=$(aws ec2 allocate-address --domain vpc --region $AWS_REGION --query 'AllocationId' --output text)
EIP_ALLOC_2_ID=$(aws ec2 allocate-address --domain vpc --region $AWS_REGION --query 'AllocationId' --output text)
| 引数 | 意味 |
|---|---|
allocate-address |
Elastic IPを確保 |
--domain vpc |
VPC用Elastic IPとして作成 |
--query 'AllocationId' |
Allocation IDだけ取得 |
NAT GatewayをPublic Subnetに作成します。
NAT Gatewayは、Private Subnet内のEC2が外部へ出るために使います。
Private EC2
↓
NAT Gateway
↓
Internet
NAT_GW_1_ID=$(aws ec2 create-nat-gateway --subnet-id $PUBLIC_SUBNET_1 --allocation-id $EIP_ALLOC_1_ID --region $AWS_REGION --query 'NatGateway.NatGatewayId' --output text)
NAT_GW_2_ID=$(aws ec2 create-nat-gateway --subnet-id $PUBLIC_SUBNET_2 --allocation-id $EIP_ALLOC_2_ID --region $AWS_REGION --query 'NatGateway.NatGatewayId' --output text)
| 引数 | 意味 |
|---|---|
create-nat-gateway |
NAT Gatewayを作成 |
--subnet-id $PUBLIC_SUBNET_1 |
NATを配置するPublic Subnet |
--allocation-id $EIP_ALLOC_ID |
NATに割り当てるElastic IP |
NAT Gatewayが利用可能になるまで待ちます。
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW_1_ID --region $AWS_REGION
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW_2_ID --region $AWS_REGION
| 引数 | 意味 |
|---|---|
wait nat-gateway-available |
NAT Gatewayがavailableになるまで待機 |
--nat-gateway-ids |
待機対象のNAT Gateway |
Private App Route Table作成
Private App Subnet用のRoute Tableを作ります。
APP_RT_1=$(aws ec2 create-route-table --vpc-id $VPC_ID --region $AWS_REGION --query 'RouteTable.RouteTableId' --output text)
APP_RT_2=$(aws ec2 create-route-table --vpc-id $VPC_ID --region $AWS_REGION --query 'RouteTable.RouteTableId' --output text)
Private EC2から外部通信するとき、NAT Gatewayを通るようにします。
aws ec2 create-route --route-table-id $APP_RT_1 --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_GW_1_ID --region $AWS_REGION
aws ec2 create-route --route-table-id $APP_RT_2 --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_GW_2_ID --region $AWS_REGION
| 引数 | 意味 |
|---|---|
--destination-cidr-block 0.0.0.0/0 |
外部全体 |
--nat-gateway-id $NAT_GW_ID |
NAT Gatewayへ流す |
Private App SubnetにこのRoute Tableを適用します。これにより、APIサーバー用EC2は外部へ出られます
aws ec2 associate-route-table --route-table-id $APP_RT_1 --subnet-id $APP_SUBNET_1 --region $AWS_REGION
aws ec2 associate-route-table --route-table-id $APP_RT_2 --subnet-id $APP_SUBNET_2 --region $AWS_REGION
Private EC2
↓
Private App Route Table
↓
NAT Gateway
↓
Internet Gateway
↓
Internet
Private DB Route Table 作成
DB Subnet は基本的にインターネットへ出さないため、0.0.0.0/0 は作りません。
export PRIVATE_DB_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--region $AWS_REGION \
--query "RouteTable.RouteTableId" \
--output text)
aws ec2 associate-route-table \
--route-table-id $PRIVATE_DB_RT \
--subnet-id $PRIVATE_DB_SUBNET_A \
--region $AWS_REGION
aws ec2 associate-route-table \
--route-table-id $PRIVATE_DB_RT \
--subnet-id $PRIVATE_DB_SUBNET_C \
--region $AWS_REGION
Security Group作成
Security GroupはEC2やALBやRDSに付けるファイアウォールです。
ALB用の通信ルールを作ります。
ALB_SG=$(aws ec2 create-security-group --group-name inventory-alb-sg --description "ALB security group" --vpc-id $VPC_ID --region $AWS_REGION --query 'GroupId' --output text)
| 引数 | 意味 |
|---|---|
create-security-group |
Security Group作成 |
--group-name |
SG名 |
--description |
説明 |
--vpc-id |
どのVPC用か |
--query 'GroupId' |
SG IDだけ取得 |
インターネットからALBへのHTTPアクセスを許可します。
aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 80 --cidr 0.0.0.0/0 --region $AWS_REGION
| 引数 | 意味 |
|---|---|
authorize-security-group-ingress |
インバウンド許可 |
--group-id $ALB_SG |
対象SG |
--protocol tcp |
TCP通信 |
--port 80 |
HTTPポート |
--cidr 0.0.0.0/0 |
全世界から許可 |
APIサーバー用EC2のSGを作成します。
APP_SG=$(aws ec2 create-security-group --group-name inventory-app-sg --description "App EC2 security group" --vpc-id $VPC_ID --region $AWS_REGION --query 'GroupId' --output text)
ALBからEC2 APIへのアクセスだけ許可します。
aws ec2 authorize-security-group-ingress --group-id $APP_SG --protocol tcp --port 8080 --source-group $ALB_SG --region $AWS_REGION
| 引数 | 意味 |
|---|---|
--port 8080 |
APIサーバーの待受ポート |
--source-group $ALB_SG |
ALB SGからの通信だけ許可 |
つまり、ALB → EC2:8080だけ許可します。
RDS用のSecurity Groupを作ります。
DB_SG=$(aws ec2 create-security-group --group-name inventory-db-sg --description "RDS security group" --vpc-id $VPC_ID --region $AWS_REGION --query 'GroupId' --output text)
EC2 APIからRDS MySQLへの接続だけ許可します。
aws ec2 authorize-security-group-ingress --group-id $DB_SG --protocol tcp --port 3306 --source-group $APP_SG --region $AWS_REGION
| 引数 | 意味 |
|---|---|
--port 3306 |
MySQLのポート |
--source-group $APP_SG |
APIサーバーからのみ許可 |
Routetableで通信を制御できるならSecurity Groupいらなくない?
必要です。
Route Table と Security Group は役割が違います。
Route Table
= どこへ送るかを決める
Security Group
= 通してよい通信かを判定する
例えると、
Route Table
= 道案内・カーナビ
Security Group
= 建物の入口の警備員
です。
例
App EC2 から RDS に接続する場合、
App EC2 → RDS
Route Table があることで、
RDSがあるサブネットへ到達できる
ようになります。
しかし Route Table は、
どこへ送るか
しか判断しません。
つまり、
MySQLの3306番ポートだけ許可
や
このEC2だけ接続可能
という制御はできません。
そこで Security Group を使います。
例えば RDS の Security Group に、
App EC2のSecurity Groupから
3306番ポートのみ許可
を設定します。
Route Table と Security Group の違い
| 項目 | Route Table | Security Group |
|---|---|---|
| 役割 | 経路制御 | 通信制御 |
| 見るもの | 宛先CIDR | 送信元・ポート・プロトコル |
| 単位 | サブネット | ENI/EC2/RDS/ALB |
| 主な目的 | どこへ流すか | 誰が何番ポートへ来てよいか |
| 許可単位 | IPレンジ | IP・SG・Port |
| Statefull | 関係なし | Stateful |
Route Tableだけだと危険な例
VPC
10.0.0.0/16
App Subnet
10.0.11.0/24
DB Subnet
10.0.21.0/24
Route Table があるため、
10.0.11.0/24
↓
10.0.21.0/24
へ通信できます。
つまり、
App EC2 → RDS
だけでなく、
別のEC2 → RDS
Bastion → RDS
テスト用EC2 → RDS
なども到達できてしまいます。
Route Table は
同じVPC内だから送る
という判断しかしません。
Security Groupを使う場合
RDSのSecurity Group
許可:
Source = App-SG
Port = 3306
すると、
App EC2 → RDS
だけ許可されます。
実際の通信判定
例えば
Internet
↓
ALB
↓
EC2
↓
RDS
の場合、
通信ごとに
① Route Table
② Security Group
の両方を通過する必要があります。
ブラウザ → ALB
Route Table
0.0.0.0/0 → IGW
OK
Security Group
80
443
許可
OK
ALB → EC2
Route Table
VPC内部通信
OK
Security Group
ALB-SGから80許可
OK
EC2 → RDS
Route Table
VPC内部通信
OK
Security Group
App-SGから3306許可
OK
AWSでは両方必要
Route Table
どこへ送るか
Security Group
通してよいか
です。
一言でいうと
Route Table は
道路
です。
Security Group は
関所
です。
道路があっても、
関所が通行禁止
なら通れません。
逆に、
関所がOK
でも
道路が存在しない
なら到達できません。
そのため AWS では、
Route Table
+
Security Group
の両方を使って通信を制御します。
RDS作成
RDS作成
aws rds create-db-subnet-group --db-subnet-group-name inventory-db-subnet-group --db-subnet-group-description "Inventory DB subnet group" --subnet-ids $DB_SUBNET_1 $DB_SUBNET_2 --region $AWS_REGION
MySQL作成
aws rds create-db-instance --db-instance-identifier inventory-db --db-instance-class db.t3.micro --engine mysql --master-username $DB_USER --master-user-password $DB_PASS --allocated-storage 20 --db-name $DB_NAME --vpc-security-group-ids $DB_SG --db-subnet-group-name inventory-db-subnet-group --backup-retention-period 0 --no-publicly-accessible --region $AWS_REGION
aws rds wait db-instance-available --db-instance-identifier inventory-db --region $AWS_REGION
DB_ENDPOINT=$(aws rds describe-db-instances --db-instance-identifier inventory-db --region $AWS_REGION --query 'DBInstances[0].Endpoint.Address' --output text)
echo $DB_ENDPOINT
EC2用IAM Role作成
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
EOF
| 項目 | 意味 |
|---|---|
Principal: ec2.amazonaws.com |
EC2サービスに許可 |
sts:AssumeRole |
Roleを引き受ける権限 |
Effect: Allow |
許可 |
- Roleを作る
aws iam create-role --role-name inventory-ec2-role --assume-role-policy-document file://trust-policy.json
- Roleに権限を付ける
aws iam attach-role-policy --role-name inventory-ec2-role --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
| 引数 | 意味 |
|---|---|
attach-role-policy |
Roleに権限を付与 |
--policy-arn |
付与するAWS管理ポリシー |
EC2にIAM Roleを付けるための入れ物を作ります。
-
Instance Profileを作る
aws iam create-instance-profile --instance-profile-name inventory-ec2-profile -
Instance ProfileにRoleを入れる。
aws iam add-role-to-instance-profile --instance-profile-name inventory-ec2-profile --role-name inventory-ec2-role
EC2へ直接Roleを付けるのではなく、実際にはInstance Profile経由で付けます。
この一連のコマンドは、「EC2にSSM接続できる権限を付与する」ための設定です。
全体の流れは次のようになります。
IAM Policy
↓
IAM Role
↓
Instance Profile
↓
EC2
このポリシーでできること
EC2
↓
Systems Manager
↓
Session Manager
を利用可能にします。
つまり
aws ssm start-session
で接続できるようになります。
AWSでは
EC2
↓
Role
ではなく
EC2
↓
Instance Profile
↓
Role
という構造になっています。
結果
inventory-ec2-profile
↓
inventory-ec2-role
という関係になります。
aws ec2 run-instances \
--iam-instance-profile Name=inventory-ec2-profile
EC2起動時にProfileを指定します。
すると
EC2
↓
inventory-ec2-profile
↓
inventory-ec2-role
↓
AmazonSSMManagedInstanceCore
という権限構造になります。
なぜ直接Roleを付けないの?
AWSの内部構造上、
EC2
↓
Instance Profile
↓
Role
という形式だからです。
実際には
Role = 権限
Instance Profile = EC2にRoleを装着するための部品
と考えると分かりやすいです。
最終形
AmazonSSMManagedInstanceCore
↓
inventory-ec2-role
↓
inventory-ec2-profile
↓
EC2
これによりEC2はSSMの権限を取得し、
aws ssm start-session --target i-xxxxxxxx
でSSH不要のリモート接続ができるようになります。
APIプログラムをUserDataで作る
EC2起動時に実行する初期化スクリプトを作ります。
EC2のUser Dataは、インスタンス起動時にコマンドを実行するために使えます。
cat > user-data.sh << EOF
#!/bin/bash
dnf update -y
dnf install -y python3 python3-pip mariadb105
pip3 install pymysql
cat > /home/ec2-user/app.py << 'PYEOF'
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import pymysql
import os
DB_HOST = os.environ.get("DB_HOST")
DB_USER = os.environ.get("DB_USER", "admin")
DB_PASS = os.environ.get("DB_PASS")
DB_NAME = os.environ.get("DB_NAME", "inventorydb")
def get_connection():
return pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
database=DB_NAME,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor
)
def init_db():
conn = get_connection()
with conn:
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL
)
""")
cur.execute("SELECT COUNT(*) AS count FROM products")
count = cur.fetchone()["count"]
if count == 0:
cur.execute("INSERT INTO products (name, stock) VALUES ('Laptop', 12)")
cur.execute("INSERT INTO products (name, stock) VALUES ('Mouse', 40)")
cur.execute("INSERT INTO products (name, stock) VALUES ('Keyboard', 25)")
conn.commit()
class Handler(BaseHTTPRequestHandler):
def send_json(self, data, status=200):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/" or self.path == "/health":
self.send_json({"status": "ok", "service": "inventory-api"})
return
if self.path == "/products":
try:
conn = get_connection()
with conn:
with conn.cursor() as cur:
cur.execute("SELECT id, name, stock FROM products ORDER BY id")
rows = cur.fetchall()
self.send_json(rows)
except Exception as e:
self.send_json({"error": str(e)}, 500)
return
self.send_json({"error": "not found"}, 404)
if __name__ == "__main__":
init_db()
server = HTTPServer(("0.0.0.0", 8080), Handler)
server.serve_forever()
PYEOF
cat > /etc/systemd/system/inventory-api.service << SERVICEEOF
[Unit]
Description=Inventory API
After=network.target
[Service]
Environment=DB_HOST=$DB_ENDPOINT
Environment=DB_USER=$DB_USER
Environment=DB_PASS=$DB_PASS
Environment=DB_NAME=$DB_NAME
ExecStart=/usr/bin/python3 /home/ec2-user/app.py
Restart=always
User=ec2-user
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload
systemctl enable inventory-api
systemctl start inventory-api
EOF
EC2 API Server作成
Amazon Linux 2023の最新AMI IDを取得します
AMI_ID=$(aws ec2 describe-images --owners amazon --filters "Name=name,Values=al2023-ami-*-x86_64" "Name=state,Values=available" --region $AWS_REGION --query 'sort_by(Images, &CreationDate)[-1].ImageId' --output text)
| 引数 | 意味 |
|---|---|
describe-images |
AMI一覧取得 |
--owners amazon |
Amazon公式AMIのみ |
--filters |
条件指定 |
Name=name,Values=al2023-ami-*-x86_64 |
Amazon Linux 2023 x86_64 |
Name=state,Values=available |
利用可能なAMI |
sort_by(...)[-1] |
最新のAMIを選ぶ |
Private App SubnetにEC2 APIサーバーを作成します。
APP_INSTANCE_1_ID=$(aws ec2 run-instances --image-id $AMI_ID --instance-type t3.micro --subnet-id $APP_SUBNET_1 --security-group-ids $APP_SG --iam-instance-profile Name=inventory-ec2-profile --user-data file://user-data.sh --region $AWS_REGION --query 'Instances[0].InstanceId' --output text)
APP_INSTANCE_2_ID=$(aws ec2 run-instances --image-id $AMI_ID --instance-type t3.micro --subnet-id $APP_SUBNET_2 --security-group-ids $APP_SG --iam-instance-profile Name=inventory-ec2-profile --user-data file://user-data.sh --region $AWS_REGION --query 'Instances[0].InstanceId' --output text)
| 引数 | 意味 |
|---|---|
run-instances |
EC2を起動 |
--image-id $AMI_ID |
使用するAMI |
--instance-type t3.micro |
EC2サイズ |
--subnet-id $APP_SUBNET_1 |
配置先Private Subnet |
--security-group-ids $APP_SG |
API用SGを付与 |
--iam-instance-profile |
EC2用IAM権限 |
--user-data file://user-data.sh |
起動時スクリプト |
--query |
Instance IDだけ取得 |
aws ec2 wait instance-running --instance-ids $APP_INSTANCE_1_ID --region $AWS_REGION
aws ec2 wait instance-running --instance-ids $APP_INSTANCE_2_ID --region $AWS_REGION
echo $APP_INSTANCE_ID
ALB作成
インターネット向けのApplication Load Balancerを作成します。
ブラウザからのアクセスはまずALBに来ます。
Browser → ALB → EC2 API
ALB_ARN=$(aws elbv2 create-load-balancer --name inventory-alb --subnets $PUBLIC_SUBNET_1 $PUBLIC_SUBNET_2 --security-groups $ALB_SG --scheme internet-facing --type application --region $AWS_REGION --query 'LoadBalancers[0].LoadBalancerArn' --output text)
| 引数 | 意味 |
|---|---|
elbv2 create-load-balancer |
ALB/NLBを作成 |
--name inventory-alb |
ALB名 |
--subnets |
ALBを配置するPublic Subnet |
--security-groups $ALB_SG |
ALB用SG |
--scheme internet-facing |
インターネット公開 |
--type application |
Application Load Balancer |
--query |
ALB ARNだけ取得 |
target group
ALBが転送する先のグループを作ります。
ALB
↓
Target Group
↓
EC2 API
TG_ARN=$(aws elbv2 create-target-group --name inventory-tg --protocol HTTP --port 8080 --vpc-id $VPC_ID --target-type instance --health-check-path /health --region $AWS_REGION --query 'TargetGroups[0].TargetGroupArn' --output text)
| 引数 | 意味 |
|---|---|
create-target-group |
Target Group作成 |
--name inventory-tg |
Target Group名 |
--protocol HTTP |
HTTPで転送 |
--port 8080 |
EC2側の待受ポート |
--vpc-id $VPC_ID |
対象VPC |
--target-type instance |
EC2インスタンスIDを登録 |
--health-check-path /health |
正常確認URL |
作成したEC2をTarget Groupに登録します。
aws elbv2 register-targets --target-group-arn $TG_ARN --targets Id=$APP_INSTANCE_1_ID --region $AWS_REGION
aws elbv2 register-targets --target-group-arn $TG_ARN --targets Id=$APP_INSTANCE_2_ID --region $AWS_REGION
| 引数 | 意味 |
|---|---|
register-targets |
ターゲット登録 |
--target-group-arn |
登録先Target Group |
--targets Id=$APP_INSTANCE_ID |
登録するEC2 |
ALB Listener作成
ALBが80番ポートでHTTPリクエストを受け取る設定を作ります。
Browser
↓ HTTP:80
ALB Listener
↓
Target Group
↓
EC2:8080
aws elbv2 create-listener --load-balancer-arn $ALB_ARN --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=$TG_ARN --region $AWS_REGION
| 引数 | 意味 |
|---|---|
create-listener |
ALBの受付設定を作成 |
--load-balancer-arn $ALB_ARN |
対象ALB |
--protocol HTTP |
HTTPで受ける |
--port 80 |
80番ポートで受ける |
--default-actions Type=forward,... |
Target Groupへ転送 |
ブラウザ確認
ALBのDNS名を取得します。
ALB_DNS=$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --region $AWS_REGION --query 'LoadBalancers[0].DNSName' --output text)
| 引数 | 意味 |
|---|---|
describe-load-balancers |
ALB情報取得 |
--load-balancer-arns $ALB_ARN |
対象ALB |
--query 'LoadBalancers[0].DNSName' |
DNS名だけ取得 |
echo "http://$ALB_DNS/"
echo "http://$ALB_DNS/products"
ブラウザ
↓
ALB:80
↓
EC2 API:8080
↓
RDS MySQL:3306
まとめ
VPC
└─ ネットワーク全体
Subnet
├─ Public Subnet: ALB / NAT
├─ Private App Subnet: EC2 API
└─ Private DB Subnet: RDS
Internet Gateway
└─ Public Subnetをインターネットへ接続
NAT Gateway
└─ Private EC2から外部へ出る
Security Group
└─ ALB → EC2 → RDS の通信だけ許可
RDS
└─ 商品データ保存
EC2
└─ API実行
ALB
└─ ブラウザからの入口
エラーが出た際の確認方法
時間をおけばアクセスできる場合もある
SSM Agentの起動待ち
EC2内の amazon-ssm-agent が起動するまで少し時間がかかる
IAMロール反映待ち
EC2に付けた IAM Role / Instance Profile が反映されるまでラグがある
SSMへの登録待ち
EC2が Systems Manager に「管理対象インスタンス」として登録されるまで待ち時間がある
ネットワーク経路の準備待ち
NAT Gateway / VPC Endpoint / Route Table / Security Group の反映に時間がかかることがある
ALBターゲット状態確認
aws elbv2 describe-target-health --target-group-arn $TG_ARN --region $AWS_REGION
EC2が起動しているか
aws ec2 describe-instances --instance-ids $APP_INSTANCE_ID --region $AWS_REGION --query 'Reservations[0].Instances[0].State.Name' --output text
EC2内のAPIが起動しているか
# 初回はSessionmanager入れる
# curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb
# sudo dpkg -i session-manager-plugin.deb
aws ssm start-session --target $APP_INSTANCE_ID --region $AWS_REGION
# ここからはEC2内
# ローカルで動いているか
sudo systemctl status inventory-api
curl http://localhost:8080/health
# 内部から外に出られるか
curl https://www.google.com
環境削除
# Listener削除
LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn $ALB_ARN --region $AWS_REGION --query 'Listeners[0].ListenerArn' --output text)
aws elbv2 delete-listener --listener-arn $LISTENER_ARN --region $AWS_REGION
# ALB削除
aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN --region $AWS_REGION
# Target Group削除
aws elbv2 delete-target-group --target-group-arn $TG_ARN --region $AWS_REGION
# EC2削除
aws ec2 terminate-instances --instance-ids $APP_INSTANCE_1_ID --region $AWS_REGION
aws ec2 terminate-instances --instance-ids $APP_INSTANCE_2_ID --region $AWS_REGION
aws ec2 wait instance-terminated --instance-ids $APP_INSTANCE_1_ID --region $AWS_REGION
aws ec2 wait instance-terminated --instance-ids $APP_INSTANCE_2_ID --region $AWS_REGION
# RDS削除
aws rds delete-db-instance --db-instance-identifier inventory-db --skip-final-snapshot --region $AWS_REGION
aws rds wait db-instance-deleted --db-instance-identifier inventory-db --region $AWS_REGION
# DB Subnet Group削除
aws rds delete-db-subnet-group --db-subnet-group-name inventory-db-subnet-group --region $AWS_REGION
# Security Group削除
aws ec2 delete-security-group --group-id $DB_SG --region $AWS_REGION
aws ec2 delete-security-group --group-id $APP_SG --region $AWS_REGION
aws ec2 delete-security-group --group-id $ALB_SG --region $AWS_REGION
# NAT Gateway削除
aws ec2 delete-nat-gateway --nat-gateway-id $NAT_GW_1_ID --region $AWS_REGION
aws ec2 delete-nat-gateway --nat-gateway-id $NAT_GW_2_ID --region $AWS_REGION
aws ec2 describe-nat-gateways --nat-gateway-ids $NAT_GW_1_ID --region $AWS_REGION
aws ec2 describe-nat-gateways --nat-gateway-ids $NAT_GW_2_ID --region $AWS_REGION
# Elastic IP解放
aws ec2 release-address --allocation-id $EIP_ALLOC_ID --region $AWS_REGION
# Route Table削除
aws ec2 describe-route-tables --filters Name=vpc-id,Values=$VPC_ID --region $AWS_REGION
aws ec2 disassociate-route-table --association-id rtbassoc-xxxxxxxx --region $AWS_REGION
aws ec2 delete-route-table --route-table-id $PUBLIC_RT --region $AWS_REGION
aws ec2 delete-route-table --route-table-id $APP_RT --region $AWS_REGION
# Internet Gateway削除
aws ec2 detach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $AWS_REGION
aws ec2 delete-internet-gateway --internet-gateway-id $IGW_ID --region $AWS_REGION
# Subnet削除
aws ec2 delete-subnet --subnet-id $PUBLIC_SUBNET_1 --region $AWS_REGION
aws ec2 delete-subnet --subnet-id $PUBLIC_SUBNET_2 --region $AWS_REGION
aws ec2 delete-subnet --subnet-id $APP_SUBNET_1 --region $AWS_REGION
aws ec2 delete-subnet --subnet-id $APP_SUBNET_2 --region $AWS_REGION
aws ec2 delete-subnet --subnet-id $DB_SUBNET_1 --region $AWS_REGION
aws ec2 delete-subnet --subnet-id $DB_SUBNET_2 --region $AWS_REGION
# IAM削除
aws iam remove-role-from-instance-profile --instance-profile-name inventory-ec2-profile --role-name inventory-ec2-role
aws iam delete-instance-profile --instance-profile-name inventory-ec2-profile
aws iam detach-role-policy --role-name inventory-ec2-role --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam delete-role --role-name inventory-ec2-role
# VPC削除
aws ec2 delete-vpc --vpc-id $VPC_ID --region $AWS_REGION