สรุปมาจาก An Introduction to Terraform และเสริมด้วยข้อมูลจาก Official Doc
จาก EP ที่แล้ว เราน่าจะพอเข้าใจ Terraform แล้วว่ามันคืออะไร คราวนี้เรามาลงมือเล่นกันเถอะ โดยเรามาลอง ทำ cluster ของ web server พร้อมด้วย load balancer บน AWS ด้วย Terraform กัน
ถ้าใครไม่เคยใช้ทั้ง Terraform และ AWS ทำใจร่มๆ ไว้ได้เลย เพราะผมก็ไม่เคย เรามาเรียนรู้มันไปพร้อมๆ กัน
Topics
- Install Terraform
- Terraform Configuration Syntax
- Set up AWS account
- Deploy a single server
- Deploy a single web server
- Play around with variables
- Deploy a cluster of web servers
- Deploy a load balancer
Install Terraform
การ install terraform
นั้นทำได้ง่ายมาก โดยแค่ไป download binary จาก Official Website ตาม system และ CPU Architecture ของเรา แล้ว run ได้เลย ซึ่งสามารถดู tutorial นี้ได้เลย
เนื่องจากผมทำบน Ubuntu ดังนั้น ผมจะแสดงแค่ Ubuntu นะครับ
$ sudo -i
$ wget https://releases.hashicorp.com/terraform/0.12.23/terraform_0.12.23_linux_amd64.zip
$ unzip terraform_0.12.23_linux_amd64.zip
$ mv terraform /usr/local/sbin/
$ terraform
Usage: terraform [-version] [-help] <command> [args]
The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you''re just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.
Common commands:
apply Builds or changes infrastructure
console Interactive console for Terraform interpolations
destroy Destroy Terraform-managed infrastructure
env Workspace management
fmt Rewrites config files to canonical format
get Download and install modules for the configuration
graph Create a visual graph of Terraform resources
import Import existing infrastructure into Terraform
init Initialize a Terraform working directory
login Obtain and save credentials for a remote host
logout Remove locally-stored credentials for a remote host
output Read an output from a state file
plan Generate and show an execution plan
providers Prints a tree of the providers used in the configuration
refresh Update local state file against real resources
show Inspect Terraform state or plan
taint Manually mark a resource for recreation
untaint Manually unmark a resource as tainted
validate Validates the Terraform files
version Prints the Terraform version
workspace Workspace management
All other commands:
0.12upgrade Rewrites pre-0.12 module source code for v0.12
debug Debug output management (experimental)
force-unlock Manually unlock the terraform state
push Obsolete command for Terraform Enterprise legacy (v1)
state Advanced state management
Terraform Configuration Syntax
Terraform ใช้ HCL syntax ในการ configure ซึ่งสามารถอ่านแบบเต็มๆ ได้จาก Github ของ HCL ในที่นี้ผมจะเกริ่นคร่าวๆ ถึง key syntax หลักๆ เพื่อความเข้าใจซักเล็กน้อย ดังนี้
-
Arguments: เป็นการ assign ค่าให้กับตัวแปร ใน HCL document มักจะเรียกว่า "attribute" แต่ใน Terraform ใช้คำว่า attribute กับ resource อื่นแล้ว เลยเลี่ยงมาใช้คำว่า "argument" แทน
image_id = "abc123"
-
Blocks: เป็น container ที่เก็บ content อื่นๆ เพื่อใช้ในการกำหนด specification ของ resource ที่เราต้องการ create โดย โครงสร้างจะเป็น
<type> <label#1> <label#2> ... <label#N> { ... }
และ block สามารถซ้อนเป็น nested block ได้
resource "aws_instance" "example" { ami = "abc123" network_interface { # ... } }
Identifiers: คือชื่อของสิ่งต่างๆ ใน configuration เช่น Argument names, block type names, resources และ input variables เป็นต้น สามารถใช้ letters, digits, underscores (_) และ hyphens (-) เป็น identifier ได้ แต่ไม่ควรใช้ digits ขึ้นต้น identifier เพราะจะทำให้สับสนกับเลขจริงๆ
-
Comments: สามารถ
- ใช้
#
(default) ในการ comment แต่ละบรรทัด - ใช้
//
ในการ comment แต่ละบรรทัด - ใช้
/* ... */
ในการ comment หลาย บรรทัด
- ใช้
Character Encoding and Line Endings: configuration ของ Terraform เป็น UTF-8 แต่ delimiter ยังต้องเป็น ASCII อยู่ ตัวแบ่งบรรทัดสามารถเป็นได้ทั้ง Unix-style (LF only) และ Windows-style (CR then LF) แต่ที่ถูกต้องต้องเป็น Unix-style
-
Variables: ชนิดของตัวแปรหลักๆ มีดังนี้
- string: ตัวหนังสือ
- number: ตัวเลข
- bool: true หรือ false
-
list(type): เป็น list โดยใน list ต้องเป็น type เดียวกัน และมีลำดับ
[0,5,2,4,5,4]
-
set(type): เหมือน list แต่ element ต้องไม่ซ้ำกัน และ ไม่มี order เพราะ มันจะ sort จากน้อยไปมากโดยอัตโนมัติ
[0,2,4,5]
-
tuple([, ...]) เหมือน list แต่ element ต่าง type กันได้
[0, 'string', false]
-
map(type): ใช้เก็บ key-value pair
{ "key" = "value" }
-
object({<attr_name>=<type>, ...}): ใช้เก็บ key-value pair แต่ต่าง type กันได้
{ firstname = 'john' housenumber = 10 }
เรามาลองเล่นกันตัวแปรกันดีกว่า โดยเราใช้ใช้ command terraform console
เพื่อ interact กับ configuration file ของเราดู
$ mkdir -p terraform-variables && cd terraform-variables
$ cat > main.tf << EOF
variable "myvar" {
type = string
default = "hello terraform"
}
variable "mymap" {
type = map(string)
default = {
mykey = "my value"
}
}
variable "mylist" {
type = list
default = [1,2,3]
}
EOF
$ terraform console
> var.myvar # Access variable แบบที่ 1
hello terraform
> "${var.myvar}" # Access variable แบบที่ 2
hello terraform
> var.mymap
{
"mykey" = "my value"
}
> var.mymap["mykey"] # Access variable ที่เป็น Map โดยระบุ key ที่ต้องการเข้าถึง
my value
> var.mylist
[
1,
2,
3,
]
> var.mylist[0] # Access variable แบบ list index ที่ 0
1
> var.mylist[1] # Access variable แบบ list index ที่ 1
2
> var.mylist[2] # Access variable แบบ list index ที่ 2
3
> element(var.mylist, 0) # Access variable แบบ list index ที่ 0 โดยใช้ function "element"
1
> element(var.mylist, 1) # Access variable แบบ list index ที่ 1 โดยใช้ function "element"
2
> element(var.mylist, 2) # Access variable แบบ list index ที่ 2 โดยใช้ function "element"
3
> slice(var.mylist, 0, 2) # Slice list เพื่อดึงเฉพาะค่าที่ต้องการ
[
1,
2,
]
> exit
Set up AWS account
- ถ้าใครยังไม่มี account เราสามารถสมัครใช้บริการ Free Tier ได้ 1 ปี แต่ต้องผู้บัตร credit ด้วย อย่าไปกลัว 1 ปีนี้เราไม่เสียตังค์แน่นอน 😎
- ทำการ login ด้วย email ของเรา โดยเลือกเป็น Root user
- ทำการสร้าง IAM user ชื่อ "terraform" และ ให้สิทธิ์ "AmazonEC2FullAccess" เพื่อให้ Terraform เข้าไปสั่งงานสร้าง EC2 (เป็นชื่อเรียก VM ของ AWS) ผ่าน API ของ AWS
- กดที่ "Services" ตรงมุมซ้ายบน
- พิมพ์ "IAM" ในช่อง search แล้วกดที่ "IAM"
- ในหน้า "IAM"
- กด "Users" ตรง menu ทางซ้าย
- กดปุ่ม "Add user" สีน้ำเงิน
- ในหน้า "Add user"
- ช่อง "User name" ใส่ "terraform"
- ช่อง "Access type" เลือก "Programmatic access"
- กดปุ่ม "Next: Permissions"
- เลือก "Add user to group"
- กดปุ่ม "Create group" จะมี Popup "Create group" ปรากฏขึ้นมา
- ช่อง "Group name" ใส่ "terraform-administrator"
- เลือก Policy "AmazonEC2FullAccess"
- กดปุ่ม "Create group" สีน้ำเงิน
- เมื่อกลับมาหน้า "Add user" กดปุ่ม "Next: Tags" สีน้ำเงิน
- กดปุ่ม "Next: Review" สีน้ำเงิน
- กดปุ่ม "Create user" สีน้ำเงิน
- ถ้า ✔ "Success" กดปุ่ม "Download .csv" เพื่อเก็บ Access key ID และ Secret access key เอาไว้ใช้กับ Terraform ต่อไป
- กดปุ่ม "Close"
- จะได้ user ดังนี้
Deploy a single server
ถ้าคุณใช้ editor ต่างๆ เช่น vim, emacs, Sublime Text, Atom, Visual Studio Code หรือ IntelliJ ลอง search หา "HCL" ดูจะช่วย highlight syntax ให้เราดูง่ายขึ้น
-
ทำการ set credential ของ AWS ให้กับ shell ของคุณก่อน โดย เอาข้อมูลมาจาก CSV file ที่เรา download มาก่อนหน้านี้
$ export AWS_ACCESS_KEY_ID=(your access key id) $ export AWS_SECRET_ACCESS_KEY=(your secret access key)
-
สร้าง Terraform configuration file เพื่อสร้าง EC2 1 server
ในการสร้าง EC2 เราต้องทำการระบุ image ที่จะใช้สร้าง servers ด้วย AMI-ID ซึ่งเราสามารถ หา AMI-ID ที่เราต้องการได้จาก
- เข้า EC2 console
- เลือก Region ที่เราต้องการสร้าง EC2
- กดที่ AMIs แล้วเลือก Public Image แล้วหา AMI-ID ที่ต้องการ
$ cd ~ && mkdir -p tf_01 && cd tf_01 $ cat > main.tf << EOF provider "aws" { region = "us-east-2" } resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" } EOF
อธิบาย
- provider: เป็นการระบุ cloud provider ที่เราต้องการ นั่นคือ aws
- region: เป็น region ของ AWS ที่เราต้องการจะสร้าง EC2
- resource: ใช้ระบุ resource ที่ต้องการสร้าง ในที่นี้จะสร้าง aws_instance (EC2) โดยให้มีชื่อว่า example
- ami: ระบุ ami-id ของ image ที่เราจะใช้สร้าง server สามารถหาได้จาก aws marketplace
- instance_type: กำหนด size ของ server ที่เราต้องการสร้าง สามารถดู type ทั้งหมดได้จาก Amazon EC2 Instance Types
-
ทำการสร้าง EC2 ด้วย Terraform
1. ทำการ init terraform เพื่อ download library ที่จำเป็นต้องใช้ทั้งหมด $ terraform init Initializing the backend... Initializing provider plugins... - Checking for available provider plugins... - Downloading plugin for provider "aws" (hashicorp/aws) 2.53.0... (...) * provider.aws: version = "~> 2.53" Terraform has been successfully initialized! (...) 2. library จะอยู่ใน .terraform $ tree -a .terraform/ .terraform/ └── plugins └── linux_amd64 ├── lock.json └── terraform-provider-aws_v2.53.0_x4 3. ตรวจสอบว่า จาก configuration file ของเรา Terraform จะทำอะไรบ้าง + คือ สร้างเพิ่ม - คือ ลบออก ~ คือ มีการแก้ไข -/+ คือ destroy and then create replacement $ terraform plan (...) Terraform will perform the following actions: # aws_instance.example will be created + resource "aws_instance" "example" { + ami = "ami-0c55b159cbfafe1f0" + arn = (known after apply) + associate_public_ip_address = (known after apply) + availability_zone = (known after apply) + cpu_core_count = (known after apply) + cpu_threads_per_core = (known after apply) + get_password_data = false + host_id = (known after apply) + id = (known after apply) + instance_state = (known after apply) + instance_type = "t2.micro" + ipv6_address_count = (known after apply) + ipv6_addresses = (known after apply) + key_name = (known after apply) + network_interface_id = (known after apply) + password_data = (known after apply) + placement_group = (known after apply) + primary_network_interface_id = (known after apply) + private_dns = (known after apply) + private_ip = (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) + security_groups = (known after apply) + source_dest_check = true + subnet_id = (known after apply) + tenancy = (known after apply) + volume_tags = (known after apply) + vpc_security_group_ids = (known after apply) (...) Plan: 1 to add, 0 to change, 0 to destroy. (...) 4. ทำการสร้าง EC2 server โดยมันจะให้เรา review อีกครั้ง ถ้าเราตอบ yes มันจึงจำเริ่มสร้าง $ terraform apply (...) Terraform will perform the following actions: # aws_instance.example will be created + resource "aws_instance" "example" { + ami = "ami-0c55b159cbfafe1f0" + arn = (known after apply) + associate_public_ip_address = (known after apply) + availability_zone = (known after apply) + cpu_core_count = (known after apply) + cpu_threads_per_core = (known after apply) + get_password_data = false + host_id = (known after apply) + id = (known after apply) + instance_state = (known after apply) + instance_type = "t2.micro" + ipv6_address_count = (known after apply) + ipv6_addresses = (known after apply) + key_name = (known after apply) + network_interface_id = (known after apply) + password_data = (known after apply) + placement_group = (known after apply) + primary_network_interface_id = (known after apply) + private_dns = (known after apply) + private_ip = (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) + security_groups = (known after apply) + source_dest_check = true + subnet_id = (known after apply) + tenancy = (known after apply) + volume_tags = (known after apply) + vpc_security_group_ids = (known after apply) (...) Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_instance.example: Creating... aws_instance.example: Still creating... [10s elapsed] aws_instance.example: Still creating... [20s elapsed] aws_instance.example: Still creating... [30s elapsed] aws_instance.example: Creation complete after 35s [id=i-0610f6754ebbc6824] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
-
เข้าไปดูผลงานของเรากัน
-
ทำการเพิ่ม tag ให้กับ EC2 ที่เราเพิ่มสร้างไป
1. เพิ่ม Tag ลงไปใน block resooure {...} ดัง เครื่องหมาย + $ vi main.tf | provider "aws" { | region = "us-east-2" | } | | resource "aws_instance" "example" { | ami = "ami-0c55b159cbfafe1f0" | instance_type = "t2.micro" +| tags = { +| Name = "terraform-example" +| } | } 2. ทำการ apply configuration ใหม่เพื่อเพิ่ม tag $ terraform apply (...) Terraform will perform the following actions: # aws_instance.example will be updated in-place ~ resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" arn = "arn:aws:ec2:us-east-2:139383842991:instance/i-0610f6754ebbc6824" associate_public_ip_address = true availability_zone = "us-east-2b" cpu_core_count = 1 cpu_threads_per_core = 1 disable_api_termination = false ebs_optimized = false get_password_data = false hibernation = false id = "i-0610f6754ebbc6824" instance_state = "running" instance_type = "t2.micro" ipv6_address_count = 0 ipv6_addresses = [] monitoring = false primary_network_interface_id = "eni-01e4dd5120846158a" private_dns = "ip-172-31-27-212.us-east-2.compute.internal" private_ip = "172.31.27.212" public_dns = "ec2-18-218-190-100.us-east-2.compute.amazonaws.com" public_ip = "18.218.190.100" security_groups = [ "default", ] source_dest_check = true subnet_id = "subnet-e0a68f9a" ~ tags = { + "Name" = "terraform-example" } (...) Plan: 0 to add, 1 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_instance.example: Modifying... [id=i-0610f6754ebbc6824] aws_instance.example: Still modifying... [id=i-0610f6754ebbc6824, 10s elapsed] aws_instance.example: Modifications complete after 12s [id=i-0610f6754ebbc6824] Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
-
เข้าไปดูผลงาน จะเห็นว่า EC2 เรามีชื่อแล้ว
Deploy a single web server
ในหัวข้อนี้เราจะมาทำให้ EC2 ของเราเป็น web server ง่ายๆ บน port 8080 ซึ่งตอบแค่ "Hello, World" ด้วย script ดังนี้
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p 8080 &
วิธีที่ควรทำคือ ทำ custome image ด้วย Packer แล้วสร้าง EC2 ใหม่จาก image นั้น แต่ใน case นี้เราจะทำง่ายๆ ด้วยการสั่งให้ image run script ผ่านทาง User Data ซึ่งจะถูก run ตอน boot เครื่องขึ้นมา
และเพื่อให้เราเข้าถึงหน้า web จากภายนอกได้ ต้องเพิ่ม security group เพื่อ allow traffic จากภายนอกผ่านทาง port 8080 ด้วย
ในการ apply security group ให้ EC2 เราต้องใส่ ID ของ security group ไปใน attribute ของ EC2 ด้วย แต่เนื่องจากเรายังไม่รู้ ID ของ security group เราสามารถ reference ค่าได้ โดยใช้ format ดังนี้
Format: [<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>]
Result: [aws_security_group.instance.id]
การทำ reference นี้ จะเป็นการสร้าง "implicit dependency" เป็นผลให้เวลาที่ Terraform ทำงานมันจะไปสร้าง security group ก่อน เพื่อให้ได้ ID แล้วจึงนำมา apply ให้ EC2 ใช้งาน
เวลาที่ Terraform parse dependency tree มันจะพยายามสร้าง plan ในการทำงานให้ parallel มากที่สุดเท่าที่จะทำได้ เพื่อให้สร้างได้รวดเร็วที่สุด
ลงมือทำกันเลย 🖐
-
เราจะแก้ไข main.tf อีกครั้ง ด้วยการเพิ่มบรรทัดที่มีเครื่องหมาย + ดังนี้
$ vi main.tf | provider "aws" { | region = "us-east-2" | } | | resource "aws_instance" "example" { | ami = "ami-0c55b159cbfafe1f0" | instance_type = "t2.micro" +| vpc_security_group_ids = [aws_security_group.instance.id] +| user_data = <<-EOF +| #!/bin/bash +| echo "Hello, World" > index.html +| nohup busybox httpd -f -p 8080 & +| EOF | tags = { | Name = "terraform-example" | } | } +| +| resource "aws_security_group" "instance" { +| name = "terraform-example-instance" +| ingress { +| from_port = 8080 +| to_port = 8080 +| protocol = "tcp" +| cidr_blocks = ["0.0.0.0/0"] +| } +| }
-
ทำการ apply configuration ใหม่ที่เราเพิ่งทำไป
$ terraform apply (...) Terraform will perform the following actions: # aws_instance.example must be replaced -/+ resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" (...) instance_type = "t2.micro" (...) + user_data = "c765373c563b260626d113c4a56a46e8a8c5ca33" # forces replacement (...) ~ vpc_security_group_ids = [ - "sg-594b143f", ] -> (known after apply) (...) } # aws_security_group.instance will be created + resource "aws_security_group" "instance" { + arn = (known after apply) + description = "Managed by Terraform" + egress = (known after apply) + id = (known after apply) + ingress = [ + { + cidr_blocks = [ + "0.0.0.0/0", ] + description = "" + from_port = 8080 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + security_groups = [] + self = false + to_port = 8080 }, ] + name = "terraform-example-instance" (...) } Plan: 2 to add, 0 to change, 1 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_instance.example: Destroying... [id=i-0610f6754ebbc6824] aws_security_group.instance: Creating... aws_security_group.instance: Creation complete after 9s [id=sg-0d7d9b50531db13c5] aws_instance.example: Still destroying... [id=i-0610f6754ebbc6824, 10s elapsed] aws_instance.example: Still destroying... [id=i-0610f6754ebbc6824, 20s elapsed] aws_instance.example: Destruction complete after 24s aws_instance.example: Creating... aws_instance.example: Still creating... [10s elapsed] aws_instance.example: Still creating... [20s elapsed] aws_instance.example: Creation complete after 30s [id=i-0db21cb267f702ce7] Apply complete! Resources: 2 added, 0 changed, 1 destroyed.
-
ไปดูผลงานของเรากัน จะเห็นว่า EC2 อันเก่าถูก terminate ไป แล้ว และมี EC2 ใหม่ขึ้นมาแทน นั้นเป็นเพราะ Terraform ทำงาน แบบ Immutable ดังนั้น การเปลี่ยนแปลงภายใน EC2 เช่น การ assign "User Data" นั้น จะต้องสร้าง EC2 ใหม่ เท่านั้น
-
ในส่วนของ Security Group ก็มีการ create และ allow port 8080 เรียบร้อย
-
ลองเข้า web โดยใช้ public IP ของ EC2 ของเราก็จะได้ "Hello, World" กลับมา
Play around with variables
ก่อนไปสร้าง cluster ยังจำ Don't Repeat Yourself (DRY) Principle กันได้รึเปล่า ถ้าจำได้ จะเห็นว่าเรามีการ define port 8080 ซ้ำกัน 2 รอบคือ ที่ security group และ user data
เรามาทำให้มันไม่ซ้ำกันก่อนดีกว่า ด้วย input variable
Input Variables
syntax ของ Input variable เป็นดังนี้
Syntax
variable "NAME" {
[CONFIG ...]
}
Example
variable "server_port" {
description = "The port the server will use for HTTP requests"
type = number
default = 8080
}
ในส่วนของ [CONFIG ...] จะประกอบด้วย 3 parameters คือ
-
description: เป็นคำอธิบายตัวแปรนี้ ว่าใช้ทำอะไร ค่านี้จะถูกแสดงตอน
plan
และapply
ด้วย -
default: เป็นการกำหนดค่า default value ให้กับตัวแปรดังกล่าว โดย เราสามารถ pass ค่าของตัวแปร ตอน
apply
ได้ด้วย-
การใช้
-var
option แล้ว pass ค่าของตัวแปรเข้ามา
$ terraform apply -var "server_port=8080"
-
การใช้
-var-file
option แล้ว pass file ที่เก็บค่าของตัวแปรเข้าไป
$ terraform apply -var-file="testing.tfvars"
-
การใช้ Environment Variable ใน format TF_VAR_<variable_name>
$ export TF_VAR_server_port=8080 $ terraform apply
-
ถ้าเราไม่ได้กำหนด default value และ ไม่ได้ pass ค่าเข้าไป Terraform จะถามเราทางหน้า Terminal
$ terraform apply var.server_port The port the server will use for HTTP requests Enter a value:
-
type: เป็นการกำหนด type ของตัวแปร สามารถเป็นได้ทั้ง string, number, bool, list, map, set, object, tuple หรือ any (ถ้าไม่ใส่ default เป็น any)
ในการใช้งานเราสามารถเข้าถึงด้วย var.<variable_name>
ซึ่งทำให้เรานำไปแทนค่าใน main.tf ได้ดังนี้
-
ในส่วน Security Group
resource "aws_security_group" "instance" { name = "terraform-example-instance" ingress { from_port = var.server_port to_port = var.server_port protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }
-
ในส่วนของ User Data นั้นเป็น string ดังนั้นเวลาที่จะไปแทนคนต้องใช้ expression
"${...}"
ซึ่งเรียกว่า interpolation
user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p "${var.server_port}" & EOF
ดังนั้น main.tf ใหม่จะเป็นแบบนี้
provider "aws" {
region = "us-east-2"
}
variable "server_port" {
description = "The port the server will use for HTTP requests"
type = number
default = 8080
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p "${var.server_port}" &
EOF
tags = {
Name = "terraform-example"
}
}
resource "aws_security_group" "instance" {
name = "terraform-example-instance"
ingress {
from_port = var.server_port
to_port = var.server_port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
Output Variable
เป็นการดึงค่าต่างๆ ที่ได้จากการทำงานมาใส่ในตัวแปร เพื่อให้เรานำไปใช้งานต่อไป โดยเราสามารถเลือกให้แสดงผลหลังจากที่ทำการ terraform apply
เรียบร้อย หรือ เก็บไว้ใช้แต่ตอนเรียกใช้ด้วย command terraform output
ก็ได้
Syntax
output "<NAME>" {
value = <VALUE>
[CONFIG ...]
}
Example
output "public_ip" {
value = aws_instance.example.public_ip
description = "The public IP of the web server"
}
output "private_ip" {
value = aws_instance.example.private_ip
description = "The private IP of the web server"
sensitive = true
}
ในส่วนของ [CONFIG ...] มีได้ 2 parameters คือ
- description: เป็นคำอธิบายตัวแปรนี้ ว่าใช้เก็บค่าอะไร
-
sensitive: ถ้าเป็น true คือ sensitive จะไม่แสดง ค่าของ variable หลังจาก
terraform apply
เสร็จ
ถ้าหากเรา run terraform apply
ซ้ำเป็นครั้งที่ 2 terraform จะไปทำอะไรนอกจากแสดงค่า output ออกมา
$ terraform apply
aws_security_group.instance: Refreshing state... [id=sg-0d7d9b50531db13c5]
aws_instance.example: Refreshing state... [id=i-0db21cb267f702ce7]
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
private_ip = <sensitive>
public_ip = 18.191.93.62
เพื่อป้องกันการ apply โดยบังเอิญ และเพื่อให้สะดวกต่อการนำไปเขียน script เพื่อทำงานต่อไป สามารถใช้ terraform output
เพื่อดึงเฉพาะ output ออกมาแสดงโดยไม่ apply
$ terraform output
private_ip = <sensitive>
public_ip = 18.191.93.62
$ terraform output private_ip
172.31.45.142
$ terraform output public_ip
18.191.93.62
ทำลายสิ่งที่เราเพิ่งสร้างไป เตรียมตัวทำ cluster กัน
$ terraform destroy
(...)
Terraform will perform the following actions:
# aws_instance.example will be destroyed
- resource "aws_instance" "example" {
(...)
# aws_security_group.instance will be destroyed
- resource "aws_security_group" "instance" {
(...)
Plan: 0 to add, 0 to change, 2 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.example: Destroying... [id=i-0db21cb267f702ce7]
aws_instance.example: Still destroying... [id=i-0db21cb267f702ce7, 10s elapsed]
aws_instance.example: Still destroying... [id=i-0db21cb267f702ce7, 20s elapsed]
aws_instance.example: Still destroying... [id=i-0db21cb267f702ce7, 30s elapsed]
aws_instance.example: Destruction complete after 40s
aws_security_group.instance: Destroying... [id=sg-0d7d9b50531db13c5]
aws_security_group.instance: Destruction complete after 2s
Destroy complete! Resources: 2 destroyed.
Deploy a cluster of web servers
ถ้าเราจะทำ web ทั้งทีแล้วมีแค่ server เดียวคงจะเป็นเรื่องที่เสี่ยงไม่น้อย เพราะถ้าหากมันล่มขึ้นมาเป็นอันว่าจบกัน ดังนั้นเพื่อให้สมจริง เรามาสร้าง cluster ของ web server กัน แม้จะเป็น web hello world ก็เถอะ
ในการทำ cluster ใน AWS นั้น จะใช้ Auto Scaling Group (ASG) ในการ launch EC2 servers เพราะนอกจากจะ launch ได้หลาย servers ในครั้งเดียวแล้ว มันยัง ช่วย monitor health ของ servers และช่วย restart server ที่ fail หรือ unhealthy ให้ด้วย และที่ขาดไม่ได้ มันยังช่วยปรับขนาดของ cluster ตาม load (autoscaling) ด้วย
แต่ก่อนจะสร้าง ASG เราต้องสร้าง Launch Configuration ก่อน โดย Launch Configuration ทำหน้าที่เป็น template ที่ระบุ specification ในการ launch EC2 ของ ASG
จากห้วข้อก่อนหน้า เราสามารถนำข้อมูลมาเขียน specification ของ Launch Configuration ได้ดังนี้
resource "aws_launch_configuration" "example" {
image_id = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
security_groups = [aws_security_group.instance.id]
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p "${var.server_port}" &
EOF
lifecycle {
create_before_destroy = true
}
}
จาก configure ข้างบน มีของใหม่แค่ 1 อันคือ lifecycle settings ซึ่งเป็นตัวที่กำหนดวิธีในการ create และ destroy resource โดย default คือ ทำลายของเก่าก่อนแล้วค่อยสร้างใหม่ แต่การ set ให้ create_before_destroy = true
คือ สร้างให้เสร็จก่อนแล้วค่อยทำลายของเก่า
จากนั้น เรามาดู specification ของ ASG ที่เราจะให้ Terraform create ให้ เป็นดังนี้
data "aws_availability_zones" "all" {}
resource "aws_autoscaling_group" "example" {
launch_configuration = aws_launch_configuration.example.id
availability_zones = data.aws_availability_zones.all.names
min_size = 2
max_size = 10
tag {
key = "Name"
value = "terraform-asg-example"
propagate_at_launch = true
}
}
จาก specification ข้างบน ใน cluster นี้จะมี EC2 อยู่ 2 - 10 servers ขึ้นอยู่กับ traffic โดยให้ server run แยกในทุกๆ Available Zones (AZs) มากที่สุดเท่าที่จะทำได้ เพื่อที่ว่าถ้ามี AZ ไหน fail ไปจะได้ไม่กระทบ service ของเรา
โดย AZs คือ data center ของ AWS ที่แยกขาดออกจากกันทั้งทั้งน้ำ ไฟ และ ระบบทำความเย็น แต่อยู่ใน region เดียวกัน
แล้ว ทำไม data.aws_availability_zones.all.names
หมายถึง ทุก AZ ล่ะ ?
data คือ data source เป็นการดึงข้อมูลมาจาก provider (ในที่นี้คือ AWS) ในทุกครั้งที่ terraform ถูก run ตัวอย่าง data ที่สามารถดึงได้จาก providers ก็เช่น VPC data, subnet data, AMI IDs, IP address ranges, current user’s identity และ อื่นๆ อีกมากมาย
ในการใช้งานเราต้อง define มันก่อนดังนี้
Format
data "<PROVIDER>_<TYPE>" "<NAME>" {
[CONFIG ...]
}
Example
data "aws_availability_zones" "all" {}
โดย [CONFIG ...] ของ aws_availability_zones สามารถดูได้จาก doc
ส่วนการเข้าถึงจะเป็น format ดังนี้
Format: data.<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>
Result: data.aws_availability_zones.all.names
ดังนั้น เราสามารถสร้าง cluster ของ EC2 ด้วย ASG ดังนี้
-
สร้าง configuration file ใหม่ เพื่อสร้าง cluster
$ cd ~ && mkdir -p tf_02 && cd tf_02 $ cat > custer.tf << EOF provider "aws" { region = "us-east-2" } variable "server_port" { description = "The port the server will use for HTTP requests" type = number default = 8080 } resource "aws_launch_configuration" "example" { image_id = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" security_groups = [aws_security_group.instance.id] user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p "\${var.server_port}" & EOF lifecycle { create_before_destroy = true } } data "aws_availability_zones" "all" {} resource "aws_autoscaling_group" "example" { launch_configuration = aws_launch_configuration.example.id availability_zones = data.aws_availability_zones.all.names min_size = 2 max_size = 10 tag { key = "Name" value = "terraform-asg-example" propagate_at_launch = true } } resource "aws_security_group" "instance" { name = "terraform-example-instance" ingress { from_port = var.server_port to_port = var.server_port protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } EOF
-
ทำการสร้าง cluster
$ terraform init $ terraform apply (...) Terraform will perform the following actions: # aws_autoscaling_group.example will be created + resource "aws_autoscaling_group" "example" { + arn = (known after apply) + availability_zones = [ + "us-east-2a", + "us-east-2b", + "us-east-2c", ] (...) } # aws_launch_configuration.example will be created + resource "aws_launch_configuration" "example" { + arn = (known after apply) + associate_public_ip_address = false + ebs_optimized = (known after apply) + enable_monitoring = true + id = (known after apply) + image_id = "ami-0c55b159cbfafe1f0" + instance_type = "t2.micro" + key_name = (known after apply) + name = (known after apply) + security_groups = (known after apply) + user_data = "4430fd6498339061effa6d27ccf341a1e94569d7" (...) } # aws_security_group.instance will be created + resource "aws_security_group" "instance" { + arn = (known after apply) + description = "Managed by Terraform" + egress = (known after apply) + id = (known after apply) + ingress = [ + { + cidr_blocks = [ + "0.0.0.0/0", ] + description = "" + from_port = 8080 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + security_groups = [] + self = false + to_port = 8080 }, ] (...) } Plan: 3 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_security_group.instance: Creating... aws_security_group.instance: Creation complete after 9s [id=sg-022d3b1e15e652188] aws_launch_configuration.example: Creating... aws_launch_configuration.example: Creation complete after 5s [id=terraform-20200319075721931300000001] aws_autoscaling_group.example: Creating... aws_autoscaling_group.example: Still creating... [10s elapsed] aws_autoscaling_group.example: Still creating... [20s elapsed] aws_autoscaling_group.example: Still creating... [30s elapsed] aws_autoscaling_group.example: Still creating... [40s elapsed] aws_autoscaling_group.example: Still creating... [50s elapsed] aws_autoscaling_group.example: Still creating... [1m0s elapsed] aws_autoscaling_group.example: Still creating... [1m10s elapsed] aws_autoscaling_group.example: Still creating... [1m20s elapsed] aws_autoscaling_group.example: Still creating... [1m30s elapsed] aws_autoscaling_group.example: Still creating... [1m40s elapsed] aws_autoscaling_group.example: Still creating... [1m50s elapsed] aws_autoscaling_group.example: Still creating... [2m0s elapsed] aws_autoscaling_group.example: Still creating... [2m10s elapsed] aws_autoscaling_group.example: Still creating... [2m20s elapsed] aws_autoscaling_group.example: Still creating... [2m30s elapsed] aws_autoscaling_group.example: Still creating... [2m40s elapsed] aws_autoscaling_group.example: Still creating... [2m50s elapsed] aws_autoscaling_group.example: Creation complete after 2m54s [id=tf-asg-20200319075725933800000002] Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
-
ตรวจสอบ EC2 ที่เรา create ไป โดยกดที่ Instance
-
ตรวจสอบ Launch Configuration ที่เรา create ไป โดยกดที่ "Launch Configurations"
-
ตรวจสอบ Launch Configuration ที่เรา create ไป โดยกดที่ "Auto Scaling Groups"
เรียบร้อย! ต่อไป เรามาทำตัวที่ช่วย share load ไปยังทุก server ใน ASG ของเรา ที่ support การ auto scaling ของ ASG ด้วย กันต่อดีกว่า
Deploy a load balancer
ตอนนี้เรามีหลาย server เพื่อทำหน้าที่เป็น web server ละ ต่อมาสิ่งที่เราต้องทำเพิ่มคือ load balancer เพื่อช่วยแจก traffic ไปยังทุก servers ใน cluster ของเรา โดย ผู้ใช้งานจะได้ IP ของ load balancer ผ่านการ solve domain จาก DNS
เช่นเดิม เราจะใช้ Amazon’s Elastic Load Balancer (ELB) service ของ AWS ซึ่ง มี Auto Scaling และ สามารถทำการ failover ได้ โดย default โดย ELB มี 3 แบบ คือ
- Application Load Balancer (ALB): เหมาะสำหรับ HTTP และ HTTPS traffic
- Network Load Balancer (NLB): เหมาะสำหรับ TCP and UDP traffic
- Classic Load Balancer (CLB): เป็นแบบ legacy ใช้ได้ทั้ง HTTP, HTTPS และ TCP แต่มี feature น้อยกว่า ALB และ NLB
ในตัวอย่างนี้จะใช้ CLB เพราะ configure ง่ายกว่า โดย สามารถดูรายละเอียดเพิ่มเติมของ ELB ได้จาก doc
โดย configuration ของ ELB และ security group เป็นดังนี้
variable "elb_port" {
description = "The port the server will use for HTTP load balancer"
type = number
default = 80
}
resource "aws_elb" "example" {
name = "terraform-asg-example"
security_groups = [aws_security_group.elb.id]
availability_zones = data.aws_availability_zones.all.names
# This add a health checking for ASG
health_check {
target = "HTTP:${var.server_port}/"
interval = 30
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
# This adds a listener for incoming HTTP requests.
listener {
lb_port = var.elb_port
lb_protocol = "http"
instance_port = var.server_port
instance_protocol = "http"
}
}
resource "aws_security_group" "elb" {
name = "terraform-example-elb"
# Allow all outbound
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound HTTP from anywhere
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
จาก configure
- define ตัวแปร elb_port = 80
- define CLB โดยให้ listen port 80 (default) แล้ว share load ไปยัง port 8080 ของ instance ใน ASG
- CLB ของเรามีการ health check backend ด้วย ถ้าตัวไหน fail จะได้ไม่ส่ง traffic ไป
- เช่นเดียวกับ EC2 เราต้อง configure "security group" ให้ allow traffic ขาเข้าจาก port 80 ของ load balancer และ allow ขาออกทั้งหมด
เมื่อเราได้ CLB แล้ว เราต้องเพิ่ม configuration ของ ASG ให้ register ตัวเองไปยัง CLB เพื่อรับ traffic ด้วย ดังนี้
resource "aws_autoscaling_group" "example" {
launch_configuration = aws_launch_configuration.example.id
availability_zones = data.aws_availability_zones.all.names
min_size = 2
max_size = 10
# Register ASG ไปยัง CLB
load_balancers = [aws_elb.example.name]
health_check_type = "ELB"
tag {
key = "Name"
value = "terraform-asg-example"
propagate_at_launch = true
}
}
และเพื่อให้ได้ domain name ของ CLB เราต้อง เพิ่ม output ดังนี้
output "clb_dns_name" {
value = aws_elb.example.dns_name
description = "The domain name of the load balancer"
}
ดังนั้น เรามาสร้าง CLB ให้ ASG ของเรากัน ดังนี้
-
สร้าง configuration ใหม่ของ cluster.tf โดยเพิ่ม CLB, Security Group, ASG และ DNS output
$ cat > cluster.tf << EOF provider "aws" { region = "us-east-2" } variable "server_port" { description = "The port the server will use for HTTP requests" type = number default = 8080 } variable "elb_port" { description = "The port the server will use for HTTP load balancer" type = number default = 80 } resource "aws_launch_configuration" "example" { image_id = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" security_groups = [aws_security_group.instance.id] user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p "\${var.server_port}" & EOF lifecycle { create_before_destroy = true } } data "aws_availability_zones" "all" {} resource "aws_autoscaling_group" "example" { launch_configuration = aws_launch_configuration.example.id availability_zones = data.aws_availability_zones.all.names min_size = 2 max_size = 10 load_balancers = [aws_elb.example.name] health_check_type = "ELB" tag { key = "Name" value = "terraform-asg-example" propagate_at_launch = true } } resource "aws_security_group" "instance" { name = "terraform-example-instance" ingress { from_port = var.server_port to_port = var.server_port protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_elb" "example" { name = "terraform-asg-example" security_groups = [aws_security_group.elb.id] availability_zones = data.aws_availability_zones.all.names # This add a health checking for ASG health_check { target = "HTTP:\${var.server_port}/" interval = 30 timeout = 3 healthy_threshold = 2 unhealthy_threshold = 2 } # This adds a listener for incoming HTTP requests. listener { lb_port = var.elb_port lb_protocol = "http" instance_port = var.server_port instance_protocol = "http" } } resource "aws_security_group" "elb" { name = "terraform-example-elb" # Allow all outbound egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # Inbound HTTP from anywhere ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } output "clb_dns_name" { value = aws_elb.example.dns_name description = "The domain name of the load balancer" } EOF
-
ทำการ apply configuration
$ terraform apply (...) Terraform will perform the following actions: # aws_autoscaling_group.example will be updated in-place ~ resource "aws_autoscaling_group" "example" { (...) ~ health_check_type = "EC2" -> "ELB" id = "tf-asg-20200319075725933800000002" launch_configuration = "terraform-20200319075721931300000001" ~ load_balancers = [ + "terraform-asg-example", ] (...) # aws_elb.example will be created + resource "aws_elb" "example" { (...) + name = "terraform-asg-example" (...) + health_check { + healthy_threshold = 2 + interval = 30 + target = "HTTP:8080/" + timeout = 3 + unhealthy_threshold = 2 } + listener { + instance_port = 8080 + instance_protocol = "http" + lb_port = 80 + lb_protocol = "http" } } # aws_security_group.elb will be created + resource "aws_security_group" "elb" { + arn = (known after apply) + description = "Managed by Terraform" + egress = [ + { + cidr_blocks = [ + "0.0.0.0/0", ] + description = "" + from_port = 0 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "-1" + security_groups = [] + self = false + to_port = 0 }, ] + id = (known after apply) + ingress = [ + { + cidr_blocks = [ + "0.0.0.0/0", ] + description = "" + from_port = 80 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + security_groups = [] + self = false + to_port = 80 }, ] (...) Plan: 2 to add, 1 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_security_group.elb: Creating... aws_security_group.elb: Still creating... [10s elapsed] aws_security_group.elb: Creation complete after 11s [id=sg-0aa521374a7f5a184] aws_elb.example: Creating... aws_elb.example: Still creating... [11s elapsed] aws_elb.example: Creation complete after 19s [id=terraform-asg-example] aws_autoscaling_group.example: Modifying... [id=tf-asg-20200319075725933800000002] aws_autoscaling_group.example: Still modifying... [id=tf-asg-20200319075725933800000002, 10s elapsed] aws_autoscaling_group.example: Modifications complete after 11s [id=tf-asg-20200319075725933800000002] Apply complete! Resources: 2 added, 1 changed, 0 destroyed. Outputs: clb_dns_name = terraform-asg-example-1486800117.us-east-2.elb.amazonaws.com
-
ตรวจสอบ Load Balancers ที่เราเพิ่ม create ไป โดยกดที่ "Load Balancers"
-
ตรวจสอบ Security Group ที่เราเพิ่ม create ไป โดยกดที่ "Security Groups"
-
ลองเข้า web ตาม domain ที่เราได้มา
ทำลายทุกอย่างให้หมด
$ terraform destroy
(...)
Terraform will perform the following actions:
# aws_autoscaling_group.example will be destroyed
- resource "aws_autoscaling_group" "example" {
(...)
# aws_elb.example will be destroyed
- resource "aws_elb" "example" {
(...)
# aws_launch_configuration.example will be destroyed
- resource "aws_launch_configuration" "example" {
(...)
# aws_security_group.elb will be destroyed
- resource "aws_security_group" "elb" {
(...)
# aws_security_group.instance will be destroyed
- resource "aws_security_group" "instance" {
(...)
Plan: 0 to add, 0 to change, 5 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_autoscaling_group.example: Destroying... [id=tf-asg-20200319075725933800000002]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 10s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 20s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 30s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 40s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 50s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 1m0s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 1m10s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 1m20s elapsed]
aws_autoscaling_group.example: Still destroying... [id=tf-asg-20200319075725933800000002, 1m30s elapsed]
#aws_autoscaling_group.example: Destruction complete after 1m39s
aws_elb.example: Destroying... [id=terraform-asg-example]
aws_launch_configuration.example: Destroying... [id=terraform-20200319075721931300000001]
aws_launch_configuration.example: Destruction complete after 1s
aws_security_group.instance: Destroying... [id=sg-022d3b1e15e652188]
aws_security_group.instance: Destruction complete after 3s
aws_elb.example: Destruction complete after 5s
aws_security_group.elb: Destroying... [id=sg-0aa521374a7f5a184]
aws_security_group.elb: Still destroying... [id=sg-0aa521374a7f5a184, 10s elapsed]
aws_security_group.elb: Destruction complete after 16s
Destroy complete! Resources: 5 destroyed.
จบ แย้วววววว.... ยาวหน่อยนะ แต่ถ้าลองทำตามสนุกแน่นอน 🤯🤯🤯
Top comments (0)