현재 EC2에서 SSH 기능이 활성화되어 있지 않고, SSM 방식으로 사용하고 있습니다.
GitHub Actions Workflow서 AWS Systems Manager (SSM) Send-Command를 사용하여 AWS EC2 인스턴스에 명령을 전송할 수 있도록 수정해야 합니다.
...
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-01f7b8f6885df7f82"
working-directory: /
comment: Deploy
command: |
# 기존에 실행된 컨테이너 제거
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)
이제 github 리포지터리에 접속해 해당 프로젝트의 Settings ➡️ Actions secrets and variables ➡️ Actions 탭에서 AWS_REGION , AWS_ACCESS_KEY_ID , AWS_SECRET_ACCESS_KEY 를 추가합니다.
AWS_REGION : AWS의 리전. (ex. ap-northeast-2)AWS_ACCESS_KEY_ID : IAM 사용자 액세스 키AWS_SECRET_ACCESS_KEY : IAM 사용자 비밀 액세스 키이제 git에 push하면 서버에도 자동으로 변경사항이 적용됩니다.

배포 완전 자동화가 완료되었습니다. 하지만 현재 서버를 중단하고, 새로 생성해서 교체하는 방식이므로 배포되는 시간이 약간 소요되는 단점이 있습니다.
무중단 배포 스크립트를 작성하기 위해 EC2 콘솔에서 파이썬과 socat을 설치하겠습니다.
socat?
네트워크 데이터 전송을 위한 다목적 CLI (Command Line Interface) 유틸리티입니다. 일반적으로 데이터를 읽고 쓰는 데 사용되며, 포트 포워딩, SSL 터널링, 파일 전송 등 다양한 용도로 활용됩니다.
yum install python
yum install python -y
yum install socat
yum install socat -y

terraform 파일에도 파이썬과 socat을 설치하는 명령어를 추가합니다.
...
locals {
ec2_user_data_base = <<-END_OF_FILE
#!/bin/bash
yum install python -y <<< 파이썬을 설치하는 명령어 추가함
yum install socat -y <<< socat을 설치하는 명령어 추가함
yum install docker -y
systemctl enable docker
systemctl start docker
...
서비스를 지속적으로 업데이트하며, 한 서비스가 종료되면 자동으로 다른 서비스로 전환하는 작업을 수행하는 스크립트입니다.
#!/usr/bin/env python3
import os
import subprocess
import time
from typing import Dict, Optional
class ServiceManager:
# 초기화 함수
def __init__(self, socat_port: int = 8080, sleep_duration: int = 20) -> None:
self.socat_port: int = socat_port
self.sleep_duration: int = sleep_duration
self.services: Dict[str, int] = {
'tagging_1': 8081,
'tagging_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 _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 = 'tagging_2', self.services['tagging_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/tagging/volumes/gen:/gen --restart unless-stopped -e TZ=Asia/Seoul --pull always -d ghcr.io/jhs512/tagging")
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)
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()
이제 git에 push를 하고, 배포 서버를 확인해보겠습니다.

잘 작동합니다.