들어가며

드디어 HCL 기본 문법 스터디가 끝나고 다른 주제로 넘어가게 되었습니다. 물론 HCL 문법을 계속 사용해야 하는 이상 지속적인 공부와 추가/변경되는 문법을 계속 팔로우 해야겠지만, 한 단계를 넘겼다는 느낌에 뿌듯합니다. :thumbsup:

이번 주에는 계속 사용은 해왔지만 잘은 모르고 있던 ProviderState에 대해 테라폼으로 시작하는 IaC를 통해 알아 보겠습니다.

테라폼으로 시작하는 IaC

테라폼으로 시작하는 IaC

프로바이더 (Provider)

  • 테라폼은 다양한 클라우드 서비스와 SaaS 등을 지원합니다. 이를 위해 사용하는 것이 Provider입니다.
  • Provider는 클라우드 서비스나 SaaS를 지원하는 테라폼의 플러그인이며 각 서비스의 API 와 통신하여 리소스를 관리합니다.
  • Provider는 다음의 3가지 등급으로 나뉩니다.

    등급 설명 네임스페이스
    Official 테라폼 팀에서 제공하는 공식 플러그인입니다. hashicorp
    Partner 테라폼과 파트너십을 맺은 회사에서 제공하는 플러그인으로 주로 해당 파트너의 클라우드/SaaS 제품에 대한 관리 기능을 제공합니다.
    Partner 프로바이더가 되려면 HarshiCorp Technology Partnet Program 에 가입되어야 합니다.
    게시한 조직 이름
    (mongodb/mongodbatlas)
    Community 커뮤니티에서 제공하는 플러그인으로 개별 관리자와 그룹에서 등록한 프로바이더 입니다.
    사용시 주의가 필요합니다.
    개인 및 조적 계정 이름
    (DeviaVir/gsuite)
  • 추가적으로 Archived 등급이 있을 수 있으며, 더이상 유지보수 되지 않는 이전 버전을 의미합니다.
  • Community 제공 Provider충분히 검증되지 않거나, 신뢰성이 충분히 입증되지 않은 개인(또는 조직)이 등록한 경우도 있기 때문에 사용시 주의가 필요합니다. 특히 보안에 민감한 정보를 다루는 경우에는 특히 주의해야 합니다. github의 star 수, issue, PR 등을 확인하여 신뢰성을 판단한 다음 사용하는 것이 좋습니다.
  • 사용가능한 Provider의 목록은 다음의 링크에서 확인할 수 있습니다.

Provider 설정

  • Provider 설정은 다음과 같이 provider 블록을 사용하여 설정합니다. (1주차 참조)
    # main.tf
    terraform {
      required_version = "~> 1.3.0" # 테라폼 버전
      
      required_providers { # 프로바이더 버전을 나열
        random = {
          # Official Tier는 source를 생략할 수 있습니다.
        }
        architech-http = {
          # Official Tier 가 아닌 경우 source를 명시해야 합니다.
          source = "architect-team/http"
          version = "~> 3.0"
        }
        my-peter-aws = {   # 프로바이더의 로컬 이름은 임의로 지정 가능합니다.
                           # 원래는 aws = { version = "~> 5.56.1" } 과 같이
                           # 사용했겠지만 이렇게 이름을 바꿀 수 있습니다.
          source  = "hashicorp/aws"
          version = "~> 5.56.1"
        }
      }
      ... 
    } 
    ...
    
    • 위의 예제에서 random은 Official Tier 이므로 source를 생략할 수 있습니다.
    • 하지만 architech-team/http는 Official Tier가 아니므로 source를 명시해야 합니다.
    • my-peter-aws 처럼 provider는 로컬 이름을 임의로 지정할 수 있습니다. 하지만 특별한 이유가 없다면 aws와 같은 기본 이름을 사용하는 것이 좋습니다.

로컬 이름과 명시적 프로바이더 지정

# main.tf
terraform {
  required_providers {
    architect-http = {
      source = "architect-team/http"
      version = "~> 3.0"
    }
    http = {
      source = "hashicorp/http"
    }
    aws-http = {
      source = "terraform-aws-modules/http"
    }
  }
}

