Terraform Study #2

오늘은 데이터소스와 변수 그리고 반복문에 대해 진행할 예정이다.

1. Data Source(도전과제 #1 포함)

Terraform에서 Data Source는 구성된 제공자(provider)에서 읽을 수 있는 데이터를 나타낸다. Data Source는 이미 존재하는 리소스에 대한 정보를 조회하거나 외부와 통신하여 정보를 얻을 수도 있다. 이 정보는 다른 리소스를 새엉하거나 구성할 때 사용할 수 있다.
예를 들어, AWS에서 실행 중인 EC2 인스턴스의 정보를 얻으려면 aws_instance Data Source을 사용해서 얻을 수 있다.

Data Source는 data 블록을 사용한다. 미리 배포한 테스트 목적의 EC2에 대한 Public IP을 출력하는 내용을 진행해봤다.

data "aws_instance" "example" {
  instance_id = "i-12345678"
}

output "instance_public_ip" {
  value = data.aws_instance.example.public_ip
}

#초기화 및 plan 진행
terraform init
terraform plan

EC2 ID에 맞는 Public IP을 호출하는 것을 확인할 수 있다.

간단하게 Data Source을 사용해봤다면 도전과제를 진행해보도록 한다.
해당 내용을 진행하기 위한 과정을 간단하게 정리하면 다음과 같다.
1. AWS 서울 리전에서 사용 가능한 가용 영역을 Data Source을 통해 가져온다.
2. 가져온 가용 영역을 사용하여 VPC 내 Subnet을 생성

위 내용을 아래 코드로 작성하였다.
사용 가능한 가용 영역 목록을 Data Source을 통해 불러오고 VPC IP CIdr을 10.0.0./16으로 선택한 후 서브넷을 각각 /24로 생성하는 과정이다.

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "ybs-c1-vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "ybs-c1-vpc"
  }
}

resource "aws_subnet" "ybs-c1-vpc-subnet" {
  count = length(data.aws_availability_zones.available.names)

  cidr_block = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  vpc_id = aws_vpc.ybs-c1-vpc.id

  tags = {
    Name = "ybs-c1-vpc-subnet-${count.index}"
  }
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

VPC와 서브넷 4개가 잘 생성된 것을 볼 수 있다.

콘솔에서도 확인했을 때 가용 영역이 a~d까지 잘 생성된 것으로 보인다.

graph 생성을 통해 본 로직은 아래와 같다.

2. 입력 변수 Variable

Terraform에서 입력 변수(Variable)는 Terraform 구성에 값을 주입하는 방법을 제공한다. 이는 동적인 구성을 가능하게 하고 동일한 코드를 다양한 환경 또는 조건에 재사용할 수 있게 해준다. 변수는 var 식별자를 통해 참조할 수 있고 구성 내에서 다양한 리소스의 출력에 사용 된다.

변수를 선언하기 위해 variable 블록을 사용한다. 이 블록에는 변수의 이름, 타입과 기본값 그리고 설명 등을 포함한다.

간단하게 작성을 하면 아래와 같다. region이라는 변수를 선언하고 provider 블록에서 var.region을 통해 region 변수 값을 호출할 수 있다.

variable "region" {
  description = "The AWS region to deploy into"
  default     = "ap-northeast-2"
  type        = string
}

provider "aws" {
  region = var.region
}

아무 변수 이름이나 사용할 수 있는 것은 아니고 Terraform에서 예약 변수로 사용하고 있는 변수들은 사용할 수 없다.(source, version, providers, count, for_each, lifecycle, depends_on, locals 등)

변수 타입은 아래와 같다.

String : 텍스트 값
Number : 숫자
Bool : true/false
List([type]) : 같은 유형의 여러 값
Set([type]) : 유일한 값들의 집합
Map([type]) : 키-값 쌍의 집합
Object({ key: type, … }) : 여러 필드를 가진 객체
Tuple([type, …]) : 여러 유형을 가진 리스트

3. VPC/SG/EC2 배포(Data Source+변수 사용, 도전과제 #2, #3, #4)

Data Source와 변수를 사용해서 VPC/SG/EC2를 배포하는 실습을 진행해봤다.

우선 vpc.tf을 통해 vpc와 Subnet, Route Table 그리고 IGW를 생성했다.
가용 영역의 경우 az1, az3라는 변수를 통해 서브넷 생성 시 변수 호출할 수 있게 작성했다.
IGW로 라우팅을 할 수 있도록 Route Table을 만들고 2개의 Subnet에 연결해준 뒤 0.0.0.0/0에 대해 IGW로 라우팅할 수 있는 Route을 등록해줬다.

variable "region" {
  description = "The AWS region to deploy into"
  default     = "ap-northeast-2"
  type        = string
}

variable "az1" {
  description = "The availability zone a"
  default     = "ap-northeast-2a"
  type        = string
}

variable "az3" {
  description = "The availability zone c"
  default     = "ap-northeast-2c"
  type        = string
}

provider "aws" {
  region  = var.region
}

resource "aws_vpc" "ybs-vpc" {
  cidr_block       = "10.10.0.0/16"

  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "ybs-vpc"
  }
}

resource "aws_subnet" "ybs-subnet1" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = var.az1

  tags = {
    Name = "ybs-subnet1"
  }
}

