새소식

IaC/Terraform

4주차(1)_Terraform Module

  • -
CloudNet@ 팀의 가시다님께서 Leading 하시는 Terraform T101 Study 내용 요약

 

해당 Terraform Study 는 Terraform Up and Running 책을 기반으로 진행 중입니다.

 

 

1. Terraform Module ??


테라폼 모듈은 둘 이상의 환경 (Dev, Stage, Production ...) 에서 코드를 재사용하고,

여러 테라폼 리소스를 하나의 논리적 그룹으로 관리하기 위해 사용한다.

 

 

모듈을 사용하면 어떠한 점이 좋은 지 예시를 들어 이해해보자 !

  1. Dev 환경에 1개의 VPC 가 존재하고 그 속에 2개의 서브넷이 존재한다고 가정해보자.
  2. Prod 환경에서도 똑같은 구성으로 1개의 VPC와 그 속에 2개의 서브넷이 필요하다고 가정해보자.

 

이런 환경을 만들기 위해 우리는 Dev 에서 1개의 VPC 와 2개의 서브넷이 존재하는 테라폼 코드를 만들고,
Prod 에서도 동일하게 1개의 VPC 와 2개의 서브넷이 존재하는 테라폼 코드를 각각 만들어 배포 하였다.

 

 

예시는 굉장히 간단한 구성이기 때문에 코드를 작성하는 데 어려움이 없을 것이다.
하지만 좀 더 복잡하고 세부 설정이 필요한 리소스를 생성할 때, 각 환경에 필요한 코드를 각각 만드는 것은 굉장히 비효율적일 것이다.

 

 

이런 비효율을 탈피하기 위해 이런 생각을 할 수 있다.
동일한 환경이라면 구성에 대한 거대한 틀만 미리 잡아놓고 세세한 설정은 별도로 지정해주면 어떨까??


1개의 VPC 와 2개의 서브넷을 생성하는 틀만 잡아놓고 그 안에 부여되는 세부 CIDR Block 이나,

태그 등 변동값만 환경마다 다르게 부여하면 편하지 않을까??

 

 

이 때, 1개의 VPC와 2개의 서브넷을 생성하는 틀이 바로 모듈이라고 지칭한다.
아래 그림을 통해 더 자세하게 이해해보자 !

 

Before 방식이 이전에 우리가 작업했던 방식이다.
각 환경마다 테라폼 코드를 만들어 관리해줘야 했다.
재사용하기 위해서는 코드 전체를 복사한 후, 삽입되어 있는 값들을 하나씩 바꿔줘야 했기에 시간이 많이 소모되었다.

 

 

After 방식은 테라폼에서 제공하는 모듈 방식이다.
각 환경 (dev, prod, stg) Root_Module 의 테라폼 코드들은 Child_Module 의 테라폼 코드를 가져와서 그대로 사용하되,
Child_Module 에서 지정된 Variables 값만 변경해서 사용하면 된다.


때문에, 테라폼 코드의 재사용이 훨씬 쉬워지는 것이다.

 

테라폼 모듈에 대한 용어를 좀 더 알아보자 !

 

 

1.1. Root Module

루트 모듈은 현재 내가 작업하고 있는 작업 디렉토리 (Working Directory)

실제 Terraform init, plan, apply 명령어를 입력하는 디렉토리가 루트 모듈이라고 생각하면 된다.
편의를 위해 루트 모듈이라고 부르기 보다는 각 환경(Dev, Stg, Prod) 의 이름으로 더 많이 지칭하는 듯 하다.

 

 

1.2. Child Module

차일드 모듈은 루트 모듈이 테라폼 구성에 필요한 리소스를 가져오는 곳

우리가 모듈화를 시켜 재사용 가능한 모듈을 만든다고 할 때는 이 차일드 모듈을 생성한다고 생각하면 된다.
편의를 위해 그냥 '모듈', '소스 모듈' 이라고 지칭하기도 한다.

 

 

1.3. Published Module

Local 공간이 아닌, Terraform Registry, Terraform Cloud 등과 같이 원격에 저장된 모듈

Published Module 은 원격에 저장되어 권한이 있는 사람이 호출할 수 있게 되어있다.

 

  • 사용 가능한 원격 모듈 저장소가 궁금하다면 다음 링크를 참고해보자!
 

Module Sources | Terraform | HashiCorp Developer

The source argument tells Terraform where to find child modules's configurations in locations like GitHub, the Terraform Registry, Bitbucket, Git, Mercurial, S3, and GCS.

developer.hashicorp.com

 

 

 

2. Terraform Module 사용법


