AWS에서 서버를 생성하는 방법에는 여러가지가 있습니다.
여기서는 Terraform과 Docker를 사용해 무중단 배포를 해보겠습니다.
Terraform?
인프라 자동화 툴 중 하나로, 원하는 인프라를 코드로 서술할 수 있습니다.
프로젝트와 다음과 같은 파일들을 생성합니다.
Terraform에서 사용되는 변수들을 정의하는 부분입니다. 코드의 유연성과 재사용성을 위해 사용되며, Terraform 코드 내에서 ${var.prefix}, ${var.region}, ${var.nickname}와 같은 형식으로 참조될 수 있습니다.
variable "prefix" {
description = "Prefix for all resources"
default = "dev"
}
variable "region" {
description = "region"
default = "ap-northeast-2"
}
variable "nickname" {
description = "nickname"
default = "t189216"
}
AWS 인프라를 구성합니다. AWS 버전, VPC, 인터넷 게이트웨이, 라우팅 테이블, 보안 그룹, EC2, IAM 등을 설정합니다.
terraform {
# AWS 라이브러리 import
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
# AWS 설정 시작
provider "aws" {
region = var.region
}
# AWS 설정 끝
# VPC 설정 시작
resource "aws_vpc" "vpc_1" {
cidr_block = "10.0.0.0/16"
# DNS 지원을 활성화
enable_dns_support = true
# DNS 호스트 이름 지정을 활성화
enable_dns_hostnames = true
tags = {
Name = "${var.prefix}-vpc-1"
}
}
resource "aws_subnet" "subnet_1" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.region}a"
map_public_ip_on_launch = true # 이 서브넷이 배포되는 인스턴스에 공용 IP를 자동으로 할당
tags = {
Name = "${var.prefix}-subnet-1"
}
}
resource "aws_subnet" "subnet_2" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.region}b"
map_public_ip_on_launch = true
tags = {
Name = "${var.prefix}-subnet-2"
}
}
resource "aws_subnet" "subnet_3" {
vpc_id = aws_vpc.vpc_1.id
cidr_block = "10.0.3.0/24"
availability_zone = "${var.region}c"
map_public_ip_on_launch = true
tags = {
Name = "${var.prefix}-subnet-3"
}
}
# AWS 인터넷 게이트웨이
resource "aws_internet_gateway" "igw_1" {
vpc_id = aws_vpc.vpc_1.id
tags = {
Name = "${var.prefix}-igw-1"
}
}
# AWS 라우트 테이블
resource "aws_route_table" "rt_1" {
vpc_id = aws_vpc.vpc_1.id
# 라우트 규칙을 설정. 모든 트래픽(0.0.0.0/0)을 'igw_1' 인터넷 게이트웨이로 보냄
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw_1.id
}
tags = {
Name = "${var.prefix}-rt-1"
}
}
# 라우트 테이블과 서브넷을 연결
resource "aws_route_table_association" "association_1" {
subnet_id = aws_subnet.subnet_1.id
route_table_id = aws_route_table.rt_1.id
}
resource "aws_route_table_association" "association_2" {
subnet_id = aws_subnet.subnet_2.id
route_table_id = aws_route_table.rt_1.id
}
resource "aws_route_table_association" "association_3" {
subnet_id = aws_subnet.subnet_3.id
route_table_id = aws_route_table.rt_1.id
}
# AWS 보안 그룹 리소스
resource "aws_security_group" "sg_1" {
name = "${var.prefix}-sg-1"
# 인바운드 트래픽 규칙
ingress {
from_port = 0
to_port = 0
protocol = "all"
cidr_blocks = ["0.0.0.0/0"]
}
# 아웃바운드 트래픽 규칙
egress {
from_port = 0
to_port = 0
protocol = "all"
cidr_blocks = ["0.0.0.0/0"]
}
vpc_id = aws_vpc.vpc_1.id
tags = {
Name = "${var.prefix}-sg-1"
}
}
# EC2 설정 시작
# EC2 역할 생성
resource "aws_iam_role" "ec2_role_1" {
name = "${var.prefix}-ec2-role-1"
# 이 역할에 대한 신뢰 정책 설정. EC2 서비스가 이 역할을 가정할 수 있도록 설정
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow"
}
]
}
EOF
}
# EC2 역할에 AmazonS3FullAccess 정책을 부착
resource "aws_iam_role_policy_attachment" "s3_full_access" {
role = aws_iam_role.ec2_role_1.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
# EC2 역할에 AmazonEC2RoleforSSM 정책을 부착
resource "aws_iam_role_policy_attachment" "ec2_ssm" {
role = aws_iam_role.ec2_role_1.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
}
# IAM 인스턴스 프로파일 생성
resource "aws_iam_instance_profile" "instance_profile_1" {
name = "${var.prefix}-instance-profile-1"
role = aws_iam_role.ec2_role_1.name
}
locals {
ec2_user_data_base = <<-END_OF_FILE
#!/bin/bash
yum install docker -y # Docker 설치
systemctl enable docker # Docker 부팅 시 자동 시작 설정
systemctl start docker # Docker 서비스 시작
curl -L https:#github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose # 최신 Docker Compose 다운로드 및 설치
chmod +x /usr/local/bin/docker-compose # Docker Compose 실행 권한 부여
yum install git -y # Git 설치
sudo dd if=/dev/zero of=/swapfile bs=128M count=32 # 4GB 스왑 파일 생성
sudo chmod 600 /swapfile # 스왑 파일 권한 변경
sudo mkswap /swapfile # 스왑 파일을 스왑 공간으로 설정
sudo swapon /swapfile # 스왑 파일 활성화
sudo swapon -s # 활성화된 스왑 파일 목록 확인
sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab' # 부팅 시 스왑 파일 자동 활성화 설정
END_OF_FILE
}
# EC2 인스턴스 생성
resource "aws_instance" "ec2_1" {
ami = "ami-0c031a79ffb01a803" # Amazon Linux 2023 AMI
instance_type = "t2.micro"
subnet_id = aws_subnet.subnet_1.id
vpc_security_group_ids = [aws_security_group.sg_1.id]
associate_public_ip_address = true # 퍼블릭 IP 연결 설정
# 인스턴스에 IAM 역할 연결
iam_instance_profile = aws_iam_instance_profile.instance_profile_1.name
tags = {
Name = "${var.prefix}-ec2-1"
}
# 루트 볼륨 설정
root_block_device {
volume_type = "gp3"
volume_size = 32
}
# User data script for ec2_1
user_data = <<-EOF
${local.ec2_user_data_base}
mkdir -p /docker_projects/tagging/source
cd /docker_projects/tagging/source
git clone https://github.com/T189216/tagging .
# 도커 이미지 생성
docker build -t tagging_1:1 .
# 생성된 이미지 실행
docker run \
--name=tagging_1_1 \
-p 8080:8080 \
-v /docker_projects/tagging_1/volumes/gen:/gen \
--restart unless-stopped \
-e TZ=Asia/Seoul \
-d \
tagging_1:1
EOF
}
프로젝트 내에 해당 파일들이 있는 폴더로 이동한 후, 다음 명령어를 입력합니다.
cd terraform
terraform init
이 명령어는 Terraform이 필요로 하는 모든 설정과 플러그인을 로드하고 준비합니다.

이제 설정된 인프라 리소스대로 terraform을 실행합니다.
terraform apply

잘 실행됐습니다! 이제 AWS 콘솔로 이동해 생성된 EC2와 VPC, 인터넷 게이트웨이, 라우팅 테이블, 보안 그룹 등을 확인하면 됩니다.

AWS 콘솔에서도 확인할 수 있습니다.
인스턴스에 설정이 올바르게 적용됐는지 확인하려면 EC2 콘솔에 접속해서 확인할 수 있습니다.
EC2 콘솔에 접속하기
1. 인스턴스 연결 버튼을 클릭합니다.
2. Session Manager 탭에서 연결을 클릭합니다.
3. 해당 터미널에서 다음과 같이 입력합니다. 리눅스나 macOS에서 사용자의 권한을 루트(superuser)로 전환하는 명령어입니다.
sudo su
- 스왑파일 확인
sudo swapon -s
- 도커 이미지 확인
docker images
퍼블릭 IPv4 주소:8080에 접속하면 해당 브라우저를 확인할 수 있습니다.

먼저, 프로젝트에서 변경사항을 적용하고 git에 업로드합니다.
git에 업로드할 때, 업로드하는 위치에 주의하세요!
업로드 하는 위치
업로드 하면 안되는 위치
이제 변경사항이 서버에도 적용되도록 해야겠죠?
EC2 콘솔에 접속해 다음과 같이 명령어를 입력합니다.
# 소스코드 폴더로 이동
cd /docker_projects/tagging/source
# 소스코드 최신화
git pull origin main
변경사항이 적용된 소스코드를 가져옵니다.

이 소스코드로 v2를 만들어야 합니다. 새로운 도커 이미지를 생성합시다.
# 도커 이미지 생성(v2)
docker build -t tagging_1:2 .

이제 현재 실행중인 v1을 삭제하고, v2를 실행합니다.
# 현재 실행중인 도커 컨테이너 확인
docker ps -a

# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1
# 생성된 이미지 실행
docker run \
--name=tagging_1_1 \
-p 8080:8080 \
-v /docker_projects/tagging_1/volumes/gen:/gen \
--restart unless-stopped \
-e TZ=Asia/Seoul \
-d \
tagging_1:2

v2가 정상적으로 실행됐습니다!

아직 도커 이미지에는 v1 이미지가 남아있는 상태입니다. 필요없는 v1 이미지는 삭제합니다.

# 기존 이미지 제거
docker rmi tagging_1:1

현재 소스코드를 변경할 때마다 도커 이미지를 다시 생성하고 컨테이너를 제거하는 작업을 반복해야 합니다. 이 과정을 GitHub Actions을 사용해 자동화해보려고 합니다.
코드 변경이나 특정 파일 변경이 발생할 때 GitHub Actions를 통해 자동으로 빌드, 태깅, 릴리스, Docker 이미지 빌드 및 푸시를 수행하는 CI/CD 파이프라인을 구성하는 파일입니다.
name: 'deploy'
on:
push:
paths:
- '.github/workflows/**'
- 'src/**'
- 'build.gradle'
- 'Dockerfile'
- 'readme.md'
- 'infraScript/**'
branches:
- 'main'
jobs:
makeTagAndRelease:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.create_tag.outputs.new_tag }}
steps:
- uses: actions/checkout@v4
- name: Create Tag
id: create_tag
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.create_tag.outputs.new_tag }}
release_name: Release ${{ steps.create_tag.outputs.new_tag }}
body: ${{ steps.create_tag.outputs.changelog }}
draft: false
prerelease: false
buildImageAndPush:
name: 도커 이미지 빌드와 푸시
needs: makeTagAndRelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Buildx 설치
uses: docker/setup-buildx-action@v2
- name: 레지스트리 로그인
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: application-secret.yml 생성
env:
ACTIONS_STEP_DEBUG: true
APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET_YML }}
run: echo "$APPLICATION_SECRET" > src/main/resources/application-secret.yml
- name: 빌드 앤 푸시
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: |
ghcr.io/${{ env.OWNER_LC }}/tagging:${{ needs.makeTagAndRelease.outputs.tag_name }},
ghcr.io/${{ env.OWNER_LC }}/tagging:latest
애플리케이션에서 사용되는 JWT(Json Web Token)의 비밀 키를 설정하는 파일입니다. NEED_TO_INPUT는 임의로 설정하면 됩니다.
custom:
jwt:
secretKey: NEED_TO_INPUT
이제 github 리포지터리에 접속해 해당 프로젝트의 Settings ➡️ Actinos ➡️ General 탭을 클릭하고,

