Canvas 1 Layer 1

테라폼(Terraform) 기초 튜토리얼
AWS와 테라폼으로 구현하는 Infrastructure as Code

들어가며: Infrstructure as Code 도구 테라폼Terraform

테라폼Terraform하시코프Hashicorp에서 오픈소스로 개발중인 인프라스트럭처 관리 도구입니다. 서비스 실행에 필요한 환경을 구축하는 도구라는 점에서 셰프Chef앤서블Ansible 같은 설정 관리 도구와 더불어 프로비저닝 도구로 분류됩니다. 테라폼은 코드로서의 인프라스트럭처Infrstructure as Code를 지향하고 있는 도구로서, GUI나 웹 콘솔을 사용해 서비스 실행에 필요한 리소스를 관리하는 대신 필요한 리소스들을 선언적인 코드로 작성해 관리할 수 있도록 해줍니다.

이 글에서는 테라폼의 기본적인 개념들을 소개하고, 테라폼으로 아마존 웹 서비스Amazon Web Serivce(이하 AWS)에서 간단한 웹 애플리케이션을 배포하기 위한 인프라스트럭처를 프로비저닝해보겠습니다.

테라폼 설치

테라폼을 사용하려면 먼저 설치를 해야 합니다. 맥OSmacOS에서는 홈브류Homebrew를 사용해 간단히 설치할 수 있습니다.

$ brew install terraform

설치가 잘 되었는지 확인해봅니다.

$ terraform version
Terraform v0.11.7

홈브류를 사용하면 일반적으로 현재 릴리즈된 최신 버전이 설치 됩니다. 이 글은 v0.10.2를 기반으로 작성되었습니다.

다른 운영체제를 사용한다면 다운로드 페이지에서 직접 실행 가능한 바이너리 파일을 다운로드 받을 수 있습니다. 과거 버전이 필요하다면 테라폼 릴리즈 페이지에서 찾을 수 있습니다.

테라폼을 사용한 웹 애플리케이션 인프라스트럭처 프로비저닝

간단한 웹 애플리케이션을 아마존 웹 서비스에 배포하는 상황을 가정하겠습니다. 이 애플리케이션은 EC2 가상 머신과 RDS 데이터베이스를 사용합니다. 테라폼으로 이 인프라스트럭처를 구축하려면 다음과 같은 단계를 거칩니다.

1단계는 미리 준비되어있다면 건너뛰어도 무방합니다. 2단계는 생성하고자 하는 리소스 수 만큼 반복하면서 점진적으로 진행합니다. 반복해나갈 때마다 테라폼 정의 파일을 조금씩 완성시켜 나갑니다. 3단계는 준비된 인프라스트럭처에 웹 애플리케이션을 배포하는 단계입니다. 여기서는 인프라스트럭처를 프로비저닝하는 2단계까지 진행해나갑니다.

테라폼의 기본 개념들

이 글에서 다룰 테라폼의 기본 개념들입니다. 각 개념들에 대해서는 튜토리얼을 진행해나가면서 자세히 설명합니다.

프로비저닝Provisioning
어떤 프로세스나 서비스를 실행하기 위한 준비 단계를 프로비저닝이라고 이야기합니다. 프로비저닝에는 크게 네트워크나 컴퓨팅 자원을 준비하는 작업과 준비된 컴퓨팅 자원에 사이트 패키지나 애플리케이션 의존성을 준비하는 단계로 나뉘어집니다. 명확한 경계는 불분명하지만 테라폼은 전자에 치우쳐있는 도구라고 할 수 있습니다.
프로바이더Provider
테라폼과 외부 서비스를 연결해주는 기능을 하는 모듈입니다. 예를 들어 테라폼으로 AWS 서비스의 컴퓨팅 자원을 생성하기 위해서는 aws 프로바이더를 먼저 셋업해야합니다. 프로바이더로는 AWS, 구글 클라우드 플랫폼Google Cloud Platform, 마이크로소프트 애저Microsoft Azure와 같은 범용 클라우드 서비스를 비롯해 깃허브Github, 데이터도그Datadog, DNSimple과 같은 특정 기능을 제공하는 서비스, MySQL, 레빗MQRabbitMQ, 도커Docker와 같은 로컬 서비스 등을 지원합니다. 전체 목록은 테라폼 프로바이더 문서에서 찾아볼 수 있습니다.
리소스(자원)Resource
리소스란 특정 프로바이더가 제공해주는 조작 가능한 대상의 최소 단위입니다. 예를 들어 AWS 프로바이더는 aws_instance 리소스 타입을 제공하고, 이 리소스 타입을 사용해 Amazon EC2의 가상 머신 리소스를 선언하고 조작하는 것이 가능합니다. EC2 인스턴스, 시큐리티 그룹, 키 페어 모두 aws 프로바이더가 제공해주는 리소스 타입입니다.
HCLHashicorp Configuration Language
HCL은 테라폼에서 사용하는 설정 언어입니다. 테라폼에서 모든 설정과 리소스 선언은 HCL을 사용해 이루어집니다. 테라폼에서 HCL 파일의 확장자는 .tf를 사용합니다.
계획Plan
테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용을 실제로 적용 가능한지 확인하는 작업을 계획이라고 합니다. 테라폼은 이를 terraform plan 명령어로 제공하며, 이 명령어를 실행하면 어떤 리소스가 생성되고, 수정되고, 파괴될지 계획을 보여줍니다.
적용Apply
테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용대로 리소스를 생성, 수정, 파괴하는 일을 적용이라고 합니다. 테라폼은 이를 terraform apply 명령어로 제공합니다. 이 명령어를 실행하기 전에 변경 예정 사항은 plan 명령어를 사용해 확인할 수 있습니다.

첫 번째 단계 - 아마존 웹 서비스 설정

