Terraform 0.12.23 버전 기준이다. for문은 0.12 버전부터 지원하기 때문에 0.12 이전 버전에서는 작동하지 않을 것이다.
Terraform 코드 시나리오
아래와 같은 VPC를 Terraform 코드로 개발&배포 하기로 했다. 이 문서는 테라폼 모듈을 이용한 코드의 구조화가 목적이므로 VPC 외에 다른 AWS 자원들은 포함하지 않을 것이다.
VPC : 10.20.0.0/16
Internet gateway : VPC 내부와 외부 인터넷 간에 통신을 하기 위한 관문
Public subnet : VPC 안에 만들어진 24bit 서브넷이다. Internet gateway와 연결 (associate)된다.
Private subnet : Internet gateway와 연결되지 않는다. 즉 인터넷에서 접근 할 수 없으며, 인터넷으로 나갈 수도 없다. 나가는 것은 NAT gateway를 전개하면 되는데, 여기에서는 NAT gateway를 사용하지 않는다.
우리는 이 VPC 구조를 레퍼런스로 해서 DEV, STG, PRD 3개의 네트워크를 만들 것이다.
DEV : 개발을 위해서 사용하는 네트워크
STG : 개발이 끝난 애플리케이션을 검증하는 네트워크
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"상태의 가용영역 목록을 저장하고 있다.
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
지금까지의 내용을 이해하기 쉽게 그림으로 묘사했다.
자 이렇게 해서 local 변수에 public subnet과 private subnet 을 저장했다. 이제 terraform resource를 이용해서 전개하면 된다. main.tf를 참고하자.
앞서 subnet 만들어 둔 것들을 resource로 배치하는 거라서 주목해서 볼만한 것은 없다. 그나마 좀 관심있는 것은 배열로된 subnet을 for_each로 배포한다는 정도가 되겠다. 0.12 이전 버전에서 index, count 루프 돌던 것에 비하면 한층 세련된 방법이다.
이렇게 해서 하나의 코드로 dev, stg, prd를 배포할 수 있는 구조를 만들었다.
Terraform 모듈
environment를 이용해서 하나의 코드로 여러 형상을 배포하는 방법을 삺펴봤다. 장황하게 설명했지만 결국은 외부에서 변수 값을 설정하는 것으로 코드를 제어하는 방식이다. 다양한 프로젝트에서 효과적으로 사용하기 위해서는 이 것으로는 부족하다. 라이브러리 형태로 만들 필요가 있겠다. Terraform에서 제공하는 모듈(module)이라는 것으로 재사용성을 높일 수 있다. 모듈은 별 것 없다. 자유롭게 가져다 쓸 수 있는 라이브러리라고 보면 된다.
Terraform 모듈을 이용해서 코드의 재 사용성을 높여보기로 했다. 아래와 같은 구조를 만들었다.
재 사용 가능한 인프라 단위로 모듈화 한다. 처음에는 VPC, RDS, ElastiCache, Kafka(MSK), Kinesis 와 같은 기본 요소들을 모듈화 하는 것부터 시작해보자.
dev, prd, stg는 이 모듈을 이용해서, 자원을 전개한다. 같은 모듈을 이용하기 때문에 모든 단계에서 동일한 인프라를 구성 할 수 있다. 이 모듈은 다른 프로젝트에서도 사용 할 수 있기 때문에 재 사용성이 크게 높아진다. 모듈만 따로 빼서 git으로 관리하는 것을 권장한다. 모듈도 dev, stg, prd를 갖춰서 모듈 자체를 개발/테스트/유지보수 할 수 있게 한다.
s3와 iam과 같은 조직 전체에 영향을 주는 인프라자원은 모듈로 부터 분리한다.
이 문서에서는 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 다.
public_subnets variable의 데이터 타입은 list다. module을 가져다 사용하는 코드에서 subnet 목록을 설정하면, 해당 설정을 읽어서 aws subnet을 생성한다.
바로 모듈을 사용해보자. dev/main.tf 파일이다. 원래 variable는 variable.tf에 설정했으나 귀찮아서 main.tf에 포함했다.
Contents
Terraform 코드 시나리오
가능한 Terraform 코드 구조 만들기
Terraform 모듈
정리
Recent Posts
Archive Posts
Tags