AWS VPC with Terraform Modules

지난 블로그 AWS VPC Network with Terraform 에서 Terraform을 사용해서 dev라는 이름의 AWS VPC Network를 구성해봤다. dev 환경에서 개발이 어느정도 진행되면 dev VPC에서 개발했던 프로젝트를 staging 환경이나 production 환경에 배포하게 되는데, 이 때 dev VPC와는 격리된 staging, production VPC 생성이 필요해진다.

staging, production VPC를 생성하려고 보니 기본적인 AWS 리소스의 골격만 봤을 때, dev VPC와 거의 동일한 형태의 리소스 구조에서 ip대역 등의 파라미터 값만이 바뀔 텐데… 이것 때문에 dev VPC를 정의한 *.tf (Terraform Configuration) 파일을 하나하나 카피해서 수정을 해야하나 하고 생각이 들었다.

게다가 VPC가 간단히 구성되어 있으면 몰라도, 대개는 아래 그림처럼 복잡도가 올라간 상태에서 각 VPC간 AWS 리소스의 구성을 동일한 구조로 생성/유지하는 것은 큰 고통이 뒤따른다.

How to Build Reusable, Composable, Battle tested Terraform Modules - Yevgeniy Brikman

이미지 출처:
youtube: How to Build Reusable, Composable, Battle tested Terraform Modules – Yevgeniy Brikman
slideshare: Reusable, composable, battle-tested Terraform modules

이런 상황에서 Terraform Modules를 사용하면 한번 정의한 리소스를 Component Group 단위로 유연하게 재 사용하는 것이 가능해져 비슷한 리소스를 반복해서 정의해야 하는 고통에서 해방될 수 있다.

기존에 dev VPC가 정의되어 있는 상황에서 staging VPC를 Module을 이용해서 정의해보고, staging VPC가 성공적으로 생성되면 기존에 생성했던 dev VPC도 동일한 Module을 사용해서 재 생성할 예정이다. (리소스의 재 생성이 부담되는 작업으로 느껴질 수 있으나 Code를 기반으로 리소스가 작성되어 있으면, 생각보다 그리 어려운 작업은 아님을 알 수 있다.)

먼저 Module의 효과를 실감해보자.

아직 Terraform Modules가 익숙하지 않은 상황에서 파일을 나열하고 설명을 주르륵 늘어놓다 보면 지금 내가 가는 이 길이 어디로 가는 길인지 혼돈과 마주할 수 있으니, 이번에는 Module을 구성하는 각 파일의 설명에 앞서 VPC module을 사용해서 staging VPC를 정의한 결과를 먼저 확인하고 구체적인 설명으로 넘어가자.

# staging_vpc.tf

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

  name = "staging"
  cidr = "172.17.0.0/16"

  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets   = ["172.17.1.0/24", "172.17.2.0/24"]
  private_subnets  = ["172.17.101.0/24", "172.17.102.0/24"]
  database_subnets = ["172.17.201.0/24", "172.17.202.0/24"]

  tags = {
    "TerraformManaged" = "true"
  }
}

위와 같이 간단한 모듈 정의만으로 다음의 AWS VPC Network를 생성할 수 있다.

  • 모듈 반영 시 생성되는 리소스
    • VPC
    • VPC에서 사용할 Internet Gateway
    • VPC에서 사용할 Default NACL, Default Security Group
    • AZ 1개당 public, private, DB Subnet 각 1개
    • AZ 1개당 NAT Gateway(EIP 적용) 1개
    • Internet Gateway, NAT Gateway에 Subnet을 연결하는 Route Table, Route Table Association

staging VPC

위 모듈이 apply 되면, 다음과 같이 리소스가 생성되고 연결된다.

AWS VPC Resources

이제 VPC module을 작성해 보자.

먼저 작업할 경로에 main.tf, output.tf, variables.tf 파일을 작성한다.

Jetbrains의 IDE를 사용한다면 HashiCorp Terraform / HCL language support Plugin 을 설치하면 HCL Syntax highlighting, Validate, Auto Completion등의 기능을 통해서 수월하게 terraform configuration파일을 작성할 수 있다.

나는 2dal-infrastructure 프로젝트의 ./terraform/modules/vpc 경로에 다음과 같이 파일을 생성했다.

$ cd 2dal-infrastructure
$ cd terraform/modules/vpc
$ tree
.
├── README.md    # module의 설명/사용방법 등을 작성한다.
├── main.tf      # module의 리소스를 정의한다.
├── outputs.tf   # 외부에서 module에 접근해서 사용할 output 변수를 정의한다. 
└── variables.tf # module을 사용할 때 입력받는 variable 변수를 정의한다. 

