Terraform Study #4(State, Module, 협업)

오늘은 Terraform state, Module 그리고 협업에 대해 간략하게 알아볼 예정이다. IaC는 협업을 하기 위함임을 생각하면 오늘 배우는 내용이 중요할 거 같다.

1. State

1.1 State의 목적과 의미

State의 주요 목적과 의미는 다음과 같다.
현재 인프라 상태의 추적: State 파일은 Terraform 구성이 실제 클라우드 인프라에 어떻게 매핑되는지에 대한 정보를 포함한다. 이를 통해 Terraform은 다음 실행 시 어떤 리소스를 생성, 수정, 삭제할지 결정할 수 있다.
변경의 빠른 감지: Terraform은 State 파일을 기반으로 현재 인프라 상태와 구성의 차이점을 판단한다. 이를 통해 Terraform은 최소한의 변경만을 적용하여 인프라를 원하는 상태로 만든다.
출력 변수 저장: Terraform 출력 변수는 State에 저장된다. 이를 통해 다른 Terraform 구성에서 이 값을 참조할 수 있다.
팀과의 협업: 원격 상태 저장소를 사용하면 여러 팀원이 동일한 인프라에 대해 Terraform을 실행할 때 State의 일관성과 동기화를 유지할 수 있다.
리소스 의존성 관리: State를 통해 Terraform은 리소스 간의 의존성을 파악하고, 리소스를 올바른 순서로 생성, 수정, 삭제한다.
드리프트 감지: State 파일을 사용하여 드리프트를 감지하고, 필요한 경우 Terraform을 통해 원하는 상태로 되돌릴 수 있다.
요약하면, State는 Terraform에서 중심적인 역할을 하는 요소로, 실제 클라우드 인프라와 Terraform 구성 간의 차이점과 연관성을 추적하고 관리하는 데 필요하다.

이런 중요한 State에는 몇 가지 조건이 필요하다.
정확성: State 파일은 항상 현재 인프라의 정확한 표현이어야 한다. State와 실제 리소스 간의 불일치는 문제를 일으킬 수 있으므로, State는 최신 상태를 유지해야 한다.
접근 제한: State 파일은 민감한 정보(예: 비밀번호, 엑세스 키 등)를 포함할 수 있기 때문에 적절한 접근 제어가 필요하며, 필요한 팀원만 접근할 수 있도록 해야 한다.
동기화: 여러 개발자나 시스템 관리자가 동일한 인프라에 작업할 경우 원격 상태 저장소를 사용하여 State를 중앙에서 관리하고, 동시에 발생하는 변경을 피하도록 구성해야 한다.
백업: State 파일은 중요한 데이터를 포함하므로, 정기적으로 백업해야 하고, 이를 통해 잘못된 변경이나 손상된 경우 이전 상태로 복구할 수 있다.
버전 관리: 원격 상태 저장소에서는 State 파일의 버전 관리 기능을 활용할 수 있다. AWS S3의 경우 Object Versioning을 활성화하여 State의 변경 이력을 추적할 수 있다.
잠금 Mechanism: Terraform은 리소스 생성 및 수정 작업 중에 state 파일을 잠글 수 있는 기능을 제공한다. 이렇게 하면 동시에 여러 사용자가 같은 state 파일을 변경하는 것을 방지할 수 있다.
암호화: 위의 접근 제한과 마찬가지로 중요 정보를 담고 있을 수 있기 때문에 저장소에 저장되는 상태 파일은 암호화되어야 한다. AWS S3를 원격 상태 저장소로 사용하는 경우, S3의 서버 측 암호화를 활용하여 데이터를 암호화할 수 있다.

1.2 State 동기화

테라폼 구성 파일은 기존 State와 구성을 비교해 실행 계획에서 생성, 수정, 삭제 여부를 결정한다.

https://kschoi728.tistory.com/135

Terraform 액션에 따라 State에 어떤 동작이 발생하는지 유형 별로 확인해보도록 하겠다.

테라폼 구성과 State 흐름 : Plan 과 Apply 중 각 리소스에 발생할 수 있는 네 가지 사항, 아래 실행 계획 출력 기호와 의미

기호의미
+Create
Destroy
-/+Replace
~Updated in-place

