시리즈 목차⌗

Terraform을 사용하다보면 동일한 유형의 리소스를 여러번 생성해야 할 때가 있습니다. 예를 들면 개발, 스테이징, 운영 환경을 각각 분리된 EC2 인스턴스로 생성한다고 했을 때, 거의 동일한 EC2 인스턴스를 리소스로 정의하게 되겠죠. 지금까지는 동일한 코드를 복사해서 붙여넣는 방식으로 각 리소스를 정의했습니다. 그런데 이렇게 하면 EC2 인스턴스의 설정이 복잡해지고, 개수가 많아질 수록 중복되는 코드의 양이 늘어나겠죠? 만약 기존에 정의된 EC2 인스턴스의 AMI를 변경해야 한다면, 모든 코드를 찾아서 일일이 변경해야만 할 겁니다. 그리고 이렇게 하다 보면 실수할 확률이 높아지겠죠?

이렇게 동일한 리소스를 반복해서 사용해야 하는 경우, 리소스의 공통적인 내용을 재사용 가능한 형태로 정의해놓고 필요할 때마다 참조해서 사용하되, 바뀌는 부분만 따로 명시해줄 수 있으면 좋을 겁니다. 이런 재사용 가능한 형태로 정의된 리소스를 모듈이라고 합니다.

모듈이란?⌗

Terraform 모듈은 동일한 유형의 리소스를 여러 번 생성할 때, 코드를 중복해서 작성하지 않도록 재사용 가능한 형태로 정의할 수 있습니다. 프로그래밍 언어에서는 함수나 클래스를 떠올려보시면 비슷합니다. 자 그럼 우선 모듈을 사용하기 전에 중복해서 리소스를 정의하는 코드를 먼저 확인해볼까요?

// AWS 프로바이더 설정
provider "aws" {
  // 아시아 태평양(서울) 리전
  region = "ap-northeast-2"
}

