Terraform을 사용한 AWS 인프라 구축

김상진 ·2025년 3월 23일

infra

목록 보기
1/6
post-thumbnail

이번에 프로젝트에서 인프라 담당을 맡아 Terraform을 활용해 AWS 리소스를 프로비저닝한 경험을 공유하고자 합니다. 프론트엔드 팀과 Swagger를 통한 API 명세서 연동을 위해 빠르게 인프라를 구축했고, 데이터베이스도 미리 설정해 개발 환경을 준비했습니다.

인프라 구축의 목적과 앞으로의 계획

현재는 프론트엔드 팀이 API 명세서를 빠르게 확인하고 개발을 시작할 수 있도록 Swagger를 통한 API 문서화와 테스트 환경을 우선적으로 구축했습니다. 초기 개발 단계에서는 모든 서비스를 하나의 EC2 인스턴스에 통합했지만, 프로젝트가 성장함에 따라 다음과 같은 인프라 확장 계획을 가지고 있습니다:

  1. 데이터베이스 서버 분리: 현재는 애플리케이션 서버와 같은 인스턴스에 MySQL을 구성했지만, 향후 데이터베이스 서버를 분리할 예정입니다. 두 가지 방향을 고려 중입니다:

    • RDS 활용: 관리형 서비스인 RDS를 사용하면 백업, 복구, 패치 관리 등을 AWS가 대신 해주어 운영 부담을 크게 줄일 수 있습니다.
    • 전용 EC2 인스턴스: 비용 효율성이 중요하거나 특수한 데이터베이스 설정이 필요한 경우, 별도의 EC2 인스턴스에 MySQL을 직접 구성하는 방법도 고려 중입니다.
  2. 멀티 AZ 배포: 고가용성을 위해 여러 가용 영역에 걸쳐 서비스를 배포할 계획입니다. 현재는 단일 EC2 인스턴스로 구성되어 있지만, 트래픽이 증가하면 Auto Scaling 그룹을 활용해 부하에 따라 자동으로 인스턴스를 확장하도록 설계할 예정입니다.

  3. 모니터링 체계 구축: CloudWatch와 Prometheus, Grafana를 활용한 종합적인 모니터링 시스템을 구축하여 서비스 상태와 성능을 실시간으로 관찰할 계획입니다.

  4. CI/CD 파이프라인 개선: 현재는 기본적인 배포 자동화만 구현되어 있지만, GitHub Actions와 AWS CodePipeline을 결합하여 더 견고한 CI/CD 파이프라인을 구축할 예정입니다.

이러한 계획은 현재 Terraform 코드에서 이미 고려하여 설계되었으며, 모듈화된 구조로 작성하여 향후 확장이 용이하도록 했습니다. 지금은 빠른 개발 시작을 위해 간소화된 환경을 제공하는 데 중점을 두었지만, 코드베이스가 성장함에 따라 인프라도 함께 진화시켜 나갈 계획입니다.

프로젝트 개요

팀 프로젝트에서 인프라를 담당하면서 다음과 같은 환경을 구축했습니다:

  • AWS EC2 인스턴스 프로비저닝
  • Docker를 활용한 컨테이너 환경 구성
  • HAProxy를 통한 로드밸런싱
  • MySQL, Redis 등 데이터베이스 설정
  • Nginx Proxy Manager를 통한 웹서버 및 프록시 관리

Terraform 코드 구성

Terraform을 통해 AWS 리소스를 코드로 관리하기 위해 다음과 같이 파일을 구성했습니다:

  • main.tf: 주요 AWS 리소스 정의
  • variables.tf: 변수 정의
  • secrets.tf: 비밀번호, 토큰 등 민감 정보 관리 (gitignore에 추가)

주요 인프라 구성 요소

1. VPC 및 네트워크 구성

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개의 서브넷을 여러 가용 영역에 분산 배치했으며, 인터넷 게이트웨이와 라우팅 테이블을 설정해 퍼블릭 접근이 가능하도록 구성했습니다. 모든 리소스에는 태그를 부여해 관리가 용이하도록 했습니다.

2. 보안 그룹 설정

인바운드와 아웃바운드 트래픽을 모두 허용하는 보안 그룹을 생성했습니다. 실제 프로덕션 환경에서는 보안을 강화하기 위해 필요한 포트만 선별적으로 열어야 하지만, 개발 환경을 위해 임시로 모든 트래픽을 허용했습니다.

3. EC2 인스턴스 및 IAM 역할

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
  // 태그 생략
}

4. EC2 인스턴스 생성 및 구성

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
}

사용자 데이터(User Data) 스크립트

EC2 인스턴스 생성 시 실행되는 스크립트를 통해 다양한 서비스를 자동으로 설치 및 구성했습니다:

1. 스왑 메모리 설정

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'

2. Docker 설치 및 네트워크 구성

Docker를 설치하고 서비스로 등록한 후, 컨테이너들이 서로 통신할 수 있도록 common 네트워크를 생성했습니다:

yum install docker -y
systemctl enable docker
systemctl start docker
docker network create common

3. Nginx Proxy Manager 설정

웹 트래픽 관리와 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 옵션을 설정했습니다.

4. HAProxy 설정

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

5. Redis 설치

세션 관리와 캐싱을 위한 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 변수를 활용했습니다.

6. MySQL 설치 및 초기화

데이터베이스로 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;
"

7. GitHub Container Registry 인증

프라이빗 도커 이미지를 사용하기 위해 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 인프라를 코드로 관리함으로써 다음과 같은 이점을 얻을 수 있었습니다:

  1. 인프라의 버전 관리: 코드로 관리되어 변경 사항을 추적하고 롤백할 수 있습니다.
  2. 반복 가능한 배포: 동일한 환경을 여러 번 쉽게 복제할 수 있습니다.
  3. 자동화: EC2 인스턴스 생성부터 필요한 모든 서비스 설치까지 자동화했습니다.
  4. 문서화: 코드 자체가 인프라의 문서 역할을 합니다.

특히 Docker를 활용한 컨테이너화를 통해 서비스 간 격리와 관리가 용이해졌으며, 네트워크로 묶어 서비스 간 원활한 통신이 가능하도록 했습니다. 모든 컨테이너에 --restart unless-stopped 옵션을 적용해 서비스의 안정성을 높였고, 볼륨을 통해 데이터 영속성을 보장했습니다.

앞으로 프로덕션 환경으로 전환 시에는 보안 그룹 설정을 강화하고, 백업 전략을 수립하는 등의 추가 작업이 필요할 것으로 보입니다.

profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글