테라폼은 인프라스트럭처 리소스를 선언할 수 있는 도구로서 다양한 서비스들을 지원합니다. 테라폼에서는 이를 프로바이더Providers라고 부릅니다. 테라폼은 다양한 프로바이더를 지원하지만, 이 중에서 가장 중요한 프로바이더는 단연 클라우드 서비스입니다. 대표적으로 아마존 웹 서비스Amazon Web Service, 마이크로소프트 애저Microsoft Azure, 구글 클라우드 플랫폼Google Cloud Platform가 있습니다.

여기서는 아마존 웹 서비스의 리소스를 테라폼으로 작성해나가겠습니다. 아마존 웹 서비스 프로바이더를 사용하기 위해서는 먼저 AWS 계정이 있어야합니다. 계정이 있다면 해당 계정을 사용하면 되고, 계정이 없다면 새로 만들어야 합니다.* 이 튜토리얼에서는 서울 리전을 사용하며, 계정 생성 시 함께 만들어지는 기본 VPC를 사용한다고 가정합니다.

아마존 루트 계정에 액세스 키를 발급하고 아래의 내용을 진행하는 것도 가능합니다. 하지만 루트 계정을 직접 사용하는 것은 권장하지 않습니다. terraform 이라는 이름으로 IAM 사용자를 생성하고 관리자 권한을 부여해 진행할 것을 추천합니다. IAM 사용자 개념과 생성에 대한 자세한 내용은 아마존 웹 서비스 IAM 사용자의 액세스 키 발급 및 관리를 참고해주세요.

* 클라우드 서비스는 사용한 만큼 비용이 발생하므로 주의가 필요합니다. 자세한 내용은 각 서비스의 요금 페이지를 참고해주세요.

두 번째 단계 - HCL로 리소스 정의하고 AWS에 프로비저닝

이제 본격적으로 테라폼을 사용해 필요한 리소스들을 정의해나갑니다. 이 단계는 다시 세 개의 작은 스텝으로 나뉘어져있으며, 리소스 별로 반복해나갑니다. 두 번째 단계를 다시 보면 다음과 같습니다.

EC2와 RDS 리소스를 생성하기 위해 4개의 리소스(aws_key_pair, aws_security_group, aws_instance, aws_db_instance)를 정의해나갈 것입니다. 3스텝을 하나의 이터레이션을 4번의 이터레이션을 진행합니다. 리소스를 정의하기에 앞서서 먼저 해야할 일이 있습니다. AWS의 자원을 테라폼으로 관리하려면 aws 프로바이더를 정의해야합니다.

AWS 프로바이더 정의

먼저 프로젝트 디렉터리와 테라폼으로 정의할 리소스가 담길 web_infra.tf 파일을 생성하겠습니다.

$ mkdir web_infra
$ cd web_infra
$ touch provider.tf web_infra.tf

디렉터리 이름과 파일 이름에 특별한 원칙은 없습니다. 테라폼은 기본적으로 특정 디렉터리에 있는 모든 .tf 확장자를 가진 파일을 읽어들인 후, 리소스 생성, 수정, 삭제 작업을 진행합니다. 파일은 상황에 따라 적절히 나눠줄 필요가 있지만, 여기서는 작성할 내용이 많지 않기 때문에 provider.tfweb_infra.tf 두 개로 나눠서 작성해보겠습니다.

먼저 provider.tf 파일을 작성합니다. .tf 확장자의 파일은 HCL 언어로 작성 됩니다. HCL은 Hashicorp Configuration Language의 줄임말로 테라폼이나 다른 하시코프 애플리케이션에서 사용하기 위해 만들어진 설정 언어입니다.

HCL로 다음과 같이 AWS 프로바이더를 정의합니다.

provider "aws" {
  access_key = "<AWS_ACCESS_KEY>"
  secret_key = "<AWS_SECRET_KEY>"
  region = "ap-northeast-2"
}

이 때 <AWS_ACCESS_KEY><AWS_SECRET_KEY>는 앞서 생성한 terraform 사용자의 인증 정보로 대체해줍니다. region은 리소스를 정의할 AWS 리전을 설정합니다. 여기서 사용한 ap-northeast-2는 AWS의 서울 리전을 의미합니다.*

* 사용 가능한 모든 리전에 대한 정보는 Amazon Elastic Compute Cloud - 리전 및 가용 영역 문서에서 찾아볼 수 있습니다.

프로바이더 선언은 아래와 같은 형식을 따릅니다.

provider "<PROVIDER_NAME>"

위의 예제에서는 aws 프로바이더를 사용하므로 <PROVIDER_NAME> 자리에 aws가 왔습니다. 프로바이더 선언 뒤로 중괄호 블록이 따라올 수 있습니다.

provider "<PROVIDER_NAME>" { }

중괄호 사이에는 프로바이더에서 사용가능한 하나 이상의 속성(CONFIG)들을 지정할 수 있습니다.

provider "<PROVIDER_NAME>" {
  <OPTION_NAME> = "<OPTION_VALUE>"
}

위에서 정의한 내용을 다시 살펴보면 aws 프로바이더를 선언하고, 이 프로바이더에 access_key, secret_key, region 속성을 지정했다고 이해할 수 있습니다. 참고로 뒤에서 다루게 될 리소스 정의도 프로바이더 정의와 크게 다르지 않습니다.

여기서는 3가지 속성만을 지정했지만, aws 프로바이더는 더 다양한 속성들을 지원합니다. AWS 프로바이더 정의 블록에서 사용할 수 있는 모든 옵션은 테라폼 공식 문서 AWS 프로바이더의 인자 레퍼런스Argument Reference 절에서 찾아볼 수 있습니다.

노트
환경변수로 AWS 프로바이더 설정