variable 변수와 output 변수에 대해서는 Local Values 페이지의 설명에 따르면
module을 함수에 대응해서 생각했을 때
variable은 함수 파라미터
output은 함수 리턴값으로 매칭시켜서 이해할 수 있다.
링크에서 설명하는 Local 변수는 함수내에서 사용하는 지역변수 로 이해할 수 있다.

variables.tf

먼저 variables.tf 파일을 살펴보자. 이 값들은 module을 선언해서 사용할 때 입력받게 된다.
위에서 작성했던 staging_vpc.tf 파일에서 각 variable의 입력 예제를 확인할 수 있다.

# variables.tf

variable "name" {
  description = "모듈에서 정의하는 모든 리소스 이름의 prefix"
  type        = "string"
}

variable "cidr" {
  description = "VPC에 할당한 CIDR block"
  type        = "string"
}

variable "public_subnets" {
  description = "Public Subnet IP 리스트"
  type        = "list"
}

variable "private_subnets" {
  description = "Private Subnet IP 리스트"
  type        = "list"
}

variable "database_subnets" {
  description = "Database Subnet IP 리스트"
  type        = "list"
}

variable "azs" {
  description = "사용할 availability zones 리스트"
  type        = "list"
}

variable "tags" {
  description = "모든 리소스에 추가되는 tag 맵"
  type        = "map"
}
  • description은 variable에 대한 설명으로, README파일 생성 시에도 사용되니 가급적 자세히 적는다.
  • type 은 해당 variable의 type을 의미하고 validate 과정에서 type을 벗어나는 값이 입력됐는 지 체크한다.
  • default = "t2.nano" 와 같이 variable의 값을 정의하지 않았을 때 default로 사용할 값을 넣어 줄 수 있다.
  • 좀더 자세한 설명은 Configuring Variables 페이지를 참고한다.

main.tf

Module을 처음 접하고 혼란스러웠던 점 중 하나는 Module 키워드를 (리소스 그룹을) 선언할 때 사용하는 것이 아니라, 특정 형태로 디렉터리(module source)를 구성해서 리소스 그룹을 정의한 다음 모듈을 사용할 때 Module 키워드로 해당 리소스 그룹을 가져와서 사용하는 것이었다.

김춘수 시인의 에서처럼
다만 하나의 리소스 그룹에 지나지 않았던 terraform configuration 묶음이
내가 Module이라 불러 주었을 때 (선언 했을 때) 비로서 Module로서 작용하는 그런 느낌이랄까?

main.tf 파일에는 Module이 사용하는 리소스(그룹)를 정의한다.

# main.tf

# VPC
resource "aws_vpc" "this" {
  cidr_block           = "${var.cidr}"
  enable_dns_hostnames = true
  enable_dns_support   = true
  instance_tenancy     = "default"

  tags = "${merge(var.tags, map("Name", format("%s", var.name)))}"
}

VPC 를 정의한다.

  • 리소스 이름은 Module 예제를 찾아보면 thismain을 사용한다. 예제에서는 this를 사용했다.
  • variables.tf 에서 정의한 외부에서 입력받는 variable은 "${var.cidr}" 과 같이 사용한다.
    merge, format 의 낯선 키워드가 보이는데 Interpolation Syntax 를 사용한 것으로
  • merge는 2개 이상의 map을 병합한다. 여기서는 default로 넘어온 tags 맵에 리소스마다 새로운 포맷으로 Name tag 를 추가하기 위해서 사용한다.
  • format은 sprintf 함수처럼 변수를 formatted된 문자열에 적용해서 사용할 수 있다.
# internet gateway
resource "aws_internet_gateway" "this" {
  vpc_id = "${aws_vpc.this.id}"

  tags = "${merge(var.tags, map("Name", format("%s", var.name)))}"
}

Internet gateway를 정의한다.

  • 앞에서 생성한 vpc의 id를 ${aws_vpc.this.id}로 접근한다.
# default network ACL
resource "aws_default_network_acl" "dev_default" {
  default_network_acl_id = "${aws_vpc.this.default_network_acl_id}"

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  subnet_ids = [
    "${aws_subnet.public.*.id}",
    "${aws_subnet.private.*.id}",
    "${aws_subnet.db.*.id}"
  ]

  tags = "${merge(var.tags, map("Name", format("%s-default", var.name)))}"
}

