terraform
terraform 서비스 구축
Intro
본 문서는 terraform 으로 실습한 소스코드를 설명하는 데 중점을 두었습니다.
본 문서는 markdown 언어로 제작 되었습니다.
김태형(kimutae@naver.com) 의 저작물이며 무단 배포 / 재배포를 금합니다.
IAC(infra as code) 이란
인프라를 코드로 프로비저닝 / 관리한다는 의미입니다.
장점
비용 절감
배포 속도 향상
오류 감소
인프라 일관성 향상
구성 변동 제거
terraform 이란
테라폼(Terraform)은 하시코프(Hashicorp)에서 오픈소스로 개발중인 클라우드 인프라스트럭처 입니다.
대표적인 public cloud 인 aws / gcp / azure 등을 모두 지원합니다.
하시코프 설정 언어HCL(Hashicorp Configuration Language)을 사용해 클라우드 리소스를 선언합니다
파일 확장자는 .tf입니다.
terraform 명령어
init : . terrform 초기화 이후 각 vendor사에 맞는 provier 파일을 설치 합니다.
plan :
테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용을 실제로 적용 가능한지 확인하는 작업을 계획
테라폼은 이를 terraform plan 명령어로 제공하며, 이 명령어를 실행하면 어떤 리소스가 생성되고, 수정되고, 삭제
apply :
테라폼 프로젝트 디렉터리 아래의 모든 .tf 파일의 내용대로 리소스를 생성, 수정, 삭제
테라폼은 이를 terraform apply 명령어로 제공합니다. 이 명령어를 실행하기 전에 변경 예정 사항은 plan 명령어
terraform 으로 구성하기 앞서 생각해야 할 점
상시 변경 인프라 / 기반인프라 구분
테라폼을 사용하게 되면 한 디렉토리 내에 있는 모든 tf 파일들을 동시에 실행을 합니다.
그렇기에 한번 구축하면 거의 변경 할 일이 없는 인프라와 상시로 변경이나 구축이 자주 일어나는 인프라를 구분해야 합니다.
초기 구성 이후 거의 변하지 않는 항목은 vpc등 네트워크 인프라이며 - resource(신규생성)를 지정 해서 인프라를 생성 합니다.
서비스 구성 시 항상 자주 구성해야 하는 항목 - data(불러오기) / resource를 혼용 하여 사용 합니다.
EX
resourcedatavpc
security gruop
subnet
Load Balancer
igw
target group
NAT
EC2
ECS-Cluster
ecs
task difinition
ecr
RDS
code 재 사용이 가능하도록 설계
IAC 를 사용 한다는 것은 결국 인프라를 프로그래밍한다는 의미이며 프로그래밍 언어의 특징도 그대로 가져옵니다.
한번 작성한 코드는 재 사용이 가능 하도록 설계 되어야 합니다.
공통으로 사용하는 부분은 변수로 처리 해야 합니다.
자주 사용 하는 구문은 모듈화 해야 합니다.
variables를 활용한 code 사용 예시
변수지정
variable "cidr_numeral" {
description = "The VPC CIDR numeral (10.x.0.0/16)"
type = number
default = "30"
}
variable "cidr_numeral_public" {
default = {
"0" = "0"
"1" = "16"
"2" = "32"
}
}
############# 가용 영역 지정 - 가용영역의 갯수만큼 서브넷이 만들어진다
variable "availability_zones" {
type = list(string)
default = [
"ap-northeast-2a" ,
"ap-northeast-2b"
]
}
사용
resource "aws_subnet" "public" {
count = length(var.availability_zones) # 변수로 지정된 가용 영역의 갯수 현재 2 즉 변수는 0,1
vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력
#################
# count.index = for 문 역할 0 = 0 / 1=16
cidr_block = "10.${var.cidr_numeral}.${var.cidr_numeral_public[count.index]}.0/20"
# resource참조는 element 구문을 활용 하여 for 문을 작성합니다
availability_zone = element(var.availability_zones, count.index)
#################
map_public_ip_on_launch = true
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}
디렉토리 구성
main
├── global >> global 설정 - vpc를 추가 할때 사용
│ └── mk-network-var >> vpc / subnet /igw 등 네트워크 설정
│ ├── main.tf
│ ├── network.tf
│ ├── terraform.tfstate
│ ├── terraform.tfstate.backup
│ └── variables.tf
└── service
└── ecs-web
├── falgate >> ecs-cluster 내 다중 서비스를 포함하고 있어 별도로 생성 할수 있도록 구성
│ ├── ecr.tf
│ ├── ecs-cluster.tf
│ ├── td-service
│ │ ├── ecs.tf
│ │ ├── serevice.conf.json
│ │ ├── serevice.conf.json.tpl
│ │ ├── td.tf
│ │ ├── terraform.tfstate
│ │ ├── terraform.tfstate.backup
│ │ └── variables.tf -> ../../variables.tf >> 공용 변수를 사용 하기 위해 심볼릭 링크로 구성
│ ├── terraform.tfstate
│ ├── terraform.tfstate.backup
│ └── variables.tf -> ../variables.tf
├── sg-lb >> security group 과 로드벨런서를 구성
│ ├── lb.tf
│ ├── output.tf
│ ├── sg.tf -> ../sg.tf
│ ├── terraform.tfstate
│ ├── terraform.tfstate.backup
│ ├── variables.tf -> ../variables.tf
├── sg.tf
└── variables.tf
VPC 구성
코드 펼치기
공용 변수 : variables.tf
variable "access_key" {
type = string
}
variable "secret_key" {
type = string
}
variable "region" {
type = string
default = "ap-northeast-2"
}
variable "vpc_name" {
description = "The name of the VPC"
default = "terra"
}
########### CIDR 변수 지정
variable "cidr_numeral" {
description = "The VPC CIDR numeral (10.x.0.0/16)"
type = number
default = "30"
}
############# 가용 영역 지정 - 가용영역의 갯수만큼 서브넷이 만들어진다
variable "availability_zones" {
type = list(string)
default = [
"ap-northeast-2a" ,
"ap-northeast-2b"
]
}
variable "cidr_numeral_public" {
default = {
"0" = "0"
"1" = "16"
"2" = "32"
}
}
variable "cidr_numeral_private" {
default = {
"0" = "80"
"1" = "96"
"2" = "112"
}
}
variable "cidr_numeral_private_db" {
default = {
"0" = "160"
"1" = "176"
"2" = "192"
}
}
variable "site_name" {
type = string
default = "terra"
}
프로바이더 지정 - main.tf
provider "aws" {
access_key = var.access_key
secret_key = var.secret_key
region = var.region
}
subnet / igw / nat 구성
resource "aws_vpc" "vpc" {
# cidr_block = "10.30.0.0/16" #IPv4 CIDR Block
cidr_block = "10.${var.cidr_numeral}.0.0/16" # Please set this according to your company size
enable_dns_hostnames = true #DNS Hostname 사용 옵션, 기본은 false
tags = { Name = "${var.site_name}_vpc" } #tag 입력
}
resource "aws_internet_gateway" "default" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "igw-${var.vpc_name}"
}
}
## NAT Gateway
resource "aws_nat_gateway" "nat" {
count = length(var.availability_zones)
allocation_id = element(aws_eip.nat.*.id, count.index)
subnet_id = element(aws_subnet.public.*.id, count.index)
lifecycle {
create_before_destroy = true
}
tags = {
Name = "NAT-GW${count.index}-${var.site_name}"
}
}
# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
count = length(var.availability_zones)
vpc = true
lifecycle {
create_before_destroy = true
}
}
#### PUBLIC SUBNETS
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력
cidr_block = "10.${var.cidr_numeral}.${var.cidr_numeral_public[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = true
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}
# Route Table for public subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id #위에서 생성한 vpc 별칭 입력
tags = {
Name = "publicrt-${var.vpc_name}"
}
}
# Route Table Association for public subnets
resource "aws_route_table_association" "public" {
count = length(var.availability_zones)
subnet_id = element(aws_subnet.public.*.id, count.index)
route_table_id = aws_route_table.public.id
}
#### PRIVATE SUBNETS
# Subnet will use cidr with /20 -> The number of available IP is 4,096 (Including reserved ip from AWS)
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
cidr_block = "10.${var.cidr_numeral}.${var.cidr_numeral_private[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)
tags = {
Name = "private${count.index}-${var.vpc_name}"
Network = "Private"
}
}
# Route Table for private subnets
resource "aws_route_table" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
tags = {
Name = "private${count.index}rt-${var.vpc_name}"
Network = "Private"
}
}
# Route Table Association for private subnets
resource "aws_route_table_association" "private" {
count = length(var.availability_zones)
subnet_id = element(aws_subnet.private.*.id, count.index)
route_table_id = element(aws_route_table.private.*.id, count.index)
}
# DB PRIVATE SUBNETS
# This subnet is only for the database.
# For security, it is better to assign ip range for database only. This subnet will not use NAT Gateway
# This is also going to use /20 cidr, which might be too many IPs... Please count it carefully and change the cidr.
resource "aws_subnet" "private_db" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
cidr_block = "10.${var.cidr_numeral}.${var.cidr_numeral_private_db[count.index]}.0/20"
availability_zone = element(var.availability_zones, count.index)
tags = {
Name = "db-private${count.index}-${var.vpc_name}"
Network = "Private"
}
}
# Route Table for DB subnets
resource "aws_route_table" "private_db" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
tags = {
Name = "privatedb${count.index}rt-${var.vpc_name}"
Network = "Private"
}
}
# Route Table Association for DB subnets
resource "aws_route_table_association" "private_db" {
count = length(var.availability_zones)
subnet_id = element(aws_subnet.private_db.*.id, count.index)
route_table_id = element(aws_route_table.private_db.*.id, count.index)
}
Load Balancer 구성
코드 펼치기
data "aws_subnet" "public" {
count = length(var.availability_zones)
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}
#resource "aws_lb" "terra-lb" {
resource "aws_lb" "terra-lb" {
count = length(var.product)
name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
subnets = data.aws_subnet.public[*].id
load_balancer_type = "application"
security_groups = [aws_security_group.lb.id]
# access_logs {
# bucket = aws_s3_bucket.log_storage.id
# prefix = "frontend-alb"
# enabled = true
# }
tags = {
Environment = "terra-lb"
Application = var.app_name
}
}
#resource "aws_lb_listener" "https_forward" {
# load_balancer_arn = aws_lb.terra-lb.arn
# port = 443
# protocol = "HTTPS"
# certificate_arn = aws_acm_certificate.cert.arn
# ssl_policy = "ELBSecurityPolicy-2016-08"
#
# default_action {
# type = "forward"
# target_group_arn = aws_lb_target_group.terra-lb.arn
# }
#}
resource "aws_lb_listener" "http_forward" {
count = length(var.product)
load_balancer_arn = element(aws_lb.terra-lb.*.arn,count.index)
#load_balancer_arn = aws_lb.terra-lb.arn
port = 80
protocol = "HTTP"
default_action {
# type = "redirect"
type = "forward"
target_group_arn = element(aws_lb_target_group.terra-lb.*.arn,count.index)
# redirect {
# port = "443"
# protocol = "HTTPS"
# status_code = "HTTP_301"
# }
}
}
resource "aws_lb_target_group" "terra-lb" {
count = length(var.product)
name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
# name = "service-alb-tg"
port = 80
protocol = "HTTP"
vpc_id = data.aws_vpc.vpc.id
target_type = "ip"
health_check {
enabled = true
interval = 300
path = "/"
timeout = 60
matcher = "200"
healthy_threshold = 5
unhealthy_threshold = 5
}
lifecycle {
create_before_destroy = true
}
}
task definition 구성
코드 펼치기
templete 를 활용 하여 사전에 작성한 td.json파일을 import 합니다
data "aws_iam_role" "ecs_task_execution_role" { name = "ecsTaskExecutionRole" } data "aws_ecr_repository" "repo" { count = length(var.product) name = "${var.site_name}-${var.service_name}-${var.product[count.index]}" # name = "ecr-repository" } data "template_file" "service" { template = file(var.tpl_path) count = length(var.product) vars = { region = var.region aws_ecr_repository = element(data.aws_ecr_repository.repo.*.repository_url,count.index) tag = "latest" container_port = var.container_port host_port = var.host_port app_name = var.app_name } } #output "template_file" { # value = data.template_file.service.*.vars #} #output "aws_ecs_task_definition" { # value = data.template_file.service #} resource "aws_ecs_task_definition" "service" { count = length(var.product) family = "${var.site_name}-${var.service_name}-${var.product[count.index]}" network_mode = "awsvpc" execution_role_arn = data.aws_iam_role.ecs_task_execution_role.arn cpu =2048 memory =4096 requires_compatibilities = ["FARGATE"] container_definitions = element(data.template_file.service.*.rendered,count.index) tags = { Environment = "${var.site_name}-${var.service_name}-${var.product[count.index]}" Application = "${var.site_name}-${var.service_name}-${var.product[count.index]}" } }
ECS - service 구성
코드 펼치기
data "aws_ecs_cluster" cluster {
cluster_name = "${var.site_name}-${var.service_name}"
}
data "aws_security_group" sg {
name = "${var.app_name}"
}
data "aws_lb_target_group" tg {
count = length(var.product)
name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
}
data "aws_subnet" "public_sub" {
count = length(var.availability_zones)
tags = {
Name = "public${count.index}-${var.vpc_name}"
}
}
resource "aws_ecs_service" "ecs_service" {
count = length(var.product)
name = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
cluster = data.aws_ecs_cluster.cluster.id
task_definition = element(aws_ecs_task_definition.service.*.arn,count.index)
desired_count = 3
launch_type = "FARGATE"
network_configuration {
security_groups = [data.aws_security_group.sg.id]
# count = length(var.availability_zones)
subnets = data.aws_subnet.public_sub[*].id
assign_public_ip = true
}
load_balancer {
# count = length(var.product)
target_group_arn = element(data.aws_lb_target_group.tg.*.arn,count.index)
container_name = var.app_name
container_port = var.container_port
}
# depends_on = [aws_lb_listener.https_forward, aws_lb_listener.http_forward, aws_iam_role_policy_attachment.ecs_task_execution_role]
tags = {
count = length(var.product)
Environment = "ecs_service"
Application = "${var.site_name}-${var.service_name}-${var.product[count.index]}"
}
}
결과
두 신뢰관계 정책 파일을 병합하는 jq 명령어
TrustShip
aws iam update-assume-role-policy --role-name MyRole --policy-document "$(
aws iam get-role --role-name MyRole --query "Role.AssumeRolePolicyDocument" --output json | \
jq -c '.Statement |= map(
if .Principal.AWS then
.Principal.AWS |= (if type == "string" then [.] else . end + ["arn:aws:iam::xxxx:root"] | unique)
else
.
end
)'
)"
Last updated