[Terraform] Terraform Module

Sunwu Park·2024년 11월 19일

싱송생송

목록 보기
6/6
post-thumbnail

서론

  • 프로젝트를 진행함에 있어 빠르게 배포를 하기 위해 그저 계속해서 리소스만 만들어대니깐 코드가 정말 많이 더러워 졌다. 고로 어서 모듈화를 진행하여 관련된, 중복되는 코드들을 최대한 줄여 모듈화를 진행하려고한다

What is Terraform Module?

  • groups of .tf files kept in different directory
  • 관련있는 리소스를 한곳에 모아 하나의 패키지를 만들어 사용할 수 있도록 하는 테라폼의 자원

Advantages

  • 캡슐화: 서로 연관이 있는 리소스들을 한데 모아 결합시키고 필요한 인자만 넘긴다
  • 재사용성: 한번 정의해둔 모듈은 여러번 호출 가능
  • 일관성: 리소스에 필요한 옵션들을 정의하여 옵션 없이 같은 설정값으로 리소스 생성 가능하다!

Types of Terraform Modules

  1. 루트 모듈 (Root Module)
  • 실제로 수행하게 되는 작업 디렉터리의 terraform 코드 모음
  • 모든 Terraform 구성이 루트 모듈에서 시작
  • main.tf(root)
  1. 자식 모듈 (Child Module)
  • root module에서 리소스를 생성하기 위해 참조하고 있는 module block
  • root module(모듈을 사용하는 코드)에서 건네주는 변수들을 사용해서 리소스를 생성
  • 특정 기능을 캡슐화하여 재사용성을 높이고, 코드의 유지보수를 용이
  • EC2, VPC, IAM, SG
    Terraform module을 활용한 기본 인프라 구축 - ImOk
  1. 공개 모듈 (Published Modules)

  • Terraform Registry에 게시된 모듈
  • 다른 사용자들이 사용할 수 있도록 공유
  • 표준화된 구조와 설정으로 쉽게 사용
    Terraform Registry Link

모듈: 입력 (Input)

  • 입력 변수는 모듈에 전달되는 매개변수
  • 자식 모듈의 변수는 호출하는 모듈에서 module 블록을 사용해 값을 전달
    (root 모듈에서 module블록 선언시 값 전달)
  • 변수 선언은 자식 모듈 내에서 variable 블록을 통해 이루어짐
  • 기본값 (Default Value)을 설정할 경우, 호출 시 값을 전달하지 않아도 기본값 사용

예제

resource "aws_instance" "appserver" { 
    ami           = var.ami
    instance_type = "t2.medium"
    tags = { 
        name = "${var.app_region}-app-server" 
    }
}

module "primaryapp" { 
    source     = "./modules/appstorage"
    app_region = "us-east-2"
    ami        = "ami-0010d386b82bc06f0"
}

모듈: 출력 (Output)

  • 출력 변수는 모듈이 특정 값을 반환
  • 이를 통해 호출 모듈이 자식 모듈의 결과값에 접근
  • 출력 변수는 module.<MODULE_NAME>.<OUTPUT_NAME> 형식으로 접근
  • 설명 (Description): 출력 변수에 포함된 내용에 대해 사용자에게 정보를 제공하는 데 사용
  • 값 (Value): 출력 변수의 결과로 반환될 표현식을 정의

예제

output "subnetid" {
    value = aws_instance.appserver.subnet_id
}

간단하게 나의 프로젝트에서 적용을 해보자

나의 VPC를 만든다고 할때

이런식으로 여러개의 모듈을 만든다고 하자.
간단하게 VPC모듈에 선언을 해야하는 것은 main.tf(로직), variables.tf(입력변수), outputs.tf(출력변수) 이다

간단한 VPC를 구성한다고 할때 아래와 같이 구성을 해보자

main.tf

// VPC 생성
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true
}

// Public Subnet 1개 생성
resource "aws_subnet" "public1" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnet_cidr_blocks[0]
  availability_zone = "ap-northeast-2a"
}

// Private Subnet 1개 생성
resource "aws_subnet" "private1" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidr_blocks[0]
  availability_zone = "ap-northeast-2a"
}

// Internet Gateway 생성
resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
}

// Public Route Table 생성
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = var.vpc_cidr_block
    gateway_id = "local"
  }

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }
}

// Public Route Table Association
resource "aws_route_table_association" "public_subnet1" {
  subnet_id      = aws_subnet.public1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_subnet2" {
  subnet_id      = aws_subnet.public2.id
  route_table_id = aws_route_table.public.id
}

variables.tf

variable "vpc_cidr_block" {
  description = "The CIDR block for the VPC"
  type        = string
}

variable "public_subnet_cidr_blocks" {
  description = "List of CIDR blocks for public subnets"
  type        = list(string)
}

variable "private_subnet_cidr_blocks" {
  description = "List of CIDR blocks for private subnets"
  type        = list(string)
}

variable "region" {
  description = "The AWS region where resources will be created"
  type        = string
}

다음과 같이 Variables.tf 를 선언함으로써 vpc모듈안에서의 main.tf 에서 var.~~ 으로 접근을 할 수가 있다.

또한 다른 리소스들은 vpc_id를 중요시하기 때문에 필요한 값들을 출력해야한다

outputs.tf

output "vpc_id" {
    description = "The VPC ID where the RDS instance is deployed"
    value       = aws_vpc.this.id
}

output "public_subnet_ids" {
    description = "A list of subnet IDs for the RDS subnet group"
    value       = [aws_subnet.public1.id, aws_subnet.public2.id]
}

