메뉴

문서정보

목차

Terraform 0.12.23 버전 기준이다. for문은 0.12 버전부터 지원하기 때문에 0.12 이전 버전에서는 작동하지 않을 것이다.

Terraform 코드 시나리오

아래와 같은 VPC를 Terraform 코드로 개발&배포 하기로 했다. 이 문서는 테라폼 모듈을 이용한 코드의 구조화가 목적이므로 VPC 외에 다른 AWS 자원들은 포함하지 않을 것이다.

 Terraform 테스트 VPC

  1. VPC : 10.20.0.0/16
  2. Internet gateway : VPC 내부와 외부 인터넷 간에 통신을 하기 위한 관문
  3. Public subnet : VPC 안에 만들어진 24bit 서브넷이다. Internet gateway와 연결 (associate)된다.
  4. Private subnet : Internet gateway와 연결되지 않는다. 즉 인터넷에서 접근 할 수 없으며, 인터넷으로 나갈 수도 없다. 나가는 것은 NAT gateway를 전개하면 되는데, 여기에서는 NAT gateway를 사용하지 않는다.
우리는 이 VPC 구조를 레퍼런스로 해서 DEV, STG, PRD 3개의 네트워크를 만들 것이다.
  1. DEV : 개발을 위해서 사용하는 네트워크
  2. STG : 개발이 끝난 애플리케이션을 검증하는 네트워크
  3. PRD : 검증이 끝난 애플리케이션을 고객에게 서비스하는 네트워크
DEV, STG, PRD는 네트워크 대역대만 다르고 다를 뿐 모든 구성이 완전히 동일하다. DEV는 10.20.0.0/16, STG는 10.21.0.0/16, PRD 는 10.22.0.0/16 이다.

가능한 Terraform 코드 구조 만들기

우리가 만들려고 하는 인프라는 "네트워크 주소"만 변경될 뿐 완전히 동일한 네트워크 구조를 가지는 것을 알 수 있다. 코드를 잘 구조화 하면, 단지 하나의 코드로 DEV, STG, PRD 모두 배포 할 수 있을 것이다.

DEV, STG, PRD를 환경(environment)로 빼내고, 각 환경에 따라서 네트워크 값만 변경해 준다면 하나의 코드로 여러 환경으로의 배포가 가능할 것이다. Terrform의 environment variable을 이용해서 이러한 코드를 만들 수 있다.
variable "environment" {
	type = string
	description = "Options: dev, stg, prd"
}

variable "cidr_ab" {
	type = map
	default = {
		dev = "10.20"
		stg = "10.21"
		dev = "10.22"
	}
}

이 Terraform 코드를 실행 할 때 environment를 설명하면, 변수에 환경에 맞는 값을 설정 할 수 있다. 예를 들어서 plan 을 실행하면 아래와 같이 프롬프트가 뜬다.
# terraform plan
var.environment
  Options: dev, stg, prd

  Enter a value: 
입력한 값은 var.environment 에 저장이 되므로 이 값을 이용해서 환경을 선택 할 수 있다. 예를 들어서 "dev"를 입력했다면, lookup 함수를 이용해서 dev 환경의 vpc cidr 값을 읽을 수 있다.
lookup(var.cidr_ab, var.environment)
# cidr_ab["dev"] 와 같은 효과를 가진다. 
# 따라서 "10.22"가 리턴된다.

environment variable를 이용한 코드를 만들어보자. 이 코드의 파일 구조는 아래와 같다.
tree
.
├── README.md
├── provider.tf
├── main.tf
└── variable.tf

variable.tf
variable "aws_region" {
	description = "test region"
	default = "ap-northeast-2"
}

variable "environment" {
	type = string
	description = "Options: dev, stg, prd"
}

variable "cidr_ab" {
	type = map
	default = {
		dev = "10.20"
		stag = "10.21"
		prod = "10.22"
	}
}

variable "project" {
	type = string
	default = "helloworld"
}

data "aws_availability_zones" "available" {
	state = "available"
}