AWS 프로바이더를 설정할 때 파일 안에 액세스 키를 기록했습니다. 테라폼의 코드는 일반적으로 깃Git과 같은 버전 관리 도구를 통해 관리되기 때문에 민감한 정보를 기록해서는 안 됩니다. 이러한 문제를 피하기 위해 테라폼을 실행하는 환경에 직접 환경변수를 정의하는 방법을 사용할 수 있습니다. 이 때 사용하는 환경변수는 AWS 커맨드라인 인터페이스AWSCLI에서 사용하는 환경 변수와 같습니다.

$ export AWS_ACCESS_KEY_ID="<AWS_ACCESS_KEY_ID>"         # access_key
$ export AWS_SECRET_ACCESS_KEY="<AWS_SECRET_ACCESS_KEY>" # secret_key
$ export AWS_DEFAULT_REGION="ap-northeast-2"             # region

이러한 환경변수를 정의하면 프로바이더에서 대응하는 옵션들을 생략할 수 있습니다. 자세한 내용은 AWS 커맨드라인 인터페이스(awscli) 기초의 환경변수 절을 참고해주세요.

테라폼 프로젝트 초기화

현재 사용하는 버전이 0.10보다 낮다면 테라폼 프로젝트를 별도로 초기화하지 않아도 됩니다. 0.10 이전에는 테라폼 본체에 프로바이더들이 포함되어 있었지만 0.10부터 프로바이더가 플러그인으로 분리되었고, 이에 따라서 테라폼 프로젝트를 별도로 초기화할 필요가 있어졌습니다. 테라폼은 테라폼 프로젝트를 초기화할 때 프로바이더 설정을 보고 필요한 플러그인을 설치합니다.

web_infra 디렉터리에서 terraform init 명령어를 실행합니다.

$ terraform init
Initializing provider plugins...
 - Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.24.0)...
...

테라폼이 현재 디렉터리에 아래에 선언된 프로바이더 플러그인을 설치해줍니다.

첫 번째 이터레이션: EC2 용 SSH 키 페어 정의

프로바이더 셋업까지가 테라폼을 사용하기 위한 준비 작업이었다면 여기서부터는 리소스를 정의하는 작업입니다. 첫 번째 리소스는 aws_key_pair입니다. AWS에서 제공하는 가상 컴퓨팅 자원인 EC2 인스턴스를 생성하더라도 키 페어가 미리 정의되어 있지 않다면, 생성한 인스턴스에 접근할 수 없습니다. 따라서 인스턴스를 생성하기 전에 먼저 키 페어를 생성해야합니다. 앞서 이야기한 대로 한 스텝씩 진행해나가겠습니다.

첫 번째 스텝: HCL 언어로 필요한 리소스를 정의

첫 번째 스텝은 HCL로 리소스를 정의하는 일입니다. 리소스 정의 형식은 프로바이더와 비슷합니다. web_infra.tf 파일에 다음 내용을 추가해줍니다.

resource "aws_key_pair" "web_admin" {
  key_name = "web_admin"
  public_key = "<PUBLIC_KEY>"
}

여기서 주목할 부분은 resource 키워드 다음에 "aws_key_pair" "web_admin"과 같이 두 개의 문자열이 온다는 점입니다.

첫 번째 문자열은 리소스 타입의 이름입니다. 따라서 이 자리에 올 수 있는 값들은 프로바이더에서 제공하는 리소스 타임의 이름들로 한정되어있습니다. 리소스 이름에서는 관습적으로 프로바이더 이름에 언더스코어 붙여 전치사(prefix)로 사용합니다. 즉, 리소스 이름 aws_key_pair에서 aws 프로바이더가 제공하는 key_pair 리소스라는 것을 유추할 수 있습니다.

두 번째 문자열 web_admin은 이 리소스에 임의로 붙이는 이름입니다. 이 이름은 테라폼 코드의 다른 곳에서 이 리소스를 참조하기 위해서 사용합니다. 보통 리소스 타입과 이름을 .으로 이어 aws_key_pair.web_admin과 같은 형식으로 참조합니다.

그 뒤로 중괄호가 따라오며, 속성을 지정하는 방식은 프로바이더와 같습니다.

{
  key_name = "web_admin"
  public_key = "<PUBLIC_KEY>"
}

key_name은 AWS 상에 현재 정의하는 키 페어를 등록할 이름입니다. 필수적인 것은 아니지만 관리 상의 편의를 위해 리소스의 이름과 같은 이름을 사용하는 것을 권장합니다.

public_key에는 접속에 사용할 공개키의 값을 넣어야합니다. 로컬 환경에 미리 생성해둔 SSH 키가 있다면 이 키를 사용해도 무방합니다. 키가 없다면 새로 생성해야합니다. 다음 명령어로 SSH 키를 생성할 수 있습니다.

$ ssh-keygen -t rsa -b 4096 -C "<EMAIL_ADDRESS>" -f "$HOME/.ssh/web_admin" -N ""

명령어를 실행하고 ~/.ssh 디렉터리를 확인 하면 비밀키 web_admin과 공개키 web_admin.pub 두 개의 파일이 생성되어 있을 것입니다. 여기서 필요한 것은 공개키인 web_admin.pub 파일의 내용입니다. 공개키의 내용을 테라폼에 추가하는 방법은 크게 두 가지가 있습니다.

첫번째 방법은 파일의 내용을 복사해서 <PUBLIC_KEY>를 대체하는 방법입니다.

단, 파일 내용이 문자열에 담기에는 조금 길어보일 수도 있습니다. 그렇다면 두 번째 방법을 사용할 수 있습니다. file() 함수를 사용하면, 로컬 파일의 내용을 읽어와 속성값을 지정할 수 있습니다. 위와 같이 ~/.ssh/web_admin.pub 경로에 공개키가 있다면 다음과 같이 public_key 속성을 지정하면 됩니다.

public_key = "${file("~/.ssh/web_admin.pub")}"

