LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 18분

프로바이더와 리소스 의존성 관리

Provider는 Terraform의 드라이버다

Terraform 본체는 HCL을 읽고, 상태를 비교하고, 실행 순서를 계산한다. 하지만 AWS VPC를 만들거나 Kubernetes Secret을 바꾸거나 GitHub repository를 생성하는 실제 API 호출은 Terraform 본체가 직접 알지 못한다. 그 일을 하는 플러그인이 Provider다.

그래서 Terraform 코드를 읽을 때는 리소스 블록만 보면 부족하다. 먼저 이 질문을 확인해야 한다.

  1. 이 리소스 타입은 어떤 Provider가 구현하는가?
  2. 그 Provider는 어떤 버전으로 고정되어 있는가?
  3. 어느 계정, 리전, 엔드포인트, 인증 정보로 실행되는가?
  4. 리소스 간 순서는 참조로 충분히 드러나는가, 아니면 명시적 의존성이 필요한가?

이 장의 핵심은 "Provider 설정"과 "의존성 그래프"를 같은 운영 문제로 보는 것이다. 잘못된 Provider 인스턴스를 쓰면 엉뚱한 리전에 리소스가 생기고, 잘못된 의존성은 apply 중 간헐적인 실패나 불필요한 교체로 이어진다.

required_providers: 설치할 플러그인을 선언한다

Provider는 Terraform과 별도로 배포되고, 각자 버전 체계를 가진다. 운영 코드에서는 루트 모듈에 필요한 Provider의 source와 version 제약을 명시해야 한다.