테라폼 모듈은 다음과 같은 구문으로 사용할 수 있다.

module "<NAME>" {
  source = "<SOURCE>"

  [CONFIG ...]
}
  • NAME : 해당 모듈을 지칭할 이름
  • SOURCE : 불러올 모듈(Child Module)이 존재하는 디렉토리
                       만약 로컬 경로라면 해당 경로는 상대 경로로 지정해줘야 한다.
  • CONFIG : Child Module 의 변수 입력값
                      CONFIG 를 어떻게 지정해주는 가에 따라 모듈을 잘 사용할 수 있게 된다.



2.1. Child Module 작성

간단한 사용 예시를 보면서 이해해보자 !
현재 디렉토리 구조이다.
이 구조는 이전 포스팅인 File Layout 에서 사용되었던 것과 동일하지만 편의를 위해 디렉토리 구조만 살짝 변경했다.

 

.
├── 01_global
│   └── 01_s3
│       ├── 10_main_backend.tf
│       └── 99_outputs.tf
├── 02_module
│   └── 01_data-stores
│       ├── 01_variables.tf
│       ├── 10_main_vpc.tf
│       ├── 11_main_rds.tf
│       └── 99_outputs.tf
└── 03_stage
    └── 01_mysql
        └── 10_main.tf

 

S3 백엔드는 이제 기본이니 미리 배포해놓자 !
그럼 Module 안 코드를 살펴보자

 

 

참고로 현재 실습에서는 입력 변수를 사용하지 않고 이전 포스팅에서 사용한 코드를 그대로 사용할 것이다.
왜 모듈에서 입력 변수를 사용해야 하는 지 따라하면 알 수 있을 것이다 !!

 

또한, Child Module 에서는 Provider 를 지정하지 않는다.
Provider 에 대한 설정은 Root Module 에서 지정해줄 것이다.

모듈 재사용성에 대한 유연함을 위해 Provider 버전 등의 값은 Root Module 에서 따로 지정해주는 것이다.

 

Module. 10_main_vpc.tf

resource "aws_vpc" "scott_vpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-study"
  }
}

resource "aws_subnet" "scott_subnet3" {
  vpc_id     = aws_vpc.scott_vpc.id
  cidr_block = "10.10.3.0/24"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "terraform-subnet3"
  }
}

resource "aws_subnet" "scott_subnet4" {
  vpc_id     = aws_vpc.scott_vpc.id
  cidr_block = "10.10.4.0/24"

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "terraform-subnet4"
  }
}

resource "aws_route_table" "scott_rt2" {
  vpc_id = aws_vpc.scott_vpc.id

  tags = {
    Name = "terraform-rt2"
  }
}

resource "aws_route_table_association" "scott_rt_association3" {
  subnet_id      = aws_subnet.scott_subnet3.id
  route_table_id = aws_route_table.scott_rt2.id
}

resource "aws_route_table_association" "scott_rt_association4" {
  subnet_id      = aws_subnet.scott_subnet4.id
  route_table_id = aws_route_table.scott_rt2.id
}

resource "aws_security_group" "scott_sg2" {
  vpc_id      = aws_vpc.scott_vpc.id
  name        = "terraform SG - RDS"
  description = "terraform Study SG - RDS"
}

resource "aws_security_group_rule" "rds_sg_inbound" {
  type              = "ingress"
  from_port         = 0
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.scott_sg2.id
}

resource "aws_security_group_rule" "rds_sg_outbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.scott_sg2.id
}

 

Module. 11_main_rds.tf

resource "aws_db_subnet_group" "scott_db_subnet" {
  name       = "scott_db_subnet_group"
  subnet_ids = [aws_subnet.scott_subnet3.id, aws_subnet.scott_subnet4.id]

  tags = {
    Name = "My DB subnet group"
  }
}

resource "aws_db_instance" "scott_rds" {
  identifier_prefix      = "terraform"
  engine                 = "mysql"
  allocated_storage      = 10
  instance_class         = "db.t2.micro"
  db_subnet_group_name   = aws_db_subnet_group.scott_db_subnet.name
  vpc_security_group_ids = [aws_security_group.scott_sg2.id]
  skip_final_snapshot    = true

  db_name                = var.db_name
  username               = var.db_username
  password               = var.db_password
}

 

Module. 01_variables.tf
variable "db_username" {
  description = "The username for the database"
  type        = string
  sensitive   = true
}


variable "db_password" {
  description = "The password for the database"
  type        = string
  sensitive   = true
}

variable "db_name" {
  description = "The name to use for the database"
  type        = string
  default     = "terraformdb"
}



2.2. Stage Root Module 작성