여기서는 ${} 형식의 문법이 사용되었습니다. 이는 프로그래밍 언어에서 문자열 보간String Interpolation이라고 부르는 문법으로 프로그래밍 언어 코드를 실행시킨 반환값을 문자열 중간에 삽입하는 문법입니다. HCL에서는 문자열에서 ${} 문법으로 문자열 보간을 사용할 수 있습니다. 중괄호 사이에는 변수를 참조하거나 함수를 사용할 수 있습니다. public_key를 파일에서 읽어온다면 최종적인 모습은 다음과 같습니다.

resource "aws_key_pair" "web_admin" {
  key_name = "web_admin"
  public_key = "${file("~/.ssh/web_admin.pub")}"
}

여기까지 첫 번째 리소스를 작성했습니다.

두 번째 스텝: 선언한 리소스들이 생성가능한지 계획(Plan)을 확인

이제 앞서 작성한 aws_key_pair 리소스를 실제로 AWS에 생성할 수 있는지 확인해야합니다.

프로젝트 디럭터리에서 terraform plan을 실행합니다. plan 명령어를 사용하면 현재 정의되어있는 리소스들을 실제로 프로바이더에 적용했을 때 테라폼이 어떤 작업을 수행할지 계획을 보여줍니다.

❯ terraform plan
...
  + aws_key_pair.web_admin
      fingerprint: "<computed>"
      key_name:    "web_admin"
      public_key:  "ssh-rsa ..."

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

aws_key_pair.wed_admin 리소스를 +(추가)하겠다는 계획을 보여줍니다.

plan의 작동 방식을 이해하기 위해서는 테라폼의 동작 방식을 이해할 필요가 있습니다. 테라폼에서 리소스는 선언적으로 기술됩니다. 테라폼은 .tf 파일에 기술되어있는 모든 리소스를 읽어들이고 먼저 이 리소스들이 존재하는 상태를 가정합니다. 편의상 이를 이상적인 상태라고 부르겠습니다. 예를 들어 aws_key_pair.web_admin은 이상적인 상태에 존재하는 리소스입니다. 하지만 이 리소스는 생성된 적이 없기 때문에 프로바이더에 지정한 AWS 계정에는 존재하지 않습니다. 즉, 실제 상태에는 aws_key_pair.web_admin이 아직 존재하지 않습니다.

리소스를 추가하기 전의 최초 상태
리소스를 추가하기 전의 최초 상태

테라폼의 가장 중요한 역할을 실제 상태를 이상적인 상태와 동일하게 만드는 일입니다. 테라폼에서는 이 작업을 적용한다apply고 표현합니다. 테라폼 planapply하기 전에 이상적인 상태와 실제 상태를 비교해 둘을 동일하게 만들기 위해서 해야할 일을 비교하는 작업입니다. 이 예제에서 aws_key_pair.web_admin 리소스는 이상적인 상태에만 존재하고 실제 상태에는 존재하지 않습니다. 따라서 테라폼이 실제 상태를 이상적인 상태와 동일하게 만들기 위해서는 실제 상태에 aws_key_pair.web_admin 리소스를 추가해야 합니다.

terraform plan이 보여주는 리소스 생성 계획
terraform plan이 보여주는 리소스 생성 계획

이게 바로 + aws_key_pair.web_admin이 의미하는 바입니다. 그 아래로 보여지는 정보들은 새로 추가될 리소스의 속성들입니다. 이는 .tf 파일에서 정의한 내용일 수도 있고, 직접 지정하지 않았다면 기본값일 수도 있습니다. 단, <computed>라고 표시된 값은 실제로 리소스를 생성해야만 확정되는 값입니다.

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

맨 아래 줄에는 변경 예정 사항을 한 줄로 요약해서 보여줍니다. 현재 정의된 .tf 파일들을 적용할 경우 1개의 리소스를 추가하고, 0개를 변경하고, 0개를 제거한다는 의미입니다.

세 번째 스텝: 선언된 리소스들을 아마존 웹 서비스에 적용(Apply)

이제 계획plan을 통해 확인한 내용을 실제로 프로바이더에 적용해봅니다. terraform apply 명령어를 실행합니다.

$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:                                                                               + create                                                                                                                                                                                                                                                                    Terraform will perform the following actions:                                                                                                                                                                                                                                   + aws_key_pair.web_admin
      id:          <computed>
      fingerprint: <computed>
      key_name:    "web_admin"
      public_key:  "sha-rsa ..."

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

Do you want to perform these actions? yes
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve

이전에는 apply 명령어를 실행하면 곧바로 리소스를 생성했습니다. 현재는 리소스를 생성하기 전에 plan 결과를 보여주고 yes를 입력해야만 리소스를 생성합니다. yes를 입력합니다.

aws_key_pair.web_admin: Creating...
  fingerprint: "" => "<computed>"
  key_name:    "" => "web_admin"
  public_key:  "" => "ssh-rsa ..."
aws_key_pair.web_admin: Creation complete (ID: web_admin)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

정상적으로 aws_key_pair.web_admin 리소스가 생성된 것을 확인할 수 있습니다. 추가된 리소스는 웹 콘솔의 EC2 서브 메뉴에서도 확인할 수 있습니다.

웹 콘솔에서 새로 추가된 web_admin 키를 확인하기
웹 콘솔에서 새로 추가된 web_admin 키를 확인하기

앞서 설명했듯이 테라폼은 로컬에 정의된 이상적인 상태와 실제 상태를 동일하게 만듭니다. 계획에서 보여준대로 실제 상태에 aws_key_pair.web_admin 리소스를 생성했습니다.

tf 파일들에 정의된 리소스들을 AWS에 적용한 후의 상태
tf 파일들에 정의된 리소스들을 AWS에 적용한 후의 상태

이제 이상적인 상태와 실제 상태는 같습니다. 이 상태에서 terraform plan을 실행해봅니다.

$ terrafrom plan
...
aws_key_pair.web_admin: Refreshing state... (ID: web_admin)
No changes. Infrastructure is up-to-date.

