인프라 구축기
인프라 구축기 (2)에서 Terraform을 활용하여 VPC를 구성하는 코드를 작성하였다. 이어서 Instance와 Storage를 구성하는 코드를 작성해 볼 것이다. 추가로 vpc, instance, storage 폴더로 구분하여 각 역할에 맞게 분리하고, 하나의 파일 실행으로 인프라 구성이 진행되도록 해보려고 한다.
최종 파일 구조
최종적인 파일 구조는 다음과 같다. instance, storage, vpc로 구분한 폴더에 역할에 맞는 .tf 파일을 저장하였다. 최상단에 존재하는 main.tf를 실행하게 되면, 작성한 모든 요소가 생성된다.
# tree /f
aws-terraform
│
│ main.tf
│
├───instance
│ ec2.tf
│ variables.tf
│
├───storage
│ rds.tf
│ redshift_serverless.tf
│ s3.tf
│ variables.tf
│
└───vpc
internet_gateway.tf
nat_gateway.tf
output.tf
route_table.tf
s3_endpoint.tf
security_group.tf
subnet.tf
vpc.tf
vpc
세 개의 요소 중 vpc를 가장 먼저 소개하려고 한다. 대부분은 인프라 구축기 (2)에서 설명했던 부분이기에 새롭게 추가된 output.tf, security_group.tf 코드만 소개할 것이다.
output.tf
일반적으로 다른 폴더에 존재하는 tf 파일의 변수 값을 가져오지 못한다. 따라서 instance, storage 폴더의 .tf 파일에서 vpc 폴더에서 사용된 변수를 가져오려면, output과 variable을 사용해야 한다.
- output : 사용할 변수가 저장된 폴더 (vpc)에서 선언하여 value에 원하는 값을 입력
- variable : 변수를 사용할 폴더 (instance, storage)에서 선언하여 value를 사용 가능
# vpc & subnet ids
output "vpc_id" {
value = aws_vpc.popboard.id
}
output "public_subnet_2a_1_id" {
value = aws_subnet.public_subnet_2a_1.id
}
output "private_subnet_2a_1_id" {
value = aws_subnet.private_subnet_2a_1.id
}
output "private_subnet_2a_2_id" {
value = aws_subnet.private_subnet_2a_2.id
}
output "private_subnet_2b_1_id" {
value = aws_subnet.private_subnet_2b_1.id
}
output "private_subnet_2b_2_id" {
value = aws_subnet.private_subnet_2b_2.id
}
output "private_subnet_2c_1_id" {
value = aws_subnet.private_subnet_2c_1.id
}
output "private_subnet_2c_2_id" {
value = aws_subnet.private_subnet_2c_2.id
}
output "private_subnet_2c_3_id" {
value = aws_subnet.private_subnet_2c_3.id
}
# security group ids
output "ssh_access_sg_id" {
value = aws_security_group.ssh_access_sg.id
}
output "airflow_web_access_sg_id" {
value = aws_security_group.airflow_web_access_sg.id
}
output "all_outbound_traffic_sg_id" {
value = aws_security_group.all_outbound_traffic_sg.id
}
output "redshift_access_sg_id" {
value = aws_security_group.redshift_access_sg.id
}
output "airflow_meta_db_access_sg_id" {
value = aws_security_group.airflow_meta_db_access_sg.id
}
security_group.tf
- 역할에 따라 그룹을 세분화하여 보안 그룹 생성 (ssh, outbound, airflow-web, redshift, rds)
- ssh_access_sg : inbound - port 22
- all_outbound_traffic_sg : outbound - all
- airflow_web_access_sg : inbound - port 8080
- redshift_access_sg : inbound - port 5439
- airflow_meta_db_access_sg : inbound - port 5432
- 이후 instance, storage에서 ec2, redshift, rds 등에서 생성된 보안 그룹을 연결
- 참고 : 인프라 구축기 (4)에서 Bastion Host -> Private Subnet 접근 확인 중 코드 수정
# security group : ssh access
resource "aws_security_group" "ssh_access_sg" {
name = "popboard-ssh-access-sg"
description = "Security Group For SSH Access"
vpc_id = aws_vpc.popboard.id
tags = {
Name = "popboard-ssh-access-sg"
}
}
resource "aws_security_group_rule" "ssh_access_ipv4" {
type = "ingress"
security_group_id = aws_security_group.ssh_access_sg.id
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [
# 허용 IP 설정
]
}
# security group : all outbound traffic
resource "aws_security_group" "all_outbound_traffic_sg" {
name = "popboard-all-outbound-traffic-sg"
description = "Security Group For Outbound"
vpc_id = aws_vpc.popboard.id
tags = {
Name = "popboard-all-outbound-traffic-sg"
}
}
resource "aws_vpc_security_group_egress_rule" "all_outbound_traffic_ipv4" {
security_group_id = aws_security_group.all_outbound_traffic_sg.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
# security group : airflow web access
resource "aws_security_group" "airflow_web_access_sg" {
name = "popboard-airflow-web-access-sg"
description = "Security Group For Bastion Host"
vpc_id = aws_vpc.popboard.id
tags = {
Name = "popboard-airflow-web-access-sg"
}
}
resource "aws_security_group_rule" "airflow_web_access_ipv4" {
type = "ingress"
security_group_id = aws_security_group.airflow_web_access_sg.id
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = [
# 허용 IP 설정
]
}
# security group : redshift access
resource "aws_security_group" "redshift_access_sg" {
name = "popboard-redshift-access-sg"
description = "Security Group For Redshift"
vpc_id = aws_vpc.popboard.id
tags = {
Name = "popboard-redshift-access-sg"
}
}
resource "aws_security_group_rule" "redshift_access_ipv4" {
type = "ingress"
security_group_id = aws_security_group.redshift_access_sg.id
from_port = 5439
to_port = 5439
protocol = "tcp"
cidr_blocks = [
# 허용 IP 설정
]
}
# security group : airflow meta db access
resource "aws_security_group" "airflow_meta_db_access_sg" {
name = "popboard-airflow-meta-db-access-sg"
description = "Security Group For Redshift"
vpc_id = aws_vpc.popboard.id
tags = {
Name = "popboard-airflow-meta-db-access-sg"
}
}
resource "aws_security_group_rule" "airflow_meta_db_access_ipv4" {
type = "ingress"
security_group_id = aws_security_group.airflow_meta_db_access_sg.id
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [
# 허용 IP 설정
]
}
instance
ec2.tf
- var. 형태로 vpc에서 output.tf로 작성했던 변수를 가져와 사용
- 키 페어는 비용이 들지 않기에 미리 AWS에 생성해 두고 사용
- bastion host
- ami : ami-040c33c6a51fd5d96 (Ubuntu Server 24.04 LTS (HVM), SSD Volume Type)
- instance_type : t2.micro (free tier)
- subnet : public_subnet_2b_1
- volume : gp3, 8GB
- Airflow
- ami : ami-040c33c6a51fd5d96 (Ubuntu Server 24.04 LTS (HVM), SSD Volume Type)
- instance_type : t3.large
- subnet : private_subnet_2c_3
- volume : gp3, 15GB
# bastion host
resource "aws_instance" "bastion_host" {
ami = "ami-040c33c6a51fd5d96"
instance_type = "t2.micro"
subnet_id = var.public_subnet_2a_1_id
key_name = "popboard-bastion-host-keypair"
vpc_security_group_ids = [
var.ssh_access_sg_id,
var.all_outbound_traffic_sg_id
]
root_block_device {
volume_type = "gp3"
volume_size = 8
delete_on_termination = true
}
tags = {
Name = "popboard-bastion-host"
}
}
# airflow
resource "aws_instance" "airflow" {
ami = "ami-040c33c6a51fd5d96"
instance_type = "t3.large"
subnet_id = var.private_subnet_2c_3_id
key_name = "popboard-airflow-keypair"
vpc_security_group_ids = [
var.ssh_access_sg_id,
var.airflow_web_access_sg_id,
var.all_outbound_traffic_sg_id
]
root_block_device {
volume_type = "gp3"
volume_size = 15
delete_on_termination = true
}
tags = {
Name = "popboard-airflow"
}
}
variables.tf
- vpc에서 output.tf로 작성했던 것 중 사용할 변수를 variable로 선언
# vpc & subnet ids
variable "vpc_id" {
description = "vpc id for security group"
type = string
}
variable "public_subnet_2a_1_id" {
description = "subnet id for ec2, bastion host"
type = string
}
variable "private_subnet_2c_3_id" {
description = "subnet id for ec2, airflow"
type = string
}
# security groups ids
variable "ssh_access_sg_id" {
description = "security group id for ssh access"
type = string
}
variable "airflow_web_access_sg_id" {
description = "security group id for airflow web access"
type = string
}
variable "all_outbound_traffic_sg_id" {
description = "security group id for all outbound traffic"
type = string
}
storage
rds와 redshift serverless를 생성할 때의 password로 data.aws_ssm_parameter.db_password.value를 사용한다. 이는 aws에서 제공하는 ssm parameter store로 민감한 정보를 aws 내부에 저장하여 사용할 수 있는 서비스이다.
rds.tf
- airflow meta db로 사용할 rds 생성
- subnet group : private_subnet_2a_2, private_subnet_2b_2, private_subnet_2c_2
- engine : postgres (16.3)
- volume : gp3, 100GB, db.t4g.micro (free tier)
- port : 5432
# VPC 내에서 사용할 RDS 서브넷 그룹
resource "aws_db_subnet_group" "rds_subnet_group" {
name = "popboard-rds-subnet-group"
description = "Subnet group for RDS within the popboard VPC"
subnet_ids = [
var.private_subnet_2a_2_id,
var.private_subnet_2b_2_id,
var.private_subnet_2c_2_id
]
tags = {
Name = "popboard-rds-subnet-group"
}
}
resource "aws_db_instance" "airflow_meta_db" {
depends_on = [aws_db_subnet_group.rds_subnet_group]
engine = "postgres"
engine_version = "16.3"
multi_az = false
identifier = "popboard-airflow-meta-db"
availability_zone = "ap-northeast-2c"
skip_final_snapshot = true
username = "postgres"
password = data.aws_ssm_parameter.db_password.value
storage_type = "gp3"
allocated_storage = 100
instance_class = "db.t4g.micro"
db_subnet_group_name = aws_db_subnet_group.rds_subnet_group.name
publicly_accessible = false
vpc_security_group_ids = [
var.airflow_meta_db_access_sg_id,
var.all_outbound_traffic_sg_id
]
port = 5432
monitoring_interval = 0
parameter_group_name = "default.postgres16"
}
redshift_serverless.tf
- redshift serverless (namespace, workgroup) 생성
- iam_roles의 경우 미리 aws에 생성해 두고 가져오도록 설정 (S3FullAccess)
- base_capacity : 32 (RPU)
- subnet : private_subnet_2a_1, private_subnet_2b_1, private_subnet_2c_1
- 퍼블릭 접근 차단
# namespace
resource "aws_redshiftserverless_namespace" "namespace" {
namespace_name = "popboard-redshiftserverless-namespace"
db_name = "dev"
admin_username = "redshift"
admin_user_password = data.aws_ssm_parameter.db_password.value
iam_roles = [data.aws_iam_role.popboard-redshift-role.arn]
tags = {
Name = "popboard-redshiftserverless-namespace"
}
}
# workgroup
resource "aws_redshiftserverless_workgroup" "workgroup" {
depends_on = [aws_redshiftserverless_namespace.namespace]
namespace_name = aws_redshiftserverless_namespace.namespace.id
workgroup_name = "popboard-redshiftserverless-workgroup"
base_capacity = 32
security_group_ids = [
var.redshift_access_sg_id,
var.all_outbound_traffic_sg_id
]
subnet_ids = [
var.private_subnet_2a_1_id,
var.private_subnet_2b_1_id,
var.private_subnet_2c_1_id
]
publicly_accessible = false
}
s3.tf
- s3 생성
resource "aws_s3_bucket" "s3_bucket" {
bucket = "popboard-s3-bucket"
tags = {
Name = "popboard-s3-bucket"
}
}
variables.tf
- ssm paratemer store에서 db_password 가져오기
- redshift role 가져오기
- vpc에서 output.tf로 작성했던 것 중 사용할 변수를 variable로 선언
# db_password
data "aws_ssm_parameter" "db_password" {
name = "/popboard/db_password"
with_decryption = true
}
# redshift role
data "aws_iam_role" "popboard-redshift-role" {
name = "popboard-redshift-role" # 콘솔에서 생성한 역할 이름
}
# vpc & subnet ids
variable "private_subnet_2a_1_id" {
description = "subnet id for redshift serverless"
type = string
}
variable "private_subnet_2a_2_id" {
description = "subnet id for rds, airflow meta db"
type = string
}
variable "private_subnet_2b_1_id" {
description = "subnet id for redshift serverless"
type = string
}
variable "private_subnet_2b_2_id" {
description = "subnet id for rds, airflow meta db"
type = string
}
variable "private_subnet_2c_1_id" {
description = "subnet id for redshift serverless"
type = string
}
variable "private_subnet_2c_2_id" {
description = "subnet id for rds, airflow meta db"
type = string
}
# security groups ids
variable "redshift_access_sg_id" {
description = "security group id for redshift access"
type = string
}
variable "airflow_meta_db_access_sg_id" {
description = "security group id for airflow meta db access"
type = string
}
variable "all_outbound_traffic_sg_id" {
description = "security group id for all outbound traffic"
type = string
}
main.tf
- main.tf 파일을 실행해 모든 인프라를 구성할 수 있도록 작성
provider "aws" {
region = "ap-northeast-2"
}
module "vpc" {
source = "./vpc"
}
module "instance" {
depends_on = [module.vpc]
source = "./instance"
# variables
vpc_id = module.vpc.vpc_id
public_subnet_2a_1_id = module.vpc.public_subnet_2a_1_id
private_subnet_2c_3_id = module.vpc.private_subnet_2c_3_id
ssh_access_sg_id = module.vpc.ssh_access_sg_id
all_outbound_traffic_sg_id = module.vpc.all_outbound_traffic_sg_id
airflow_web_access_sg_id = module.vpc.airflow_web_access_sg_id
}
module "storage" {
depends_on = [module.vpc]
source = "./storage"
# variables
private_subnet_2a_1_id = module.vpc.private_subnet_2a_1_id
private_subnet_2a_2_id = module.vpc.private_subnet_2a_2_id
private_subnet_2b_1_id = module.vpc.private_subnet_2b_1_id
private_subnet_2b_2_id = module.vpc.private_subnet_2b_2_id
private_subnet_2c_1_id = module.vpc.private_subnet_2c_1_id
private_subnet_2c_2_id = module.vpc.private_subnet_2c_2_id
redshift_access_sg_id = module.vpc.redshift_access_sg_id
airflow_meta_db_access_sg_id = module.vpc.airflow_meta_db_access_sg_id
all_outbound_traffic_sg_id = module.vpc.all_outbound_traffic_sg_id
}
실행 결과
main.tf가 존재하는 경로에서 terraform apply로 실행하였다. VPC 이외의 ec2, redshift, rds, s3가 생성된 것을 확인할 수 있다.
- 작업 내용
- 각 instance, storage에 연결될 보안 그룹 생성
- bastion host, airflow를 위한 ec2 생성
- DW를 위한 Redshift Serverless, Meta DB를 위한 PostgreSQL RDS, Data Lake S3 생성
고민 과정
Terraform 폴더 구성 방식
원래 vpc 폴더와 나머지는 모두 최상단에 위치하게 하려고 하였다. 그러나 각 역할에 맞는 폴더를 구성해 가독성을 높이고자 하였다. 그 결과 세 개의 폴더 (vpc, instance, storage)로 분리하게 되었고, 자세한 내용은 아래와 같다.
- Terraform 폴더 구조 및 사용법을 참고하여 Terraform의 폴더 구성
- 변경이 적고, 네트워크와 관련된 부분은 vpc 폴더에 저장
- security_group.tf의 경우 instance, storage로 분리할 경우 코드 중복이 생겨 vpc에 위치
- 폴더 구성 변경 후 생긴 문제점
- 다른 폴더의 변수에 접근 불가 : output - variable을 활용해 해결
- 폴더마다 실행 명령어 입력 : 최상단에 main.tf 및 module을 활용해 해결
Airflow Web 접근을 위한 ALB or Public Subnet + Nginx
- Airflow 웹 서버 접속 방법
- ALB를 활용해 ALB 도메인으로 접속 : ALB 비용 발생
- ec2 및 nginx를 활용해 Elastic IP로 접속 : Elastic IP 비용 발생
- ALB는 고가용성을 위해 사용되는 서비스
- ex) Airflow 서버를 두 개 두고, 트래픽 분산
- 여기에 ALB 사용은 역할에 맞지 않다고 생각하여 두 번째 방식 채택
Reference
https://registry.terraform.io/providers/hashicorp/aws/latest/docs
'Infra > [인프라 구축기] Terraform 활용 AWS 인프라 구축' 카테고리의 다른 글
인프라 구축기 (6) - 로컬에서 Private EC2 Airflow Web Server 접속 (0) | 2024.10.17 |
---|---|
인프라 구축기 (5) - Private Subnet EC2에서 다른 Subnet의 인스턴스 접근 확인 (0) | 2024.10.11 |
인프라 구축기 (4) - Bastion Host에서 Private Subnet 접근 확인 (5) | 2024.10.09 |
인프라 구축기 (2) - Terraform을 활용한 VPC 구성 (0) | 2024.10.06 |
인프라 구축기 (1) - 개요, IAM 설정, 아키텍처 소개 (1) | 2024.10.02 |