저번 글에서 terraform을 이용해 ec2인스턴스를 만드는 방법에대해 정리했다.
오늘은 이 생성된 ec2서버에 프로젝트를 자동으로 배포하는 작업에대해 정리하려한다.
Github Action을 이용하면 이를 해결할 수 있다.
로컬에서 branch에 배포 -> github Action이 deploy.yml파일을 따라서 도커이미지 빌드및 푸쉬 그리고 배포를 진행해준다.

일단 프로젝트의 세팅에서 Actions의 이 설정을 read and write permissions로 바꾸어준다.
그 후 프로젝트의 .github/workflows/deploy.yml 파일을 생성한 후 작성해주면 된다
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 }}/sb_2024_02_20:${{ needs.makeTagAndRelease.outputs.tag_name }},
ghcr.io/${{ env.OWNER_LC }}/sb_2024_02_20:latest
deploy:
runs-on: ubuntu-latest
needs: [ buildImageAndPush ]
steps:
- name: AWS SSM Send-Command
uses: peterkimzz/aws-ssm-send-command@master
id: ssm
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
instance-ids: "i-0b65f83c8434b8534"
working-directory: /
comment: Deploy
command: |
mkdir -p /docker_projects/sb_2024_02_20
curl -o /docker_projects/sb_2024_02_20/zero_downtime_deployment.py https://raw.githubusercontent.com/chanw12/sb_2024_02_20/main/infraScript/zero_downtime_depolyment.py
chmod +x /docker_projects/sb_2024_02_20/zero_downtime_deployment.py
/docker_projects/sb_2024_02_20/zero_downtime_deployment.py
예를 들면 이렇게 작성 할 수 있는데
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
이 부분에서 secrets.~~ 이 부분은

이 이미지의 Actions에 들어가서

여기에 내용을 채워줘야한다. 로컬에서는 application-secrets.yml파일이 있기때문에 빌드할때 자동으로 secrets에 저장된 내용들이 들어간 상태로 이미지가 만들어지지만 github에는 올라가지 않기때문에 이 내용을 채워주어야 한다.
마지막
/docker_projects/sb_2024_02_20/zero_downtime_deployment.py
이 부분은 무중단 배포를 구현하기 위한 코드이다.
#!/usr/bin/env python3
import os
import requests
import subprocess
import time
from typing import Dict, Optional
class ServiceManager:
# 초기화 함수
def __init__(self, socat_port: int = 8080, sleep_duration: int = 3) -> None:
self.socat_port: int = socat_port
self.sleep_duration: int = sleep_duration
self.services: Dict[str, int] = {
'sb_2024_02_20_1': 8081,
'sb_2024_02_20_2': 8082
}
self.current_name: Optional[str] = None
self.current_port: Optional[int] = None
self.next_name: Optional[str] = None
self.next_port: Optional[int] = None
# 서비스 상태를 확인하는 함수
def _is_service_up(self, port: int) -> bool:
url = f"http://127.0.0.1:{port}/actuator/health"
try:
response = requests.get(url, timeout=5) # 5초 이내 응답 없으면 예외 발생
if response.status_code == 200 and response.json().get('status') == 'UP':
return True
except requests.RequestException:
pass
return False
# 현재 실행 중인 서비스를 찾는 함수
def _find_current_service(self) -> None:
cmd: str = f"ps aux | grep 'socat -t0 TCP-LISTEN:{self.socat_port}' | grep -v grep | awk '{{print $NF}}'"
current_service: str = subprocess.getoutput(cmd)
if not current_service:
self.current_name, self.current_port = 'sb_2024_02_20_2', self.services['sb_2024_02_20_2']
else:
self.current_port = int(current_service.split(':')[-1])
self.current_name = next((name for name, port in self.services.items() if port == self.current_port), None)
# 다음에 실행할 서비스를 찾는 함수
def _find_next_service(self) -> None:
self.next_name, self.next_port = next(
((name, port) for name, port in self.services.items() if name != self.current_name),
(None, None)
)
# Docker 컨테이너를 제거하는 함수
def _remove_container(self, name: str) -> None:
os.system(f"docker stop {name} 2> /dev/null")
os.system(f"docker rm -f {name} 2> /dev/null")
# Docker 컨테이너를 실행하는 함수
def _run_container(self, name: str, port: int) -> None:
os.system(
f"docker run --name={name} -p {port}:8080 -v /docker_projects/sb_2024_02_20/volumes/gen:/gen --restart unless-stopped -e TZ=Asia/Seoul --pull always -d ghcr.io/chanw12/sb_2024_02_20")
def _switch_port(self) -> None:
# Socat 포트를 전환하는 함수
cmd: str = f"ps aux | grep 'socat -t0 TCP-LISTEN:{self.socat_port}' | grep -v grep | awk '{{print $2}}'"
pid: str = subprocess.getoutput(cmd)
if pid:
os.system(f"kill -9 {pid} 2>/dev/null")
time.sleep(5)
os.system(
f"nohup socat -t0 TCP-LISTEN:{self.socat_port},fork,reuseaddr TCP:localhost:{self.next_port} &>/dev/null &")
# 서비스를 업데이트하는 함수
def update_service(self) -> None:
self._find_current_service()
self._find_next_service()
self._remove_container(self.next_name)
self._run_container(self.next_name, self.next_port)
while not self._is_service_up(self.next_port):
print(f"Waiting for {self.next_name} to be 'UP'...")
time.sleep(self.sleep_duration)
self._switch_port()
if self.current_name is not None:
self._remove_container(self.current_name)
print("Switched service successfully!")
if __name__ == "__main__":
manager = ServiceManager()
manager.update_service()
이게 무중단 배포를 위한 파이썬 파일이다. 이 코드를 보면 알 수 있듯이
원래는 8080포트 하나만을 이용했다면 이제는 새로 배포를 진행하게 될때 8080포트를 이용하는 것이아닌 8081과 8082 포트를 두개중 하나를 사용하다가 새로운 배포가 일어나면 다른 포트에서 미리 새로운 배포에 대한 도커컨터이너를 실행시켜놓고 8080포트로 접속되면 그 포트에 연결되도록 한 후 다른 컨테이너를 삭제하는 방식을 이용한다. 여기서는 socat을 이용해 포트를 전환하고 있다.
os.system(
f"docker run --name={name} -p {port}:8080 -v /docker_projects/sb_2024_02_20/volumes/gen:/gen --restart unless-stopped -e TZ=Asia/Seoul --pull always -d ghcr.io/chanw12/sb_2024_02_20")
이 부분이 중요한데 마지막 ghcr.io/chanw12/sb_2024_02_20
이게 아까 github action으로 만든 도커 이미지를 받아오는 것이다.
위의 deploy.yml이 실행되고 나면

이 이미지의 Packages에 파일이 생기게 되는데
이를 ghcr.io/chanw12/sb_2024_02_20:v0.0.27 이 링크로 다운 받을 수 있다. 저 코드에서는 이 주소를 이용해 빌드된 도커이미지를 받아서 ec2인스턴스의 도커컨테이너에서 실행시킨다.