현재 상태를 최신화하고 다시 두 상태를 비교합니다. 결과는 No changes. 즉, 두 상태가 같기 때문에 더 이상 변경할 게 없다는 의미입니다. 테라폼에서 리소스를 선언적으로 정의한다는 의미는 여기에서도 잘 드러납니다. 테라폼의 리소스 정의는 어떤 리소스를 생성하라는 절차적인 명령어가 아닙니다. 단지 이상적인 상태를 정의할 뿐입니다. 따라서 terraform apply를 여러번 실행하더라도 아무런 일도 일어나지 않습니다. 다시 한 번 terraform apply를 실행해보겠습니다.

$ terraform apply
aws_key_pair.web_admin: Refreshing state... (ID: web_admin)

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

아무런 변화도 일어나지 않습니다. 그렇다면 테라폼은 이 상태 변화를 어떻게 관리하는 걸까요. terraform apply를 실행하고 나면 프로젝트 상에 중요한 변화가 하나 생깁니다. 작업 디렉터리 아래에 terraform.tfstate 파일이 하나 생성됩니다. 이 파일은 실제 상태를 임시로 저장하는 동시에 어떤 리소스가 테라폼에서 관리하는 리소스임을 기록합니다.

두 번째 이터레이션: SSH 접속 허용을 위한 시큐리티 그룹

두 번째로 정의할 리소스는 aws_security_group입니다. 이 리소스 역시 키 페어와 마찬가지로 인스턴스를 정의하는 데 필요합니다. 인스턴스를 생성해도 밖에서 접근할 수 있는 방법이 없다면 사용할 수가 없습니다. 따라서 SSH port를 외부에 열어주는 시큐리티 그룹을 만들 필요가 있습니다. 다음 내용을 web_infra.tf 맨 아래에 추가해줍니다

resource "aws_security_group" "ssh" {
  name = "allow_ssh_from_all"
  description = "Allow SSH port from all"
  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

여기서는 이름을 ssh로 붙여주었습니다. 키 페어 정의와 마찬가지 방법으로 속성을 지정합니다. namedescription에는 각각 시큐리티 그룹의 이름과 설명을 기입합니다. 여기서 특이한 점은 ingress 속성에 직접 값을 지정하는 대신 중괄호 블록이 따라온다는 점입니다. ingress는 인바운드 트래픽을 정의하는 속성으로 시큐리티 그룹에는 ingress 블록이 하나 이상 올 수 있습니다. *

* 여기서는 사용하지 않았지만, 필요에 따라 아웃바운드 트래픽을 제어하는 egress 속성도 사용할 수 있습니다)

ingress 블록 안에는 from_port, to_port, protocol, cidr_blocks 속성을 지정합니다.

from_portto_port는 열어줄 포트의 범위를 의미합니다. 예를 들어 from_port가 60001이고 to_port가 60010이면, 60001부터 60010까지 10개의 포트를 열어줍니다. 여기서는 SSH(22) 포트만 허용하므로 둘 다 22로 지정합니다.

protocol은 통신에 사용할 프로토콜입니다. SSH는 TCP를 사용하므로 tcp를 지정합니다.

마지막으로 cidr_blocks은 배열로 시큐리티 그룹을 적용할 사이더 범위를 지정합니다. 여기서 지정된 0.0.0.0/0은 모든 IP에서 접속을 허용한다는 의미입니다.

이제 plan을 실행해봅니다.

$ terraform plan
  + aws_security_group.ssh
      name:                                  "allow_ssh_from_all"
      description:                           "Allow SSH port from all"
      egress.#:                              "<computed>"
      ingress.#:                             "1"
      ingress.2541437006.cidr_blocks.#:      "1"
      ingress.2541437006.cidr_blocks.0:      "0.0.0.0/0"
      ingress.2541437006.from_port:          "22"
      ingress.2541437006.ipv6_cidr_blocks.#: "0"
      ingress.2541437006.protocol:           "tcp"
      ingress.2541437006.security_groups.#:  "0"
      ingress.2541437006.self:               "false"
      ingress.2541437006.to_port:            "22"
      name:                                  "<computed>"
      owner_id:                              "<computed>"
      vpc_id:                                "<computed>"

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

하나의 시큐리티 그룹이 추가될 예정임을 확인할 수 있습니다. 이 계획을 적용합니다.

$ terraform apply
aws_security_group.ssh: Creating...
aws_security_group.ssh: Creation complete

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

세 번째 이터레이션: EC2 인스턴스 정의

앞서 정의한 키 페어와 시큐리티 그룹을 사용해 EC2 인스턴스를 정의할 차례이지만, 그 전에 한 가지 추가할 내용이 있습니다. 바로 VPC의 기본(default) 시큐리티 그룹을 불러오는 일입니다. 테라폼에서는 이미 클라우드 상에 정의되어 있는 리소스를 데이터 소스로 불러오는 기능을 제공합니다. aws_security_group 데이터 소스를 사용해, 미리 정의된 시큐리티 그룹을 불러올 수 있습니다. 리소스를 정의하기 전에 다음 내용을 먼저 추가해줍니다.

data "aws_security_group" "default" {
  name = "default"
}

이 데이터 소스는 이름이 default인 시큐리티 그룹을 찾아 해당 리소스의 속성들을 참조할 수 있게해줍니다. *

* VPC가 하나가 아니거나 별도로 default와 같은 이름으로 시큐리티 그룹을 생성한 적이 있다면, 찾고자 하는 리소스가 유일하다는 걸 보장할 수 없습니다. 이 경우 id, vpc_id, tags와 같은 추가적인 필터를 사용해서 정확하게 사용하고자 하는 VPC의 default 시큐리티 그룹을 참조해야합니다.

이제 인스턴스를 정의해보겠습니다. EC2 인스턴스를 정의하는 리소스는 aws_instance입니다. 다음 내용을 맨 아래에 추가합니다.