resource "aws_subnet" "ybs-subnet2" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = var.az3

  tags = {
    Name = "ybs-subnet2"
  }
}

resource "aws_internet_gateway" "ybs-igw" {
  vpc_id = aws_vpc.ybs-vpc.id

  tags = {
    Name = "ybs-igw"
  }
}

resource "aws_route_table" "ybs-rt1" {
  vpc_id = aws_vpc.ybs-vpc.id

  tags = {
    Name = "ybs-rt1"
  }
}

resource "aws_route_table_association" "ybs-rt1-asso1" {
  subnet_id      = aws_subnet.ybs-subnet1.id
  route_table_id = aws_route_table.ybs-rt1.id
}

resource "aws_route_table_association" "ybs-rt1-asso2" {
  subnet_id      = aws_subnet.ybs-subnet2.id
  route_table_id = aws_route_table.ybs-rt1.id
}

resource "aws_route" "ybs-rt1-drt" {
  route_table_id         = aws_route_table.ybs-rt1.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.ybs-igw.id
}

output "aws_vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

보안그룹은 특별한 내용 없이 지난 주 실습 내용을 토대로 Egress Any와 Ingress 80 Port에 대한 허용 설정을 진행해줬다. 여기서도 Ingress 80 Port와 Any Cidr Block는 변수로 설정해서 추후 SG가 추가될 때 참조할 수 있도록 했다.

variable "http" {
  description = "The HTTP port"
  default     = 80
  type        = number
}

variable "anycidr" {
  description = "The any cidr block"
  default     = ["0.0.0.0/0"]
  type        = list
}

resource "aws_security_group" "ybs-sg1" {
  vpc_id      = aws_vpc.ybs-vpc.id
  name        = "YBS SG 1"
  description = "YBS Study SG 1"
}

resource "aws_security_group_rule" "ybs-in-http-rule" {
  type              = "ingress"
  from_port         = var.http
  to_port           = var.http
  protocol          = "tcp"
  cidr_blocks       = var.anycidr
  security_group_id = aws_security_group.ybs-sg1.id
}

resource "aws_security_group_rule" "ybs-out-any-rule" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = var.anycidr
  security_group_id = aws_security_group.ybs-sg1.id
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_security_group.ybs-sg1
aws_security_group_rule.ybs-in-http-rule
aws_security_group_rule.ybs-out-any-rule
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

앞서 생성한 VPC와 SG을 참조해서 EC2를 생성하도록 한다. 해당 EC2에는 Data Source을 통해 ami id을 참조하도록 한다. 그리고 아래 다룰 locals을 사용해서 ec2 이름을 입력하도록 했다.
배포를 하고 Public IP을 통해 접속 테스트까지 진행했다.

data "aws_ami" "ybs-aml2id" {
  most_recent = true
  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-ebs"]
  }

  owners = ["amazon"]
}

locals {
  ec2name = {
    Name = "ybs-ec2"
  }
}


resource "aws_instance" "ybs-ec2" {

  depends_on = [
    aws_internet_gateway.ybs-igw
  ]

  ami                         = data.aws_ami.ybs-aml2id.id
  associate_public_ip_address = true
  instance_type               = "t2.micro"
  vpc_security_group_ids      = ["${aws_security_group.ybs-sg1.id}"]
  subnet_id                   = aws_subnet.ybs-subnet1.id

  user_data = <<-EOF
              #!/bin/bash
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
              IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
              LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
              echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

  user_data_replace_on_change = true

  tags = local.ec2name
}

