Outsider's Dev Story

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

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

얼마 전에 Terraform 코드를 0.13으로 업그레이드 했는데 0.13 릴리스 이후 4달이 채 지나지 않아서 0.14가 나왔다.

0.14의 전체 변경사항은 CHANGELOG에서 볼 수 있는데 이전 0.12 업그레이드0.13 업그레이드와 달리 코드나 문법의 개선 사항이 없다. 그래서 이전처럼 자동으로 코드를 수정해 주는 terraform 0.14upgrade같은 명령어가 없다.

Terraform 0.14에서 변경된 기능

먼저 0.14에다 달라진 점을 살펴보자. 릴리스 공지도 있지만 Upgrading to Terraform v0.14를 보면 달라진 기능을 자세히 살펴볼 수 있다.

Dependency Lock

0.13 이하까지는 terraform init을 하면 항상 설정 버전의 조건에 맞는 최신 버전의 프로바이더를 설치했다. 설정 버전을 특별히 지정하지 않으면 새 버전의 프로바이더 때문에 동작이 달라질 수도 있다. 의도적으로 변경하지 않았는데 동작이 달라지는 이러한 위험성을 막기 위해 0.14부터는 Dependency Lock 파일이 도입되었다.

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

init을 할때 위와 같은 안내메시지가 나오는데 자동으로 .terraform.lock.hcl가 프로젝트 루트 위치에 생성되고 다음과 같이 생겼다. 이 파일이 존재하면 terraform init을 할 때 이 파일에서 지정한 버전에 따라 설치되게 된다. 협업을 위해 당연히 이 파일도 커밋해야 한다.

# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "3.22.0"
  constraints = "~> 3.22.0"
  hashes = [
    "h1:f/Tz8zv1Zb78ZaiyJkQ0MGIViZwbYrLuQk3kojPM91c=",
    "zh:4a9a66caf1964cdd3b61fb3ebb0da417195a5529cb8e496f266b0778335d11c8",
    "zh:514f2f006ae68db715d86781673faf9483292deab235c7402ff306e0e92ea11a",
    "zh:5277b61109fddb9011728f6650ef01a639a0590aeffe34ed7de7ba10d0c31803",
    "zh:67784dc8c8375ab37103eea1258c3334ee92be6de033c2b37e3a2a65d0005142",
    "zh:76d4c8be2ca4a3294fb51fb58de1fe03361d3bc403820270cc8e71a04c5fa806",
    "zh:8f90b1cfdcf6e8fb1a9d0382ecaa5056a3a84c94e313fbf9e92c89de271cdede",
    "zh:d0ac346519d0df124df89be2d803eb53f373434890f6ee3fb37112802f9eac59",
    "zh:d6256feedada82cbfb3b1dd6dd9ad02048f23120ab50e6146a541cb11a108cc1",
    "zh:db2fe0d2e77c02e9a74e1ed694aa352295a50283f9a1cf896e5be252af14e9f4",
    "zh:eda61e889b579bd90046939a5b40cf5dc9031fb5a819fc3e4667a78bd432bdb2",
  ]
}

lock 파일을 사용하고 싶지 않다면 .gitignore.terraform.lock.hcl를 추가해서 버전 관리 시스템에 이 파일이 추가되지 않도록 하면 된다.

lock 파일의 기능은 프로바이더 레지스트리가 제공하는 정보에 의존하고 있기 때문에 파일 시스템이나 네트워크 미러를 통해서 설치한다면 이런 기능을 다 다양하지 못할 수 있다. 만약 패키지 체크섬을 제공하지 않는 미러를 사용한다면 현재 사용하는 플랫폼의 프로바이더 패키지의 체크섬만을 기록할 것이다. 이렇게 되면 팀 내에서 여러 OS를 사용하고 있다면 체크섬이 달라고 오류가 발생할 수 있다. 만일 여러 플랫폼을 사용하고 있다면 terraform providers lock을 사용해서 원하는 플랫폼을 지정할 수 있다.

참고로 현재 외부 모듈의 의존성은 lock에 포함되지 않는다. HashiCorp에서 나중에는 외부 모듈도 포함 시킬 계정이라고 했지만, 현재 0.14에서는 프로바이더에게만 국한되어 있다.

로컬 프로바이더 캐시 디렉터리

Terraform은 설치한 플러그인을 풀어서 .terraform/plugins 디렉터리에 두는데 이 디렉터리는 Terraform의 내부 사용 목적이었다. 하지만 HashiCorp내에서 의사소통이 잘못되어 "여기에 로컬 프로바이더를 두면 Terraform Cloud에 업로드할 수 있는 파일 시스템 미러"라고 잘못된 문서로 만들게 되었다. 0.14에서는 프로바이더 버전 선택을 dependency lock 파일로 바꾸기 위해 동작이 변경되어 수동으로 추가 플러그인을 로컬 캐시 디렉터리에 두는 것은 동작하지 않는다. 이러한 동작을 시도할 경우 경고 메시지가 나오게 된다. 경고 메시지에서는 원래 의도된 terraform.d 디렉터리를 안내한다고 하는데 이런 동작은 시도해 본 적이 없어서 잘 모르겠다.