locals {
	availability_zones = data.aws_availability_zones.available.names
	vpc_cidr = "${lookup(var.cidr_ab, var.environment)}.0.0/16"
}


locals {
	cidr_c_public_subnets  = 1
	cidr_c_private_subnets = 3
   	max_subnets     = 2
}

locals {
	public_subnets = merge({ 
		for az in local.availability_zones: 
         "${lookup(var.cidr_ab, var.environment)}.${local.cidr_c_public_subnets + index(local.availability_zones, az)}.0/24" => az
		if index(local.availability_zones, az) < local.max_subnets
    })
	private_subnets = merge({
		for az in local.availability_zones:
         "${lookup(var.cidr_ab, var.environment)}.${local.cidr_c_private_subnets + index(local.availability_zones, az)}.0/24" => az
		if index(local.availability_zones, az) < local.max_subnets
	})
}

output "vpc" {
	value = local.vpc_cidr
}


output "public_subnet" {
	value = local.public_subnets
}

output "private_subnet" {
	value = local.private_subnets
}

output "aws_availability_zones" {
	value = data.aws_availability_zones.available
}

locals.public_subnets 이 코드가 생소 할 수 있으니 분석을 해보자. 이 코드는 가용영역별로 subnet을 설정하는 작업을 한다.
locals {
	public_subnets = merge({ 
		for az in local.availability_zones: 
         "${lookup(var.cidr_ab, var.environment)}.${local.cidr_c_public_subnets + index(local.availability_zones, az)}.0/24" => az
		if index(local.availability_zones, az) < local.max_subnets
    })
	private_subnets = merge({
		for az in local.availability_zones:
         "${lookup(var.cidr_ab, var.environment)}.${local.cidr_c_private_subnets + index(local.availability_zones, az)}.0/24" => az
		if index(local.availability_zones, az) < local.max_subnets
	})
}
Terraform은 자원설정을 위해서 HCL(Hashicorp Configuration Language)라는 언어를 제공한다. 하지만 C, C++, Java와 같은 프로그래밍 언어들과 비교하면 유연성이 크게 떨어진다. 모든 작업을 다 처리 할 수 있을 것을 기대하는 언어들과는 달리 Terraform은 "자원이 어디에, 어떻게, 어떤 속성을 가지고 전개 하면 될지에 대한 정보"만 설정하는 것으로 대부분의 작업을 할 수 있기 때문이다. 말 그대로 설정(Configuration)을 목적으로 하는 특수 언어다.

이를테면 for, if 같은 흐름제어 문도(다른 방법으로 루프를 돌 수 있기는 하지만 직관적이지는 않다) 제공하지 않았다. 생각해보면 설정을 하는데 굳이 if, for 문 등은 필요 없기는 하다. 하지만 다루어야 하는 인프라의 규모가 커지면서 범용언어들이 제공하는 유연성도 필요하게 됐다. 그래서 0.12 버전 부터는 for, if 문등을 제공하고 있다.

local.availability_zones에는 "available"상태의 가용영역 목록을 저장하고 있다.
locals {
	availability_zones = data.aws_availability_zones.available.names
	vpc_cidr = "${lookup(var.cidr_ab, var.environment)}.0.0/16"
}

서울리전의 경우 아래와 같은 정보가 저장된다.
local.availability_zones = [
  "ap-northeast-2a",
  "ap-northeast-2b",
  "ap-northeast-2c",
]

local.vpc_cidr은 environment에 따라서, 10.20, 10.21, 10.22 중 하나가 설정된다. 이제 가용영역의 갯수만큼을 돌면서 public subent과 private subnet을 설정하면 된다. go 언어라면 대략 아래와 같이 표현 할 수 있을 것이다.
package main

import (
    "fmt"
)