Stage. 10_main.tf
provider "aws" {
    region = "ap-northeast-2"
}

terraform {
  backend "s3" {
    bucket = "scott-terraform-study-tfstate"
    key    = "stage/data-stores/mysql/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks-files"
  }
}

module "mysql" {
    source = "../../02_module/01_data-stores/"

    db_username = "terraform"
    db_password = "terraform!!"
}
  1. 해당 모듈의 이름은 mysql 이라고 지칭하였다.
  2. 해당 모듈이 참조할 Child Module 위치를 상대 경로로 지정해주었다.
  3. Child Module 내에 Default 값이 지정되어 있지 않은 변수인
    db_username , db_password 에 대해 Stage 에서 사용할 값을 입력했다.

 

2.3. Stage Module 배포

terraform init 단계에서는 모듈 설치, 백엔드 설정, Provider 리소스 다운로드 등의 작업을 실시한다고 했다.

 

만약 init 단계를 실행하지 않으면 어떻게 될까??

예상했듯이 모듈이 설치되어 있지 않다고 나온다.
때문에, 모듈을 사용하기 위해서는 반드시 init 작업이 수행되어야함을 알 수 있다.

 

 

  • terraform init 을 수행하자

 

.terraform 디렉토리에 module 에 대한 정보가 새로 생성된 것을 확인할 수 있다.
해당 파일을 확인하면 Source Module 의 위치를 바라보고 있는 것을 알 수 있다.

 

 

이후 apply 까지 실행시켜보자
RDS 배포에 시간이 걸리지만 성공적으로 배포했다 !

 

간단하게 Stage 환경에 배포를 완료하였다.
앞서 모듈은 재사용성이 높다고 했으니, dev 폴더를 만들고 동일하게 코드를 작성해서 배포해보자

 

 

2.4. Dev Module 배포 ( ! 오류 발생 !)

Dev. 10_main.tf
provider "aws" {
    region = "ap-northeast-2"
}

terraform {
  backend "s3" {
    bucket = "scott-terraform-study-tfstate"
    key    = "dev/data-stores/mysql/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks-files"
  }
}

module "mysql" {
    source = "../../02_module/01_data-stores/"

    db_username = "terraform"
    db_password = "terraform!!"
}

 

일련의 작업을 거쳐 apply 까지 실행해보면 다음과 같은 오류가 발생함을 알 수 있다.

이것이 바로 Module 에서 입력 변수를 사용해야 하는 이유이다.

 

Child Module 에서 하드 코딩되어 값이 입력되어 있기 때문에, Root Module 에서는 그 값을 그대로 가져다 사용한다.
하지만 AWS 에서는 동일한 값이 들어갈 수 없는 리소스들이 있다.

 

해당 오류에서 볼 수 있는 DB서브넷 그룹, S3 버킷 등이 그것이다.
때문에 입력변수를 사용하여 문제를 해결해야 한다.



 

3. 입력 변수를 활용한 Module 생성


3.1. Child Module 편집

Module. 10_main_vpc.tf
  • 만드는 김에 이후 알아보기 쉽게 vpc 에 환경에 따른 태그를 추가하고 VPC 및 서브넷 범위를 변수처리했다.

resource "aws_vpc" "scott_vpc" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true

  tags = {
    Name = "${var.env}-terraform-study"
  }
}

resource "aws_subnet" "scott_subnet3" {
  vpc_id     = aws_vpc.scott_vpc.id
  cidr_block = var.subnet3_cidr

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "terraform-subnet3"
  }
}

resource "aws_subnet" "scott_subnet4" {
  vpc_id     = aws_vpc.scott_vpc.id
  cidr_block = var.subnet4_cidr

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "terraform-subnet4"
  }
}

resource "aws_route_table" "scott_rt2" {
  vpc_id = aws_vpc.scott_vpc.id

  tags = {
    Name = "terraform-rt2"
  }
}

resource "aws_route_table_association" "scott_rt_association3" {
  subnet_id      = aws_subnet.scott_subnet3.id
  route_table_id = aws_route_table.scott_rt2.id
}

resource "aws_route_table_association" "scott_rt_association4" {
  subnet_id      = aws_subnet.scott_subnet4.id
  route_table_id = aws_route_table.scott_rt2.id
}

resource "aws_security_group" "scott_sg2" {
  vpc_id      = aws_vpc.scott_vpc.id
  name        = "terraform SG - RDS"
  description = "terraform Study SG - RDS"
}

resource "aws_security_group_rule" "rds_sg_inbound" {
  type              = "ingress"
  from_port         = 0
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.scott_sg2.id
}

