Module

테라폼 구성 리팩터링

튜토리얼을 진행 하며 퍼블릭 클라우드에 프로비저닝 하는 방식을 살펴 보았다면 해당 챕터에서는 비효율적인 부분을 개선합니다.

Local scope variables

이전에 진행 했던 Tutorial 챕터에서 서브넷과 IGW, NAT Gateway 등을 생성할 때 변수를 분리하여 내부망과 외부망을 분리 시켰다.

하지만, 사용하는 값이 비슷했기 때문에 가독성을 더 향상 시키기 위해 변수를 활용 하여 서브넷 그룹을 하나로 묶는 방법을 선택할 수 있다.

Examples

resource "aws_subnet" "public" {
  for_each                = var.public_subnets
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.availability_zone
  map_public_ip_on_launch = true

  tags = {
    Name = each.key
  }
}

resource "aws_subnet" "private" {
  for_each                = var.private_subnets
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.availability_zone
  map_public_ip_on_launch = false

  tags = {
    Name = each.key
  }
}

"서브넷 그룹에서 Private 서브넷만 필요한데 어떻게 가져오지?"

테라폼 문법에서 제공 하는 for expression 을 살펴보면 반복문 수행 중 키의 값을 새로운 값으로 할당할 수 있는데 이 문법을 활용 하여 private subnet 을 지역 변수로 구성한다.

locals {
  private_subnets = {
    for key, subnet in var.subnets :
    key => subnet
    if(lookup(subnet, "nat_gateway_subnet", null)) != null
  }
}

💡 지역 변수를 할당한 후 제대로 구성 되었는지 확인하고 싶다면 아래와 같은 명령어를 통해 확인할 수 있다.

Command

terraform console

> local.private_subnets

그래서 현재 테라폼 구성을 살펴보면 서브넷이 하나의 그룹으로 통합 되어 있고 지역 변수로 내부망 서브넷을 별도로 분리 해두었다.

기존 내부망 서브넷을 사용하고 있던 NAT 관련 리소스는 리팩터링으로 인해 동작하지 않기 때문에 이 부분도 새롭게 수정한다.

resource "aws_eip" "nat_ips" {
  count = var.private_subnets != {} ? length(var.private_subnets) : 0
}

resource "aws_nat_gateway" "gateways" {
  count = var.private_subnets != {} ? length(var.private_subnets) : 0

  allocation_id = aws_eip.nat_ips[count.index].id
  subnet_id     = tolist(values(aws_subnet.public))[count.index].id
}

테라폼 구성은 동일 하지만 가독성을 위해 리팩터링을 모두 마쳤다면 테라폼 실행 계획을 살펴본 후 생성 후 재 생성 하는 테라폼 상태 파일을 제어 하여 변경 사항 없음으로 만들어준다.