func main() {
    cidr_ab := "10.20"
    cidr_public_subnets := 1
    cidr_private_subnets := 3
    max_az := 2

    av_zone := []string{
        "ap-northeast-2a",
        "ap-northeast-2b",
        "ap-northeast-2c",
    }

    public_subnets := make(map[string]string)
    private_subnets := make(map[string]string)

    var i = 0
    for _, v := range av_zone {
        public := fmt.Sprintf("Public  : %s.%d.0/24", cidr_ab, cidr_public_subnets+i)
        private := fmt.Sprintf("Private : %s.%d.0/24", cidr_ab, cidr_private_subnets+i)
        public_subnets[public] = v
        private_subnets[private] = v
        i++
        if i == max_az {
            break
        }
    }
    for k, v := range public_subnets {
        fmt.Printf("%s => %s\n", k, v)
    }
    for k, v := range private_subnets {
        fmt.Printf("%s => %s\n", k, v)
    }

}

실행 결과
Public  : 10.20.2.0/24 => ap-northeast-2b
Public  : 10.20.1.0/24 => ap-northeast-2a
Private : 10.20.3.0/24 => ap-northeast-2a
Private : 10.20.4.0/24 => ap-northeast-2b
지금까지의 내용을 이해하기 쉽게 그림으로 묘사했다.

 Terraform 예제-1

자 이렇게 해서 local 변수에 public subnet과 private subnet 을 저장했다. 이제 terraform resource를 이용해서 전개하면 된다. main.tf를 참고하자.
resource "aws_vpc" "default" {
 	cidr_block = local.vpc_cidr
 	enable_dns_hostnames = true
 	tags = {
		Project = var.project 
		Environment = var.environment 
 	}
}

resource "aws_subnet" "public" {
	for_each = local.public_subnets

	vpc_id = aws_vpc.default.id
	cidr_block = each.key
	availability_zone = each.value
	tags = {
		Project = var.project
		Environment = var.environment
	}
}

resource "aws_subnet" "private" {
	for_each = local.private_subnets

	vpc_id = aws_vpc.default.id
	cidr_block = each.key
	availability_zone = each.value
	tags = {
		Project = var.project
		Environment = var.environment
	}
}

resource "aws_internet_gateway" "default" {
	vpc_id = aws_vpc.default.id
	tags = {
		Project = var.project
		Environment = var.environment
	}
}

resource "aws_route_table" "public" {
	vpc_id = aws_vpc.default.id
	route {
		cidr_block = "0.0.0.0/0"
		gateway_id = aws_internet_gateway.default.id
	}
	tags = {
		Project = var.project
		Environment = var.environment
	}
}

resource "aws_route_table_association" "public" {
	for_each = local.public_subnets
	subnet_id = aws_subnet.public[each.key].id
	route_table_id = aws_route_table.public.id
}
앞서 subnet 만들어 둔 것들을 resource로 배치하는 거라서 주목해서 볼만한 것은 없다. 그나마 좀 관심있는 것은 배열로된 subnet을 for_each로 배포한다는 정도가 되겠다. 0.12 이전 버전에서 index, count 루프 돌던 것에 비하면 한층 세련된 방법이다.

이렇게 해서 하나의 코드로 dev, stg, prd를 배포할 수 있는 구조를 만들었다.

Terraform 모듈

environment를 이용해서 하나의 코드로 여러 형상을 배포하는 방법을 삺펴봤다. 장황하게 설명했지만 결국은 외부에서 변수 값을 설정하는 것으로 코드를 제어하는 방식이다. 다양한 프로젝트에서 효과적으로 사용하기 위해서는 이 것으로는 부족하다. 라이브러리 형태로 만들 필요가 있겠다. Terraform에서 제공하는 모듈(module)이라는 것으로 재사용성을 높일 수 있다. 모듈은 별 것 없다. 자유롭게 가져다 쓸 수 있는 라이브러리라고 보면 된다.

 Terraform Module

Terraform 모듈을 이용해서 코드의 재 사용성을 높여보기로 했다. 아래와 같은 구조를 만들었다.
.
├── dev
│   ├── main.tf
│   └── variable.tf
├── global
│   ├── iam
│   └── s3
├── module
│   ├── kafka
│   ├── vpc
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variable.tf
│   └── wordexpress
├── prd
└── stg
이러한 구조를 만든 이유는 아래와 같다. 이 문서에서는 vpc만 모듈로 만들 것이다. 먼저 variable.tf를 정의 한다.
variable "project" {
    description = "프로젝트 이름"
    type = string
}
                                     