Replace 동작은 기본값을 삭제 후 생성하지만 lifecycle의 create_before_destroy 옵션을 통해 생성 후 삭제 설정 가능

1.3 워크스페이스

Terraform의 워크스페이스는 Terraform 구성의 여러 버전 또는 세트를 관리하도록 도와주는 기능이다. 워크스페이스를 사용하면 동일한 구성으로 여러 다른 환경(예: 개발, 스테이징, 프로덕션 등)을 관리할 수 있다.
워크스페이스의 주요 특징 및 사용 사례는 몇 가지가 있다.
환경 분리: 워크스페이스는 다양한 환경을 분리된 상태로 유지하는 데 도움이 된다. 예를 들어 dev, staging, prod와 같은 워크스페이스를 만들어 환경별로 다른 변수나 리소스를 적용할 수 있다.
독립적인 State 관리: 각 워크스페이스는 자체적인 Terraform state 파일을 갖게 된다. 따라서, 한 환경에서의 변경이 다른 환경의 state에 영향을 주지 않는다.
변수 재사용: 워크스페이스별로 변수를 재정의하면서 동일한 Terraform 코드를 재사용할 수 있다. 예를 들어, 각 환경에 대한 다른 서브넷이나 크기의 VM을 지정할 수 있다.
워크스페이스 명령어는 몇 가지가 있다.
terraform workspace new [workspace-name]: 새로운 워크스페이스를 생성한다.
terraform workspace select [workspace-name]: 특정 워크스페이스로 전환한다.
terraform workspace list: 사용 가능한 모든 워크스페이스를 나열한다.
terraform workspace show: 현재 선택된 워크스페이스를 표시한다.

간단한 실습으로 State와 워크스페이스를 확인해본다.

# workspace 확인
terraform workspace list
* default

# 간단 EC2 배포 main.tf
resource "aws_instance" "mysrv1" {
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
  tags = {
    Name = "t101-week4"
  }
}

# Terraform 배포 후 state 확인
terraform init && terraform apply -auto-approve
terraform state list
aws_instance.mysrv1

# Public/Private IP 확인
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.39.231.x
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.private_ip'
172.31.39.105
cat terraform.tfstate | jq -r '.resources[0].instances[0].private' | base64 -d | jq
{
  "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
    "create": 600000000000,
    "delete": 1200000000000,
    "update": 600000000000
  },
  "schema_version": "1"
}

새로운 워크스페이스를 만들어서 apply을 진행해봤다. workspace가 새로 생기면 state도 새로 생기고 개별로 구성되는 것을 확인할 수 있다.

# 새 워크스페이스 생성
terraform workspace new mywork1
terraform workspace list
  default
* mywork1
terraform workspace show
mywork1

tree terraform.tfstate.d
terraform.tfstate.d
└── mywork1
2 directories, 0 files

# plan 할 경우 기존 워크스페이스라면 create가 표시되지 않아야 하나 새로운 워크스페이스(mywork1)이 선택되어 있어서 create로 표시
terraform plan

# apply을 하면 plan 내용처럼 배포가 진행 된다.
terraform apply -auto-approve

# 워크스페이스 확인
terraform workspace list
  default
* mywork1

# State 확인하면 기존 배포한 것과 새로 배포한 것의 Public IP가 차이가 난다.
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.39.231.x
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
15.164.x.x

# 실습 리소스 삭제
terraform workspace select default
terraform destroy -auto-approve
terraform workspace select mywork1
terraform destroy -auto-approve

워크스페이스의 장단점은 아래와 같다.

장점
하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝하고 관리
기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능
깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리

단점
State가 동일한 저장소에 저장되어 State 접근 권한 관리가 불가능(어려움)
모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생 가능
프로비저닝 대상에 대한 인증 요소를 완벽히 분리하기 어려움-> 가잔 큰 단점은 완벽한 격리가 불가능
-> 해결방안 1. 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃 사용
-> 새결방안 2. Terraform Cloud 환경의 워크스페이스를 활용

2. 모듈

Terraform 모듈은 Terraform 코드의 집합이다. 이를 사용하면 코드를 재사용하고, 구조화하며, 공유할 수 있다. 기본적으로 모듈은 Terraform의 스크립트나 구성을 논리적 단위로 캡슐화하는 방법이다. 모듈을 사용하여 공통 구성 요소를 쉽게 재사용하고 관리할 수 있다.
Terraform 모듈의 주요 특징 및 용도는 아래와 같다.
코드 재사용: 모듈은 반복되는 코드 패턴을 줄이기 위해 공통 리소스나 구성을 캡슐화한다. 예를 들어, 여러 프로젝트나 환경에서 동일한 VPC, 보안 그룹, EC2 인스턴스 설정을 사용해야 할 때 모듈로 정의하면 이를 쉽게 재사용할 수 있다.
논리적 구조: 모듈을 사용하여 복잡한 Terraform 코드를 관리하기 쉬운 논리적 단위로 분리할 수 있다. 이렇게 하면 코드의 가독성과 유지 관리성이 향상된다.
변수 및 출력: 모듈은 입력 변수를 통해 매개 변수화되고, 출력 변수를 통해 다른 Terraform 구성 또는 모듈에 데이터를 반환할 수 있다.
버전 관리: Terraform 모듈은 Git과 같은 소스 코드 관리 시스템에 저장되어 버전 관리될 수 있다. 또한, Terraform Registry나 다른 버전화된 저장소를 사용하여 공유 및 배포될 수 있다.
안정성과 표준화: 모듈을 사용하면 안정적이고 표준화된 인프라 구성 요소를 제공할 수 있다. 특히 큰 팀이나 여러 프로젝트에서 일관된 인프라 구성을 유지하기 위해 유용하다.

module "vpc" {
  source  = "aws/vpc/aws"
  version = "2.0.0"
  ...
}

간단한 예제로 위의 예제에서는 AWS VPC 모듈을 사용하여 VPC를 생성하며, 해당 모듈은 공식 Terraform Registry에서 가져온다
모듈을 사용하면 Terraform 코드의 재사용성과 유지 관리성을 크게 향상시킬 수 있다. 여러 프로젝트나 환경 간에 일관된 인프라 코드를 유지하기 위한 기본 도구로 간주된다.

간단한 모듈화 테스트를 해보려고 한다. VPC을 만드는 모듈을 사용해볼 예정이다.
먼저 my_vpc_module라는 디렉터리를 만들고 모듈 코드를 작성한다.

#my_vpc_module/main.tf
resource "aws_vpc" "ybs-vpc" {
  cidr_block = var.cidr_block
  tags = {
    Name = var.vpc_name
  }
}

output "vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "vpc_name" {
  description = "Name tag for the VPC"
  type        = string
}

루트 디렉토리에 main.tf을 만들어서 vpc 모듈을 호출한다.

module "my_vpc" {
  source    = "./my_vpc_module"
  cidr_block = "10.0.0.0/16"
  vpc_name   = "ybs-test-vpc"
}

output "created_vpc_id" {
  value = module.my_vpc.vpc_id
}

해당 모듈을 호출하는 테스트를 진행해본다. 잘 생성되는 것과 더불어 vpc 이름까지 잘 적용된 것을 확인할 수 있다.

#terraform init 및 Plan/Apply 실행
terraform init && terraform apply -auto-approve

#terraform state 확인
terraform state list                          
module.my_vpc.aws_vpc.ybs-vpc

조금 더 심화 내용으로 모듈을 2개 사용하는 실습을 진행할 예정이다. VPC 모듈과 EC2 모듈을 각각 작성하고 두 모듈을 호출해서 리소스를 생성할 예정이다.

먼저 vpc 폴더를 만들고 main.tf로 vpc 모듈을 작성한다.

#vpc/main.tf
resource "aws_vpc" "ybs-vpc" {
  cidr_block = var.cidr_block
  tags = {
    Name = var.vpc_name
  }
}

resource "aws_subnet" "ybs-subnet" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = var.subnet_cidr_block
  availability_zone = var.availability_zone
  tags = {
    Name = "${var.vpc_name}-subnet"
  }
}

output "vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

output "subnet_id" {
  value = aws_subnet.ybs-subnet.id
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
 
}

variable "vpc_name" {
  description = "VPC Name"
  type = string
 
}
variable "subnet_cidr_block" {
  description = "CIDR block for the Subnet"
 
}

variable "availability_zone" {
  description = "The availability zone where the subnet will be created"
  type = string
}

EC2도 마찬가지로 폴더를 만들고 모듈을 작성한다.

#ec2/main.tf
resource "aws_instance" "ybs-ec2" {
  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  tags = {
    Name = var.instance_name
  }
}

output "instance_id" {
  value = aws_instance.ybs-ec2.id
}

variable "ami_id" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "Instance type for the EC2 instance"
  type        = string
}

variable "subnet_id" {
  description = "Subnet ID where the EC2 instance will be launched"
  type        = string
}

variable "instance_name" {
  description = "Name tag for the EC2 instance"
  type        = string
}

두 모듈을 호출하는 main.tf를 루트 디렉토리에 만든다.

#main.tf
module "my_vpc" {
  source    = "./vpc"
  cidr_block = "10.0.0.0/16"
  vpc_name   = "MyVPC"
}

module "my_ec2" {
  source         = "./ec2"
  ami_id         = "ami-0123456789abcdef0" # 예시 AMI ID
  instance_type  = "t2.micro"
  subnet_id      = module.my_vpc.vpc_id # VPC 모듈에서 생성된 VPC의 ID를 사용
  instance_name  = "MyInstance"
}

output "created_vpc_id" {
  value = module.my_vpc.vpc_id
}

output "created_instance_id" {
  value = module.my_ec2.instance_id
}

모듈을 실행시키면 VPC와 EC2가 제대로 만들어지는 걸 확인할 수 있다.

#terraform init 및 Plan/Apply 실행
terraform init && terraform apply -auto-approve

#terraform state 확인
terraform state list 
module.my_ec2.aws_instance.ybs-ec2
module.my_vpc.aws_subnet.ybs-subnet
module.my_vpc.aws_vpc.ybs-vpc      

3.협업

인프라 규모가 커지고 관리 팀원이 늘어날 수록 구성 코드 관리가 필요하다. → 서로 작성 코드 점검 및 협업 환경 구성

구성 요소 : 코드를 다수의 작업자가 유지 보수 할 수 있도록 돕는 VCS Version Control System + 테라폼 State를 중앙화하는 중앙 저장소

https://kschoi728.tistory.com/139

유형 1 : VCS, 중앙 저장소 없음

  • 동일한 대상을 관리하는 여러 작업자는 동일한 프로비저닝을  위해 각자 자신이 작성한 코드를 수동으로 공유가 필요
  • 작업자의 수가 늘어날수록 코드 동기화는 어려워지고, 각 작업자가 작성한 코드를 병합하기도 어렵다 → VCS 도구 도입 고민 시점

유형 2 : VCS(SVN, Git), 중앙 저장소 도입

  • 형성관리 도구를 통해 여러 작업자가 동일한 테라폼 코드를 공유해 구성 작업
    • 변경 이력 관리 및 이전 버전으로 롤백 가능
  • 공유파일은 테라폼 구성파일과 State ← 테라폼 프로비저닝의 결과물로 데이터 저장소와 같음
  • 작업자가 서로 다른 프로비저닝한 State 결과를 공유를 위해서 백엔드(공유 저장소) 설정을 제공

유형 3 : VCS(Github), 중앙 저장소 도입

  • 테라폼 코드 형상관리를 위한 중앙 저장소 + State 백엔드 → 작업자는 개별적으로 프로비저닝을 테스트하고 완성된 코드를 공유
    • State 는 중앙 관리되어, 작업자가 프로비저닝을 수행하면 원격 State의 상태를 확인하고 프로비저닝 수행

4. 정리

협업은 아직 실습까진 진행하지 않았고 기초적인 이론만 살펴보았다. IaC는 궁극적으로 협업을 하기 위한 도구에 지나지 않는다고 생각한다. 그 협업을 하기 위해 State 관리가 필요하고 모듈을 세분화해서 관리하는 게 유리하기 때문에 State와 모듈에 대해 자세히 알아볼 필요가 있는 것 같다.
다음 시간에는 협업에 대한 실습을 상세히 진행해보면 좋을 거 같다.