Setup Infrastructure ง่าย ๆ ด้วย Terraform

Banyawat
I GEAR GEEK
Published in
7 min readMay 26, 2024

ในการ process การทำ Software นั้น ความลำบากหนึ่งที่หลายทีมเจอก็คือการจัดการ resource on cloud โดยปกติโปรเจคหนึ่งจะประกอบไปด้วยองค์ประกอบหลายอย่างเช่น VM, Storage, Database, Queue, Secret Management Service, .etc ซึ่งทั้งหมดนี้อาจจะต้องทำซ้ำกันหลายครั้งอีกด้วย เนื่องจาก software process ต้องมี environment หลากหลายเช่น test, release, beta, production เป็นเรื่องน่าเศร้ามากถ้าหากพิธี setup เหล่านี้ยังเป็น manual ทั้งหมด บทความนี้เลยอยากมาเล่าสู่กันฟังว่าทำไมถึงต้องใช้ครับ

Terraform คืออะไร

เป็นหนึ่งเครื่องมือที่จะมาช่วยเราจัดการ cloud resource โดยอิงหลักการ IaC (Infrastructure as Code) เพื่อให้การ setup cloud resource สามารถทำซ้ำได้ง่ายและเป็น pattern

โดย terraform นั้น support cloud resource ได้หลากหลายเจ้า ไม่ว่าจะเป็น AWS, Google Cloud, Azure, Docker, Kubernetes ฯลฯ จากที่ใช้งานมาแล้ว พบว่าครอบคลุมเคสการใช้งานโดยทั่วไปครับ

ปัญหา — ฉันรู้อยู่คนเดียว

หลักการ IaC นั้นมีไว้เพื่อแก้ปัญหาฉันรู้อยู่คนเดียวนี่แหละครับ การที่เราหรือใครสักคนต้องไป setup cloud resource หรือ infrastructure ต่าง ๆ แบบ manual นั้น มีความเสี่ยงสูงในเรื่องการ re-run step เอามาก ๆ เนื่องจากเราไม่ trust ว่าเวลาผ่านไปคนนั้นจะจำวิธีการที่ทำได้เป๊ะ ๆ และยิ่งไปกว่านั้นถ้าไม่มี document เขียนไว้ก็จะสร้างความปวดหัวให้ทีมที่มาทำต่อได้อย่างดี

ดังนั้นการจดไว้ดีกว่าจำครับ เมื่อเราทำ Terraform แล้ว code เหล่านี้จะบอก ลำดับ ความเชื่อมโยง เช่น ต้องสร้าง S3 Bucket ก่อนแล้วค่อยเอา ID ของ Bucket ไป reference เพื่อสร้าง key ให้ user ใช้ต่อ

มาลองสร้าง Terraform Code กัน

ในตัวอย่างนี้ เราจะมาลองสร้างของบน AWS กันครับ โจทย์มีอยู่ว่า Project ต้องการสร้าง API Service ที่มีความสามารถในการจัดการรูปของ application user เมื่อ list resource มาแล้วจะต้อง setup ดังนี้

  • S3 Bucket— สำหรับ File Storage
  • IAM User Access Key— สร้าง Access Key เปรียบเหมือนเป็นกุญแจมอบสิทธิ์ให้ API service สามารถเข้าถึง S3 Bucket และ Database ได้
  • Lightsail — Virtual Machine สำหรับ Host API service

ก่อนเริ่มให้ติดตั้ง Terraform CLI, AWS CLI ให้เรียบร้อย

โครงสร้างโปรเจค

.
└── apps/
└── myapi/
├── test/
│ └── main.tf
├── release/
│ └── main.tf
└── prod/
└──main.tf

จะวางไปตาม environment ที่ต้องใช้ประกอบไปด้วย test, release, prod ซึ่งตามจริง code main.tf แต่ละ environment ควรจะต่างกันแค่ variable ซึ่งเราสามารถ reuse import pattern มาใช้ในแต่ละที่ได้ แต่ในบทความนี้จะเป็น basic ดังนั้นเราจะสนใจแค่ /apps/myapi/test/main.tf เท่านั้น