Default NACL을 정의한다.

  • * (Asterisk)를 사용하면, 뒤에서 설명할 count 키워드를 사용해서 생성한 리소스 리스트에 접근할 수 있다.
  • 예제에서는 aws_subnet.public subnet 을 var.public_subnets 의 length 만큼 생성하게 되는데 만약 2개의 AZ에 각 1개씩 public subnet을 2개 생성하게 된다면 [${aws_subnet.public.0.id}, ${aws_subnet.public.1.id}] 로 나열하는 대신 ${aws_subnet.public.*.id}와 같이 축약해서 표현할 수 있다.
# default security group
resource "aws_default_security_group" "dev_default" {
  vpc_id = "${aws_vpc.this.id}"

  ingress {
    protocol  = -1
    self      = true
    from_port = 0
    to_port   = 0
  }

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

  tags = "${merge(var.tags, map("Name", format("%s-default", var.name)))}"
}

Default Security Group을 정의한다.

# public subnet
resource "aws_subnet" "public" {
  count = "${length(var.public_subnets)}"

  vpc_id            = "${aws_vpc.this.id}"
  cidr_block        = "${var.public_subnets[count.index]}"
  availability_zone = "${var.azs[count.index]}"

  tags = "${merge(var.tags, map("Name", format("%s-public-%s", var.name, var.azs[count.index])))}"
}

# private subnet
resource "aws_subnet" "private" {
  count = "${length(var.private_subnets)}"

  vpc_id            = "${aws_vpc.this.id}"
  cidr_block        = "${var.private_subnets[count.index]}"
  availability_zone = "${var.azs[count.index]}"

  tags = "${merge(var.tags, map("Name", format("%s-private-%s", var.name, var.azs[count.index])))}"
}

Public Subnet, Private Subnet을 정의한다.

  • count 키워드를 사용한다. count 키워드는 해당 resource를 count에 입력한 수만큼 반복해서 생성한다.
    count를 사용해 생성한 리소스는 앞에서 설명한 것과 같이 zero-based index나, *를 사용해서 접근할 수 있다. ex) ${aws_subnet.public.0.id} or ${aws_subnet.public.*.id}
  • count.index에는 0 부터 1씩 index가 증가한 값이 입력된다.
  • 예제에서는 동일 영역(public, private, private db)AZ 개수만큼 subnet을 생성한다고 가정하고 ${var.azs[count.index]} 의 값을 availability_zone에 입력한다.
# private database subnet
resource "aws_subnet" "database" {
  count = "${length(var.database_subnets)}"

  vpc_id            = "${aws_vpc.this.id}"
  cidr_block        = "${var.database_subnets[count.index]}"
  availability_zone = "${var.azs[count.index]}"

  tags = "${merge(var.tags, map("Name", format("%s-db-%s", var.name, var.azs[count.index])))}"
}

resource "aws_db_subnet_group" "database" {
  count = "${length(var.database_subnets) > 0 ? 1 : 0}"

  name        = "${var.name}"
  description = "Database subnet group for ${var.name}"
  subnet_ids  = ["${aws_subnet.database.*.id}"]

  tags = "${merge(var.tags, map("Name", format("%s", var.name)))}"
}

(Private) Database Subnet, Database Subnet Group을 정의한다.

  • RDS 생성 시 Subnet을 묶은 DB Subnet Group이 필수 입력 값이므로 DB Subnet Group도 같이 생성한다.
  • 이 때 RDS를 사용하지 않아 ${length(var.database_subnets}) 이 0인 경우 DB Subnet Group도 생성하지 않도록 ${length(var.database_subnets) > 0 ? 1 : 0} 조건문으로 count 값을 정의한다.
  • db subnet group 생성을 위해서는 Terraform을 사용하는 IAM 계정에 RDS 권한을 부여해야 한다. 나는 Terraform 계정에 AmazonRDSFullAccess을 부여했다.
# EIP for NAT gateway
resource "aws_eip" "nat" {
  count = "${length(var.azs)}"

  vpc = true
}

# NAT gateway
resource "aws_nat_gateway" "this" {
  count = "${length(var.azs)}"

  allocation_id = "${aws_eip.nat.*.id[count.index]}"
  subnet_id     = "${aws_subnet.public.*.id[count.index]}"
}

