지난번 'terraform으로 간단한 인프라 구축하기'에 대한 내용을 작성했습니다. Terraform 기본 인프라 구축하기
비슷한 인프라 구축 요구 사항이 들어왔을 때는 해당 코드를 활용하기 좋았지만, 조금이라도 새로운 리소스 구성이 필요하면 코드의 재사용이 어렵다는 것을 느꼈습니다.
또한 비슷한 환경을 생성할 때 마다 terraform의 .tf
파일을 복사해 계속 새로운 파일을 만들어 내는 점이 비효율적이라 생각되었습니다.
💡 따라서 공통으로 사용하는 리소스를 만들어 코드를 재사용하기 위해
terraform module
을 사용하고, 팀원들과 동일 코드을 다른 환경에서 사용하기 위해terraform workspace
를 도입하기로 했습니다.
⚠️ 본 내용은 Terraform스터디를 진행하며 현업에 적용 해 본 내용을 기재하였기 때문에, AWS 및 Terraform의 기본 개념에 대해서는 생략하였습니다.
지난번 사원 👶🏻 OK는 terraform으로 기본 인프라 구성을 완료했습니다.
하지만 코드의 재사용에 있어 몇 가지 불편한 점이 있었고, 규모가 커질수록 단순한 방식으로 처리가 힘들어졌습니다.
또한 추가로 팀원들과 공동 작업을 하기 위해, 다음과 같이 코드를 개선하기로 했습니다.
terraform의 count 매개변수의 제약 사항
➡️ for_each 표현식으로 변경
구성 환경의 중복된 코드
➡️ terraform module
과 terraform workspace
를 활용하는 방식으로 코드 변경
규모가 커짐에 따라 필요한 리소스들(EC2, S3, DynamoDB 등)이 증가해, root 경로에서 파일 이름만으로 분리된 소스를 관리하는 것이 불편해짐
➡️ 폴더별로 리소스를 구분하고, root 경로에서 리소스별 모듈을 사용하는 코드로 변경
Bastion Host를 통한 Tunneling 접속 방식의 불편함
➡️ Private Server에 직접 접속할 수 있는 AWS Systems Manager(AWS SSM) 구성
👶🏻 OK는 개선이 필요한 사항을 확인하고, Terraform 구성 전에 미리 구축 내용을 작성했습니다.
VPC(Virtual Private Cloud)
다양한 네트워크 환경 구성
10.60.0.0/16
Subnet 구성
Public 환경과 Private 환경 구분
Region : ap-northeast-2 [Asia Pacific (Seoul)]
Env | AZ | Subnet | IPv4 CIDR |
---|---|---|---|
Public | ap-northeast-2a | T101-pub-sub-2a | 10.60.0.0/24 |
Public | ap-northeast-2c | T101-pub-sub-2c | 10.60.1.0/24 |
Private | ap-northeast-2a | T101-pri-sub-2a | 10.60.2.0/24 |
Private | ap-northeast-2c | T101-pri-sub-2c | 10.60.3.0/24 |
SG(Security Group)
보안그룹은 언제든 확장할 수 있게 구성
EC2 접속을 위한 On-Premise의 IP Inbound Source 정보 (IP는 보안을 위해 임의로 생성했습니다.)
CIDR | Port | Desc |
---|---|---|
151.149.23.124/32 | 22 | From Imok SSH(random test ip) |
151.149.23.124/32 | 25 | From Imok SMTP(random test ip) |
151.149.23.124/32 | 80 | From Imok HTTP(random test ip) |
EC2
다양한 환경에서 EC2 접속할 수 있게 구성
Public EC2 : pem key를 이용한 ssh 접속
Private EC2 : AWS SSM을 이용해 접속
OS : Amazon Linux 2
용도 | Name | CPU | Memory | Disk | Type |
---|---|---|---|---|---|
public-server | T101-public-server | 2 | 1GiB | 30GiB | t3.micro |
private-server | T101-private-server | 2 | 1GiB | 3oGiB | r5.micro |
IAM(Identity and Access Management)
SSM 사용 및 S3 접근을 위해 Private EC2에 IAM role
적용
AmazonSSMManagedInstanceCore
AmazonS3FullAccess
👶🏻 OK는 다음과 같이 구축하게 될 예상 아키텍처를 그려봤습니다.
👶🏻 OK는 팀원들에게 공통
terraform module
을 작성해 배포해야 했기 때문에 다음과 같은 목표를 세웠습니다.💡 서비스별로 폴더를 분리해,
root module
에서만 코드 수정하도록 환경 구성
💡 각자의 aws 환경에서 진행할 수 있도록terraform workspace
환경 구성
작업 공간을 통한 Workspaces 격리
HashiCorp 공식 문서 : Terraform Workspaces
default
작업 공간을 사용합니다. 새 작업 공간을 만들거나 전환하려면 terraform workspace
명령을 사용합니다.terraform.tfstate.d
라는 폴더 아래에 workspace 별로 폴더가 생성되고, 각각 상태 파일인 .tfstate
파일이 관리되는 형태입니다.
# workspace 구조 예시
├── terraform.tfstate.d
│ ├── imok
│ │ ├── terraform.tfstate
│ │ └── terraform.tfstate.backup
│ └── project
│ ├── terraform.tfstate
│ └── terraform.tfstate.backup
새 workspace 생성
terraform workspace new [workspace name]
workspace 전환
terraform workspace select [workspace name]
workspace 리스트 확인
terraform workspace list
현재 workspace 확인
terraform workspace show
terraform 프로젝트를 두 개 이상의 어카운트에서 사용해야 할 경우, aws credential
의 profile
이름을 workspace 이름과 일치시키는 방법을 사용합니다.
~/.aws/credentials
파일 예[imok]
aws_access_key_id = []
aws_secret_access_key = []
[workspace name]
aws_access_key_id = []
aws_secret_access_key = []
...
root.tf
EC2
, VPC
, IAM
, SG
child module에서 모듈이 생성하는 resource
는 보통의 리소스를 생성하는 코드와 동일하지만, root module(모듈을 사용하는 코드)에서 건네주는 변수들을 사용해서 리소스를 생성해야 합니다.
저는 root module을 root.tf
파일로 정의하고, 해당 파일에서 모듈을 사용하는 코드를 작성했습니다.
var.az_names
와 같이 root module에서 설정한 변수를 받아와 child module에서 리소스를 생성할 수 있습니다.
이러한 변수를 받아오기 위해서는 child module에서 variable
을 설정해주어야 합니다.
child module에서 리소스를 생성한 후, 생성한 리소스에 대한 정보(arn, id 등)를 받기 위해 child module에서 output
을 통해 리소스 정보를 출력해주어야 합니다.
root module에서는 module.<MODULE_NAME>.id
와 같이 output
을 받아올 수 있습니다.
⚠️ 해당 내용이 모듈을 도입하면서 제일 많이 헷갈리고, 제일 많은 오류를 겪었던 부분입니다. 😵
반복해서 환경을 구성해보면서 이해하는 것이 좋습니다.
저는 다음과 같이 root module과 서비스 별 child module영역으로 구분해 코드를 작성했습니다.
.
├── ec2
│ ├── ami.tf
│ ├── key-pair.tf
│ ├── main.tf
│ ├── outputs.tf
│ ├── user_data
│ │ ├── user_data_private.sh
│ │ └── user_data_public.sh
│ └── variables.tf
├── iam
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── sg
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├──vpc
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── terraform.tfstate.d # workspace
│ └── imok
│ ├── terraform.tfstate
│ └── terraform.tfstate.backup
├── terraform.tfvars
├── outputs.tf
├── provider.tf
├── root.tf # root module
└── variables.tf
지난 번 구성과 다른 점은 count
를 for_each
문으로 변경했다는 점입니다.
for each 표현식에 대한 설명은 제 블로그 내용 참고 부탁드립니다. Terraform 반복문 Loops 사용하기
main.tf
파일 구성
resource "aws_subnet" "private" {
for_each = var.private_subnets
vpc_id = aws_vpc.vpc.id
cidr_block = each.value["cidr"]
availability_zone = each.value["zone"]
tags = merge(
{
Name = format(
"%s-pri-sub-%s",
var.name,
element(split("_", each.key), 2)
)
},
var.tags,
)
}
variables.tf
파일 구성
root module에서 정의한 변수를 받아오기 위해 다음과 같이 variable
을 설정했습니다.
# project name
variable "name" {}
# VPC default CIDR
variable "vpc_cidr" {}
# Availability Zones
variable "az_names" {}
# public subnet list
variable "public_subnets" {}
# private subnet list
variable "private_subnets" {}
# Tags
variable "tags" {}
output.tf
파일 구성
ec2 리소스에서 vpc의 subnet id
변수를 사용하기 위해 output
에 정의했습니다.
output "public_subnet_ids" {
value = values(aws_subnet.public)[*].id
}
output "private_subnet_ids" {
value = values(aws_subnet.private)[*].id
}
main.tf
파일 구성미리 ami.tf
파일에서 정의한 ami 중 최신 amazon linux2의 이미지를 선택했고, 인스턴스 타입, 볼륨 등을 설정했습니다.
user_data
는 미리 파일을 구성해서 ${path.module}
를 사용해 해당 모듈 경로에 있는 파일을 불러와 인스턴스를 생성했습니다.
resource "aws_instance" "private" {
key_name = var.key_name
ami = data.aws_ami.amazon_linux2_kernel_5.id
instance_type = var.ec2_type_private
vpc_security_group_ids = [var.security_group_id_private]
subnet_id = var.pri_sub_ids[0]
iam_instance_profile = var.iam_instance_profile
disable_api_termination = var.instance_disable_termination
user_data = file("${path.module}/user_data/user_data_private.sh")
root_block_device {
volume_size = 10
volume_type = "gp3"
delete_on_termination = true
}
tags = merge(
{
Name = format(
"%s-private-server",
var.name
)
},
var.tags,
)
}
variables.tf
파일 구성
root module에서 정의한 변수를 받아오기 위해 다음과 같이 variable
을 설정했습니다.
다른 모듈의 output
에서 정의된 변수를 여기에서 받아 옵니다.
# For EC2
variable "name" {}
variable "tags" {}
variable "az_names" {}
variable "instance_disable_termination" {}
variable "key_name" {}
variable "ec2_type_public" {}
variable "ec2_type_private" {}
variable "volume_size" {}
variable "public_subnets" {}
variable "private_subnets" {}
# From module VPC
variable "pub_sub_ids" {}
variable "pri_sub_ids" {}
# From module IAM
variable "iam_instance_profile" {}
# From module SG
variable "security_group_id_public" {}
variable "security_group_id_private" {}
output.tf
파일 구성
root module의 output
에서 ec2 접속을 위한 정보를 확인하기 위해 다음과 같이 필요한 변수들을 정의했습니다.
output "key_pair" {
value = var.key_name
}
output "public_eip" {
value = aws_eip.public.public_ip
}
output "ec2_private_id" {
value = aws_instance.private.id
}
key-pair.tf
파일 구성
${path.module}
를 사용해 local의 모듈 경로에 key file을 생성합니다.
# Generates a secure private key and encodes it as PEM
resource "tls_private_key" "key_pair" {
algorithm = "RSA"
rsa_bits = 4096
}
# Create the Key Pair
resource "aws_key_pair" "key_pair" {
key_name = "${var.name}-key"
public_key = tls_private_key.key_pair.public_key_openssh
}
# Save Pem Key
resource "local_file" "ssh_key" {
filename = "${path.module}/${aws_key_pair.key_pair.key_name}.pem"
content = tls_private_key.key_pair.private_key_pem
}
SG 구성은 지난번 구성과 거의 동일하고, locals
변수를 추가해 tag에 활용했습니다.
main.tf
파일 구성
locals {
public_sg = format("%s-%s-sg", var.name, "public")
private_sg = format("%s-%s-sg", var.name, "private")
}
resource "aws_security_group" "private" {
name = local.private_sg
description = "private security group for ${var.name}"
vpc_id = var.vpc_id
# inbound rule
dynamic "ingress" {
for_each = [for s in var.private_ingress_rules : {
from_port = s.from_port
to_port = s.to_port
desc = s.desc
cidrs = [s.cidr]
}]
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
cidr_blocks = ingress.value.cidrs
protocol = "tcp"
description = ingress.value.desc
}
}
# outbound rule
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(
{
Name = local.private_sg
},
var.tags
)
}
variables.tf
파일 구성
root module에서 정의한 변수를 받아오기 위해 다음과 같이 variable
을 설정했습니다.
# Project name
variable "name" {}
# Tags
variable "tags" {}
# public ingress IP list
variable "public_ingress_rules" {}
# private ingress IP list
variable "private_ingress_rules" {}
# From module VPC
variable "vpc_id" {}
output.tf
파일 구성
ec2 리소스에서 sg의 security_group_id
변수를 사용하기 위해 output
에 정의했습니다.
output "security_group_id_public" {
value = aws_security_group.public.id
}
output "security_group_id_private" {
value = aws_security_group.private.id
}
Private EC2에 AWS SSM을 통해 접속하기 위한 IAM Role 구성입니다.
SSM의 AmazonSSMManagedInstanceCore
정책과 S3의 AmazonS3FullAccess
정책을 붙였습니다.
main.tf
파일 구성
# IAM role
resource "aws_iam_role" "private" {
name = format("%s-private-role", lower(var.name))
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
# Attaches a Managed IAM Policy to an IAM role
resource "aws_iam_role_policy_attachment" "private_for_s3" {
role = aws_iam_role.private.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
resource "aws_iam_role_policy_attachment" "private_for_ssm" {
role = aws_iam_role.private.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# IAM instance profile
resource "aws_iam_instance_profile" "private" {
name = format("%s-private", lower(var.name))
role = aws_iam_role.private.name
}
variables.tf
파일 구성
root module에서 정의한 변수를 받아오기 위해 다음과 같이 variable
을 설정했습니다.
# Project name
variable "name" {}
# Tags
variable "tags" {}
output.tf
파일 구성
ec2 리소스에서 iam의 iam_instance_profile
변수를 사용하기 위해 output
에 정의했습니다.
output "iam_instance_profile" {
value = aws_iam_instance_profile.private.name
}
provider.tf
파일 구성
terraform.workspace
를 사용해 workspace를 변경할 때마다 환경이 적용되도록 구성했습니다.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.45.0"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.region
profile = terraform.workspace
}
root.tf
파일 구성
root module에서 child module을 사용하기 위한 코드를 작성했습니다.
각각의 모듈의 경로를 source = "./vpc"
다음과 같이 정의하고 시작하고, child module에 건네주기 위한 변수를 정의합니다.
이제 파일 한 곳에서 모든 리소스를 통제할 수 있습니다. 🤗
module "vpc" {
# Required
source = "./vpc"
# environment = var.environment
name = var.name
tags = var.tags
az_names = var.az_names
vpc_cidr = var.vpc_cidr
public_subnets = var.public_subnets
private_subnets = var.private_subnets
}
module "iam" {
# Required
source = "./iam"
name = var.name
tags = var.tags
}
module "ec2" {
# Required
source = "./ec2"
name = var.name
tags = var.tags
az_names = var.az_names
public_subnets = var.public_subnets
private_subnets = var.private_subnets
# module vpc
pub_sub_ids = module.vpc.public_subnet_ids
pri_sub_ids = module.vpc.private_subnet_ids
# module iam
iam_instance_profile = module.iam.iam_instance_profile
# module sg
security_group_id_public = module.sg.security_group_id_public
security_group_id_private = module.sg.security_group_id_private
instance_disable_termination = var.instance_disable_termination
key_name = "${var.name}-key"
volume_size = var.ec2_volume_size
ec2_type_public = var.ec2_type_public
ec2_type_private = var.ec2_type_private
}
module "sg" {
# Required
source = "./sg"
name = var.name
tags = var.tags
public_ingress_rules = var.public_ingress_rules
private_ingress_rules = var.private_ingress_rules
# module vpc
vpc_id = module.vpc.vpc_id
}
terraform.tfvars.tf
파일 구성
variables에서 사용하기 위한 변수를 tfvars.tf
에서 정의했습니다.
# Terraform setting
environment = "dev"
region = "ap-northeast-2"
tags = {
MadeBy = "imok"
}
# Project name
name = "T101"
# Network setting
vpc_cidr = "10.60.0.0/16"
az_names = [
"ap-northeast-2a",
"ap-northeast-2c"
]
public_subnets = {
pub_sub_2a = {
zone = "ap-northeast-2a"
cidr = "10.60.0.0/24"
},
pub_sub_2c = {
zone = "ap-northeast-2c"
cidr = "10.60.1.0/24"
}
}
variables.tf
파일 구성
root module 구성에 필요한 모든 변수를 정의합니다.
# env - e.g: dev|prd|stage
variable "environment" {}
# Region - e.g: ap-northeast-2
variable "region" {}
# project name
variable "name" {}
# EC2 instance type
variable "ec2_type_public" {}
variable "ec2_type_private" {}
# EC2 volume size
variable "ec2_volume_size" {}
# EC2 termination protection
variable "instance_disable_termination" {}
# VPC default CIDR
variable "vpc_cidr" {}
# Availability Zones
variable "az_names" {}
# public subnet list
variable "public_subnets" {}
# private subnet list
variable "private_subnets" {}
# Tag
variable "tags" {}
# public ingress IP list
variable "public_ingress_rules" {}
# private ingress IP list
variable "private_ingress_rules" {}
# DB port
variable "db_port" {}
output.tf
파일 구성
최종 출력이 필요한 output을 모두 정의합니다.
output "vpc_id" {
value = module.vpc.vpc_id
}
output "public_subnet_ids" {
value = module.vpc.public_subnet_ids
}
output "private_subnet_ids" {
value = module.vpc.private_subnet_ids
}
output "nat_eip" {
value = module.vpc.nat_eip
}
output "key_pair" {
value = module.ec2.key_pair
}
output "public_eip" {
value = module.ec2.public_eip
}
output "ec2_private_id" {
value = module.ec2.ec2_private_id
}
terraform output
VPC - Subnet
IAM Role
EC2
ssh -i "T101-key.pem" ec2-user@15.164.88.41
aws ssm start-session \
--target i-080061b0ee6c5e7bb
👶🏻 OK는 이제
root module
파일 한 곳에서 모든 리소스를 통제, 관리할 수 있게 되었습니다. 💜
완성 코드는 제 깃허브에 올려놨습니다 참고 부탁드립니다 :)
https://github.com/euneun316/terraform-study/tree/main/Modules/default
사실 terraform module은 이미 잘 구성된 코드들이 많습니다. 하지만, 정확하게 module에 대해서 파악하지 않으면 처음부터 그 소스를 가져와 활용하기 무척 어렵습니다.
이번에 terraform module을 직접 구성하고 나니, 이제야 전반적으로 terraform이 동작하는 원리에 대해 더 정확히 파악되었습니다.
현재는 EC2, IAM, VPC, SG와 같은 기본 서비스 구성만 완료된 상태지만,
앞으로 kinesis, glue, athena와 같은 타 서비스 생성에 대한 기본 코드도 작성해 놓고, 필요할 때마다 코드를 활용할 수 있도록 module을 고도화할 생각입니다.
테라폼 모듈의 고수가 될 그날까지... 🏃🏻♀️
와우! 개선 사항 정리 부터, 실제 개선 내용을 코드로 반영해서 배포 내용까지 잘 정리해주셨네요.
8주 기간 동안 매주 주말 마다 스터디 참여해서 과제 제출까지 고생 많으셨습니다.