Terraform มันคืออาวุธที่ใช้ในการทำ infrastructure as code เพื่อจัดการการวาง infrastructure ต่างๆ เราสามารถสร้างเครื่อง จัดการเครื่อง หรือลบเครื่องได้โดยไม่ต้องไปกดๆ ใน cloud console เลย และที่สำคัญคือมันทำ immutable infrastructure ลองดูวีดีโอด้านล่างนี้ 👇 เค้าอธิบาย mutable กับ immutable infrastructure ว่าต่างกันอย่างไร
ส่วน blue-green deployment มันคืออะไร? ถ้าใครไม่รู้จัก ผมอยากให้ตามไปอ่านโพสต์ BlueGreenDeployment ของ Martin Fowler กันก่อนครับ 😆 ด้านล่างนี่เป็นรูป blue-green deployment ที่ Martin เค้าวาดไว้
มันก็ประมาณว่า เวลาที่เรา deploy server เราจะมอง server ตัวเก่าเป็น blue และตัวที่กำลังจะ deploy เป็น green โดยที่ตัว infrastructure ของเราจะรอให้ green ทำงานได้ก่อน รับ request ได้ก่อน แล้วค่อยฆ่าตัว blue ทิ้ง (ซึ่งแน่นอนครับ ต้องอาศัย load balancer สักตัวหนึ่งมาช่วย)
ซึ่งตัว Terraform ที่เกริ่นมาข้างต้นนี่แหละ สามารถเอามาทำ blue-green deployment ได้นะ โดยหลักการที่เราจะทำเนี่ย เนื่องจาก Terraform ออกแบบมาเพื่อ immutable infrastructure ซึ่งการที่เราจะสร้าง infrastructure stack ทั้งหมด ตั้งแต่ DNS ยัน database เลยเนี่ย มันจะทำให้เราต้องมาทำ load balancer ครอบ stack ของเราอีกรอบ (ย้อนกลับไปดูรูปด้านบนครับ ตัว load balancer ก็คือ Router ในรูปนั่นเอง)
ดังนั้นเราจะสร้างโครงขึ้นมาก่อน แล้วส่วนที่เราต้องการให้มีการเปลี่ยนแปลงอยู่ตลอดเวลา เช่น เครื่อง server เราก็จะแยกออกมาจากโครงนั้นเอามาทำ blue-green deployment ดูโค้ดกันเลย (ในทีนี้ใช้ AWS เป็น cloud provider นะ)
เบื้องต้นผมจะทำไว้ 2 โฟลเดอร์มีหน้าตาประมาณนี้
➜ terraform tree -L 2
.
├── app
│ ├── bootstrap.sh
│ └── main.tf
└── base
├── bootstrap.sh
└── main.tf
2 directories, 4 files
โฟลเดอร์ base จะเป็น folder ที่ผมจะเอาไว้สร้างโครงครับ ซึ่งตรงนี้จะเป็น Terraform ก็ได้ หรือจะสร้างผ่าน CLI ก็ได้ หรือจะไป manual กดๆ ในหน้า AWS console เลยก็ได้ครับ ส่วนโฟลเดอร์ app ผมก็จะมีแค่การสร้าง EC2 instance แล้วก็การเอา instance นั้นไป register เข้ากับ load balancer (ในที่นี้ใช้ ALB)
ส่วนไฟล์ bootstrap.sh
ทั้ง 2 ไฟล์ มีหน้าตาเหมือนกันครับ จะเป็นสคริปสำหรับติดตั้งอะไรก็ได้ตอนที่เครื่อง EC2 ถูก provision ขึ้นมา ในที่นี้ผมจะเอาไว้ลง Docker เฉยๆ เนื้อหาในไฟล์จะประมาณนี้
#!/bin/sh
rm -rf /var/lib/cloud/*
sudo apt-get update -y
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
sudo usermod -aG docker ubuntu
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
มาดู base/main.tf
กัน จะยาวๆ หน่อย
# base/main.tf
provider "aws" {
access_key = var.access_key_id
secret_key = var.secret_access_id
region = var.region
}
variable "access_key_id" {
type = string
description = "AWS Access Key ID"
}
variable "secret_access_id" {
type = string
description = "AWS Secret"
}
variable "region" {
type = string
default = "ap-southeast-1"
description = "AWS Region"
}
variable "product_area" {
type = string
default = "lost-in-space"
description = "Product Area"
}
variable "environment" {
type = string
default = "dev"
description = "Product Environment"
}
resource "aws_vpc" "lost_in_space" {
assign_generated_ipv6_cidr_block = true
cidr_block = "10.30.0.0/21"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = var.product_area
environment = var.environment
product-area = var.product_area
}
}
resource "aws_internet_gateway" "lost_in_space_internet_gateway" {
vpc_id = aws_vpc.lost_in_space.id
tags = {
Name = "${var.product_area}-internet-gateway"
environment = var.environment
product-area = var.product_area
}
}
resource "aws_subnet" "lost_in_space_public_subnet_zone_a" {
vpc_id = aws_vpc.lost_in_space.id
assign_ipv6_address_on_creation = true
availability_zone = "${var.region}a"
cidr_block = "10.30.0.0/24"
ipv6_cidr_block = cidrsubnet(aws_vpc.lost_in_space.ipv6_cidr_block, 8, 0)
tags = {
Name = "${var.product_area}-public-subnet-zone-a"
environment = var.environment
product-area = var.product_area
}
}
resource "aws_route_table" "lost_in_space_public_route_table" {
vpc_id = aws_vpc.lost_in_space.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.lost_in_space_internet_gateway.id
}
tags = {
Name = "${var.product_area}-public-route-table"
environment = var.environment
product-area = var.product_area
}
}
resource "aws_route_table_association" "lost_in_space_public_route_table_association_zone_a" {
subnet_id = aws_subnet.lost_in_space_public_subnet_zone_a.id
route_table_id = aws_route_table.lost_in_space_public_route_table.id
}
resource "aws_security_group" "lost_in_space" {
name = "lost-in-space-dev"
description = "Security group for Lost In Space (dev)"
vpc_id = aws_vpc.lost_in_space.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow SSH inbound traffic"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP inbound traffic"
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTPS inbound traffic"
}
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow PostgreSQL inbound traffic"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow Internet Outbound"
}
tags = {
Name = "lost-in-space-dev"
environment = var.environment
product-area = var.product_area
}
}
resource "aws_alb" "lost_in_space" {
name = "lost-in-space-dev"
security_groups = [aws_security_group.lost_in_space.id]
subnets = [aws_subnet.lost_in_space_public_subnet_zone_a.id, aws_subnet.lost_in_space_public_subnet_zone_b.id]
enable_deletion_protection = true
idle_timeout = 300
tags = {
Name = "lost-in-space-dev"
environment = var.environment
product-area = var.product_area
}
}
resource "aws_alb_target_group" "lost_in_space" {
name = "lost-in-space-alb-target"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.lost_in_space.id
stickiness {
type = "lb_cookie"
cookie_duration = 3600
}
health_check {
path = "/"
port = 80
}
}
resource "aws_alb_listener" "listener_http" {
load_balancer_arn = aws_alb.lost_in_space.arn
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = aws_alb_target_group.lost_in_space.arn
type = "forward"
}
}
ก็จะมีการสร้าง VPC, Internet Gateway, Subnet, Route Table, Security Group, Application Load Balancer (ALB) ซึ่งของพวกนี้เราไม่จำเป็นต้อง deploy ใหม่ทุกครั้งครับ เรา define ไว้ได้เลย
ต่อไปมาดู app/main.tf
กัน ซึ่งไฟล์นี้แหละ เราจะเอาไว้ทำ blue-green deployment
# app/main.tf
provider "aws" {
access_key = var.access_key_id
secret_key = var.secret_access_id
region = var.region
}
variable "access_key_id" {
type = string
description = "AWS Access Key ID"
}
variable "secret_access_id" {
type = string
description = "AWS Secret"
}
variable "region" {
type = string
default = "ap-southeast-1"
description = "AWS Region"
}
variable "product_area" {
type = string
default = "lost-in-space"
description = "Product Area"
}
variable "environment" {
type = string
default = "dev"
description = "Product Environment"
}
variable "infrastructure_version" {
type = string
description = "Infrastructure Version"
}
data "aws_alb_target_group" "lost_in_space" {
name = "lost-in-space-alb-target"
}
data "aws_subnet" "selected" {
filter {
name = "tag:Name"
values = ["${var.product_area}-public-subnet-zone-a"]
}
}
data "aws_security_group" "selected" {
filter {
name = "tag:Name"
values = ["lost-in-space-dev"]
}
}
resource "aws_alb_target_group_attachment" "lost_in_space_target_group_attachment" {
target_group_arn = data.aws_alb_target_group.lost_in_space.arn
target_id = aws_instance.lost_in_space.id
port = 80
}
resource "aws_instance" "lost_in_space" {
associate_public_ip_address = true
subnet_id = data.aws_subnet.selected.id
security_groups = [data.aws_security_group.selected.id]
ami = "ami-09a4a9ce71ff3f20b"
instance_type = "t2.medium"
key_name = "rocket-dev"
user_data = file("bootstrap.sh")
root_block_device {
volume_type = "gp2"
volume_size = 30
}
volume_tags = {
Name = "lost-in-space-dev"
environment = var.environment
product-area = var.product_area
}
tags = {
Name = "lost-in-space-dev"
InfrastructureVersion = var.infrastructure_version
environment = var.environment
product-area = var.product_area
}
lifecycle {
create_before_destroy = true
}
}
ไฟล์ app/main.tf
จะมีจุดสำคัญอยู่ 3 จุดที่เราจำเป็นต้องรู้คือ
- การใช้ Data Source สาเหตุที่เราต้องใช้เพราะว่าเราจำเป็นต้องรู้ว่าตอนที่เราจะ deploy เราจะเอา EC2 instance ของเราไปผูกกับ ALB target group อะไร Subnet อะไร และ Security Group อะไร ที่เราสร้างไว้ตอนแรกใน
base/main.tf
ซึ่งตัว Data Source นี่แหละ เหมือนให้เราสามารถไปดึงค่า AWS service ที่เราเคยสร้างเอาไว้แล้วมาใช้ในสคริปนี้ต่อได้ - การใช้ variable ที่ชื่อ
InfrastructureVersion
ซึ่งตรงนี้ จะเป็นจุดที่ผมใช้เพื่อให้ Terraform ตรวจจับการเปลี่ยนแปลงเพื่อที่มันจะสร้าง stack ใหม่ให้ผมได้ ซึ่งตรงนี้ก็มีหลากหลายเทคนิคนะครับ จะแก้ตัวไฟล์app/main.tf
เลย หรือจะมีอีกสคริปหนึ่งมา search & replace ค่าอะไรสักอย่างในapp/main.tf
ก็ได้ ใครชอบแบบไหนก็เลือกเอาได้เลย - จุดสำคัญที่สุดที่จะทำให้เราลด downtime เวลาที่ deploy ได้คือตรง
lifecycle
ครับ ในที่นี้ผมเซตcreate_before_destroy = true
จะหมายความว่าให้สร้าง stack ใหม่ให้เสร็จก่อน แล้วค่อยลบ stack เก่า ซึ่งถ้าไม่ได้เซตตรงนี้ไว้ stack เก่าจะโดนลบทันทีที่เรากำลังสร้าง stack ใหม่ครับ แนะนำให้อ่าน Zero Downtime Updates with HashiCorp Terraform ต่อ เค้าจะบอกวิธีแก้ในกรณีที่ application ของเรายังไม่ได้รันขึ้นมาจริงๆ (รัน instance ขึ้นมาได้ ไม่ได้หมายความว่า application ของเราจะรันเสร็จ)
จบ! 😎
Top comments (0)