들어가며
이번 주차에는 State와 Module에 대한 스터디를 진행했습니다. State는 지난 스터디들에서 계속 언급이 되고, 직접 확인을 했던 요소이다 보니 익숙했습니다. Module은 기존에 만들던 WAS에 많이 적용되어 있는 내용이라 친숙했고, 또 실무에서 주의 깊게 고민하고 있는 이슈인 재사용성 역시 올려줄 수 있는 것이라 정확히 이해하고 넘어가는 것이 중요하다고 생각했습니다.
* 본 스터디의 자료는 아래의 책을 기반으로 합니다.
테라폼으로 시작하는 IaC | 김민수, 김재준, 이규석, 이유종 | 링크
State
테라폼은 어떤 리소스를 관리하고 있는지, 그리고 그 리소스의 현재 상태가 어떤지를 관리합니다. 이러한 리소스 상태를 관리하는 파일은 tfstate와 tfstate.backup 입니다.
State (terraform.tfstate)
- 이 파일은 테라폼이 관리하는 리소스의 현재 상태를 JSON 형식으로 저장합니다.
- 테라폼 명령을 실행하게 되면 이 파일이 업데이트됩니다.
- 이 파일을 통해 실제 인프라와 코드 간의 차이점을 파악하고 필요한 변경 사항을 파악합니다.
State Backup (terraform.tfstate.backup)
- terraform.tfstate 파일을 수정하기 전에 현재 상태 파일의 백업을 생성합니다.
- tfstate 파일에 문제가 생기면 이 백업 파일을 사용하여 상태를 복원할 수 있습니다.
- 일반적으로 이 백업 파일은 최근 상태의 이전 버전을 포함하게 됩니다.
요약하자면 다음과 같은 도식으로 표현할 수 있습니다.
State의 목적과 의미
State의 목적과 의미를 알기 위해 다음의 실습을 진행합니다.
# 최초 생성
cat <<EOT > vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.10.0.0/16"
tags = {
Name = "t101-study"
}
}
EOT
# 실행
terraform init && terraform plan && terraform apply -auto-approve
# 상태 파일 확인
cat terraform.tfstate | jq | grep serial
# 내용 업데이트 #1
cat <<EOT > vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.10.0.0/16"
tags = {
Name = "t101-study"
}
}
EOT
# 실행
terraform init && terraform plan && terraform apply -auto-approve
# 상태 파일 확인
cat terraform.tfstate | jq | grep serial
# 상태 파일 확인
cat terraform.tfstate | jq | grep serial
# 내용 업데이트 #2
cat <<EOT > vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.10.0.0/16"
tags = {
Name = "tf-state-tag-change"
}
}
EOT
# 실행
terraform init && terraform plan && terraform apply -auto-approve
# 상태 파일 확인
cat terraform.tfstate | jq | grep serial
실행 결과는 다음과 같습니다.
앞서 설명드렸던 것과 같이 이 백업 파일은 최근 상태의 이전 버전을 포함하게 되기 때문에 이전 버전의 serial이 tfstate.backup에 기록됩니다. 실습으로 확인한 State 변경을 정리하자면 아래의 그림과 같습니다.
State 관리 시 주의사항
테라폼의 상태파일을 직접 편집하거나 직접 읽는 코드를 작성해서는 안됩니다. 테라폼 코드를 팀원과 동시에 업데이트하여 충돌하는 상황이 되거나, 예기치 못한 인프라 롤백 및 복제의 위험이 있습니다. 또한 시크릿 정보가 다른 사람에게 노출이 될 수 있기 때문입니다.
그렇기 때문에 원격 백엔드를 이용하여 State를 관리하는 것이 좋다고 합니다. 원격 백엔드를 이용하면 다음과 같은 기능을 통해 위에서 설명한 문제점들을 예방할 수 있습니다.
- 수동 오류 해결 : plan/apply 실행 시마다 해당 백엔드에서 파일을 자동을 로드, apply 후 상태 파일을 백엔드에 자동 저장합니다.
- 잠금(Lock) : apply 실행 시 테라폼은 자동으로 잠금을 활성화, -lock-timout=<TIME> 로 대기 시간을 설정할 수 있습니다.
- 시크릿 : 대부분 원격 백엔드는 기본적으로 데이터를 보내거나 상태 파일을 저장할 때 암호화(Encryption)하는 기능을 지원합니다.
다음은 다양한 종류의 Terraform State Backend입니다.
[출처] - 링크
실제로 워크숍에서 state파일을 훼손하여 인프라를 수동으로 복구한 사례를 들으면서 관리를 잘해야겠다는 생각이 강력히 들었습니다.
State 동기화
테라폼의 구성 파일은 기존 State와 구성을 비교해 실행 계획에서 생성, 수정, 삭제 여부를 결정합니다. 이는 스터디 2기 구구달스 님께서 작성하신 도식과 같은 형태로 진행됩니다.
[출처] - 링크
이 그림에 대해서 설명하자면 다음과 같습니다.
1. tf, tf.state 파일을 배교합니다.
2. terraform plan 명령어를 실행하면 refresh 과정이 자동으로 실행되며 실제로 클라우드 환경에 배포된 리소스와 비교합니다. 여기서 terraform plan -refresh=false를 실행하면 클라우드 리소스와 비교하는 과정을 생략하여 응답속도를 더욱 빨리 만들 수 있습니다.
인프라를 정의하는 소스코드와 State 그리고 실제 리소스들의 관계에서 다음과 같은 6가지 유형에 대해서 정리했고, 이를 직접 실습을 통해 확인해 보았습니다.
유형 | 리소스 (코드) | State 데이터 | 실제 배포된 리소스 | 예상 동작 |
1 | 있음 | 리소스 생성 | ||
2 | 있음 | 있음 | 리소스 생성 | |
3 | 있음 | 있음 | 있음 | 동작 없음 |
4 | (일부 삭제) | 있음 | 있음 | 리소스 삭제 |
5 | 있음 | |||
6 | 있음 | 있음 |
유형 1 - 신규 리소스 정의
locals {
name = "mytest"
}
resource "aws_iam_user" "myiamuser1" {
name = "${local.name}1"
}
resource "aws_iam_user" "myiamuser2" {
name = "${local.name}2"
}
terraform init && terraform apply -auto-approve
aws iam list-users | jq
유형 2 - 실제 리소스 수동 제거
aws iam delete-user --user-name mytest1
aws iam delete-user --user-name mytest2
terraform plan
terraform plan -refresh=false
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
terraform state list
cat terraform.tfstate | jq .serial
terraform plan -refresh=false 입력 시 클라우드 리소와 비교하지 않아 새로 리소스가 생성되는 것처럼 보입니다.
실제로 배포를 수행해도 클라우드 리소스가 사라졌음을 확인하고 동일하게 생성되는 것을 확인할 수 있습니다.
유형 3 - 코드와 State 데이터, 실제 배포된 리소스 모두 일치
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
유형 4 - 코드에서 일부 리소스 삭제
locals {
name = "mytest"
}
resource "aws_iam_user" "myiamuser1" {
name = "${local.name}1"
}
유형 5 - 실제 배포된 리소스만 있을 경우
import 또는 신규 코드 작성이 필요합니다.
유형 6 - 실수로 tfstate 파일 삭제
rm -rf terraform.tfstate*
terraform plan
terraform plan -refresh=false
terraform apply -auto-approve
terraform state list
cat terraform.tfstate | jq
terraform plan을 수행하게 되면 state 파일이 없기 때문에 두 명령 모두 새로운 리소스 추가를 한다고 표시됩니다.
하지만 실제로 리소스를 배포하게 되면 다음과 같이 이미 배포된 리소스이므로 에러가 발생하게 됩니다.
워크스페이스 (Work Space)
테라폼 워크 스페이스는 테라폼의 상태를 분리하여 여러 환경의 인프라를 관리할 수 있도록 해주는 기능입니다. 워크스페이스를 사용하면 코드를 기반으로 개발, 스테이징, 프로덕션 등의 다양한 환경을 관리할 수 있습니다. 워크스페이스 명령을 사용하게 되면 기본적으로 로컬환경(로컬 백엔드)에서 워크스페이스별로 각각의 state를 관리하게 됩니다.
로컬 백엔드를 사용하면 작업 디렉터리에 terraform.tfstate.d 라는 디렉터리가 생성되고, 이 디렉토리 내부에 워크스페이스 이름별로 다음과 같이 하위 디렉토리가 만들어집니다.
개발 워크 스페이스 : terraform.tfstate.d/dev/terraform.tfstate
프로덕션 워크 스페이스 : terraform.tfstate.d/prod/terraform.tfstate
워크스페이스의 특징은 다음과 같습니다.
- 각 워크스페이스는 고유한 terraform.tfstate 파일을 가짐으로써 독립적으로 인프라를 관리합니다.
- 워크스페이스를 통해 개발, 테스트, 프로덕션과 같은 다양한 환경을 분리하고 관리할 수 있습니다.
- 워크스페이스별로 다른 변수 값을 설정하여 동일한 코드에 다른 파라미터를 정의할 수 있습니다.
워크스페이스는 다음과 같이 사용합니다.
- 생성: terraform workspace new [name]
- 선택: terraform workspace select [name]
- 리스트 조회: terraform workspace list
환경을 격리하기 위해서는 워크스페이스를 이용하면 되지만, 기본적으로 로컬 백엔드로 관리가 될 경우 협업 및 관리적 측면에서 다양한 문제가 발생할 수 있습니다. 그래서 실제 운영환경에서는 위에서 언급했던 여러 가지 종류의 외부 백엔드를 이용하여 관리하는 것이 현명하다고 할 수 있습니다. 스터디에서는 각 환경별로 별도 계정의 백엔드 서비스를 이용하는 사례가 소개되었습니다.
또한 파일 레이아웃을 이용한 경리방식도 있는데, 각 환경마다 분리하여 폴더 형태로 관리하는 것입니다. 스터디에서 좋은 예시를 들어주었습니다.
stage (사전 프로덕션 워크로드)
└ vpc (해당 환경을 위한 네트워크 토폴로지)
└ services (해당 환경에서 서비스되는 애플리케이션)
└ frontend
└ backend
└ data-storage (환경별 데이터 저장소)
└ mysql
└ redis
prod (프로덕션 워크로드)
└ vpc
└ services
└ frontend
└ backend
mgmt (데브옵스 도구용)
└ vpc
└ services
└ bestion-host
└ jenkins
global (모든 환경에서 사용되는 리소스)
└ iam
└ s3
또한, 이 예시와 함께 리소스에 대한 네이밍 컨벤션의 예시에 대해서도 사례를 들어주셨습니다.
- variables.tf : 입력 변수
- outputs.tf : 출력 변수
- main-xxx.tf : 리소스 → 개별 테라폼 파일 규모가 커지면 특정 기능 기준 별도 파일로 분리 (ex. main-iam.tf, main-s3.tf 등) 혹은 모듈 단위로 나눔
- dependencies.tf : 데이터 소스
- providers.tf : 공급자
테라폼 클라우드를 통해 다음과 같이 워크스페이스를 구축할 수 있습니다.
모듈
모듈은 테라폼으로 인프라를 관리하게 되면서 리소스가 증가하고 구성이 복잡해지는 상황을 관리하기 위해 사용합니다.
일반적인 프로그래밍에서 볼 수 있는 모듈 개념과 동일하다고 이해할 수 있습니다.
테라폼의 모듈은 루트 모듈과 자식 모듈로 구분이 됩니다. 이중 루트 모듈은 테라폼을 실행하고 프로비저닝 하는 최상위 모듈로써 이하 많은 자식 모듈의 리소스를 호출하여 프로비저닝 하게 됩니다.
[참고] - 링크
모듈은 다음과 같은 특성을 가집니다.
- 관리성 : 모듈은 서로 연관 있은 구성의 묶음입니다. 원하는 구성요소를 단위별로 쉽게 찾고 업데이트할 수 있다. 모듈은 다른 구성에서 쉽게 하나의 덩어리로 추가하거나 삭제할 수 있습니다. 또한 모듈이 업데이트되면 이 모듈을 사용하는 모든 구성에서 일관된 변경 작업을 진행할 수 있습니다.
- 캡슐화 : 테라폼 구성 내에서 각 모듈은 논리적으로 묶여 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만을 외부에 노출시킵니다.
- 재사용성 : 구성을 처음부터 작성하는 것에는 시간과 노력이 필요하고 작성 중간에 디버깅과 오류를 수정하는 반복 작업이 발생합니다. 테라폼 구성을 모듈화 하면 이후에 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있다.
- 일관성과 표준화 : 테라폼 구성 시 모듈을 활용하는 워크플로는 구성의 일관성을 제공하고 서로 다른 환경과 프로젝트에도 이미 검증한 모듈을 적용해 복잡한 구성과 보안 사고를 방지할 수 있습니다.
모듈 작성 기본원칙
스터디에서 학습한 모듈 작성에 대한 기본 원칙은 다음과 같습니다.
- 모듈 디렉터리 형식을 terraform-<프로바이더 이름>-<모듈 이름> 형식을 제안한다. 이 형식은 Terraform Cloud, Terraform Enterprise에서도 사용되는 방식으로 1) 디렉터리 또는 레지스트리 이름이 테라폼을 위한 것이고, 2) 어떤 프로바이더의 리소스를 포함하고 있으며, 3) 부여된 이름이 무엇인지 판별할 수 있도록 한다.
- 테라폼 구성은 궁극적으로 모듈화가 가능한 구조로 작성할 것을 제안한다. 처음부터 모듈화를 가정하고 구성파일을 작성하면 단일 루트 모듈이라도 후에 다른 모듈이 호출할 것을 예상하고 구조화할 수 있다. 또한 작성자는 의도한 리소스 묶음을 구상한 대로 논리적인 구조로 그룹화할 수 있다.
- 각각의 모듈을 독립적으로 관리하기를 제안한다. 리모트 모듈을 사용하지 않더라도 처음부터 모듈화가 진행된 구성들은 때로 루트 모듈의 하위 파일 시스템에 존재하는 경우가 있다. 하위 모듈 또한 독립적인 모듈이므로 루트 모듈 하위에 두기보다는 동일한 파일 시스템 레벨에 위치하거나 별도 모듈만을 위한 공간에서 불러오는 것을 권장한다. 이렇게 하면 VCS를 통해 관리하기가 더 수월하다.
- 공개된 테라폼 레지스트리의 모듈을 참고하기를 제안한다. 대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해 두었다. 물론 그대로 가져다 사용하는 것보다는 프로비저닝 하려는 상황에 맞게 참고하는 것을 권장한다.
- 작성된 모듈은 공개 또는 비공개로 게시해 팀 또는 커뮤니티와 공유하기를 제안한다. 모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있는 자극이 된다.
모듈화 실습
06-module-training
└ module (child module의 홈 디렉터리)
└ terraform-aws-ec2
└ main.tf
└ multi_provider_for_module (root module)
└ main.tf
child module 생성
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
resource "aws_default_vpc" "default" {}
data "aws_ami" "default" {
most_recent = true
owners = ["amazon"]
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm*"]
}
}
resource "aws_instance" "default" {
depends_on = [aws_default_vpc.default]
ami = data.aws_ami.default.id
instance_type = var.instance_type
tags = {
Name = var.instance_name
}
}
# variable.tf
variable "instance_type" {
description = "vm 인스턴스 타입 정의"
default = "t2.micro"
}
variable "instance_name" {
description = "vm 인스턴스 이름 정의"
default = "my_ec2"
}
# output.tf
output "private_ip" {
value = aws_instance.default.private_ip
}
root module
# main.tf
provider "aws" {
region = "ap-southeast-1"
}
provider "aws" {
alias = "seoul"
region = "ap-northeast-2"
}
module "ec2_singapore" {
source = "../modules/terraform-aws-ec2"
}
module "ec2_seoul" {
source = "../modules/terraform-aws-ec2"
providers = {
aws = aws.seoul
}
instance_type = "t3.small"
}
# output.tf
output "module_output_singapore" {
value = module.ec2_singapore.private_ip
}
output "module_output_seoul" {
value = module.ec2_seoul.private_ip
}
cat .terraform/modules/modules.json | jq
#
terraform apply -auto-approve
terraform output
terraform state list
terraform state show module.ec2_seoul.data.aws_ami.default
terraform state show module.ec2_singapore.data.aws_ami.default
cat terraform.tfstate | grep module
# graph 확인
terraform graph > graph.dot
# aws cli로 ec2 확인
aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
aws ec2 describe-instances --region ap-southeast-1 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve
cat. terraform/modules/modules.json | jq 결과는 다음과 같습니다.
위의 모듈에서 정의한 대로, child module의 ami 정보를 토대로 region과 ec2, vpc가 잘 만들어진 것을 확인할 수 있습니다.
모듈 소스 관리
모듈 블록에 정의된 소스구성으로 모듈의 코드 위치를 정합니다. terraform init을 수행할 때 지정된 모듈을 다운로드하여 사용합니다. 모듈들의 의존성을 관리하기 위해 로컬 디렉터리 경로를 활용할 수 있습니다.
- 로컬 경로를 지정할 때는 테라폼 레지스트리와 구분하기 위해 하위 디렉터리는./로 상위 디렉터리는../로 시작합니다.
- 대상 모듈은 이미 같은 로컬 파일시스템에 존재하므로 다운로드 없이 바로 사용합니다. 앞서 언급한 대로 재사용성이 고려된다면 상위 디렉터리에 별도 관리하는 것을 권장하고, 항상 루트 모듈과 함께 동작해야 하는 경우 하위 디렉터리에 모듈을 정의합니다.
module "local_module" {
source = "../modules/my_local_module"
}
모듈을 이용하기 위해서는 테라폼 모듈 레지스트리를 활용할 수 있습니다. 공개된 테라폼 모듈을 사용하거나 Terraform Cloud, Terraform Enterprise에서 제공되는 비공개 (Private) 테라폼 모듈을 사용할 때 설정하는 소스 지정 방식입니다.
우리가 흔히 사용하는 모듈 들이며, 공개된 테라폼 모듈을 source에 선언할 때는 <네임스페이스>/<이름>/<프로바이더> 형태로 설정합니다.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
}
public repository의 모듈들이 잘 만들어져 있기 때문에 분석하여 실제 업무 모듈 생성 시 활용할 수 있습니다.
마치며
이번 포스팅에서는 테라폼의 State에 대한 상세 개념과 실습, 그리고 모듈에 대해 스터디 시간에 배운 내용을 정리했습니다. 이번 스터디에서 배운 내용을 토대로 EKS 클러스터 IaC 구성을 해보면 좋을 것 같다는 생각이 듭니다. 어느덧 스터디도 마지막이 되어가는군요.. 추석이 다가오면서 다들 바빠지실 텐데도 불구하고 다들 열심히 하시는 것 같아서 너무 멋집니다.
조금만 더 있으면 졸업이군요!
긴 포스팅을 따라와 주시느라 고생 많으셨습니다, 감사합니다! 그럼 다음 스터디 후에 뵙겠습니다 :)
'클라우드 컴퓨팅 & NoSQL > [T101] 테라폼 기초 입문 스터디' 카테고리의 다른 글
[6주차] T101 테라폼 기초 입문 스터디 (23.10.08) (2) | 2023.10.10 |
---|---|
[5주차] T101 테라폼 기초 입문 스터디 (23.09.24) (1) | 2023.09.29 |
[3주차] T101 테라폼 기초 입문 스터디 (23.09.10) (2) | 2023.09.15 |
[2주차] T101 테라폼 기초 입문 스터디 (23.08.27) (0) | 2023.09.08 |
[1주차] T101 테라폼 기초 입문 스터디 (23.08.27) (0) | 2023.09.01 |