CLOUDWAVE

IaC : Terraform - 테라폼 상태관리 및 상태파일(tfstate)

갬짱 2025. 2. 7. 23:26

Terraform상태관리

terraform apply를 실행할때마다 생성한 리소스를 찾아 그에 따라 업데이트를 수행

terraform의 상태관리 : 상태정보를 저장한 파일의 내용(terraform.tfstate)과수행작업을 담은 main.tf파일을 비교하여 상태와 일치하지 않은 경우에만 작업을 수행 → 멱등성을 보장

( terraform.tfstate :: 멱등성을 위해 상태정보를 저장 )

 

업무 프로세스는 다양한 리소스의 상태파일(tfstate)을 공유

상태파일 공유저장소(동일한 상태파일에 접근)를 구축 → 상태파일 잠금기능(lock)으로 무결성 보장, 격리

git에 업로드하는 것은 권장하지 않음( 버전관리 서비스가 아닌 공유저장소 : S3 등 ⇒ 원격 백앤드가 지원됨 )

S3 : DB를 통한 잠금(lock)을 수행( DynamoDB ) → 트랜잭션 관리

 

Terrafrom 상태파일(tfstate)

  • Terraform을 실행할 때마다 Terraform 상태 파일에 생성된 인프라에 대한 정보가 기록 = Terraform 상태 파일(terraform.tfstate)은 terraform apply 명령어가 실행될 때마다 업데이트
  • 실제 리소스 표현으로의 매핑을 기록하는 사용자 지정 JSON형식, tfstate확장자
  • 상태파일은 비공개 API → 내부 용도로만 사용됨, 편집하거나 읽는것에 제한

[ Terraform 백엔드 ] : Terraform이 상태를 로드하고 저장하는 방법을 결정( 상태파일관리 시스템 )

  • 로컬 디스크에 상태파일을 저장하는 로컬 백엔드
  • 원격 백엔드 : 상태파일을 원격 공유 저장소에 저장 → Amazon S3, Azure Storage, Google Cloud Storage, HashiCorp의 Terraform Cloud와 Terraform Enterprise
    • 수동작업에 대한 오류 해소 : 해당 백엔드에서 상태파일을 자동으로 로드하고 테라폼 수행후 자동으로 저장
    • 잠금지원( 한 구성원이 apply를 실행중이라면 다른 구성원이 접근불가 ) 잠금없이 여러 구성원이 동시에 접근하는 경우 충돌, 데이터 손실 등을 초래
    • 상태파일 암호화 지원
    • 상태파일 격리 : 다양한 환경에 따라 인프라 격리

 

상태파일을 위한 공유 저장소

로컬로 존재하는 단일 파일에 상태저장 → 프로젝트 환경에서 팀 구성원이 공유위치의 동일한 상태파일에 엑세스

  • 버전 관리 저장소는 부적절( 수동작업 오류, 잠금지원X, 비밀화 불가 )
  • Amazon S3(Simple Storage Service)
    • 관리형 서비스로 추가 배포 및 관리 X, 강한 내구성과 가용성
    • DynamoDB를 통한 잠금지원 ( 트랜잭션을 통한 데이터 lock ) : S3와 상호호환성이 뛰어난 DynamoDB를 사용
    • 버전관리를 지원( 롤백가능 )

[ 실습 ] 상태파일 공유 저장소 구축

(1) ~/terraform-demo/storage의 main.tf : S3리소스, dynamoDB리소스 ⇒ 공유저장소 완성

캐시참조를 위해 .terraformrc파일 복사

+) terraform fmt : 형식 맞는지 확인

 

  • S3 버킷 리소스 생성
    • bucket명은 전역적으로 고유한 이름
    • 삭제방지 : 라이프사이클에 prevent_destroy 설정
      Terraform이 리소스를 삭제하지 않도록 강제함 ( terraform destory를 수행해도 콘솔로 직접 삭제하지 않으면 삭제되지 않음 )

