Cloud Infra의 IaC 도구로 잘 알려진 Terraform을 On-Premise 환경에서 사용하려면 On-Prem Cloud를 위한 Terraform Provider 가 제공되어야 한다.
이 글에서는 IaC 도구인 Terraform이 무엇인지 간단히 소개하고, Hashicorp에서 새로이 제공하는 Plugin Framework을 이용해 Terraform Provider를 만들 때 기존과 달라진 것들을 기술하려 한다.

Brief history of Terraform

2021년 7월 8일은 Terraform의 첫 메이저 버전 1.0이 릴리즈된 날이다. 같은 날 Hashicorp 홈페이지에 공개된 글 “The Story of HashiCorp Terraform with Mitchell Hashimoto”와 유튜브 영상 “The Making of HashiCorp Terraform with Mitchell Hashimoto”에는 Hashimoto가 Terraform을 만들기까지의 여정이 담겨있다.

2011년, AWS가 CloudFormation을 출시했을 때 미첼은 AWS 리소스를 코드로 모델링하고 프로비저닝한다는 아이디어에 큰 감명을 받았다. 하지만 그는 AWS에 국한되지 않고 여러 클라우드 플랫폼에서 사용할 수 있는 범용적인 인프라스트럭처 관리 도구가 필요하다고 생각했다.
이 아이디어를 바탕으로 미첼은 Tumblr에 블로그 포스트를 작성했다.(원본은 찾을 수 없으며, GitHub에 아카이브가 남겨져 있다. 링크: CloudFormation: The Big Picture) 그는 이 포스트에서 오픈소스 기반의 Cloud-Agnostic Infrastructure as Code 솔루션의 필요성을 언급하며, 누군가가 이 문제를 해결해주기를 바랐다. 사실상 Terraform의 아이디어를 공개적으로 제안한 셈.

  • CloudFormation은 Declarative Language를 기반으로 한 인프라 프로비저닝 도구로, 당시 유행하던 Chef나 Puppet과는 Orchestration의 첫 단계를 시작하고 구성한다는 점에서 다르다.
  • 하지만 CloudFormation은 인프라를 점진적으로 개선하고 재구성할 수 있는 방법을 제공하지 않는다.
  • 또한 오픈소스이자 특정 벤더 종속성이 없는 Cloud-Agnostic Solution이 필요하다.
출처: 침하하

그러나 몇 년이 지나도 아무도 이 아이디어를 실현하지 않았다. 그 사이 미첼이 예측했던 인프라 관리의 문제점들이 현실화되기 시작했고, 결국 그는 직접 이 문제를 해결하기로 결심했다.(누군가는 해야 하잖아?) 2014년 7월, 미첼과 그의 팀은 Terraform 0.1 버전을 출시했는데, 이는 그가 2011년 블로그에서 제안했던 바로 그 아이디어를 구현한 것이었다. 초기 버전의 Terraform은 AWS와 DigitalOcean만을 지원했지만, 점차 다른 인프라에도 확장될 수 있도록 개선되었다.

Terraform의 초기 18개월 동안은 다운로드 수가 거의 정체되어 있었으며, 심지어 프로젝트를 중단할 것인지에 대한 논의도 있었다고 한다. 하지만 미첼과 그의 팀은 Terraform의 잠재력을 믿었고, 특히 에코시스템의 중요성을 인식했기 때문에,Terraform Provider를 쉽게 작성하고 사용할 수 있도록 하는 데 집중했다.
생태계를 중요하게 여겼던 미첼의 인사이트는 틀리지 않았다. 2016년 말까지 Terraform은 750명 이상의 기여자와 수십 개의 Provider를 확보했고, 다운로드 수가 매월 두 배씩 증가하기 시작했다.(현재는 1000여 개 이상의 Terraform Provier가 Teraform Registry에 등재되어 있다.)