resource "aws_instance" "web" {
  ami = "ami-e21cc38c" # Amazon Linux AMI 2017.03.1 Seoul
  instance_type = "t2.micro"
  key_name = "${aws_key_pair.web_admin.key_name}"
  vpc_security_group_ids = [
    "${aws_security_group.ssh.id}",
    "${data.aws_security_group.default.id}"
  ]
}

리소스 이름은 web으로 지정했습니다. AMIAmazon Machine Image는 AWS에서 기본적으로 제공하는 아마존 리눅스 이미지 최신 버전*ami-e21cc38c를 사용합니다. HCL에서 # 뒤에 오는 문자열은 주석으로 무시됩니다. instance_typet2.micro로 지정했습니다.

* 2017년 8월 기준 최신 AMI입니다. 다른 리전을 사용한다면 Amazon Linux AMI 문서에서 해당 리전의 AMI ID를 확인하고 대체해야 합니다.

key_name에는 EC2 키 페어의 이름을 지정합니다. 앞서 정의한 web_admin을 문자열로 지정할 수도 있지만, 여기서는 변수를 사용해 앞서 정의한 aws_key_pair.web_admin 리소스의 key_name이라는 속성을 참조하는 방식을 사용했습니다.

여기에는 두 가지 이유가 있습니다. 첫 번째 이유는 어떤 리소스를 정의할 때 다른 리소스들의 속성을 참조할 수 있음으로 보여주기 위해서입니다. 두 번째 이유가 더 중요합니다. 어떤 리소스(B 리소스)에서 다른 리소스(A 리소스)의 속성을 참조하면, 두 리소스 간에 간접적 의존 관계가 생깁니다. 즉, B 리소스는 A 리소스에 의존적이 됩니다. 테라폼은 (소스코드의 순서가 아니라) 그래프 모델로 이러한 의존 관계를 정의하고 리소스를 생성할 순서를 결정합니다. 따라서 위와 같이 정의한 경우 테라폼은 aws_key_pair.web_admin 리소스가 aws_instance.web 리소스보다 먼저 생성 되는 것을 보장해줍니다.

마지막으로 vpc_security_group_ids를 지정합니다. 이 값은 배열로 지정합니다. 첫 번째 값은 앞서 생성한 ssh 시큐리티 그룹의 id 속성을 참조합니다. 두 번째 값은 역시 앞서 정의한 default 데이터 소스의 id 속성을 참조합니다. 데이터 소스를 참조하는 방법은 기본적으로 리소스를 참조하는 방법과 같습니다만, 앞에 data.을 붙여주어야 합니다.

이제 terraform plan을 실행합니다. aws_instance.web 리소스를 추가할 것이라는 계획을 보여줍니다.

$ terrafrom plan
  + aws_instance.web
      ami:                          "ami-e21cc38c"
      associate_public_ip_address:  "<computed>"
      availability_zone:            "<computed>"
      ...

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

바로 적용해보겠습니다. EC2 인스턴스 생성에는 20초~1분 정도의 시간이 소요됩니다.

$ terraform apply
aws_key_pair.web_admin: Refreshing state... (ID: web_admin)
aws_instance.web: Creating...
  ami:                          "" => "ami-e21cc38c"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  ...
aws_instance.web: Still creating... (10s elapsed)
aws_instance.web: Still creating... (20s elapsed)
aws_instance.web: Creation complete (ID: i-066207312757476b7)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

리소스 생성이 완료 되었다면 웹 콘솔에서도 확인할 수 있습니다. 출력된 인스턴스 ID으로 검색해봅니다.*

* 위의 예제에서는 i-066207312757476b7. 실행 결과는 매번 다릅니다.

웹 콘솔에서 테라폼으로 생성한 EC2 인스턴스를 확인
웹 콘솔에서 테라폼으로 생성한 EC2 인스턴스를 확인

정상적으로 인스턴스가 생성되었음을 확인할 수 있습니다. terraform console을 실행하면 대화형 콘솔에서 생성된 리소스의 속성을 확인해볼 수 있습니다. SSH 접속을 위해 aws_instance.web.public_ip를 조회해봅니다.

$ terraform console
> aws_instance.web.public_ip
13.124.222.81

SSH로 출력된 IP에 접속합니다.* -i 옵션에는 앞서 생성한 비밀키 경로를 지정해줍니다. 로그인 아이디에는 아마존 리눅스의 기본 유저인 ec2-user를 사용합니다.

* 직접 실행해서 출력된 IP로 대체해야합니다.

$ ssh -i ~/.ssh/web_admin ec2-user@13.124.222.81
The authenticity of host '13.124.222.81 (13.124.222.81)' can't be established.
ECDSA key fingerprint is SHA256:48eSPznWLvWIuFkUsdudCsLJCGHIMPxHYOxq72bqdGc.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '13.124.222.81' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.03-release-notes/
$ 

정상적으로 SSH에 접속이 가능한 것을 확인할 수 있습니다.

네 번째 이터레이션: RDS 인스턴스 정의

웹 서비스 실행을 위한 마지막 리소스입니다. 이번에는 AWS가 직접 관리해주는 데이터베이스 서비스 RDS의 MySQL 리소스를 생성해보겠습니다. 다음 내용을 추가합니다.

resource "aws_db_instance" "web_db" {
  allocated_storage = 8
  engine = "mysql"
  engine_version = "5.6.35"
  instance_class = "db.t2.micro"
  username = "admin"
  password = "<DB_PASSWORD>"
  skip_final_snapshot = true
}

RDS 인스턴스 리소스는 aws_db_instance입니다. 이름은 web_db로 지정했습니다. 위에서 지정한 각 속성의 의미는 다음과 같습니다.

