초보를 위한 테라폼

초보를 위한 테라폼

서비스의 복잡도가 증가하면서 인프라를 어떻게 더 쉽게 관리할 수 있을까에 대한 고민은 지속해서 증가했다. 정적인 상태와 안전한 코드로서 인프라를 관리하고 인프라에서 해야 하는 작업을 확장성있는 방식으로 구성하기 위해 개발자들은 인프라를 코드로써 표현하려고 했다. 이를 IaC (Infra as Code)라고 한다. 현재는 Terraform이 주류를 잡고 있는 듯 하다. 이 글은 IaC에 대한 이야기부터 테라폼의 얕은 이야기를 다룰 예정이다.

이번 글은 테라폼이 무엇일지 대략적인 감을 잡기 위한 글이다. tfstate, 협업하는 방법, Best Practice같은 얘기는 이번 글에서 다루지 않는다.

IaC (Infrastructure as Code)

말 그대로 인프라를 코드로 관리하는 방법이다. 이런 구성의 목적은 무엇일까?

  • 재사용성: 손으로 하던 작업을 코드로 변경하게 되면 대표적으로 바뀌는 점은 반복 가능하다는 점이다. 그리고 코드를 어떻게 짜는지에 따라 다르겠지만, 작은 단위마다 재사용이 가능하다. 예를 들어서 웹 서버를 클러스터 형태로 배포하고 앞에 로드 밸런서를 달아둔다고 해보자. 이 구성은 여러 서비스를 새로 배포할 때마다 몇 가지 설정을 제외하고 모두 같은 과정을 거치게 된다. 만약 코드가 있다면 이 과정을 반복하는 지루한 일을 할 필요 없다.

  • 안정성: 코드를 재사용할 수 있다는 것은 안전한 흐름을 재사용하는 것과 같다. 안전하다는 것은 테스트를 통해 확인할 수 있고, 테라폼 역시 테스트 코드를 만들 수 있다. 어찌 됐든 안전한 작은 흐름을 모아 크고 안전한 흐름을 구성할 수 있게 해준다.

  • 가시성: 가시성이라고 표현할만한지 사실 잘 모르겠지만 코드로 인프라를 구성하는 것은 인프라의 구성을 파악하기 훨씬 쉬워진다고 생각한다. 또한 Orphan이 된 인프라를 손쉽게 제거할 수도 있고, 사람이 보지 못해서 생기는 문제들을 줄여준다.

이외에도 여러 장점이 있겠지만 제일 중요한 포인트는 재사용성인 것 같다. 재사용이 가능하다는 것은 여러 스테이지로 현재 서비스 구성을 복사할 수도 있다는 것을 의미하고, 완전히 동일한 인프라 구성에서 테스트 스테이지를 구성할 수도 있다는 것을 뜻한다. IaC의 장점은 이렇게 쉽게 환경을 만들고 부술 수 있는 능력에 있다고 생각한다.

IaC 구분

IaC는 크게 다섯 가지로 구분한다. 애드훅 스크립트, 구성 관리 도구, 서버 템플릿 도구, 오케스트레이션 도구, 프로비전 도구로 나눌 수 있다.

애드훅 스크립트

인프라 자동화의 가장 기본적인 방법이고 가장 쉽다. 서버에서 수행할 작업을 스크립트로 구성하고 직접 실행시키는 방법이다. Shell Script 또는 익숙한 코드로 필요한 내용을 작성하고 이를 동작시키는 방식이다. 소규모 인프라에서 일회성 작업이 필요한 경우 아주 적합한 방법이다. 하지만 복잡한 작업을 해야 한다면 결국 새로운 코드 베이스를 만들어 관리하는 형태가 되기 때문에 바람직하지 않다.

구성 관리 도구

타겟 서버가 가지고 있어야 하는 소프트웨어를 관리하도록 설계되어있다. 애드훅 스크립트와 비슷한데 인프라 관련된 규칙을 가진 도구이기 때문에 무슨 작업을 하는지 비교적 쉽게 코드로 파악할 수 있다. 애드훅 스크립트와 비교했을 때 가장 큰 차이점은 이 종류에 해당하는 도구들의 대부분이 멱등성을 가지고 있다는 점이다. 쉘 스크립트로 인프라에 대한 멱등성 있는 코드를 작성하는 것은 귀찮은 일이고, 코드도 복잡해진다. 이러한 도구의 가장 대표적인 도구는 앤서블이다.

