[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:

vpc.tf:

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)
}

variables.tf:

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:

vpc.tf:

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)
}

variables.tf:

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:

sg.tf:

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"]
    }
  }
}

variables.tf

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:

https://www.infracost.io/

https://www.checkov.io/