이번에 프로젝트에서 인프라 담당을 맡아 Terraform을 활용해 AWS 리소스를 프로비저닝한 경험을 공유하고자 합니다. 프론트엔드 팀과 Swagger를 통한 API 명세서 연동을 위해 빠르게 인프라를 구축했고, 데이터베이스도 미리 설정해 개발 환경을 준비했습니다.
현재는 프론트엔드 팀이 API 명세서를 빠르게 확인하고 개발을 시작할 수 있도록 Swagger를 통한 API 문서화와 테스트 환경을 우선적으로 구축했습니다. 초기 개발 단계에서는 모든 서비스를 하나의 EC2 인스턴스에 통합했지만, 프로젝트가 성장함에 따라 다음과 같은 인프라 확장 계획을 가지고 있습니다:
데이터베이스 서버 분리: 현재는 애플리케이션 서버와 같은 인스턴스에 MySQL을 구성했지만, 향후 데이터베이스 서버를 분리할 예정입니다. 두 가지 방향을 고려 중입니다:
멀티 AZ 배포: 고가용성을 위해 여러 가용 영역에 걸쳐 서비스를 배포할 계획입니다. 현재는 단일 EC2 인스턴스로 구성되어 있지만, 트래픽이 증가하면 Auto Scaling 그룹을 활용해 부하에 따라 자동으로 인스턴스를 확장하도록 설계할 예정입니다.
모니터링 체계 구축: CloudWatch와 Prometheus, Grafana를 활용한 종합적인 모니터링 시스템을 구축하여 서비스 상태와 성능을 실시간으로 관찰할 계획입니다.
CI/CD 파이프라인 개선: 현재는 기본적인 배포 자동화만 구현되어 있지만, GitHub Actions와 AWS CodePipeline을 결합하여 더 견고한 CI/CD 파이프라인을 구축할 예정입니다.
이러한 계획은 현재 Terraform 코드에서 이미 고려하여 설계되었으며, 모듈화된 구조로 작성하여 향후 확장이 용이하도록 했습니다. 지금은 빠른 개발 시작을 위해 간소화된 환경을 제공하는 데 중점을 두었지만, 코드베이스가 성장함에 따라 인프라도 함께 진화시켜 나갈 계획입니다.
팀 프로젝트에서 인프라를 담당하면서 다음과 같은 환경을 구축했습니다:
Terraform을 통해 AWS 리소스를 코드로 관리하기 위해 다음과 같이 파일을 구성했습니다:
resource "aws_vpc" "vpc_1" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.prefix}-vpc-1"
Team = var.team_tag
}
}
4개의 서브넷을 여러 가용 영역에 분산 배치했으며, 인터넷 게이트웨이와 라우팅 테이블을 설정해 퍼블릭 접근이 가능하도록 구성했습니다. 모든 리소스에는 태그를 부여해 관리가 용이하도록 했습니다.
인바운드와 아웃바운드 트래픽을 모두 허용하는 보안 그룹을 생성했습니다. 실제 프로덕션 환경에서는 보안을 강화하기 위해 필요한 포트만 선별적으로 열어야 하지만, 개발 환경을 위해 임시로 모든 트래픽을 허용했습니다.
EC2 인스턴스에 필요한 권한을 부여하기 위해 IAM 역할을 생성하고, S3 접근 권한과 SSM 접근 권한을 부여했습니다. 인스턴스 프로파일을 통해 EC2에 이 역할을 연결했습니다.
resource "aws_iam_role" "ec2_role_1" {
name = "${var.prefix}-ec2-role-1"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow"
}
]
}
EOF
// 태그 생략
}
Amazon Linux 2023 최신 AMI를 사용해 t3.micro 인스턴스를 생성했습니다. 30GB 크기의 EBS 볼륨을 연결하고, 탄력적 IP를 할당해 고정 IP 주소를 유지했습니다.
data "aws_ami" "latest_amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
// 필터 생략
}
resource "aws_instance" "ec2_1" {
ami = data.aws_ami.latest_amazon_linux.id
instance_type = "t3.micro"
subnet_id = aws_subnet.subnet_2.id
vpc_security_group_ids = [aws_security_group.sg_1.id]
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.instance_profile_1.name
// 태그 및 볼륨 설정 생략
user_data = <<-EOF
${local.ec2_user_data_base}
EOF
}
EC2 인스턴스 생성 시 실행되는 스크립트를 통해 다양한 서비스를 자동으로 설치 및 구성했습니다:
RAM이 제한된 t3.micro 인스턴스에서 성능 향상을 위해 4GB 크기의 스왑 파일을 생성했습니다:
sudo dd if=/dev/zero of=/swapfile bs=128M count=32
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab'
Docker를 설치하고 서비스로 등록한 후, 컨테이너들이 서로 통신할 수 있도록 common 네트워크를 생성했습니다:
yum install docker -y
systemctl enable docker
systemctl start docker
docker network create common
웹 트래픽 관리와 SSL 인증서 자동화를 위해 Nginx Proxy Manager를 설치했습니다:
docker run -d \
--name npm_1 \
--restart unless-stopped \
--network common \
-p 80:80 \
-p 443:443 \
-p 81:81 \
-e TZ=Asia/Seoul \
-v /dockerProjects/npm_1/volumes/data:/data \
-v /dockerProjects/npm_1/volumes/etc/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:latest
볼륨을 마운트해 설정과 인증서를 영구적으로 저장하도록 했고, 컨테이너 충돌 시 자동으로 재시작하도록 --restart unless-stopped 옵션을 설정했습니다.
API 서버의 로드밸런싱을 위해 HAProxy를 구성했습니다:
mkdir -p /dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/lua
# 502, 504 에러 발생 시 재시도하는 Lua 스크립트 생성
cat << 'EOF' > /dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/lua/retry_on_502_504.lua
core.register_action("retry_on_502_504", { "http-res" }, function(txn)
local status = txn.sf:status()
if status == 502 or status == 504 then
txn:Done()
end
end)
EOF
# HAProxy 설정 파일 생성
echo -e "
global
lua-load /usr/local/etc/haproxy/lua/retry_on_502_504.lua
resolvers docker
nameserver dns1 127.0.0.11:53
resolve_retries 3
timeout retry 1s
hold valid 10s
defaults
mode http
timeout connect 5s
timeout client 60s
timeout server 60s
frontend http_front
bind *:80
acl host_app1 hdr_beg(host) -i ${var.app_1_domain}
use_backend http_back_1 if host_app1
backend http_back_1
balance roundrobin
option httpchk GET /actuator/health
default-server inter 2s rise 1 fall 1 init-addr last,libc,none resolvers docker
option redispatch
http-response lua.retry_on_502_504
server app_server_1_1 app1_1:8080 check
server app_server_1_2 app1_2:8080 check
" > /dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy/haproxy.cfg
특히 Lua 스크립트를 사용해 502, 504 에러 발생 시 자동으로 재시도하는 기능을 구현했고, health check를 통해 서버 상태를 모니터링하도록 설정했습니다. 로드밸런싱 방식으로는 라운드로빈 방식을 채택했습니다.
docker run \
-d \
--network common \
-p 8090:80 \
-v /dockerProjects/ha_proxy_1/volumes/usr/local/etc/haproxy:/usr/local/etc/haproxy \
-e TZ=Asia/Seoul \
--name ha_proxy_1 \
haproxy
세션 관리와 캐싱을 위한 Redis를 설치했습니다:
docker run -d \
--name=redis_1 \
--restart unless-stopped \
--network common \
-p 6379:6379 \
-e TZ=Asia/Seoul \
redis --requirepass ${var.password_1}
비밀번호 보안을 위해 Terraform 변수를 활용했습니다.
데이터베이스로 MySQL을 설치하고 초기 설정을 자동화했습니다:
docker run -d \
--name mysql_1 \
--restart unless-stopped \
-v /dockerProjects/mysql_1/volumes/var/lib/mysql:/var/lib/mysql \
-v /dockerProjects/mysql_1/volumes/etc/mysql/conf.d:/etc/mysql/conf.d \
--network common \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=${var.password_1} \
-e TZ=Asia/Seoul \
mysql:latest
볼륨을 마운트해 데이터와 설정 파일을 영구적으로 저장하도록 했습니다. 그리고 MySQL이 완전히 시작된 후에 초기화 스크립트를 실행하도록 대기 로직을 구현했습니다:
# MySQL 컨테이너가 준비될 때까지 대기
echo "MySQL이 기동될 때까지 대기 중..."
until docker exec mysql_1 mysql -uroot -p${var.password_1} -e "SELECT 1" &> /dev/null; do
echo "MySQL이 아직 준비되지 않음. 5초 후 재시도..."
sleep 5
done
echo "MySQL이 준비됨. 초기화 스크립트 실행 중..."
그리고 데이터베이스와 사용자를 생성하고 권한을 설정했습니다:
docker exec mysql_1 mysql -uroot -p${var.password_1} -e "
CREATE USER 'lldjlocal'@'127.0.0.1' IDENTIFIED WITH caching_sha2_password BY '1234';
CREATE USER 'lldjlocal'@'172.18.%.%' IDENTIFIED WITH caching_sha2_password BY '1234';
CREATE USER 'lldj'@'%' IDENTIFIED WITH caching_sha2_password BY '${var.password_1}';
GRANT ALL PRIVILEGES ON *.* TO 'lldjlocal'@'127.0.0.1';
GRANT ALL PRIVILEGES ON *.* TO 'lldjlocal'@'172.18.%.%';
GRANT ALL PRIVILEGES ON *.* TO 'lldj'@'%';
CREATE DATABASE dementor_prod;
FLUSH PRIVILEGES;
"
프라이빗 도커 이미지를 사용하기 위해 GitHub Container Registry에 로그인하도록 설정했습니다:
echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin
민감한 정보는 secrets.tf 파일에 분리하고, .gitignore에 추가해 실수로 공개되지 않도록 했습니다:
### terraform ###
# Terraform 상태 파일 (보안상 중요)
terraform/terraform.tfstate
terraform/terraform.tfstate.backup
# Terraform 상태 디렉터리
terraform/.terraform
terraform/.terraform.lock.hcl
# Terraform 변수 파일 (AWS Access Key 등 민감 정보 포함)
terraform/secrets.tf
Terraform을 활용해 AWS 인프라를 코드로 관리함으로써 다음과 같은 이점을 얻을 수 있었습니다:
특히 Docker를 활용한 컨테이너화를 통해 서비스 간 격리와 관리가 용이해졌으며, 네트워크로 묶어 서비스 간 원활한 통신이 가능하도록 했습니다. 모든 컨테이너에 --restart unless-stopped 옵션을 적용해 서비스의 안정성을 높였고, 볼륨을 통해 데이터 영속성을 보장했습니다.
앞으로 프로덕션 환경으로 전환 시에는 보안 그룹 설정을 강화하고, 백업 전략을 수립하는 등의 추가 작업이 필요할 것으로 보입니다.