data "http" "example" {
  provider = aws-http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}
  • 로컬이름
    • 위의 예제에서 architect-http = { ... }architect-http, http = { ... }http, aws-http = { ... }aws-http는 로컬 이름입니다.
    • 앞서 말씀드린것 처럼 로컬이름은 임의로 지정할 수 있으나 특별한 이유가 없다면 “use provider”에서 권장하는 기본 이름을 사용하는 것이 좋습니다.
  • 명시적 프로바이더 지정
    • 위의 예제처럼 http라는 동일한 이름의 리소스를 가진 경우 dataresource 블록에서 provider를 명시적으로 지정할 수 있습니다.
      • 위의 예제에서 data.http.exampleaws-http로 지정된 provider를 사용합니다.
      • 만약 data "http" "example" {에서 provider = aws-http를 생략했다면, 위의 required_providers에서 http라는 로컬 이름을 지정한 hashicorp/http 프로바이더가 사용됩니다.

단일 프로바이더의 다중 정의

  • 동일한 프로바이더를 사용하지만 다른 설정을 필요로 하는 경우, 리소스마다 별도로 선언된 프로바이더를 지정해야 하는 경우가 있습니다.
  • 예를들어서 aws 프로바이더를 사용할때 두개 이상의 region이나 access_key, secret_key 등이 다른 환경을 사용하는 경우가 있는데 이때 아래와 같이 정의 할 수 있습니다.
    # main.tf
    provider "aws" {
      region = "ap-southeast-1"
    }
    provider "aws" {
      alias  = "seoul"
      region = "ap-northeast-2"
    }
    
    resource "aws_instance" "app_server1" {
      ami           = "ami-06b79cf2aee0d5c92"
      instance_type = "t2.micro"
    }
      
    resource "aws_instance" "app_server2" {
      provider      = aws.seoul
      ami           = "ami-0ea4d4b8dc1e46212"
      instance_type = "t2.micro"
    }  
    

    첫 aws 프로바이더는 alias 없이 사용되고 두번째 aws 프로바이더는 “seoul”이라는 alias를 사용하였고, aws.seoul과 같이 사용할 수 있습니다.

    • 실행 결과
      $ terraform apply
      $ terraform state list 
          
      $ echo "aws_instance.app_server1.public_ip" | terraform console
      # => "13.215.47.148"
          
      $ echo "aws_instance.app_server1.availability_zone" | terraform console
      # => "ap-southeast-1b"
           
      $ echo "aws_instance.app_server2.public_ip" | terraform console
      # => "43.203.243.238"
      
      $ echo "aws_instance.app_server2.availability_zone" | terraform console    
      # => "ap-northeast-2a"
      

프로바이더 요구사항 정의

  • 테라폼 실행시 요구되는 프로바이더 요구사항은 terraform블록의 required_providers 블록에 여러개를 정의할 수 있습니다.
  • source에는 프로바이더 다운로드 경로를 지정하고 version은 버전 제약을 명시합니다.
  • 프로바이더 요구사항 정의 블록
    terraform {
      required_providers {
        <프로바이더 로컬 이름> = {
          source = [<호스트 주소>/]<네임스페이스>/<유형>
          version = <버전 제약>
        }
        ...
      }
    } 
    
    • source : 프로바이더가 호스팅되는 주소와 네임스페이스, 유형을 지정합니다.
      • 호스트 주소 : 프로바이더가 호스팅되는 주소로, 필수는 아니며 기본값은 registry.terraform.io 입니다.
      • 네임스페이스 : 프로바이더의 네임스페이스로, 공개된 레지스트리 및 Terraform Cloud의 비공개 레지스트리의 프로바이더를 게시하는 조직을 나타냅니다..
      • 유형 : 프로바이더에서 관리되는 플랫폼이나 서비스 이름입니다.
      • 예제)
        • source = "hashicorp/aws"hashicorp 네임스페이스의 aws 프로바이더를 의미합니다. 호스트 주소가 생략되어 registry.terraform.io로 간주됩니다.
        • source = "mycorp.com/myns/mytype"mycorp.com 호스트 주소의 myns 네임스페이스의 mytype라는 프로바이더를 의미합니다.
    • version : 버전 제약은 1주차에서 설명한 것과 동일합니다.

프로바이더 설치

  • terraform init 명령을 실행시 지정된 source에서 version의 버전 제약에 맞는 프로바이더를 다운로드 합니다.
  • 항상 지정한 특정 버전을 사용하려면 terraform 블록에서 정의하거나 .terraform.lock.hcl 잠금파일을 코드 저장소에 공유하는 등의 방법이 있습니다.
  • required_providers 블록을 사용하면 실제로 리소스를 사용하지 않더라도 설치가 되며, required_providers 블록을 사용하지 않으면 리소스를 사용할 때 테라폼이 추론해서 최신 버전의 프로바이더를 설치가 진행됩니다.

프로바이더 간 전환 여부

  • AWS, GCP 같은 클라우드 서비스와 같이 유사한 서비스들을 제공하는 경우 프로바이더를 바꿔서 쉽게 사용할 수 있을까요? 불가능합니다.
  • 각 프로바이더 별로 리소스 이름도 다르고 설정도 다르기 때문에 프로바이더를 바꾸는 것은 쉽지 않습니다.
  • 하지만 1:1로 매칭 되지는 않지만, 유사 프로바이더 간에는 리소스 명이나 옵션에도 유사점이 있기 때문에 이점은 있습니다.

