Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Terraform 0.13으로 테라폼 코드 업그레이드하기

작년에 Terraform을 0.12로 업그레이드하는 방법을 올렸는데 HCL이 많이 바뀌어서 이전보다 훨씬 쓰기가 좋아졌다. 지난 8월 0.13이 릴리스 되었는데 마찬가지로 0.13으로 올려야지 하고 있다가 미루고 있었더니 0.14 GA가 나와버렸다.

더는 미룰 수가 없어서 버전을 올리면서 이 글을 쓴다. 한 번에 0.14로 올라가기보다는 안전하게 한 버전씩 올리면서 달라진 부분을 살펴보려고 한다. Terraform에서도 마이그레이션 명령어를 제공하지만 바로 전 버전에서의 마이그레이션만 지원하므로 0.11 이하 버전을 쓰고 있다면 0.12로 먼저 올린 뒤 0.13으로 올려야 한다.

0.13 릴리스 공지를 보면 주요 변경사항을 알 수 있고 자세한 내용은 CHANGELOG를 보면 된다. 그리고 Upgrading to Terraform v0.13 문서가 잘 되어 있어서 이 문서를 따라 하면 업그레이드할 수 있다.

Terraform 0.13 업그레이드

Terraform CLI를 0.13 버전으로 다운로드받는다. 지금은 0.14가 나왔기 때문에 과거 버전 페이지에서 다운로드받아야 하고 0.13의 최신 버전은 0.13.5다.

$ terraform -v
Terraform v0.13.5

Your version of Terraform is out of date! The latest version
is 0.14.0. You can update by downloading from https://www.terraform.io/downloads.html

코드를 0.13에 맞게 업그레이드하기 전에 로컬에서 수정 중이던 부분이나 클라우드와 동기화가 제대로 안 된 리소스가 있으면 업그레이드할 때 헷갈리므로 업그레이드 전에 terraform plan으로 확인해 보는 게 좋다.

테라폼 상태 파일 보관용으로 S3 백엔드를 쓰고 있어서인지 몰라도 사용하던 코드인데 plan을 하면 오류가 났다.

$ terraform plan
Backend reinitialization required. Please run "terraform init".
Reason: Backend configuration changed for "s3"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

Changes to backend configurations require reinitialization. This allows
Terraform to setup the new configuration, copy existing state, etc. This is
only done during "terraform init". Please run that command now then try again.

If the change reason above is incorrect, please verify your configuration
hasn't changed and try again. At this point, no changes to your existing
configuration or state have been made.


Error: Initialization required. Please see the error message above.

그래서 다시 terraform init을 실행했다. 내가 사용하는 여러 폴더에서 똑같은 걸 보니 내 설정의 영향이거나 테라폼을 0.13으로 올려서 그런 것 같다. 그래서 init을 시도하면 또 에러가 난다.

$ terraform init
Initializing modules...

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.


Error: Failed to decode current backend config

The backend configuration created by the most recent run of "terraform init"
could not be decoded: unsupported attribute "lock_table". The configuration
may have been initialized by an earlier version that used an incompatible
configuration structure. Run "terraform init -reconfigure" to force
re-initialization of the backend.

이건 리소스에 따라 약간 달랐는데 lock_table은 예전 S3 백엔드에서 쓰이던 이름이고 지금은 dynamodb_table로 바뀌었는데도 메시지를 보면 지원하지 않는 lock_table 때문에 오류가 났다고 한다. 추측이지만 예전에 lock_tabledynamodb_table로 바꾸면서 로컬에 남은 파일에 관련 부분이 있는데 0.13은 lock_table을 이해하지 못해서 발생한 게 아닌가 싶다.

어쨌든 안내대로 terraform init -reconfigure로 재설정을 하면 문제없이 초기화가 된다. 아니면 .terraform 디렉터리를 지우고 다시 init 해도 된다.

$ terraform init -reconfigure
Initializing modules...

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform
- Finding hashicorp/aws versions matching "~> 2.7"...
- Finding latest version of -/aws...
- Installing hashicorp/aws v2.70.0...
- Installed hashicorp/aws v2.70.0 (signed by HashiCorp)
- Installing -/aws v3.19.0...
- Installed -/aws v3.19.0 (signed by HashiCorp)

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, we recommend adding version constraints in a required_providers block
in your configuration, with the constraint strings suggested below.

* -/aws: version = "~> 3.19.0"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