서버 템플릿 도구

말 그대로 서버 구성을 스냅샷화 해서 사용하는 도구이다. 운영체제, 소프트웨어, 파일 및 기타 필요한 모든 상황에 대해 스냅샷을 만들 수 있는 도구이다. 이런 스냅샷은 보통 “이미지”라고 불린다. 서버 템플릿 도구로 만들어진 이미지를 구성 관리 도구를 사용해 서버에 다운받는 방식으로 사용할 수도 있다. 이러한 도구의 가장 대표적인 도구는 도커패커가 있다.

서버 템플릿은 Immutable Infrastructure를 구성하기 위한 핵심적인 요소이다. 한 번 만들어진 스냅샷은 변하지 않고, 새로운 버전을 만들고 싶으면 아예 새로운 스냅샷을 빌드해 배포해야 한다. Snow Flake를 확실히 막는 방법이지만, 아주 작은 변경 사항마다 새 버전을 빌드하고 기존 버전의 서버를 내리는 방식은 조금 무거운 작업일 수 있다.

오케스트레이션 도구

오케스트레이션은 VM및 컨테이너를 어떻게 관리할지에 관해 관심을 두는 도구이다. 몇 개의 컨테이너가 유지되어야 하는지, 어떤 컨테이너에서 장애가 발생했는지, 어디로 요청을 포워딩해야 하는지 등 여러 서비스 사이의 컨트롤 타워 역할을 한다. 가장 대표적인 도구로는 쿠버네티스가 있다.

쿠버네티스도 IaC라고 할 수 있는 건가? 쿠버네티스는 원하는 상태를 오브젝트로 만들어 피드백 루프를 돌면서 지속해서 바람직한 상태에 있는지 확인하면서 오케스트레이션을 수행한다. 개발자는 이러한 “상태”를 yaml 구성 파일을 가지고 정의한다. 따라서 일종의 IaC라고 볼 수 있다.

프로비전 도구

구성 관리 도구, 서버 템플릿 도구, 오케스트레이션 도구는 크게 서비스에 대해 주된 관심을 두는 도구들이다. 서버 내부가 어떻게 되어야 하는지 또는 서비스가 어떻게 실행되고 있어야 하는지를 관리하는 도구들이다. 반면 프로비전 도구는 이름처럼 서버 자체를 생성하고 관리한다. 인스턴스의 개수, 인프라 사이의 연결 등 인프라에 대한 모든 것을 프로비저닝 하는 데 사용된다. 테라폼은 대표적인 프로비전 도구다.

테라폼

테라폼은 IaC를 위한 선언형 언어이다. 선언형 언어라는 것은 인프라의 상태를 정의하는 코드라는 뜻이다. “프로그래밍 언어”로서 기능을 한다고 생각하는데, 이유는 반복문과 조건절, 함수와 비슷한 모듈의 개념이 있기 때문이다. 테라폼 맛을 한번 보자.

1
2
3
4
5
6
7
8
9
10
11
12
resource "aws_instance" "example" {
ami = "ami-something"
instance_type = "t3.micro"
}

resource "google_dns_record_set" "a" {
name = "demo.google-example.com"
managed_zone = "example_zone"
type = "A"
ttl = 300
rrdatas = [aws_instance.example.public_ip]
}

AWS에서 인스턴스를 만들고 GCP에서 AWS 서버에 접속할 IP를 DNS 레코드에 넣는 작업이다. 모르는 사람이 봐도 대충 이렇겠구나 싶은 느낌이 오는 정도의 가독성을 가지고 있다.

핵심적인 테라폼 흐름

테라폼의 핵심적인 흐름은 다음 구성을 가지고 있다.

  • Write: 테라폼 코드를 적는 단계. 어떤 리소스들이 어떤 방식으로 구성되어있는지 선언하는 단계이다.
  • Plan: 실제 인프라에 적용하기 전에 어떻게 변화가 생기는지 확인하는 단계이다.
  • Apply: 인프라에 실제로 의도한 대로 변화를 가하는 단계이다.

