본문 바로가기
스터디 이야기/Terraform

[T101] 5-1. Terraform - module

by lakescript 2024. 7. 9.

 

더보기

이 스터디는 CloudNet@에서 진행하는 T101 스터디를 참여하면서 공부하는 내용을 기록하는 블로그 포스팅입니다.

CloudNet@에서 제공해주는 자료들과 테라폼으로 시작하는 IaC 를 바탕으로 작성되었습니다.

Module

테라폼 모듈이란 단일 디렉토리에 있는 Terraform 구성 파일 세트입니다. 

 

테라폼으로 인프라와 서비스를 관리하면 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 자연스럽게 늘어나게 됩니다. 테라폼의 구성 파일과 디렉토리 구성에는 제약이 없기 때문에 단일 파일 구조상에서 지속적으로 업데이트가 가능하지만 몇가지 문제가 발생합니다.

단일 구조의 단점

1. 테라폼 구성에서 원하는 항목을 찾고 수정하는 것이 점점 번거로워짐

2. 리소스들 간의 연관 관계가 복잡해질수록 변경 작업의 영향도를 분설하기 위한 노력이 늘어남

3. 개발/스테이징/프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복

4. 새로운 프로젝트를 구성하는 경우 기존의 리소스 구성과 종속성 파악이 어려움

 

 

구성과 모듈 간 정의를 통한 프로비저닝 과정

 

 

모듈은 Root Module과 Child Module로 구분됩니다.

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

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

 

모듈은 테라폼 구성의 집합으로 테라폼으로 관리하는 대상의 규모가 커지고 복잡해져 생긴 문제를 보완하고 관리하는 작업을 효율적으로 하기 위한 방안으로 활용됩니다. 즉, 하나 이상의 .tf 파일이 있는 단일 디렉토리 구조로 구성된 간단한 구조조차 모듈입니다. 이러한 디렉토리에서 직접 terraform 명령어를 실행하면 루트 모듈로 간주됩니다.

 

관리성

- 모듈을 서로 연관 있는 구성의 묶음입니다.

- 원하는 구성요소를 단위별로 쉽게 찾고 업데이트를 할 수 있습니다.

- 모듈은 다른 구성에서 쉽게 하나의 덩어리로 추가하거나 삭제할 수 있습니다.

- 모듈이 업데이트되면 이 모듈을 사용하는 모든 구성에서 일관된 변경작업을 진행할 수 있습니다.

 

캡슐화

- 테라폼 구성 내에서 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만 외부에 노출시킵니다.

 

재사용성

- 구성을 처음부터 작성하는 것에는 시간과 노력이 필요하고 작성 중간에 디버깅과 오류를 수정하는 반복 작업이 발생합니다.

- 테라폼 구성을 모듈화하면 이후에 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있습니다.

 

일관성과 표준화

- 테라폼 구성 시 모듈을 활용하는 워크플로는 구성의 일관성을 제공하고 서로 다른 환경과 프로젝트에도 이미 검증한 모듈을 적용하여 복잡한 구성과 보안사고를 방지할 수 있습니다.

 

모듈 작성 기본 원칙

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

terraform-<프로바이더 이름>-<모듈 이름>

모듈 디렉토리 형식은 위와 같은 형식을 사용으로하는데 이는 Terraform Cloud, Terraform Enterprise에서 사용되는 형식입니다. 그리고, 디렉토리 또는 레지스트리 이름이 테라폼을 위한 형식이며 어떤 프로바이더의 리소스를 포함하고 있는지 쉽게 알 수 있고 부여된 이름이 무엇인지 쉽게 판별할 수 있는 형식입니다.

 

테라폼 구성은 궁극적으로 모듈화가 가능한 구조로 작성할 것을 추천합니다. 처음부터 모듈화를 가정하고 구성 파일을 작성하면 단일 루트 모듈이라도 추후에 호출할 것을 예상하고 구조화할 수 있습니다. 또한 작성자는 의도한 리소스 묶음으로 구성한 대로 논리적인 구조로 그룹화할 수 있습니다.

 

각각의 모듈을 독립적으로 관리해야 합니다. 리모드 모듈을 사용하지 않더라도 처음부터 모듈화가 진행된 구성들은 때로 루트 모듈의 하위 파일 시스템에 존재하는 경우가 있습니다. 하위 모듈 또한 독립적인 모듈이므로 루트 모듈 하위에 두기보다는 동일한 파일 시스템 레벨에 위치하거나 별도 모듈만을 위한 공간에서 불러오는 것을 권장합니다. -> 이렇게 하면 VCS를 통해 관리하기 수월해집니다.

 

공개된 테라폼 레지스트리의 모듈을 참고하기를 추천합니다. 대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해두었습니다. (물론 그래도 가져다 사용하는 것이 아닌 상황에 맞게 수정해야 합니다!)

 

작성된 모듈은 공개 또는 비공개로 게시하여 팀 또는 커뮤니티와 공유하는 것을 추천합니다. 모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있습니다.

 

자식 모듈과 루트 모듈의 디렉토리 구조