[ +추가적인 보호기능 ]

  • aws_s3_bucket_versioning 리소스 : S3 버저닝 기능 버전관리 활성화하여 파일 업데이트마다 버전으로 관리 → 잘못된 데이터 입력시 이전버전으로 회귀가능
  • aws_s3_bucket_server_side_encryption_configuration 리소스 : S3 server_side 암호화 기능 기록된 모든 데이터에 대해 서버측 암호화를 활성화
  • aws_s3_bucket_public_access_block 리소스 : 버킷에 대한 퍼블릭 접근을 차단 상태파일의 민감데이터, secret이 포함되기에 외부 접근 차단 버킷 주소나 이름이 노출되더라도 외부에서 접근이 차단
  • aws_dynamodb_table 리소스 : 잠금에 사용할 dynamo DB생성 ( 분산 키-값 저장소 → 강력하게 일관된 읽기 및 조건부 쓰기를 지원 ) LockID라는 기본키가 있는 DynamoDB 테이블을 생성 ⇒ Terraform이 상태 파일 수정 시 해당 테이블의 LockID를 사용하여 락(Lock)을 걸고 작업
terraform {
	backend "s3" {
		bucket = "terraform-state-rudalsss-wave"
		key = "global/s3/terraform.tfstate"
		region = "ap-northeast-2"
		dynamodb_table = "terraform-locks"
		encrypt = true
	}
}

provider "aws" {
  region = "ap-northeast-2"
}
resource "aws_s3_bucket" "terraform_state" {
  bucket = "terraform-state-rudalsss-wave"
  # Prevent accidental deletion of this S3 bucket
  lifecycle {
    prevent_destroy = true
  }
}
resource "aws_s3_bucket_versioning" "enabled" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}
resource "aws_s3_bucket_public_access_block" "public_access" {
  bucket = aws_s3_bucket.terraform_state.id
  block_public_acls = true
  block_public_policy = true
  ignore_public_acls = true
  restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
  name = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key = "LockID"
  attribute {
    name = "LockID"
    type = "S"
  }
}

output "s3_bucket_arn" {
value = aws_s3_bucket.terraform_state.arn
description = "The ARN of the S3 bucket"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_locks.name
description = "The name of the DynamoDB table"
}

 

 

(2) 공유저장소(원격백엔드)에 상태저장을 사용할 테라폼 tf파일

S3 버킷에 상태를 저장하도록 Terraform을 구성하려면(암호화 및 잠금 포함) Terraform 코드에 백엔드 구성을 추가

terraform {
	backend "<BACKEND_NAME>" {
	[CONFIG...]
	}
}