2017년은 미첼이 “Terraform의 해”라고 부를 정도로 중요한 해였다. 마이크로소프트와의 첫 주요 클라우드 파트너십을 발표했는데, 이는 대형 클라우드 제공업체가 공개적으로 Terraform을 인정하고 지원한 첫 사례였다.
Terraform은 현재 1억 회 이상의 다운로드를 기록한 인기 있는 도구로 성장했다. 미첼의 단순한 아이디어에서 시작된 Terraform과 IaC가 이제는 인프라 관리의 표준으로 자리 잡은 것.

Terraform 주요 특징

위 일화에서 언급되었듯, Terraform은 IaC의 원칙을 기반으로 클라우드 인프라를 관리하고 배포하는데 사용된다. 주요 특징은 다음과 같다.

클라우드 중립성

Terraform은 멀티 클라우드 환경을 지원한다. AWS, Azure, Google Cloud와 같은 여러 클라우드 서비스 제공자의 인프라를 한 곳에서 코드로 관리할 수 있으며, 기업은 특정 클라우드 벤더에 종속되지 않고, 유연한 인프라 관리를 할 수 있게 된다.

Declarative Infrastructure Provisioning

Terraform은 내가 원하는 최종 상태를 정의하면 인프라를 그 상태에 달성하도록 변경한다. 작업 절차의 서순이 바뀌었기 때문에 원치 않는 결과를 초래하는 일은 없다.
Declarative Provisioning을 위해 Terraform은 HCL이라는 언어로 인프라 리소스를 표현한다.(링크: Terraform Language Documentation)

Plan & Apply

Terraform의 핵심적인 기능 중 하나는 plan과 apply다. terraform plan 명령어를 통해 현재 설정과 미래의 변경 사항을 비교하고, 인프라 변경이 시스템에 어떤 영향을 미칠지 사전에 예측할 수 있다. 이를 통해 변경 사항을 안전하게 적용할 수 있다.

State

Terraform은 인프라의 현재 상태를 파일로 관리하여, 코드에서 정의된 리소스와 실제 인프라 리소스를 동기화한다. 이 상태 파일은 클라우드 리소스 변경을 추적하고 관리하는 데 중요한 역할을 하며, 팀 간 협업 시에도 일관된 상태를 유지할 수 있게 해준다.

모듈화 및 재사용성

Terraform은 코드의 모듈화를 지원하여 복잡한 인프라를 여러 모듈로 나눠 관리할 수 있다. 모듈화를 통해 인프라의 각 부분을 독립적으로 정의하고 재사용할 수 있어, 유지보수성과 확장성이 크게 향상된다.
Terraform Registry는 써드파티에서 만든 수많은 모듈을 검색하고 적용할 수 있도록 하여 인프라 리소스 관리 코드의 재사용이 가능하도록 돕는다.

On-Premise Cloud를 위한 Terraform Provider 만들기

현재 Terraform은 IaC의 de-facto 표준 격이어서 대부분의 Public Cloud에서 사용이 가능하다. 하지만 당신의 회사에 On-Premise Cloud가 구축되어 있다면 이 Infrastructure를 Terraform으로 제어하기 위해 Terraform Provider를 직접 만들어야 한다. 걱정하지 마라. Hashicorp는 당신이 Provider를 손쉽게 작성할 수 있도록 친절하게 tutorial을 준비해 두었다.(링크: Call APIs with Custom Framework Providers)
이 예제는 Terraform Provider 개발자가 구현해야 할 주요요소들을 완결성 있게 배울 수 있도록 돕는다. 예제에서 작성한 Provider를 동작시켜볼 수 있도록 간단한 테스트 API 서버를 Docker로 제공하기 때문에 처음 Terraform을 사용하는 사람도 동작을 이해하기 쉬울 것이다. 총 13개의 tutorial이 존재하며, 각 tutorial 마다 학습 예상시간이 표기되어 있는데, 모든 tutorial을 마치기까지 대략 2~3시간 정도 걸린다.

Pre-requisites