[1] เริ่มต้นการประกาศ Dependency ที่ต้องใช้

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.44.0"
}
}
}

provider "aws" {
region = "ap-southeast-1"
}

เริ่มจากการประกาศ dependency หรือ provider เป็น aws โดยการ run code จะต้อง setup AWS credentials ในเครื่องก่อน และไม่แนะนำให้ใช้ IAM Access Key ตรงนี้ แต่ให้ใช้จาก configure ในเครื่องตัวเองแทน ซึ่งได้แนบ link ref ไว้แล้วนะครับ

และใน code นี้ เราจะใช้ AWS provider version 5.44 และ setup region เป็น Singapore ap-southeast-1 นะครับ หากผู้อ่านใช้ config ที่ต่างกันออกไปให้ดูตรงนี้เป็นหลักนะครับ

references:

[2] ประกาศ cloud resource ของ S3 Bucket File Storage

## [2.1] Create bucket
resource "aws_s3_bucket" "my_bucket" {
bucket = "banyawat-demo-bucket"
}

## [2.2] Configure bucket ownership
resource "aws_s3_bucket_ownership_controls" "my_bucket" {
depends_on = [aws_s3_bucket.my_bucket]
bucket = aws_s3_bucket.my_bucket.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}

แต่ละ Step ผมจะขออธิบายเป็นข้อดังนี้

  • [2.1] ตั้งค่าเพื่อประกาศชื่อให้กับ Bucket ที่ต้องการสร้าง โดยข้อกำหนดในการตั้งชื่อของ bucket จะต้องเป็น global unique หรือไม่ซ้ำกับคนทั้งโลกที่ใช้ AWS นะครับ
  • [2.2] ตั้งค่าสิทธิ์การเข้าถึง object สูงสุดเป็น BucketOwnerPreferred สังเกตว่า block นี้มีการใช้ depends_on ที่หมายถึงให้รอจนกว่า Bucket สร้างเสร็จ แล้วถึงนำ bucket ID มาใช้เป็น reference ในการ config

[3] ประกาศ cloud resource ของ S3 IAM User + Access Key

## [3.1] Define local variable
locals {
iam_name = "my-demo-user"
}

## [3.2] Create new IAM User
resource "aws_iam_user" "my_iam" {
name = local.iam_name
}

## [3.3] Create IAM User's Policy
resource "aws_iam_user_policy" "bucket_rw" {
name = "${local.iam_name}-direct-policy"
user = aws_iam_user.my_iam.name

# Terraform's "jsonencode" function converts a
# Terraform expression result to valid JSON syntax.
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject"
]
Effect = "Allow"
Resource = [
aws_s3_bucket.my_bucket.arn,
"${aws_s3_bucket.my_bucket.arn}/*"
]
}
]
})
}

## [3.4] Create IAM User's Access Key
resource "aws_iam_access_key" "my_iam_access_key" {
user = aws_iam_user.my_iam.name
}
  • [3.1] ประกาศตัวแปรแบบ local
  • [3.2] สร้าง IAM User โดยใช้ค่าจากตัวแปร local
  • [3.3] สร้าง IAM User Policy โดยใช้ค่าจากตัวแปร local และให้สิทธิ์ IAM User สามารถ สร้าง / ลบ / อ่าน Object ใน Bucket นี้ได้ พร้อมทั้งระบุจุดที่สิทธิ์นี้เข้าถึงได้ใน Resource
  • [3.4] สร้าง Access Key, Secret Key จาก IAM User ที่จะนำไปใช้ใน API ต่อ

[4] ประกาศ cloud resource Lightsail VM