지정한 버전에 맞게 hashicorp/aws v2.70.0을 설치했다가 -/aws v3.19.0으로 업데이트한 걸 볼 수 있다. 표시되는 이름이 약간 다른데 <NAMESPACE>/<TYPE> 형식이므로 hashicorp/aws로 표시되고 이후에는 네임스페이스를 생략해서 보여준 것 같다.

이제 plan을 해서 변경사항이 없으면 테라폼 코드를 terraform 0.13upgrade 명령어로 마이그레이션 한다. 현재 폴더에서 실행하려면 그냥 terraform 0.13upgrade로 실행해도 되고 다른 폴더에 실행하려면 terraform 0.13upgrade <TARGET_DIR> 처럼 지정해도 된다. 설정 파일을 업데이트할 거라고 경고 메시지가 나오는데 yes를 입력하고 계속 진행한다.

$ terraform 0.13upgrade

This command will update the configuration files in the given directory to use
the new provider source features from Terraform v0.13. It will also highlight
any providers for which the source cannot be detected, and advise how to
proceed.

We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.

Would you like to upgrade the module in the current directory?
  Only 'yes' will be accepted to confirm.

  Enter a value: yes

-----------------------------------------------------------------------------

Upgrade complete!

Use your version control system to review the proposed changes, make any
necessary adjustments, and then commit.

회사 테라폼 코드가 아니라 개인 AWS의 테라폼 코드라서 복잡한 건 없어서 쉽게 업그레이드가 되었다.

required_providers

0.13의 주요 변경사항 중 하나는 Automatic Installation of Third-Party Providers with Terraform 0.13이다. 서드파티 프로바이더도 초기화할 때 자동으로 설치해 준다는 얘기이다. 이전에도 사실 레지스트리에 있는 모듈 등을 자동으로 설치해 주었기 때문에 정확한 차이는 이해하지 못했지만, 공식이 아닌 서드파티에서는 동작이 달랐던 것 같다.

0.13에서는 Terrafrom Registry에서 프로바이더와 모듈을 모두 제공한다.

이전에는 AWS 프로바이더를 사용하려면 다음과 같이 선언했다.

provider "aws" {
  version = "~> 2.26.0"
  region  = "ap-northeast-1"
}

여기서 서드파티가 들어오니까 네임스페이스가 필요하게 된다. 예를 들어 HashiCorp가 관리하는 aws 프로바이더가 있을 수 있고 AWS가 관리하는 aws 프로바이더가 있을 수 있다. 이렇게 같은 이름의 프로바이더가 있으면 위와 같은 방식으로는 어떤 프로바이더인지 구별을 할 수 없다.

그래서 0.13에서는 terraform 블록에 required_providers가 추가되었다. terraform 0.13upgrade을 실행하면 현재 설정을 맞게 다음과 같은 versions.tf 파일을 생성한다.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
    }
  }
  required_version = ">= 0.13"
}