모듈화의 목적은 테라폼 코드를 작성하는 작업자마다 다릅니다. 점점 많아지고 복잡해지는 구성 파일을 관리하기 위함이기도 하고 연관성 있는 리소스 집합을 묶어 모듈화하기도 합니다. 여기서는 모듈을 독립적으로 관리하기 위해 디렉토리 구조를 생성할 때마다 모듈을 위한 별도 공간을 생성하는 방식으로 진행됩니다. 특정 루트 모듈 하위에 자식 모듈을 구성하는 경우 단순히 복잡한 코드를 분리하는 용도로 사용되며 종속성이 발생하므로 루트 모듈 상위에 모듈 디렉토리를 지정합니다.

 

모듈화 해보기

모듈의 기본 구조

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

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

 

루트 모듈과 자식 모듈

 

기존에 작성된 모듈은 다른 모듈에서 참조하여 사용할 수 있으며 사용 방식은 리소스와 비슷합니다. 모듈에서 필요한 값은 variable로 선언하여 결정하고, 모듈에서 생성된 값 중 외부 모듈에서 참조하고 싶은 값은 output으로 설정합니다. (JAVA의 getter, setter와 비슷합니다.)

 

자식 모듈 생성 실습

하나의 프로비저닝에서 사용자와 패스워드를 여러 번 구성해야 하는 경우 random 프로바이저를 활용할 수 있습니다.
시나리오
random_pet이라는 이름을 자동으로 생성해주고 random_password는 사용자가 어떤 패스워드를 설정할지 고민하는 수고를 덜어주는 random 프로바이더 리소스입니다. 

 

루트 모듈과 자식 모듈의 디렉토리 구조

.
├── 06-01-basic					# root-module
│   └── main.tf
└── modules
    └── terraform-random-pwgen			# child-module
        └── main.tf
            ├── main.tf
            ├── output.tf
            └── variable.tf

 

 

 

모듈화 하려는 구성의 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 "isDB" {
  type        = bool
  default     = false
  description = "패스워드 대상의 DB 여부"
}

모듈화하려는 구성의 output.tf

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

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

 

terraform init

terraform init을 통해 초기화를 시켜줍니다.

 

terraform apply

terraform apply --auto-approve -var=isDB=true

 

terraform apply를 실행할 때에 변수(isDB)를 지정하여 실행합니다.

위와 같이 output이 정상적으로 출력되는 것으로 modules 디렉토리내에 하나의 테라폼 구성을 실행해보았습니다.

 

 

자식 모듈 호출 실습

다수의 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복하여 구성 파일에 정의해야 하고 이름도 고유하게 설정해줘야 하는 불편함이 있지만, 모듈을 사용하면 이러한 반복되는 리소스 묶음을 최소화할 수 있습니다.

 

자식 모듈을 호출하는 루트 모듈 정의

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
}

06-01-basic 디렉토리를 생성하고 그 안에 main.tf를 만들어 위의 코드를 작성합니다.

 

terraform init

terraform init

terraform init을 통해 초기화를 시켜줍니다.

이때Initializing modules... 을 통해 module을 초기화하는데 그 경로가 ../modules/~ 인 것을 확인하실 수 있습니다.

 

terraform apply

terraform apply

terraform apply를 통해 테라폼을 배포하겠습니다.

 

위의 실습으로 보신바와 같이 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있습니다.

 

 

모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의됩니다. 

 

terraform init 이후에 .terraform/modules/modules.json 파일을 확인해보면 여러 개의 모듈 정의가 선언되어 있고 테라폼 구성에서 선언한 값이 각각 정의된 것을 확인하실 수 있습니다.

 

 

모듈 사용 방식

모듈과 프로바이더

모듈에서 사용되는 모든 리소스는 관련 프로바이더 정의가 필요합니다. 여기서 주된 고민 거리는 프로바이더 정의를 모듈 안에 두어야 할지 밖에 두어야 할지입니다.

 

 

 

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

 

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

 

Initializing the backend...
Initializing modules...
- mypw1 in terraform-random-pwgen
- mypw2 in terraform-random-pwgen

Initializing provider plugins...
- Finding hashicorp/random versions matching "~> 3.1.0, >= 3.3.0"

Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider hashicorp/random:
no available releases match the given
containts ~> 3.1.0, ~> 3.3.0

terraform init 후 루트 모듈의 프로바이더 버전 조건이 >= 3.3.0이고 자식 모듈의 프로바이더 조건이 ~> 3.1.0인 경우 자식 모듈에 정의된 프로바이더 조건이 루트 모듈에 정의된 프로바이더 버전 조건을 만족하지 않아서 발생하는 오류 예시입니다.

 

Initializing modules...
There are some problems with the configuration, described below.

The Terraform configuration must be valid before initialization so that 
Terraform can determine which modules and providers need to be installed.

Error: Module is incompatible with count, for_each, and depends_on

	on main. tf line 2, in module "myaws" :
	2:	count = 3
    
The module at module.myaws is a legacy module which contains its own local provider 
configurations, and so calls to it
may not use the count, for_each, or depends_on arguments.

