⭐ 가시다(gasida) 님이 진행하는 Terraform T101 4기 실습 스터디 게시글입니다.
책 '테라폼으로 시작하는 IaC'을 참고했습니다! 
게시글 상 소스코드, 사진에서 **굵게** 혹은 '''코드쉘''' 에 대한 부분이 들어가있을수도 있습니다.

Terraform 에서 Module 이 필요한 이유?

Terraform 으로 인프라 프로비저닝을 사용한다고 하였을때, 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게된다.

이에따라 자연스레 아래의 문제가 발생하게된다.

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

이러한 이유로 Terraform 의 Module 기능이 필요해진다

Module 구분

Module 은 Root Module, Child Module 로 구분된다.

  • Root(루트) Module: 테라폼을 실행하고 프로비저닝하는 최상위 모듈
  • Child(자식) Module: 루트 모듈의 구성에서 호출되는 외부 구성 집합

Module 은 테라폼 구성의 집합으로, Terraform 으로 관리하는 대상의 규모가 커지고 복잡해질때 생긴 문제를 보완하기 위한 방안으로 사용한다.

모듈 작성원칙

  • 모듈 디렉토리 형식을 terraform-<프로바이더 이름>-<모듈 이름> 으로 지정하기를 권고한다.
  • Terraform 구성을 Module화가 가능한 구조로 작성하는것을 권고한다.
  • 각각의 모듈을 독립적으로 관리하는걸 권고한다.
  • 모듈을 독립적으로 관리하기 위해 디렉토리 구조를 만들때, 모듈을 위한 별도공간을 생성하는 방식으로 진행한다.

모듈화?

Terraform 에서 권장 모듈의 구조: 테라폼 구성으로 입력변수 구성, 결과를 출력하는 구조

모듈화: 구조를 재활용 하기 위한 템플릿 작업을 지칭함.

모듈단위(루트, 자식) 로 모듈화를 진행하면, 아래의 구조로 만들 수 있다.

모듈작성 실습

시나리오: 하나의 Provisioning → User/Password 를 여러번 구성해야 하는 상황

단순 배포

실습구조 생성

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) 
}

배포

terraform init && terraform plan

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

배포 결과 중 확인가능한 정보: 단일 id/pw 로 배포 확인

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

id = "active-duckling"
pw = "U8e4DSQNfKwHsnez"

자식 모듈 호출 실습

  • 시나리오 : 다수의 리소스 생성을 목적으로 여러번 반복하여 리소스를 생성해야하는 경우
  • 자식모듈을 사용하지 않는경우: 리소스 수만큼 반복해 구성파일 정의. 이름을 고유하게 설정해줘야함
  • 자식모듈을 사용하는경우: 모듈을 활용하면 반복되는 리소스 묶음을 최소화 하는게 가능함

환경구성

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

배포 결과 중 확인가능한 정보: 다중 id/pw 로 배포 확인

mypw1 = {
  "id" = "enabling-lark"
  "pw" = "8NHdWc7XFW"
}
mypw2 = {
  "id" = "accurate-mole"
  "pw" = "o%mXhnq$W*cfMw1e"
}

배포확인

terraform state list

module.mypw1.random_password.password
module.mypw1.random_pet.name
module.mypw2.random_password.password
module.mypw2.random_pet.name
tree .terraform

.terraform
├── modules
│   └── modules.json
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── random
                └── 3.6.2
                    └── darwin_arm64
                        ├── LICENSE.txt
                        └── terraform-provider-random_v3.6.2_x5

8 directories, 3 files

Module & Provider?

문제 상황: Module 에서 사용되는 모든 리소스는 Provider 의 정의가 필요함.

문제점: Provider 정의를 모듈 안 or 밖 어디에 해야할까?