이미 aws 프로바이더를 사용하고 있었으므로 required_providers에서 aws라는 프로바이더의 소스가 hashicorp/aws임을 구체적으로 명시했다. 만약 다른 aws 프로바이더라면 여기서 source = "aws/aws" 같은 식이 될 것이다. 여기서aws라는 이름은 임의로 정할 수 있는 이름으로 앞에서 이미 있던provider "aws" {}부분의aws`가 이 이름이 된다.

versions.tf는 0.12로 업그레이드할 때도 생성되었는데 그때는 required_version만 있어서 사용하지 않았는데 앞으로 계속 이 파일을 사용할 것 같아서 다른 곳에 정의되어 있던 terraform {} 블록을 이곳으로 옮겨왔다.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.19.0"
    }
  }
  required_version = ">= 0.13"

  backend "s3" {
    // 중량
  }
}

그리고 provider 블록에서는 버전을 제거하고 버전도 required_providers로 가져왔다. 이유는 모르겠지만 이건 자동으로 안 만들어 준다.

required_providers에 대해서 좀 더 설명하면 여기서 source = "hashicorp/aws"문서에 따르면 형식은 [<HOSTNAME>/]<NAMESPACE>/<TYPE>가 된다. 여기서는 HOSTNAME을 생략했으므로 Terraform 공식 레지스트리가 된다. 이는 다른 레지스트리도 섞어서 사용할 수 있다는 의미가 되므로 프로바이더 사용이 훨씬 유연해졌다.

전체 형식을 다 맞춰서 적으면 다음과 같이 된다.

random = {
    source = "registry.terraform.io/hashicorp/random"
}

레지스트리가 registry.terraform.io이면 생략할 수 있다.

random = {
    source = "hashicorp/random"   
}

대소문자를 가리지 않음으로 다음도 똑같은 설정이다.

random = {
    source = "HashiCorp/random"
}

네임스페이스가 hashicorp라면 생략할 수 있다.

random = {
    source =  "random"
}

source를 아예 지정 안 하면 블록에 지정한 이름으로 프로바이더의 이름을 찾기 때문에 다음도 똑같은 프로바이더를 바라보게 된다.

random = {}

앞에서 terraform 0.13upgrade를 실행했을 때는 HashiCorp가 관리하는 프로바이더라서 문제없이 적용되었지만 다른 서드파티 프로바이더를 사용하고 있다면 init 할 때부터 문제가 된다.

$ terraform init -reconfigure

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/digitalocean versions matching "~> 1.2"...
- Finding latest version of -/digitalocean...
- Installing -/digitalocean v2.2.0...
- Installed -/digitalocean v2.2.0 (signed by HashiCorp)

Warning: Additional provider information from registry

The remote registry returned warnings for
registry.terraform.io/-/digitalocean:
- For users on Terraform 0.13 or greater, this provider has moved to
digitalocean/digitalocean. Please update your source in required_providers.


Error: Failed to install providers

Could not find required providers, but found possible alternatives:

  hashicorp/digitalocean -> digitalocean/digitalocean

If these suggestions look correct, upgrade your configuration with the
following command:
    terraform 0.13upgrade .

여기서는 다음과 같이 digitalocean 프로바이더를 사용하고 있었기 때문에 hashicorp/digitalocean를 시도했다가 없으니까 가능한 다른 대안으로 digitalocean/digitalocean이 있음을 알려주고 위 대안이 맞으면 terraform 0.13upgrade .를 실행하라고 나왔다.

provider "digitalocean" {
  version = "~> 1.2"
  token   = var.digitalocean_token
}

그래서 terraform 0.13upgrade .를 실행하면 다음과 같은 versions.txt를 만들어 준다.

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
    }
  }
  required_version = ">= 0.13"
}


모듈의 메타 인자 추가

이전까지는 for_eachcount를 리소스 블록에서 여러 리소스를 만드는 데만 사용할 수 있었지만 0.13부터는 모듈에 바로 for_eachcount를 쓸 수 있게 되었다. 복잡한 모듈은 쓰고 있지 않아서 문서의 내용을 참고하면 다음과 같이 여러 Kubernetes 클러스터를 한 번에 만들 수 있다.

module "kubernetes_cluster" {
  source   = "terraform-google-modules/kubernetes-engine/google"
  for_each = var.regions

  project_id        = var.project_id
  name              = each.key
  region            = each.value.region
  network           = each.value.network
  subnetwork        = each.value.subnetwork
  ip_range_pods     = each.value.ip_range_pods
  ip_range_services = each.value.ip_range_services
}

그리고 이전에는 Terraform이 의존 관계를 추적할 때 모듈의 input/output으로만 의존성 그래프를 그렸는데 이젠 module에서 depends_on을 지정할 수 있어서 다른 리소스에 의존성이 있는 경우 명시할 수 있다.

module "uses-role" {
  # ...

  depends_on = [aws_iam_policy_attachment.example]
}


커스텀 변수 유효성 검사

0.12.20부터 시험한 기능이라는데 모르고 있었다. 0.13부터 정식으로 도입되었는데 자세한 내용은 Custom Variable Validation in Terraform 0.13에 나와 있다.

variable "ami_id" {
  type = string

  validation {
    condition = (
      length(var.ami_id) > 4 &&
      substr(var.ami_id, 0, 4) == "ami-"
    )
    error_message = "The ami_id value must start with \"ami-\"."
  }
}

이전에는 variable에 타입만 지정할 수 있었는데 validation 블록을 이용해서 변수에 들어오는 값의 유효성 검사를 하고 원하는 오류 메시지를 뿌려줄 수 있게 되었다. 변수 이름만 보고 어떤 값을 넣어야 하는지 헷갈릴 수가 있고 변수에 따라 들어가면 안 되는 문자가 길이가 있는 경우가 있는데 이를 검사할 수 있는 게 편해 보인다.

수많은 폴더를 돌아다니면서 하나씩 검사하면서 업그레이드하는 것도 보통 일이 아니다. 조만간 0.14로 올려야겠다.

2020/12/03 19:36 2020/12/03 19:36