왜 테라폼인가?
클라우드와 코드형 인프라스트럭처
코드형 인프라스트럭처(Infrastructure as Code, laC)는 시스템 인프라스트럭처(system infrastructur) 이하 인프라를 사람의 개입에 의한 물리적인 방식에 의존하지 않고, 기계가 읽을 수 있는 설정 코드로 정의하여 자동으로 관리하는 프로비저닝(provisioning) 방식을 말한다. IaC가 관리하는 인프라는 베어메탈 서버(bare-metal server), 가상 머신(virtual machine, VM) 등을 가리지 않는다. 다만 laC라는 개념이 대중화된 계기는 클라우드 컴퓨팅의 발전과 이에 따른 클라우드 네이티브 패러다임의 탄생이다. 그러므로 클라우드 네이티브 패러다임에 대한 이해가 먼저 필요하다.
이미 테라폼(Terraform)과 아마존 웹 서비스(Amazon Web Services, AWS)에 대한 경험이 있는 독자들도 많겠지만, 배경지식을 짧게 소개하고자 한다. 다만 이는 이 책 전체의 엄밀한 서술을 위한 것이지, 사전 지식이 없는 사람들에게 배경지식을 전달하기 위해 설명하는 것은 아니다.
클라우드 컴퓨팅 vs. 온프레미스 컴퓨팅
클라우드 컴퓨팅(cloud computing)이 고도로 발전하면서 AWS나 마이크로소프트 애저(Microsoft Azure) 등 사설 클라우드 제공자로부터 컴퓨팅 인스턴스를 대여하고 실행하는 것이 매우 쉽고 저렴해졌다. 이에 따라 전통적으로 베어메탈 서버를 업체마다 보유하고, 그 위에 애플리케이션을 배포하는 온프레미스(on-premises) 접근 방식의 단점이 드러나기 시작했다.
- 저렴한 초기 비용
클라우드 컴퓨팅의 발전이 불러온 근본적인 변화는 인프라 프로비저닝에 들어가는 초기 비용 투자가 없어졌다는 것이다. 이로 인해 수많은 기업이 적은 자본으로도 애플리케이션 개발을 시작할 수 있게 되었으며, 특히 소규모 스타트업이나 1인 개발자들이 저렴한 비용으로 자신의 아이디어를 애플리케이션으로 구현하여 세상에 보여줄 수 있게 되었다. 이로 인해 파생된 가장 중요한 클라우드 컴퓨팅의 특성이 바로 다음에 언급할 수직 확장성과 수평 확장성이다.
- 수직 확장성
애플리케이션의 복잡성이 증가하거나 요청 수요가 증가하면 점점 컴퓨팅 리소스 한 대가 애플리케이션의 요구사항을 소화할 수 없게 되는 시점이 온다. 이를 리소스 과다사용(resource over-utilization)이라고 한다. 이때 CPU 성능이나 랜덤 액세스 메모리(random-access memory, RAM)의 용량 및 네트워크 성능을 업그레이드를 해야 하는데, 클라우드 컴퓨팅을 이용하면 간단한 API 요청으로 업그레이드를 수행할 수 있다. 물론 리소스 과소사용(resource under-utilization)의 경우에는 성능을 낮춰 비용을 절약하는 것도 가능하다. 이를 클라우드 컴퓨팅 리소스의 수직 확장성(vertical scalability)이라고 한다
- 수평 확장성
베어메탈 서버를 물리적인 공간에 둘 필요가 없어지면서 자연스럽게 여러 대의 컴퓨팅 리소스를 프로비저닝하는 것이 매우 용이해졌다. 이러한 특성을 이용하여 HTTP 요청의 수가 급증하는 등의 이벤트가 있을 때 컴퓨팅 리소스를 일시적으로 더 띄워서 요청을 처리할 수 있게 되었다. 또한 요청 수요가 평상시 상태로 돌아갔을 때 다시 원래의 컴퓨팅 리소스의 수로 되돌리는 것도 가능해졌다. 이를 클라우드 컴퓨팅 리소스의 수평 확장성(horizontal scalability)이라고 한다.
온프레미스 컴퓨팅 환경이라면 트래픽 상황에서 매우 무겁고 공간을 많이 차지하는 서버 랙을 긴급하게 구매할 수도 없고, 설사 서버를 증설했다 하더라도 비상 상황이 끝난 이후 쉽사리 되팔 수도 없다. 그러므로 클라우드 컴퓨팅의 수평 확장성은 온프레미스 컴퓨팅에 비해 확실한 비교우위를 가지고 있다. 수평 확장성 개념으로 인해 사용자들은 확장 가능성을 염두에 둔 애플리케이션을 개발하기 시작했고, 끊임없이 더 높은 단계의 확장성을 추구하며 컨테이너 기반 서비스 및 마이크로 서비스(microservice) 등의 개념이 탄생했다.
선언형 laC 도구의 필요성
지금까지 언급한 것처럼 클라우드 인프라 관리는 다양한 종류의 인프라 리소스를 동시에 대규모로 관리해야 하므로, 서비스가 성장할수록 수동으로 관리하기에는 무리가 따른다. 특히 인프라의 규모와 복잡도만 증가하는 것이 아니라, 이를 관리하는 관리자의 수도 늘어나며 자연스럽게 협업에 대한 안전성이 확보되어야 한다. 이것이 IaC 도구에 선언형(declarative) 패러다임이 사용되기 시작한 이유다.
선언형 패러다임은 명령형 패러다임과 반대로 프로그램이나 시스템의 상태가 ‘어떻게’ 구성되어야 하는지를 미리 ‘선언’하는 방식을 취한다. IaC 도구를 선언형으로 만들면 사용자는 행동을 코드에 작성하는 것이 아니라 인프라 리소스와 리소스 사이의 관계를 작성하게 된다.
선언형 IaC 도구에서 인프라 리소스의 상태 변경이 필요한 경우, 상태 변경 스크립트를 작성하는 것이 아니라 리소스의 기대 상태(desired status)를 변경하는 것으로 목적을 달성할 수 있다. 또한 현재 인프라 프로비저닝 상태와 코드의 기대 상태가 같다면, 인프라의 상태 변경은 일어나지 않는다. 이미 코드가 기대하는 상태를 실제 인프라가 만족하기 때문이다.
그러므로 이 책에서 주로 이야기할 테라폼을 포함한 선언형 IaC 도구에서는 상태 변경 연산을 여러 번 반복해 수행하더라도 해당 연산을 딱 한 번 수행했을 때와 동일한 인프라 상태를 유지한다. 수학이나 컴퓨터 과학에서는 이렇게 반복 연산에도 결과가 달라지지 않는 성질을 멱등성이 있다(idempotent)라고 말한다. 이는 명령형 패러다임에서는 달성하기 어려운 선언형 IaC 도구의 중요 특징이다.
또한 인프라 리소스별 기대 상태를 코드에 기술하는 방식이므로 형상관리 시 가장 최근 커밋을 보기만 해도 현재 인프라의 상태를 잘 이해할 수 있다. 또한 인프라의 전체적인 모습이 시간순으로 어떻게 변경되어 왔는지 맥락을 잘 파악할 수 있으며, 장애 상황에서 이전 커밋으로 쉽게 롤백하여 프로비저닝할 수 있다.
우리는 왜 테라폼을 쓰는가?
IaC 도구는 우선순위에 따라 나열된 다음과 같은 특성을 가져야 한다.
1) 해당 도구의 인프라 관리 방식이 선언형 패러다임을 따라야 한다.
2) 최소 AWS, 쿠버네티스, 헬름을 포함한 다양한 프로바이더를 제공해야 한다.
3) 해당 도구의 스크립트 언어가 선언형 패러다임을 따라야 한다.
선언형 인프라 관리
테라폼은 전형적인 선언형 laC 도구로, 리소스에 대한 명령이 아닌 리소스의 상태를 선언하는 형태로 스크립트를 작성한다. 그리고 멱등성을 보장하기 위해 현재 인프라의 프로비저닝 상태를 스캔하여 테라폼 상태(Terraform states)의 형태로 저장하며, 코드의 기대 상태와 비교한다. 만약 테라폼 상태가 기대 상태와 다르다면 현재 상태를 기대 상태에 맞게 변경한 뒤 테라폼 상태를 갱신한다. 만약 테라폼 상태가 기대 상태와 같다면, 아무런 행동도 하지 않는다.
다양한 프로바이더
우리는 IaC 도구를 사용하여 주로 AWS 인프라를 관리하고자 한다. 여기에 더해, EKS(Elastic Kubernetes Service)로 프로비저닝되는 쿠버네티스 클러스터의 불변(immutable) 리소스를 관리하기 위해 쿠버네티스나 헬름 등의 프로바이더를 지원해야 한다. 주로 AWS를 사용한다 하더라도, 비즈니스 확장에 따른 요구사항 만족을 위해 여러 개의 클라우드 프로바이더, 더 나아가 온프레미스 사용에 대한 대비도 필요하다. 예를 들면 부분적인 멀티 클라우드 환경을 구성하는 것도 매우 가능성이 높은 시나리오다. 데이터 웨어하우스(data warehouse)를 도입해야 할 경우 아마존 레드시프트(Amazon Redshift)를 사용하기보다는 경영진의 의견이나 데이터 분석을 실제로 수행하는 팀원의 의견에 따라 구글 빅쿼리(Google BigQuery)나 스노우플레이크(Snowflake)를 사용해야 할 수도 있다.
테라폼은 위에서 언급한 모든 서비스에 대한 프로바이더를 제공하며, 현 시점에서 스노우플레이크를 제외한 모든 프로바이더가 서드파티가 아닌 공식 테라폼 프로바이더로 제공된다. 위에 언급한 프로바이더뿐만 아니라 DNS, HTTP, TLS 등 전통적인 네트워크 기술에 대한 프로바이더와 구글 워크스페이스(Google Workspace), 마이크로소프트 액티브 디렉터리(Active Directory, AD), VMWare vSphere 등 굉장히 다양한 종류의 프로바이더를 공식으로 제공하며, 더 넓은 범위의 서드파티 프로바이더도 제공하고 있다.
테라폼 기본
테라폼 작동 방식
테라폼 프로젝트 구조
우선 테라폼 프로젝트가 어떻게 구성되는지 살펴보자. 가장 먼저, 인프라의 기대 상태를 HCL로 기술한 파일을 구성 파일(configuration file)이라고 한다. 이 파일은 어떤 인프라 리소스가 프로비저닝되어야 하는지, 각 리소스는 어떤 설정값을 가져야 하는지 선언하기 위해 쓰이며, 인프라 구성 의도가 담겨 있는 테라폼의 핵심이다. 확장자는 일반적으로 .tf를 가지며, 하나의 디렉터리 안에 여러 개의 구성 파일을 둘 수 있다. 이 책에서는 설명의 편의상 테라폼 코드라고도 부를 것이다.
구성 파일을 담고 있는 디렉터리에서 테라폼 초기화 명령(terraform init)을 수행하면 주요 테라폼 명령을 수행할 수 있도록 필요한 설정이 진행된다. 이렇게 초기화가 완료된 디렉터리를 루트 모듈이라고 한다. 루트 모듈 안에는 초기화 이후 .terraform 디렉터리가 생성되며, 이 디렉터리에 뒤에 언급할 프로바이더와 모듈이 저장되는 등 테라폼 명령 실행에 중요한 역할을 한다. 사용자는 이 루트 모듈에서 테라폼 명령을 실행할 수 있다. 그러므로 루트 모듈을 실행 환경(execution environment)이라고도 부를 수 있다. 이 책에서는 특별한 언급이 없다면 두 용어를 같은 뜻으로 사용할 것이다. 실행 환경에 대한 자세한 사항은 3부에서 자세히 설명한다.
충분한 AWS 권한이 있는 상태로 루트 모듈에서 상태 동기화가 수반되는 플래닝, 반영 등의 명령을 실행하면, 테라폼이 코드에 정의한 리소스에 따른 실제 인프라 리소스를 읽어들여 JSON 형식의 테라폼 상태로 저장한다. 이렇게 저장된 JSON 형식의 파일을 상태 파일(state file)이라고 하며, 확장자로 .tfstate를 가진다.
상태 파일은 한 실행 환경 안에 하나만 가질 수 있으며, 별도의 설정을 하지 않으면 terraform. tfstate의 이름으로 루트 모듈 최상위에 생성된다. 하지만 이러한 방식은 실무상 권장하지 않으며, S3 등의 상태 백엔드를 설정하기를 권장한다. 상태 백엔드를 설정하면 상태 파일이 로컬 환경이 아닌 S3 등의 리모트 스토리지에 저장되며 .terraform 디렉터리 안에 상태 백엔드에 대한 정보가 저장된다. 상태 백엔드에 대한 자세한 내용은 다음 절에서 설명한다.
테라폼 상태의 역할
앞서 테라폼을 통한 인프라 관리에서 관리해야 할 세 가지 상태, 즉 ‘기대 상태’, ‘테라폼 상태’, ‘실제 인프라 상태’가 테라폼의 프로젝트 구조에 어떻게 표현되는지 살펴보았다. 요약하자면 테라폼 코드를 통해 기대 상태를 선언하고, 상태 동기화가 일어나는 테라폼 명령을 통해 테라폼 상태를 실제 인프라 리소스로부터 읽어들인다.
테라폼이 인프라 관리 도구로서 의미를 가지려면 앞서 읽어온 실제 리소스의 상태와 기대 상태를 비교하여 기대 상태를 인프라에 반영할 수 있어야 한다. 일단 상태 비교를 통해 인프라에 어떤 조작을 가해야 할지 결정이 나면, 실제로 조작을 가하기 위해서는 각 클라우드 제공자, 즉 프로바이더에서 제공하는 API를 올바른 순서대로 호출하면 된다. 그렇다면 실제 상태와 기대 상태의 차이로부터 어떤 인프라 조작을 해야 하는지 어떻게 올바르게 유도할 수 있을까? 또한 인프라 반영 과정에서 어떻게 멱등성을 보장할 수 있을까? 바로 테라폼 상태가 이 목표를 달성하는 데 중요한 역할을 하는 구성 요소다.
- 기대 상태와 실제 인프라 리소스의 매핑
테라폼 상태의 역할은 여러 가지가 있지만, 주된 목적은 테라폼 코드에 정의한 리소스와 실제 인프라 리소스의 매핑에 있다. 즉 테라폼으로 EC2 인스턴스 100개를 프로비저닝한다면, 각각의 EC2 인스턴스를 선언한 테라폼 코드의 리소스와 실제 EC2 인스턴스의 ID를 테라폼 상태를 통해 매핑하는 것이다. 리소스 매핑을 통해 테라폼 코드에 정의된 리소스 하나가 정확히 한 개의 실제 인프라 리소스를 바라보게 되며, 비슷한 종류의 리소스가 프로비저닝된 상황에서도 모호함 없이 수정 및 삭제 연산을 수행할 수 있다.
- 테라폼을 통한 관리 대상 명시적 지정
테라폼 상태에 리소스 매핑이라는 주된 기능 외에도, 실제 상태와 기대 상태의 차이로부터 필요한 조작의 목록을 올바르게 얻어내기 위해서 반드시 필요한 기능이 있다. 바로 어떤 인프라 리소스가 테라폼을 통한 관리 대상인지 지정하는 것이다. 테라폼은 상태 동기화를 할 때, 우선 테라폼 코드에 존재하는 리소스마다 하나하나 테라폼 상태에 저장하고, 이 테라폼 상태를 최소 단위로 하여 실제 인프라의 상태를 읽어들인다.
- 리소스 조작 순서와 의존성 지정
테라폼 상태의 리소스 매핑과 관리 대상 지정을 통해 어떤 리소스에 어떤 작업을 수행해야 하는지 결정할 수 있다. 하지만 인프라 반영 작동을 정확하게 수행하기 위해서는 어떤 순서로 리소스를 조작해야 하는지도 알 수 있어야 한다. 예를 들어 AWS VPC가 존재하고 VPC 아래에 서브넷이 존재한다면 VPC를 먼저 생성하고 서브넷을 생성해야 한다. 반대로 두 리소스를 삭제하고 싶다면 서브넷을 먼저 삭제하고 VPC를 삭제해야 한다. 그러므로 인프라 반영 단계에서 리소스를 올바른 순서로 조작하기 위해 상태 동기화 과정에서 다음에 열거한 종류의 의존성을 테라폼 상태에 저장한다.
1) 테라폼 코드에서 명시적으로 지정한 의존성으로 리소스, 모듈, 프로바이더 모두 해당
2) 테라폼 프로바이더에서 참조(reference)를 통해 암시적으로 추론한 의존성
테라폼 기본 문법
반복문
테라폼에서 리소스를 여러 번 생성하기 위한 반복문은 count 또는 for_each라는 메타인수를 통해 만들 수 있다. HCL에서 메타인수(meta-argument)는 모든 타입의 리소스에서 사용할 수 있는 특수한 인수를 말한다. 두 메타인수는 서로 다른 사용 사례와 장점을 가지고 있지만, 이 책에서는 for_each 사용을 권장할 것이다. 각 메타인수의 사용법과 for_each를 더 권장하는 이유에 대해서 같이 살펴보도록 하자.
- count 사용법
count는 정수를 매개변수로 받아, 해당 정수의 값만큼 리소스를 반복 생성한다. 이 방법은 주로 동일한 설정을 가진 리소스를 여러 개 생성할 때 사용한다.
- for_each 사용법
for_each는 맵(map)이나 집합(set) 데이터 유형을 매개변수로 받아, 각 데이터를 순회하여 리소스를 생성한다. for_each를 사용하는 주된 이유는 리소스 관리의 유연성과 명확성 때문이다. 다음과 같은 예시를 한 번 생각해보자. 우리는 윈도우 서버 한 대, 리눅스 서버 한 대를 생성해야 한다. 두 서버는 사용해야 하는 AMI도, CPU와 메모리 요구사항, 즉 사용해야 하는 인스턴스 타입도 다르다. 이 두 서버가 가져야 하는 설정값은 정수인 인덱스값으로 구분할 수 없다. 따라서 각 서버에 필요한 옵션값들은 결국 각각 명세해줄 필요가 있다. for_each에 맵 타입의 값을 입력으로 넣어 두 대의 서버를 각각 원하는 스펙으로 생성하는 코드를 살펴보자.
- for_each 사용을 권장하는 이유
반복문에서 for_each를 권장하는 가장 큰 이유는 리소스 주소의 가독성이 개선되고, 의미가 훨씬 명확해지기 때문이다. 앞에서도 설명했지만, count를 사용하는 경우에는 aws_instance.this[0]으로 표시되고, for_each를 사용하는 경우에는 aws_instance.this[“windows”]로 표시된다. 전자의 경우는 리소스 블록 이름만으로 해당 리소스의 목적을 알기 쉽지 않다. 또한 서로 다른 리소스끼리 참조해야 하는 경우 더욱 불편해진다.
for_each를 권장하는 두 번째 이유는 리스트 타입의 값을 다룰 때 치명적인 실수가 발생할 가능성이 있기 때문이다. 앞에서 사용 예시를 제시한 것처럼, 실제 업무 환경에서 count를 사용할 때 단순히 정숫값만 넘기는 일은 많지 않다. 보통 리스트 변수에서 원하는 값들을 담고, 해당 리스트의 길이를 계산해 count를 사용하게 된다. 이때 간과하기 쉬운 지점이, 리스트에는 순서가 있다는 점이다.
조건문
테라폼에서 조건부 로직을 처리하기 위해서 기본적으로 삼항 연산을 사용한다. 그리고 이 삼항 연산과 count 메타인수를 조합해 이 리소스를 생성할지 여부 등 리소스 단위의 조건문도 사용할 수 있다. 여기에 더해 다음 절에서 다룰 예정인 for 표현식에서는 아예 if 구문도 사용 가능하다. 각 방법을 살펴보도록 하자.
- 삼항 연산자
삼항 연산자(ternary operation)는 많은 프로그래밍 언어에서 유용하게 사용되며, 직접적인 형태로 삼항 연산이 지원되지 않더라도 유사한 작동을 하도록 구현할 수 있다. 구조는 다음과 같다.
조건 ? 참일 때의 값 : 거짓일 때의 값
이는 특정 조건에 따른 변수나 리소스의 설정값을 결정할 때 유용하다.