간결한 Terraform Plan 출력

terraform plan의 출력은 원래도 간결했지만 컨텍스트가 부족한 관계로 잘못 이해하고 적용하는 바람에 장애를 일으키는 경우가 있다는 피드백을 받았다고 한다. 그래서 0.12부터는 각 리소스의 전체 컨택스트를 보여주고 +, -, ~로 변경사항을 표시해 주기 시작했다. 이는 속성이 많거나 중첩된 내용이 많으면 내용이 너무 많아서 0.14부터는 각 객체에 집중하면서도 컨텍스트에 대한 걱정을 해결하는 것을 목표로 했다고 한다.

0.14에서는 사람이 인지할 수 있는 정보를 담고 있는 다수의 속성은 제외하고 변경되지 않은 속성은 생략한다. 속성과 블록이 생략되었을 때 Terraform은 모호함을 피하고자 무엇이 포함되지 않았는지 항상 보여줄 것이다. 이전의 diff를 보고 싶다면 환경변수 TF_X_CONCISE_DIFF=0을 지정하면 된다.

0.13에서 plan 출력은 다음과 같이 길게 나왔다.

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.side_effect_ecs_cluster.aws_autoscaling_group.main will be updated in-place
  ~ resource "aws_autoscaling_group" "main" {
        arn                       = "arn:aws:autoscaling:ap-northeast-1:12345:autoScalingGroup:c5d96ab6-c498-4618:autoScalingGroupName/example"
        availability_zones        = [
            "ap-northeast-1a",
            "ap-northeast-1c",
        ]
        capacity_rebalance        = false
        default_cooldown          = 300
        desired_capacity          = 1
        enabled_metrics           = []
        force_delete              = false
        health_check_grace_period = 300
        health_check_type         = "EC2"
        id                        = "example"
        launch_configuration      = "example-00df47c901e"
        load_balancers            = []
        max_instance_lifetime     = 0
      ~ max_size                  = 2 -> 3
        metrics_granularity       = "1Minute"
      ~ min_size                  = 1 -> 2
        name                      = "example"
        protect_from_scale_in     = false
        service_linked_role_arn   = "arn:aws:iam::12345:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"
        suspended_processes       = []
        target_group_arns         = []
        termination_policies      = []
        vpc_zone_identifier       = [
            "subnet-12345678",
            "subnet-23456789",
        ]
        wait_for_capacity_timeout = "10m"

        timeouts {}
    }

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

TF_X_CONCISE_DIFF=0 환경 변수를 설정하지 않으면 0.14부터는 훨씬 간결하게 출력된다.

Terraform will perform the following actions:

  # module.side_effect_ecs_cluster.aws_autoscaling_group.main will be updated in-place
  ~ resource "aws_autoscaling_group" "main" {
        id                        = "example"
      ~ max_size                  = 2 -> 3
      ~ min_size                  = 1 -> 2
        name                      = "example"
        # (20 unchanged attributes hidden)


        # (4 unchanged blocks hidden)
    }

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


Plan 출력에서의 민감한 데이터

리소스의 특정 속성을 "sensitive"로 지정하면 plan 출력에서 실제 값 대신 (sensitive value)라고 표시되었다. 0.14부터는 민감한 입력 변수민감한 출력값에서 나온 표현식도 추적해서 값을 감춰주기 때문에 0.13 때보다 더 많은 값이 가려질 것이다.

variableoutput에서 다음과 같이 sensitive 설정을 할 수 있다.

variable "foo" {
  sensitive = true
}

output "bar" {
  value     = var.foo
  # sensitive 입력 변수를 참조할때 반드시 true로 설정해야 한다
  sensitive = true
}

프로바이더가 sensitive로 설정한 속성을 sensitive로 인식하는 실험적 기능도 있는데 다음과 같이 terraform 블록에서 이 기능을 활성화할 수 있다.

terraform {
  experiments = [provider_sensitive_attrs]
}


Terraform 0.14 업그레이드

앞에서 코드를 변경할 부분은 없다고 했지만 그래도 업그레이드는 해야 한다. 테라폼을 썼으면 알다시피 최신 버전으로 apply를 해서 state를 업데이트하고 나면 이 state는 그보다 하위 버전으로는 사용할 수 없다. 그러므로 0.14로 Terraform 코드를 업그레이드하려면 변경사항이 없더라도 0.14로 한 번씩 apply를 해주는 게 좋다.

그리고 0.14에서는 dependency lock 파일이 생겼기 때문에 0.14로 업그레이드 하기 위해서 새로 init을 하면 .terraform.lock.hcl 파일이 생기고 이 파일을 버전 관리 시스템에 커밋해야 한다.(lock을 관리 안 하기로 한 게 아니라면...)

나 같은 경우 .terraform.lock.hcl 외에도 terraform 블록의 required_version 버전도 0.14로 올려주었다.