terraform {
  required_version = ">= 1.6"

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

    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

여기서 aws는 모듈 안에서 쓰는 로컬 이름이고, hashicorp/aws는 Terraform Registry에서 Provider를 찾기 위한 source address다. 대부분은 Provider가 권장하는 로컬 이름을 그대로 쓴다. 예를 들어 aws_instance, aws_security_group 같은 리소스 타입의 첫 단어가 aws이므로 Terraform이 기본 Provider를 추론하기 쉽다.

terraform init은 이 선언을 보고 Provider를 설치하고 .terraform.lock.hcl을 갱신한다. 팀 저장소에서는 lock file을 커밋하는 편이 안전하다. 같은 ~> 5.0 제약이라도 실행 시점에 따라 최신 patch/minor 버전이 달라질 수 있기 때문이다. lock file은 CI와 로컬, HCP Terraform이 같은 Provider 빌드를 쓰도록 맞춰주는 장치다.

provider block: 같은 Provider도 여러 실행 문맥을 가진다

required_providers가 "무엇을 설치할지"라면, provider block은 "어디에 어떤 권한으로 호출할지"를 정한다.

provider "aws" {
  region = "ap-northeast-2"

  default_tags {
    tags = {
      ManagedBy = "terraform"
      Service   = "payment"
    }
  }
}

실무에서 자주 놓치는 부분은 Provider 설정도 코드의 일부라는 점이다. region, assume_role, custom endpoint, default tags 같은 값은 리소스의 실제 위치와 권한 범위를 바꾼다. 같은 aws_s3_bucket 코드라도 Provider가 다르면 완전히 다른 계정에 만들어질 수 있다.

alias: 멀티 리전·멀티 계정의 기본 도구

하나의 Terraform 실행에서 같은 Provider를 여러 설정으로 써야 할 때 alias를 둔다.

provider "aws" {
  region = "ap-northeast-2"
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

resource "aws_s3_bucket" "logs_kr" {
  bucket = "example-logs-kr"
}

resource "aws_s3_bucket" "logs_us" {
  provider = aws.us_east_1
  bucket   = "example-logs-us"
}

logs_kr은 기본 Provider를 쓰고, logs_usaws.us_east_1 Provider 인스턴스를 쓴다. 이 구분을 코드에 명시하지 않으면 읽는 사람은 리소스가 어느 리전에 생기는지 파일 바깥의 실행 환경까지 추적해야 한다.

주의할 점이 있다. 특정 Provider의 모든 block에 alias만 있고 alias 없는 기본 Provider가 없으면, Terraform은 비어 있는 기본 Provider 설정을 암묵적으로 만들 수 있다. 그러면 provider = aws.some_alias를 빠뜨린 리소스가 빈 설정을 사용하려다 region 같은 필수 값 누락으로 실패한다. 멀티 리전 코드에서도 기본 Provider 하나는 명시적으로 두고, 예외 리소스만 alias를 지정하는 방식이 읽기 쉽다.

terraform block
required_providers
source + version
.terraform.lock.hcl
선택된 버전 고정
provider instances
aws
ap-northeast-2
aws.us_east_1
us-east-1
resource / module
provider 생략
기본 aws 사용
provider = aws.us_east_1
별도 리전 사용
Provider 선택 흐름

모듈에는 Provider를 주입한다

재사용 모듈 내부에 Provider 인증·리전 설정을 박아두면 모듈의 재사용성이 떨어진다. 모듈은 필요한 Provider requirement만 선언하고, 실제 Provider configuration은 루트 모듈에서 넘겨주는 패턴이 낫다.

# root module
provider "aws" {
  region = "ap-northeast-2"
}

provider "aws" {
  alias  = "dr"
  region = "ap-northeast-1"
}

module "primary_vpc" {
  source = "./modules/vpc"

  providers = {
    aws = aws
  }

  cidr_block = "10.10.0.0/16"
}

module "dr_vpc" {
  source = "./modules/vpc"

  providers = {
    aws = aws.dr
  }

  cidr_block = "10.20.0.0/16"
}

이렇게 하면 같은 modules/vpc 코드가 서울 리전과 도쿄 리전에 각각 배포된다. 모듈 입장에서는 aws_vpc, aws_subnet을 평범하게 정의하지만, 호출자가 어떤 Provider instance를 연결했는지에 따라 실행 문맥이 달라진다.

Terraform의 기본 의존성: 참조가 곧 순서다

Terraform은 리소스 간 참조를 분석해서 dependency graph를 만든다. 예를 들어 subnet이 VPC ID를 참조하면 VPC가 먼저 만들어져야 한다는 것을 자동으로 안다.

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

여기서는 depends_on이 필요 없다. 오히려 모든 곳에 depends_on을 붙이면 그래프가 불필요하게 보수적으로 변하고, 병렬 실행 기회가 줄어든다. 좋은 Terraform 코드는 출력과 입력 참조만으로 대부분의 순서가 드러난다.

의존성은 생성 순서뿐 아니라 삭제 순서에도 영향을 준다. 위 예에서 삭제할 때는 subnet이 먼저 지워지고 VPC가 나중에 지워진다. Terraform의 graph는 apply 전체의 순서를 다루므로, 참조 관계를 정확히 쓰는 것이 운영 안정성의 출발점이다.

depends_on은 "숨은 선행 조건"에만 쓴다

depends_on은 Terraform이 참조만으로 알 수 없는 관계를 알려주는 meta-argument다.

resource "aws_iam_role_policy" "app" {
  role   = aws_iam_role.app.id
  policy = data.aws_iam_policy_document.app.json
}

resource "aws_lambda_function" "app" {
  function_name = "example-app"
  role          = aws_iam_role.app.arn
  handler       = "index.handler"
  runtime       = "nodejs20.x"

  filename = "function.zip"

  depends_on = [
    aws_iam_role_policy.app
  ]
}

Lambda 함수는 role ARN만 참조하므로 IAM role 자체와의 의존성은 생긴다. 하지만 role policy attach가 완료되어야 런타임에서 필요한 권한을 사용할 수 있다는 선행 조건은 role ARN 참조만으로 충분히 표현되지 않을 수 있다. 이런 경우 depends_on은 합리적이다.

반대로 아래처럼 이미 직접 참조가 있는 경우에는 중복이다.

resource "aws_subnet" "bad_example" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"

  depends_on = [aws_vpc.main] # 불필요한 중복
}

운영 기준은 단순하다. 값 참조로 표현할 수 있으면 참조를 쓰고, 값 참조가 없는 외부 효과나 eventually consistent API 순서만 depends_on으로 표현한다.

lifecycle은 의존성 그래프를 바꾸는 안전장치다

lifecycle block은 리소스의 생성·수정·삭제 방식을 바꾼다. 특히 create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by는 운영에서 자주 본다.

resource "aws_lb_target_group" "app" {
  name_prefix = "app-"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id

  lifecycle {
    create_before_destroy = true
  }
}

create_before_destroy는 교체가 필요할 때 새 리소스를 먼저 만들고 기존 리소스를 나중에 지운다. 무중단에 가까운 교체를 만들 수 있지만, 이름 유일성이나 quota 제약이 있으면 실패할 수 있다. 그래서 name 대신 name_prefix를 쓰거나 랜덤 suffix를 붙이는 식의 리소스별 설계가 필요하다.

resource "aws_db_instance" "primary" {
  identifier = "prod-primary"
  engine     = "postgres"

  lifecycle {
    prevent_destroy = true
  }
}

prevent_destroy는 실수로 DB 같은 핵심 리소스를 삭제하는 계획을 막는다. 다만 리소스 block 자체를 코드에서 제거하면 이 보호 규칙도 같이 사라진다. "Terraform 관리에서는 빼되 실제 인프라는 보존"하려면 terraform state rm 같은 state 분리 절차를 따로 써야 한다.

resource "aws_autoscaling_group" "app" {
  # ...

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

ignore_changes는 외부 시스템이 바꾸는 값을 Terraform이 되돌리지 않게 한다. 예를 들어 autoscaler가 desired_capacity를 바꾸는 구조라면 Terraform 코드의 초기값과 실제 값이 달라지는 것이 정상이다. 하지만 너무 넓게 쓰면 drift를 숨긴다. ignore_changes = all은 거의 마지막 수단으로 봐야 한다.

운영 리뷰 체크리스트

Terraform PR에서 Provider와 의존성을 볼 때는 다음을 확인한다.

점검 항목좋은 신호위험 신호
Provider 버전required_providers와 lock file이 함께 관리됨버전 제약이 없거나 너무 넓음
Provider 문맥기본 Provider와 alias 용도가 명확함리전·계정이 변수/환경에 숨어 있어 추적 어려움
모듈 Provider루트에서 providers로 주입child module 내부에 인증·리전 하드코딩
의존성 표현리소스 속성 참조로 자연스럽게 연결모든 리소스에 습관적 depends_on
명시적 의존성숨은 API 순서·외부 효과만 표현이미 참조가 있는데 중복 선언
lifecycle무중단·보호·외부 drift 목적이 주석/코드로 드러남ignore_changes가 drift 은폐용으로 남용됨

Provider와 의존성 관리는 "Terraform 문법을 아는가"보다 "실제 인프라 변경 순서를 예측할 수 있는가"에 가깝다. 계획 파일에서 어떤 Provider가 어떤 리소스를 만들고, 어떤 그래프 순서로 실행되는지 설명할 수 있다면 운영 중 사고 가능성이 크게 줄어든다.

References