Terraform Provider를 개발하려면 다음을 준비하여야 한다.

  • Terraform CLI(링크: Install Terraform)
  • go 언어 개발환경
  • Infrastructure를 제어할 수 있는 Web API 및 go 언어용 API client
  • (optional) Private Terraform Registry
    • 여건이 허락된다면 Public Registry 써도 무방

Private Terraform Registry

필자가 다니는 회사의 On-Premise Cloud는 격리된 사설망에 존재한다. 회사 방침상 Public Registry 사용이 어려우므로 Private Registry를 직접 구성하였다. 만일 여러분의 엔터프라이즈 환경이 필자와 유사하게 폐쇄적이라면 직접 Private Registry를 구성하여야 한다.
Provider Registry Protocol을 준수하는 Registry Server를 손수 구현(링크: Provider Registry Protocol)할 수도 있지만, 특별한 이유가 없다면 누군가 구현해놓은 Opensource Private Registry를 사용하자.

다음은 필자가 검토하였던 Private Registry 목록이다.

Citizen은 유튜브 채널 44BITS양질의 블로그로 유명하신 개발자 Outsider 님이 만드셨다. Terustry는 이름에서 알 수 있듯, rust로 구현한 프로젝트이고, Terrakube는 k8s 환경에서 동작하며 Registry 뿐 아니라 Custom Flow 주입 / State 가시화 / Schedule 관리 / Team 관리 등, Terraform Enterprise 의 대안으로 사용할 만한 오픈소스 서비스이다.

이 중에서 필자는 Terustry로 Registry를 구성하였다. 아쉽게도 Citizen은 현 시점 Runtime error 가 발생하여 정상적인 실행이 되지 않아 사용해보지 못했다. Terrakube는 기능이 강력하기 때문에 적절한 시점이 되면 제대로 리뷰를 진행할 예정이다. 아직 필자의 진행단계에서는 과하다는 생각이 든다.
Terustry는 GitHub 에 Release된 Artifact를 Provider로 제공하기 때문에 별도의 스토리지가 필요하지 않다. GitHub Release만 잘 관리하면 되기에 운영 cost가 낮고 구성이 쉬운 장점이 선택의 이유가 되었다.

Terraform Plugin Framework(vs SDKv2)

필자가 Terraform Provider를 개발을 위해 참고하였던 프로젝트들은 다음과 같다.

  • Terraform Provider AWS
  • Terraform Provider Azure
  • Terraform Provider NCloud

이 프로젝트들은 오래 전부터 Provider를 제공하였기에 Terraform Plugin SDKv2 로 개발이 되었는데, Hashicorp는 SDKv2 대신 새로이 제공하는 Plugin Framework로 개발할 것을 권장하고 있다. 심지어 가능하다면 SDKv2에서 Framework로 마이그레이션 할 것을 추천하고 있는 실정이다. 과연 Framework는 SDKv2에 비해 어떤 점이 개선되었을까?

Terraform Plugin Protocol Version 6 지원

Terraform Plugin Protocol이란 Terraform CLI와 Plugin 사이에 정의된, 버전관리되는 인터페이스이다.(링크: Terraform Plugin Protocol)

v6에서는 NestedAttributes를 지원한다. 기존의 Block Syntax에 비해 간결한 표현이 가능한 Argument Syntax를 사용할 수 있다. 예를 들면 다음과 같은 block syntax를 argument syntax로 표현할 수 있다.

resource "example_network" "test" {
  name = "example-network"

  network_interface {
    name = "eth0"
    type = "internal"

    ip_configuration {
      ip_address = "192.168.1.10"
      subnet     = "subnet-1"

      dns_settings {
        primary   = "8.8.8.8"
        secondary = "8.8.4.4"
      }
    }
  }

// network_interface block의 반복
  network_interface {
    name = "eth1"
    type = "external"

    ip_configuration {
      ip_address = "10.0.0.1"
      subnet     = "subnet-2"

      dns_settings {
        primary   = "1.1.1.1"
        secondary = "1.0.0.1"
      }
    }
  }
}
resource "example_network" "test" {
  name = "example-network"
  
  // List Nested Attribute로 표현
  network_interface = [
    {
      name = "eth0"
      type = "internal"
      ip_configuration = {
        ip_address = "192.168.1.10"
        subnet     = "subnet-1"
        dns_settings = {
          primary   = "8.8.8.8"
          secondary = "8.8.4.4"
        }
      }
    },
    {
      name = "eth1"
      type = "external"
      ip_configuration = {
        ip_address = "10.0.0.1"
        subnet     = "subnet-2"
        dns_settings = {
          primary   = "1.1.1.1"
          secondary = "1.0.0.1"
        }
      }
    }
  ]
}