terraform {
  required_version = ">= 0.14"
}


Invalid legacy provider address 오류

Terraform 0.14로 plan을 시도하니 다음과 같은 오류가 나는 경우가 있었다. 나중엔 무슨 문제인지 알게 되었지만, 처음에는 왜 발생하는지 몰라서 당황했다.

$ terraform plan

Error: Could not load plugin


Plugin reinitialization required. Please run "terraform init".

Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.

Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints, run "terraform providers".

2 problems:

- Failed to instantiate provider "registry.terraform.io/hashicorp/aws" to
obtain schema: unknown provider "registry.terraform.io/hashicorp/aws"
- Failed to instantiate provider "registry.terraform.io/-/aws" to obtain
schema: unknown provider "registry.terraform.io/-/aws"

지난번 0.13으로 올리면서 versions.tf 파일에서 0.13에서 맞게 프로바이더를 지정해 주었는데(이전에는 그냥 aws라고만 지정했다.) 그런데도 위와 같은 오류가 났다. unknown provider "registry.terraform.io/hashicorp/aws" 자체도 정확하게 이해를 못 했고 unknown provider "registry.terraform.io/-/aws"는 이전에 aws라고 지정했던 부분 때문에 나오는 것 같은데 왜 나오는지 잘 몰랐다. 프로바이더 정의는 아래처럼 잘 되어 있었다.

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

.terraform 폴더를 지우고 다시 init을 시도해도 더 알기 어려운 unqualified provider "aws" 오류가 발생했다.

$ terraform init

Initializing the backend...

Error: Invalid legacy provider address

This configuration or its associated state refers to the unqualified provider
"aws".

You must complete the Terraform 0.13 upgrade process before upgrading to later
versions.

해당 내용을 찾아보니 대부분은 0.12 이하의 상태로 남아있을 때 0.14로의 업그레이드를 시도하면 이런 문제가 발생하는 것 같은데 나는 이미 0.13으로 올려놓은 상태였다. 코드는 0.13으로 바꾸었지만 상태 파일에는 아직 0.12 때의 데이터가 남아있어서 이런 문제가 발생할 수 있는 것 같아서 terraform state replace-provider "registry.terraform.io/-/aws" "hashicorp/aws"를 시도했지만 init이 안된 상태라서 사용할 수가 없었다.

다양한 시도를 하다가 알게 됐는데 로컬의 코드는 0.13에 맞게 바뀌었지만 한 번도 apply를 한 적이 없어서 원격의 state 파일에는 과거 데이터가 남아있어서 문제가 된 것이다. 이전 0.13 업그레이드 글에서 hashicorp/aws-/aws v3.19.0가 둘 다 나오는 걸 보면서 이부분이 처리 과정에서 표시된 거로 생각했는데 실제로는 원격 state에는 aws 프로바이더(hashicorp/aws 말고)가 남아있어서 두 프로바이더를 모두 처리하면서 나온 것이었다. 이를 0.13에서 변경사항이 없더라도 apply를 해주었어야 필요 없는 -/aws 프로바이더가 정리되는데 변경사항 없다고 apply를 안 했기 때문에 생긴 문제였다.

Terraform 0.13으로 apply를 하고 난 뒤에 다시 0.14로 init하고 plan 하니 잘 동작했다.

이 문제는 terraform providers로도 확인할 수 있는데 아래는 보면 원격 state에 registry.terraform.io/-/aws가 남아 있는 것을 알 수 있다.(이는 0.13으로 확인한 것이다.)

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/aws] ~> 3.19.0
└── provider[terraform.io/builtin/terraform]

Providers required by state:

    provider[registry.terraform.io/-/aws]

    provider[terraform.io/builtin/terraform]

앞에서 0.13으로 apply 뒤에 .terraform을 지우고 0.14에서 다시 init으로 사용했지만 사실 그렇게 하지 않아도 된다. 앞에 검색했을 때 나온 0.12 상태에서 0.14로 해서 그렇게 된다는 게 사실은 맞는 말이다. apply를 하지 않아서 state 파일은 0.12로 남아있었으니까... 그래서 apply를 하지 않아도 terraform state replace-provider 명령어로 네임스페이스가 없던 프로바이더를 명시적으로 바꿔주어도 된다.

$ terraform state replace-provider "registry.terraform.io/-/aws" "hashicorp/aws"
Terraform will perform the following actions:

  ~ Updating provider:
    - registry.terraform.io/-/aws
    + registry.terraform.io/hashicorp/aws

Changing 6 resources:

  aws_acm_certificate_validation.example_a
  aws_acm_certificate_validation.example_b
  aws_route53_record.example_a_validation
  aws_route53_record.example_b_validation
  aws_acm_certificate.example_a
  aws_acm_certificate.example_b

Do you want to make these changes?
Only 'yes' will be accepted to continue.

Enter a value: yes

Successfully replaced provider for 6 resources.

이렇게 state 파일을 변경하고 나면 0.14에서 init해서 업그레이드할 수 있다.

2021/01/04 01:20 2021/01/04 01:20