allocated_storage
할당할 용량(기가바이트 단위)
engine
데이터베이스 엔진
engine_version
사용할 데이터베이스 엔진 버전
instance_class
인스턴스 타입(RDS 인스턴스 타입만 사용 가능)
username
계정 이름
password
암호. <DB_PASSWORD>는 자신이 사용할 값으로 적절히 변경해줍니다.
skip_final_snapshot
인스턴스 제거 시 최종 스냅샷을 만들지 않고 제거할 지를 결정합니다. 기본값은 false입니다. 단, 이 경우 테라폼에서 인스턴스 삭제가 어려우므로, 여기서는 true를 지정해줍니다.

레퍼런스 매뉴얼을 보더라도 속성에 어떤 값을 지정해야할 지 알기 어려운 경우도 있습니다. 예를 들어 사용가능한 engine_version 속성은 어떻게 확인해야할까요. 웹 콘솔의 RDS 메뉴에서, MySQL 생성 메뉴에 들어가보는 것이 정확합니다. 엔진 버전을 지정하는 셀렉트 박스를 열어보면 현재 사용가능한 엔진의 버전을 확인할 수 있습니다.

이러한 과정이 불필요하고 번거롭게 느껴질지도 모릅니다. 엄밀히 말해 테라폼은 이러한 과정을 대체하기 위한 도구가 아닙니다. 테라폼은 프로비저닝을 위한 이상적인 상태를 파일로 선언하기 위한 도구라고 생각하는 게 좋습니다. 적절한 방법으로 사용가능한 속성 값을 찾아서 채워주어야 합니다.

plan 명령어를 실행해봅니다.

$ terraform plan
  + aws_db_instance.web_db
      address:                    "<computed>"
      allocated_storage:          "8"
      apply_immediately:          "<computed>"
      ...
Plan: 1 to add, 0 to change, 0 to destroy.

하나의 aws_db_instance를 추가할 것이라는 계획을 보여줍니다. 바로 적용합니다.

$ terraform apply
aws_key_pair.web_admin: Refreshing state... (ID: web_admin)
aws_instance.web: Refreshing state... (ID: i-066207312757476b7)
aws_db_instance.web_db: Creating...
  address:                    "" => "<computed>"
  allocated_storage:          "" => "8"
  apply_immediately:          "" => "<computed>"
  ...
aws_db_instance.web_db: Still creating... (10s elapsed)
aws_db_instance.web_db: Still creating... (20s elapsed)
...
aws_db_instance.web_db: Still creating... (4m0s elapsed)
aws_db_instance.web_db: Creation complete (ID: terraform-002795237b5fb0e3536a82fa85)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

RDS 생성에는 5분 정도 꽤 오랜 시간이 걸립니다. RDS 인스턴스가 생성되고 나면, 이번에는 terraform console에서 데이터베이스의 endpoint 속성을 확인합니다.

$ terraform console
> aws_db_instance.web_db.endpoint
terraform-009ef698a81157df21254b7c4d.crl9efu8steh.ap-northeast-2.rds.amazonaws.com:3306

그럼 이제 이 주소로 접속해보겠습니다. 먼저 앞서 생성한 EC2 인스턴스로 접속하고, mysql을 설치합니다.

$ ssh -i ~/.ssh/web_admin ec2-user@13.124.222.81

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

$ sudo yum install -y mysql
...

MySQL 클라이언트 설치가 완료되었으면, 위에서 출력한 엔드포인트로 접속해봅니다.

$ mysql -h terraform-009ef698a81157df21254b7c4d.crl9efu8steh.ap-northeast-2.rds.amazonaws.com -u admin -p
Enter password:

...
mysql>

정상적으로 MySQL 서버에 접속이 되면 성공입니다.

여기까지 테라폼을 사용해 웹 서비스를 배포할 EC2 인스턴스와 RDS 인스턴스를 생성해보았습니다. 즉, 웹 서비스 배포를 위한 준비가 모두 끝났습니다. 이제 프로비저닝 EC2 인스턴스에서 웹 서비스 실행을 위한 라이브러리를 설치하고, RDS 접속 정보를 추가해 애플리케이션을 배포하기만 하면 됩니다.

최종 테라폼 프로젝트 소스 코드

여기까지 50줄 남짓한 테라폼 정의 파일을 작성해보았습니다. 아래는 provider.tf 소스 코드입니다.

provider "aws" {
  access_key = "<AWS_ACCESS_KEY>"
  secret_key = "<AWS_SECRET_KEY>"
  region = "ap-northeast-2"
}

아래는 web_infra.tf 소스코드입니다.

resource "aws_key_pair" "web_admin" {
  key_name = "web_admin"
  public_key = "${file("~/.ssh/web_admin.pub")}"
}

resource "aws_security_group" "ssh" {
  name = "allow_ssh_from_all"
  description = "Allow SSH port from all"
  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

data "aws_security_group" "default" {
  name = "default"
}

resource "aws_instance" "web" {
  ami = "ami-e21cc38c" # Amazon Linux AMI 2017.03.1 Seoul
  instance_type = "t2.micro"
  key_name = "${aws_key_pair.web_admin.key_name}"
  vpc_security_group_ids = [
    "${aws_security_group.ssh.id}",
    "${data.aws_security_group.default.id}"
  ]
}

resource "aws_db_instance" "web_db" {
  allocated_storage = 8
  storage_type = "gp2"
  engine = "mysql"
  engine_version = "5.6.35"
  instance_class = "db.t2.micro"
  username = "admin"
  password = "<DB_PASSWORD>"
  skip_final_snapshot = true
}

프로비저닝된 인프라스트럭처 일괄 종료

테라폼의 장점은 단순히 코드로 인프라를 생성하는 데만 있지는 않습니다. 생성한 인프라 전체를 한 번에 종료하는 기능도 지원합니다. 이해를 돕기 위해 잠시 web_infra.tf을 다른 곳으로 옮겨두겠습니다.

$ mv web_infra.tf /tmp/

즉, 프로젝트 디렉터리에는 provider.tf만 남아있는 상태입니다. 이 상태에서 terraform plan을 실행하면 어떻게 될까요? 잠시 상상해보신 후 실제 결과를 확인해보시기 바랍니다.

$ terraform plan
...
  - aws_instance.web
  - aws_security_group.ssh
  - aws_db_instance.web_db
  - aws_key_pair.web_admin

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

+와 반대로 -는 어떤 리소스를 제거한다는 의미입니다. 테라폼은 모든 리소스를 제거할 거라는 계획을 보여줍니다. 이유는 간단합니다. 이상적인 상태에 리소스가 없기 때문에, 실제로 생성되어 있는 리소스들을 제거하려고 하는 것입니다.

리소스 정의는 없지만, 실제로는 리소스가 생성되어 있는 상태
리소스 정의는 없지만, 실제로는 리소스가 생성되어 있는 상태

이제 다시 web_infra.tf 파일을 복사해옵니다.

$ mv /tmp/web_infra.tf .

이 상태에서 plan을 실행하면, No changes가 출력 됩니다.

테라폼은 리소스 정의 파일을 프로젝트에서 제거하지 않고도 리소스가 아무것도 없는 상태에서 plan을 실행해볼 수 있는 옵션을 지원합니다. plan-destroy 옵션을 붙여서 실행해봅니다.

$ terraform plan -destroy
...
  - data.aws_security_group.default
  - aws_security_group.ssh
  - aws_instance.web
  - aws_key_pair.web_admin
  - aws_db_instance.web_db

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

여기서는 데이터 소스까지 표시되었지만, 실질적으로 앞에서 리소스 정의 파일을 제외시키고 plan한 것과 같은 결과를 얻을 수 있습니다. 이 계획을 적용하려면 terraform destroy 명령어를 실행합니다.

$ terraform destroy
...
  - aws_instance.web
  - aws_db_instance.web_db
  - data.aws_security_group.default
  - aws_security_group.ssh
  - aws_key_pair.web_admin

Do you really want to destroy?
  Terraform will delete all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

정확히 yes라고 입력하고 엔터를 누르면 지금까지 생성한 리소스들을 전부 제거합니다.

모든 리소스 다시 프로비저닝하기

모든 리소스를 파괴했지만, 리소스 정의 파일은 그대로 남아있습니다. 따라서 현재 상태는 정확히 아래 그림과 같습니다.

모든 리소스가 정의되어 있고, 아직 생성은 되지 않은 상태
모든 리소스가 정의되어 있고, 아직 생성은 되지 않은 상태

반드시 plan부터 실행해서 계획을 확인합니다.

$ terraform plan
...
  + aws_instance.web
  ...
  + aws_db_instance.web_db
  ...
  + aws_security_group.ssh
  ...
  + aws_key_pair.web_admin
  ...

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

4개의 리소스가 생성될 계획입니다. 바로 apply해서 리소스를 생성해봅니다.

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

4개의 리소스가 성공적으로 생성되면 아래 그림과 같은 상태가 됩니다.

정의된 모든 리소스가 한 번에 생성된 상태
정의된 모든 리소스가 한 번에 생성된 상태

이렇게 테라폼을 사용하면 plan, apply, destroy를 반복하면서 큰 부담 없이 인프라를 생성하고 파괴하면서 점진적으로 구성해나갈 수 있습니다. 그리고 최종적으로 구성된 결과는, 마지막에 확인한 것처럼 쉽게 파괴하고 재구성할 수 있습니다.

마치며: 테라폼을 사용하는 이유

여기까지 테라폼으로 AWS 리소스들을 정의하고 프로비저닝하는 방법에 대해서 알아보았습니다. 웹 콘솔을 사용해 리소스를 관리하는 것과는 많이 다릅니다. 여기서는 정말 간단한 사례를 구현해보았지만, 그럼에도 불구하고 간단해보이지는 않습니다. 프로젝트에서 테라폼을 사용해보면 매번 리소스 레퍼런스를 확인하고, 웹 콘솔과 비교해보는 과정을 계속해서 반복해야합니다. 어떤 면에서는 웹 콘솔보다 오히려 어렵고 귀찮습니다. 그렇다면 테라폼을 사용할 필요가 있을까요.

이 글에서는 테라폼의 사용법을 소개하는 데 집중해서 자세히는 다루지 않았지만, 테라폼을 사용하면 좋은 점들이 있습니다. 먼저 웹 콘솔을 사용해 리소스들을 관리하면서 차츰 리소스가 많아지면, 어느 지점에는 더 이상 전체를 관리할 수 없는 순간이 오게 됩니다. 언제 왜 만들었는지 알 수 없는 리소스들도 점점 쌓여나갑니다. 테라폼을 사용하면 프로비저닝하고자 하는 상태를 코드로 명확히 기록해두기 때문에 웹 콘솔만 사용할 때보다 세심한 관리가 가능해집니다.

또한 테라폼은 코드로서의 인프라스터럭처Infrastructure as Code를 지향하는 도구로서, 코드를 작성할 때 누리던 생태계의 이점을 그대로 이용할 수 있습니다. 저장소에서 이력 추적을 할 수도 있고, 깃허브Github에서 팀원들과 코드 리뷰를 진행할 수도 있습니다. 이 과정에서 누가 어떤 리소스를 왜 추가했는지, 투명성은 자동적으로 얻어집니다. 좀 더 잘 활용한다면 CI를 사용해 코드 리뷰가 된 사항을 자동적으로 플랜 및 적용하는 것도 가능합니다.

마지막으로, 테스트나 연습으로 만든 리소스들은 삭제하는 걸 잊지 마시기 바랍니다.

$ terraform destroy

더 읽을거리

테라폼 공식 문서

테라폼 - AWS 프로바이더 레퍼런스

테라폼을 소개한 한국어 문서