Write 단계에서는 테라폼의 문법대로 tf 확장자의 파일을 구성하는 방식이다. Plan은 이렇게 구성한 테라폼 파일들의 실행 계획을 확인하는 단계이다. terraform plan이라는 명령어를 사용한다. Apply는 위 계획대로 인프라에 적용하는 단계이다. terraform apply 명령어로 수행한다.

Plan 단계에서 문제없이 실행될 것처럼 보여도 실제 Apply 단계에서 문제가 발생할 수도 있다. 예를 들어 변경하려고 하는 S3 버킷의 이름이 이미 있는 이름이라든지, 여러 이유가 있을 수 있다.

테라폼 단계

테라폼 코드를 작성하거나 팀이 이미 작성한 테라폼 코드를 가져온 다음 테라폼 코드가 실행될 수 있도록 초기화해주는 단계가 있다. terraform init 명령어인데, 이 과정에서 테라폼을 실행하기 위해 필요한 플러그인을 설치하기도 하지만 신경 써야 하는 점은 테라폼 백앤드를 설정하는 과정이다. 기본은 로컬에서 동작하지만, 이는 팀과 함께 작업하기에는 부적합하다. init 명령어가 하는 역할과 협업 과정에서 백앤드 초기화하기 위한 좋은 방법을 이 링크에서 확인해보자.

Syntax 구성 요소

테라폼의 문법적 요소를 구성하는 키워드와 규칙이 크게 두 가지 있다. 하나는 Argument, 다른 하나는 Block이다.

Argument

특정한 식별자에 할당된 값을 의미한다.

1
image_id = "1234"

등호를 기준으로 좌측이 식별자, 우측이 표현 식이다. 프로그래밍 언어처럼 그냥 리터럴한 값이 올 수도 있지만, 프로그램에 의해 결정되는 표현 식이 오기도 한다. Argument는 특정 컨텍스트 안에서 유효성이 결정된다. 즉, 어떤 컨텍스트 안에 속해있는지에 따라 타입, 식별자 이름 등이 유효한지 아닌지 판단된다.

Block

1
2
3
4
resource "aws_instance" "example" {
ami = "ami-id"
...
}

{ ... } 부분이 블록이다. 블록은 타입을 가지고 있는데 위 예시의 타입은 resource라는 타입이다. resource 타입에서는 두 가지의 라벨을 기대하고 있다. 하나는 aws_instance와 같이 정해진 리소스 이름이고 다른 하나는 example과 같이 임의로 정하는 식별자 이름이다. 지금 당장 resource 타입에 대해 설명하고자 하는 것은 아니니 블록에는 라벨이 붙을 수도 있고 아닐 수도 있다는 점만 알고 있자. 블록 내부와 라벨은 어떤 타입의 블록 컨텍스트인지에 따라 달라진다.

Module

