본문 바로가기
DevOps

[T1014-이론] 6장 Module

by 서어켜엉 2024. 7. 10.
해당 내용은 cloudNet@ 팀의 가시다 님이 진행하는 테라폼 스터디 T101 4기에서 다룬 내용과 "테라폼으로 시작하는 IaC" (한빛미디어) 저서 내용을 정리한 것입니다.

 

테라폼으로 인프라와 서비스를 관리하게 되면 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게 된다. 따라서 단일 파일 구조로 관리하다 보면 다음과 같은 문제 발생한다.

  • 테라폼 구성에서 원하는 항목을 찾고 수정하는 것이 점점 어려워짐.
  • 리소스들 간의 연관 관계가 복잡해질수록 변경 작업의 영향도를 분석하기 위한 노력이 늘어남.
  • 개발/스테이징/프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복되어 업무 효율이 줄어듦.
  • 새로운 프로젝트를 구성하는 경우 기존 구성에서 취해야 할 리소스 구성과 종속성 파악이 어려움.

루트모듈(Root module) : 테라폼을 실행하고 프로비저닝하는 최상위 모듈

자식모듈(Child module) : 루트 모듈의 구성에서 호출되는 외부 구성 집합

 

모듈은 테라폼 구성의 집합으로 다음과 같은 특성을 가진다.

  • 관리성
    모듈은 서로 연관 있는 구성의 묶음이다. 원하는 구성요소를 단위별로 쉽게 찾고 업데이트할 수 있다. 모듈은 다른 구성에서 쉽게 하나의 덩어리로 추가하거나 삭제할 수 있다. 또한 모듈이 업데이트되면 이 모듈을 사용하는 모든 구성에서 일관된 변경 작업을 진행할 수 있다.
  • 캡슐화
    테라폼 구성 내에서 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만을 외부에 노출시킨다.
  • 재사용성
    구성을 처음부터 작성하는 것에는 시간과 노력이 필요하고 작성 중간에 디버깅과 오류를 수정하는 반복 작업이 발생한다. 테라폼 구성을 모듈화하면 이후에 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있다.
  • 일관성과 표준화
    테라폼 구성 시 모듈을 활용하는 워크플로는 구성의 일관성을 제공하고 서로 다른 환경과 프로젝트에도 이미 검증한 모듈을 적용해 복잡한 구성과 보안 사고를 방지할 수 있다.

1. 모듈 작성 기본 원칙

모듈은 대부분의 프로그래밍 언어에서 쓰이는 패키지나 라이브러리와 역할이 비슷하다.

  • 모듈 디렉토리 형식은 terraform-<프로바이더>-<모듈 이름> 형식을 제안한다.
    이 형식은 Terraform cloud, Terraform Enterprise 에서도 사용되는 방식으로 1) 디렉토리 또는 레지스트리 이름이 테라폼을 위한 것, 2) 어떤 프로바이더의 리소스를 포함하고 있으며, 3) 부여된 이름이 무엇인지 판별할 수 있도록 한다.
  • 테라폼 구성은 궁극적으로 모듈화가 가능한 구조로 작성할 것을 제안한다.
    처음부터 모듈화를 가정하고 구성파일을 작성하면 단일 루트 모듈이라도 후에 다른 모듈이 호출할 것을 예상하고 구조화할 수 있다. 또한 작성자는 의도한 리소스 묶음을 구상한대로 논리적인 구조로 그룹화할 수 있다.
  • 각각의 모듈을 독립적으로 관리하기를 제안한다.
    리모트 모듈을 사용하지 않더라도 처음부터 모듈화가 진행된 구성들은 때로 루트 모듈의 하위 파일 시스템에 존재하는 경우가 있다. 하위 모듈 또한 독립적인 모듈이므로 루트 모듈 하위에 두기보다는 동일한 파일 시스템 레벨에 위치하거나 별도 모듈만을 위한 공간에서 불러오는 것을 권장한다. 이렇게 하면 VCS를 통해 관리하기가 더 수월하다.
  • 공개된 테라폼 레지스트리의 모듈을 참고하기를 제안한다.
    대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해두었다. 상황에 맞게 참고하는 것을 권장한다.
  • 작성된 모듈은 공개 또는 비공개로 게시해 팀 또는 커뮤니티와 공유하기를 제안한다.
    모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있는 자극이 된다.

아래 그림과 같이 모듈을 위한 별도 공간을 생성하는 방식으로 관리하는 것이 좋다. 코드간 종속성을 낮출 수 있는 장점이 있다.

 

2. 모듈화 해보기

  • 모듈의 기본적 구조는 테라폼 구성으로 입력 변수를 구성하고 결과를 출력하기 위한 구조로 구성한다.

모듈의 기본 구조

 

  • '모듈화'라는 용어는 이런 구조를 재활용하기 위한 템플릿 작업을 말한다.
  • 애플리케이션 개발시에도 자주 사용되는 용어로 테라폼은 작성된 모듈을 다른 루트 모듈에서 가져다 사용하며 이를 통해 재사용성과 표준화 구조를 구성할 수 있다.

루트 모듈과 자식 모듈

  • 기존에 작성된 모듈은 다른 모듈에서 참조해 사용할 수 있다.
  • 모듈에서 필요한 값은 variable로 선언해 설정하고, 모듈에서 생성된 값 중 외부 모듈에서 참조하고 싶은 값은 output으로 설정한다. 마치 자바 개발 시 getter, setter 로 캡슐화된 클래스를 활용하는 것과 비슷하다.
  • 예제 코드
mkdir -p 06-module-traning/modules/terraform-random-pwgen
cd 06-module-traning/modules/terraform-random-pwgen
touch main.tf variable.tf output.tf

modules 라는 독립적인 공간에 모듈을 생성했고, 모듈 이름도 앞서 모듈 작성 기본 원칙에서 언급한대로 작성되었다.

 

main.tf

# main.tf
resource "random_pet" "name" {
  keepers = {
    ami_id = timestamp()
  }
}

resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}

variable.tf

# variable.tf
variable "isDB" {
  type        = bool
  default     = false
  description = "패스워드 대상의 DB 여부"
}

output.tf

# output.tf
output "id" {
  value = random_pet.name.id
}

output "pw" {
  value = nonsensitive(random_password.password.result) 
}

 

  • 생성한 모듈 실행 테스트
# 모듈 동작 테스트
terraform init && terraform plan

# 테스트를 위해 apply 시 변수 지정
terraform apply -auto-approve -var=isDB=true

# 확인
terraform state list

terraform state show random_pet.name
echo "random_pet.name.id" | terraform console
echo "random_pet.name.keepers" | terraform console

terraform state show random_password.password
echo "random_password.password.length" | terraform console
echo "random_password.password.special" | terraform console
cat terraform.tfstate| grep result

# tfstate에 모듈 정보 확인 : VSCODE에서 terraform.tfstate 파일 확인
cat terraform.tfstate | grep module

# graph 확인
terraform graph > graph.dot

 

  • 자식 모듈 호출 테스트
mkdir -p 06-module-traning/06-01-basic
cd 06-module-traning/06-01-basic
touch main.tf
module "mypw1" {
  source = "../modules/terraform-random-pwgen"
}

module "mypw2" {
  source = "../modules/terraform-random-pwgen"
  isDB   = true
}

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}
# 실행
terraform init && terraform plan && terraform apply -auto-approve

# 결과 확인
terraform state list

# 모듈 정보 확인
cat terraform.tfstate | grep module

# module.json 확인
tree .terraform

## 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있다.
## 모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의된다.
cat .terraform/modules/modules.json | jq

apply 했을 때 random passwd가 두 개 생성 되었다.

 

3. 모듈 사용 방식

모듈과 프로바이더

모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의가 필요하다. 여기서 사용자는 프로바이더를 모듈 안 or 밖에서 정의할지 고민해야 한다.

 

유형 1. 자식 모듈에서 프로바이더 정의

  • 모듈에서 사용하는프로바이더 버전과 구성 상세를 자식 모듈에서 고정하는 방법이다.
  • 프로바이더 버전과 구성에 민감하거나, 루트 모듈에서 프로바이더 정의 없이 자식 모듈이 독립적인 구조일 때 고려할 방법이다.
  • 하지만 동일한 프로바이더가 루트와 자식 양쪽에 또는 서로 다른 자식 모듈에 버전 조건이 합의가 안 되면, 오류가 발생하고 모듈에 반복문을 사용할 수 없다는 단점이 있다.

유형 2. 루트 모듈에서 프로바이더 정의(실습)

  • 자식 모듈은 루트 모듈의 프로바이더 구성에 종속되는 방식이다.
  • 디렉토리 구조로는 분리되어 있지만 테라폼 실행 단계에서 동일 계층으로 해석되므로 프로바이더 버전과 구성은 루트 모듈의 설정이 적용된다. 
  • 예제 코드

디렉터리 생성 및 06-module-traning/modules/terraform-aws-ec2/main.tf variable.tf output.tf 파일 생성

mkdir -p 06-module-traning/modules/terraform-aws-ec2/
cd 06-module-traning/modules/terraform-aws-ec2/
touch main.tf variable.tf output.tf
# main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

resource "aws_default_vpc" "default" {}

data "aws_ami" "default" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

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

resource "aws_instance" "default" {
  depends_on    = [aws_default_vpc.default]
  ami           = data.aws_ami.default.id
  instance_type = var.instance_type

  tags = {
    Name = var.instance_name
  }
}
# variable.tf
variable "instance_type" {
  description = "vm 인스턴스 타입 정의"
  default     = "t2.micro"
}

variable "instance_name" {
  description = "vm 인스턴스 이름 정의"
  default     = "my_ec2"
}
# output.tf
output "private_ip" {
  value = aws_instance.default.private_ip
}

 

작성된 모듈을 사용할 루트 모듈 디렉터리 생성 및 06-module-traning/multi_provider_for_module/main.tf output.tf 파일 생성

mkdir -p 06-module-traning/multi_provider_for_module/
cd 06-module-traning/multi_provider_for_module/
touch main.tf output.tf
# main.tf
provider "aws" {
  region = "ap-southeast-1"  
}

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

module "ec2_singapore" {
  source = "../modules/terraform-aws-ec2"
}

module "ec2_seoul" {
  source = "../modules/terraform-aws-ec2"
  providers = {
    aws = aws.seoul
  }
  instance_type = "t3.small"
}
# output.tf
output "module_output_singapore" {
  value = module.ec2_singapore.private_ip
}

output "module_output_seoul" {
  value = module.ec2_seoul.private_ip
}

 

apply 후 outputs 출력 확인
output, state list 확인
tfstate에 모듈 정보 확인 : VSCODE에서 terraform.tfstate 파일 확인

 

모듈의 반복문

  • 모듈 또한 리소스에서 반복문을 사용하듯 구성할 수 있다.
  • 모듈이라는 리소스 정의 묶음을 원하는 수량으로 프로비저닝할 수 있으므로 모듈 없이 구성하는 것과 대비해 리소스 종속성 관리와 유지 보수에 장점이 있다. count를 사용한 반복문 리소스에서의 사용 방식처럼 module 블록 내에 선언한다.
  • 예제코드
terraform init && terraform apply -auto-approve

terraform output
mkdir -p 06-module-traning/module_loop_count/
cd 06-module-traning/module_loop_count/
touch main.tf
provider "aws" {
  region = "ap-northeast-2"  
}

module "ec2_seoul" {
  count  = 2
  source = "../modules/terraform-aws-ec2"
  instance_type = "t3.small"
}

output "module_output" {
  value  = module.ec2_seoul[*].private_ip   
}
terraform init
cat .terraform/modules/modules.json | jq

# 
terraform apply -auto-approve
terraform output
terraform state list

# state 파일에서 module 정보 확인
cat terraform.tfstate | grep module

# 생성된 ec2 확인
aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text

# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve

terraform apply -auto-approve

  • 모듈 묶음에 일관된 구성과 구조로 프로비저닝이 되는 경우라면 count가 간편한 방안이지만, 동일한 모듈 구성에 필요한 인수 값이 다르다면 for_each를 활용한다.

 

  • 예제코드
    동일한 모듈에 개발과 상용에 대한 입력 변수를 다르게 처리하는 방법
locals {
  env = {
    dev = {
      type = "t3.micro"
      name = "dev_ec2"
    }
    prod = {
      type = "t3.medium"
      name = "prod_ec2"
    }
  }
}

module "ec2_seoul" {
  for_each = local.env
  source = "../modules/terraform-aws-ec2"
  instance_type = each.value.type
  instance_name = each.value.name
}

output "module_output" {
  value  = [
    for k in module.ec2_seoul: k.private_ip
  ]
}
  • 로컬 변수를 선언/참조하여 for_each 반복문으로 dev / prod 환경의 ec2를 각각 실행시킴.

4. 모듈 소스 관리

모듈 소스 관리

  • 깃허브
    • 깃의 원격 저장소로 널리 알려진 깃허브는 테라폼 구성에 대한 CI 용도로 사용할 수 있고, 저장된 구성을 테라폼 모듈의 소스로 선언할 수도 있다.
    • 6.3에서 사용한 06-module-training/modules/terraform-aws-ec2/ 를 깃허브에 업로드한 후 source의 경로에 깃허브 레포지토리 주소를 넣어준다.
# main.tf
provider "aws" {
  region = "ap-southeast-1"  
}

module "ec2_seoul" {
  source = "github.com/<Owner Name>/terraform-module-repo/terraform-aws-ec2"
  instance_type = "t3.small"
}