CASE1. 자식 모듈에서 프로바이더 정의

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

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

  • 자식 모듈루트 모듈프로바이더 구성에 종속되는 방식이다.
  • 디렉터리 구조로는 분리되어 있지만 테라폼 실행 단계에서 동일 계층으로 해석되므로 프로바이더 버전과 구성은 루트 모듈의 설정이 적용된다.
  • 모듈에는 다수의 프로바이더가 사용될 가능성이 있으므로 map 타입으로 구성하는 provider로 정의한다.

CASE2의 구조는 아래와 같다.

실습환경구성(자식모듈)

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
}

배포

cd 06-module-traning/multi_provider_for_module/
terraform init

**terraform apply -auto-approve**

배포확인

terraform output
terraform state list
# 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 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

TMI) AWS 싱가포르 리전에 접근했다고 이런메일도 보내준다

Module & loop

  • 모듈 또한 리소스에서 반복문을 사용하듯 구성할 수 있다.
  • 모듈 없이 구성하는 것과 대비해 리소스 종속성 관리와 유지 보수에 장점이 있다

실습 환경구성

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

배포

cd 06-module-traning/module_loop_count/
terraform init
**terraform apply -auto-approve**

배포확인

terraform output
terraform state list
# 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**

코드수정

cd 06-module-traning/module_loop_count
vi 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

배포확인

terraform output
terraform state list
# 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

Module Source 관리

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

e.g) Local Dir, Terraform Registry, Github, S3 ….

로컬 디렉터리 경로

  • 하위 디렉터리는 ./로, 상위 디렉터리는 ../로 시작
  • 재사용성이 고려된다면 상위 디렉터리에 별도 관리하는 것을 권장하고, 항상 루트 모듈과 함께 동작해야 하는 경우 하위 디렉터리에 모듈을 정의
#e.g.
module "local_module" {
  source = "../modules/my_local_module"
}

테라폼 레지스트리

  • 테라폼의 프로토콜을 사용해 모듈을 사용하는 방식
  • 공개된 테라폼 모듈, Terraform Cloud, Terraform Enterprise 에서 제공되는 모듈을 사용가능함
#e.g.
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
}

Module Source : Git example

Github 로그인 → Repo 생성

  • 이번 예제에서 User Owner 는 digzect 로 표현합니다.
  • 실습을 따라할 때엔 digzect 대신에, 각자의 id 로 지정합니다.

환경구성

git clone https://github.com/digzect/terraform-module-repo.git
cd terraform-module-repo
mkdir terraform-aws-ec2

코드작성

#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
  }
}
#variables.tf

variable "instance_type" {
  description = "EC2 인스턴스 타입"
  type        = string
  default     = "t2.micro"
}

variable "instance_name" {
  description = "EC2 인스턴스 이름"
  type        = string
  default     = "my_ec2"
}
#outputs.tf

output "private_ip" {
  value = aws_instance.default.private_ip
}

코드 푸쉬

git add .
git commit -m "Add EC2 module"
git push

로컬 코드 작업환경구성

mkdir module-source-mygithub
cd module-source-mygithub

코드추가

#main.tf

provider "aws" {
  region = "ap-southeast-1"  
}

module "ec2_instance" {
  source        = "github.com/digzect/terraform-module-repo//terraform-aws-ec2"
  instance_type = "t3.small"
  instance_name = "github_module_ec2"
}

output "ec2_private_ip" {
  value = module.ec2_instance.private_ip
}

구조확인

tree .
.
├── module-source-mygithub
│   └── main.tf
└── terraform-aws-ec2
    ├── main.tf
    ├── outputs.tf
    └── variables.tf

3 directories, 4 files
git status
현재 브랜치 main
브랜치가 'origin/main'에 맞게 업데이트된 상태입니다.

추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    module-source-mygithub/

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)

테라폼배포

terraform init
terraform plan
terraform apply -auto-approve

결과

  • 성공적으로 github repo 에 있는 Terraform Module 정보를 가져와서 Provisioning 하였다.

jjongguet