BACKEND_NAME은 사용하려는 백엔드의 이름(예: "s3” )

S3는 디렉토리 구조가 아니라(평면구조) 키로 구성되어있는 항목

terraform {
	backend "s3" {
		bucket = "terraform-state-rudalsss-wave"
		key = "global/s3/terraform.tfstate"
		region = "ap-northeast-2"
		dynamodb_table = "terraform-locks"
		encrypt = true
	}
}

bucket : 사용할 S3 버킷의 이름

key : Terraform 상태 파일을 작성해야 하는 S3 버킷 내의 파일 경로

region : 버킷이 존재하는 AWS 리전

dynamodb_table : lock에 사용할 DynamoDB 테이블

 

** 원격 백엔드로 이용되는 S3 버킷과 DynamoDB 테이블 이름만 제대로 지정되면 작업 디렉터리 위치와 관계없이 Terraform 상태관리가 정상적으로 동작

 

(3) terraform init으로 Terraform 백엔드를 구성상태파일 복사

이미 로컬에 상태 파일이 있음을 자동으로 감지하고 이를 새 S3 백엔드로 복사하라는 메시지

 

출력변수로 S3 버킷의 Amazon 리소스 이름(ARN)과 DynamoDB 테이블의 이름을 출력

 

테라폼 제한사항

(1) Terraform을 사용하여 Terraform 상태를 저장할 S3 버킷을 생성할 때 닭과 달걀이 결정되는 상황

  • 생성 : 코드로 s3를 생성( terraform apply ) ⇒ 특정 테라폼의 local backend의 tfstate를 remote backend(S3)에 저장( backend구성이후 terraform init ) ⇒ 저장공간을 생성후 데이터를 저장
  • 원격 삭제 : 삭제먼저 한다면 데이터가 소실되는 문제 저장된 상태파일을 local backend에 재복사( backend구성제거이후 terraform init ) → 원격 저장소 인프라를 삭제( terraform destory, 콘솔삭제 )

(2) Terraform의 백엔드 블록에서는 변수나 참조를 사용할 수 없음 → 구성파일을 분리 .hcl
→ Terraform 모듈에 수동으로 복사하여 붙여넣기를 수행해야함

 

상태파일 환경격리

  • 개발(Dev), 테스트(Test), 운영(Prod) 등의 각 환경마다 별도의 상태 파일을 사용하여 서로 영향을 미치지 않도록 격리하는 방법
    • 인프라의 환경별 격리 필요성 : 스테이징 단계에서 앱의 새 버전을 배포하려고 시도하는 동안 프로덕션 단계 에서 앱이 중단될 수 있습니다
  • 단일 Terraform 구성세트에서 모든 환경을 관리하는 경우 격리가 수행되지 않음 → 환경별로 다른 Terraform 및 인프라 구성을 갖춤

 

(1) 작업공간(workspace) 기능으로 격리( git의 branch와 유사 ) ⇒ 동일한 구성에 대한 빠르고 격리된 테스트에 유용

(2) 파일레이아웃을 통한 격리 : 디렉토리를 구축하여 파일을 개별저장 → 더욱 가시적이고 직관적임 ⇒ 환경간 강력한 분리가 필요한 production 사례에서 많이 사용됨

 

(1) 작업공간을 통한 격리

Terraform workspace : 별도의 이름이 지정된 작업공간, 독립적인 Terrform 상태( 상태파일 tfstate )를 소유함

( 작업 공간을 명시적으로 지정하지 않으면 기본 작업 공간이 전체 시간 동안 사용됨 )

 

terraform workspace show

현재 workspace 목록확인 ( 기본 default )

 

cp -r storage workspace-demo

storage 디렉터리에서 정의된 원격 백엔드 설정을 workspace-demo에서 사용

 

terraform-demo/storage/main.tf

provider aws {}

resource "aws_instance" "workspace-test" {
 ami = "ami-024ea438ab0376a47"
 instance_type = "t2.micro"
}

terraform{
 backend "s3" {
  bucket = "terraform-state-rudalsss-wave"
  key = "workspaces-example/terraform.tfstate"
  region = "ap-northeast-2"
  dynamodb_table = "terraform-locks"
  encrypt = true
 }
}

 

key 값 : Terraform 상태 파일(terraform.tfstate)이 S3 버킷 내에 저장되는 경로(키)

 

terraform init -reconfigure

terraform apply -auto-approve

 

 

[ 워크스페이스 실습 ]

위에는 default 워크스페이스에서 수행하고 지정된 경로의 원격백엔드에 상태가 저장됨

새로운 워크스페이스 생성후에는 apply하면 별도의 원격백엔드에 상태가 저장됨 & 동일한 구성임에도 인스턴스가 하나 더 생성

 

terraform workspace new example1

terraform apply -auto-approve

 

terraform workspace new example2

terraform apply -auto-approve

 

범용버킷 terraform-state-rudalsss-wave > env:/ 하의 workspace

env: 폴더 내에는 각 작업 공간에 대해 하나씩 폴더가 생성됨

 

연속적으로 생성된 3개의 인스턴스

 

Terraform에서 새로운 워크스페이스를 생성하면 해당 워크스페이스의 상태 파일(terraform.tfstate)이 자동으로 지정된 S3 버킷에 저장

⇒ 환경 분리(dev/prod/test)에 매우 유용

key 경로가 워크스페이스에 맞춰 변경되어 정상적으로 동작

key = "envs/${terraform.workspace}/원래 지정경로"

workspace의 전환 = 상태파일이 저장된 경로(키)를 변경

 

(2) 파일레이아웃을 통한 격리

⇒ 인프라의 격리성 & 리소스 코드에 대한 명확한 이해와 파악

  • 각 "환경"에 대한 별도의 폴더( stage, prod .. ) → "구성 요소"에 대한 별도의 폴더 →각각 main.tf를 구축 ( ex. prod/vpc/main.tf )
  • 각 환경에 대해 서로다른 백엔드 구성 ( 서로다른 S3 버킷, 계정을 사용 )

 

 

main.tf를 분리 → 변수, 데이터 소스용 tf파일을 별도로 구축

⇒ 이는 terraform이 작업 디렉토리에서 모든 tf확장자를 모두 읽어들여서 실행(apply)하기에 가능함, 관리용이

[root@controller terraform-demo]# ls
run-test.sh  stage  storage  terraform.tfstate  terraform.tfstate.backup
[root@controller terraform-demo]# mkdir -p stage/services/webserver-cluster
[root@controller terraform-demo]# mv [main.tf](<http://main.tf/>) stage/services/webserver-cluster
  • tfstate파일은 s3저장소에서 공유하기에 별도로 복사하지 않아도됨
  • .terraformrc에 캐시를 설정하여 개별적인 .terraform 폴더를 구축하고 사용하지 않도록 함
    mkdir ~/.terraform.d/plugin-cache/
    vi ~/.terraformrc # plugin_cache_dir = "$HOME/.terraform.d/plugin-cache" 추가
    ​

[ 실습 ]

서버 클러스터 코드와 이 장에서 작성한 Amazon S3 및 DynamoDB 코드를 가져와 다음 그림의 폴더 구조를 사용하여 재정렬해보기

 

웹 서버 클러스터가 MySQL 데이터베이스와 통신하도록 구축 → 이때 별도의 디렉토리 data-stores/my-sql를 작업디렉토리로 이용하여 코드 관리 및 격리

 

 

terraform_remote_state 데이터 소스

데이터 소스 data.terraform_remote_state을 통해 테라폼 리소스의 상태파일을 가져오고 참조가능

( ↔ 데이터소스 aws_subnets는 AWS에서 읽기 전용 정보를 가져옴 )

  • 테라폼으로 프로비저닝된 리소스에 대한 상태를 저장하고 그 값을 가져오는 것과 같다( 테라폼 생성 리소스에 한함 )
  • 다른 작업 디렉터리나 워크스페이스에서 생성된 리소스의 상태 파일(tfstate)을 참조하여 리소스 정보를 가져오는 데 사용하는 Terraform 데이터 소스

[ 선언 ]

data "terraform_remote_state" "NAME" {
 backend = "s3"  # 원격 백엔드 유형
 config = {
  bucket         = "terraform-state-bucket"  # 상태 파일이 저장된 S3 버킷
  key            = "project-path/terraform.tfstate"  # 상태 파일 경로
  region         = "ap-northeast-2"  # 버킷 지역
  dynamodb_table = "terraform-locks" # (선택) Lock 관리 테이블
 }
}

config : S3내에서 해당 리소스의 상태파일을 찾기 위한 설정값

NAME : 리소스의 상태파일을 테라폼 코드내에서 참조하기 위한 이름

 

[ 사용 ]

data.terraform_remote_state.<NAME>.outputs.<ATTRIBUTE>

⇒ NAME을 가진 리소스의 속성 값을 참조하는 식

 

리소스 내부 속성들은 상태 파일에 있지만 직접 참조하기 어렵기 때문에 outputs속성을 사용하여 접근하는 것이 일반적 ( 이때 output 값은 현재 작업 디렉터리가 아닌, 리모트 상태 파일에 기록된 테라폼 코드에서 정의된 output 값 ) *참조된 상태 파일에서 정의된 output 들을 참조

 

[ 실습 ]

기존에 복사해온 main.tf의 내용 → db_instance를 추가하는 작업을 data-stores/mysql 디렉토리에서 진행 ( 격리된 디렉토리 환경을 구성( services ↔ data-stores ), 상태파일관리 )

[root@controller terraform-demo]# ls
[run-test.sh](<http://run-test.sh/>)  stage  storage  terraform.tfstate  terraform.tfstate.backup
[root@controller terraform-demo]# cd stage/
[root@controller stage]# mkdir data-stores/mysql -p

 

 

[ DB구축 전용 디렉토리(data-stores) ]

기능별로 tf파일을 분리하여 구축

 

(1) main.tf

 

stag/data-stores/mysql/main.tf

provider "aws" {
 region = "ap-northeast-2"
}
resource "aws_db_instance" "example" {
 identifier_prefix = "terraform-mysql"
 engine = "mysql"
 allocated_storage = 10
 instance_class = "db.t3.micro"
 skip_final_snapshot = true
 db_name = "example_database"
 # How should we set the username and password?
 username = var.db_username
 password = var.db_password
}

variable "db_username" {
 description = "The username for the database"
 type = string
 sensitive = true
}
variable "db_password" {
 description = "The password for the database"
 type = string
 sensitive = true
}

 

  • mysql DB인스턴스 리소스를 생성 → 이때 db_username, db_password라는 입력변수로 입력값을 받아서 만들어서 속성값으로 활용 ( 비번은 8자리 이상으로 지정 )

  • 환경변수를 이용하는 방법 : TF_VAR_db_username 및 TF_VAR_db_password를 설정( export )

 

(2) 백앤드 구성 추가

terraform.tf라는 새로운 tf파일을 구성하여 구축하게 함

 

stag/data-stores/mysql/terraform.tf

terraform {
 backend "s3" {
 # Replace this with your bucket name!
  bucket = "terraform-state-rudalsss"
  key = "stage/data-stores/mysql/terraform.tfstate"
  region = "ap-northeast-2"
 # Replace this with your DynamoDB table name!
  dynamodb_table = "terraform-locks"
  encrypt = true
 }
}

 

 

(3) 출력변수(output)정의 → 생성된 리소스( AWS RDS 인스턴스 )에 대한 정보 출력

 

stag/data-stores/mysql/output.tf

output "address" {
 value = aws_db_instance.example.address
 description = "Connect to the database at this endpoint"
}

output "port" {
 value = aws_db_instance.example.port
 description = "The port the database is listening on"
}

⇒ 추후에 해당 리소스를 remote_state 데이터 소스로 참조할때 리소스 속성으로 활용될 수 있다.

 

(4) DB 생성

terraform init, terraform apply ⇒ DB가 생성된다

기본적으로 백앤드의 동작은 lock을 잡고 terraform apply시에 리소스의 ftstate를 관리( 이떄 lock파일을 잡고 격리적으로 수행 )

그러나 interrupt와 같이 비정상종료를 하게 되면 lock파일이 잡혀있는 것으로 인식 ⇒ 강제적으로 terraform apply 시에 옵션으로 -lock=false로 lock을 무시하게끔한다

** Terraform 백엔드 동작: terraform apply 시 lock 파일을 잡아 상태 파일(tfstate)을 관리합니다. 이는 여러 사용자가 동시에 상태 파일을 변경하지 못하도록 보장하는 중요한 메커니즘입니다.

 

user : rudals

pwd : 12345678

로 지정하여 데이터베이스 생성

생성되면 address가 반환된다 ( endpoint = address + port )

DB관련 tfstate파일이 생성됨

지정된 저장위치 ⇒ s3://terraform-state-rudalsss-wave/stage/data-stores/mysql/terraform.tfstate

 

(5) web-cluster에서, 생성한 DB에 대한 데이터소스의 상태파일을 terraform_remote_state로 가져와서 활용하기

 

5-1) 스크립트 환경에서(template함수를 이용하지 않고 userdata구성시) terraform_remote_state의 값에 대한 랜더링 처리

/root/terraform-demo/stage/services/webserver-cluster 의 내용변경

terraform_remote_state 데이터 소스에서 데이터베이스 주소와 포트를 가져오고 해당 정보를 HTTP 응답에 노출

데이터소스를 참조하는 식 → 랜더링작업이 필요함 ${{ }}

 

stag/services/webserver-cluster/main.tf

...

resource "aws_launch_configuration" "example" {
 image_id        = "ami-024ea438ab0376a47"
 instance_type   = var.instance_type
 security_groups = [aws_security_group.instance.id]
 user_data       = <<-EOF
 #!/bin/bash
cat > index.html <<EOF
<h1>Hello, World</h1>
<p>DB address: ${data.terraform_remote_state.db.outputs.address}</p>
<p>DB port: ${data.terraform_remote_state.db.outputs.db_port}</p>
nohup busybox httpd -f -p ${server_port} &
EOF
 lifecycle {
  create_before_destroy = true
 }
}

...

data "terraform_remote_state" "db" {
 backend = "s3"
 config = {
  bucket = "terraform-state-rudalsss-wave"
  key = "stage/data-source/mysql/terraform.tfstate"
  region = "ap-northeast-2"
 }
}

생성되는 인스턴스들에 테라폼을 이용하여 생성해두었던 RDS정보를 가져와서 연결하는 설정

 

 

 

data.terraform_remote_state.db.outputs.address

data.terraform_remote_state.db.outputs.port

stag/data-stores/mysql/output.tf에서의 지정했던 출력값들을 속성으로 이용

 

+) terraform init 실행 결과 추가 정리