resource "aws_security_group_rule" "rds_sg_outbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.scott_sg2.id
}

 

Module. 11_main_rds.tf
  • 서브넷 그룹 네임을 변수 처리하였다.

resource "aws_db_subnet_group" "scott_db_subnet" {
  name       = var.db_sub_group_name
  subnet_ids = [aws_subnet.scott_subnet3.id, aws_subnet.scott_subnet4.id]

  tags = {
    Name = "My DB subnet group"
  }
}

resource "aws_db_instance" "scott_rds" {
  identifier_prefix      = "terraform"
  engine                 = "mysql"
  allocated_storage      = 10
  instance_class         = "db.t2.micro"
  db_subnet_group_name   = aws_db_subnet_group.scott_db_subnet.name
  vpc_security_group_ids = [aws_security_group.scott_sg2.id]
  skip_final_snapshot    = true

  db_name                = var.db_name
  username               = var.db_username
  password               = var.db_password
}

 

Module. 01_variables.tf
  • 앞에서 변수 처리한 값들을 모두 variables.tf 내에 정의해주었고, default 값은 따로 주지 않았다.
    이렇게 된다면 반드시 Root Module 에서 변수 값을 지정해주어야 한다.
variable "db_username" {
  description = "The username for the database"
  type        = string
  sensitive   = true
}

variable "db_password" {
  description = "The password for the database"
  type        = string
  sensitive   = true
}

variable "db_name" {
  description = "The name to use for the database"
  type        = string
  default     = "terraformdb"
}

variable "env" {
  description = "Environment"
  type        = string
}

variable "vpc_cidr" {
  description = "VPC CIDR Block"
  type        = string
}

variable "subnet3_cidr" {
  description = "Subnet3 CIDR Block"
  type        = string
}

variable "subnet4_cidr" {
  description = "Subnet4 CIDR Block"
  type        = string
}

variable "db_sub_group_name" {
  description = "DB Subnet Group Name"
  type        = string
}

 

3.2. Stage Root Module 편집

Stage. 10_main.tf
provider "aws" {
    region = "ap-northeast-2"
}

terraform {
  backend "s3" {
    bucket = "scott-terraform-study-tfstate"
    key    = "stage/data-stores/mysql/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks-files"
  }
}

module "mysql" {
    source = "../../02_module/01_data-stores/"

    db_username  = "terraform"
    db_password  = "terraform!!"

    env          = "stg"
    vpc_cidr     = "10.10.0.0/16"
    subnet3_cidr = "10.10.3.0/24"
    subnet4_cidr = "10.10.4.0/24"
    db_sub_group_name = "stg-subnet-group"
}

 

3.3. Dev Root Module 편집

Dev. 10_main.tf
  • Dev 환경에 맞게 살짝 변경해주자
provider "aws" {
    region = "ap-northeast-2"
}

terraform {
  backend "s3" {
    bucket = "scott-terraform-study-tfstate"
    key    = "dev/data-stores/mysql/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks-files"
  }
}

module "mysql" {
    source = "../../02_module/01_data-stores/"

    db_username  = "terraform"
    db_password  = "terraform!!"

    env          = "dev"
    vpc_cidr     = "10.20.0.0/16"
    subnet3_cidr = "10.20.3.0/24"
    subnet4_cidr = "10.20.4.0/24"
    db_sub_group_name = "dev-subnet-group"
}

 

 

stg , dev 모두 배포해보자

 

입력한 변수값에 맞게 리소스가 생성된 것을 확인할 수 있다 !!

 

 

 

실습 진행 후에는 반드시 리소스를 삭제하자!

 


 

이번 포스팅에서는 모듈을 사용하는 가장 기본적인 방법에 대해서 알아보았다.

 

기본적인 원리는 이러하고 이를 변형시켜서 tfvars 파일에 모듈 변수값을 모두 입력한다던지,

output 을 통해 모듈의 output 값을 가져온다던지 하는 방식이 존재한다.

 

또한, 앞에서 배웠던 terraform_remote_state 를 활용하면 Add-On 모듈을 생성할 수도 있을 것이다.

 

모듈에 대한 기본적인 구조만 이해하고 있다면 이제 어떻게 가독성 있고 유연하게 만들어 내냐가 문제인 것이다.

이는 많은 테라폼 코드를 보거나, 코드를 많이 생성해보면서 습득하는게 방법이 아닐까 싶다 !

 

다음 포스팅에서는 테라폼 반복문을 통해 반복적인 리소스를 간단하게 생성하는 법을 알아보자 !!

 

Contents

포스팅 주소를 복사했습니다