> terraform state list
> terraform state mv aws_subnet.public\[\"oimarket-apne2-public-subnet-b\"\] aws_subnet.subnets\[\"oimarket-apne2-public-subnet-b\"\]
> terraform state mv aws_subnet.public\[\"oimarket-apne2-public-subnet-a\"\] aws_subnet.subnets\[\"oimarket-apne2-public-subnet-a\"\]
# ... 나머지 구성 변경 사항도 동일하게 적용
> terraform plan

튜토리얼에서 비용 발생에 대한 우려로 시나리오만 진행 하여 NAT Gateway와 Private Subnet을 만들지 않은 상태라면 동일하게 6개의 리소스가 새롭게 생성 되는 화면이 나오면된다.

Summary

이렇게 테라폼을 구성할 때 리소스를 가독성 있게 구성하는 방식이 중요한데, 그 중 첫번째 인 지역변수를 활용한 방식을 사용해보았다.

  • 리팩터링 시 terraform state mv 명령어를 사용하여 기존에 만들어둔 리소스를 이전해야 한다.

  • 유사한 변수를 하나로 합친 후 로컬 변수를 사용하여 리팩터링 할 수 있다.

  • lookup() 함수를 사용하여 Map type 의 변수에서 특정 키의 값을 조회 할 수 있다.

Routing table

기존 Public subnet 은 IGW를 통해 인터넷 통신을 이루고 Private subnet은 NAT Gateway를 통해 인터넷 통신을 이루도록 리스소를 정의했다.

이렇게 서브넷을 분리 시켰지만 별도의 라우팅 테이블을 만들지 않는다면 AWS VPC 정책에 의해 기본적으로 모두 로컬 라우팅 테이블에 포함되어 인터넷을 사용할 수 있게 된다.

그렇기 때문에 라우팅 테이블을 별도로 생성 하여 특정 서브넷에 대한 제어를 하도록 만든다.

라우팅 테이블을 만들기 위해 필요한 세 개의 리소스

  1. route_table: 라우팅 룰을 담고 있는 테이블

  2. route: 라우팅 룰, 주로 어떤 라우트 테이블에 속해 있으며 어떤 게이트웨이를 사용 하여 통신할지 정의

  3. route_table_association: 라우팅 룰에 포함 될 서브넷들

resource "aws_route_table" "route_tables" {
  for_each = var.subnets
  vpc_id   = aws_vpc.main.id

  tags = {
    Name = "${each.key}-route-table"
  }
}

resource "aws_route" "routes" {
  for_each = var.subnets

  route_table_id         = aws_route_table.route_tables[each.key].id
  destination_cidr_block = "0.0.0.0/0"

  gateway_id     = lookup(each.value, "nat_gateway_subnet", null) == null ? aws_internet_gateway.main.id : null
  nat_gateway_id = lookup(each.value, "nat_gateway_subnet", null) != null ? aws_nat_gateway.gateways[each.key].id : null
}

resource "aws_route_table_association" "associations" {
  for_each = var.subnets

  subnet_id      = aws_subnet.subnets[each.key].id
  route_table_id = aws_route_table.route_tables[each.key].id
}
Summary
  • lookup() 함수와 조건문을 조합해서 Private subnet, Public subnet일 때 게이트웨이를 다르게 설정할 수 있다.

Dynamic block with SG

동적 블럭을 활용하기 위해서 보안 그룹 규칙을 생성할 때 사용 하며 map(object) type, for_each, dynamic 등을 활용한다

"만약 동적 블럭을 구성하지 않고 보안 그룹을 생성하면 어떻게 될까?"

동적 블럭 없이 리소스를 정의 하면 리소스 정의 코드가 길어지며 그에 걸맞는 다양한 변수를 사용해야한다.

resource "aws_security_group" "permit_ssh_access" {

  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Permit SSH from anywhere"
  }

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

  tags = {
    Name = "permit_ssh_sg"
  }

}


resource "aws_security_group" "permit_http_" {

  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Permit HTTP from anywhere"
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Permit HTTP from anywhere"
  }

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

  tags = {
    Name = "permit_http_sg"
  }

}

두 코드는 거의 비슷하며 지금 단 두개의 보안 그룹 규칙을 생성했다. 만약, 4개, 5개 일 때 이와 같이 구성할 수 있겠는가? 이러한 비효율적인 코드를 줄이고 가독성을 높히는 것이 목적이기 때문에 동적 블럭을 활용하여 이 코드를 간소화할 수 있다.

resource "aws_security_group" "groups" {
  for_each = var.security_groups

  name   = each.key
  vpc_id = aws_vpc.main.id

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

  dynamic "egress" {
    for_each = each.value.egress_rules
    content {
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
    }
  }

  tags = {
    Name : each.key
  }
}
Summary
  • dynamic 지시자와 for_each를 조합해서 동적 블럭을 생성 하면 불필요한 코드를 효율적으로 리팩터링 할 수 있다.

Module

"모듈은 언제 사용할까?"

여러 리전에 걸쳐 위에서 만든 VPC를 재사용해야 한다고 하면 이런 시나리오를 만들어낼 수 있다.