terraform init명령어의 역할 : provider plugin 다운로드, backend 로드, module구성

즉, 공급자를 설치하고, 백엔드를 구성하고, 모듈을 다운로드하는 등 모두 init 이라는 하나의 편리한 명령으로 수행 Initializing modules... Initializing the backend... Initializing provider plugins... Terraform has been successfully initialized!

 

+) templatefile 내장 함수

[ Terraform의 내장함수 형식 ]

함수명( 매개변수, 처리대상값 )

# format function
format(<FMT>, <ARGS>, ...)

# template function
templatefile(<PATH>, <VARS>)

templatefile 함수 : PATH 에서 파일을 읽고, 이를 템플릿으로 렌더링하고, 결과를 문자열로 반환

파일을 읽어서 해당 변수를 치환

⇒ 스크립트를 간결하게 작성가능, Bash 스크립트를 인라인으로 작성하는 것보다 간결함

 

이전상태( 스크립트내부에서 직접 정의 )

resource "aws_launch_configuration" "example" {
  image_id        = "ami-024ea438ab0376a47"
  instance_type   = "t3.micro"
  security_groups = [aws_security_group.instance.id]
  user_data       = <<-EOF
 #!/bin/bash
 echo "Hello, World" > index.html
 echo "${data.terraform_remote_state.db.outputs.address}" >> index.html
 echo "${data.terraform_remote_state.db.outputs.port}" >> index.html
 nohup busybox httpd -f -p ${var.server_port} &
 EOF
 lifecycle {
  create_before_destroy = true
 }
}

 

 

랜더링 대상이되는 스크립트를 따로 분리( user_data.sh )

template함수에서 해당 스크립트의 경로와, 내부의 변수에 대한 정의를 매개변수로 지정

스크립트 내의 변수는 접두사가 필요하지 않음 ( ${var.server_port} 가 아닌 ${server_port} 를 사용 )

resource "aws_launch_configuration" "example" {
  image_id        = "ami-024ea438ab0376a47"
  instance_type   = "t3.micro"
  security_groups = [aws_security_group.instance.id]
  
  user_data = templatefile("user-data.sh", {
   server_port = var.server_port
   db_address = data.terraform_remote_state.db.outputs.address
   db_port = data.terraform_remote_state.db.outputs.port
  })
 lifecycle {
  create_before_destroy = true
 }
}