เริ่มต้นลองใช้ AWS ECS ด้วย Terraform
เนื่องด้วยว่าปกติได้ใช้ aws ecs อยู่แล้ว แต่ไม่เคยเริ่มต้น provisioning resource ด้วย terraform เลย คราวนี้เลยลองใช้ซะหน่อยครับ
บทความภาคที่แล้ว และที่เกี่ยวข้อง
AWS ECS (Amazon Elastic Container Service) คือ service นึงของทาง aws ที่สามารถให้เรา manage application ของเราด้วย concept container base
โดยการที่เราจะสร้างเจ้าตัว ECS อาจมี service ที่เกี่ยวข้องหลายตัว คร่าว ๆ สำหรับ workshop นี้จะเป็นไปตาม diagram นี้ครับ

api service ของเราจะเป็น container ที่ ECS, pull images จาก ECR โดย api ที่เราทำจะต้องเรียกผ่าน load balancer อีกทีครับ เมื่อออกแบบระบบเบื้องต้นแล้ว ก็เริ่มกันเลยยย
Let’s start !!
ก่อนจะเริ่ม ตรวจสอบให้แน่ใจว่าได้ ติดตั้ง terraform และมี account aws เรียบร้อยแล้ว ส่วนขั้นตอนของการกำหนด terraform version, cloud provider และผูก aws account กับ terraform ผมจะขอข้ามไปนะครับ เพราะมีจากครั้งก่อน ๆ แล้ว
1. สร้าง VPC และ Internet Gateway
กำหนดค่า ip ที่ต้องการก่อน ผมใส่ค่าพวกนี้ที่ var.tf
เผื่ออยากเปลี่ยนค่าทีหลัง จะได้หาง่าย ๆ และเนื่องจากเป็น variable ทำให้เราสามารถเปลี่ยนค่าได้ผ่าน command line อีกด้วย แต่ใครจะกำหนดค่าไปเลยที่ main.tf
ไปเลยก็ไม่ว่ากัน
# var.tf
..
..
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
# main.tf
..
..
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = "terraform vpc"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "terraform igw"
}
}
2. สร้าง public, private subnet และ route table
# var.tf
..
..
variable "public_subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "azs" {
type = list(string)
default = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
}
# main.tf
..
..
resource "aws_subnet" "public_subnets" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.vpc.id
cidr_block = element(var.public_subnet_cidrs, count.index)
availability_zone = element(var.azs, count.index)
tags = {
Name = "terraform public subnet ${count.index + 1}"
}
}
resource "aws_subnet" "private_subnets" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.vpc.id
cidr_block = element(var.private_subnet_cidrs, count.index)
availability_zone = element(var.azs, count.index)
tags = {
Name = "terraform private subnet ${count.index + 1}"
}
}
resource "aws_route_table" "alb_route_table" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "terraform alb route table"
}
}
resource "aws_route_table" "api_route_table" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "terraform api route table"
}
}
resource "aws_route_table_association" "public_subnet_asso" {
count = length(var.public_subnet_cidrs)
subnet_id = element(aws_subnet.public_subnets[*].id, count.index)
route_table_id = aws_route_table.alb_route_table.id
}
resource "aws_route_table_association" "private_subnet_asso" {
count = length(var.private_subnet_cidrs)
subnet_id = element(aws_subnet.private_subnets[*].id, count.index)
route_table_id = aws_route_table.api_route_table.id
}
เราสร้าง subnet สองประเภทที่เป็น public และ private ส่วนของ public จะสามารถเข้าถึงจาก internet ภายนอก VPC ได้ (เราจะเอา alb ไว้ที่ส่วนนี้) ส่วน private จะไม่สามารถเข้าถึงได้ ต้องเข้าถึงภายใน VPC (เราจะเอา service api เราไว้ที่ส่วนนี้)
ทั้ง public และ private subnet ผมจะให้มีอย่างละ 2 ตัว ทำการผูก route table กับ subnets ให้เรียบร้อยด้วย aws_route_table_association
3. สร้าง ECR และ push images เข้า registry
# main.tf
..
..
resource "aws_ecr_repository" "api_ecr" {
name = "tf-api"
force_delete = true
tags = {
Name = "terraform api ecr"
}
}
เมื่อสร้าง registry แล้ว เอา docker image ที่ต้องการ push เข้า registry ที่เราสร้างเมื่อกี้ โดยวิธี push image สามารถดูได้จากลิ้งนี้
4. สร้าง ECS Cluster

