0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWSのネットワーク回りのすべてがわかる

0
Posted at

今回作る環境

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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?