상태관리
상태 파일 확인 실습
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
# 상태 파일 확인 : JSON 형식
ls
cat terraform.tfstate | jq
...
"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"
}
}
EOT
# 배포 : plan 시 tfstate 상태와 코드 내용을 비교해서 검토
terraform plan && terraform apply -auto-approve
# 상태 파일 비교 : 백업 파일 생성됨
ls terraform.tfstate*
terraform.tfstate terraform.tfstate.backup
diff terraform.tfstate terraform.tfstate.backup
< "serial": 4,
---
> "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 plan && terraform apply -auto-approve
# 상태 파일 비교(바로 직전 상태 백업)
ls terraform.tfstate*
terraform.tfstate terraform.tfstate.backup
diff terraform.tfstate terraform.tfstate.backup
< "serial": 6,
---
> "serial": 4,
...
다음 실습을 위해 삭제: **terraform destroy -auto-approve**
이론 내용
- 상태 파일은 배포할 때마다 변경되는 프라이빗 API private API로, 오직 테라폼 내부에서 사용하기 위한 것입니다.
- 테라폼 상태 파일을 직접 편집하거나 직접 읽는 코드로 작성해서는 안됩니다.
팀 단위에서 테라폼 운영 시 문제점
- 상태 파일을 저장하는 공유 스토리지 Shared storage for state files
- 각 팀원이 동일한 테라폼 상태 파일 사용을 위해서, 공유 위치에 저장이 필요
- 상태 파일 잠금 Locking state files
- 잠금 기능 없이 두 팀원이 동시에 테라폼 실행 시 여러 테라폼 프로세스가 상태 파일을 동시에 업데이트하여 충돌 가능(경쟁 상태 race condition)
- 상태 파일 격리 Isolating state files
- 예를 들면 테스트 dev 와 검증 stage 과 상용 prodction 각 환경에 대한 격리가 필요
상태 파일 공유로 버전 관리 시스템 비추천
- 수동 오류 Manual error
- 테라폼을 실행하기 전에 최신 변경 사항을 가져오거나 실행하고 나서 push 하는 것을 잊기 쉽습니다(?).
- 팀의 누군가가 이전 버전의 상태 파일로 테라폼을 실행하고, 그 결과 실수로 이전 버전으로 롤백하거나 이전에 배포된 인프라를 복제하는 문제가 발생 할 수 있음.
- 잠금 Locking
- 대부분의 버전 관리 시스템은 여러 명의 팀 구성원이 동시에 하나의 상태 파일에 terraform apply 명령을 실행하지 못하게 하는 잠금 기능이 제공되지 않음.
- 시크릿 Secrets
- 테라폼 상태 파일의 모든 데이터는 평문으로 저장됨. 민감 정보가 노출될 위험.
지원되는 원격 백엔드
: AWS S3, Azure Blob Storage, Google Cloud Storage, Consul, Postgres database 등 - 링크
- 수동 오류 해결 : plan/apply 실행 시 마다 해당 백엔드에서 파일을 자동을 로드, apply 후 상태 파일을 백엔드에 자동 저장
- 잠금 : apply 실행 시 테라폼은 자동으로 잠금을 활성화, -lock-timout=TIME 로 대기 시간 설정 지정 가능
- 시크릿 : 대부분 원격 백엔드는 기본적으로 데이터를 보내거나 상태 파일을 저장할 때 암호화하는 기능을 지원
DynamoDB 기본 사용 - 링크
테이블 생성
# 테이블 생성
aws dynamodb create-table \
--table-name Music \
--attribute-definitions \
AttributeName=Artist,AttributeType=S \
AttributeName=SongTitle,AttributeType=S \
--key-schema \
AttributeName=Artist,KeyType=HASH \
AttributeName=SongTitle,KeyType=RANGE \
--provisioned-throughput \
ReadCapacityUnits=5,WriteCapacityUnits=5 \
--table-class STANDARD
# 테이블 생성 확인
aws dynamodb list-tables --output text
TABLENAMES Music
aws dynamodb describe-table --table-name Music | jq
aws dynamodb describe-table --table-name Music --output table
데이터 쓰기 - 링크
# DynamoDB API
aws dynamodb put-item \
--table-name Music \
--item \
'{"Artist": {"S": "No One You Know"}, "SongTitle": {"S": "Call Me Today"}, "AlbumTitle": {"S": "Somewhat Famous"}, "Awards": {"N": "1"}}'
aws dynamodb put-item \
--table-name Music \
--item \
'{"Artist": {"S": "No One You Know"}, "SongTitle": {"S": "Howdy"}, "AlbumTitle": {"S": "Somewhat Famous"}, "Awards": {"N": "2"}}'
# PartiQL for DynamoDB
aws dynamodb execute-statement --statement "INSERT INTO Music \
VALUE \
{'Artist':'Acme Band','SongTitle':'Happy Day', 'AlbumTitle':'Songs About Life', 'Awards':'10'}"
aws dynamodb execute-statement --statement "INSERT INTO Music \
VALUE \
{'Artist':'Acme Band','SongTitle':'PartiQL Rocks', 'AlbumTitle':'Another Album Title', 'Awards':'8'}"
데이터 읽기 - 링크
# DynamoDB API
aws dynamodb get-item --consistent-read \
--table-name Music \
--key '{ "Artist": {"S": "Acme Band"}, "SongTitle": {"S": "Happy Day"}}' | jq
# PartiQL for DynamoDB
aws dynamodb execute-statement --statement "SELECT * FROM Music \
WHERE Artist='Acme Band' AND SongTitle='Happy Day'" | jq
aws dynamodb execute-statement --statement "SELECT * FROM Music \
WHERE Artist='Acme Band' AND SongTitle='Happy Day'" --output table
aws dynamodb execute-statement --statement "SELECT * FROM Music" --output table
aws dynamodb execute-statement --statement "SELECT * FROM Music" --output text
데이터 업데이트
# PartiQL for DynamoDB
aws dynamodb execute-statement --statement "UPDATE Music \
SET AlbumTitle='Updated Album Title' \
WHERE Artist='Acme Band' AND SongTitle='Happy Day' \
RETURNING ALL NEW *"
데이터 쿼리 - 링크
# PartiQL for DynamoDB
aws dynamodb execute-statement --statement "SELECT * FROM Music \
WHERE Artist='Acme Band'" | jq
테이블 삭제
aws dynamodb delete-table --table-name Music
S3/DynamoDB 생성
S3 버킷 생성 : backend.tf
# 디렉터리 생성
mkdir tfstate-backend
cd tfstate-backend
# S3 버킷 이름은 고유해야되어서 자신의 NICKNAME 을 사용할 것
NICKNAME=gasida
cat <<EOT > backend.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "mys3bucket" {
bucket = "$NICKNAME-t101study-tfstate"
}
# Enable versioning so you can see the full revision history of your state files
resource "aws_s3_bucket_versioning" "mys3bucket_versioning" {
bucket = aws_s3_bucket.mys3bucket.id
versioning_configuration {
status = "Enabled"
}
}
output "s3_bucket_arn" {
value = aws_s3_bucket.mys3bucket.arn
description = "The ARN of the S3 bucket"
}
EOT
# 배포
terraform init && terraform plan && terraform apply -auto-approve
terraform state list
aws_s3_bucket.mys3bucket
aws_s3_bucket_versioning.mys3bucket_versioning
# S3 버킷 확인
aws s3 ls
2022-10-23 13:48:05 gasida-t101study-tfstate
...
DynamoDB 테이블 생성 : 테라폼에서 DynamoDB 잠금을 사용하기 위해서는 LockID 라는 기본 키가 있는 테이블을 생성해야됨
# 코드 파일 수정
cat <<EOT > backend.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "mys3bucket" {
bucket = "$NICKNAME-t101study-tfstate"
}
# Enable versioning so you can see the full revision history of your state files
resource "aws_s3_bucket_versioning" "mys3bucket_versioning" {
bucket = aws_s3_bucket.mys3bucket.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_dynamodb_table" "mydynamodbtable" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
output "s3_bucket_arn" {
value = aws_s3_bucket.mys3bucket.arn
description = "The ARN of the S3 bucket"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.mydynamodbtable.name
description = "The name of the DynamoDB table"
}
EOT
# 배포
terraform plan && terraform apply -auto-approve
dynamodb_table_name = "terraform-locks"
s3_bucket_arn = "arn:aws:s3:::gasida-t101study-tfstate"
terraform state list
aws_dynamodb_table.mydynamodbtable
aws_s3_bucket.mys3bucket
aws_s3_bucket_versioning.mys3bucket_versioning
# DynamoDB 테이블 생성 확인
aws dynamodb list-tables --output text
TABLENAMES terraform-locks
aws dynamodb describe-table --table-name terraform-locks | jq
aws dynamodb describe-table --table-name terraform-locks --output table
dev 환경의 VPC와 EC2 생성
# 신규 디렉터리 생성 후 작업 진행
cd ..
mkdir dev
cd dev
cat <<EOT > vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "t101-study"
}
}
resource "aws_subnet" "mysubnet1" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.10.1.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "t101-subnet1"
}
}
resource "aws_subnet" "mysubnet2" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.10.2.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "t101-subnet2"
}
}
resource "aws_internet_gateway" "myigw" {
vpc_id = aws_vpc.myvpc.id
tags = {
Name = "t101-igw"
}
}
resource "aws_route_table" "myrt" {
vpc_id = aws_vpc.myvpc.id
tags = {
Name = "t101-rt"
}
}
resource "aws_route_table_association" "myrtassociation1" {
subnet_id = aws_subnet.mysubnet1.id
route_table_id = aws_route_table.myrt.id
}
resource "aws_route_table_association" "myrtassociation2" {
subnet_id = aws_subnet.mysubnet2.id
route_table_id = aws_route_table.myrt.id
}
resource "aws_route" "mydefaultroute" {
route_table_id = aws_route_table.myrt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.myigw.id
}
EOT
cat <<EOT > sg.tf
resource "aws_security_group" "mysg" {
vpc_id = aws_vpc.myvpc.id
name = "T101 SG"
description = "T101 Study SG"
}
resource "aws_security_group_rule" "mysginbound" {
type = "ingress"
from_port = 0
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.mysg.id
}
resource "aws_security_group_rule" "mysgoutbound" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.mysg.id
}
EOT
cat <<EOT > ec2.tf
data "aws_ami" "my_amazonlinux2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}
resource "aws_instance" "myec2" {
depends_on = [
aws_internet_gateway.myigw
]
ami = data.aws_ami.my_amazonlinux2.id
associate_public_ip_address = true
instance_type = "t2.micro"
vpc_security_group_ids = ["\${aws_security_group.mysg.id}"]
subnet_id = aws_subnet.mysubnet1.id
user_data = <<-EOF
#!/bin/bash
wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
mv busybox-x86_64 busybox
chmod +x busybox
IID=\$(curl 169.254.169.254/latest/meta-data/instance-id)
LIP=\$(curl 169.254.169.254/latest/meta-data/local-ipv4)
echo "<h1>Instance ID(\$IID) : Private IP(\$LIP) : Web Server</h1>" > index.html
nohup ./busybox httpd -f -p 80 &
EOF
user_data_replace_on_change = true
tags = {
Name = "HallsHolicker-jjang"
}
}
output "myec2_public_ip" {
value = aws_instance.myec2.public_ip
description = "The public IP of the Instance"
}
EOT
# 배포
terraform init && terraform plan && terraform apply -auto-approve
terraform state list
ls terraform.tfstate*
terraform.tfstate terraform.tfstate.backup
# 출력된 EC2 퍼블릭IP로 cul 접속 확인
MYIP=$(terraform output -raw myec2_public_ip)
while true; do curl --connect-timeout 1 http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done
dev 환경에 백엔드 적용 실습
# [터미널1] S3 버킷 모니티렁
NICKNAME=gasida
while true; do aws s3 ls s3://$NICKNAME-t101study-tfstate --recursive --human-readable --summarize ; echo "------------------------------"; date; sleep 1; done
코드 파일 생성 : backend.tf
NICKNAME=gasida
cat <<EOT > backend.tf
terraform {
backend "s3" {
bucket = "$NICKNAME-t101study-tfstate"
key = "dev/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
# encrypt = true
}
}
EOT
- dev 환경에 백엔드 적용
# init 시 백엔드 적용
terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
ls terraform.tfstate*
# S3 버킷에 파일 확인
# DynamoDB에 LockID 확인
EC2에 태그 변경 후 수정 적용 및 대기 시간 설정
# 태그 변경
sed -i -e 's/HallsHolicker-jjang/akbun-jjangg/g' ec2.tf
# 빠르게 수정되어서 lock-timeout 적용될 필요가 없는 것 같다...
terraform plan && terraform apply -lock-timeout=60s -auto-approve
백엔드 좀 더 알아보기
- 강제로 로컬에 terraform.tfstate 파일 삭제 후 확인
# 강제로 로컬에 terraform.tfstate 파일 삭제 후 확인
rm -f terraform.tfstate terraform.tfstate.backup
sed -i -e 's/akbun-jjang/linuxer-jjangg/g' ec2.tf
# plan 실행 결과 어떻게 되나요?
terraform plan
# apply 실행 결과 어떻게 되나요?
terraform apply -auto-approve
ls terraform.tfstate*
# 버저닝된 파일 확인
aws s3api list-object-versions --bucket gasida-t101study-tfstate | egrep "Key|VersionId|LastModified"
"Key": "dev/terraform.tfstate",
"VersionId": "oyo59fIIQ239_tVTY88upFoVgnp630BC",
"LastModified": "2022-10-23T08:54:04+00:00",
"Key": "dev/terraform.tfstate",
"VersionId": "vlOO1wCPEy3mzAOB7W76171u_i3WSG8r",
"LastModified": "2022-10-23T08:53:04+00:00",
"Key": "dev/terraform.tfstate",
"VersionId": "ukF5F_CMKQhoF_jDGftbsMO0eNIGLmDV",
"LastModified": "2022-10-23T08:51:55+00:00",
S3 버킷에 버전 표시 확인
stg 환경의 VPC와 EC2 생성 및 백엔드 설정
# 신규 디렉터리 생성 후 작업 진행
cd ..
mkdir stg
cd stg
cat <<EOT > vpc.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_vpc" "stg_myvpc" {
cidr_block = "10.20.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "stg_t101-study"
}
}
resource "aws_subnet" "stg_mysubnet1" {
vpc_id = aws_vpc.stg_myvpc.id
cidr_block = "10.20.1.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "stg_t101-subnet1"
}
}
resource "aws_subnet" "stg_mysubnet2" {
vpc_id = aws_vpc.stg_myvpc.id
cidr_block = "10.20.2.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "stg_t101-subnet2"
}
}
resource "aws_internet_gateway" "stg_myigw" {
vpc_id = aws_vpc.stg_myvpc.id
tags = {
Name = "stg_t101-igw"
}
}
resource "aws_route_table" "stg_myrt" {
vpc_id = aws_vpc.stg_myvpc.id
tags = {
Name = "stg_t101-rt"
}
}
resource "aws_route_table_association" "stg_myrtassociation1" {
subnet_id = aws_subnet.stg_mysubnet1.id
route_table_id = aws_route_table.stg_myrt.id
}
resource "aws_route_table_association" "stg_myrtassociation2" {
subnet_id = aws_subnet.stg_mysubnet2.id
route_table_id = aws_route_table.stg_myrt.id
}
resource "aws_route" "stg_mydefaultroute" {
route_table_id = aws_route_table.stg_myrt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.stg_myigw.id
}
EOT
cat <<EOT > sg.tf
resource "aws_security_group" "stg_mysg" {
vpc_id = aws_vpc.stg_myvpc.id
name = "T101 SG"
description = "T101 Study SG"
}
resource "aws_security_group_rule" "stg_mysginbound" {
type = "ingress"
from_port = 0
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.stg_mysg.id
}
resource "aws_security_group_rule" "stg_mysgoutbound" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.stg_mysg.id
}
EOT
cat <<EOT > ec2.tf
data "aws_ami" "my_amazonlinux2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}
resource "aws_instance" "stg_myec2" {
depends_on = [
aws_internet_gateway.stg_myigw
]
ami = data.aws_ami.my_amazonlinux2.id
associate_public_ip_address = true
instance_type = "t2.micro"
vpc_security_group_ids = ["\${aws_security_group.stg_mysg.id}"]
subnet_id = aws_subnet.stg_mysubnet1.id
user_data = <<-EOF
#!/bin/bash
wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
mv busybox-x86_64 busybox
chmod +x busybox
IID=\$(curl 169.254.169.254/latest/meta-data/instance-id)
LIP=\$(curl 169.254.169.254/latest/meta-data/local-ipv4)
echo "<h1>Instance ID(\$IID) : Private IP(\$LIP) : Web Server</h1>" > index.html
nohup ./busybox httpd -f -p 80 &
EOF
user_data_replace_on_change = true
tags = {
Name = "HallsHolicker-jjang"
}
}
output "myec2_public_ip" {
value = aws_instance.stg_myec2.public_ip
description = "The public IP of the Instance"
}
EOT
NICKNAME=gasida
cat <<EOT > backend.tf
terraform {
backend "s3" {
bucket = "$NICKNAME-t101study-tfstate"
key = "stg/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
# encrypt = true
}
}
EOT
# 배포
terraform init
terraform plan && terraform apply -lock-timeout=60s -auto-approve
terraform state list
ls terraform.tfstate*
terraform.tfstate terraform.tfstate.backup
LockID 상태 확인 : 테라폼을 통해 apply 하는 도중에 아래 정보 확인 가능
- LockID : gasida-t101study-tfstate/stg/terraform.tfstate
- Info
{"ID":"354ed19e-0446-d8c7-269a-3139fbdac463","Operation":"OperationTypeApply","Info":"","Who":"gasida@JongHoui-MacBookPro.local","Version":"1.3.3","Created":"2022-10-23T06:20:42.753567Z","Path":"gasida-t101study-tfstate/stg/terraform.tfstate"}
배포 완료 후 S3 버킷과 DynamoDB 확인
테라폼 백엔드 단점 - 링크
테라폼의 backend 블록에는 변수나 참조를 사용 할 수 없음 → 아래 코드 사용할 수 없음
# This will NOT work, Variables aren't allow in backend configuration.
terraform {
backend "s3"{
bucket = var.bucket
region = var.region
dynaodb_table = var.danamodb_table
key = "example/terraform.tfstate"
encrypt = ture
}
}
그 결과 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 매개 변수는 테라폼 코드에 있어야함
# Partial configuration. The other settings (e.g., bucket, region) will be
# passwd in form a file via -backend-config arguments to "terraform init"
terraform {
backend "s3" {
key = "example/terraform.tfstate"
}
}
부분적으로 구성한 것들을 모두 결합하려면 -backend-config 인수와 함께 terraform init 명령을 실행
terraform init -backend-config=backend.hcl
위 단점을 보완해주는 오픈 소스 테라그런트(Terragrunt)가 있음. 8장에서 다룰 예정 - 링크
실습 리소스 삭제
# 각 폴더에서 리소스 삭제
stg$ terraform destroy -auto-approve
dev$ terraform destroy -auto-approve
# S3 버킷에 객체 삭제
aws s3 rm s3://$NICKNAME-t101study-tfstate --recursive
# S3 버킷에 버저닝 객체 삭제
aws s3api delete-objects \
--bucket $NICKNAME-t101study-tfstate \
--delete "$(aws s3api list-object-versions \
--bucket "${NICKNAME}-t101study-tfstate" \
--output=json \
--query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
# S3 버킷에 삭제마커 삭제
aws s3api delete-objects --bucket $NICKNAME-t101study-tfstate \
--delete "$(aws s3api list-object-versions --bucket "${NICKNAME}-t101study-tfstate" \
--query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')"
# 백엔드 리소스 삭제
tfstate-backend$ terraform destroy -auto-approve