NAT gateway와 NAT에 할당할 EIP를 정의한다.

  • NAT는 AZ마다 1개씩 생성할 예정이므로 count = "${length(var.azs)}" 를 사용한다.
  • NAT 게이트웨이 – Amazon Virtual Private Cloud의 설명을 보면 NAT에도 AZ 가용성을 보장하기 위해서 각 AZ마다 NAT Gateway를 생성하도록 권장하고 있다.
# public route table
resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.this.id}"

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

  tags = "${merge(var.tags, map("Name", format("%s-public", var.name)))}"
}

# private route table
resource "aws_route_table" "private" {
  count = "${length(var.azs)}"

  vpc_id = "${aws_vpc.this.id}"

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = "${aws_nat_gateway.this.*.id[count.index]}"
  }

  tags = "${merge(var.tags, map("Name", format("%s-private-%s", var.name, var.azs[count.index])))}"
}

# route table association
resource "aws_route_table_association" "public" {
  count = "${length(var.public_subnets)}"

  subnet_id      = "${aws_subnet.public.*.id[count.index]}"
  route_table_id = "${aws_route_table.public.id}"
}

resource "aws_route_table_association" "private" {
  count = "${length(var.private_subnets)}"

  subnet_id      = "${aws_subnet.private.*.id[count.index]}"
  route_table_id = "${aws_route_table.private.*.id[count.index]}"
}

resource "aws_route_table_association" "database" {
  count = "${length(var.database_subnets)}"

  subnet_id      = "${aws_subnet.database.*.id[count.index]}"
  route_table_id = "${aws_route_table.private.*.id[count.index]}"
}

Route Table과 Route Table Association을 정의한다.
Private DB Subnet도 aws_route_table.private Table을 사용한다.

main.tf 파일의 세부적인 내용을 설명하느라 내용이 길어져 어려워 보이는 감이 있지만
2dal-infrastructure/main.tf · GitHub 파일을 보면 실제 코드의 양은 그렇게 많지 않음을 알 수 있다.

outputs.tf

output.tf 파일에는 모듈내부에서 생성한 리소스를 module 외부에서 ${module.vpc.vpc_id} 와 같이 접근할 수 있도록 output 변수를 정의한다.

# output.tf 

# VPC
output "vpc_id" {
  description = "VPC ID"
  value       = "${aws_vpc.this.id}"
}

output "vpc_cidr_block" {
  description = "VPC에 할당한 CIDR block"
  value       = "${aws_vpc.this.cidr_block}"
}

output "default_security_group_id" {
  description = "VPC default Security Group ID"
  value       = "${aws_vpc.this.default_security_group_id}"
}

output "default_network_acl_id" {
  description = "VPC default network ACL ID"
  value       = "${aws_vpc.this.default_network_acl_id}"
}

# internet gateway
output "igw_id" {
  description = "Interget Gateway ID"
  value       = "${aws_internet_gateway.this.id}"
}

# subnets
output "private_subnets_ids" {
  description = "Private Subnet ID 리스트"
  value       = ["${aws_subnet.private.*.id}"]
}

output "public_subnets_ids" {
  description = "Public Subnet ID 리스트"
  value       = ["${aws_subnet.public.*.id}"]
}

output "database_subnets_ids" {
  description = "Database Subnet ID 리스트"
  value       = ["${aws_subnet.database.*.id}"]
}

output "database_subnet_group_id" {
  description = "Database Subnet Group ID"
  value       = "${aws_db_subnet_group.database.id}"
}

# route tables
output "public_route_table_ids" {
  description = "Public Route Table ID 리스트"
  value       = ["${aws_route_table.public.*.id}"]
}

output "private_route_table_ids" {
  description = "Private Route Table ID 리스트"
  value       = ["${aws_route_table.private.*.id}"]
}

# NAT gateway
output "nat_ids" {
  description = "NAT Gateway에 할당된 EIP ID 리스트"
  value       = ["${aws_eip.nat.*.id}"]
}

output "nat_public_ips" {
  description = "NAT Gateway에 할당된 EIP 리스트"
  value       = ["${aws_eip.nat.*.public_ip}"]
}

output "natgw_ids" {
  description = "NAT Gateway ID 리스트"
  value       = ["${aws_nat_gateway.this.*.id}"]
}
  • description은 ouput 변수에 대한 설명으로, README파일 생성 시에도 사용되니 가급적 자세히 적는다.
  • value 는 해당 모듈 내부에서 생성한 실제 리소스의 value를 입력한다.
  • output 변수에 대한 좀 더 자세한 설명은 Configuring Outputs를 참고한다.