위 코드를 Copy & Paste 로 사용할 경우 추후 변경되는 리소스에 대한 동기화를 위해 각 리전에서 정의 된 변수 파일을 탐색 하며 수정해야한다. 이 때 휴먼에러가 발생할 확률이 굉장히 높기 때문에 코드화 하는 의미가 많이 퇴색된다.

바람직하게 모듈을 이용하여 미리 정의된 리소스를 참조 하도록 만들기 위해서 아래와 같이 구성할 수 있다.

"모듈을 참조하는 방법"

기존 변수를 담고 있던 terraform.tfvars 파일은 이제 필요가 없어졌다. 정의된 리소스에 맞게 변수에 값을 할당 하는 것은 해당 모듈을 참조하는 main.tf로 책임을 넘겼기 때문이다. 그래서 아래 모듈 코드를 보면 변수를 길게 설정해둔 것을 볼 수 있다.

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

  vpc_name           = "oimarket-apne2"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["a", "b"]

  subnets = {
    "oimarket-apne2-public-subnet-a" = {
      availability_zone = "ap-northeast-2a"
      cidr_block        = "10.0.1.0/24"
    },
    "oimarket-apne2-public-subnet-b" = {
      availability_zone = "ap-northeast-2b"
      cidr_block        = "10.0.2.0/24"
    },
    "oimarket-apne2-private-subnet-a" = {
      availability_zone  = "ap-northeast-2a"
      cidr_block         = "10.0.11.0/24"
      nat_gateway_subnet = "oimarket-apne2-public-subnet-a"
    }
    "oimarket-apne2-private-subnet-b" = {
      availability_zone  = "ap-northeast-2b"
      cidr_block         = "10.0.12.0/24"
      nat_gateway_subnet = "oimarket-apne2-public-subnet-b"
    },
  }

  security_groups = {
    "oimarket-apne2-permit-ssh-security-group" = {
      ingress_rules = [
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 22
          protocol    = "tcp"
          to_port     = 22
        }
      ]

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

    "oimarket-apne2-permit-http-security-group" = {
      ingress_rules = [
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 80
          protocol    = "tcp"
          to_port     = 80
        },
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 443
          protocol    = "tcp"
          to_port     = 443
        }
      ]

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

}

위 모듈은 하위 모듈 파일을 참조하도록 선언하고, 해당 모듈이 사용하는 변수를 그대로 선언하여 사용한다.

이렇게 기존 리소스 구성에서 모듈화를 진행 하면 테라폼을 사용할 준비를 다시 해야하기 때문에 워크플로우를 익혀보자면아래와 같다.

1

모듈 참조 구성 및 Terraform init

기존 리소스에서 사용하던 상태 파일은 모듈로 넘어가있기 때문에 참조된 곳에 테라폼을 사용하기 위한 준비를 마친다.

2

Terraform plan

이 때, 실행 계획을 살펴보면 기존에 만들어둔 리소스가 모두 삭제되고 새롭게 재생성 되는 것을 볼 수 있는데 이러한 이유는 기존에는 모듈에서 참조하지 않았기 때문에 "aws_vpc.main"이었다면 모듈에 참조된 후 "module.aws_vpc.main"이 되어 상태파일에 변경이 생긴다.

3

Terraform state mv

terraform show -json | jq -r ".values.root_module.resources[] | .address" | awk '{gsub(/"/, "\\\"");print "terraform state mv "$1" module.vpc."$1}' | /bin/bash
  • terraform show -json: 테라폼의 상태 파일을 JSON으로 확인

  • jq -r: JSON Pretty print로 출력하고 읽음

    • ".values.root_module.resources[] | address" : 상태 파일의 리소스 이름만 출력

    • awk: 파일의 내용을 데이터화 하는 데 사용

    • gsub(/"/, "\\\""): " 로 시작하는 문자를 찾아 \" 로 변경

    • print "terraform state mv "$1": address에서 추출한 이름을 변경 대상으로 사용

    • module.vpc."$1: module.vpc.\"address 에서 추출한 이름으로 변경

    • bin/bash: 실행되는 코드를 라인마다 읽어 들여 Bash 로 실행 -> 이 때 반영 됨

