04. 테라폼 모듈
이 문서는 책 "테라폼 설치에서 운영까지"을 읽고 작성되었습니다. 최대한 요약해서 책 내용을 최소로 하는데 목표를 두고 있어서, 더 친절하고 정확한 내용을 원하신다면 책을 구매하시는 것을 추천드립니다. => 책 링크
3장을 모두 진행하고 오면 디렉토리 구조는 다음과 같다.
src
|- ch03
|- global
|- s3
|- stage
|- data-stores
|- mysql
|- services
|- webserver-cluster
여기서 4장 코드는 stage
를 복사해서 prod
를 만들어서 다음과 같은 구조로 변경한다.
src
|- ch04
|- global
|- s3
|- stage
|- data-stores
|- mysql
|- services
|- webserver-cluster
|- prod (stage 복사)
|- data-stores
|- mysql
|- services
|- webserver-cluster
보통 stage
는 개발 환경, prod
는 상용 환경을 뜻한다. 그리고 이들의 코드는 각 환경에 의존해야 하는 코드 외 거의 유사하다. 때문에 필연적으로 중복되는 코드가 발생한다. module
은 Terraform
에서 코드 중복을 제거하는 주요한 기능 중 하나이다. 이번 장에서는 이를 다룬다.
모듈 만드는 법
이제 data-stores/mysql
코드를 모듈로 변경해보자. 먼저 다음과 같이 디렉토리 구조로 변경한다.
src
|- ch04
|- global
|- s3
|- modules
|- data-stores
|- mysql
|- services
|- webserver-cluster
|- stage
|- data-stores
|- mysql
|- services
|- webserver-cluster
|- prod (stage 복사)
|- data-stores
|- mysql
|- services
|- webserver-cluster
먼저 3장에서 코드는 다음과 같았다.
src/ch03/stage/data-stores/mysql/main.tf
terraform {
backend "s3" {
bucket = "gurumee-terraform-state"
key = "prod/data-stores/mysql/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "gurumee-terraform-lock"
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_db_instance" "example" {
engine = "mysql"
allocated_storage = 10
instance_class = "db.t2.micro"
name = "example_database"
username = "admin"
password = var.db_password
skip_final_snapshot = true
}
여기서 resource
부분만 따로 복사해서 modules/data-stores/mysql/main.tf
에 붙여 넣는다. 그 후 다음과 같이 수정한다.
src/ch04/modules/data-stores/mysql/main.tf
resource "aws_db_instance" "mysql" {
engine = "mysql"
allocated_storage = var.db_allocated_storage
instance_class = var.db_instance_class
name = var.db_name
username = var.db_username
password = var.db_password
skip_final_snapshot = true
}
위 코드는 프로그래밍 언어로 치면 함수 본문과 같다. 그리고 같은 경로에 다음과 같이 variables.tf
를 만든다.
src/ch04/modules/data-stores/mysql/variables.tf
variable "db_allocated_storage" {
type = number
description = "The allocated storage for the database"
}
variable "db_instance_class" {
type = string
description = "The instance class for the database"
}
variable "db_name" {
type = string
description = "The name for the database"
}
variable "db_username" {
type = string
description = "The username for the database"
}
variable "db_password" {
type = string
description = "The password for the database"
}
module
에서 variables.tf
는 프로그래밍 언어로 치면 입력과 같다. 이제 출력을 담당하는 outputs.tf
를 다음과 같이 만들어둔다.
src/ch04/modules/data-stores/mysql/outputs.tf
output "address" {
value = aws_db_instance.mysql.address
}
output "port" {
value = aws_db_instance.mysql.port
}
이렇게 하면 mysql
데이터베이스를 생성하는 module
의 만들어진 것이다. 이런 방식으로 services/webserver-cluster
역시 module
로 만들 수 있다. 이는 스스로 해본다. 코드는 다음을 확인하면 된다.
모듈 사용하는 법
이제 module
을 사용해보자. prod/data-stores/mysql
의 main.tf
를 다음과 같이 수정한다.
src/ch04/prod/data-stores/mysql/main.tf
# ...
# resource 부분을 아래와 같이 변경한다.
module "mysql" {
source = "../../../modules/data-stores/mysql"
db_allocated_storage = 10
db_instance_class = "db.t2.micro"
db_name = "prod_db"
db_username = "admin"
db_password = var.db_password
}
위와 같이 함수의 입력을 파라미터 전달하듯이, module
의 variables.tf
에서 선언했던 변수들의 값을 할당해주어야 한다. 이제 outputs.tf
를 다음과 같이 변경한다.
src/ch04/prod/data-stores/mysql/outputs.tf
output "address" {
value = module.mysql.address
}
output "port" {
value = module.mysql.port
}
그렇다. module.<모듈 이름>.<모듈에서 선언한 output 이름>
으로 module
의 출력 값을 다른 코드에서 쓸 수가 있다. 그리고 module
로 코드로 변경했을 때 terraform plan
이나 terraform apply
등의 명령어를 쓸 때 terraform init
명령어를 먼저 해주어야 한다.
module
을 사용할 경우 terraform init
명령어 사용 시, .terraform/modules
라는 디렉토리가 생성되는데 이 때 필요한 코드가 저장되어 있다. 따라서 module
을 추가/변경이 있을 경우 반드시 terraform init
명령어가 필요하다.
github에서 모듈 가져오기
이제 github
에서 소스코드를 가져와보자. 이번에는 stage
환경에서 진행한다. main.tf
를 다음과 같이 변경한다.
src/ch04/stage/data-stores/mysql/main.tf
# ...
# resource 부분을 아래와 같이 변경한다.
module "mysql" {
source = "github.com/gurumee92/today-i-learned//terraform_up_and_running/src/ch04/modules/data-stores/mysql"
db_allocated_storage = 10
db_instance_class = "db.t2.micro"
db_name = "stage_db"
db_username = "admin"
db_password = var.db_password
}
역시 outputs.tf
를 prod
때와 같이 module
의 output
을 이용하도록 코드를 변경한다.
src/ch04/stage/data-stores/mysql/outputs.tf
output "address" {
value = module.mysql.address
}
output "port" {
value = module.mysql.port
}
이제 terraform init
후, terraform apply
하면 역시 mysql
1대를 구성할 수 있음을 확인할 수 있다. 이 때 주의할 점은 해당 레포지토리 다음 경로를 "//" 이렇게 잡아주어야 한다.
현재 내 레포지토리는 "github.com/gurumee92/today-i-learned"이다. 그리고 현재 작업한 코드 경로는 "terraform_up_and_running/src/ch04/modules/data-stores/mysql"에 있다.
따라서 "../today-i-learned//terraform_up_and_running/.." 이런식으로 경로를 잡아주어야 한다. 외부에서 코드를 가져올 때는 항상 최신인지 확인이 필요하다. 만약 최신 버전이 아니라면 다음 명령어로 업데이트가 가능한다.
$ terraform get -update
모듈 주의점
모듈을 만들 때, 다음과 같은 사항을 주의해야 한다.
- 파일 경로
- 인라인 블록
먼저 인라인 블록의 경우는 최대한 분리하는 것이 좋다. 3장에서 elb
의 security group
의 코드는 다음과 같았다.
src/ch03/stage/services/webserver-cluster/main.tf
# ...
resource "aws_security_group" "elb" {
name = "terraform-example-elb"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ...
이 때 ingress
, egress
모두 다음과 같이 분리할 수 있다.
src/ch04/modules/services/webserver-cluster/main.tf
# ...
resource "aws_security_group" "elb" {
name = "${var.cluster_name}-elb"
}
resource "aws_security_group_rule" "allow_http_inbound" {
type = "ingress"
security_group_id = aws_security_group.elb.id
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "allow_all_outbound" {
type = "egress"
security_group_id = aws_security_group.elb.id
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# ...
이렇게 분리하면 뭐가 좋으냐.. 환경에 따라 같은 security group
일지라도 다른 inbound/outbound 설정이 가능하다. 가령 security group elb
의 개발 환경에서는 port 12345가 inbound 규칙에 추가되어야 한다면 어떻게 할 것인가? 위와 같이 분리가 되었다면 그 작업은 매우 쉽다. security group elb
의 id를 module
의 output
으로 지정한 뒤 다음처럼 코드를 작성하면 된다.
src/ch04/stage/services/webserver-cluster/main.tf
module "webserver_cluster" {
source = "github.com/gurumee92/today-i-learned//terraform_up_and_running/src/ch04/modules/services/webserver-cluster"
cluster_name = "websever-stage"
db_remote_state_bucket = "gurumee-terraform-state"
db_remote_state_key = "stage/data-stores/mysql/terraform.tfstate"
instance_type = "t2.micro"
min_size = 2
max_size = 2
server_port = 8080
}
# 이렇게 추가하면 된다.
resource "aws_security_group_rule" "allow_testing_inbound" {
type = "ingress"
security_group_id = module.webserver_cluster.elb_security_group_id
from_port = 12345
to_port = 12345
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
이번엔 파일 경로이다. 3장의 경우 user-data
의 값을 주기 위해서 user-data.tpl
을 작성하고 이에 대한 값을 넣어주었었다.
src/ch03/stage/services/webserver-cluster/main.tf
# ...
resource "aws_launch_configuration" "example" {
image_id = "ami-0d5eff06f840b45e9"
instance_type = "t2.micro"
security_groups = [aws_security_group.instance.id]
user_data = templatefile("user-data.tpl", {
server_port = var.server_port,
db_address = data.terraform_remote_state.db.outputs.address,
db_port = data.terraform_remote_state.db.outputs.port,
})
lifecycle {
create_before_destroy = true
}
}
# ...
하지만 module/service/webserver-cluster
에도 user-data.tpl
을 넣더라도 정상적으로 작동하지 않을 것이다. 왜냐하면 현재 terraform
명령어가 실행된 파일 경로에서 파일을 탐색하기 때문이다. 이를 위해서 코드를 다음과 같이 변경해야 한다.
src/ch04/modules/services/webserver-cluster/main.tf
# ...
resource "aws_launch_configuration" "example" {
image_id = "ami-0d5eff06f840b45e9"
instance_type = var.instance_type
security_groups = [aws_security_group.instance.id]
# 여기가 중요하다. ${path.module}로 값을 지정해주어야 한다.
user_data = templatefile("${path.module}/user-data.tpl", {
server_port = var.server_port,
db_address = data.terraform_remote_state.db.outputs.address,
db_port = data.terraform_remote_state.db.outputs.port,
})
lifecycle {
create_before_destroy = true
}
}
# ...
${path.module}
로 terraform
에서 module
경로를 지정해주어야 코드가 정상적으로 동작한다. 이는 모듈 작업할 때 실수 할 수 있으니 잘 기억해두길 바란다.
4장 전체 코드
4장 전체 코드는 다음 링크에서 확인할 수 있다.