테라폼은 tf 확장자를 가진 파일들로 만들어진다. 이 파일들이 모여 모듈이 구성되는데 모듈의 기준은 디렉토리이다. 같은 디렉토리 안에 모여있는 tf 파일들이 모두 모듈로 활용된다. 어떤 디렉토리의[ 하위에 위치하더라도 그 둘은 다른 모듈이다.

테라폼이 실행되는 모듈은 하나뿐이다. 이 모듈을 “루트 모듈“(Root Module)이라고 한다. 그리고 이 모듈에서 사용하는 다른 디렉토리에 위치한 모듈들을 “자식 모듈“(Child Module)이라고 부른다.

모듈은 테라폼의 함수와 같은 역할을 해준다. 작은 단위로 나누고 테스트하고 확장성 있게 만들어서 여러 스테이지에서 반복되는 코드를 쓸 필요 없도록 해준다. 구체적으로 자식 모듈을 가져와 사용하는 예시는 이후 후술한다.

Provider

Provider는 인프라를 제공하는 클라우드 혹은 오픈 스택과 같은 인프라 차원의 API 종류를 정하는데 사용하는 블록 타입이다. 제공하는 벤더에 따라 테라폼이 실행할 때 다운로드 해 오는 코드가 달라진다. Provider는 실행하고자 하는 루트 모듈에만 있으면 된다. 자식 모듈은 실행하는 루트 모듈의 프로바이더 정보를 사용한다.

1
2
3
4
provider "aws" {
profile = "terraform"
region = "ap-northeast-2"
}

위 코드는 AWS를 사용하겠다는 것을 의미한다. provider 블록은 정해진 벤더사의 이름을 넣어야 하는 라벨을 요구한다. 그리고 블록 안에서는 필수적인 Argument들이 있을 수 있다.

테라폼은 정말 많은 프로바이더를 가지고 있다. 이 링크에서 확인해볼 수 있다.

Resource

테라폼은 인프라를 다루는 언어인 만큼 인프라 리소스를 정의할 수 있는 블록이 필요하다. 이 역할을 resource 타입이 해준다.

1
2
3
4
resource "aws_instance" "web" {
ami = "ami-id"
instance_type = "t3.small"a
}

위에서 예시로도 사용됐는데, 두 가지의 라벨을 요구하는 블록 타입이다. 하나는 리소스 종류를 정해야 하고, 그다음 나오는 라벨은 로컬 네임이다. 로컬 네임은 테라폼 모듈 안에서 해당하는 리소스를 참조하기 위해 사용된다. 다른 모듈에서 같은 이름이 나오는 것은 상관없다. 리소스가 무엇이냐에 따라 안에 Argument들도 달라진다. AWS를 생각해보면 정말 수많은 리소스 종류가 있기 때문에 어떻게 작성해야 하는지를 일일히 외울 수는 없다. 공식문서에서 프로바이더마다 제공하는 리소스와 어떻게 테라폼 리소스 코드를 써야 하는지 알려주는 문서를 확인하면서 코드를 써야 한다. AWS의 경우 이 링크를 통해 알 수 있다.

Data

테라폼 외부의 데이터를 사용하기 위한 타입이다. 여기서 외부라는 말은 분리된 다른 테라폼의 설정이라든지, 인프라 내에서 변경되는 값들을 의미한다.

1
2
3
4
5
6
7
8
data "aws_ami" "example" {
most_recent = true
owner = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}

위 데이터 블록은 테라폼에게 aws_ami를 읽어와서 example이라는 이름으로 필요한 정보를 읽을 수 있게 해준다. 예를 들어 테라폼에 DB를 생성한 다음 만들어지는 주소를 웹서버에게 넘겨주기 위해 사용할 수 있다. 데이터 블록 안의 Argument들은 데이터 블록의 쿼리를 만들어주는 역할을 한다.

1
2
3
4
5
6
7
data "aws_vpc" "default" {
default = true
}

data "aws_subnet_ids" "default" {
vpc_id = data.aws_vpc.default.id
}

위 예시 코드는 기본 VPC를 가져와서 data.aws_vpc.default에서 참조할 수 있게 하는 데이터 블록을 만들고 그 블록을 사용해 해당 VPC 안에 있는 서브넷들을 가져오는 코드이다. 서브넷들은 마찬가지로 data.aws_subnet_ids.default로 참조할 수 있다.

Variable

확장적인 인프라 모듈을 위해서는 적절한 변수들이 활용되어야 한다. 100개의 인스턴스 이름을 바꾸는 작업에 단순 작업이 필요하다면 확장이 어려운 것으로 볼 수 있다. 임의의 변수를 설정하는 것은 variable이라는 변수 타입으로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
variable "server_port" {
description = "The port for HTTP requests.
type = number
default = 8080
}

variable "alb_name" {
description = "The name of the ALB"
type = string
default = "terraform-asg"
}

이 변수는 모듈에 대한 input이다. 루트 모듈이 자식 모듈을 불러올 때 변수를 지정할 수도 있고, 루트 모듈을 실행할 때 환경 변수 및 실행할 인자 값 등으로 넣어줄 수 있다. 만약 위 예시처럼 기본값이 정해져 있다면 따로 넣어줄 필요 없다.

위 예시에서는 server_port, alb_name이라는 이름을 모듈 안에서 var.<name> 형태로 사용할 수 있다.

1
2
3
4
resource "alb_something" "example" {
port = var.server_port
name = "${var.alb_name}-cluster-lb"
}

Output

함수처럼 사용할 수 있으려면 반환 값도 필요하다. 모듈의 결과를 내보낼 수 있는 블록이 있는데, 이 블록이 output이다.

1
2
3
4
output "asg_name" {
value = aws_autoscaling_group.example.name
description = "The name of the ASG"
}

aws_autoscaling_group.example.name은 모듈 안에 있는 ASG 리소스 중 example이라는 식별자가 붙은 것의 이름을 의미한다. 다른 모듈에서 이 변수를 사용한다면 다음과 같이 사용하게 된다.

1
2
module.[module_name].[output_name]
module.webclusters.asg_name

반복문

선언적 언어는 절차적인 유형의 작업을 처리하기 어렵다. 반복, 조건문 등을 지원하지 않는 경우가 많이 있다. 테라폼에서는 루프를 지원해서 이런 상황에서 사용이 가능하다. 테라폼은 다음과 같은 루프가 있다.

  • for_each: 리소스 내에서 리소스 또는 인라인 블록을 반복한다.
  • for: 리소스와 맵 타입을 반복한다.
  • count: 리소스를 지정한 수만큼 반복한다.
1
2
3
4
5
6
7
8
9
10
variable "aws_iam_user" {
description = "IAM users with these names"
type = list(string)
default = ["neo", "trinity", "morpheus"]
}

resource "aws_iam_user" "example" {
count = length(var.aws_iam_user)
name = var.aws_iam_user[count.index]
}

length는 내장함수이다. 여러 내장 함수가 있으므로 이 링크에서 학습하는 것을 추천한다.

countindex를 통해 순회한다. 위 예시는 aws_iam_user 리소스를 콜렉션의 길이만큼 반복한다.

count를 사용할 때는 다음과 같은 제약이 생긴다.

  • 리소스 안에서 특정 인라인 블록을 반복할 수는 없다. 인라인 블록은 태그나, 로컬 변수 등 블록 안에서 만들어지는 블록을 의미한다.
  • 배열의 인덱스대로 처리한다. 만약 위 예시에서 trinity를 지운다면, 기존 IAM 중에서 morpheus를 지우고 trinity의 이름을 morpheus로 변경한다.

for, for_each에 대한 설명은 이 글에서 하지 않았다. 구체적인 타입에 대한 설명과 몇 가지 내장 함수들을 설명해야 하는데, 글의 취지와 적합하지 않은 듯 하여 뺐다. 자세한 설명은 이 링크를 읽자.

조건문

조건문은 아쉽지만 삼항 연산자로만 표현해야 한다.

1
condition ? true : false

위와 같이 조건이 맞으면 앞의 값을 사용하고 틀리면 뒤의 값을 사용한다. 리소스를 생성할지 말지를 결정하는 것은 count를 함께 사용해 구성할 수 있다.

1
2
3
4
resource "something" "something" {
count = var.some_boolean ? 1 : 0
...
}

만약 var.some_boolean 값이 true라면 이 리소스는 생성될 것이고 그렇지 않으면 생성되지 않는다.

Usecase

지금까지의 내용을 통해 테라폼이 어떤 느낌의 언어인지 파악할 수 있다. 선언형 언어라는 점, 프로그래밍 언어적 특성들을 어떤 방식으로 제공하고 있는지 등. 테라폼의 모든 내용을 이 글에서 배우기는 어렵고, 더 자세한 내용은 테라폼의 공식 문서가 굉장히 잘 정리해주고 있으니 참고해서 학습할 수 있다. 테라폼을 사용한 조금 구체적인 활용 예시는 다음과 같다.

  • 테라폼을 사용하면 인프라에 대한 테스트 코드를 작성할 수 있게 된다. 그렇게 되면 안정적인 인프라 구성을 할 수 있게 된다.
  • Stage를 분리하려고 할 때 동일한 상태를 간단하게 복사할 수 있다. 테스트 환경을 제거하는 방법도 destory 명령으로 간단히 해결할 수 있다.
  • 테라폼으로 인프라를 관리함으로써 제거해야 하는 리소스를 빼먹고 남겨두는 상황을 최소화할 수 있다. 적어도 테라폼으로 삭제하지 못하게 되면 어떤 리소스가 왜 삭제되지 않았는지 파악할 수도 있다.

이 글로도 사실 테라폼이 어떤 건지 쉽게 감을 잡기 어려울 수 있다. 개인적인 생각으로는 직접 Example을 따라 해본다면 쉽게 이해할 수 있고, 프로그래밍 관점에서 테라폼을 바라볼 때 이 글이 도움이 될 수 있을 것 같다.

Reference

Author

changhoi

Posted on

2022-05-08

Updated on

2022-05-08

Licensed under

댓글

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×