프로바이더 에코 시스템

  • 테라폼의 에코시스템은 사용자가 사용하는 방식과 구조에 따라 테라폼을 적용할 수 있도록 설계되었습니다. img.png
  • 테라폼의 에코시스템을 구성하는 파트너는 크게 워크플로우 파트너와 인프라스트럭쳐 파트너로 나뉩니다.
    • 워크플로우 파트너 : 테라폼 클라우드와 테라폼 엔터프라이즈에 대한 지원을 제공하는 파트너로, 주로 자신들의 기존 플랫폼을 테라폼에서 사용할 수 있도록 돕는 파트너입니다. 대표적으로 Github, Gitlab, Jenkins, CircleCI, TravisCI, Datadog, NewRelic 등이 있습니다.
      • 워크 플로우 파트너의 유형
        • 코드 스캐닝 : IaC 구성을 검토하여 오류나 보안 문제를 방지하는 도구를 제공하는 파트너
        • 비용 관리 : 새로운 인프라의 비용 영향을 분석하고 비용 관리를 적용하는 파트너
        • 관찰성/모니터링 : 성능의 가시성을 제공하거나 인프라 변경을 자동으로 감지하여 최적의 관찰성을 보장하는 데 중점을 둔 파트너
        • 보안 : 보안 및 준수 정책에 맞지 않는 Terraform 구성 오류를 감지하는 파트너
        • 감사 : 멀티 클라우드 인프라 자원을 관리하여 서비스 중단을 방지하고, 거버넌스를 개선하며, 효율성을 높이는 데 도움을 주는 파트너
        • 노 코드/로우 코드 : IT, 공급망, 운영 관리, 비즈니스 관리 및 기타 워크플로우의 구현, 배포 및 전달에 중점을 둔 파트너
        • SSO(Single Sign On) : 최종 사용자가 안전하게 로그인할 수 있도록 인증에 중점을 둔 파트너
        • CI/CD : 지속적 통합 및 지속적 전달/배포에 중점을 둔 파트너
        • VCS : 소프트웨어 코드 변경을 추적하고 관리하는 데 중점을 둔 파트너
    • 인프라스트럭처 파트너 : 모든 테라폼 에디션을 지원하며, 자신들의 플랫폼의 API를 통해 리소스를 활용할 수 있게 하는 파트너입니다. 대표적으로 퍼블릭 클라우드 서비스 업체인 AWS, Azure, GCP와 PaaS 인 Heroku, SaaS 서비스인 DNSimple, CloudFlare 등이 있습니다.
      • 워크 플로우 파트너의 유형
        • 퍼블릭 클라우드 : 대규모 글로벌 클라우드 제공업체로 IaaS, SaaS, PaaS를 포함한 다양한 서비스를 제공
        • 컨테이너 오케스트레이션 : 컨테이너 프로비저닝 및 배포를 관리
        • IaaS(Infrastructure-as-a-Service) : 스토리지, 네트워킹, 가상화와 같은 솔루션을 제공하는 인프라 및 IaaS 제공
        • 보안 및 인증 : 인증 및 보안 모니터링 플랫폼을 제공
        • 자산 관리 : 소프트웨어 라이선스, 하드웨어 자산, 클라우드 리소스를 포함한 주요 조직 및 IT 리소스의 자산 관리를 제공
        • CI/CD : 지속적 통합 및 지속적 전달/배포
        • 로깅 및 모니터링 : 로거, 메트릭 도구, 모니터링 서비스와 같은 서비스를 구성하고 관리
        • 유틸리티 : 랜덤 값 생성, 파일 생성, http 상호작용, 시간 기반 리소스와 같은 도움 기능을 제공
        • 클라우드 자동화 : 구성 관리와 같은 특수 클라우드 인프라 자동화 관리
        • 데이터 관리 : 데이터 센터 저장소, 백업, 복구 솔루션 제공
        • 네트워킹 : 라우팅, 스위칭, 방화벽, SD-WAN 솔루션과 같은 네트워크 특정 하드웨어 및 가상화 제품과 통합
        • VCS(Version Control System) : Terraform 내에서 VCS 프로젝트, 팀, 저장소에 중점을 둠
        • 통신 및 메시징 : 통신, 이메일, 메시징 플랫폼과 통합
        • 데이터베이스 : 데이터베이스 리소스를 프로비저닝하고 구성할 수 있는 기능을 제공
        • PaaS(Platform-as-a-Service) : 이들은 하드웨어, 소프트웨어, 애플리케이션 개발 도구를 포함한 다양한 범위를 제공하는 플랫폼 및 PaaS 제공
        • 웹 서비스 : 웹 호스팅, 웹 성능, CDN 및 DNS 서비스

상태 (State)

State의 목적과 의미