If you also control the module " /terraform-aws-test", consider updating this module 
to instead expect provider
configurations to be passed by its caller.

 

 

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

자식 모듈은 루트 모듈의 프로바이더 구성에 종속되는 방식입니다. 디렉토리 구조는 분리되어 있지만 테라폼 실행 단계에서 동일한 계층으로 해석되므로 프로바이더 버전과 구성은 루트 모듈의 설정이 적용됩니다. 프로바이더를 모듈 내 리소스와 데이터 소스에 일괄 적용하고, 자식 모듈에 대한 반복문 사용에 자유로운 것이 장점입니다. 자식 모듈에 특정 프로바이더 구성의 중속성은 반영할 수 없으므로 자식 모듈을 테스트한 프로바이더 조건에 대해 기록하고, 자식 모듈을 사용하는 루트 모듈에서 정의하는 프로바이더에 맞게 업데이트해야 합니다. 

 

루트 모듈에서 프로바이더 정의 실습

전체 디렉토리 구성

.
├── modules				# child-modules
│   └── terraform-aws-ec2
│       └── main.tf
│       └── output.tf
│       └── variable.tf
└── multi_provider_for_module		# root-module
    └── main.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
}

 

루트 모듈의 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"
}

 

주의!!
ap-southeast-1 로 배포하게 되면 AWS에서 경고 메일이 발송됩니다...ㅎ

 

 

루트 모듈의 output.tf

output "module_output_singapore" {
  value = module.ec2_singapore.private_ip
}

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

 

 

terraform apply

# /multi_provider_for_module
terraform apply --auto-approve

 

루트 모듈(multi_provider_for_module)에서 terraform apply를 실행하여 프로바이더 구성에서 정의한 대로 서로 다른 리전에 EC2 인스턴스가 생성되는지 확인합니다.

ec2-seoul 모듈에서 구성한 리소스와 데이터 소스는 aws 프로바이더 중 alias가 seoul로 지정된 프로바이더 구성에 의해 생성됩니다. 

 

aws 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 cli를 통해 ap-northeast-2 (서울) 리전에 생성되어 있는 EC2를 확인해보면 위와 같이 출력되는 것을 확인하실 수 있습니다.

 

모듈의 반복문

모듈은 리소스에서 반복문을 사용하듯 구성할 수 있습니다. 모듈이라는 리소스 정의 묶음을 원하는 수량으로 프로비저닝할 수 있으므로 모듈 없이 구성하는 것과 비교했을 때 리소스 종속성 관리와 유지 보수에 장점이 있습니다. 

 

모듈 선언에 count가 적용된 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   
}

 

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

 

module 선언에 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
  ]
}

 

모듈 소스 관리

Module 블록에 정의된 소스 구성으로 모듈의 코드 위치를 정의합니다. 보통 terraform init을 수행할 때 지정된 모듈을 다운로드해 사용합니다. 

모듈 소스 유형

  • 로컬 디렉토리 경로
  • 테라폼 레지스트리
  • GitHub
  • Bitbucket
  • git
  • HTTP URLs
  • S3
  • GCS(Google Cloud Storage)

그외에 아래에 보이는 사진처럼 테라폼 공식 문서에 나온 내용과 같이 루트 경로에 모듈을 배치하는 것 외에 패키지 하위 디렉토리 경로를 참조하는 것도 하나의 방안입니다. 

 

로컬 디렉토리 경로

로컬 경로를 지정할 때는 테라폼 레지스트리와 구분하기 위해 하위 디렉토리는 ./ 로 사용하며, 상위 디렉토리는 ../로 시작합니다. 대상 모듈은 이미 같은 로컬 파일 시스템에 존재하므로 다운로드 없이 바로 사용 가능합니다.

module "local_module" {
  source = "../modules/my_local_module"
}

 

테라폼 레지스트리

테라폼 모듈 레지스트리는 테라폼의 프로토콜을 사용하여 모듈을 사용하는 방식입니다. 공개된 테라폼 모듈을 사용하거나 TerraformCloud, Terraform Enterprise에서 제공되는 비공개 테라폼 모듈을 사용할 때 설정하는 소스 지정 방식입니다. 테라폼 레지스트리의 모듈은 특정 버전을 명시할 수 있으므로 안정된 버전을 지정하여 사용할 수 있습니다.(협업에 용이)

 

공개된 모듈은 위의 화면과 같이 공식 페이지에서 확인할 수 있으며, 각 모듈을 선택하면 테라폼 구성 시 어떻게 정의해야 하는지에 대한 내용을 확인하실 수 있습니다.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
}

공개된 테라폼 모듈을 source에 선언할 때는 <네임스페이스>/<이름>/<프로바이더> 형태로 설정합니다.

module "ec2_instance" {
  source  = "app.terraform.io/alibaba/ecs-instance/alicloud"
  version = ">= 2.9.0"
}

Terraform Enterprise처럼 비공개 모듈을 사용할 때는 source 선언 시 주소가 앞에 추가되는 <호스트 이름>/<네임스페이스>/<이름>/<프로바이더> 형태가 됩니다.