Skip to content
Merged

Dev #19

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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 }}/commitfield:${{ needs.makeTagAndRelease.outputs.tag_name }},
ghcr.io/${{ env.OWNER_LC }}/commitfield: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-00847119db824184b"
working-directory: /
comment: Deploy
command: |
mkdir -p /dockerProjects/cmf
curl -o /dockerProjects/cmf/zero_downtime_deploy.py https://raw.githubusercontent.com/CommitField/commitfield/main/infraScript/zero_downtime_deploy.py
chmod +x /dockerProjects/cmf/zero_downtime_deploy.py
/dockerProjects/cmf/zero_downtime_deploy.py
File renamed without changes.
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ์ฒซ ๋ฒˆ์งธ ์Šคํ…Œ์ด์ง€: ๋นŒ๋“œ ์Šคํ…Œ์ด์ง€
FROM gradle:jdk21-graal-jammy as builder

# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
WORKDIR /app

# ์†Œ์Šค ์ฝ”๋“œ์™€ Gradle ๋ž˜ํผ ๋ณต์‚ฌ
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .

# Gradle ๋ž˜ํผ์— ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ
RUN chmod +x ./gradlew

# ์ข…์†์„ฑ ์„ค์น˜
RUN ./gradlew dependencies --no-daemon

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ
COPY src src

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋นŒ๋“œ (ํ…Œ์ŠคํŠธ ์Šคํ‚ต)
RUN ./gradlew clean build -x test --no-daemon

# ๋‘ ๋ฒˆ์งธ ์Šคํ…Œ์ด์ง€: ์‹คํ–‰ ์Šคํ…Œ์ด์ง€
FROM ghcr.io/graalvm/jdk-community:21

# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
WORKDIR /app

# ์ฒซ ๋ฒˆ์งธ ์Šคํ…Œ์ด์ง€์—์„œ ๋นŒ๋“œ๋œ JAR ํŒŒ์ผ ๋ณต์‚ฌ
COPY --from=builder /app/build/libs/*.jar app.jar

# ์‹คํ–‰ํ•  JAR ํŒŒ์ผ ์ง€์ •
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
99 changes: 99 additions & 0 deletions infraScript/zero_downtime_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env python3

import os
import requests # HTTP ์š”์ฒญ์„ ์œ„ํ•œ ๋ชจ๋“ˆ ์ถ”๊ฐ€
import subprocess
import time
from typing import Dict, Optional


class ServiceManager:
# ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
def __init__(self, socat_port: int = 8081, sleep_duration: int = 3) -> None:
self.socat_port: int = socat_port
self.sleep_duration: int = sleep_duration
self.services: Dict[str, int] = {
'cmf_1': 8082,
'cmf_2': 8083
}
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 = 'cmf_2', self.services['cmf_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 -d --name={name} --restart unless-stopped -p {port}:8090 -e TZ=Asia/Seoul -v /dockerProjects/cmf/volumes/gen:/gen --pull always ghcr.io/commitfield/commitfield")

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 _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 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)

# ์ƒˆ ์„œ๋น„์Šค๊ฐ€ 'UP' ์ƒํƒœ๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ
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()