기본 구조의 차이: 모듈화와 재사용성

Terraform SDK에서는 리소스를 정의할 때 상대적으로 많은 boilerplate 코드가 필요하며, 개별적인 기능을 구현하는 데에도 반복되는 부분이 많다. 반면, Framework Plugin은 코드의 모듈화를 더 잘 지원하여 반복적인 작업을 줄이고 재사용성을 높이도록 개선하였다.

예를 들어보자. SDK에서의 리소스 생성 방식은 보통 다음과 같이 많은 양의 SchemaCRUD 관련 함수를 작성해야 했다.

func resourceVirtualMachine() *schema.Resource {
	return &schema.Resource{
		Schema: map[string]*schema.Schema{
			"name": {
				Type:     schema.TypeString,
				Required: true,
			},
			"cpu": {
				Type:     schema.TypeInt,
				Optional: true,
			},
		},
		Create: resourceVirtualMachineCreate,
		Read:   resourceVirtualMachineRead,
		Update: resourceVirtualMachineUpdate,
		Delete: resourceVirtualMachineDelete,
	}
}

func resourceVirtualMachineCreate(d *schema.ResourceData, meta interface{}) error {
        // 명시적인 name type assertion
	name := d.Get("name").(string)
	// 가상 머신 생성 로직
	return nil
}

Create 함수 구현부를 보면, ResourceData d의 구성요소 name의 값을 string type으로 직접 변환한다. 나머지 Read / Update / Delete 함수에서도 name의 값을 string type으로 직접 변환하여야 하는 boilerplate code 가 반복적으로 발생한다.
또한 type assertion의 validity를 사용하는 쪽에서 매번 체크하여야 한다.

반면, Framework는 다음과 같은 구조적 개선이 있다.

  • Attributes 의 추가: Attribute란 encoder/decoder가 사전 정의된 type의 추상화이다. Schema의 구성요소 Attribute만 잘 정의하면 Resource / Data source 의 데이터 변환 및 validation을 신경쓰지 않아도 된다.
  • Metadata / Schema / CRUD 책임 분리
  • context.Context를 통한 일관된 상태관리 및 에러처리
func NewVirtualMachineResource() resource.Resource {
	return &VirtualMachineResource{}
}

type VirtualMachineResource struct {
	resource.Resource
}

func (r *VirtualMachineResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = "example_virtual_machine"
}

func (r *VirtualMachineResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
                        // name StringAttribute는 string assertion을 신경쓰지 않아도 됨
			"name": schema.StringAttribute{
				Required: true,
			},
			"cpu": schema.IntAttribute{
				Optional: true,
			},
		},
	}
}

func (r *VirtualMachineResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data VirtualMachine
	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	// 가상 머신 생성 로직
}

Terraform Provider Tutorial

공식 Tutorial 링크 https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework

이런 저런 이야기를 하다보니 예상보다 글이 길어져서 Terraform Provider 개발은 다음 글에서 다룰까 한다. 위의 공식 Tutorial을 따라하면서 Go 개발환경을 구성하여 나만의 Provider 개발하는 방법을 소개하겠다.

결론

Terraform Provider를 만들 일이 많지는 않겠지만, 혹시라도 Terraform Provider를 만들어야 할 경우 이 글이 도움이 되길 바란다.

댓글 남기기

인기 검색어

01010011에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기