블로그 글을 쓰기 시작해서 여기까지 오는데 중간에 점심도 먹고 나가서 저녁도 먹고 이제 밤이 늦어졌다.
읽는 분들도 두서없이 정리된 글을 읽고 많이 힘드셨을 것으로 예상된다.
그래도 조금만 더 힘을 내서 같이 가보자!

README 작성

Creating Modules 페이지를 보면 The root module and any nested modules should have README files. 라고 되어있다. 고등학교 때 영어 선생님이 should have가 붙으면 안하면 죽을 정도는 아니지만 그래도 왠만하면 꼭 해주오! 정도의 의미가 있다고 했으니 README도 작성해보자!

고맙게도 Segment라는 회사에서 Terraform Modules의 description 속성을 이용해서 문서를 생성해주는 terraform-docs툴을 만들어서 오픈했다. terraform-docs를 소개해주신 @Outsider 님께 감사드린다.

Segment라는 회사는 잘 몰랐는데 이 밖에도 GitHub – segmentio/stack: A set of Terraform modules for configuring production infrastructure with AWS 과 같이 작성한 Terraform modules을 공유해 주기도 하고 Terraform 관련해서 왕성히 오픈소스 활동을 하는 회사인 것 같다.

mac user 라면 brew를 사용해서 terraform-docs를 쉽게 설치할 수 있다.

$ brew install terraform-docs

다음 명령으로 variables.tf, outputs.tf 파일의 description을 기반으로 README를 생성한다.

$ cd terraform/modules/vpc

# markdown 포맷으로 terraform-docs 생성
$ terraform-docs md .

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|:----:|:-----:|:-----:|
| azs | 사용할 availability zones 리스트 | list | - | yes |
| cidr | VPC에 할당한 CIDR block | string | - | yes |
| database_subnets | Database Subnet IP 리스트 | list | - | yes |
| name | 모듈에서 정의하는 모든 리소스 이름의 prefix | string | - | yes |
| private_subnets | Private Subnet IP 리스트 | list | - | yes |
| public_subnets | Public Subnet IP 리스트 | list | - | yes |
| tags | 모든 리소스에 추가되는 tag 맵 | map | - | yes |

## Outputs

| Name | Description |
|------|-------------|
| database_subnet_group_id | Database Subnet Group ID |
| database_subnets_ids | Database Subnet ID 리스트 |
| default_network_acl_id | VPC default network ACL ID |
| default_security_group_id | VPC default Security Group ID |
| igw_id | internet gateway |
| nat_ids | NAT gateway |
| nat_public_ips | NAT Gateway에 할당된 EIP 리스트 |
| natgw_ids | NAT Gateway ID 리스트 |
| private_route_table_ids | Private Route Table ID 리스트 |
| private_subnets_ids | subnets |
| public_route_table_ids | route tables |
| public_subnets_ids | Public Subnet ID 리스트 |
| vpc_cidr_block | VPC에 할당한 CIDR block |
| vpc_id | VPC |  # 여기는 오류인지 VPC ID 에서 VPC만 출력됨

이 내용을 기반으로 README.md 파일을 작성했다.

VPC module을 사용해서 staging VPC 생성

이제 작성한 VPC 모듈을 기반으로 staging VPC를 생성해 보자.

# terraform/common/vpc/staging.tf

# module키워드를 사용해서 vpc module을 정의한다.
module "vpc" {
  # source는 variables.tf, main.tf, outputs.tf 파일이 위치한 디렉터리 경로를 넣어준다.
  source = "../../modules/vpc"

  # VPC이름을 넣어준다. 이 값은 VPC module이 생성하는 모든 리소스 이름의 prefix가 된다.
  name = "staging"
  # VPC의 CIDR block을 정의한다.
  cidr = "172.17.0.0/16"

  # VPC가 사용할 AZ를 정의한다.
  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  # VPC의 Public Subnet CIDR block을 정의한다.
  public_subnets   = ["172.17.1.0/24", "172.17.2.0/24"]
  # VPC의 Private Subnet CIDR block을 정의한다.
  private_subnets  = ["172.17.101.0/24", "172.17.102.0/24"]
  # VPC의 Private DB Subnet CIDR block을 정의한다. (RDS를 사용하지 않으면 이 라인은 필요없다.)
  database_subnets = ["172.17.201.0/24", "172.17.202.0/24"]