ใน ECS นั้นมีส่วนประกอบต่าง ๆ หลายอย่างดังรูป รายละเอียดจะขอข้ามไปนะครับ เราจะไปโฟกัสกับการสร้าง ECS กัน
# main.tf
..
..
resource "aws_ecs_cluster" "cluster" {
name = "tf_cluster"
tags = {
Name = "terraform cluster"
}
}
resource "aws_iam_role" "ecsTaskExecutionRole" {
name = "tf-ecsTaskExecutionRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" {
role = aws_iam_role.ecsTaskExecutionRole.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
5. สร้าง task definition และ service ใน ECS Cluster
# main.tf
..
..
resource "aws_ecs_task_definition" "api_task" {
family = "api_task"
container_definitions = <<DEFINITION
[
{
"name": "api_task",
"image": "${aws_ecr_repository.api_ecr.repository_url}",
"essential": true,
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000
}
],
"memory": 512,
"cpu": 256
}
]
DEFINITION
requires_compatibilities = ["FARGATE"] # Stating that we are using ECS Fargate
network_mode = "awsvpc" # Using awsvpc as our network mode as this is required for Fargate
memory = 512 # Specifying the memory our container requires
cpu = 256 # Specifying the CPU our container requires
execution_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
}
resource "aws_ecs_service" "api_service" {
name = "api_service"
cluster = aws_ecs_cluster.cluster.id
task_definition = aws_ecs_task_definition.api_task.arn
launch_type = "FARGATE"
desired_count = 1 # number of task
network_configuration {
subnets = [for subnet in aws_subnet.private_subnets : subnet.id]
assign_public_ip = true
security_groups = [aws_security_group.api_security_group.id]
}
}
resource "aws_security_group" "api_security_group" {
name = "terraform-api-sg"
vpc_id = aws_vpc.vpc.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
กำหนด port และขนาด cpu, memory ใน task definition ให้เรียบร้อย ส่วน ECS service กำหนดจำนวนของ task และสร้าง security group มาผูกรอไว้ เมื่อเรามี ALB ค่อยมาเพิ่ม ingress ให้กับ api security group
เมื่อรัน terraform apply
เพื่อสร้าง resource แล้ว ล๊อกอิน aws console มาที่ส่วนของ task ใน ECS จะขึ้น error ว่า pull image จาก ECR ไม่ได้ ให้เพิ่ม NAT Gateway เพื่อให้ ECR มองเห็น ECS ที่อยู่ใน private subnet (หรืออาจใช้ vpc endpoint ก็ได้ แต่จะยุ่งยากหน่อย)
6. สร้าง NAT Gateway
NAT Gateway จะเหมือนเป็นช่องทางระหว่าง public subnet และ private subnet ให้สามารถคุยกันได้ ให้เราเพิ่มส่วนของ NAT เข้าไปใน main.tf
# main.tf
..
..
resource "aws_eip" "nat_eip" {
vpc = true
depends_on = [aws_internet_gateway.igw]
}
resource "aws_nat_gateway" "nat" {
subnet_id = element(aws_subnet.public_subnets.*.id, 0)
allocation_id = aws_eip.nat_eip.id
depends_on = [aws_internet_gateway.igw]
tags = {
Name = "terraform nat"
}
}
และปรับ api_route_table
ที่มีอยู่ก่อนแล้ว
# main.tf
resource "aws_route_table" "api_route_table" {
vpc_id = aws_vpc.vpc.id
# เพิ่มส่วนนี้
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
..
..
}
รันอีกทีแล้วเข้ามาเช็คที่ console ทีนี้ ECS ใน private subnet ควรจะ pull image จาก ECR ได้แล้ว ถ้า status ของ service เป็น active และ task running ขึ้นปกติ ก็คือโอเคดีแล้ว

7. สร้าง ALB (Application Load Balancer)
สำหรับ ALB เราจะไว้ที่ public subnet เพื่อให้ใครก็ได้สามารถเข้าถึงได้ และตั้งค่า security group เป็นอนุญาตให้ทุก traffic เรียกได้
# main.tf
..
..
resource "aws_lb" "alb" {
name = "terraform-alb"
load_balancer_type = "application"
subnets = [for subnet in aws_subnet.public_subnets : subnet.id]
security_groups = [aws_security_group.alb_security_group.id]
tags = {
Name = "terraform alb"
}
}
resource "aws_security_group" "alb_security_group" {
name = "terraform-alb-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Allow traffic in from all sources
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb_target_group" "alb_target_group" {
name = "terraform-alb-target-group"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = aws_vpc.vpc.id
}
resource "aws_lb_listener" "alb_listener" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.alb_target_group.arn
}
}
จากนั้นปรับ api_service
ให้ load balancer ผูกกับ ALB ตัวของเรา
# main.tf
resource "aws_ecs_service" "api_service" {
name = "api_service"
..
..
# เพิ่มส่วนนี้
load_balancer {
target_group_arn = aws_lb_target_group.alb_target_group.arn
container_name = aws_ecs_task_definition.api_task.family
container_port = 3000
}
..
..
}
และเพิ่ม ingress ที่ api_security_group
อนุญาตให้ load balancer สามารถเรียก service ใน ECS ได้
# main.tf
resource "aws_security_group" "api_security_group" {
name = "terraform-api-sg"
vpc_id = aws_vpc.vpc.id
# เพิ่มส่วนนี้ allow speific load balancer
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.alb_security_group.id]
}
..
..
}
8. ตรวจสอบ และเรียกผ่าน ALB
รัน terraform เพื่อสร้าง resource ทั้งหมด จากนั้นเข้าไป AWS Console ที่ load balancers ใน service EC2 จะเจอ load balancer ของเรา ให้นำ dns ที่ได้ ไปแปะบน browser ควรจะสามารถเรียก api ของเราได้แล้ว


เรียบร้อยครับ 🎉 🎉 🎉 สำหรับ workshop นี้โฟกัสที่การสร้าง resource ด้วย terraform ล้วน ๆ เลยครับ ทำให้ข้ามที่มาไปหลายอย่าง ครั้งหน้าจะเอา terraform ไปทำอะไร ติดตามกันครับบบบ