TFAS (TerraForm Addicted School)의 내용을 정리한 글입니다.
심각한 테라폼 중독입니다 - 책으로 스터디를 진행합니다.

실행 환경 관리
다시 복기해 보자. 테라폼 실행환경은 테라폼의 명령어를 실행할 수 있는 디렉터리로, 루트 모듈이 실행 환경이다. 같은 루트모듈을 사용하더라도 워크스페이스를 사용하여 실행 환경을 논리적으로 분류할 수 있다.
실행 환경의 분리는 관리하려는 인프라의 수와 연관이 있다. 관리 대상 인프라가 많다면 실행 환경을 나누어서 실행하는 것이 좋다. 대략 인프라의 수를 500개 정도로 기준으로 삼아서 500개 이상이 되는 경우 실행 환경을 분리하는 게 좋을 수 있다. 물론 더 적은 환경에서도 실행 환경을 분리해서 관리 가능하다.
테라폼으로 관리하고 있는 리소스의 수는 다음과 같은 명령어를 사용해 확인가능하다.
terraform state list | wc -l
실행 환경을 분리하지 않을 때의 문제점
하나의 실행 환경에서 모든 인프라 리소르를 만들어 관리할 수 있지 않을까 생각할 수 있다. 어째서 인프라 리소스가 많이 정의되어 있는 경우 실행 환경을 분리하는 것이 좋은지 다음 문제점들을 확인해 보자.
한 번에 한 명만 테라폼 실행 가능
테라폼은 상태 백엔드를 통해 하나의 실행 환경에 여러 명의 작업자가 테라폼 상태를 공유하며 인프라 작업을 수행할 수 있다. 이때 테라폼 실행 환경은 상태 충돌을 방지하기 위해 동시에 한 명만 상태 관련 수행할 수 있도록 락을 거는 메커니즘을 가지고 있다. 그러므로 너무 많은 인프라 리소스가 하나의 실행 환경에 정의되어 있다면 일부 리소스를 수정하기 위해 전체 실행 환경을 독점적으로 락을 거는 상황에 빠질 수 있다.
예를 들어 두 명의 엔지니어가 각각 EC2와 Route53 호스트존을 정의하고 생성해야 하는 상황이라고 해보자. 여기서 두 서비스 생성은 연관성이 없지만, 두 엔지니어는 반드시 한 명의 작업이 완전히 끝난 상태에서 다른 엔지니어가 작업할 수 있는 상황이 된다.
플래닝 시간 증가
테라폼 플랜등을 사용하면 상태 동기화가 일어나며 실행 환경이 관리 중인 모든 인프라 리소스를 확인한다. 실행 환경의 리소스가 더 많아질수록 상태 동기화에 사용되는 시간이 비례하여 증가한다. 테라폼 프로바이더에서 리소스의 수만큼 API를 호출해야 하기 때문이다.
주된 역할을 하는 리소스 외에도 해당 리소스를 위해 생성되는 보조 리소스가 많다는 사실을 항상 기억해야 한다. 예를 들어 AWS 역할한 개와 해당 역할에 연결할 정책을 다섯 개 생성한다고 해보자. 여기서 주된 역할을 하는 리소스를 합하면 여섯 개이기 때문에 총 6개의 리소스가 생성될 것으로 생각된다. 하지만 실제 역할과 정책의 연관관계의 정의 또한 테라폼 리소스로 정의된다. 따라서 역할 1개, 정책 5개, 역할과 정책의 연관 관계인 5개 총 11개의 리소스가 생성된다.
이처럼 인프라를 운영하며 관리해야 할 리소스는 인프라가 발전하면서 예상 이상으로 급격하게 증가하고, 결국 플랜등 상태 동기화에 걸리는 시간의 증가로 이어진다.
신경 쓰고 싶지 않은 변경점 증가
모든 실무의 인프라를 테라폼으로 관리하는 것은 이상적이다. 클라우드 콘솔등으로 인프라 작업을 해야 하는 경우 리소스의 드리프트가 발생한다. 이때 바로 변경점을 테라폼에 반영하여 드리프트 상태를 해소하는 것이 바람직하지만 곧바로 다른 작업을 이어서 하는 경우가 많아 리소스 드리프트 상태가 계속해서 유지될 수도 있다.
여러 작업자가 작업하는 상태에서 이런 리소스 드리프트는 리소스 상태 동기화에 변경점을 발생시킨다. 리소스 하나를 만드는데도 수많은 리소스 정의가 추가되는데 생각지 못한 변경사항이 잡히는 경우 혼란스러워진다.
실행 환경 분리 사례
배포 환경 혹은 계정별 분리
관리 포인트의 분리를 실무 상황으로 알아보자. 거의 대부분의 조직은 관리 포인트를 환경별로 가지고 있다. 개발 환경, 스테이지 환경, 운영 환경으로 분리되어 관리되는 인프라를 가지고 있으며, 각각의 인프라는 요구사항이나 구성이 다를 수 있다.
조직의 요구사항에 따라 배포 환경 별 인프라 분리 포인트가 달라지며, 여러 배포 환경의 공용 리소스가 생기는 경우 어느 실행 환경에서 관리해야 할지 모호해진다. 모든 리소스가 배포 환경별로 동일하게 떠있는 구조가 아니라면 환경별 사용하는 계정별로 테라폼 실행 환경 분리를 고려할 수 있다.
여러 계정에 걸친 비슷한 리소스 한 번에 관리
AWS의 IAM 은 특정 리전에 종속되지 않는 글로벌 리소스이다. 보안 관점에서 중요한 IAM 은 중요한 리소스이며, 여러 계정에서 사용하더라도 동일한 조직 내의 보안 정책을 사용하여 일관된 규칙으로 관리가 필요하다.
이런 경우 분리된 계정마다 IAM 리소스를 생성하는 방식이 아니라 하나의 글로벌 계정을 지정해 두고 중앙 집중적으로 관리하는 것이 보안상, 관리상 유리한 점이 많다.
여러 계정에 걸쳐 한 번에 배포되어야 하는 리소스는 따로 관리
여러 계정에 여러 VPC 가 있고 각 VPC 간 통신이 필요한 경우, 서로 다른 VPC에서 통신을 위한 VPC 피어링 혹은 Transit Gateway를 사용할 수 있다. 그렇다면 VPC A와 VPC B의 피어링 통신에 대한 테라폼 정의는 어느 계정의 테라폼 환경에 있어야 할까? 여러 계정간 걸쳐 연결이 필요한 리소스에 대한 모호함이 있다.
이런 리소스를 관리하기 위해선 여러 환경 또는 여러 계정 내 관련된 리소스들을 일괄적으로 관리할 수 있는 방법이 있어야 한다. 이런 경우 교차 계정 전용 실행 환경에서 유사한 종류의 리소스를 중앙 관리하는 것이 유리하다.
TGW의 경우 VPN 리소스도 같이 만들고 VPC의 라우팅 규칙도 만든다. 이 네트워크는 처음 한 번 구성해 두면 변경할 일이 잘 생기지 않으나, 잘못 수정되는 경우에는 네트워크의 장애를 일으킬 수 있을 정도로 민감하다. 따라서 운영 안정성을 위해선 네트워크 실행 환경은 분리해서 관리하는 것이 권장된다.
또한 도메인의 경우도 교차 계정 전용 실행 환경을 통해 관리하는 것이 권장된다. terraform.com이라는 도메인을 운영환경에서 사용하고 dev.terraform.com을 개발 계정에 구성한다면 생성해야 하는 리소스는 다음과 같다.
- 운영 계정
- terraform.com 호스트존
- dev.terraform.com NS 레코드
- 개발 계정
- dev.terraform.com 호스트 존
이는 개발 계정의 호스트존을 만들기 위해 운영 계정의 호스트 존에 NS 레코드를 생성해야 한다는 의미이다. 즉 한 가지 작업을 위해 두 개의 테라폼 프로바이더가 필요하기 때문에 어디 하나의 배포 환경에서 관리되기 애매한 상황이 된다. 만약 ACM 도 테라폼으로 관리하게 된다면 더욱 어떤 도메인이 어떤 계정에 존재하는지 한 번에 확인할 수 있는 실행 환경에 있는 것이 좋다.
실무에서는 Route53의 레코드는 테라폼으로 관리하기 어렵다. A, CNAME 등 자주 사용하는 레코드는 동적인 속성이 있기 때문에 Route53의 호스트존과 ACM 위주로 관리할 수 있다.
자주 건드리지 않지만 민감한 리소스 관리하기
EKS를 테라폼으로 관리하게 되면 쿠버 리소스까지 테라폼으로 관리하게 된다. aws-auth ConfigMap 또는 클러스터 생성 시 필수로 설치해야 할 애드온, 헬름 차트 등이 주로 관리 대상이 된다. 클러스터 토큰을 활용해 쿠버네티스와 헬름 프로바이더를 사용하는데, EKS 토큰이 15분만 유효하기 때문에 상태 동기화에 오래 걸리는 경우 토큰 만료로 인한 오류가 생길 수밖에 없다. 이런 경우 리소스에 대한 오류에 굉장히 예민할 수 있기 때문에 환경을 분리해서 관리할 수 있다.
다양한 인라인 블록
중첩 블록
중첩 블록은 특정 리소스 안에서 더 세부적인 구성을 정의하는 데 사용된다. 중첩 블록의 예시로 AWS 보안 그룹의 인그레스와 이그레스 규칙을 들 수 있다.
resource "aws_security_group" "example" {
name = "example-security-group"
description = "Example security group"
vpc_id = aws_vpc.example.id
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
egress {
description = "All outbound traffic allowed"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
이처럼 중첩 블록을 하나의 리소스 블록 안에 무한대로 설정할 수 있으나, 반복적으로 생성하는 경우 코드가 중복적으로 작성되기 때문에 다이내믹 블록을 사용하여 중첩된 코드 작성을 피할 수 있다.
다이내믹 블록
다이나믹다이내믹 블록을 사용하면 반복적인 중첩 블록을 간결하게 선언할 수 있다. 다이내믹 블록은 아래와 같은 구조를 통해 선언 가능하다.
resource "type" "name" {
dynamic "block_name" {
for_each = collection
content {
}
}
}
다이내믹 블록은 리소스 블록 안에 선언되며 "block_name" 은 컬렉션을 순회하며 생성할 중첩 블록의 이름이다. ingress 중첩 블록을 다이내믹 블록으로 생성하는 경우 dynamic "ingress" {}와 같은 방법으로 선언한다. 다이내믹 블록은 for_each 와 content라는 매개변수만을 가지며 기존 중첩 블록에서 선언해야 하는 매개변수는 content 블록 안에 선언한다.
다음은 로컬 블록과 다이나믹 블록을 사용하여 컬렉션을 설정하는 예제를 확인해 보자.
locals {
sg_rules = {
inbound_https = {
type = "ingress"
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
inbound_http = {
type = "ingress"
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
outbound_all = {
type = "egress"
description = "All outbound traffic allowed"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
resource "aws_security_group" "example" {
name = "example-security-group"
description = "Example security group"
vpc_id = aws_vpc.example.id
dynamic "ingress" {
for_each = {
for k, v in local.sg_rules : k => v
if v.type == "ingress"
}
content {
description = ingress.value.description
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
dynamic "egress" {
for_each = {
for k, v in local.sg_rules : k => v
if v.type == "egress"
}
content {
description = egress.value.description
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
}
}
}
다이내믹 블록은 중첩 블록을 간결하게 정의할 수 있어 유용하다.
중첩 블록 vs 별도 리소스 블록
중첩 블록과 동일한 역할을 갖는 별도의 리소스 블록이 제공되면 별도 리소스 블록을 사용하는 것을 권장한다. 가장 큰 이유는 중첩 블록은 제어할 수 없는 순서가 존재한다. 예를 들어 보안 그룹 안의 ingress 중첩 블록을 사용할 때 새로운 인그레스 규칙이 추가되면 기존 규칙 목록과 새로운 규칙 목록을 비교하며 변경된 순서에 따라 규칙을 삭제하고 재생성할 수 있다. 이는 AWS 내부 구현에 따라 전체 포트에 관한 규칙이 0번 인덱스를 차지하게 되고 이에 따라 인덱스의 변화로 인해 기존과 동일한 규칙이어도 변경되어 새롭게 생성될 수 있는 것이다.
하지만 별도의 리소스 블록을 사용한다면 독립적인 리소스로 분리되어 규칙이 상태로 관리되기 때문에 새로운 규칙이 추가되거나 삭제되더라도 인덱스로 인한 영향을 받지 않아 리소스 운영에 대한 리스크를 회피할 수 있다.
생명주기 블록
테라폼의 생명 주기 블록은 모든 리소스 블록 안에서 사용 가능하며, 해당 리소스의 생명 주기를 관리하는 데 사용하고 여러 번 선언할 수 있다. 일반적인 중첩 블록의 한 종류라고 볼 수 있으나 다이내믹으로 정의할 수 없다.
ignore_changes(list(속성 이름), [])
ignore_changes(list(속성 이름), []) 는 생명 주기 블록에서 가장 많이 사용하는 매개변수이다. 속성 이름이 담긴 리스트를 인수로 받으며 기본값은 빈 리스트이다. 해당 옵션은 특정 속성에 대한 변경을 무시하도록 설정한다. 가장 흔한 예로는 ASG의 기대 인스턴스 수의 값이다. min_size, max_size, desired_capacity 등의 상태값을 가지는데, desired_capacity는 서버 내 리소스 사용량을 바탕으로 동적으로 변한다. 따라서 자동으로 변경되는 값은 무시되도록 설정되어야 한다.
resource "aws_autoscaling_group" "this" {
max_size = 10
min_size = 1
desired_capacity = 5
lifecycle {
ignore_changes = [
desired_capacity,
]
}
}
create_before_destroy(bool, false)
create_before_destroy(bool, false)는 불리언 값을 인수로 받으며 기본값은 false이다. 수정 불가능한 매개변수가 변경될 때 테라폼은 리소스를 삭제 후 재생성한다. 이 옵션을 true로 설정하는 경우 리소스가 삭제되기 전 생성을 먼저 실행하기 때문에 서비스 중단을 방지하는 데 유용하다. 단 리소스와 매개변수의 특징을 잘 이해하고 사용해야 한다.
예를 들어 보안 그룹의 설명이 변경되어 보안그룹을 재생성해야 하는 상황이라면 보안 그룹의 이름이 동일하다면 기존 보안 그룹이 삭제되지 않은 상태에서 동일한 리소스로 생성할 수 없다.
prevent_destroy(bool, false)
prevent_destroy(bool, false)가 활성화된다면 해당 리소스를 삭제하려는 모든 작업에 대해 오류를 일으킨다. 불리언 값을 인수로 받으며 기본값은 false이다. 실수로 삭제되면 안 되는 치명적인 리소스가 있는 경우 해당 생명 주기 블록을 사용하면 삭제를 막을 수 있다.
replace_triggered_by(list(리소스/속성 이름), [])
replace_triggered_by(list(리소스/속성 이름), []) 특정 리소스나 속성이 변경되면 해당 리소스 삭제 후 재생성되도록 설정한다. 설정한 리소스와 속성이 변경되면 해당 리소스를 대체하지 않고 삭제 후 생성한다.
유효성 검사
테라폼에서는 구성한 인프라의 정확성과 효율성을 보장하기 위해 다양한 유효성 검사 기능을 제공한다. 유효성 검사 기능은 구성 오류를 방지하고 인프라 코드가 예상대로 작동할 수 있도록 검증해 준다.
검사 블록
검사 블록은 변수 블록 내에서 사용 가능하며 변수에 대한 사용자 입력을 검증할 때 사용한다. 변수가 기준이나 조건이 만족하지 못한다면 오류 메시지를 제공한다. 예를 들어 실행 환경에 대한 변수를 env라고 했을 때 특정 값만 허용할 수 있도록 구성해 보자.
variable "env" {
type = string
description = "Environment"
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env must be one of dev, staging, or prod"
}
}
검사 블록은 단일한 변수 하나에 대해서만 유효성 검사를 할 수 있다. 여러 조건에 따른 변수 검증이 불가능하며 이를 사용하기 위해선 lifecycle 블록의 precondition을 활용할 수 있다.
생명 주기 블록
생명주기 블록에서 사용할 수 있는 유효성 검사를 위한 precondition, postcondition 중첩 블록을 제공한다. precondition 은 리소스 생성되거나 변경되기 전에 충족해야 하는 조건 정의를 할 수 있으며, postcondition 블록으로 리소스 생성 또는 변경 작업이 완료된 후 해당 리소스가 충족해야 하는 조건을 정의한다. 블록은 생성이 완료된 리소스의 상태값을 알아야 하는데, self라는 인수를 사용할 수 있다.
EC2 생성 시 AMI 아키텍처를 검사하고, 퍼블릭 dns를 검사하는 예시를 확인해 보자.
data "aws-ami" "this" {
owners = ["amazon"]
most_recent = true
filter {
name = "image-id"
values = ["ami-0c55b159cbfafe1f0"]
}
}
resource "aws_instance" "this" {
ami = data.aws_ami.this.id
instance_type = "t2.micro"
lifecycle {
precondition {
condition = data.aws_ami.this.architecture == "x86_64"
error_message = "ami architecture must be x86_64"
}
postcondition {
condition = self.public_dns != ""
error_message = "instance must have a public dns"
}
}
}
체크 블록
체크 블록은 자체적으로 사용하는 하나의 블록이다. 이 블록은 precondition이나 validation 블록처럼 유효성 검사가 실패하는 경우 오류가 발생하는 것이 아닌 경고만 발생하고 상태 동기화가 계속해서 진행된다. 따라서 반드시 설정해야 하는 유효성 검사는 precondition, validation 블록으로 설정하고 권장사항을 체크 블록을 사용하여 표현하는 것이 좋다.
ACM Validation
예를 들어 AWS ACM을 만들면서 검증을 위한 CNAME 레코드 생성을 체크블록으로 확인할 수 있다. ACM의 상태를 파악해 경고로 알려주면 작업자가 CNAME 레코드를 새로 만들어야 하는지 알 수 있다.
resource "aws_acm_certificate" "validation" {
domain_name = "terraform.com"
validation_method = "DNS"
subject_alternative_names = ["terraform.com"]
}
locals {
acm_validate_value = tolist(aws_acm_certificate.validation.domain_validation_options)[0]
}
resource "time_sleep" "wait" {
depends_on = [aws_acm_certificate.validation]
create_duration = "30s"
}
check "validation" {
data "aws_acm_certificate" "this" {
domain = aws_acm_certificate.validation.domain_name
statuses = ["VALIDATION_TIME_OUT", "PENDING_VALIDATION", "EXPIRED", "INACTIVE", "REVOKED", "ISSUED", "FAILED"]
depends_on = [time_sleep.wait]
}
assert {
condition = data.aws_acm_certificate.this.status != "PENDING_VALIDATION"
error_message = "ACM certificate validation failed"
}
}
check "validation_record" {
data "dns_cname_record_set" "validation" {
host = local.acm_validate_value.resource_record_name
depends_on = [time_sleep.wait]
}
assert {
condition = data.dns_cname_record_set.validation.cname == local.acm_validate_value.resource_record_name
error_message = "ACM certificate validation record is not a CNAME"
}
}
time_sleep 리소스 블록은 검증이 완료되는 데 걸리는 시간을 대기하기 위해 사용한다. 이 검증은 DNS 프로바이더를 사용하여 CNAME 레코드 생성 확인을 하여 검증할 수도 있다.
EKS 애드온 권장 버전 확인
EKS 모듈을 만드는 경우 EKS 애드온도 구성이 필요하다. EKS 버전별로 애드온의 권장 버전이 달라진다. 애드온은 매번 권장 버전이 달라질 때마다 업데이트하기보다는 최초 설치 시 혹은 EKS 버전 업그레이드를 하는 경우 애드온의 버전을 업데이트한다. 따라서 테라폼에서도 처음 생성 혹은 버전 업데이트 시 버전을 명시해 업데이트하고, 권장 버전이 업데이트되면 경고를 주는 것을 체크블록으로 구현할 수 있다.
variable "eks_version" {
type = string
default = "1.30"
}
variable "eks_addon_name" {
type = string
default = "vpc-cni"
}
variable "eks_addon_version" {
type = string
default = "1.18.1-eksbuild.3"
}
data "aws_eks_addon_version" "this" {
kubernetes_version = var.eks_version
addon_name = var.eks_addon_name
}
locals {
addon_default_version = data.aws_eks_addon_version.this.version
}
check "addon_version" {
assert {
condition = var.eks_addon_version == local.addon_default_version
error_message = "recommended version is ${local.addon_default_version}"
}
}
ALB/NLB 필수 옵션 강제 적용
AWS의 ELB 통합 모듈을 설계한다면, ALB와 NLB의 서로 다른 필수 강제 요소들이 존재한다. 예를 들어 NLB 리스너 안에 규칙을 만들 수 없다거나, ALPN 정책은 NLB 리스너에만 적용할 수 있다거나 하는 조건들이다. 이런 내용은 잘못 설정해서 오류를 발생시키기보다 적절히 무시해서 잘못된 명세에 대해 설정이 필요하다는 걸 안내할 수 있다.
variable "lb_info" {
default = {
name = "test-lb"
type = "application"
}
}
variable "listener_info" {
default = {
port = 443
protocol = "HTTPS"
rules = {}
alpn_policy = "None"
}
}
variable "lb_type" {
default = "application"
}
locals {
is_alb = var.lb_info.type == "application"
is_nlb = var.lb_info.type == "network"
listener_rules = var.listener_info.rules
}
resource "aws_lb_listener" "this" {
alpn_policy = var.protocol == "TLS" ? var.listener_info.alpn_policy : null
}
resource "aws_lb_listener_rule" "this" {
for_each = {
for rule in var.listener_info.rules : rule.name => rule
if local.is_alb
}
}
check "nlb_listener_have_rule" {
assert {
condition = !(local.is_nlb && length(var.listener_info.rules) > 0)
error_message = "NLB listener should not have rules"
}
}
check "alb_listener_have_aplpn_policy" {
assert {
condition = !(local.is_alb && var.listener_info.alpn_policy != "None")
error_message = "ALB listener should not have ALPN policy"
}
}
유틸리티 모듈 만들기
테라폼을 사용하면 테라폼의 모듈의 반환값은 인프라 리소스이고, 모듈 호출의 사이드 이펙트는 인프라 조작이다. 테라폼에서 인프라 리스소스가 반환값이나 사이드 이펙트의 대상이 아닌 모듈을 만들 수 있는데 이를 유틸리티 모듈이라고 부를 수 있다. 여러 프로젝트나 환경에서 공통적으로 자주 사용하는 로직을 모듈로 중앙화하면 인프라 코드 유지와 관리가 용이하다.
AWS 메타데이터 조회, 리스트 내 맵 합치기와 같은 유용한 유틸리티 모듈을 설계하고 구현해 보자.
AWS 메타데이터 가져오기
AWS 메타데이터는 실무상 다양한 환경과 프로젝트에서 수시로 사용하기 때문에 자주 호출한다. 이런 상황마다 AWS 메타데이터의 값을 매번 직접 호출하는 것은 유지 보수 측면에서 효율적이지 않다. AWS 메타데이터의 값은 서로 다른 데이터 블록을 통해 가지고 와야 한다. 이때 필요한 정보가 있을 때마다 데이터 블록을 확인하는 것은 번거롭다. 그리고 메타데이터를 다수의 데이터 블록을 호출하는 방법으로 알아야 한다면 반복적이고 장황한 코드를 작성하게 된다. 우리가 필요한 메타데이터 정보를 한 번에 받을 수 있도록 get_aws_metadata라는 유틸리티 모듈을 만들어보자.
meta_module/main.tf
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_iam_account_alias" "current" {}
data "aws_availability_zones" "available" {
state = "available"
}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
output "region_name" {
value = data.aws_region.current.name
}
output "account_alias" {
value = data.aws_iam_account_alias.current.account_alias
}
output "region_code" {
value = split("-", data.aws_availability_zones.available.zone_ids[0])[0]
}
output "az_name" {
value = data.aws_availability_zones.available.name
}
main.tf
# meta_module
module "current" {
source = "./meta_module"
}
locals {
account_id = module.current.account_id
region = module.current.region_name
account_alias = module.current.account_alias
region_code = module.current.region_code
availability_zone = module.current.az_name
}
프로바이더 동일성 체크
두 프로바이더가 동일한지 아닌지에 따라 인프라 구성 로직이 달라져야 하는 경우 프로바이더의 동일성 체크가 가능한 모듈을 만들어보자.
provider_validation_module/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
configuration_aliases = [aws.a, aws.b]
}
}
}
provider_validation_module/main.tf
# provider a
data "aws_caller_identity" "a" {
provider = aws.a
}
data "aws_region" "a" {
provider = aws.a
}
# provider b
data "aws_caller_identity" "b" {
provider = aws.b
}
data "aws_region" "b" {
provider = aws.b
}
output "is_cross_account" {
value = data.aws_caller_identity.a.account_id != data.aws_caller_identity.b.account_id
}
output "is_cross_region" {
value = data.aws_region.a.name != data.aws_region.b.name
}
main.tf
# provider_validation
module "check_cross" {
source = "./provider_validation_module"
providers = {
aws.a = aws.requester
aws.b = aws.accepter
}
}
locals {
is_cross_account = module.check_cross.is_cross_account
is_cross_region = module.check_cross.is_cross_region
need_accepter = local.is_cross_account || local.is_cross_region
}
resource "aws_vpc_peering_connection" "this" {
peer_owner_id = local.is_cross_account ? local.accepter_account : null
peer_region = local.is_cross_region ? local.accepter_region : null
auto_accept = local.need_accepter ? false : true
dynamic "requester" {
for_each = local.need_accepter ? toset([]) : toset(["1"])
content {
allow_remote_vpc_dns_resolution = true
}
}
dynamic "accepter" {
for_each = local.need_accepter ? toset([]) : toset(["1"])
content {
allow_remote_vpc_dns_resolution = true
}
}
# ...
}
리스트 맵 합치기
항목이 맵인 리스트가 있는 경우 각 항목이 가진 맵을 하나로 합치고 싶은 경우 사용할 수 있는 유틸리티를 작성해 보자. 이는 맵 형식의 리스트를 혹은 맵 형식의 맵을 인풋으로 받아 단일 리스트 값으로 얻을 수 있다.
merge_map_module/main.tf
variable "input" {
description = "list(map()) or map(map())"
type = list(map(string))
}
locals {
keys = flatten([for item in var.input : keys(item)])
values = flatten([for item in var.input : values(item)])
output = zipmap(local.keys, local.values)
}
output "output" {
value = local.output
}
main.tf
# merge_map_module
locals {
vpc_list = [
{
vpc1 = "1234",
vpc2 = "5678"
},
{
vpc3 = "9876"
}
]
vpc_map = merge(local.vpc_list)
}
module "merge_vpc_list" {
source = "./merge_map_module"
input = local.vpc_list
}
module "merge_vpc_list_from_map" {
source = "./merge_map_module"
input = local.vpc_map
}
output "output" {
value = module.merge_vpc_list.output
}
output "output_from_map" {
value = module.merge_vpc_list_from_map.output
}'School > TFAS' 카테고리의 다른 글
| [TFAS] 챕터 12 CSV 파일로 관리하는 보안 그룹 모듈 만들기 (1) | 2025.12.06 |
|---|---|
| [TFAS] 챕터 11 YAML 파일로 관리하는 VPC 모듈 만들기 (0) | 2025.12.04 |
| [TFAS] 챕터 5 테라폼 모듈 & 챕터 10 모듈을 직접 만드는 이유와 만드는 방법 (1) | 2025.11.29 |
| [TFAS] 챕터 4 테라폼 기본 문법 (1) | 2025.11.25 |
| [TFAS] 챕터 3 테라폼 작동 방식 (0) | 2025.11.24 |