output "private_subnet_ids" {
    description = "A list of subnet IDs for the RDS subnet group"
    value       = [aws_subnet.private1.id, aws_subnet.private2.id]
}

이런식으로 필요한 값들을 출력하면 된다

이제 이것을 root 모듈에서 선언을 해서 사용을 해보자!

module "vpc" {
  source = "./modules/vpc"

  vpc_cidr_block = var.vpc_cidr_block
  public_subnet_cidr_blocks = var.public_subnet_cidr_blocks
  private_subnet_cidr_blocks = var.private_subnet_cidr_blocks
  region = var.region
  vpc_name = "vpc-name"
  public_subnet_names = []
  private_subnet_names = []
  igw_name = "internet-gateway"
  public_route_table_name = "public-route-table"
  private_route_table_name = "private-route-table"
}

이러한 방식으로 VPC를 선언해주면 된다!

만약 다른 모듈에서 이 output값을 사용하고 싶다면?

module "ec2" {
  source = "./modules/ec2"

  vpc_id = module.vpc.vpc_id
  public_subnet1_id = module.vpc.public_subnet_ids[0]
}

다음과 같이 module.<MODULE_NAME>.id 와 같이 접근을 해서 가져올 수 있다!

어려웠던점

1. 만약 루트에 있는 variables.tf의 variable과 루트 안에 있는 variables.tf안의 variable이 같다면 생길 수 있는 문제?

  • 어떤 변수가 사용되고 있는지 혼란
  • 자식 모듈이 별도의 값을 기대하는데 루트 모듈의 변수를 전달하지 않으면 기본값을 사용하게 되어 의도하지 않은 결과를 초래
  • 루트 모듈에서 자식 모듈로 값을 전달하지 않고, 자식 모듈이 기본값을 사용하면 일관성 깨짐
  • 디버깅 과정에서 어떤 변수 값이 실제로 사용되고 있는지 확인하기 어려움

고로 변수 이름을 구분하여 값을 명시적으로 전달을 하였음

2. Dynamic, For_Each를 사용하는데 헷갈림

제대로 정리해보자

1. Dynamic Block

  • dynamic 블록은 리소스의 반복적인 서브블록(예: ingress, route)을 동적으로 생성하기 위해 사용
  • 블록 구조를 반복적으로 생성해야 할 때 유용

사용 목적

  • 리소스 내부의 반복적인 서브블록 생성.
  • 블록 개수나 내용이 조건이나 변수에 따라 달라질 때.

구문

dynamic "block_type" {
  for_each = var.some_list
  content {
    key = each.value.key
    value = each.value.value
  }
}

예제: Security Group의 ingress 블록

resource "aws_security_group" "example" {
  name        = "example-sg"
  vpc_id      = aws_vpc.example.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = each.value.from_port
      to_port     = each.value.to_port
      protocol    = each.value.protocol
      cidr_blocks = each.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

동작 설명

  • var.ingress_rules는 반복 가능한 값(예: list(map)).
  • 각 값은 from_port, to_port, protocol, cidr_blocks를 포함.
  • dynamic 블록이 for_each를 통해 모든 값에 대해 ingress 블록을 생성.

2. For_Each

  • for_each는 리소스나 모듈을 반복적으로 생성할 때 사용
  • 각각의 리소스가 고유한 속성을 가지며 여러 개를 생성해야 하는 경우 적합

사용 목적

  • 여러 리소스를 한 번에 생성.
  • 동적으로 특정 리소스만 선택적으로 생성.

구문

resource "resource_type" "resource_name" {
  for_each = var.some_map
  key      = each.key
  value    = each.value
}

예제: Subnet 생성

resource "aws_subnet" "this" {
  for_each = {
    public1  = { cidr_block = "10.0.1.0/24", availability_zone = "ap-northeast-2a" }
    public2  = { cidr_block = "10.0.2.0/24", availability_zone = "ap-northeast-2c" }
    private1 = { cidr_block = "10.0.3.0/24", availability_zone = "ap-northeast-2a" }
    private2 = { cidr_block = "10.0.4.0/24", availability_zone = "ap-northeast-2c" }
  }

  vpc_id            = aws_vpc.this.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone
}

동작 설명

  • var.some_map에는 각 서브넷의 속성이 포함된 맵(map)이 들어있음.
  • aws_subnet.this["public1"], aws_subnet.this["private1"] 등으로 생성된 리소스를 개별적으로 참조 가능.

혼란스러운 상황과 해결

Q1. Dynamic을 언제 사용해야 하나요?

  • 리소스 내에서 반복적으로 동일한 서브블록을 정의해야 할 때 사용.
  • 예: Security Group의 여러 ingress/egress 규칙, Route Table의 여러 route.

Q2. For_Each를 언제 사용해야 하나요?

  • 여러 개의 리소스를 반복적으로 생성할 때 사용
  • 예: 여러 Subnet, 여러 EC2 인스턴스.

Q3. Dynamic과 For_Each를 함께 사용할 수 있나요?

  • 가능합니다. Dynamic은 서브블록 반복에 사용하고, For_Each는 리소스 반복에 사용
  • 예:
resource "aws_security_group" "this" {
  for_each = var.security_groups

  dynamic "ingress" {
    for_each = each.value.ingress_rules
    content {
      from_port   = each.value.from_port
      to_port     = each.value.to_port
      protocol    = each.value.protocol
      cidr_blocks = each.value.cidr_blocks
    }
  }
}

Q4. 값이 없는 경우 처리 방법은?

  • for_each나 dynamic의 반복 대상이 비어 있으면 Terraform은 해당 리소스를 생성하지 않는다.
  • 이를 통해 조건에 따라 동적으로 리소스를 제외

0개의 댓글