variable "owner" {
    description = "인프라 담당자"
    type = string
}
  
variable "stage" {
    description = "배포 단계(stg, prd, dev)"
    type = string
}

variable "azs" {
    description = "VPC에 전개할 az 목록"
    type = list
}

variable "cidr" {
    description  = "VPC의 CIDR block"                                  
    type = string
}
                                 
variable "public_subnets" {
    description  = "public subnet 목록"
    type = list 
}

variable "private_subnets" {
    description  = "private subnet 목록"
    type = list                                 
}

variable "tags" {
    description  = "private subnet 목록"
    type = map
}
module의 variable는 값이 설정되지 않는다. 이 값들은 모듈을 가져다 쓰는 쪽에서 설정해야 한다. 모듈을 작동하게 하기 위한 설정 값 혹은 파라메터라고 생각 하면 되겠다.

아래는 main.tf 다.
resource "aws_vpc" "this" {
	cidr_block = var.cidr
	enable_dns_hostnames = true
	instance_tenancy = "default"
	tags = merge(var.tags, map("Name", format("")))
}

resource "aws_internet_gateway" "this" {
	vpc_id = aws_vpc.this.id
}

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-%s-%s", var.project, var.stage, "publicSubnet")))
}

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-%s-%s", var.project, var.stage, "privateSubnet")))
}

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-%s-%s", var.project, var.stage, "publicRt")))
}

resource "aws_route_table" "private" {
	vpc_id = aws_vpc.this.id

	tags = merge(var.tags, map("Name", format("%s-%s-%s", var.project, var.stage, "privateRt")))
}

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
}
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-%s-%s", var.project, var.stage, "publicSubnet")))
}
public_subnets variable의 데이터 타입은 list다. module을 가져다 사용하는 코드에서 subnet 목록을 설정하면, 해당 설정을 읽어서 aws subnet을 생성한다.

바로 모듈을 사용해보자. dev/main.tf 파일이다. 원래 variable는 variable.tf에 설정했으나 귀찮아서 main.tf에 포함했다.
provider "aws" {
	region = "ap-northeast-2"
	profile = "joinc"
}

variable "project" {
	type = string
	default = "joincWiki"
}

variable "owner" {
	type = string
	default = "yundream@gmail.com"
}

variable "stage" {
	type = string
	default = "dev"
}

variable "cidr" {
	type = string
	default = "10.100.0.0/16"
}

variable "public_subnets" {
	type = list 
	default = ["10.100.0.0/24", "10.100.1.0/24"]

}

variable "private_subnets" {
	type = list 
	default = ["10.100.2.0/24", "10.100.3.0/24"]

}

module "vpc" {
	project = var.project 
	owner = var.owner 
	stage = var.stage 

	source ="../module/vpc"	

	cidr = var.cidr 

	azs = ["ap-northeast-2a", "ap-northeast-2c"]
	public_subnets = var.public_subnets 
	private_subnets = var.private_subnets 
	tags =  {
		project = var.project 
		owner = var.owner
		state = var.stage
	}
}
  1. source = "../module/vpc" 어떤 모듈을 사용 할지를 설정한다. 로컬 패스, Terraform Registry, GitHub, Bitbucket 등을 source 경로로 사용 할 수 있다.
  2. project, owner, stage, cidr, aza, public_subnets, private_subnets 등 module variable에 있는 변수를 모두 설정한다. 하나라도 빠지면 에러가 생긴다.
stg와 prd 단계는 위 코드를 복사해서 사용하면 된다. stage, cidr, public_subnets, private_subnet만 수정하면 된다.

정리

  1. 그냥 모듈 쓰자.
  2. input, output, variable, locals 등을 좀 깔끔히 정리해야 겠다.