테라폼의 State는 테라폼이 관리하는 인프라의 현재 상태를 저장하는 파일입니다. 그 동작의 특징을 알아 보기위해서 다음과 같이 State의 목적과 의미를 알아보겠습니다. 기본적으로 `terraform.tfstate 파일에 저장되며, 이 파일은 JSON 형식으로 저장됩니다.

  • 실습
    • main.tf 생성
      provider "aws" {
        region  = "ap-northeast-2"
      }
          
      resource "aws_vpc" "peter_vpc" {
        cidr_block       = "10.10.0.0/16"
          
        tags = {
          Name = "t101-study"
        }
      }
      
    • 실행
      $ terraform init && terraform plan && terraform apply -auto-approve
          
      # 상태 파일 확인
      $ cat terraform.tfstate  
      # => ...
      #    "serial": 1,
      #    ...
      #    "instances": [ 
      #       { "id": "vpc-0bebcf57eff3753a3",
      #         "tags": { "Name": "t101-study" }, 
      #    ...
          
      $ echo "aws_vpc.peter_vpc.id" | terraform console
      # => "vpc-0bebcf57eff3753a3"
          
      $ echo "aws_vpc.peter_vpc.tags.Name" | terraform console
      # => "t101-study"
      
    • main.tf의 태그 수정 수정
      provider "aws" {
        region  = "ap-northeast-2"
      }
          
      resource "aws_vpc" "peter_vpc" {
        cidr_block       = "10.10.0.0/16"
          
        tags = {
          Name = "Warmachine Rox"
        }
      }
      
    • 실행
      $ terraform init && terraform plan && terraform apply -auto-approve
          
      # 상태 파일 확인
      $ cat terraform.tfstate  
      # => ...
      #    "serial": 1,
      #    ...
      #    "instances": [ 
      #       { "id": "vpc-0bebcf57eff3753a3",
      #         "tags": { "Name": "Warmachine Rox" }, 
      #    ...
          
      $ echo "aws_vpc.peter_vpc.id" | terraform console
      # => "vpc-0bebcf57eff3753a3"
          
      $ echo "aws_vpc.peter_vpc.tags.Name" | terraform console
      # => "Warmachine Rox"
      
    • 상태비교
      $ diff terraform.tfstate terraform.tfstate.backup
      4c4
      <   "serial": 3,
      ---
      >   "serial": 1,
      39c39
      <               "Name": "Warmachine Rox"
      ---
      >               "Name": "t101-study"
      42c42
      <               "Name": "Warmachine Rox"
      ---
      >               "Name": "t101-study" 
      
  • 위와 같이 상태가 관리되는 파일인 terraform.tfstate에 최근 적용 상태가 적용됩니다.

멱등성 (Idempotency)

  • 이렇게 저장된 상태는 멱등성를 제공하는데 활용됩니다.
  • terraform plan이나 terraform apply를 했을때, 프로비저닝된 리소스와 .tf 파일의 리소스가 일치하지 않으면 테라폼은 .tf 파일의 리소스를 프로비저닝된 리소스와 일치하도록 변경합니다. 이때 프로비저닝된 리소스와 .tf 파일의 리소스가 같은 리소스를 가리키고 있는지 아는데 id값 등을 사용합니다.
  • 즉, 상태 파일에 문제가 생길 경우 기존에 생성된 리소스에 대한 추적이 불가능해져서 실제로는 리소스가 존재해도 테라폼 입장에서는 없는것과 마찬가지가 됩니다.
  • 이렇게 중요하기 때문에 상태 파일은 테라폼에서만 관리되도록 하고, 직접 편집하거나 작성하거나 삭제하면 안됩니다.

상태 파일을 팀 단위에서 테라폼 운영시 문제점

  • 로컬의 상태 파일은 팀 단위에서 운영시 다음과 같은 어려움이 있습니다.
    • 상태 파일은 최종 상태를 갖고 있어야 하기 때문에 모든 팀원들이 동일한 상태 파일을 갖고 있어야 합니다.
    • 두명이상이 동시에 적용하는것을 막기 위해 상태 파일을 잠그는 기능이 필요합니다.
    • 개발 단계, 스테이징 단계, 프로덕션 단계 등 다양한 환경에서 각 환경별로 상태파일을 격리할 필요가 있습니다.
    • 상태파일에는 sensitive=true인 민감한 값도 평문으로 저장됩니다.
  • 상태파일을 VCS로 관리하는 것의 문제점
    • 수동 오류
      • 상태 파일의 최신 변경 사항을 가져오거나, 변경 되었을때 push 하는것을 잊기 쉽습니다.
      • 상태 파일을 잘못 병합하거나, 잘못된 상태 파일을 사용하는 경우가 발생할 수 있습니다.
    • 잠금
      • VCS 사용시 잠금 기능을 사용할 수 없습니다.
    • 민감한 정보
      • 민감한 정보가 평문으로 저장되어 VCS 에 권한이 있는 사람은 누구나 볼 수 있어서 노출 위협이 있습니다.
  • 이러한 문제점을 해결하기 위해 테라폼 클라우드에서는 안전하게 상태를 관리하는 기능을 제공합니다.

원격지원 백엔드

  • 테라폼 클라우드를 사용하지 않는 경우는 원격 백엔드를 사용하면 어느정도 해결이 가능합니다.
    • 수동 오류 해결 : plan/apply 시 백엔드에 최신의 상태가 저장 됩니다.
    • 잠금 : 백엔드에서 잠금 기능을 제공합니다. -lock-timeout=<TIME>로 잠금 대기 시간을 설정할 수 있습니다.
    • 민감한 정보 : 대부분의 백엔드는 민감한 정보를 암호화하여 저장합니다. 또한 권한관리 기능으로 상태 파일에 대한 접근을 차단할 수 있습니다.
  • 원격 백엔드 종류 : AWS S3, Azure Blob Storage, Google Cloud Storage, HashiCorp Consul 등

State 동기화

  • 테라폼은 구성 파일과 기존 State와 실제 리소스 구성을 비교하여 변경 사항을 찾아내고 적용합니다.
    • 테라폼 구성과 State 흐름 img.png 출처 : https://kschoi728.tistory.com/135
      • plan과 apply 중 리소스에 발생할 수 있는 네 가지 변경 사항이 있고, 아래의 출력 기호로 표시 되었습니다.
        • + create : 리소스의 생성
        • - destroy : 리소스의 삭제
        • -/+ replace : 삭제 후 생성 (lifecyclecreate_before_destroy 설정시 생성 후 삭제 설정이 가능합니다.)
        • ~ update in-place : 리소스가 있는 상태에서 일부 상태만 업데이트
  • 유형별 실습 + 문제사항 -> 복구 import
    • 테라폼 구성에 추가된 리소스와 State에 따라 어떤 동작이 발생하는지 표로 정리하였습니다.

      유형 구성 리소스 정의(.tf) State 구성 데이터 실제 리소스 기본 예상 동작
      1 있음     리소스 생성
      2 있음 있음   리소스 생성
      3 있음 있음 있음 동작 없음
      4   있음 있음 리소스 삭제
      5     있음 동작 없음
    • 유형 1 : 신규 리소스 정의 -> Apply -> 리소스 생성
      locals {
        name = "peter_test"
      }
          
      resource "aws_iam_user" "peter_iamuser1" {
        name = "${local.name}1"
      }
          
      resource "aws_iam_user" "peter_iamuser2" {
        name = "${local.name}2"
      }     
      
      • 실행
        # 첫번째 실행. 기존에 iam 이 없기 때문에 리소스가 생성됩니다.
              
        $ terraform init && terraform plan && terraform apply -auto-approve
        # => ...
        #    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
              
        $ ls -l terraform.tfstate
        $ cat terraform.tfstate | jq 
              
        # main.tf 수정없이 두번째 실행. 변경사항이 없기 때문에 아무런 동작을 하지 않습니다. (유형 3)
              
        $ terraform apply -auto-approve
        # => No changes. Your infrastructure matches the configuration.
        #    ...
        #    Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
              
        # iam 사용자 리스트 확인
        $ aws iam list-users | jq
        
    • 유형 2 : 실제 리소스 수동제거 -> Apply => 리소스 생성
      # 실제 리소스 수동 제거
      $ aws iam delete-user --user-name peter_test1
      $ aws iam delete-user --user-name peter_test2
          
      # iam 사용자 리스트에서 실제 삭제됨을 확인 
      $ aws iam list-users | jq
          
      $ terraform plan
      # => ...
      #    Plan: 2 to add, 0 to change, 0 to destroy.
      

      이와 같이 리소스 구성정의(main.tf)을 수정하지 않고 실제 리소스가 제거되면 테라폼이 실제 리소스와 상태 파일을 비교해서 차이를 인지하고 동일하게 상태를 만들기위해 리소스를 생성하려 합니다.

      $ terraform plan -refresh=false
      # => No changes. Your infrastructure matches the configuration.
      

      하지만 -refresh=false 옵션을 주어 강제로 실제 리소스를 refresh 하지 않도록 하면 리소스 구성정의와 상태 파일만을 비교하게 됩니다. -refresh=false 을 준 상태에서는 상태파일과 리소스 구성정의가 일치하므로 변경사항이 없습니다.

      $ terraform apply -auto-approve
      # => ...
      #    Apply complete! Resources: 2 added, 0 changed, 0 destroyed. 
      

      -refresh=false 옵션을 주지 않고 apply 시키면 테라폼이 똑똑하게 실제 리소스와의 차이를 인식하여 수동 제거된 리소스를 생성합니다.

    • 유형 3 : 코드, 상태, 실제 리소스가 일치하는 경우
      $ terraform apply -auto-approve
      $ cat terraform.tfstate | jq .serial
      # => 8 
      
      $ terraform apply -auto-approve
      $ cat terraform.tfstate | jq .serial
      # => 8 
      
      $ terraform apply -auto-approve
      $ cat terraform.tfstate | jq .serial
      # => 8 
      

      코드, 상태, 실제 리소스가 일치하는 경우 아무 동작을 하지 않으며 상태파일의 serial도 변하지 않습니다.

    • 유형 4 : 리소스 구성정의에서 일부 리소스 삭제 -> Apply
      # main.tf 
      locals {
        name = "peter_test"
      }
            
      resource "aws_iam_user" "peter_iamuser1" {
        name = "${local.name}1"
      }
            
      # peter_test2 리소스를 구성정의에서 삭제
      
      • 실행
        $ terraform apply -auto-approve
        # => ...
        #    Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
                
        $ ls *.tfstate
        $ cat terraform.tfstate | jq
                     
        # iam 사용자 리스트에서 삭제됨을 확인
        $ aws iam list-users | jq
        
    • 유형 5 : 실제 리소스만 존재하는 경우 상태파일에는 리소스가 존재하지 않지만 실제 리소스는 존재하는 경우 테라폼과 연결고리가 없기 때문에, 실제 존재하는 리소스는 테라폼에서는 없는 것으로 보입니다. 또한 리소스 구성정의 파일에도 없기 때문에 아무런 동작을 하지 않습니다.
    • 유형 6 : 실수로 tfstate 파일이 삭제 된 경우
      $ terraform state list
      # => aws_iam_user.peter_iamuser1
          
      # 상태 파일 삭제
      $ rm terraform.tfstate*
          
      # terraform plan시 실제 리소스에 연결고리가 없기 때문에, 실제 리소스가 없다고 판한하고 리소스를 생성하려 합니다.
      $ terraform plan
      # => Plan: 1 to add, 0 to change, 0 to destroy.
          
      # 실제 리소스를 확인해보면 존재합니다.
      $ aws iam list-users | jq 
          
      # 현재 상태에서 적용시 이미 존재하는 IAM을 만들려한다고 오류가 발생합니다.
      $ terraform apply -auto-approve
      # => Error: creating IAM User (peter_test1): operation error IAM: CreateUser, ...
          
      # 상태 보기를 하면 상태 파일이 없는것을 확인할 수 있습니다.
      $ terraform state list
      # => No state file was found!
      

      상태 파일이 삭제되면 연결고리가 없어지면서 리소스를 생성하려 하지만, 실제 리소스에 동일한 이름의 IAM이 존재하여 실패하게 됩니다.

      • 해결 방법 : import 명령을 사용하여 state 파일을 복구합니다.
    • 유형 7 : 실수로 삭제된 tfstate를 import로 복구
      • terraform import 명령은 기존에 생성된 리소스를 테라폼의 상태 파일에 추가하는 명령입니다.
        # 실제 IAM 사용자 리스트 확인
        $ aws iam list-users | jq
              
        # 실제 IAM 사용자의 이름을 가져와서 import 명령을 실행합니다.
        $ terraform import aws_iam_user.peter_iamuser1 peter_test1
        # => aws_iam_user.peter_iamuser1: Importing from ID "peter_test1"...
        #    aws_iam_user.peter_iamuser1: Import prepared!
        #    Prepared aws_iam_user for import
        #    aws_iam_user.peter_iamuser1: Refreshing state... [id=peter_test1]
        #    
        #    Import successful!
        #    
        #    The resources that were imported are shown above. These resources are now in
        #    your Terraform state and will henceforth be managed by Terraform.
              
        # 상태 파일 확인하면 상태파일이 있고, aws_iam_user 리소스가 추가된것을 확인할 수 있습니다.
        $ cat terraform.tfstate | jq
              
        $ terraform state list
        # => aws_iam_user.peter_iamuser1
              
        # 실제 리소스와 상태 파일이 일치하여 변경사항이 없습니다.
        $ terraform apply -auto-approve
        

        이와 같이 terraform import 명령을 사용하여 실수로 삭제된 상태 파일을 복구할 수 있습니다. 하지만 import 명령은 지정한 리소스만 복구할 수 있기 때문에, 여러 리소스를 복구할 경우 어려움이 따릅니다.

      • 따라서 상태파일을 S3의 버전관리 기능을 사용하는 등 잃어버리지 않도록 주의해야 합니다.

Terraform Backend : AWS S3 + DynamoDB

  • 앞서 언급한것 처럼 로컬에서 상태파일을 관리할 경우 여러 문제점이 있어서 그것을 해결하기 위해 AWS S3 백엔드를 사용하여 모든 팀원들이 하나의 상태를 공유하도록 할 수 있습니다.
  • 단, AWS S3만 사용시 동시성 잠금 관리가 되지 않기 때문에, DynamoDB 를 사용하여 잠금을 수행하여 보완할 수 있습니다.

악분님 실습 따라하기

AWS S3와 DynamoDB를 사용한 상태 파일 관리를 악분님이 정리해주신 실습을 따라하며 살펴보겠습니다. 악분님 블로그 링크

  • 사전준비 리모트 공용 저장소 AWS S3 생성
    $ git clone https://github.com/sungwook-practice/t101-study.git example
    $ cd example/state/step3_remote_backend/s3_backend 
      
    # terraform.tfvars 파일을 수정하여 버킷명을 bucket_name = "<닉네임>-hello-tf1014-remote-backend" 로 수정합니다.
    # 저의 경우는 bucket_name = "peter-hello-tf1014-remote-backend" 가 되겠습니다.
      
    # S3 버킷 생성
    $ terraform init && terraform plan && terraform apply -auto-approve
      
    # 확인
    $ terraform state list
    $ aws s3 ls
    # => 2024-07-XX 01:07:50 peter-hello-tf1014-remote-backend 
    
  • 테라폼 백엔드를 AWS S3로 설정
    $ cd ../vpc
    $ ls
    $ cat provider.tf
    
    • provider.tf 파일을 수정하여 백엔드를 S3로 설정합니다.
      ...
      backend "s3" {
        # bucket       = "<닉네임>-hello-tf1014-remote-backend"
        bucket         = "peter-hello-tf1014-remote-backend"
        key            = "terraform/state-test/terraform.tfstate"
        region         = "ap-northeast-2"
        # dynamodb_table = "terraform-lock"
      }
      ...
      
    • terraform init 명령을 실행하면 백엔드 설정이 적용됩니다.
      $ terraform init
      # => Initializing the backend...
      #    Successfully configured the backend "s3"! Terraform will automatically
      #    ...    
          
      # 리소스 생성 
      $ terraform apply -auto-approve
          
      $ terraform state list
          
      # 상태는 있지만 상태파일(terraform.tfstate)이 없음을 확인
      $ ls *.tfstate
      # => 파일이 없습니다.
          
      # AWS S3 버킷 내에 tfstate 파일 확인
      $ MYBUCKET=peter-hello-tf1014-remote-backend
      $ aws s3 ls s3://$MYBUCKET --recursive --human-readable --summarize
      # => 2024-07-07 01:23:35    1.7 KiB terraform/state-test/terraform.tfstate
      #    Total Objects: 1
      #    Total Size: 1.7 KiB
      

      상태가 로컬 파일이 아닌 S3에 저장됨을 확인하였습니다. 하지만 이 상태에서는 동시성 문제가 발생할 수 있기 때문에 DynamoDB를 추가하여 잠금을 설정하겠습니다.

  • DynamoDB 생성
    • 테라폼에서 DynamoDB 잠금을 사용하기 위해서는 LockID 라는 기본 키가 있는 테이블을 생성해야 됩니다.
      $ cd ../dynamodb
      $ ls 
      $ cat main.tf  
        
      # DynamoDB 생성
      $ terraform init && terraform plan && terraform apply -auto-approve
        
      # 확인
      $ terraform state list
      $ terraform state show aws_dynamodb_table.terraform_state_lock
        
      # DynamoDB 테이블 확인
      $ aws dynamodb list-tables --output text
      # => TABLENAMES   terraform-lock
      $ aws dynamodb describe-table --table-name terraform-lock | jq
      $ aws dynamodb describe-table --table-name terraform-lock --output table
      
  • 테라폼 백엔드 설정에 dynamodb_table 속성을 적용
    $ cd ../vpc 
      
    # vscode에서 provider.tf 수정
    $ code provider.tf 
    
    • provider.tf 파일을 수정하여 dynamodb_table = "terraform-lock"을 추가합니다.
      ...
      backend "s3" {
        bucket         = "peter-hello-tf1014-remote-backend"
        key            = "terraform/state-test/terraform.tfstate"
        region         = "ap-northeast-2"
        dynamodb_table = "terraform-lock"   # 추가
      }
      ...
      
    • 백엔드 설정이 바꼈으므로 terraform init 명령을 실행합니다.
      $ terraform init -migrate-state
      # => Initializing the backend...
      #    Backend configuration changed!
      #    ...
      
    • VPC의 tag 수정 후 apply와 Locking 확인
      # main.tf 수정 
      resource "aws_vpc" "main" {
        cidr_block = var.vpc_cidr
          
        tags = {
          # Name = "terraform VPC"
          Name = "terraform VPC 2" # 2추가
        }
      }
      
      • apply를 실행하고 “Enter a value:”에서 대기하면서 Dynamo DB 테이블을 확인합니다.
        $ terraform apply
        # => ...
        #    Enter a value: <입력하지 않고 대기>
        
      • AWS Console에서 DynamoDB의 lock 테이블을 확인합니다. 20240704_terraform_w4_dynamo_lock.png

        {
          "ID":"d210a853-3361-527a-dc34-8f6b310ba77b",
          "Operation":"OperationTypeApply",
          "Info":"",
          "Who":"peter",
          "Version":"1.8.5",
          "Created":"2024-07-XXT16:38:55.21022Z",
          "Path":"peter-hello-tf1014-remote-backend/terraform/state-test/terraform.tfstate"
        }
        
      • apply의 “Enter a value:”에 yes를 입력하여 apply하면 락이 해제됩니다. 이 상태에서 dynamodb를 확인하면 lock이 해제된것을 확인할 수 있습니다. 20240704_terraform_w4_dynamo_unlock.png
  • S3 버저닝 정보 확인
    # S3 버킷의 파일 확인
    $ aws s3 ls s3://$MYBUCKET --recursive --human-readable --summarize
    # => 2024-07-07 01:50:50    1.7 KiB terraform/state-test/terraform.tfstate
    # 
    #    Total Objects: 1
    #    Total Size: 1.7 KiB
      
    # 버저닝된 파일 확인
    $ aws s3api list-object-versions --bucket $MYBUCKET | jq
    # => {
    #      "Versions": [
    #        {
    #          "ETag": "\"f223f4da33dcd26ee1ee736212d57604\"",
    #          ...
    #          "Key": "terraform/state-test/terraform.tfstate",
    #          "VersionId": "wMK8alO1E2luobYHL1Bqj4RdbV1rLrv3",
    #          "IsLatest": true,
    #          "LastModified": "2024-07-XXT16:50:50+00:00",
    #          ...
    #        },
    #        {
    #          "ETag": "\"16bd360e57c83dee7ad657086cc8fc49\"",
    #          ...
    #          "Key": "terraform/state-test/terraform.tfstate",
    #          "VersionId": "8iagh8nWNMXeI.Ax5oEZJ38N684s5N3D",
    #          "IsLatest": false,
    #          "LastModified": "2024-07-XXT16:23:35+00:00",
    #          ...
    #        }
    #      ],
    #      "RequestCharged": null
    #    }
    
    • apply 되어 상태가 변경 되면 S3에 버저닝된 파일이 생성됨을 확인 할 수 있습니다.
    • S3 콘솔의 버킷에서도 버전 표시를 확인할 수 있습니다. 20240704_terraform_w4_backend_s3
  • 실습 리소스 삭제 : 리소스를 삭제하지 않으면 비용이 발생할 수 있습니다. 꼼꼼하게 잘 삭제하여 요금 폭탄을 막읍시다.
    # vpc 삭제 : 현재 vpc 디렉터리
    $ terraform destroy -auto-approve
      
    # dynamoDB 삭제
    $ cd ../dynamodb
    $ terraform destroy -auto-approve
      
    # S3 삭제
    $ cd ../s3_backend
    $ terraform destroy -auto-approve
    # => Error : (버킷이 비어있지 않다며 오류 발생합니다.)
      
    # 버킷 내용을 먼저 삭제 합니다.
    $ aws s3api delete-objects \
        --bucket $MYBUCKET \
        --delete "$(aws s3api list-object-versions \
        --bucket "$MYBUCKET" \
        --output=json \
        --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
      
    # S3 버킷에 삭제마커 삭제
    $ aws s3api delete-objects --bucket $MYBUCKET \
        --delete "$(aws s3api list-object-versions --bucket "$MYBUCKET" \
        --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')"
      
    # S3 삭제
    $ terraform destroy -auto-approve
    

테라폼 백엔드의 단점

  • backend 블록에는 변수나 참조를 사용할 수 없습니다.
  • 따라서 S3 버킷 이름, 리전, DynamoDB 테이블 이름을 하드코딩해야 합니다. 심지어 key 값은 중복이 되면 안 되며 고유해야 합니다.
  • 하지만 partial configuration 을 사용하여 일부 매개 변수를 전달하여 사용할 수 있습니다.
    # backend.hcl
    bucket = "terraform-up-and-running-state"
    region = "us-east-2"
    dynamodb_table = "terraform-Up-and-running-locks"
    encrypt = true 
    
    • 이 경우에도 모듈마다 다른 key 값을 가져야 하므로 key 매개 변수는 테라폼 코드에 있어야 합니다.
      terraform {
        backend "s3" {
          key = "example/terraform.tfstate"
        }
      }   
      
    • 부분적으로 구성한 것을 사용하려면 terraform init -backend-config=backend.hcl 명령을 사용합니다.
      $ terraform init -backend-config=backend.hcl
      
  • 이러한 단점을 보완해주는 오픈 소스 테라그런트(Terragrunt)가 있습니다. 다음 링크를 참고하십시오.

워크스페이스

  • 인프라를 구축할때는 dev(개발), stage(스테이지), prod(프로덕션) 등 개발 단계별로 각각 다른 환경을 구축해야 할 필요가 있습니다.
  • 이러한 환경을 구분하기 위해서는 파일 레이아웃을 사용하여 격리하거나 테라폼의 워크스페이스 기능을 사용할 수 있습니다.
  • 파일 레이아웃을 이용한 격리 (Isolation via file layout)
    • 파일 레이아웃을 사용하여 각 환경별로 디렉터리를 나누어 구성하면 각 환경별로 구성을 분리할 수 있습니다.
      $ tree
      # => .
      #    ├── common
      #    │   └── variables.tf
      #    ├── dev
      #    │   ├── vpc
      #    │   │   ├── main.tf
      #    │   │   └── variables.tf
      #    │   ├── main.tf
      #    │   └── variables.tf
      #    ├── prod
      #    │   ├── vpc
      #    │   │   ├── main.tf
      #    │   │   └── variables.tf
      #    │   ├── main.tf
      #    │   └── variables.tf
      #    └── stage
      #        ├── vpc
      #        │   ├── main.tf
      #        │   └── variables.tf
      #        ├── main.tf
      #        └── variables.tf
      
    • 환경별로 완전히 독립시켜 원하는 데로 구성할 수 있는 강력한 방법이지만, 동일한 파일도 환경별로 복사해야 할 수 있으며, 따라서 리소스 수가 많아지면 관리가 어려울 수 있습니다.
    • 공용 .tf 파일을 만들어서 각 환경별로 사용하면 중복을 줄일 수 있습니다.
  • 테라폼 워크스페이스 기능을 통한 격리 (Isolation via workspaces)
    • 동일한 구성에서 빠르고 격리된 환경을 만들 수 있습니다.
    • 실습
      # 현재 워크스페이스 목록 확인
      $ terraform workspace list 
      # => * default
      
      • main.tf 생성
        resource "aws_instance" "peter_srv1" {
          ami           = "ami-0ea4d4b8dc1e46212"
          instance_type = "t2.micro"
          tags = {
            Name = "t101-study"
          }
        }
        
      • 실행
        $ terraform init && terraform plan && terraform apply -auto-approve
              
        $ terraform state list 
              
        $ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 43.201.149.141
        $ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.private_ip'
        # => 172.31.8.91
              
        # 워크스페이스 확인
        $ terraform workspace list
        # => * default
              
        # graph 확인
        $ terraform graph > graph.dot
        
        aws_instance.peter_srv1
      • 신규 워크스페이스 생성
        # 새 작업 공간 workspace 생성 : mywork1
        $ terraform workspace new mywork1
        $ terraform workspace show               
        # => mywork1
              
        # 서브 디렉터리 확인
        $ tree terraform.tfstate.d
        # => terraform.tfstate.d
        #    └── mywork1
              
        # plan 시 새로운 작업공간이라서 별도의 상태파일을 사용하여 리소스를 생성하는 계획이 만들어집니다.
        $ terraform plan
        # => ...
        #    Plan: 1 to add, 0 to change, 0 to destroy.
              
        # apply 해보겠습니다.
        $ terraform apply -auto-approve
                    
        # 워크스페이스 확인
        $ terraform workspace list
        # =>   default
        #    * mywork1
              
        # 상태 파일을 확인합니다.
        # default(기본) 워크스페이스의 상태 파일은 terraform.tfstate이고, mywork1 워크스페이스의 상태 파일은 terraform.tfstate.d/mywork1/terraform.tfstate 로 생성됩니다.
              
        # default 워크스페이스의 상태 파일에서 ip 확인
        $ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 43.201.149.141          
              
        # mywork1 워크스페이스의 상태 파일에서 ip 확인
        $ cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 13.125.64.96
              
        # graph 확인
        $ terraform graph > graph.dot
                    
        # 새 작업 공간 workspace 생성 : mywork2
        $ terraform workspace new mywork2
              
        # 서브 디렉터리 확인
        $ tree terraform.tfstate.d
        # => terraform.tfstate.d
        #    ├── mywork1
        #    │   └── terraform.tfstate
        #    └── mywork2
              
        # plan & apply
        $ terraform plan && terraform apply -auto-approve
              
        # mywork2에서 apply 후 서브 디렉터리 확인
        $ tree terraform.tfstate.d
        # => terraform.tfstate.d
        #    ├── mywork1
        #    │   └── terraform.tfstate
        #    └── mywork2
        #        └── terraform.tfstate
              
        $ cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 43.201.149.141
        $ cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 13.125.64.96
        $ cat terraform.tfstate.d/mywork2/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
        # => 13.124.42.129
              
        # workspace 정보 확인
        $ terraform workspace show
        # => mywork2
        $ terraform workspace list
        # =>   default
        #      mywork1
        #    * mywork2
              
        # 실습 리소스 삭제
        $ terraform workspace select default
        $ terraform destroy -auto-approve
              
        $ terraform workspace select mywork1
        $ terraform destroy -auto-approve
              
        $ terraform workspace select mywork2
        $ terraform destroy -auto-approve      
        
    • 장점
      • 하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝하고 관리합니다.
      • 기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능합니다.
      • 깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리 가능합니다.
    • 단점
      • State가 동일한 저장소(로컬 또는 백엔드)에 저장되어 State 접근 권한 관리가 불가능합니다.
      • 모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생 가능이 있습니다.
      • 프로비저닝 대상에 대한 인증 요소를 완벽히 분리할 수 없습니다.
      • 가장 큰 단점은 완벽한 격리가 불가능하다는 것입니다.
        • 보완 방법
          1. 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃을 사용
          2. Terraform Cloud 환경의 워크스페이스를 활용

마치며

이번 주에는 프로바이더와 상태에 대해 보다 자세히 살펴보았습니다. 3주 동안 막연하게 사용하던 프로바이더와 상태에 대해 좀더 명확하게 이해할 수 있었습니다.

상태 관리에 대해서는 실무에서 팀, 회사 단위에서 테라폼을 사용할때 어려움을 많이 겪을 수 있겠다 라는 생각이 들었습니다. 반복된 교육과 사용으로 내제화 되기전에는 시행 착오를 많이 거쳐야할 것 같습니다. 이번 스터디를 통해서 상당수의 시행 착오는 단축할 수 있었던거 같습니다.

정말 좋은 스터디를 진행해 주시는 Gasida님과 실습을 만들어주신 악분님께 감사드립니다. :pray: