본문 바로가기
Terraform/Terraform 101 Study

5주차 1편 테라폼 Module

by 개발자 영만 2024. 7. 14.

Module

  • Terraform을 사용하여 인프라와 서비스를 관리하는 과정에서 시간이 지남에 따라 구성이 복잡해지고 관리하는 리소스가 증가하게 됩니다.
  • Terraform의 구성 파일과 디렉터리 구조에는 제약이 없어 단일 파일 구조에서 지속적으로 업데이트할 수 있지만, 다음과 같은 문제들이 발생할 수 있습니다
    • 원하는 항목을 찾고 수정하는 것이 어려워짐
    • 리소스 간의 연관 관계가 복잡해짐에 따라 변경 작업의 영향도를 분석하기 위한 노력이 증가함
    • 개발, 스테이징, 프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복되어 업무 효율이 저하됨
    • 새로운 프로젝트 구성 시 기존 구성에서 필요한 리소스 구성과 종속성 파악이 어려움

모듈의 개념

  • 모듈은 Terraform 구성의 집합으로, 규모가 커지고 복잡해진 구성을 관리하기 위한 방안입니다. 모듈은 크게 루트 모듈자식 모듈로 구분됩니다
    • 루트 모듈 (Root Module) : Terraform을 실행하고 프로비저닝하는 최상위 모듈
    • 자식 모듈 (Child Module) : 루트 모듈의 구성에서 호출되는 외부 구성 집합

모듈의 장점

  • 모듈을 사용하면 다음과 같은 장점을 제공합니다
    • 관리성 : 모듈은 연관된 구성의 묶음으로, 원하는 구성요소를 단위별로 쉽게 찾고 업데이트할 수 있습니다. 모듈은 다른 구성에서 쉽게 추가하거나 삭제할 수 있으며, 모듈이 업데이트되면 이를 사용하는 모든 구성에서 일관된 변경 작업을 진행할 수 있습니다.
    • 캡슐화 : 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만을 외부에 노출시킵니다.
    • 재사용성 : 모듈화된 구성은 이후 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있어 처음부터 작성하는 시간과 노력을 줄일 수 있습니다.
    • 일관성표준화 : 모듈을 활용한 구성은 일관성을 제공하며, 서로 다른 환경과 프로젝트에도 이미 검증된 모듈을 적용해 복잡한 구성과 보안 사고를 방지할 수 있습니다.

 

6.1 모듈 작성 기본 원칙

  • 모듈은 대부분의 프로그래밍 언어에서 사용되는 라이브러리나 패키지와 유사한 역할을 합니다. 다음과 같은 기본 작성 원칙을 제안합니다.

모듈 작성 기본 원칙

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

모듈 관리 디렉터리 구조

  • 모듈을 독립적으로 관리하기 위해 디렉터리 구조를 생성할 때 모듈을 위한 별도 공간을 생성하는 방식을 사용합니다. 
  • 특정 루트 모듈 하위에 자식 모듈을 구성하는 경우 단순히 복잡한 코드를 분리하는 용도로 명시되며 종속성이 발생합니다. 따라서 루트 모듈 사이에 모듈 디렉터리를 지정합니다.

 

6.2 모듈화 해보기

모듈화 소개

  • 모듈의 기본 구조

    • Terraform 모듈은 기본적으로 입력 변수를 받아 결과를 출력하는 구조로 구성됩니다.
    • 입력 변수: 모듈에 필요한 값들을 설정하는 부분
    • 출력 값: 모듈이 생성한 결과를 외부에 제공하는 부분
  • 모듈화의 개념

    • 모듈화는 이 구조를 재활용하기 위한 템플릿 작업을 의미합니다.
    • 모듈화를 통해 작성된 모듈을 다른 루트 모듈에서 가져다 사용할 수 있으며, 이를 통해 재사용성표준화를 구축할 수 있습니다.
  • 모듈의 사용 방식
    • 기존에 작성된 모듈은 다른 모듈에서 참조하여 사용할 수 있습니다. 이는 리소스를 사용하는 방식과 유사합니다.
    • 모듈에서 필요한 값은 variable로 선언하여 설정하고, 모듈에서 생성된 값 중 외부 모듈에서 참조하고 싶은 값은 output으로 설정합니다. 이는 Java 개발 시 getter, setter로 캡슐화된 클래스를 활용하는 것과 비슷합니다.

모듈 작성 실습

  • 하나의 프로비저닝에서 사용자와 패스워드를 여러 번 구성해야 하는 경우를 가상의 시나리오로 삼아 모듈화를 진행합니다.
    • random_pet을 사용해 이름을 자동으로 생성하고, random_password를 사용해 사용자의 패스워드를 설정합니다.
    • random_password는 random 프로바이더 리소스로 난수 형태의 패스워드를 생성할 수 있습니다.