## [4.1] Create Lightsail instance
resource "aws_lightsail_instance" "my_instance" {
name = "my-demo-lightsail-vm"

availability_zone = "ap-southeast-1a"
## [4.2] Select OS or blueprint; Check out at `aws lightsail get-blueprints`
blueprint_id = "ubuntu_22_04"
## [4.3] Select bundle; Check out at `aws lightsail get-bundles`
bundle_id = "micro_3_0"

## [4.4] Pre-install script
## user_data = "sudo yum install -y httpd && sudo systemctl start httpd && sudo systemctl enable httpd && echo '<h1>Deployed via Terraform</h1>' | sudo tee /var/www/html/index.html"
}

## [4.5] Configure public port
resource "aws_lightsail_instance_public_ports" "my_public_port" {
instance_name = aws_lightsail_instance.my_instance.name

port_info {
protocol = "tcp"
from_port = 22
to_port = 22
}

port_info {
protocol = "tcp"
from_port = 443
to_port = 443
}

port_info {
protocol = "tcp"
from_port = 80
to_port = 80
}
}
  • [4.1] ประกาศ resource Lightsail instance กำหนดชื่อและ availibility zone ให้ตรงกับ region ที่เราเลือก
  • [4.2] เลือก Blueprint ที่จะใช้งาน สามารถ List รายชื่อจาก AWS CLI เท่านั้น เพราะแต่ละ AWS Account จะไม่เหมือนกัน ในที่นี้ VM ของเราจะใช้เป็น Ubuntu 22.04 นะครับ
  • [4.3] เลือก Size ของ Lightsail โดย List จาก AWS CLI อีกเช่นกัน ในที่นี้ของเราจะเลือกเป็น micro_3_0 ซึ่งเป็นตัวเล็กสุดและอยู่ใน Free tier (3 เดือนแรก)
  • [4.4] command line script ที่ run ตอนที่ instance boot ครั้งแรก ตรงนี้เราจะต้อง install dependency และ setup app ต่าง ๆ ที่ต้องใช้ start API เช่น Docker, Node runtime, Java runtime ผ่าน command ทั่วไป
    ตรงนี้ขออนุญาต comment ไว้เนื่องจากการ setup ข้างใน VM เป็นหนังม้วนยาวที่ไปหาข้อมูลต่อเองได้ครับ
  • [4.5] เลือก port ที่จะออกสู่ internet ตรงนี้ เราจะเลือกเป็น 22 สำหรับ SSH access, 443 สำหรับ HTTPS protocol, และ 80 สำหรับ HTTP protocol

Full Code

/apps/myapi/test/main.tf

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.44.0"
}
}
}

provider "aws" {
region = "ap-southeast-1"
}

## [2.1] Create bucket
resource "aws_s3_bucket" "my_bucket" {
bucket = "banyawat-demo-bucket"
}

## [2.2] Configure bucket ownership
resource "aws_s3_bucket_ownership_controls" "my_bucket" {
depends_on = [aws_s3_bucket.my_bucket]
bucket = aws_s3_bucket.my_bucket.id

rule {
object_ownership = "BucketOwnerPreferred"
}
}

## [3.1] Define local variable
locals {
iam_name = "my-demo-user"
}

## [3.2] Create new IAM User
resource "aws_iam_user" "my_iam" {
name = local.iam_name
}

## [3.3] Create IAM User's Policy
resource "aws_iam_user_policy" "bucket_rw" {
name = "${local.iam_name}-direct-policy"
user = aws_iam_user.my_iam.name

# Terraform's "jsonencode" function converts a
# Terraform expression result to valid JSON syntax.
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject"
]
Effect = "Allow"
Resource = [
aws_s3_bucket.my_bucket.arn,
"${aws_s3_bucket.my_bucket.arn}/*"
]
}
]
})
}

## [3.4] Create IAM User's Access Key
resource "aws_iam_access_key" "my_iam_access_key" {
user = aws_iam_user.my_iam.name
}