// 개발 환경 EC2 인스턴스
resource "aws_instance" "example_dev" {
  // ap-northeast-2 리전의 Ubuntu 20.04 AMI
  ami           = "ami-0c6e5afdd23291f73"
  instance_type = "t2.micro"

  vpc_security_group_ids = [aws_security_group.example_dev.id]

  // 최초 인스턴스 생성 시에만 실행
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

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

resource "aws_security_group" "example_dev" {
  name = "terraform-example"

  // 인바운드 규칙
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

// 스테이징 환경 EC2 인스턴스
resource "aws_instance" "example2_stg" {
  // ap-northeast-2 리전의 Ubuntu 20.04 AMI
  ami           = "ami-0c6e5afdd23291f73"
  instance_type = "t2.micro"

  vpc_security_group_ids = [aws_security_group.example_stg.id]

  // 최초 인스턴스 생성 시에만 실행
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

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

resource "aws_security_group" "example_stg" {
  name = "terraform-example-stg"

  // 인바운드 규칙
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

개발 환경과 스테이징 환경에서 동작할 EC2 인스턴스를 2개 정의해놓았습니다. 대부분의 코드가 중복되어 있죠? 간단한 코드지만 계속해서 늘어난다고 생각하면 좀 별로이긴합니다. 그러면 일단 중복을 제거할 수 있도록 모듈을 작성해보겠습니다. modules/ec2 폴더를 만들고, 그 안에 ec2 폴더를 만들어서 main.tf 파일을 생성합니다.

// 로컬 변수 정의
locals {
  // EC2 인스턴스에 할당할 태그
  security_group_name = "terraform-example-${var.env}"
}

resource "aws_instance" "ec2" {
  // ap-northeast-2 리전의 Ubuntu 20.04 AMI
  ami           = var.ami_id # 변수 ami_id 참조
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.sg.id]

  // 최초 인스턴스 생성 시에만 실행
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p ${var.port} &
              EOF

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

resource "aws_security_group" "sg" {
  name = local.security_group_name

  // 인바운드 규칙
  ingress {
    from_port   = var.port
    to_port     = var.port
    protocol    = "tcp"
    cidr_blocks = var.cidr_blocks
  }
}

그리고 variables.tf 파일을 생성해서 변수를 정의합니다.

variable "env" {
  type = string
  // 변수 값이 "dev", "stg", "prod" 중에 하나로 설정되야함
  validation {
    condition = contains(["dev", "stg", "prod"], var.env)
    error_message = "env must be one of dev, stg, prod"
  }
}

variable "ami_id" {
  type = string
  default = "ami-0c6e5afdd23291f73"
}

variable "instance_type" {
  type = string
  default = "t2.micro"
}

variable "port" {
  type = number
}

variable "cidr_blocks" {
  type = list(string)
  default = ["0.0.0.0/0"]
}

마지막으로 outputs.tf 파일을 생성해서 EC2 인스턴스의 public IP 주소를 출력하도록 설정합니다.

output "public_ip" {
  value = aws_instance.ec2.public_ip
}

이제 모듈을 사용하는 코드를 작성해보겠습니다. dev/ec2 폴더에 main.tf 파일을 생성하고, 다음과 같이 작성합니다.

// AWS 프로바이더 설정
provider "aws" {
  // 아시아 태평양(서울) 리전
  region = "ap-northeast-2"
}

// 모듈 사용
module "ec2" {
  // 모듈의 소스 경로 지정
  source = "../../modules/ec2"

  // 모듈에 전달할 인자
  env = "dev"
  port = 8080
}

모듈의 소스 경로를 상대 경로로 지정해주고, 모듈에 전달하고 싶은 변수를 지정해줍니다. 그리고 stg/ec2 폴더에도 main.tf 파일을 생성하고, 다음과 같이 작성합니다.

// AWS 프로바이더 설정
provider "aws" {
  // 아시아 태평양(서울) 리전
  region = "ap-northeast-2"
}

// 모듈 사용
module "ec2" {
  source = "../../modules/ec2"

  // 모듈에 전달할 인자
  env = "stg"
  port = 8080
}

그리고 모듈의 output을 이용해서 EC2 인스턴스의 public IP 주소를 출력하도록 outputs.tf 파일을 생성합니다.

output "public_ip" {
  value = module.ec2.public_ip
}

참고로 폴더 구조는 다음과 같습니다.

.
├── modules
│   └── ec2
│       ├── main.tf
│       └── variables.tf
|       └── outputs.tf
├── dev
│   └── ec2
│       └── main.tf
│       └── outputs.tf
└── stg
    └── ec2
        └── main.tf
        └── outputs.tf

우선 dev/ec2 폴더에서 tf init 명령을 실행합니다.

terraform이라고 매번 입력하는게 어려우니 .zshrc나 .bashrc에 다음과 같이 alias 설정을 해서 tf로 사용하는 것도 방법입니다.

alias tf=terraform
> cd dev/ec2
> tf init

Initializing the backend...
Initializing modules...
- ec2 in ../../modules/ec2

Initializing provider plugins...
...

가장 먼저 지정된 모듈의 소스 경로에서 모듈을 가져와서 초기화 하는 걸 확인할 수 있죠? 그리고 tf plan 명령을 실행해볼까요?

참고로 모듈을 수정한 다음에 수정된 모듈을 반영하려면 tf get -update명령을 실행해야 합니다.

> tf plan
...
Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + public_ip = (known after apply)
...

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

Outputs:

public_ip = "..."

모듈에 설정된 내용대로 인스턴스가 생성되었고, 모듈의 출력으로 설정된 public_ip도 출력을 확인할 수 있습니다. 이제 stg/ec2 폴더에서도 동일한 명령을 실행해서 인스턴스를 추가로 생성할 수 있습니다.

정리⌗

모듈을 활용하면 코드의 중복을 줄일 수 있으며, 수작업으로 인해 실수를 방지할 수 있습니다. 하지만 모듈은 또 다른 단점을 가지고 있습니다. 모듈을 수정하게 되면 모듈을 참조하는 모든 리소스의 상태 관리에 영향을 미친다는 점입니다. 어떻게 하면 모듈을 업데이트하면서도 특정 리소스는 이전 모듈을 참조하고, 특정 리로스는 새로 업데이트된 모듈을 참조하도록 할 수 있을까요? 다음 시간에는 그 방법을 알아보겠습니다!

참고자료⌗

License⌗

저작자표시-비영리-변경금지 2.0 대한민국 (CC BY-NC-ND 2.0 KR)