Terraform module을 활용한 기본 인프라 구축
시작하기 앞서..
지난 게시글에서는 ‘Terraform으로 간단한 인프라 구축하기’에 대해 작성 해 보았습니다.Terraform으로 기본 인프라 구축하기
비슷한 인프라 구축 요구 사항이 들어왔을 때는 해당 코드를 활용하기 좋았지만, 조금이라도 새로운 리소스 구성이 필요하면 코드의 재사용이 어렵다는 것을 느꼈습니다.
또한 비슷한 환경을 생성할 때 마다 terraform의 .tf
파일을 복사해 계속 새로운 파일을 만들어 내는 점이 비효율적이라 생각되었습니다.
💡 따라서 공통으로 사용하는 리소스를 만들어 코드를 재사용하기 위해
terraform module
을 사용하고, 팀원들과 동일 코드 다른 환경에서 사용하기 위해terraform workspace
를 도입하기로 했습니다.
⚠️ 본 내용은 Terraform스터디를 진행하며 현업에 적용 해 본 내용을 기재하였기 때문에, AWS 및 Terraform의 기본 개념에 대해서는 생략하였습니다.
인프라 구축 전 체크 사항
지난번 사원 👶🏻 OK는 terraform으로 기본 인프라 구성을 완료했습니다.
하지만 코드 재 사용에 몇 가지 불편한 점이 있었고, 규모가 커질수록 단순한 방식으로는 처리가 어려워졌습니다.
추가적으로 팀원들과 공동 작업을 하기 위해, 다음과 같이 코드를 개선하기로 했습니다.
인프라 개선 필요 사항 체크 ✅
- terraform의 count 매개변수의 제약 사항
- 전체 리소스를 반복할 수는 있지만 리소스 내에서 인라인 블록을 반복할 수는 없음
- 리소스가 배열의 형식으로 생성되기 때문에, 어떤 리소스가 생성되는지 명확히 알 수 없어, 수정 시 에러 발생 가능성이 높음
➡️ for_each 표현식으로 변경
2. 구성 환경의 중복된 코드
- 개발(Dev)과 운영(Prod) 환경은 일부 속성만 다르기 때문에 코드에 중복된 내용이 상당히 많았음
➡️ terraform module
과 terraform workspace
를 활용하는 방식으로 코드 변경
3. 규모가 커짐에 따라 필요한 리소스(EC2, S3, DynamoDB 등)이 증가해, root 경로에서 파일 이름만으로 분리된 소스를 관리하는 것이 불편해짐
➡️ 폴더별로 리소스를 구분하고, root 경로에서 리소스별 모듈을 사용하는 코드로 변경
4. Bastion Host를 통한 Tunneling 접속 방식의 불편함
➡️ Private Server에 직접 접속할 수 있는 AWS Systems Manager(AWS SSM) 구성
구축 내용 ✅
👶🏻 OK는 개선이 필요한 사항을 확인하고, Terraform 구성 전에 미리 구축 내용을 작성했습니다.
- VPC(Virtual Private Cloud)
다양한 네트워크 환경 구성
- IP 대역 :
10.60.0.0/16
- Internet Gateway
- NAT gateways
- Route tables
2. Subnet 구성
Public 환경과 Private 환경 구분
- Region : ap-northeast-2 [Asia Pacific (Seoul)]
3. SG(Security Group)
보안그룹은 언제든 확장할 수 있게 구성
- EC2 접속을 위한 On-Premise의 IP Inbound Source 정보 (IP는 보안을 위해 임의로 생성했습니다.)
4. EC2
다양한 환경에서 EC2에 접속할 수 있게 구성
- Public EC2 : pem key를 이용한 ssh 접속
- Private EC2 : AWS SSM을 이용해 접속
- OS : Amazon Linux 2
5. IAM(Identity and Access Management)
AWS SSM(AWS Systems Manager) 사용 및 S3 접근을 위해 Private EC2에 IAM role
적용
- AWS SSM Policy :
AmazonSSMManagedInstanceCore
- Amazon S3 Policy :
AmazonS3FullAccess
구축 예상 아키텍처 ✅
👶🏻 OK는 다음과 같이 구축하게 될 예상 아키텍처를 그려봤습니다.
인프라 구축
👶🏻 OK는 팀원들에게 공통
terraform module
을 작성해 배포해야 했기 때문에 다음과 같은 목표를 세웠습니다.💡 서비스별로 폴더를 분리해,
root module
에서만 코드 수정하도록 환경 구성
💡 각자의 aws 환경에서 진행할 수 있도록terraform workspace
환경 구성
Terraform Workspace 구성
작업 공간을 통한 Workspaces 격리
HashiCorp 공식 문서 : Terraform Workspaces
- 테라폼은 기본
default
작업 공간을 사용합니다. 새 작업 공간을 만들거나 전환하려면terraform workspace
명령을 사용합니다. - 작업 공간은 code refactoring을 시도하는 것 같이, 이미 배포된 인프라에 영향을 주지 않고 테라폼 모듈을 테스트할 때 유용합니다.
workspace 구조
terraform.tfstate.d
라는 폴더 아래에 workspace 별로 폴더가 생성되고, 상태 파일인 .tfstate
파일이 각각 관리되는 형태입니다.
# workspace 구조 예시
├── terraform.tfstate.d
│ ├── imok
│ │ ├── terraform.tfstate
│ │ └── terraform.tfstate.backup
│ └── project
│ ├── terraform.tfstate
│ └── terraform.tfstate.backup
workspace 사용
- 새 workspace 생성
terraform workspace new [workspace name]
2. workspace 전환
terraform workspace select [workspace name]
3. workspace 리스트 확인
terraform workspace list
4. 현재 workspace 확인
terraform workspace show
aws credentials 사용
terraform 프로젝트를 두 개 이상의 어카운트에서 사용해야 할 경우, aws credential
의 profile
이름을 workspace 이름과 일치시키는 방법을 사용합니다.
- workspace name == credentials profile == imok
# ~/.aws/credentials 파일 예
[imok]
aws_access_key_id = []
aws_secret_access_key = []
[workspace name]
aws_access_key_id = []
aws_secret_access_key = []
...
Terraform module 구성
- root module : 실제로 수행하게 되는 작업 디렉터리의 terraform 코드 모음
root.tf
2. child module : root module에서 리소스를 생성하기 위해 참조하고 있는 module block
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
VPC Resource
지난 번 구성과 다른 점은 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,
)
}
2. 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" {}
3. 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
}
EC2 Resource
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,
)
}
2. 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" {}
3. 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
}
4. 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 Resource
SG 구성은 지난번 구성과 거의 동일하고, locals
변수를 추가해 tag에 활용했습니다.
main.tf
파일 구성
# locals에 변수명 구성
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
)
}
2. 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" {}
3. 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
}
IAM Resource
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
}
2. variables.tf
파일 구성
root module에서 정의한 변수를 받아오기 위해 다음과 같이 variable
을 설정했습니다.
# Project name
variable "name" {}
# Tags
variable "tags" {}
3. output.tf
파일 구성
ec2 리소스에서 iam의 iam_instance_profile
변수를 사용하기 위해 output
에 정의했습니다.
output "iam_instance_profile" {
value = aws_iam_instance_profile.private.name
}
root module
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
}
2. 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
}
3. 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"
}
}
4. 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" {}
5. 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 실행
terraform output 확인
terraform output
- instance id 확인 : i-080061b0ee6c5e7bb
- key-pair 확인 : T101-key
- public ip 확인 : 15.164.88.41
생성된 리소스 및 접속 확인
- VPC — Subnet
2. IAM Role
3. EC2
- Public EC2 접속 : pem key 사용
ssh -i "T101-key.pem" ec2-user@15.164.88.41
- Private EC2 접속 및 S3 접근 : aws ssm, s3 정책 적용한 iam role 사용
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을 고도화할 생각입니다.
테라폼 모듈의 고수가 될 그날까지… 🏃🏻♀️