자식 모듈 작성

  • 디렉터리 생성 및 파일 생성
mkdir -p 06-module-traning/modules/terraform-random-pwgen
cd 06-module-traning/modules/terraform-random-pwgen
touch main.tf variable.tf output.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 "isDB" {
  type        = bool
  default     = false
  description = "패스워드 대상의 DB 여부"
}
  • output.tf 파일 작성
output "id" {
  value = random_pet.name.id
}

output "pw" {
  value = nonsensitive(random_password.password.result)
}
  • 자식 모듈 동작 테스트
# 파일 확인 및 초기화
ls *.tf
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

# state 파일에서 특정 정보를 추출
cat terraform.tfstate | grep result
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
  • 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

# state 파일에서 특정 정보를 추출 : module
cat terraform.tfstate | grep module

# 디렉터리 구조를 트리 형식으로 출력 : modules.json 파일 확인
tree .terraform

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

# Graph 확인
terraform graph > graph.dot

 

6.3 모듈 사용 방식

모듈과 프로바이더

  • 모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의를 필요로 합니다. 여기서 프로바이더를 어디(자식 or 루트)에 정의할지에 대한 고민이 필요합니다.

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

  • 프로바이더 구성을 자식 모듈에서 직접 정의하는 방식입니다.
  • 프로바이더 구성에 민감한 경우나 자식 모듈이 독립적인 구조를 가질 때 유용합니다.
  • 그러나 루트 모듈과 자식 모듈 사이 혹은 서로 다른 자식 모듈 간의 프로바이더 버전이 일치하지 않으면 오류가 발생할 수 있고 반복문을 사용할 수 없다는 단점 때문에 이 방식은 일반적으로 권장되지 않습니다.

[유형 2] 루트 모듈에서 프로바이더 정의

  • 루트 모듈에서 정의된 프로바이더 구성이 자식 모듈에게도 적용되는 방식입니다.
  • 디렉터리가 분리되어 있어도 테라폼 실행 시 모든 모듈이 동일 계층으로 해석되므로 루트 모듈의 프로바이더 설정이 자식 모듈에도 적용됩니다.
  • 이 구조는 리소스와 데이터 소스에 일관된 프로바이더를 적용하며, 자식 모듈을 반복적으로 사용할 때 유연성을 제공합니다.
  • 자식 모듈은 특정 프로바이더 구성에 종속될 수 없기 때문에 자식 모듈의 프로바이더 요구사항을 문서화하고 루트 모듈에서 정의된 프로바이더 설정에 맞춰 자식 모듈을 업데이트해야 합니다.
  • 동일한 모듈에 다양한 프로바이더 조건을 적용할 필요가 있을 때 각 모듈별로 프로바이더를 맵핑하는 방식을 사용할 수 있습니다.
  • 리소스와 데이터 소스에 provider 메타인수를 지정하는 기존  방식과 비슷하지만 모듈에 여러 프로바이더가 사용될 가능성을 고려하여 map 타입으로 provider 를 정의합니다.

  • 디렉터리 생성 및 파일 생성
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
}

 

  • 루트 모듈 디렉터리 생성 및 파일 생성
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
}
  • 실행 및 확인
# 초기화
terraform init

# 모듈의 세부 정보 출력
cat .terraform/modules/modules.json | jq

# 리소스 배포
terraform apply -auto-approve

# output.tf 파일에 정의된 모든 출력 변수의 값 출력
terraform output

# 리소스 목록 출력
terraform state list

# 특정 리소스의 상세 정보 출력
terraform state show module.ec2_seoul.data.aws_ami.default
terraform state show module.ec2_singapore.data.aws_ami.default

# state 파일에서 특정 정보를 추출 : module
cat terraform.tfstate | grep module

# Graph 확인
terraform graph > graph.dot

# CLI를 통한 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
aws ec2 describe-instances --region ap-southeast-1 --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

  • 자식 모듈이 요구하는 프로바이더 버전이 루트 모듈에서 지정한 버전과 다를 경우, 호환성 문제로 오류가 발생할 수 있습니다.

모듈의 반복문

  • 모듈도 리소스처럼 반복문을 사용하여 구성할 수 있습니다.
  • 반복문을 사용함으로써 원하는 수량만큼 리소스 정의 묶음인 모듈을 프로비저닝할 수 있으며, 이는 모듈을 사용하지 않는 구성에 비해 리소스 종속성 관리와 유지 보수에서 큰 장점을 제공합니다.
  • 리소스에서 사용하는 방식과 유사하게 모듈 블록 내에서 count를 선언하여 반복문을 사용합니다.
  • 디렉터리 생성 및 파일 생성
mkdir -p 06-module-traning/module_loop_count/
cd 06-module-traning/module_loop_count/
touch main.tf
  • 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

# output.tf 파일에 정의된 모든 출력 변수의 값 출력
terraform output

# 리소스 목록 출력
terraform state list

# state 파일에서 특정 정보를 추출 : module
cat terraform.tfstate | grep module

# Graph 확인
terraform graph > graph.dot

# CLI를 통한 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

  • 위와 같이 모듈 묶음이 일관된 구성과 구조로 프로비저닝될 경우 count는 간편하게 사용할 수 있는 방안입니다.
  • 하지만 동일한 모듈 구성에 필요한 인수 값이 다를 경우 for_each를 사용하는 것이 더 적합합니다.
  • 예를 들어, 동일한 모듈을 사용하여 개발 환경과 상용 환경에 대한 입력 변수를 다르게 처리하는 실습을 통해 이 개념을 더 깊이 이해할 수 있습니다.
  • main.tf 파일 수정
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
  ]
}
  • 실행 및 확인
# 계획
terraform plan

# 리소스 배포
terraform apply -auto-approve

# output.tf 파일에 정의된 모든 출력 변수의 값 출력
terraform output

# 리소스 목록 출력
terraform state list

# state 파일에서 특정 정보를 추출 : module
cat terraform.tfstate | grep module

# Graph 확인
terraform graph > graph.dot

# CLI를 통한 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

 

6.4 모듈 소스 관리

모듈 소스 관리

  • 테라폼 init 명령어를 사용할 때 정의된 모듈 소스 위치를 바탕으로 필요한 모듈을 다운로드하게 됩니다. 모듈 소스를 지정하는 방법은 여러 가지가 있습니다
    • 로컬 디렉터리 경로
    • 테라폼 레지스트리
    • 깃허브
    • 비트버킷
    • HTTP URLs
    • S3 Bucket
    • GCS Bucket

로컬 디렉터리 경로

  • 테라폼 레지스트리와 구분하기 위해 하위 디렉터리는 ./로 시작하고, 상위 디렉터리는 ../로 시작합니다.
  • 해당 모듈은 이미 같은 파일 시스템에 존재하기 때문에 별도의 다운로드 과정이 필요 없습니다.
  • 모듈이 재사용 가능하도록 설계되었다면 상위 디렉터리에 모듈을 별도로 관리하는 것이 좋습니다. 반면 모듈이 특정 루트 모듈과 밀접하게 연관되어 동작해야 할 경우는 하위 디렉터리에 모듈을 배치하는 것이 바람직합니다.
  • 예를 들어, 상위 디렉터리에 있는 모듈을 참조할 때는 다음과 같이 선언합니다.
module "local_module" {
  source = "../modules/my_local_module"
}

테라폼 레지스트리

  • 테라폼 레지스트리는 테라폼의 프로토콜을 사하여 모듈을 관리하고 사용하는 시스템입니다.
  • 공개된 테라폼 모듈과 Terraform Cloud 또는 Terraform Enterprise에서 제공하는 비공개 테라폼 모듈에 접근할 수 있습니다.
  • 공개된 모듈을 사용할 때는 <네임스페이스>/<이름>/<프로바이더> 형식으로 소스를 설정합니다.
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
}

깃허브

  • 깃허브는 깃의 원격 저장소로 테라폼 구성에 대한 CI 용도로 사용할 수 있으며 저장된 구성을 테라폼 모듈의 소스로 선언할 수도 있습니다.
  • 깃허브에 테라폼 모듈 업로드하기
    • 깃허브에 로그인합니다.
    • 새로운 깃허브 저장소를 생성합니다.
      • Owner: 원하는 소유자를 선택
      • Repository name: terraform-module-repo
      • Public 선택
      • Add .gitignore의 드롭다운 메뉴에서 [Terraform]을 선택
    • 맨 아래 Create repository 버튼을 클릭합니다.
    • 해당 저장소에 terraform-aws-ec2 디렉터리를 생성하고, main.tf, variables.tf, outputs.tf 파일을 추가하여 업로드합니다.

깃허브 모듈 사용하기

  • 디렉터리 생성 및 파일 생성
mkdir module-source-mygithub
cd module-source-mygithub
touch main.tf
  • 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"
}
  • 실행 및 확인
# 초기화
terraform init

# 아래 디렉터리에 깃허브에 있던 파일이 다운로드 되어 있음을 확인
tree .terraform/modules

# 리소스 배포
terraform apply -auto-approve

# 리소스 목록 출력
terraform state list

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