- 1. ผลลัพธ์การเรียนรู้
- 2. แนวคิด VPC และองค์ประกอบที่ต้องรู้
- 3. เตรียมระบบก่อนเริ่ม
- 4. ลงมือสร้าง VPC: วิธีรัน Script แบบ End-to-End
- 5. ตรวจสอบทรัพยากรด้วย AWS CLI
- 6. ทดสอบการทำงานจริง
- 7. ทำความเข้าใจ Security Group vs Network ACL
- 8. แนวทาง Troubleshooting
- 9. คำแนะนำสำหรับระบบ Production
- 10. แบบฝึกหัดท้าย Workshop
- 11. ลบทรัพยากรทั้งหมดหลังจบ Workshop
- 12. สรุป
- 13. เอกสารอ้างอิง (AWS Official Documentation)

VPC (Virtual Private Cloud) เป็นเครือข่ายเสมือนที่กำหนดขอบเขต IP, Subnet, Routing และนโยบายการเข้าถึงทรัพยากรภายใน AWS ได้เอง บทความนี้เน้น ลงมือทำจริงด้วย AWS CLI และเรียนรู้เหตุผลของการตั้งค่าเครือข่ายแต่ละชั้น
⚠️ คำเตือนเรื่องค่าใช้จ่าย: Workshop นี้สร้าง NAT Gateway แบบ Public, Elastic IP, EC2 จำนวน 2 เครื่อง และ Public IPv4 ซึ่งอาจมีค่าใช้จ่ายจริง แม้บางบัญชีจะมีสิทธิ์ใช้งานฟรีบางรายการก็ตาม อย่ารันหากไม่มีสิทธิ์สร้างทรัพยากรและไม่พร้อมรับค่าใช้จ่าย ควรตั้ง AWS Budget ก่อนเริ่ม และทำ ขั้นตอนลบทรัพยากร เมื่อจบการทดลอง