output "ybs-ec2-publicip" {
  value       = aws_instance.ybs-ec2.public_ip
  description = "The public IP of the Instance"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
data.aws_ami.ybs-aml2id
aws_instance.ybs-ec2
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_security_group.ybs-sg1
aws_security_group_rule.ybs-in-http-rule
aws_security_group_rule.ybs-out-any-rule
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

# 데이터소스 값 확인
terraform console
> data.aws_ami.ybs-aml2id
{
  "architecture" = "x86_64"
  "arn" = "arn:aws:ec2:ap-northeast-2::image/ami-05749a4578e04e5ac"
  "block_device_mappings" = toset([
...
}

# 출력된 EC2 퍼블릭IP로 cul 접속 확인
terraform output -raw ybs-ec2-publicip
MYIP=$(terraform output -raw ybs-ec2-publicip)
while true; do curl --connect-timeout 1  http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done
2023년 9월  8일 금요일 17시 48분 27초 KST
<h1>RegionAz(apne2-az1) : Instance ID(i-06633af887ed4a47a) : Private IP(10.10.1.84) : Web Server</h1>
------------------------------
2023년 9월  8일 금요일 17시 48분 28초 KST
<h1>RegionAz(apne2-az1) : Instance ID(i-06633af887ed4a47a) : Private IP(10.10.1.84) : Web Server</h1>
------------------------------

접속이 잘 되는 것과 내가 설정한 AZ를 포함해서 Private IP까지 호출되는 화면을 확인할 수 있다.

4. local 지역 값

Terraform에서 local 지역 값은 구성 내에서 변수처럼 사용 되지만, 입력이나 출력으로 사용되지 않는 중간 계산 결과를 저장할 수 있다. Terraform 코드 내에서 재사용될 수 있는 값들을 저장하는데 사용 된다고 보면 된다. locals 블록 내에서 하나 이상의 로컬 값을 정의하여 사용할 수 있다.

local값 정의와 사용은 아래와 같이 쓸 수 있다.
아래와 같은 값을 사용하면 간단하게 resource name을 설정할 수 있다.

locals {
  calculated_value = var.some_input * 10
  name_prefix      = "dev-"
  full_name        = "${local.name_prefix}${var.resource_name}"
}

resource "aws_instance" "example" {
  tags = {
    Name = local.full_name
  }
}

간단한 실습 예제를 통해 테스트를 진행해봤다. iamuser의 이름을 locals 블록에 넣고 실행한 내용이다.

provider "aws" {
  region = "ap-northeast-2"
}

locals {
  name = "mytest"
  team = {
    group = "dev"
  }
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
  tags = local.team
}

resource "aws_iam_user" "myiamuser2" {
  name = "${local.name}2"
  tags = local.team
}

#
terraform init && terraform apply -auto-approve
terraform state list
aws_iam_user.myiamuser1
aws_iam_user.myiamuser2
terraform state show aws_iam_user.myiamuser1
resource "aws_iam_user" "myiamuser1" {
    arn           = "arn:aws:iam::xxxxxxxxxx:user/mytest1"
    force_destroy = false
    id            = "mytest1"
    name          = "mytest1"
....
}

5. 반복문

Terraform에서 반복문은 크게 4가지 정도가 있다. count, for, for_each, dynamic이다.

Count
count는 리소스 또는 모듈 블록에서 사용되며, 동일한 형태의 리소스를 여러 개 생성할 수 있다. count.index를 통해 현재 반복의 인덱스에 접근할 수 있다.
아래 예제를 실행시키면 3개의 AWS EC2를 생성하며 각 EC2의 이름에 index 번호를 붙일 수 있다.

resource "aws_instance" "example" {
  count = 3

  ami           = "ami-xxxxxxxxxxxxxx"
  instance_type = "t2.micro"

  tags = {
    Name = "example-instance-${count.index}"
  }
}

위의 예와 같이 count를 넣은 숫자 만큼 반복하며 그 count 숫자의 index 값을 tags 등에 활용할 수 있는 것을 볼 수 있다.

For_each
for_each는 count와 비슷하지만, 목록 또는 맵에 대해 반복을 실행한다. for_each를 사용하면 생성된 리소스에 더 의미 있는 이름을 부여할 수 있고, 특정 리소스에만 변경을 적용할 수 있다.
아래 코드를 실행시키면 로컬의 user_map을 기반으로 AWS IAM 사용자를 생성한다. each.key는 alice, bob 등의 사용자 이름을, each.value는 admin, developer 등의 역할을 나타낸다. 이렇게 for_each을 사용해서 tag을 입력하는 용도로 사용할 수 있다.

locals {
  user_map = {
    alice = "admin"
    bob   = "developer"
  }
}

resource "aws_iam_user" "example" {
  for_each = local.user_map

  name = each.key
  tags = {
    Role = each.value
  }
}

For 표현식
for 표현식은 목록, 맵, 집합 등에 대한 반복을 수행할 수 있다. 이는 리소스 블록 외부에서 리스트나 맵을 생성할 때 주로 사용된다.
아래 코드는 숫자 목록 [1, 2, 3, 4]에 대해 제곱을 구하고, 이를 맵 형태로 저장한다. 출력 결과는 {“1” = 1, “2” = 4, “3” = 9, “4” = 16}과 같이 출력 된다.

locals {
  numbers    = [1, 2, 3, 4]
  square_map = { for num in local.numbers : tostring(num) => num * num }
}

output "square_map" {
  value = local.square_map
}

dynamic
dynamic 블록은 복잡한 객체의 반복을 처리하기 위해 사용된다. 대부분의 경우, 리소스의 중첩된 블록(예: AWS 보안 그룹 규칙, VPC 서브넷 등)을 동적으로 생성할 때 유용하게 쓰인다. dynamic 블록을 사용하면 코드 중복을 줄이고, 조건부 또는 반복적인 구성을 쉽게 할 수 있습다.
dynamic 블록 내에서 for_each나 count 같은 반복문을 사용하여 여러 개의 중첩된 블록을 생성할 수 있으며, content 블록을 사용하여 실제로 생성될 내용을 정의한다.

아래 코드에서 dynamic “ingress” 블록은 for_each를 통해 여러 인그레스 규칙을 동적으로 생성한다. content 블록 내에서는 실제 규칙에 대한 세부 정보를 지정한다.

resource "aws_security_group" "example" {
  name        = "example"
  description = "Example security group"

  dynamic "ingress" {
    for_each = [
      {
        from_port   = 22
        to_port     = 22
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
      },
      {
        from_port   = 80
        to_port     = 80
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
      }
    ]
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

간단하게 반복문에 대해 알아봤고 반복문을 사용한 실습을 진행해보도록 한다.

count부터 실습을 진행해봤다. iam user을 count 입력 받아 반복해서 생성하는 실습이다.
iam user을 생성하고 console 명령어를 통해 user 정보까지 확인해보았다.

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  count = 3
  name  = "myuser.${count.index}"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

terraform console
> aws_iam_user.myiam[0]
{
  "arn" = "arn:aws:iam::xxxxxxxxxxxxx:user/myuser.0"
  "force_destroy" = false
  "id" = "myuser.0"
  "name" = "myuser.0"
  "path" = "/"
  "permissions_boundary" = tostring(null)
  "tags" = tomap(null) /* of string */
  "tags_all" = tomap({})
  "unique_id" = "xxxxxxxxxxxxxxxxxxxx"
}
> aws_iam_user.myiam[0].name
"myuser.0"

#삭제
terraform destroy -auto-approve

이번에는 위와 같이 count을 사용하지만 입력변수를 받아 iam user 이름을 정의하도록 한다.
iam user을 count 길이(여기서는 3) 만큼 만들고 각 사용자의 arn을 출력하도록 한다.

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["gasida", "akbun", "ssoon"]
}

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  count = length(var.user_names)
  name  = var.user_names[count.index]
}

output "first_arn" {
  value       = aws_iam_user.myiam[0].arn
  description = "The ARN for the first user"
}

output "all_arns" {
  value       = aws_iam_user.myiam[*].arn
  description = "The ARNs for all users"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

#output 출력
terraform output
terraform output all_arns
all_arns = [
  "arn:aws:iam::xxxxxxxxxxxxx:user/gasida",
  "arn:aws:iam::xxxxxxxxxxxxx:user/akbun",
  "arn:aws:iam::xxxxxxxxxxxxx:user/ssoon",
]
first_arn = "arn:aws:iam::xxxxxxxxxxxxx:user/gasida"
[
  "arn:aws:iam::xxxxxxxxxxxxx:user/gasida",
  "arn:aws:iam::xxxxxxxxxxxxx:user/akbun",
  "arn:aws:iam::xxxxxxxxxxxxx:user/ssoon",
]

이번에는 for_each을 사용해서 iam user 3명을 만들어보도록 하겠다.
count에서 이름을 받아 길이를 통해 만드는 것과 유사하다. 입력 변수에 사용자명 3개를 만들고 for_each에 변수를 호출한다. output에서 사용자 이름을 출력하는 것도 같이 입력해준다.
count와 다르게 state list을 했을 때 [0]~[2]가 아니라 [사용자명]~[사용자명]으로 출력되는 것을 볼 수 있다.

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  for_each = toset(var.user_names)
  name     = each.value
}

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["gasida", "akbun", "ssoon"]
}

output "all_users" {
  value = aws_iam_user.myiam
}


# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam["akbun"]
aws_iam_user.myiam["gasida"]
aws_iam_user.myiam["ssoon"]

#output 출력
terraform output
all_users = {
  "akbun" = {
    "arn" = "arn:aws:iam::xxxxxxxxxxxxx:user/akbun"
    "force_destroy" = false
    "id" = "akbun"
    "name" = "akbun"
    "path" = "/"
....
}

6. 정리

오늘은 프로그래밍 언어를 배우다보면 기초적으로 배우게 되는 변수 활용과 반복문에 대해서 진행해보았다. 테라폼은 비교적 단순한 구조를 갖고 있지만 이런 부분들을 활용하지 못하면 복잡한 구조의 인프라를 배포하기 어려울 거 같다는 생각을 하게 됐다.