GitHub Actions Workflow가 해당 리포지토리에 대해 읽기 및 쓰기 권한을 가질 수 있도록, Workflow permissions 탭에서 Read and wirte permissinos를 체크합니다.

이제, Settings ➡️ Actions secrets and variables ➡️ Actions 탭에서 New repository secret를 클릭합니다.

application-secret.yml 파일은 중요한 정보가 담긴 파일이므로, 외부에 공개되어서는 안됩니다. 해당 Workflow가 액세스 권한을 획득할 수 있도록 변수를 만들어 값을 입력해줍시다.

NEED_TO_INPUT 에는 application-secret.yml 파일에서 작성했던 secretKey 값을 입력하면 됩니다.
이제 소스코드를 github에 push합니다.

GitHub Actions이 실행되는 것을 확인할 수 있습니다.
Packages에서 생성된 도커 파일 내부에 시크릿 키가 담겨있기 때문에 Packages를 비공개로 전환해야 합니다.
리포지터리에서 Packages를 클릭합니다.

리포지터리에서 Package settings를 클릭합니다.

Change package visibility 를 Private로 변경합니다.

도커 이미지 파일에 접근 권한을 얻기 위해 로그인해야 합니다.
github 프로필 이미지를 클릭하고, Settings ➡️ Developer Settings 로 이동합니다.

