[T101 4기] Module
들어가며
이번 주에는 Module
과 Runner
에 대해
테라폼으로 시작하는 IaC를 통해 알아 보겠습니다.
Module
Module의 개요
테라폼으로 인프라와 서비스를 오랜 기간 관리하다보면 시간이 지날수록 관리하는 리소스가 늘어나면서 구성이 복잡해집니다. 마치 함수 하나가 수천줄 코드로 구성된 C 코드처럼, 다음과 같은 문제점이 발생합니다.
- 원하는 항목을 찾기 어렵고, 수정하기 어려워짐
- 리소스간의 연관 관계가 복잡해져서 수정하기 어려워짐
- 개발/스테이징/프로덕션 환경등으로 구분된 경우 코드 중복으로 업무효율이 줄어듬
- 새로운 프로젝트를 구성할 경우 기존 코드를 복사하여 수정하는 방식으로 진행되어 코드 중복이 발생하고 종속성 파악이 어려움
모듈은 테라폼 코드를 구조화하고 재사용 가능한 코드를 만들 수 있게 하여 이러한 문제점들을 해소해줍니다.
모듈과 모듈간 정의를 통한 프로비저닝 과정
모듈을 통한 프로비저닝 과정은 위와 같으며, 루트 모듈이 자식 모듈을 사용하여 프로비저닝을 하게 되는데 이 구조에 대해 살펴 보겠습니다.
Module의 구성
모듈은 크게 루트 모듈과 자식 모듈로 나뉩니다.
- 루트 모듈 (Root Module) : 테라폼 코드를 실행하는 최상위 모듈
- 자식 모듈 (Child Module) : 루트 모듈에서 호출되는 모듈
모듈은 루트 모듈과 자식 모듈은 모두 입력 변수를 받아서 Provider를 통해 리소스를 생성하는 등의 작업을 하고 결과를 출력하는 구조로 구성됩니다.
모듈의 기본 구조
위의 그림처럼 모듈은 레고 블럭같이 여러개를 조합하여 사용할 수 있습니다. 루트 모듈은 자식 모듈을 호출하고, 자식 모듈은 다른 자식 모듈을 호출할 수 있습니다. 이렇게 모듈로 만드는것을 모듈화라고 하며, 이를 통해 재사용성과 표준화된 구조를 구성할 수 있습니다.
루트 모듈과 자식 모듈
또한 기존에 작성된 모듈을 다른 모듈에서 참조해 사용할 수 있으며, 리소스와 유사하게 사용할 수 있습니다.
Module의 장점
모듈을 사용하면 다음과 같은 장점이 있습니다.
- 관리성 : 코드를 구조화하여 모듈 단위로 추가하거나 삭제하기 쉬워 관리가 용이해집니다.
- 재사용성 : 모듈을 통해 코드를 재사용할 수 있어 개발 시간을 단축할 수 있습니다. 또한 parameter와 output 값을 통해 다양한 목적으로 재활용 가능합니다.
- 캡슐화 : 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 필요한 항목만 외부에 노출 시켜서 다른 모듈과 의존성을 줄일 수 있습니다.
- 일관성과 표준화 : 모듈을 사용함으로써 중복을 줄이고, 구성을 일관성 있게 유지할 수 있습니다. 또한 검증된 모듈을 사용함으로써 표준화된 구성을 유지하고 보안 사고를 방지할 수 있습니다.
모듈 작성의 기본 원칙
모듈을 제대로 사용하기 위해서는 다음과 같은 기본 원칙을 지키길 추천합니다.
-
모듈 디렉터리 형식을
terraform-<프로바이더 이름>-<모듈 이름>
형식으로 사용하기 : 이 형식은 Terraform Cloud, Terraform Enterprise에서도 사용되는 방식으로, 테라폼을 위한 것임을 밝히고, 어떤 프로바이더 리소스를 사용하는지, 어떤 모듈인지 쉽게 파악할 수 있습니다. - 모듈을 독립적으로 관리하기 : 리모트 모듈을 사용하지 않더라도 하위 모듈을 서브 디렉터리에 담지 않고, 독립된 모듈로 존재할 수 있도록 하는것을 추천합니다. 이렇게 하면 다른 프로젝트에서도 쉽게 사용할 수 있고, VCS를 통해 버전관리하기도 수월합니다.
-
모듈 디렉터리 내에
main.tf
,variables.tf
,outputs.tf
파일을 포함하기 : 이 파일들은 테라폼 코드를 작성할 때 필수적으로 사용되는 파일들로, 모듈을 사용하는 사람이 쉽게 파악할 수 있도록 구성합니다. -
언제나 모듈화가 가능한 구조로 작성하기 : 테라폼 리소스 구성 파일을 작성시 항상 모듈화 할 가능성을 염두해두고
작성합니다. 이렇게 하면 리소스 구성 파일을 모듈화 할 때 추가적인 작업이 줄어들고, 일관성을 유지할 수 있게 도와줍니다.
이외에도 공개된 테라폼 레지스트리의 모듈 참고하기, 작성된 모듈을 팀 또는 커뮤니티와 공유하기도 추천합니다.
모듈을 독립적으로 관리하기 위해 루트 모듈의 하위 디렉터리에 두기 보다는,
루트 모듈과 같은 레벨에 modules
디렉터리를 두고, 아래에 각각의 모듈을 관리하기를 추천합니다.
다음과 같이 표현 할 수 있습니다.
06-module-training
├── modules # child module home
│ └── terraform-random-pwgen
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── 06-01-basic # root-module
└── main.tf
모듈화 해보기
실습 1. 비밀번호 생성하기
모듈을 만드는 것을 실습하기 위해 비밀번호를 생성하는 모듈을 만들어 보겠습니다.
- 06-module-training/modules/terraform-random-pwgen/ 디렉터리에 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) }
- 자식 모듈 테스트
$ terraform init && terraform plan # 변수 지정없이 apply $ terraform apply -auto-approve # => Apply complete! Resources: 2 added, 0 changed, 0 destroyed. # Outputs: # id = "awaited-dodo" # pw = "1tz5Cf4cuL" # var.isDB가 기본값 false 여서 10자리 비밀번호가 생성됨 # var.isDB=true 로 하여 apply $ terraform apply -auto-approve -var=isDB=true # => Apply complete! Resources: 2 added, 0 changed, 2 destroyed. # Outputs: # id = "flying-gorilla" # pw = "8LMyxla5fK$WP8x?" # var.isDB를 true로 주어 16자리 비밀번호가 생성됨 # 상태 확인 $ terraform state list $ terraform state show random_pet.name $ echo "random_pet.name.id" | terraform console # => "flying-gorilla" $ echo "random_pet.name.keepers" | terraform console # => tomap({ # "ami_id" = "2024-07-11T15:04:58Z" # }) $ terraform state show random_password.password $ echo "random_password.password.length" | terraform console # => 16 $ echo "random_password.password.special" | terraform console # => true $ cat terraform.tfstate| grep result # 상태파일에는 현재 module 에 대한 내용이 없습니다. $ cat terraform.tfstate | grep module # graph 확인 $ terraform graph > graph.dot
모듈 그래프
실습 2. 비밀번호 생성하는 모듈을 루트 모듈에서 호출하기
- 자식 모듈을 호출하는 Root Module 생성하기 위해 06-module-training/06-01-basic/main.tf 파일을 생성
# 06-module-training/06-01-basic/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 # => Apply complete! Resources: 4 added, 0 changed, 0 destroyed. # Outputs: # mypw1 = { # "id" = "absolute-goat" # "pw" = "yntpOjuyqm" # } # mypw2 = { # "id" = "intense-imp" # "pw" = "0IlU7A?uqfKR0P81" # } # 상태 확인 $ terraform state list # 모듈정보 확인. $ cat terraform.tfstate | grep module # => "module": "module.mypw1", # 하위 모듈의 정보가 출력됩니다. # "module": "module.mypw1", # "module": "module.mypw2", # "module": "module.mypw2", # terraform init 시 생성되는 module.json 확인 $ tree .terraform # => .terraform # ├── modules # │ └── modules.json # └── ... # module.json 내용 확인. 모듈 사용 정보가 출력됩니다. $ cat .terraform/modules/modules.json | jq # => { # "Modules": [ # { # "Key": "", # "Source": "", # "Dir": "." # }, # { # "Key": "mypw1", # "Source": "../modules/terraform-random-pwgen", # "Dir": "../modules/terraform-random-pwgen" # }, # { # "Key": "mypw2", # "Source": "../modules/terraform-random-pwgen", # "Dir": "../modules/terraform-random-pwgen" # } # ] # } # 자식 모듈의 output 값은 module.<모듈 이름>.<output 이름>으로 조회 할 수 있습니다. $ echo "module.mypw1.id" | terraform console # => "absolute-goat" $ echo "module.mypw2.pw" | terraform console # => "0IlU7A?uqfKR0P81" # graph 확인 $ terraform graph > graph.dot
모듈 사용 방식
모듈과 프로바이더
- 모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의가 필요합니다. 프로바이더의 정의를 모듈 안에 두느냐, 루트 모듈에서 정의하느냐에 따라 모듈의 재사용성이 달라집니다.
유형 1. 자식 모듈에서 프로바이더 정의
- 모듈에서 사용하는 프로바이더의 버전과 상세 구성을 자식 모듈에 고정하는 방법입니다.
- 프로바이더 버전과 구성에 민감하거나, 루트 모듈과 관계없이 독립적인 구조일때 사용합니다.
- 하지만 동일 프로바이더가 루트 모듈과 자식 모듈 양쪽, 또는 서로 다른 자식 모듈에 버전 조건 합의가 안 되면, 오류가 발생하고 모듈에서 반복문을 사용할 수 없다는 단점이 있어 잘 사용하지 않습니다.
유형 2. 루트 모듈에서 프로바이더 정의 (추천)
- 자식 모듈은 루트 모듈의 프로바이더에 종속되는 방식입니다.
- 디렉터리 구조는 분리되어 있지만, 실행 단계에서는 동일 계층으로 해석되어서 프로바이더 버전과 구성은 루트 모듈의 설정이 적용됩니다.
- 프로바이더를 모듈 내 리소스와 데이터 소스에 일괄 적용하고, 자식 모듈에 대한 반복문을 자유롭게 사용할 수 있는것이 장점입니다.
루트 모듈에서 프로바이더 정의 실습
- 실습을 위한 디렉터리 구성
06-module-training ├── modules # child module home │ └── terraform-aws-ec2 │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── multi_provider_for_module # root-module ├── main.tf └── outputs.tf
- 06-module-traning/modules/terraform-aws-ec2/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 파일 생성
# 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 # => { # "Modules": [ # { # "Key": "", # "Source": "", # "Dir": "." # }, # { # "Key": "ec2_seoul", # "Source": "../modules/terraform-aws-ec2", # "Dir": "../modules/terraform-aws-ec2" # }, # { # "Key": "ec2_singapore", # "Source": "../modules/terraform-aws-ec2", # "Dir": "../modules/terraform-aws-ec2" # } # ] # } $ terraform apply -auto-approve # => Apply complete! Resources: 4 added, 0 changed, 0 destroyed. # Outputs: # module_output_seoul = "172.31.0.75" # module_output_singapore = "172.31.17.130" $ terraform output $ terraform state list $ terraform state show module.ec2_seoul.data.aws_ami.default # 상태파일에서 모듈 정보 확인 $ cat terraform.tfstate | grep module # => "module_output_seoul": { # "module_output_singapore": { # "module": "module.ec2_seoul", # "module": "module.ec2_seoul", # "module": "module.ec2_seoul", # "module.ec2_seoul.aws_default_vpc.default", # "module.ec2_seoul.data.aws_ami.default" # "module": "module.ec2_singapore", # "module": "module.ec2_singapore", # "module": "module.ec2_singapore", # "module.ec2_singapore.aws_default_vpc.default", # "module.ec2_singapore.data.aws_ami.default" # 그래프 확인 $ terraform graph > graph.dot
# 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
# => my_ec2 43.202.2.105 running
$ 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
# => my_ec2 18.139.217.84 running
# 실습 완료 후 리소스 삭제
$ terraform destroy -auto-approve
모듈의 반복문
- 모듈도 리소스에서 반복문을 사용할 수 있습니다.
- 모듈이라는 잘 정의되고 테스트된 단위로 원하는 수량으로 프로비저닝을 할 수 있으므로, 모듈없이 구성하는것과 대비해서 리소스 종속성 관리와 유지보수에 장점이 있습니다.
count
를 사용한 반복문은 리소스와 유사하게module
블록 내에 선언합니다.
count를 사용한 모듈 반복문 실습
-
06-module-traning/module_loop_count/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 # => Apply complete! Resources: 4 added, 0 changed, 0 destroyed. # Outputs: # module_output = [ # "172.31.4.243", # "172.31.8.11", # ] # count = 2 한만큼 2개의 EC2 인스턴스가 생성됨 $ terraform output $ terraform state list # 상태파일에서 모듈정보 확인 # (앞서 살펴본 cat .terraform/modules/modules.json | jq 에서는 모듈이 1개만 나오지만, # 여기에서는 count 만큼 보여짐) $ cat terraform.tfstate | grep module # 그래프 확인 $ terraform graph > graph.dot
# 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
# 실습 완료 후 리소스 삭제
$ terraform destroy -auto-approve
for_each를 사용한 모듈 반복문 실습
-
06-module-traning/module_loop_for_each/main.tf 파일 생성
# main.tf locals { env = { dev = { type = "t3.nano" name = "dev_ec2" } prod = { type = "t3.micro" 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를 사용한 모듈 반복문 테스트
$ terraform init
$ terraform apply -auto-approve
$ terraform output
# => module_output = [
# "172.31.13.57",
# "172.31.12.95",
# ]
$ terraform state list
# 상태파일에서 모듈정보 확인
$ cat terraform.tfstate | grep module
# 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
# => dev_ec2 3.36.76.84 running
# prod_ec2 52.79.44.76 running
# 실습 완료 후 리소스 삭제
$ terraform destroy -auto-approve
모듈 소스 관리
- 모듈 소스 관리 방법은 크게 다음과 같이 나눌 수 있습니다.
- 로컬 디렉터리 경로
- 테라폼 레지스트리 (Terraform Registry)
- VCS (Git, GitHub, GitLab, Bitbucket 등)
- HTTP urls
- Object Storage (S3 Bucket, GCS Bucket 등)
참조 예제
- 로컬 디렉터리
- 대상 모듈이 같은 로컬 파일 시스템에 있으므로 다운로드 없이 바로 사용할 수 있습니다.
- 재사용성을 고려한다면 앞서 실습에서 처럼 상위 디렉터리에 modules 디렉터리를 만들고 별도로 관리하는것을 권장하며,
항상 루트모듈과 함께 동작하는 경우 하위 디렉터리에 모듈을 정의해도 좋습니다. - 예시
# 상위 디렉터리에 별도 관리시 예제 module "local_dir_module" { source = "../modules/terraform-aws-ec2" } # 하위 디렉터리에 관리시 예제 module "local_subdir_module" { source = "./terraform-aws-ec2" }
- 테라폼 레지스트리
- 테라폼 레지스트리에 등록된 모듈을 사용하는 방법으로 공개 모듈과 Terraform Cloud, Terraform Enterprise의 비공개 모듈을 사용할 수 있습니다.
- 공개된 모듈은 https://registry.terraform.io/browse/modules 에서 확인할 수 있습니다.
- 모듈 명은
<네임스페이스>/<이름>/<프로바이더>
의 형태를 따릅니다. - 예시
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.1.0" }
- VCS 사용 (github 예시)
- Git의 원격 저장소로 유명한 깃허브는 테라폼 구성에 대한 CI 로도 사용할 수 있고, 모듈 소스를 업로드 하여 사용 할 수 있습니다.
- 앞서 만든 06-module-traning/modules/terraform-aws-ec2/ 를 깃허브에 업로드하는 과정
- 깃허브에 로그인
- 새로운 깃허브 저장소 생성 [New repository]
- Owner : 원하는 소유자 선택
- Repository name : 예시) terraform-module-repo
- Public 선택
- Add .gitignore의 드롭박스에서 [Terraform]을 선택
- 맨 아래 [Create repository] 클릭
- 해당 저장소에 예시) ‘terraform-aws-ec2’ 디렉터리 생성 후 main.tf , variable.tf, output.tf 추가 후 업로드
- 모듈 사용 예시
# main.tf provider "aws" { region = "ap-southeast-1" } module "ec2_seoul" { source = "github.com/sweetlittlebird/terraform-module-repo/terraform-aws-ec2" instance_type = "t3.small" }
- 실행
$ terraform init # 상태 확인 $ tree .terraform/modules .terraform/modules ├── ec2_seoul │ └── terraform-aws-ec2 │ ├── main.tf │ ├── output.tf │ └── variable.tf └── modules.json # 배포 $ terraform apply -auto-approve # => ... # Downloading git::https://github.com/sweetlittlebird/terraform-module-repo.git for ec2_seoul... # ... # Apply complete! Resources: 2 added, 0 changed, 0 destroyed. $ terraform state list # => module.ec2_seoul.data.aws_ami.default # module.ec2_seoul.aws_default_vpc.default # module.ec2_seoul.aws_instance.default # 실습 완료 후 삭제 $ terraform destroy -auto-approve
마치며
5주차는 두편으로 이어집니다. 첫번째 편에서는 모듈화에 대해 알아보고, 모듈을 만들고 사용하는 방법을 실습해 보았습니다. 두번째 편에서는 Terraform Runner에 대해 알아보겠습니다.