  # VPC module이 생성하는 모든 리소스에 기본으로 입력될 Tag를 정의한다.
  tags = {
    "TerraformManaged" = "true"
  }
}
  • source에는 다음과 같은 저장소를 지원한다.
    • Local 파일 경로
    • Github, BitBucket, Git, Mercurial 레포지터리
    • HTTP URLs
    • S3 Bucket
  • 각 source별 사용방법은 Module Sources 페이지를 참고한다.
  • Terraform Module Registry를 통해서 커뮤니티에서 공개한 Terraform Module을 확인해 볼 수 있다.

Terraform get

Terraform plan을 하기전에 Module Source로 부터 Modules를 불러오는 Terraform get 명령을 수행한다.

$ cd terraform/common/vpc
$ terraform get
Get: file:///Users/asbubam/dev/2dal-infrastructure/terraform/modules/vpc

Terraform plan

$ terraform plan
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
  ...

  + module.vpc.aws_db_subnet_group.database
  + module.vpc.aws_default_network_acl.dev_default
  + module.vpc.aws_default_security_group.dev_default
  + module.vpc.aws_eip.nat[0]
  + module.vpc.aws_eip.nat[1]
  + module.vpc.aws_internet_gateway.this
  + module.vpc.aws_nat_gateway.this[0]
  + module.vpc.aws_nat_gateway.this[1]
  + module.vpc.aws_route_table.private[0]
  + module.vpc.aws_route_table.private[1]
  + module.vpc.aws_route_table.public
  + module.vpc.aws_route_table_association.database[0]
  + module.vpc.aws_route_table_association.database[1]
  + module.vpc.aws_route_table_association.private[0]
  + module.vpc.aws_route_table_association.private[1]
  + module.vpc.aws_route_table_association.public[0]
  + module.vpc.aws_route_table_association.public[1]
  + module.vpc.aws_subnet.database[0]
  + module.vpc.aws_subnet.database[1]
  + module.vpc.aws_subnet.private[0]
  + module.vpc.aws_subnet.private[1]
  + module.vpc.aws_subnet.public[0]
  + module.vpc.aws_subnet.public[1]
  + module.vpc.aws_vpc.this

Plan: 24 to add, 0 to change, 0 to destroy.

terraform plan의 결과 24개의 리소스가 생성될 예정임을 확인할 수 있다.

Terraform apply

이제 terraform apply 명령을 통해 실제 AWS에 리소스를 생성한다.

$ terraform apply
...
Apply complete! Resources: 24 added, 0 changed, 0 destroyed.

드디어 VPC Module을 사용해서 staging VPC 생성에 성공했다. 😀

AWS console에 접속해보면 staging VPC가 새로 추가되었고, 다음과 같이 staging.tf에 정의한대로 각각의 subnet이 생성되었음을 확인할 수 있다.
staging VPC subnets

Bastion

벌써 눈치 챈 분들도 계시겠지만, 앞에 첨부한 AWS VPC Network 그림을 보면 AZ-a Public Subnet에 Bastion이 있는데 설명하는 과정에서 Bastion이 누락되어있다. 이 글에서 Bastion까지 설명한다면 내용이 너무 장황해 질 것 같아(이미 충분히 장황하지만) Bastion은 별도 글로 작성해서 링크를 추가하도록 하겠다.

AWS Bastion with Terraform Modules 에서 Bastion Module에 대한 설명을 추가함.

정리

Terraform Modules를 사용해서 VPC Module을 정의하고, VPC Module을 사용해서 staging VPC를 생성해 봤다. staging.tf 파일의 몇가지 파라미터만 변경하면 production VPC나 DMZ VPC등을 어렵지 않게 동일한 리소스 구조로 생성할 수 있다. 예제에서는 AWS VPC를 통해서 설명했지만 Terraform Module Registry에서 다양한 Terraform Module의 예제를 확인할 수 있다.

설명에 사용된 파일은 GitHub – asbubam/2dal-infrastructure에서 확인할 수 있다.

회사에서 VPC Module을 설계하면서 고생했던 과정을 꼭 글로 남겨야지하고 생각했는데, 이제 막 공부하고 실패하며 기록하고 작성한 이 글이 Terraform으로 AWS 리소스를 정의하고자 하는 누군가에게 도움이 되었으면 하고 바라본다.

참고자료

Modules – Terraform by HashiCorp
Terraform module which creates VPC resources on AWS – Terraform Module Registry
Interpolation Syntax – Terraform by HashiCorp
How to Build Reusable, Composable, Battle tested Terraform Modules – YouTube
GitHub – segmentio/stack: A set of Terraform modules for configuring production infrastructure with AWS