[Terraform basics] một số demo đơn giản và 1 số lưu ý
1. Count
provider "aws" {
region = "us-east-2"
}
resource "aws_iam_user" "example" {
name = "neo"
}
Trường hợp muốn tạo 3 IAM user giống nhau, dưới đây là pseudo code:
for (i = 0; i < 3; i++) {
resource "aws_iam_user" "example" {
name = "neo"
}
}
Terraform không có for loop, thay vào đó khi muốn loop trong Terraform thì sẽ sử dụng count, for_each hoặc for expression (loop over lists and maps)
Chúng ta có thể triển khai trong Terraform như sau:
resource "aws_iam_user" "example" {
count = 3
name = "neo"
}
Nếu chỉ làm như trên thì vấn đề là 3 ông IAM users đều có chung tên, mà chúng ta muốn nó như này cơ:
# This is just pseudo code. It won't actually work in Terraform.
for (i = 0; i < 3; i++) {
resource "aws_iam_user" "example" {
name = "neo.${i}"
}
}
Để đạt được điều trên thì ta sửa Terraform như sau:
resource "aws_iam_user" "example" {
count = 3
name = "neo.${count.index}"
}
Khi ta thực hiện câu lệnh terraform plan thì sẽ được kết quả như sau:
Terraform will perform the following actions:
# aws_iam_user.example[0] will be created
+ resource "aws_iam_user" "example" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "neo.0"
+ path = "/"
+ unique_id = (known after apply)
}
# aws_iam_user.example[1] will be created
+ resource "aws_iam_user" "example" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "neo.1"
+ path = "/"
+ unique_id = (known after apply)
}
# aws_iam_user.example[2] will be created
+ resource "aws_iam_user" "example" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "neo.2"
+ path = "/"
+ unique_id = (known after apply)
}
Plan: 3 to add, 0 to change, 0 to destroy.
Tên IAM user là neo.o thì sẽ thường không readable. Chúng ta sẽ cần kết hợp count.index với một số built-in functions của Terraform. Chúng ta sẽ customize mỗi “iteration” của “loop” nhiều hơn.
Ví dụ, chúng ta sẽ define tất cả các IAM usernames chúng ta muốn ở input variables user_names:
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
Bây giờ mã giả sẽ như sau:
# This is just pseudo code. It won't actually work in Terraform.
for (i = 0; i < 3; i++) {
resource "aws_iam_user" "example" {
name = vars.user_names[i]
}
}
Lookup In Array trong Terraform:
Trong Terraform, chúng ta có thể làm được việc này bằng cách sử dụng count với 2 tricks:
Trick đầu tiên là sử dụng array lookup syntax:
LIST[<INDEX>]
Ví dụ:
var.user_names[1]
Trick thứ hai là sử dụng built-in functiuon length:
length(<LIST>)
Kết hợp cả 2 tricks trên, chúng ta sửa code Terraform như sau:
resource "aws_iam_user" "example" {
count = length(var.user_names)
name = var.user_names[count.index]
}
Bây giờ khi chạy terraform plan, chúng ta sẽ thấy Terraform sẽ tạo ra 3 IAM users, với các tên khác nhau (“neo”, “trinity”, “morpheus”)
Vì aws_iam_user.example hiện tại đã trở thành một list các IAM users, thay vì sử dụng syntax đọc attribute từ resource (<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>) thì giờ chúng ta sẽ phải chỉ định rõ là IAM user nào với syntax như sau:
<PROVIDER>_<TYPE>.<NAME>[INDEX].ATTRIBUTE
Ví dụ, nếu chúng ta muốn ARN của 1 IAM users trong output:
output "neo_arn" {
value = aws_iam_user.example[0].arn
description = "The ARN for user Neo"
}
Nếu chúng ta muốn của tất cả IAM users:
output "all_arns" {
value = aws_iam_user.example[*].arn
description = "The ARNs for all users"
}
Vấn đề với count:
Count có 2 hạn chế làm giảm mất tính hữu dụng của nó:
Thứ nhất, không thể sử dụng count trong 1 resource để loop over inline blocks.
Một inline block argument set bên trong 1 resource như sau:
resource "xxx" "yyy" {
<NAME> {
[CONFIG...]
}
}
Trong đó, NAME là tên của inline block, và CONFIG bao gồm một hoặc nhiều argument. Ví dụ:
resource "aws_autoscaling_group" "example" {
launch_configuration = aws_launch_configuration.example.name
vpc_zone_identifier = data.aws_subnet_ids.default.ids
target_group_arns = [aws_lb_target_group.asg.arn]
health_check_type = "ELB"
min_size = var.min_size
max_size = var.max_size
tag {
key = "Name"
value = var.cluster_name
propagate_at_launch = true
}
}
Mỗi tag yêu cầu chúng ta phải tạo 1 inline block với values key, value và propagate_at_launch
.
Chúng ta có thể muốn thử sử dụng count parameter để loop over các tags và tạo ra 1 dynamic inline tag block, nhưng rất tiếc, count trong một inline block thì không support!
Vấn đề thứ hai với count là những gì diễn ra khi chúng ta thay đổi. Xem xét list IAM user chúng ta đã tạo lúc trước:
variable "user_names" {
description = "Create IAM users with these names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
Tưởng tượng khi chúng ta bỏ “trinity” ra khỏi list, thì đây là những gì chúng ta thấy khi chạy terraform plan:
terraform plan
(...)
Terraform will perform the following actions:
# aws_iam_user.example[1] will be updated in-place
~ resource "aws_iam_user" "example" {
id = "trinity"
~ name = "trinity" -> "morpheus"
}
# aws_iam_user.example[2] will be destroyed
- resource "aws_iam_user" "example" {
- id = "morpheus" -> null
- name = "morpheus" -> null
}
Plan: 0 to add, 1 to change, 1 to destroy.
Thay vì xóa “trinity” thì lại rename trinity thành morpheus và xóa morpheus user.
Lý do là vì khi chúng ta sử dụng count với một resource, thì resource sẽ trở thành một list hoặc array các resource, nhưng Terraform lại identifies mỗi resource trong 1 array bằng index. Khi chúng ta thực hiện apply với 3 user names thì kết quả sẽ như sau:
aws_iam_user.example[0]: neo
aws_iam_user.example[1]: trinity
aws_iam_user.example[2]: morpheus
Khi chúng ta remove phần tử ở giữa một array, thì các phần tử đằng sau sẽ shift back lên 1 đơn vị. Do đó, khi chúng ta chạy plan với 2 user names thì sẽ như sau:
aws_iam_user.example[0]: neo
aws_iam_user.example[1]: morpheus
Chỉ khi các resources tạo ra gần như là giống nhau, thì count là phù hợp. Nhưng nếu arguments khác nhau thì hãy nghĩ đến việc sử dụng for_each.
Sử dụng count để tạo condition flag:
Chúng ta xem xét demo sau:
terraform {
backend "s3" {
bucket = "yen-poc-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
# Configure the AWS Provider
provider "aws" {
region = "ap-northeast-1"
}
# Create a VPC
resource "aws_vpc" "example" {
count = var.create ? 2: 0
cidr_block = var.cidr_block
}
resource "aws_subnet" "example" {
vpc_id = aws_vpc.example[0].id
availability_zone = var.availability_zone_a
cidr_block = cidrsubnet(aws_vpc.example[0].cidr_block, 4, 1)
}
resource "aws_subnet" "example1" {
vpc_id = aws_vpc.example[1].id
availability_zone = var.availability_zone_a
cidr_block = cidrsubnet(aws_vpc.example[1].cidr_block, 4ye, 1)
}
variable "cidr_block" {
default = "10.0.0.1/24"
}
variable "create" {
type = string
default = "false"
}
variable "availability_zone_a" {
default = "ap-northeast-1a"
}
terraform.tfvars:
cidr_block = "10.0.0.0/16"
create = "true"
Lúc này chúng ta có thể define để Terraform tạo hay không tạo VPC. Nếu chúng ta khai báo trong file tfvars create = “false” thì VPC sẽ không được tạo ra, nếu khai báo true sẽ có 2 VPC được tạo ra.
Bạn có thể xem thêm về count tại: https://learn.hashicorp.com/tutorials/terraform/count?in=terraform/configuration-language
2. Demo đơn giản về for_each:
terraform {
backend "s3" {
bucket = "yen-poc-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
# Configure the AWS Provider
provider "aws" {
region = "ap-northeast-1"
}
# Create a VPC
resource "aws_vpc" "example" {
for_each = var.project
cidr_block = each.value.cidr_block
}
resource "aws_subnet" "example" {
for_each = var.project
vpc_id = aws_vpc.example[each.key].id
availability_zone = each.value.availability_zone
cidr_block = cidrsubnet(aws_vpc.example[each.key].cidr_block, 4, 1)
}
variable "cidr_block" {
default = "10.0.0.1/24"
}
variable "vpc_id" {}
variable "project" {
type = map
default = {}
}
terraform.tfvars:
project = {
project_a = {
cidr_block = "10.0.0.0/16"
availability_zone = "ap-northeast-1b"
}
project_b = {
cidr_block = "10.0.0.0/24"
availability_zone = "ap-northeast-1c"
}
}
Kết quả khi plan:
PS D:\demo_terraform> terraform validate
Success! The configuration is valid.
PS D:\demo_terraform> terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_subnet.example["project_a"] will be created
+ resource "aws_subnet" "example" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "ap-northeast-1b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.16.0/20"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
# aws_subnet.example["project_b"] will be created
+ resource "aws_subnet" "example" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "ap-northeast-1c"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.0.16/28"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
# aws_vpc.example["project_a"] will be created
+ resource "aws_vpc" "example" {
+ arn = (known after apply)
+ cidr_block = "10.0.0.0/16"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = (known after apply)
+ enable_dns_support = true
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_network_border_group = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags_all = (known after apply)
}
# aws_vpc.example["project_b"] will be created
+ resource "aws_vpc" "example" {
+ arn = (known after apply)
+ cidr_block = "10.0.0.0/24"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = (known after apply)
+ enable_dns_support = true
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_network_border_group = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags_all = (known after apply)
}
Plan: 4 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
Bạn có thể xem thêm về for_each tại:
https://learn.hashicorp.com/tutorials/terraform/for-each?in=terraform/configuration-language
3. Loop với for expression:
Chúng ta có đoạn code như sau:
variable "names" {
description = "A list of names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
Bài toán đặt ra là làm thế nào để có thể covert hết tất cả các tên này thành dạng upper case? Nếu sử dụng Python chúng ta có thể làm như sau:
names = ["neo", "trinity", "morpheus"]
upper_case_names = []
for name in names:
upper_case_names.append(name.upper())
print upper_case_names
# Prints out: ['NEO', 'TRINITY', 'MORPHEUS']
hoặc:
names = ["neo", "trinity", "morpheus"]
upper_case_names = [name.upper() for name in names]
print upper_case_names
# Prints out: ['NEO', 'TRINITY', 'MORPHEUS']
hoặc:
names = ["neo", "trinity", "morpheus"]
short_upper_case_names = [name.upper() for name in names if len(name) < 5]
print short_upper_case_names
# Prints out: ['NEO']
Terraform cũng offer cho chúng ta chức năng tương tự: for expression. Basic syntax như sau:
[for <ITEM> in <LIST> : <OUTPUT>]
Trong đó LIST là list cần loop over, ITEM là local variable name assign cho mỗi item trong LIST, OUTPUT là một expression transforms ITEM. Ví dụ, chúng ta conver list các tên trong biến var.names thành dạng upper case trong Terraform như sau:
variable "names" {
description = "A list of names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
output "upper_names" {
value = [for name in var.names : upper(name)]
}
Nếu chạy terraform apply sẽ được kết quả như sau:
$ terraform apply
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
upper_names = [
"NEO",
"TRINITY",
"MORPHEUS",
]
Tương tự như trong Python, bạn cũng có thể filter kết quả bằng cách chỉ định một condition:
variable "names" {
description = "A list of names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}
output "short_upper_names" {
value = [for name in var.names : upper(name) if length(name) < 5]
}
Khi chạy terraform apply thì sẽ được kết quả như sau:
short_upper_names = [
"NEO",
]
Terraform cũng cho phép loop over một map với syntax như sau:
[for <KEY>, <VALUE> in <MAP> : <OUTPUT>]
Trong đó, MAP là map để loop over, KEY và VALUE là local variable names assign cho mỗi cặp key-value in MAP, và OUTPUT là expression transforms KEY và VALUE. Ví dụ:
variable "hero_thousand_faces" {
description = "map"
type = map(string)
default = {
neo = "hero"
trinity = "love interest"
morpheus = "mentor"
}
}
output "bios" {
value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}
Khi run terraform apply sẽ được kết quả như sau:
map_example = [
"morpheus is the mentor",
"neo is the hero",
"trinity is the love interest",
]
Bạn còn có thể sử dụng for expression như sau:
# For looping over lists
{for <ITEM> in <LIST> : <OUTPUT_KEY> => <OUTPUT_VALUE>}
# For looping over maps
{for <KEY>, <VALUE> in <MAP> : <OUTPUT_KEY> => <OUTPUT_VALUE>}
Ví dụ:
variable "hero_thousand_faces" {
description = "map"
type = map(string)
default = {
neo = "hero"
trinity = "love interest"
morpheus = "mentor"
}
}
output "upper_roles" {
value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}
Đây là kết quả:
upper_roles = {
"MORPHEUS" = "MENTOR"
"NEO" = "HERO"
"TRINITY" = "LOVE INTEREST"
}
4. Demo đơn giản về data
terraform {
backend "s3" {
bucket = "yen-poc-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
# Configure the AWS Provider
provider "aws" {
region = "ap-northeast-1"
}
data "aws_vpc" "selected" {
id = var.vpc_id
}
resource "aws_subnet" "example" {
vpc_id = data.aws_vpc.selected.id
availability_zone = "ap-northeast-1a"
cidr_block = cidrsubnet(data.aws_vpc.selected.cidr_block, 4, 1)
}
⇒ một ví dụ về state đặt ở S3, và sử dụng data chứ không khai báo resource
Use case ở đây là: Đã có cái VPC rồi, chỉ muốn tạo và quản lý subnet của VPC đó trong state của Terraform thôi
5. Demo đơn giản về locals:
Ban đầu:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.64.0"
name = "vpc-${var.resource_tags["project"]}-${var.resource_tags["environment"]}" #before use locals
cidr = var.vpc_cidr_block
azs = data.aws_availability_zones.available.names
private_subnets = slice(var.private_subnet_cidr_blocks, 0, var.private_subnet_count)
public_subnets = slice(var.public_subnet_cidr_blocks, 0, var.public_subnet_count)
enable_nat_gateway = true
enable_vpn_gateway = var.enable_vpn_gateway
tags = var.resource_tags
}
module "app_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "3.17.0"
name = "web-sg-${var.resource_tags["project"]}-${var.resource_tags["environment"]}" #before use locals
description = "Security group for web-servers with HTTP ports open within VPC"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = module.vpc.public_subnets_cidr_blocks
tags = var.resource_tags #tags before use local block
}
Có thể thấy giá trị name và tag đang bị trùng lặp
Khai báo thêm 2 block locals để refactor:
locals {
required_tags = {
project = var.project_name,
environment = var.environment
}
tags = merge(var.resource_tags, local.required_tags)
}
locals {
name_suffix = "${var.project_name}-${var.environment}" # locals block after use with variables
}
Code sau khi refactor:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.64.0"
name = "vpc-${local.name_suffix}" # after use locals
cidr = var.vpc_cidr_block
azs = data.aws_availability_zones.available.names
private_subnets = slice(var.private_subnet_cidr_blocks, 0, var.private_subnet_count)
public_subnets = slice(var.public_subnet_cidr_blocks, 0, var.public_subnet_count)
enable_nat_gateway = true
enable_vpn_gateway = var.enable_vpn_gateway
tags = local.tags # tags after use local block
}
module "app_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "3.17.0"
name = "web-sg-${local.name_suffix}" # after use locals
description = "Security group for web-servers with HTTP ports open within VPC"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = module.vpc.public_subnets_cidr_blocks
tags = local.tags # tags after use local block
}
Kết quả apply sau refactor:
6. Demo đơn giản về lifecycle:
provider "aws" {
profile = "default"
region = "us-east-1"
}
resource "aws_instance" "server" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "my-server"
}
lifecycle {
prevent_destroy = true
}
}
output "public_ip" {
value = aws_instance.my_server[*].public_ip
}
Ngoài ra, có 1 use case cũng hay được dùng:
resource "aws_route53_zone" "private_zone" {
name = "${var.env_name}.${var.app_name}.local"
comment = "${var.env_name}.${var.app_name}.local"
vpc {
vpc_id = aws_vpc.vpc.id
vpc_region = var.region
}
lifecycle {
ignore_changes = [vpc]
}
}
7. Alias
Case hay dùng: Khi khai báo nhiều provider
provider "aws" {
profile = "default"
region = "us-east-1"
}
provider "aws" {
profile = "default"
region = "us-east-2"
alias = "east"
}
provider "aws" {
profile = "default"
region = us-west-1"
alias = "west"
}
data "aws_ami" "amazon-linux-2" {
most_recent = true
owners = ["amazon"]
filter {
name = "owner-alias"
value = ["amazon"]
}
filter {
name = "name"
value = ["amzn2-ami-hvm*"]
}
}
resource "aws_instance" "my_east_server" {
ami = "${data.aws_ami.amazon-linux-2.id}"
instance_type = "t2.micro"
provider = aws.east
tags = {
Name = "server-east"
}
}
resource "aws_instance" "my_west_server" {
ami = "${data.aws_ami.amazon-linux-2.id}"
instance_type = "t2.micro"
provider = aws.west
tags = {
Name = "server-west"
}
}
8. depends_on:
9. local-exec
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo ${self.private_ip} >> private_ips.txt"
}
}
Khi chỉ muốn chạy câu lệnh mà không muốn tạo ra 1 resource nào đó thì ta sẽ sử dụng null_resource:
resource "null_resource" "example2" {
provisioner "local-exec" {
command = "Get-Date > completed.txt"
interpreter = ["PowerShell", "-Command"]
}
}
interpreter: nếu không khai báo thì sensible defaults sẽ được chọn dựa vào system OS.
10. remote-exec
Lưu ý quan trọng: remote-exec phải có connection block!
resource "aws_instance" "web" {
# ...
# Establishes connection to be used by all
# generic remote provisioners (i.e. file/remote-exec)
connection {
type = "ssh"
user = "root"
password = var.root_password
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"puppet apply",
"consul join ${aws_instance.web.private_ip}",
]
}
}
Bạn sẽ không thể pass vào 1 argument khi sử dụng script hoặc scripts, nếu muốn specify arguments, upload script bằng file provisioner và sau đó sử dụng inline để call:
resource "aws_instance" "web" {
# ...
provisioner "file" {
source = "script.sh"
destination = "/tmp/script.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/script.sh",
"/tmp/script.sh args",
]
}
}
11. file
Sử dụng để copy files hoặc directories từ máy chạy Terraform lên resource mới tạo. file support cả ssh và winrm
resource "aws_instance" "web" {
# ...
# Copies the myapp.conf file to /etc/myapp.conf
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
# Copies the string in content into /tmp/file.log
provisioner "file" {
content = "ami used: ${self.ami}"
destination = "/tmp/file.log"
}
# Copies the configs.d folder to /etc/configs.d
provisioner "file" {
source = "conf/configs.d"
destination = "/etc"
}
# Copies all files and folders in apps/app1 to D:/IIS/webapp1
provisioner "file" {
source = "apps/app1/"
destination = "D:/IIS/webapp1"
}
}
12. archive
sử dụng để archive 1 file:
data "archive_file" "ec2_start" {
type = "zip"
source_file = "lambda/ec2_start/lambda_function.py"
output_path = "lambda/upload/ec2_start.zip"
}
resource "aws_lambda_function" "ec2_start" {
filename = data.archive_file.ec2_start.output_path
function_name = "ec2-start"
role = aws_iam_role.lambda_role.arn
handler = var.handler
source_code_hash = data.archive_file.ec2_start.output_base64sha256
runtime = var.runtime
environment {
variables = {
auto = var.env_name
}
}
memory_size = var.memory_size
timeout = var.timeout
}
13. Dynamic block – demo đơn giản
Dynamic block là một block đặc biệt của Terraform cung cấp chức năng của for expression bằng cách tạo ra nhiều nested blocks
Ban đầu:
# Security Groups
resource “aws_security_group” “sg-webserver” {
vpc_id = aws_vpc.vpc.id
name = “webserver”
description = “Security Group for Web Servers”
ingress {
protocol = “tcp”
from_port = 80
to_port = 80
cidr_blocks = [ “0.0.0.0/0” ]
}
ingress {
protocol = “tcp”
from_port = 443
to_port = 443
cidr_blocks = [ “0.0.0.0/0” ]
}
egress {
protocol = “tcp”
from_port = 443
to_port = 443
cidr_blocks = [ var.vpc-cidr ]
}
egress {
protocol = “tcp”
from_port = 1433
to_port = 1433
cidr_blocks = [ var.vpc-cidr ]
}
}
Có nhiều sự trùng lặp ở ingress và egress rule -> sử dụng dynamic blocks để refactor:
locals {
inbound_ports = [80, 443]
outbound_ports = [443, 1433]
}
# Security Groups
resource “aws_security_group” “sg-webserver” {
vpc_id = aws_vpc.vpc.id
name = “webserver”
description = “Security Group for Web Servers”
dynamic “ingress” {
for_each = local.inbound_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = “tcp”
cidr_blocks = [ “0.0.0.0/0” ]
}
}
dynamic “egress” {
for_each = local.outbound_ports
content {
from_port = egress.value
to_port = egress.value
protocol = “tcp”
cidr_blocks = [ var.vpc-cidr ]
}
}
}
Sử dụng dynamic với 1 list:
locals {
db_instance = “10.0.32.50/32”
inbound_ports = [80, 443]
outbound_rules = [{
port = 443,
cidr_blocks = [ var.vpc-cidr ]
},{
port = 1433,
cidr_blocks = [ local.db_instance ]
}]
}
# Security Groups
resource “aws_security_group” “sg-webserver” {
vpc_id = aws_vpc.vpc.id
name = “webserver”
description = “Security Group for Web Servers”
dynamic “ingress” {
for_each = local.inbound_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = “tcp”
cidr_blocks = [ “0.0.0.0/0” ]
}
}
dynamic “egress” {
for_each = local.outbound_rules
content {
from_port = egress.value.port
to_port = egress.value.port
protocol = “tcp”
cidr_blocks = egress.value.cidr_blocks
}
}
}
Bạn cũng có thể viết như sau:
locals {
outer_elb_sg_ingress_cidr = {
nat_gateway_eip = "${aws_eip.nat_gateway_eip.public_ip}/32"
}
}
resource "aws_security_group" "outer_elb_sg" {
count = length(var.outer_elb)
name = "${element(var.outer_elb, count.index)}-sg"
vpc_id = aws_vpc.vpc.id
description = "${element(var.outer_elb, count.index)}-sg"
dynamic "egress" {
for_each = var.security_group_common_egress
content {
from_port = egress.value["from_port"]
to_port = egress.value["to_port"]
protocol = egress.value["protocol"]
cidr_blocks = egress.value["cidr_blocks"]
description = egress.value["description"]
}
}
dynamic "ingress" {
for_each = var.outer_elb_sg_cidr_ingress
content {
from_port = ingress.value["from_port"]
to_port = ingress.value["to_port"]
protocol = ingress.value["protocol"]
cidr_blocks = ingress.value["cidr_blocks"]
description = ingress.value["description"]
}
}
dynamic "ingress" {
for_each = var.outer_elb_sg_cidr_nat_ingress
content {
from_port = ingress.value["from_port"]
to_port = ingress.value["to_port"]
protocol = ingress.value["protocol"]
cidr_blocks = [local.outer_elb_sg_ingress_cidr[ingress.value["cidr_blocks"]]]
description = ingress.value["description"]
}
}
}
variable "security_group_common_egress" {
default = {}
}
variable "outer_elb_sg_cidr_ingress" {
default = {}
}
variable "outer_elb_sg_cidr_nat_ingress" {
default = {}
}
terraform.tfvars
security_group_common_egress = {
security_group_common_egress_001 = {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "all"
}
}
outer_elb_sg_cidr_ingress = {
outer_elb_sg_cidr_ingress_001 = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["xxx.xxx.xxx.xxx/32"]
description = "demo-ingress-1"
}
outer_elb_sg_cidr_ingress_002 = {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["xxx.xxx.xxx.xxx/32"]
description = "demo-ingress-2"
}
}
outer_elb_sg_cidr_nat_ingress = {
outer_elb_sg_cidr_nat_ingress_001 = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = "nat_gateway_eip"
description = "nat gateway"
}
outer_elb_sg_cidr_nat_ingress_002 = {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = "nat_gateway_eip"
description = "nat gateway"
}
}
14. Một số lưu ý khi sử dụng Terraform
Lưu ý:
locals {
outer_elb_sg_ingress_cidr = {
nat_gateway_eip = "${aws_eip.nat_gateway_eip.public_ip}/32"
}
}
dynamic "ingress" {
for_each = var.outer_elb_sg_cidr_nat_ingress
content {
from_port = ingress.value["from_port"]
to_port = ingress.value["to_port"]
protocol = ingress.value["protocol"]
cidr_blocks = [local.outer_elb_sg_ingress_cidr[ingress.value["cidr_blocks"]]]
description = ingress.value["description"]
}
}
outer_elb_sg_cidr_nat_ingress = {
outer_elb_sg_cidr_nat_ingress_001 = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = "nat_gateway_eip"
description = "nat gateway"
}
outer_elb_sg_cidr_nat_ingress_002 = {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = "nat_gateway_eip"
description = "nat gateway"
}
}
locals ở đây sẽ đúng hơn data, nếu chỉ có 1 ingress thì mới nên sử dụng data!
Nếu sử dụng Terraform đặt state ở S3, thì cần lưu ý: tên file là terrform.tfstate, nếu apply nhiều môi trường cùng tên thế này thì state sẽ bị overwrite, mà ở backend block lại không thể nào refer tới named value (like input variables, locals, or data source attributes).
Workaround:
terraform init -reconfigure -backend-config="key=${app}/${env}-terraform.tfstate"
Một số tool hữu ích: