이 내용은 CloudNet@ 에서 진행하는 테라폼 기초 입문 스터디에 대한 연재글입니다.
스터디에서 사용하는 교재는 Terraform Up & Running 2nd Edition 입니다.
저자의 프로덕션 레벨의 인프라 정의는 아래 요소들이 모두 잘 갖추어져있고, 이중화/장애대응까지 완벽히 되어있음을 말합니다:
아울러, 저자는 프로덕션 수준의 인프라를 만드는 프로젝트에 소요되는 대략적인 시간을 아래 정도가 걸린다고 산정하였습니다.
인프라 유형 | 예 | 예상 소요 시간 |
---|---|---|
관리형 서비스 Managed service | Amazon RDS | 1~2주 |
스스로 관리하는 분산 시스템 (상태 비저장) Self-managed distributed system (stateless) | Node.js 앱이 실행되는 ASG 클러스터 | 2~4 |
스스로 관리하는 분산 시스템 (상태 저장) Self-managed distributed system (stateful) | Amazon Elasticsearch cluster | 2~4개월 |
전체 아키텍처 Entire architecture | 애플리케이션, 데이터 저장소, 로드 밸런서, 모니터링 등 | 6~36개월 |
저는 아직 프로덕션 경험이 없지만, 개인적으로 만들고싶은 서비스를 구축한다면 관리형 서비스 ~ self managed 분산 시스템(stateless) 까지는 헤메면서 2주는 쓸 것 같습니다. 전체 아키텍처를 모두 구성하는데 길면 3년이 걸린다고 하니, 역시나 쉬운길은 아니네요. 그런데, 왜 이렇게 오래 걸리는걸까요?
DevOps 프로젝트는 다른 유형의 소프트웨어 프로젝트보다 더 시간이 소요될 수 있습니다. 아래에서 이야기되는 사항들이 그 이유입니다.
호프스태터의 법칙은 일을 마치는데 더 오랜 시간이 걸리는 현상을 의미합니다. 일정보다 늦어질 것을 미리 예상했다 하더라도 여전히 일정보다 늦어진다는 것이죠. 아래와 같은 말로 언급되는 경우가 많습니다.
호프스태터의 법칙: 일은 항상 예상시간 보다 더 오래 걸린다. 호프스태터의 법칙을 고려했다고 하더라도…
Hofstadter’s Law: It always takes longer than you expect, even when you take into account Hofstadter’s Law.
저자는 이를 “아직 석기시대에 있다”(…the industry is still in its infancy) 라고 표하였습니다. 클라우드 컴퓨팅, IaC, DevOps, 컨테이너 기술의 출현과 발전속도가 매우 빠르고 성숙하는 단계에 있다고 하였기 떄문입니다.
야크 털깎기는 목적을 이루기 위해 본래 목적과 전혀 상관없어 보이는 일들을 계속 해오고, 종국에는 그것을 이루는 행동을 말합니다.
극단적인 예시를 하나 보시죠. 도널드 커누스처럼 업계 부동의 원탑인 사람이라면, 책 쓰다가 디지털 조판 시스템이 답답해서 TeX를 자신이 직접 만든 프로그래밍 언어로 완성함과 동시에 폰트도 만들고, 폰트의 그래픽스를 정의하기 위한 언어도 만들고, 장치종속을 풀기위해 포맷까지 만들었다고 하지요. (책을 쓰는데 10년이 걸렸다고 합니다. 그 책은 다름아닌 TAOCP 입니다.)
누구나 그럴 수는 없음을 압니다. 그리고 회사는 정해진 기한과 목표물이 있지요. 저는 이를 삽질 이라고 배웠습니다. 독자 여러분들도 생각해보면 ‘이런 개념이 이렇게 불리는구나’ 하셨을 것이란 생각이 드네요.
앞서말했듯, 업계가 아직 태동기에 있으니 더더욱 그럴 수 밖에 없으리라는 것이 저자의 의견입니다.
대다수 개발자가 체크 리스트에 있는 대부분의 항목을 알지 못하기 때문에, 프로젝트를 평가할 때 중요하고 시간이 많이 걸리는 세부 사항을 잊어버립니다. 체크리스트는 아래와 같습니다:
작업 | 설명 | 사용가능 도구 |
---|---|---|
설치 (Install) |
소프트웨어 바이너리, 필요 종속성 설치 | Bash, Chef, Ansible, Puppet |
설정 (Configure) |
포트 설정, TLS 인증서, 서비스 디스커버리, 리더/팔로워 복제 등의 소프트웨어 설정 | Bash, Chef, Ansible, Puppet |
프로비전 (Provision) |
서버, 로드 밸런서, 네트워크, 방화벽, IAM 권한 설정 등의 인프라 제공 | Terraform, CloudFormation |
배포 (Deploy) |
인프라 상위의 서비스 배포 중단시간 없이 업데이트 롤아웃 블루-그린, 카나리 배포 등 |
Terraform, CloudFormation, k8s, ECS |
고가용성 (High Availability) |
프로세스, 서버, 서비스, 데이터 센터, 리전 등의 장애에 대비 | 멀티 데이터센터, 멀티 리전, 복제, 오토스케일링, 로드밸런싱 |
확장성 (Scailability) |
요청량에 따른 스케일 업/아웃 수평적 확장(더 많은 서버), 수직적 확장(더 큰 용량) |
오토스케일링, 복제, 샤딩, 캐싱, 분할정복 |
성능 (Performance) |
CPU, 메모리, 디스크, 네트워크, GPU 용량 최적화 쿼리 튜닝, 벤치마킹, 테스트, 프로파일링 |
Dynatrace, valgrind, VisualVM, ab, JMeter |
네트워킹 (Networking) |
정적·동적 IP 설정, 포트, 서비스 디스커버리, 방화벽, DNS, SSH 접속, VPN 연결 | VPC, 방화벽, 라우터, DNS Registers, OpenVPN |
보안 (Security) |
TLS를 통한 통신 중 데이터 암호화, 디스크 암호화, 인증, 인가, 보안 관리, 서버 하드닝 | ACM, Let’s Encrypt, KMS, Cognito, Vault, CIS |
성능지표 (Metrics) |
가용성, 비즈니스, 애플리케이션, 서버, 이벤트, 추적, 알람에 대한 메트릭 | CloudWatch, Datadog, New Relic, Honeycomb |
로그 (Logs) |
로그 순환, 중앙으로 데이터 수집 | CloudWatch Logs, ELK, Sumo Logic, Papertrail |
백업 및 복구 (Backup and Restore) |
DB, 캐시, 기타 데이터를 일정에 따라 백업 리전 별, 계정 별 복제 |
RDS, ElastiCache, 복제 |
비용 최적화 (Cost Optimization) |
적절한 인스턴스 유형 선택, 스팟 혹은 예약 인스턴스 사용, 오토스케일링, 사용하지 않는 리소스 정리 | 오토스케일링, 스팟 인스턴스, 예약 인스턴스 |
문서화 (Documentation) |
코드, 아키텍처, 모든 내용을 문서화 장애 대응 내용 정리(Postmortem) |
README, 각종 위키, Slack |
테스트 (Tests) |
인프라 코드를 테스트 자동화 항상 테스트 후 배포 |
Terratest, inspec, serverspec, kitchen-terraform |
와 세상에 이렇게나 많습니다! 그런데…
즉, 이러한 연유로 인해 프로젝트 계획 및 시간 예측이 너무나 어렵습니다.
그렇다면 프로덕션 레벨의 테라폼 코드는 어떤 것들이 필요한지 살펴보겠습니다. 테라폼 코드는 재사용 가능한 모듈 단위로 작성하는 것이 좋습니다. 이에 대한 모범사례를 살펴봅시다. 다룰 주제는 아래와 같습니다.
3장에서 학습했던 상태 파일 격리에서 알아본 것과 같이, 모든 인프라 환경은 단일파일, 단일 모듈로 정의해서는 좋지 않은 수준이 아니라 유해 한 것으로 간주합니다. 아래의 이유로 인해 그렇습니다.
terraform plan
구동 시 20분 걸리기도 합니다.terraform plan
실행 시 오래 거리고, plan 명령의 출력이 수천 줄이며 아무도 코드를 읽으려 하지 않을겁니다.따라서, 이런 이유로 인해 소형 모듈을 사용하는 편이 보다 좋은 코드가 되겠습니다.
소형 모듈로 내용을 줄여보기 전에 클린코드에 나오는 격언 중 하나를 소개하면 좋은 대목인 것 같군요.
함수의 첫 번째 규칙은 작아야한다는 것.
함수의 두 번째 규칙은, 그보다 더 작아야한다는 것.
예시로 한번 살펴보겠습니다.
이런 아키텍처의 코드가 단 한줄뿐이라면… 그것은 코드 스멜입니다! 코드 스멜은 더 큰 문제를 일으키기 쉬운 코드지요. 저는 냄새나는 코드라고 부릅니다.
기존 예시라면, 지난 7장에서 살펴본 ASG, ALB, 헬로월드 앱을 각각의 소형 모듈로 나누어봅시다.
여기서부턴 코드확인이 필요합니다. chapter08
디렉토리를 확인해주세요. 경로는 여기 입니다. 베이스코드는 7장에서 작성한 코드입니다.
재사용 가능하고 합성 가능한 모듈
그러면, 코드 리팩토링을 진행해볼까요.
webserver-cluster
는 하드코딩 되어있었고, 이를 아래와 같이 재작성합니다.
user_data
변경 가능하도록 수정
수정한 코드를 직접 구동해봅시다.
asg-rolling-deploy
모듈을 사용하여, 크기 1인 ASG를 배포하는 코드를 봅시다.terraform {
required_version = ">= 1.0.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-2"
}
# 모듈을 사용하여, 상기 리팩토링을 통해 수정한 내용을 쉽게 배포할 수 있게 되었습니다!
#
# 개인적으로는, 모듈 직전의 코드를 직접 쳐봐야 모듈화를 할 수 있는 통찰이 생기고,
# 이를 통해 프로비저닝 및 배포를 테라폼으로 할 수 있다고 생각합니다.
module "asg" {
source = "../../modules/cluster/asg-rolling-deploy"
cluster_name = var.cluster_name
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
min_size = 1
max_size = 1
enable_autoscaling = false
subnet_ids = data.aws_subnets.default.ids
}
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
배포 및 확인은 아래 코드로 진행하면 됩니다.
# [터미널1] RDS 생성 모니터링
while true; do aws rds describe-db-instances --query "*[].[Endpoint.Address,Endpoint.Port,MasterUsername]" --output text ; echo "------------------------------" ; sleep 1; done
# RDS 배포
cd chapter08/examples01/small-modules/examples/mysql
# 환경변수에 지정
export TF_VAR_db_username='cloudneta'
export TF_VAR_db_password='cloudnetaQ!'
terraform init && terraform plan
terraform apply -auto-approve
# [터미널2]
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done
# 배포
cd chapter08/examples01/small-modules/examples/asg
terraform init
terraform plan
terraform apply -auto-approve
# ALB 배포
cd chapter08/examples01/small-modules/examples/alb
terraform init && terraform plan
terraform apply -auto-approve
# ALB DNS주소로 curl 접속 확인
ALBDNS=$(terraform output -raw alb_dns_name)
while true; do curl --connect-timeout 1 http://$ALBDNS ; echo; echo "------------------------------"; date; sleep 1; done
curl -s http://$ALBDNS
테라폼 0.13부터 등장한 validation blocks 은 입력 변수를 체크할 수 있습니다.
variable "instance_type" {
description = "The type of EC2 Instances to run (e.g. t2.micro)"
type = string
# t2.micro 혹은 t3.micro만 사용할 수 있도록 체크하는 로직입니다!
validation {
condition = contains(["t2.micro", "t3.micro"], var.instance_type)
error_message = "Only free tier is allowed: t2.micro | t3.micro."
}
}
만일 validation에 실패한다면, 아래와 같은 에러 메시지를 리턴하게 됩니다.
$ terraform apply -var instance_type="m4.large"
│ Error: Invalid value for variable
│
│ on main.tf line 17:
│ 1: variable "instance_type" {
│ ├────────────────
│ │ var.instance_type is "m4.large"
│
│ Only free tier is allowed: t2.micro | t3.micro.
│
│ This was checked by the validation rule at main.tf:21,3-13.
인프라 코드는 오늘 실행하든 3년후에 실행하든 동일한 결과를 얻을 수 있어야 합니다. 테라폼 코어, 프로바이더 및 버전을 명시하여 이를 가능하게 합니다.
모듈 버저닝
core
를 의미): 테라폼 실행파일 버전을 required_version
이란 값으로 명시합니다.terraform {
# Require any 1.x version of Terraform
required_version = ">= 1.0.0, < 2.0.0"
}
# 프로덕션은 완전히 구체적인 버전을 지칭하는 것이 좋습니다.
terraform {
# Require any 1.x version of Terraform
required_version = "1.2.3"
}
require_providers
블록으로 명시합니다.terraform {
required_version = ">= 1.0.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
tag
를 사용하는 방법이 있겠습니다.아래 사항들이 있습니다:
제 8장에서는 아래의 내용을 반드시 기억하셨으면 좋겠습니다.
이것으로 제 8장을 마칩니다. 긴 글 읽어주셔서 감사합니다.