Summary
  • 모듈을 만들면 테라폼 구성을 위한 코드 블록을 재사용할 수 있다.

  • 리팩터링을 하면 언제나 상태를 옮기는 작업이 필수적으로 수행되어야 한다.

Variables In Yaml File

"왜 YAML 파일을 활용해야 할까?"

기존 변수 파일로 관리 했을 때 변수를 선언하고 해당 변수에 대한 값을 할당 해서 사용 해야 했지만 YAML을 활용하게 되면 테라폼을 구성하는 데이터들과 실제 테라폼 로직을 분리할 수 있어 가독성이 좋아진다. 또한, 테라폼을 잘 모르더라도 YAML 파일만 수정하면 되는 유연함이 있다.

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

  vpc_name           = "oimarket-apne2"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["a", "b"]

  subnets = {
    "oimarket-apne2-public-subnet-a" = {
      availability_zone = "ap-northeast-2a"
      cidr_block        = "10.0.1.0/24"
    },
    "oimarket-apne2-public-subnet-b" = {
      availability_zone = "ap-northeast-2b"
      cidr_block        = "10.0.2.0/24"
    },
    "oimarket-apne2-private-subnet-a" = {
      availability_zone  = "ap-northeast-2a"
      cidr_block         = "10.0.11.0/24"
      nat_gateway_subnet = "oimarket-apne2-public-subnet-a"
    }
    "oimarket-apne2-private-subnet-b" = {
      availability_zone  = "ap-northeast-2b"
      cidr_block         = "10.0.12.0/24"
      nat_gateway_subnet = "oimarket-apne2-public-subnet-b"
    },
  }

  security_groups = {
    "oimarket-apne2-permit-ssh-security-group" = {
      ingress_rules = [
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 22
          protocol    = "tcp"
          to_port     = 22
        }
      ]

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

    "oimarket-apne2-permit-http-security-group" = {
      ingress_rules = [
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 80
          protocol    = "tcp"
          to_port     = 80
        },
        {
          cidr_blocks = ["0.0.0.0/0"]
          from_port   = 443
          protocol    = "tcp"
          to_port     = 443
        }
      ]

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

}
Summary
  • 테라폼이 제공해 주는 yamldecode() 함수를 사용하면 YAML 파일을 읽을 수 있다.

  • YAML 파일 활용하면 테라폼 구성의 가독성을 높일 수 있고 데이터와 로직을 분리할 수 있다.

Dynamic Syntax Optimization

기존에 사용중이던 보안 그룹 규칙은 테라폼 구성의 Inline-block 으로 Ingress, Egress를 분리 했지만 이렇게 사용할 경우 공식문서에서 다음과 같은 이슈가 있다고 설명한다.

그렇기 때문에 가장 좋은 방법은 aws_vpc_security_egress_rule 과 ingress_rule 리소스를 사용해서 CIDR 블럭을 관리하라고 설명한다.

현재 문제 상황에 가장 적합한 이슈로 이 공식문서가 가이드하는 방식대로 리소스를 새롭게 정의하여 동적 블럭에서 한 개의 리소스를 수정 했을 때 다른 리소스도 영향이 받지 않는지 확인 하는데, 이 때 기존에 사용중이던 변수 타입도 분리해야한다.

resource "aws_security_group" "groups" {
  for_each = var.security_groups

  name   = each.key
  vpc_id = aws_vpc.main.id

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

  dynamic "egress" {
    for_each = each.value.egress_rules
    content {
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
    }
  }

  tags = {
    Name : each.key
  }
}

문제가 되었던 다이나믹 블럭의 보안 그룹을 수정했을 때 다른 리소스에 영향이 가지 않는지 테스트를 한 번 해보면 정상적으로 수정된 보안 그룹 규칙만 영향을 받는 것을 알 수 있다.

Summary
  • 원하는대로 동작하지 않을때는 테라폼 공식 문서를 참고해서 확인해 봅니다.

  • terraform import 명령을 사용하면 이미 프로비저닝 되어 있는 리소스를 테라폼으로 관리할 수 있게 됩니다.

Last updated