## [4.1] Create Lightsail instance
resource "aws_lightsail_instance" "my_instance" {
name = "my-demo-lightsail-vm"

availability_zone = "ap-southeast-1a"
## [4.2] Select OS or blueprint; Check out at `aws lightsail get-blueprints`
blueprint_id = "ubuntu_22_04"
## [4.3] Select bundle; Check out at `aws lightsail get-bundles`
bundle_id = "micro_3_0"

## [4.4] Pre-install script
## user_data = "sudo yum install -y httpd && sudo systemctl start httpd && sudo systemctl enable httpd && echo '<h1>Deployed via Terraform</h1>' | sudo tee /var/www/html/index.html"
}

## [4.5] Configure public port
resource "aws_lightsail_instance_public_ports" "my_public_port" {
instance_name = aws_lightsail_instance.my_instance.name

port_info {
protocol = "tcp"
from_port = 22
to_port = 22
}

port_info {
protocol = "tcp"
from_port = 443
to_port = 443
}

port_info {
protocol = "tcp"
from_port = 80
to_port = 80
}
}

[5] Run!

command line ไปที่ path /apps/myapi/test/main.tf จากนั้น terraform init เพื่อ install required dependency ตามที่เราได้ประกาศใน step แรก

dependency installing

terraform validate เช็คว่า Syntax script ของเราถูกหรือไม่

Terraform validation

terraform apply เพื่อให้ terraform ประเมิน action list ในการสร้าง cloud resource ให้เรา

ตรงนี้แนะนำให้เช็ค action list ก่อนทุกกรณี! เนื่องจากถ้าบังเอิญมี resource ที่มีชื่อเหมือนกันและดันมี data อยู่ข้างใน การ apply code นี้อาจจะลบหรือ update config ของ resource ที่ไม่เกี่ยวข้องทิ้งไปได้

step นี้จะมีการ validate เพิ่มเติมจากการที่ไปเรียก API ของ Cloud provider เองซึ่งอาจจะเจอ error หรือติด permission ก็ให้แก้ตามเคสไปเรื่อย ๆ ครับ เมื่อสำเร็จ ก็จะแสดงข้อความดังนี้

มาเช็ค resource on AWS console กัน ก็จะได้ของตามที่สั่งไว้เป๊ะ ๆ

เป็นอันเสร็จสิ้น และเพื่อ cleanup resource ที่สร้างมา ให้ใช้ terraform destroy เพื่อย้อน step ที่เราเพิ่ง apply ไป code นี้ก็จะ clean ให้แบบหมดจดครับ

สรุป

ในสถานการณ์จริง เมื่อทีมต้องการสร้าง environment test, release, prod เราก็สามารถ reuse code แล้วปรับเป็นการระบุ parameter ในแต่ละ environment แทน แบบนี้จะลดความยุ่งยากในการที่ต้องไปกดตาม step ที่ตัวคนทำเองอาจจะจำไม่ได้แล้วว่าทำ config อะไรไปบ้าง

ใน project ครั้งต่อไปเมื่อเรามี code แล้วก็ง่ายที่จะ reuse architecture setup ของอีก project มาโดยง่าย ข้อผิดพลาดน้อยลง และทำให้ทีมอยู่ใน culture แบบ More automation, less work ครับ

ข้อควรระวังในการนำไปใช้จริง

เนื่องจากบทความนี้เป็นเพียง Demo เท่านั้น ในการนำไปใช้งาน production อาจจะต้องระวังเรื่องความปลอดภัยมากขึ้น ดังนี้

  • IAM User Access Key — ในการใช้ร่วมกับ VM ให้เป็นการมอบสิทธิ์ผ่าน IAM Role ที่ผูกไว้ที่ VM มากกว่าใช้เป็น Key
  • S3 Bucket Permission— ควรตรวจสอบ Bucket Policy หรือสิทธิ์ต่าง ๆ ในเรื่อง public / private ให้ดีก่อนใช้งาน
  • ล่าสุด Terraform เพิ่งจะประกาศแยก Community Edition ออกไปหลัง version 1.6.0 ดังนั้นในการใช้งาน ผู้ใช้ต้องยอมรับความเสี่ยงในการลิขสิทธิ์ที่ไม่ใช่ Open Source ด้วยตัวเองนะครับ

--

--