ภาพประกอบ: สถาปัตยกรรมและขั้นตอน Workshop AWS VPC
#1. ผลลัพธ์การเรียนรู้
เมื่อจบ Workshop ผู้อ่านจะสามารถ:
- อธิบายความแตกต่างของ VPC, Public Subnet และ Private Subnet
- วางแผนช่วงหมายเลข IP ด้วย CIDR ที่ไม่ทับซ้อนกัน
- สร้าง VPC, Subnet, Internet Gateway, Route Table และ NAT Gateway ด้วย AWS CLI
- ใช้ Security Group จำกัดการเข้าถึง EC2 ตามหลัก Least Privilege
- สร้าง EC2 สองชั้น โดยให้ Frontend ส่ง HTTP Request ไปยัง Backend ที่ไม่มี Public IP
- ตรวจสอบ Routing, Public IP และทดสอบการเชื่อมต่อจริง
- ลบทรัพยากรที่มีค่าใช้จ่าย และวิเคราะห์ปัญหาเครือข่ายเบื้องต้น
#2. แนวคิด VPC และองค์ประกอบที่ต้องรู้
| องค์ประกอบ | บทบาท | ตัวอย่างใน Lab |
|---|---|---|
| VPC | ขอบเขตเครือข่ายเสมือนภายใน AWS Region | 10.20.0.0/16 |
| CIDR | ระบุช่วง IPv4 ของเครือข่าย | 10.20.1.0/24 |
| Public Subnet | Subnet ที่ Route Table มีเส้นทางไป Internet Gateway | 10.20.1.0/24 |
| Private Subnet | Subnet ที่ไม่มีเส้นทางตรงไป Internet Gateway | 10.20.2.0/24 |
| Internet Gateway (IGW) | จุดเชื่อมต่อ VPC กับอินเทอร์เน็ต | Default route ของ Public Subnet |
| NAT Gateway | ช่วยให้ Private EC2 เริ่มต้นการเชื่อมต่อออก Internet ผ่าน IPv4 ได้ | Default route ของ Private Subnet |
| Route Table | ระบุปลายทาง CIDR และ Next Hop | 0.0.0.0/0 → igw-* หรือ nat-* |
| Security Group | Stateful virtual firewall ระดับ ENI/ทรัพยากร | เปิด 8080 ไปยัง Frontend และ 80 จาก Frontend SG ไป Backend |
| EC2 | เครื่องเซิร์ฟเวอร์เสมือน | Frontend Proxy + Private Backend |
ข้อควรเข้าใจ: การอยู่ใน Public Subnet ไม่ได้หมายความว่า EC2 เข้าถึงได้จาก Internet โดยอัตโนมัติ เครื่องยังต้องมี Public IP, Route และ Security Group ที่อนุญาต ส่วน NAT Gateway ไม่ใช่ช่องทางเปิดให้ Internet เข้าถึง Private EC2 โดยตรง
AWS VPC คือทรัพยากรระดับ Region ส่วนแต่ละ Subnet ผูกกับ Availability Zone (AZ) เดียว Workshop นี้จงใจใช้ AZ เดียว เพื่อทำความเข้าใจเส้นทางและลดความซับซ้อน ไม่ใช่สถาปัตยกรรม High Availability สำหรับ Production
#2.1 แผนการจัดสรร IP
| เครือข่าย | CIDR | รวม IP | IP ใช้งานได้ใน AWS |
|---|---|---|---|
| Workshop VPC | 10.20.0.0/16 |
65,536 | แบ่งย่อยเป็น Subnet |
| Public Subnet | 10.20.1.0/24 |
256 | 251 |
| Private Subnet | 10.20.2.0/24 |
256 | 251 |
AWS สำรอง 5 IP addresses ต่อ IPv4 Subnet (4 IP แรกและ IP สุดท้าย) สำหรับ /24 จึงเหลือให้จัดสรร 251 IP และช่วง CIDR ของ Subnet ใน VPC เดียวกันต้องไม่ทับซ้อน
#2.2 สถาปัตยกรรม Workshop
flowchart TD
U[ผู้ใช้งาน Internet] -->|HTTP :8080| IGW[Internet Gateway]
subgraph VPC["AWS VPC 10.20.0.0/16 — หนึ่ง AZ"]
subgraph PUBLIC["Public Subnet 10.20.1.0/24"]
FE["EC2 Frontend / Nginx Proxy :8080"]
NAT["Public NAT Gateway + Elastic IP"]
end
subgraph PRIVATE["Private Subnet 10.20.2.0/24"]
BE["EC2 Backend / Nginx :80 (No Public IP)"]
end
FE -->|HTTP :80 ผ่าน Private IP| BE
BE -->|Outbound IPv4| NAT
end
IGW --> FE
NAT -->|Outbound Internet| IGW
เส้นทาง 2 ลักษณะ:
- Inbound: ผู้ใช้ → Internet Gateway → Public EC2
:8080→ Private EC2:80ตลอดทางมีกฎ Security Group ควบคุม - Outbound ของ Private EC2: Private EC2 → NAT Gateway ใน Public Subnet → Internet Gateway → Internet เช่น สำหรับดาวน์โหลด Nginx ระหว่างติดตั้ง
ข้อสังเกต: Frontend ติดต่อ Backend ด้วย Private IP ผ่าน local route ภายใน VPC ไม่ได้ผ่าน NAT Gateway; NAT ใช้สำหรับการออกไปยังปลายทางภายนอก VPC ใน Lab นี้
#2.3 Route Table ที่คาดหวัง
Public Route Table
| Destination | Target | ความหมาย |
|---|---|---|
10.20.0.0/16 |
local |
สื่อสารภายใน VPC |
0.0.0.0/0 |
igw-... |
ส่ง IPv4 ออก Internet Gateway |
Private Route Table
| Destination | Target | ความหมาย |
|---|---|---|
10.20.0.0/16 |
local |
ติดต่อเครื่องภายใน VPC |
0.0.0.0/0 |
nat-... |
ออก Internet ผ่าน NAT Gateway |
#3. เตรียมระบบก่อนเริ่ม
#3.1 สิ่งที่ต้องมี
- บัญชี AWS พร้อมสิทธิ์สร้าง/ลบทรัพยากร EC2, VPC, NAT, Elastic IP และอ่าน AWS Systems Manager Parameter Store (
ssm:GetParameter) - ติดตั้ง AWS CLI v2 และตั้งค่า Credentials ผ่านวิธีที่เหมาะสม (เช่น AWS SSO หรือ IAM Role; ไม่ควรฝัง Secret Key ลงใน Script)
- เครื่องที่เปิด Bash,
curlและอินเทอร์เน็ต (Linux, macOS, WSL2) - EC2 quota และชนิดเครื่อง
t3.microใช้งานได้ใน Region ที่เลือก - มีสิทธิ์ตรวจสอบการคิดค่าใช้จ่ายและตั้งค่า AWS Budget
เริ่มตรวจสอบ:
aws --version
aws sts get-caller-identity
aws configure list
กำหนด AWS Region ที่จะใช้ (ตัวอย่างใช้ Singapore; เปลี่ยนได้ตามความเหมาะสม):
export AWS_REGION=ap-southeast-1
export AWS_DEFAULT_REGION="$AWS_REGION"
aws ec2 describe-availability-zones \
--filters Name=state,Values=available \
--query 'AvailabilityZones[].ZoneName' --output table
สำหรับ Windows: แนะนำ WSL2 Ubuntu เพราะสคริปต์ใช้รูปแบบ Bash,
chmod, Environment Variables และคำสั่ง Linux อย่านำสคริปต์นี้ไปรันใน Windows CMD โดยตรง
#3.2 ทำไมจึงไม่มี SSH Key ในตัวอย่าง?
ตัวอย่างนี้ใช้ EC2 User Data (cloud-init) ติดตั้งเว็บเซิร์ฟเวอร์โดยอัตโนมัติ และไม่เปิด TCP/22 จาก Internet ช่วยลดการเปิดเผย SSH โดยไม่จำเป็น หากต้องการจัดการระบบจริง ควรใช้ AWS Systems Manager Session Manager โดยกำหนด IAM Instance Profile และเส้นทางไปบริการ SSM (ผ่าน NAT หรือ VPC Endpoints) เพิ่มเติม
#4. ลงมือสร้าง VPC: วิธีรัน Script แบบ End-to-End
บทความจัดเตรียม deploy-vpc-workshop.sh เพื่อเรียก AWS CLI สร้างระบบให้ครบ และ cleanup-vpc-workshop.sh เพื่อเก็บกวาดทรัพยากร ทั้งสองสคริปต์มีโค้ดเต็มในบทความนี้ สามารถคัดลอกไปสร้างไฟล์หรือใช้ไฟล์ .sh ที่ดาวน์โหลดแยกต่างหากได้
ตัวอย่างโครงสร้างไฟล์:
vpc-workshop/
├── deploy-vpc-workshop.sh
├── cleanup-vpc-workshop.sh
└── .vpc-workshop-state # สร้างอัตโนมัติเมื่อเริ่ม deploy
ไฟล์ State บันทึก Resource ID และ Region เพื่อใช้ลบทรัพยากรให้ถูกต้อง อย่าลบทิ้งก่อน Cleanup หาก Deployment ล้มเหลวกลางทาง ให้เก็บ State และตรวจสอบทรัพยากรที่สร้างแล้วก่อนลอง Cleanup (สำหรับความผิดพลาดแบบ Partial Creation อาจต้องลบสิ่งที่ยังไม่บันทึกผ่าน AWS Console)
#4.1 Script สำหรับสร้างระบบ (deploy-vpc-workshop.sh)
ใช้คำสั่งด้านล่างหลังบันทึก Script นี้ลงไฟล์:
chmod +x deploy-vpc-workshop.sh cleanup-vpc-workshop.sh
AWS_REGION=ap-southeast-1 bash deploy-vpc-workshop.sh
ระหว่างทำงาน Script จะสร้างทรัพยากรตามลำดับดังนี้:
- VPC และ Subnet 2 ชุด (ใน AZ เดียว)
- IGW และ Public/Private Route Tables
- Public NAT Gateway พร้อม Elastic IP และ Private default route
- Security Group สองชุด (Frontend: TCP 8080, Backend: TCP 80 จาก Frontend เท่านั้น)
- Private EC2 พร้อม Nginx แล้วจึงสร้าง Public EC2 ที่ตั้งค่า Reverse Proxy ไปหา Private IP
- แสดง Public URL และบันทึก State สำหรับ Cleanup
สำคัญ: IP ที่ Script พิมพ์ออกมาจะแตกต่างกันในแต่ละบัญชี/รอบที่รัน และผลลัพธ์การตั้งเว็บเซิร์ฟเวอร์อาจใช้เวลาอีก 2–5 นาทีหลัง EC2 แสดงสถานะ
running
#!/usr/bin/env bash
# AWS VPC Networking Workshop - creates billable AWS resources.
set -euo pipefail
export AWS_DEFAULT_REGION="${AWS_REGION:-ap-southeast-1}"
export AWS_REGION="$AWS_DEFAULT_REGION"
STATE_FILE="${STATE_FILE:-.vpc-workshop-state}"
if [[ -e "$STATE_FILE" ]]; then
echo "ERROR: $STATE_FILE already exists. Clean up the previous lab first." >&2
exit 1
fi
command -v aws >/dev/null || { echo 'Please install AWS CLI v2.' >&2; exit 1; }
aws sts get-caller-identity --query Account --output text >/dev/null
VPC_ID='' PUB_SUB='' PRIV_SUB='' IGW_ID='' PUB_RTB='' PRIV_RTB=''
PUB_ASSOC='' PRIV_ASSOC='' EIP_ALLOC='' NAT_ID='' PUB_SG='' PRIV_SG=''
PRIV_EC2='' PUB_EC2=''
save_state() {
: > "$STATE_FILE"
for name in AWS_REGION VPC_ID PUB_SUB PRIV_SUB IGW_ID PUB_RTB PRIV_RTB PUB_ASSOC PRIV_ASSOC EIP_ALLOC NAT_ID PUB_SG PRIV_SG PRIV_EC2 PUB_EC2; do
printf '%s=%q\n' "$name" "${!name}" >> "$STATE_FILE"
done
chmod 600 "$STATE_FILE"
}
AZ=$(aws ec2 describe-availability-zones --filters Name=state,Values=available \
--query 'AvailabilityZones[0].ZoneName' --output text)
if [[ "$AZ" == "None" || -z "$AZ" ]]; then echo 'No available AZ.' >&2; exit 1; fi
printf 'Account: %s | Region: %s | AZ: %s\n' \
"$(aws sts get-caller-identity --query Account --output text)" "$AWS_REGION" "$AZ"
# 1. VPC and two non-overlapping subnets.
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.20.0.0/16 --query 'Vpc.VpcId' --output text)
save_state
aws ec2 create-tags --resources "$VPC_ID" --tags Key=Name,Value=Workshop-VPC
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support '{"Value":true}'
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames '{"Value":true}'
PUB_SUB=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.20.1.0/24 \
--availability-zone "$AZ" --query 'Subnet.SubnetId' --output text)
save_state
aws ec2 create-tags --resources "$PUB_SUB" --tags Key=Name,Value=Workshop-Public
aws ec2 modify-subnet-attribute --subnet-id "$PUB_SUB" --map-public-ip-on-launch
PRIV_SUB=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.20.2.0/24 \
--availability-zone "$AZ" --query 'Subnet.SubnetId' --output text)
save_state
aws ec2 create-tags --resources "$PRIV_SUB" --tags Key=Name,Value=Workshop-Private
# 2. Internet Gateway and explicit route tables.
IGW_ID=$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text)
save_state
aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
aws ec2 create-tags --resources "$IGW_ID" --tags Key=Name,Value=Workshop-IGW
PUB_RTB=$(aws ec2 create-route-table --vpc-id "$VPC_ID" \
--query 'RouteTable.RouteTableId' --output text)
save_state
aws ec2 create-tags --resources "$PUB_RTB" --tags Key=Name,Value=Workshop-Public-RT
aws ec2 create-route --route-table-id "$PUB_RTB" --destination-cidr-block 0.0.0.0/0 \
--gateway-id "$IGW_ID" >/dev/null
PUB_ASSOC=$(aws ec2 associate-route-table --route-table-id "$PUB_RTB" \
--subnet-id "$PUB_SUB" --query AssociationId --output text)
save_state
PRIV_RTB=$(aws ec2 create-route-table --vpc-id "$VPC_ID" \
--query 'RouteTable.RouteTableId' --output text)
save_state
aws ec2 create-tags --resources "$PRIV_RTB" --tags Key=Name,Value=Workshop-Private-RT
PRIV_ASSOC=$(aws ec2 associate-route-table --route-table-id "$PRIV_RTB" \
--subnet-id "$PRIV_SUB" --query AssociationId --output text)
save_state
# 3. Public NAT gateway for private subnet egress (billable).
EIP_ALLOC=$(aws ec2 allocate-address --domain vpc --query AllocationId --output text)
save_state
NAT_ID=$(aws ec2 create-nat-gateway --subnet-id "$PUB_SUB" \
--allocation-id "$EIP_ALLOC" --connectivity-type public \
--query 'NatGateway.NatGatewayId' --output text)
save_state
echo "Waiting for NAT Gateway $NAT_ID to be available ..."
aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_ID"
aws ec2 create-route --route-table-id "$PRIV_RTB" --destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id "$NAT_ID" >/dev/null
# 4. Security Groups. The public frontend exposes 8080; backend allows port 80 only from frontend SG.
PUB_SG=$(aws ec2 create-security-group --vpc-id "$VPC_ID" \
--group-name "workshop-public-$(date +%s)" --description 'Workshop public frontend' \
--query GroupId --output text)
save_state
aws ec2 authorize-security-group-ingress --group-id "$PUB_SG" \
--protocol tcp --port 8080 --cidr 0.0.0.0/0
PRIV_SG=$(aws ec2 create-security-group --vpc-id "$VPC_ID" \
--group-name "workshop-private-$(date +%s)" --description 'Workshop private backend' \
--query GroupId --output text)
save_state
aws ec2 authorize-security-group-ingress --group-id "$PRIV_SG" \
--protocol tcp --port 80 --source-group "$PUB_SG"
# 5. Amazon Linux 2023 and a private backend web server.
AMI_ID=$(aws ssm get-parameter \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameter.Value' --output text)
cat > private-userdata.sh <<'USERDATA'
#!/bin/bash
set -euxo pipefail
dnf install -y nginx
cat > /usr/share/nginx/html/index.html <<'HTML'
<!doctype html><html><head><meta charset="utf-8"><title>Private VPC backend</title></head>
<body><h1>VPC workshop: Hello from PRIVATE subnet</h1>
<p>Request reached a private EC2 through the public reverse proxy.</p></body></html>
HTML
systemctl enable --now nginx
USERDATA
PRIV_EC2=$(aws ec2 run-instances --image-id "$AMI_ID" --instance-type t3.micro \
--subnet-id "$PRIV_SUB" --security-group-ids "$PRIV_SG" \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--user-data file://private-userdata.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=Workshop-Backend}]' \
--query 'Instances[0].InstanceId' --output text)
save_state
aws ec2 wait instance-running --instance-ids "$PRIV_EC2"
PRIVATE_IP=$(aws ec2 describe-instances --instance-ids "$PRIV_EC2" \
--query 'Reservations[0].Instances[0].PrivateIpAddress' --output text)
# 6. Public frontend acts as an Nginx reverse proxy to the private EC2.
# Using port 8080 prevents conflict with default nginx server on port 80.
cat > public-userdata.sh <<USERDATA
#!/bin/bash
set -euxo pipefail
dnf install -y nginx
cat > /etc/nginx/conf.d/workshop-proxy.conf <<'NGINX'
server {
listen 8080;
server_name _;
location / {
proxy_pass http://$PRIVATE_IP:80;
proxy_connect_timeout 5s;
proxy_read_timeout 20s;
}
}
NGINX
nginx -t
systemctl enable --now nginx
USERDATA
PUB_EC2=$(aws ec2 run-instances --image-id "$AMI_ID" --instance-type t3.micro \
--subnet-id "$PUB_SUB" --security-group-ids "$PUB_SG" \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--user-data file://public-userdata.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=Workshop-Frontend}]' \
--query 'Instances[0].InstanceId' --output text)
save_state
aws ec2 wait instance-running --instance-ids "$PUB_EC2"
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$PUB_EC2" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
printf '\nWorkshop resources created. Private IP: %s\nFrontend URL (HTTP only): http://%s:8080/\n' \
"$PRIVATE_IP" "$PUBLIC_IP"
printf 'Cloud-init may need 2-5 minutes before the web server responds.\n'
printf 'Use curl -i http://%s:8080/ to test.\n' "$PUBLIC_IP"
printf 'IMPORTANT: clean up with bash cleanup-vpc-workshop.sh after the lab.\n'
#4.2 อธิบายคำสั่งที่สำคัญ
สร้าง VPC: aws ec2 create-vpc --cidr-block 10.20.0.0/16 กำหนดช่วง IP ส่วน modify-vpc-attribute เปิด DNS Support และ DNS Hostnames
สร้าง Subnet: create-subnet กำหนด VPC, CIDR และ AZ; modify-subnet-attribute --map-public-ip-on-launch ตั้งให้ EC2 ที่สร้างตามค่าปริยายใน Public Subnet ได้ Public IPv4
Routing: create-route ให้ Default Route ของ Public Table ชี้ IGW และ Private Table ชี้ NAT Gateway โดย CIDR 10.20.0.0/16 -> local ถูกจัดเตรียมโดย AWS อยู่แล้ว
Security Group: Frontend เปิด TCP/8080 ให้ทดลองจากภายนอก ส่วน Backend เปิด TCP/80 โดยอ้าง --source-group เป็น Frontend SG จึงไม่ต้องระบุหรือคาดเดาช่วง IP ที่จะเรียก Backend
Reverse Proxy: Nginx ของ Public EC2 รับ HTTP/8080 แล้วส่งต่อไปยัง Private EC2/80 ผ่าน Private IP เพื่อให้เห็นการสื่อสารภายใน VPC
#5. ตรวจสอบทรัพยากรด้วย AWS CLI
หลัง Script สำเร็จ ให้ทำงาน จากไดเรกทอรีเดิม:
source .vpc-workshop-state
export AWS_DEFAULT_REGION="$AWS_REGION"
export AWS_REGION
# ดูข้อมูล VPC
aws ec2 describe-vpcs --vpc-ids "$VPC_ID" \
--query 'Vpcs[].{VpcId:VpcId,CIDR:CidrBlock,State:State}' --output table
# ดู Subnet, CIDR, AZ และการแจก Public IP
aws ec2 describe-subnets --subnet-ids "$PUB_SUB" "$PRIV_SUB" \
--query 'Subnets[].{Subnet:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone,AutoPublicIP:MapPublicIpOnLaunch}' \
--output table
# ตรวจสอบ Route Tables
aws ec2 describe-route-tables --route-table-ids "$PUB_RTB" "$PRIV_RTB" \
--query 'RouteTables[].{RouteTable:RouteTableId,Routes:Routes[].{To:DestinationCidrBlock,IGW:GatewayId,NAT:NatGatewayId}}' \
--output json
# ตรวจสอบ NAT Gateway
aws ec2 describe-nat-gateways --nat-gateway-ids "$NAT_ID" \
--query 'NatGateways[].{Id:NatGatewayId,State:State,Subnet:SubnetId}' --output table
# ดู EC2 ว่าตัวใดมี Public IP
aws ec2 describe-instances --instance-ids "$PUB_EC2" "$PRIV_EC2" \
--query 'Reservations[].Instances[].{Id:InstanceId,Subnet:SubnetId,PrivateIP:PrivateIpAddress,PublicIP:PublicIpAddress,State:State.Name}' \
--output table
ผลที่คาดหวัง: EC2 Frontend มี Public IPv4 ส่วน EC2 Backend ไม่มี Public IPv4; Public RT ชี้ IGW และ Private RT ชี้ NAT; NAT Gateway เป็น available
#6. ทดสอบการทำงานจริง
#6.1 ทดสอบจากเครื่องผู้เรียน
source .vpc-workshop-state
export AWS_DEFAULT_REGION="$AWS_REGION"
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$PUB_EC2" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
curl -i "http://$PUBLIC_IP:8080/"
ผลที่คาดหวังใน Response Body:
<h1>VPC workshop: Hello from PRIVATE subnet</h1>
ข้อความนี้อยู่ในไฟล์เว็บบน Private EC2 แต่เข้าถึงผ่าน Public EC2 Reverse Proxy จึงยืนยันเบื้องต้นว่า Public-to-Private communication ทำงานได้ ไม่ได้หมายความว่า Private EC2 เปิดรับ Internet โดยตรง
หากเจอ Connection refused, Timeout หรือ 502 Bad Gateway ให้รอการติดตั้งผ่าน cloud-init ก่อน 2–5 นาที แล้วทดสอบซ้ำ ตรวจสอบ Route Table, Security Group, NAT status และการทำงานของ User Data (ดู Troubleshooting)
#6.2 ตรวจสอบ Security Group
aws ec2 describe-security-groups --group-ids "$PUB_SG" "$PRIV_SG" \
--query 'SecurityGroups[].{Group:GroupId,Name:GroupName,Ingress:IpPermissions}' \
--output json
ตรวจว่า Public SG ไม่มี SSH/22 และ Private SG เปิด TCP/80 โดย Source เป็น Public SG ไม่ใช่ 0.0.0.0/0
#6.3 ตรวจสอบ Outbound NAT
Private EC2 ติดตั้ง nginx ผ่าน dnf หลัง NAT Gateway พร้อมทำงาน โดยไม่กำหนด Public IP ให้เครื่อง ซึ่งเป็นการทดสอบ Outbound NAT โดยอ้อม หากต้องการทดสอบ curl จาก Private EC2 โดยตรง ให้เพิ่ม IAM Role สำหรับ SSM และเชื่อมต่อผ่าน Session Manager จากนั้นตรวจสอบ curl -I https://example.com ภายในเครื่อง ไม่ควรเปิด Public SSH เพียงเพื่อทดลอง
#7. ทำความเข้าใจ Security Group vs Network ACL
| ประเด็น | Security Group | Network ACL |
|---|---|---|
| ระดับควบคุม | ENI/ทรัพยากร | Subnet |
| Stateful | ใช่ | ไม่ใช่ (Stateless) |
| รูปแบบกฎ | Allow rules | Allow และ Deny rules |
| เหมาะกับ | ควบคุมบริการที่เชื่อมต่อ EC2 | นโยบายกรองการรับส่งข้อมูลระดับ Subnet |
สำหรับ Workshop นี้ใช้ Default Network ACL ของ VPC ที่อนุญาตทราฟฟิกตามค่าปริยาย แล้วควบคุมการเข้าถึงหลักด้วย Security Group เท่านั้น หากเพิ่ม Custom Network ACL ต้องจัดการ ephemeral response ports ด้วย
#8. แนวทาง Troubleshooting
| อาการ | สาเหตุที่ควรตรวจสอบ | วิธีแก้เบื้องต้น |
|---|---|---|
| Frontend ไม่มี Public IP | Public Subnet ไม่ได้เปิด auto-assign หรือ Public IPv4 ยังไม่ถูกกำหนด | ตรวจ MapPublicIpOnLaunch และ EC2 networking |
เข้าเว็บ :8080 ไม่ได้ |
Public RT/IGW, SG หรือ Nginx ไม่พร้อม | ตรวจ Public IPv4, Route 0.0.0.0/0 -> igw-*, TCP/8080 และ EC2 User Data |
ได้ 502 Bad Gateway |
Private Nginx ยังไม่เริ่ม, Private IP ผิด หรือ SG Backend ไม่อนุญาต | รอ cloud-init, ตรวจ Backend SG จาก Frontend SG, ดูไฟล์ Proxy |
| Private EC2 ดาวน์โหลด Package ไม่ได้ | NAT ไม่ available, Private Route ไม่ชี้ NAT |
ตรวจ NAT, EIP, IGW และ Private Route Table |
UnauthorizedOperation |
IAM สิทธิ์ไม่เพียงพอ | ตรวจ EC2/VPC/SSM permissions และ Permissions Boundary |
InstanceLimitExceeded หรือสร้างไม่ได้ |
Quota หรือ Instance Type ใน AZ ไม่พร้อม | ตรวจ Service Quotas และเลือก Instance Type/AZ ที่รองรับ |
| ลบ VPC ไม่ได้ | ยังมี ENI, NAT, IGW หรือทรัพยากรอื่นอ้างอิง | ลบทรัพยากรที่พึ่งพาตามลำดับ ตรวจ EC2 Network Interfaces |
สามารถอ่าน EC2 console output เพื่อช่วยวิเคราะห์การ Boot:
aws ec2 get-console-output --instance-id "$PRIV_EC2" --latest --output text
aws ec2 get-console-output --instance-id "$PUB_EC2" --latest --output text
การอ่าน cloud-init-output.log บนเครื่องโดยตรงต้องมีช่องทางบริหารเครื่อง เช่น SSM Session Manager ที่ตั้งค่าเหมาะสม
#9. คำแนะนำสำหรับระบบ Production
- High Availability: กระจาย Public/Private Subnet อย่างน้อย 2 AZ ตามข้อกำหนดของบริการ; เลือกรูปแบบ NAT Gateway ให้เหมาะกับความทนทานและค่าใช้จ่าย (AWS มีรูปแบบ Regional NAT เพิ่มเติมในบริการรุ่นปัจจุบัน)
- Load Balancer: ในระบบจริงนิยมใช้ Application Load Balancer (ALB) แทนการเปิด Public EC2 Reverse Proxy โดยตรง
- Private Compute: วาง Application/Worker/Database ใน Private Subnet และกำหนด SG ให้เปิดเฉพาะ Port ที่ต้องใช้
- TLS: ให้บริการผ่าน HTTPS/443 และ Certificate ที่จัดการอย่างถูกต้อง; HTTP/8080 ในบทความใช้เพื่อสาธิตเท่านั้น
- Private Connectivity: พิจารณา VPC Endpoint เช่น S3 Gateway Endpoint เพื่อลดเส้นทางผ่าน NAT เมื่อเหมาะสม
- Observability: ใช้ VPC Flow Logs, CloudWatch, Reachability Analyzer และบันทึกการเปลี่ยนแปลงระบบ
- Infrastructure as Code: สำหรับทีมที่ใช้งานจริง ใช้ Terraform, AWS CloudFormation หรือ AWS CDK แทนสคริปต์ครั้งเดียว
- Cost Management: ตั้ง AWS Budgets, ติด Tag และประเมิน NAT, EC2, Public IPv4 และ Data Transfer ตาม Region ที่ใช้จริง
#10. แบบฝึกหัดท้าย Workshop
โจทย์ที่ 1 — วิเคราะห์ Routing: หากลบ Private default route 0.0.0.0/0 -> nat-* จะเกิดอะไรขึ้นกับการดาวน์โหลด Package และการสื่อสารจาก Backend ไป Frontend ภายใน VPC? เฉลยแนวคิด: ออก Internet ผ่าน IPv4 ไม่ได้ แต่ยังมี local route สื่อสารภายใน VPC ได้หาก Security Group/ACL อนุญาต
โจทย์ที่ 2 — วิเคราะห์ Security: เปลี่ยน Backend SG ให้รับ TCP/80 จาก 0.0.0.0/0 แล้ว Backend จะเข้าถึงจาก Internet ได้ทันทีหรือไม่? เฉลยแนวคิด: ไม่ได้โดยอัตโนมัติ เพราะไม่มี Public IP และ Private Route ไม่ได้ชี้ IGW โดยตรง แต่เป็นนโยบาย SG ที่กว้างเกินจำเป็นและควรหลีกเลี่ยง
โจทย์ที่ 3 — ออกแบบ Production: แยก Frontend, Application และ Database เป็น Subnet ต่างกัน วาง ALB ใน Public Subnet และออกแบบ Availability Zones จำนวน 2 AZ พร้อมแสดง Route Tables และ SG ที่เหมาะสม
โจทย์ที่ 4 — ความมั่นคงปลอดภัย: เพิ่ม TLS บน ALB, ใช้ Session Manager สำหรับเข้าถึง Private Instance และออกแบบ Database Security Group ที่ยอมรับเฉพาะ Application Security Group
#11. ลบทรัพยากรทั้งหมดหลังจบ Workshop
🚨 ขั้นตอนนี้สำคัญมาก หากลืมลบ NAT Gateway, Elastic IP หรือ EC2 อาจเกิดค่าใช้จ่ายต่อเนื่อง Script นี้จะลบทรัพยากรที่สร้างโดย Deployment Script ผ่าน Resource IDs ที่เก็บใน
.vpc-workshop-stateเท่านั้น จึงไม่ควรแก้หรือใช้ไฟล์ State ของระบบอื่น
รัน:
bash cleanup-vpc-workshop.sh
Script จะให้พิมพ์ DELETE เพื่อยืนยัน โดยดำเนินการตามลำดับ: Terminate EC2 → ลบ NAT Gateway และรอจนเสร็จ → Release Elastic IP → ลบ Route Table Associations/Route Tables → ลบ Security Groups → ลบ Subnets → ถอด/ลบ IGW → ลบ VPC
#!/usr/bin/env bash
# Clean up resources generated by deploy-vpc-workshop.sh ONLY.
set -euo pipefail
STATE_FILE="${STATE_FILE:-.vpc-workshop-state}"
if [[ ! -f "$STATE_FILE" ]]; then
echo "ERROR: Cannot locate $STATE_FILE. Run from the deployment directory." >&2
exit 1
fi
# The state file is locally created by the deployment script. Do not load untrusted state files.
# shellcheck disable=SC1090
source "$STATE_FILE"
export AWS_DEFAULT_REGION="$AWS_REGION"
export AWS_REGION
printf 'About to DELETE lab resources in AWS region %s (VPC %s).\n' "$AWS_REGION" "$VPC_ID"
if [[ "${CONFIRM_DELETE:-}" != "yes" ]]; then
read -r -p "Type DELETE to continue: " answer
[[ "$answer" == 'DELETE' ]] || { echo 'Aborted.'; exit 1; }
fi
# EC2 instances and their network interfaces must disappear first.
INSTANCES=()
[[ -n "${PRIV_EC2:-}" ]] && INSTANCES+=("$PRIV_EC2")
[[ -n "${PUB_EC2:-}" ]] && INSTANCES+=("$PUB_EC2")
if (( ${#INSTANCES[@]} > 0 )); then
aws ec2 terminate-instances --instance-ids "${INSTANCES[@]}" >/dev/null
aws ec2 wait instance-terminated --instance-ids "${INSTANCES[@]}"
fi
# NAT deletion is asynchronous. EIP remains allocated until released explicitly.
if [[ -n "${NAT_ID:-}" ]]; then
aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_ID" >/dev/null
aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$NAT_ID"
fi
if [[ -n "${EIP_ALLOC:-}" ]]; then
aws ec2 release-address --allocation-id "$EIP_ALLOC"
fi
# Detach explicit subnet route table associations before deletion.
[[ -n "${PUB_ASSOC:-}" ]] && aws ec2 disassociate-route-table --association-id "$PUB_ASSOC"
[[ -n "${PRIV_ASSOC:-}" ]] && aws ec2 disassociate-route-table --association-id "$PRIV_ASSOC"
[[ -n "${PUB_RTB:-}" ]] && aws ec2 delete-route-table --route-table-id "$PUB_RTB"
[[ -n "${PRIV_RTB:-}" ]] && aws ec2 delete-route-table --route-table-id "$PRIV_RTB"
# Delete the private SG first because its ingress rule references the public SG.
[[ -n "${PRIV_SG:-}" ]] && aws ec2 delete-security-group --group-id "$PRIV_SG"
[[ -n "${PUB_SG:-}" ]] && aws ec2 delete-security-group --group-id "$PUB_SG"
[[ -n "${PUB_SUB:-}" ]] && aws ec2 delete-subnet --subnet-id "$PUB_SUB"
[[ -n "${PRIV_SUB:-}" ]] && aws ec2 delete-subnet --subnet-id "$PRIV_SUB"
if [[ -n "${IGW_ID:-}" ]]; then
[[ -n "${VPC_ID:-}" ]] && aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID"
fi
[[ -n "${VPC_ID:-}" ]] && aws ec2 delete-vpc --vpc-id "$VPC_ID"
rm -f "$STATE_FILE" private-userdata.sh public-userdata.sh
echo 'Cleanup complete. Verify the AWS Console and billing dashboard.'
กรณี Cleanup ติด Dependency: อย่าลบไฟล์ State ให้แก้ความผิดพลาดก่อนแล้วพิจารณารันซ้ำเฉพาะขั้นที่ยังค้าง เนื่องจาก Script เวอร์ชันนี้เป็นตัวอย่างการเรียนรู้และ ไม่ได้ออกแบบให้ Re-run หลังลบทรัพยากรไปแล้วบางส่วนได้โดยอัตโนมัติ สามารถใช้ AWS Console เพื่อตรวจสอบทรัพยากรตกค้างตาม VPC ID, Subnet ID และ Region และลบด้วยตนเอง
สุดท้าย ตรวจสอบจาก AWS Console ว่า VPC, NAT, EC2, Elastic IP และ Network Interfaces ของ Workshop ไม่เหลืออยู่ และตรวจสอบ Billing/Cost Explorer (ข้อมูลค่าใช้จ่ายอาจแสดงล่าช้า)
#12. สรุป
VPC คือโครงสร้างพื้นฐานเครือข่ายของระบบ AWS ที่ช่วยกำหนดการแบ่งแยกเครือข่าย เส้นทางและขอบเขตการเข้าถึงได้อย่างชัดเจน ใน Workshop นี้เราได้ลงมือสร้าง VPC สอง Subnet ติดตั้ง NAT/IGW กำหนด Routing และ Firewall แล้วทดลองให้ผู้ใช้เข้าถึง Private Backend ผ่าน Public Reverse Proxy โดย Private EC2 ไม่ต้องมี Public IP พร้อมฝึกการเก็บกวาดทรัพยากรหลังทดลอง
แนวทางต่อยอด ได้แก่ สร้าง VPC แบบ Multi-AZ, ใช้ ALB + Auto Scaling, เพิ่ม RDS ใน Database Subnet, ใช้ VPC Endpoint และเขียน Terraform Module เพื่อ Deploy/Destroy อย่างเป็นระบบ
#13. เอกสารอ้างอิง (AWS Official Documentation)
- Create a VPC — Amazon VPC User Guide
- Create a VPC with private subnets and NAT gateways using AWS CLI
- VPC with servers in private subnets and NAT
- Subnet CIDR blocks
- Subnet route tables
- NAT Gateway pricing
- AWS VPC pricing
- AWS CLI: Create NAT Gateway