Personal access tokens (classic)에서 Generate new token(classic)을 클릭해 권한을 생성합니다.

발급받은 토큰을 YOUR_TOKEN 에 적고, EC2 콘솔에서 다음 명령어를 입력합니다.
docker login ghcr.io -u USERNAME -p YOUR_TOKEN

성공적으로 로그인되었습니다!
이제 EC2 콘솔에서 새로 생성된 도커 이미지로 교체합니다.
# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1
# 생성된 이미지 실행
docker run \
--name=tagging_1_1 \
-p 8080:8080 \
-v /docker_projects/tagging_1/volumes/gen:/gen \
--restart unless-stopped \
-e TZ=Asia/Seoul \
-d \
ghcr.io/t189216/tagging
# 기존 이미지 제거
docker rmi tagging_1:2

이제 브라우저에서 변경된 소스코드를 확인해보겠습니다.

잘 적용됩니다!
이제 소스코드를 변경하고 git에 push하면 새로운 Releases가 생성되는 것을 확인할 수 있습니다.

EC2 콘솔에서 새로 생성된 도커 이미지로 교체하면, 서버에서도 변경된 소스코드가 적용되는 것을 확인할 수 있습니다.
# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1
# 새 도커 이미지 PULL
docker pull ghcr.io/t189216/tagging
# 생성된 이미지 실행
docker run \
--name=tagging_1_1 \
-p 8080:8080 \
-v /docker_projects/tagging_1/volumes/gen:/gen \
--restart unless-stopped \
-e TZ=Asia/Seoul \
-d \
ghcr.io/t189216/tagging
# 태그가 none 인 이미지(예전에는 latest 였으니, docker pull 에 의해서 새로운 latest 태그를 가진 이미지가 생겨서, 더 이상 사용안되는 것들) 제거
docker rmi $(docker images -f "dangling=true" -q)
