diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index f0d6d3cb0..9a3ae34a4 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -10,6 +10,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: write steps: - name: Checkout the code @@ -18,81 +19,135 @@ jobs: token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} submodules: true + # --- Java, Gradle 설정 --- - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - - # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. - # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 - + uses: gradle/actions/setup-gradle@v3 - name: Grant execute permission for Gradle wrapper(gradlew) run: chmod +x ./gradlew - - name: Build with Gradle run: ./gradlew bootJar - - name: Copy jar file to remote - uses: appleboy/scp-action@master + # --- Docker 설정 --- + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - source: "./build/libs/*.jar" - target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/" - - - name: Copy docker file to remote - uses: appleboy/scp-action@master + platforms: linux/arm64 + - name: Log in to GitHub Container Registry (GHCR) + uses: docker/login-action@v3 with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - source: "./Dockerfile" - target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/" + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Copy docker compose file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - source: "./docker-compose.dev.yml" - target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/" + # --- 2. 이미지 메타데이터(이름, 태그) 정의 --- + # 빌드/푸시 단계와 SSH 단계에서 공통으로 사용할 변수를 미리 정의합니다. + - name: Define image name and tag + id: image_meta + run: | + OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + IMAGE_TAG=$(date +'%Y%m%d-%H%M%S') + echo "image_name=ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev" >> $GITHUB_OUTPUT + echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT - - name: Copy alloy config file to remote - uses: appleboy/scp-action@master + # --- 3. Docker 이미지 빌드, 푸시, 캐시 --- + # 'docker/build-push-action'을 사용하여 캐시 옵션을 적용합니다. + - name: Build, push, and cache Docker image + uses: docker/build-push-action@v5 with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - source: "./docs/infra-config/config.alloy" - target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/" + context: . + platforms: linux/arm64 + push: true + tags: ${{ format('{0}:{1}', steps.image_meta.outputs.image_name, steps.image_meta.outputs.image_tag) }} + cache-from: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache + cache-to: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache,mode=max - - name: Copy nginx config to remote - uses: appleboy/scp-action@master + # --- 4. Github App으로 임시 토큰 생성 --- + - name: Create installation token + id: app + uses: actions/create-github-app-token@v2 with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - source: "./docs/infra-config/nginx.dev.conf" - target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/nginx" - rename: "default.conf" + app-id: ${{ secrets.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + # --- 5. 설정 파일들만 scp로 전송 --- + - name: Copy config files to remote + run: | + echo "${{ secrets.DEV_PRIVATE_KEY }}" > deploy_key.pem + chmod 600 deploy_key.pem + + scp -i deploy_key.pem \ + -o StrictHostKeyChecking=no \ + ./docker-compose.dev.yml \ + ./docs/infra-config/config.alloy \ + ./docs/infra-config/nginx.dev.conf \ + ${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }}:/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/ + # --- 6. 서버에서 'docker pull' 및 서비스 재시작 --- - name: Run docker compose and apply nginx config - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.DEV_HOST }} - username: ${{ secrets.DEV_USERNAME }} - key: ${{ secrets.DEV_PRIVATE_KEY }} - script_stop: true - script: | - sudo cp /home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/nginx/default.conf /etc/nginx/conf.d/default.conf + run: | + ssh -i deploy_key.pem \ + -o StrictHostKeyChecking=no \ + ${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }} \ + ' + set -e + + # 1. 변수를 'image_meta' 단계의 출력값에서 가져옴 + export OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + export IMAGE_TAG_ONLY=${{ steps.image_meta.outputs.image_tag }} + export FULL_IMAGE_NAME="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev:${IMAGE_TAG_ONLY}" + + # 2. 서버가 GHCR에 로그인 (pull 받기 위해) + echo "${{ steps.app.outputs.token }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + # 3. docker pull (전체 이미지 이름 사용) + echo "Pulling new image layer from GHCR..." + docker pull $FULL_IMAGE_NAME + + # 4. 작업 디렉토리로 이동 및 Nginx 설정 이동 + cd /home/${{ secrets.DEV_USERNAME }}/solid-connection-dev + mkdir -p ./nginx + mv ./nginx.dev.conf ./nginx/default.conf + + # 5. Nginx 재시작 + sudo cp ./nginx/default.conf /etc/nginx/conf.d/default.conf sudo nginx -t sudo nginx -s reload - - cd /home/${{ secrets.DEV_USERNAME }}/solid-connection-dev + + # 6. Docker Compose 재시작 + echo "Restarting Docker Compose with tag: $IMAGE_TAG_ONLY" docker compose -f docker-compose.dev.yml down - docker compose -f docker-compose.dev.yml up -d --build + IMAGE_TAG=$IMAGE_TAG_ONLY docker compose -f docker-compose.dev.yml up -d + + # 7. 이미지 정리 + echo "Pruning dangling docker images..." + docker image prune -f + + # 8. stage 인스턴스의 오래된 태그 이미지 정리 (최신 5개 유지) + echo "Cleaning up old tagged images on host, keeping last 5..." + IMAGE_NAME_BASE="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev" + + docker images "${IMAGE_NAME_BASE}" --format "{{.Tag}}" | \ + sort -r | \ + tail -n +6 | \ + xargs -I {} docker rmi "${IMAGE_NAME_BASE}:{}" || true + + echo "Deploy and Docker Compose restart finished." + ' + + # --- 6. 이미지 정리 --- + - name: Clean up old image versions from GHCR + if: success() + uses: snok/container-retention-policy@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + image-names: solid-connection-dev + delete-untagged: true + keep-n-tags: 5 + account-type: org + org-name: ${{ github.repository_owner }} + cut-off: '7 days ago UTC' diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index d52c524c9..c96074d19 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -10,89 +10,142 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: write steps: - - name: Checkout the code - uses: actions/checkout@v4 - with: - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - submodules: true + - name: Checkout the code + uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + submodules: true - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' + # --- Java, Gradle 설정 (dev와 동일하게 버전 통일) --- + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Grant execute permission for Gradle wrapper(gradlew) + run: chmod +x ./gradlew + - name: Build with Gradle + run: ./gradlew bootJar - # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. - # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - - name: Setup Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + # --- 1. Docker 설정 --- + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/arm64 + - name: Log in to GitHub Container Registry (GHCR) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Grant execute permission for Gradle wrapper(gradlew) - run: chmod +x ./gradlew + # --- 2. 이미지 메타데이터(이름, 태그) 정의 --- + - name: Define image name and tag + id: image_meta + run: | + OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + IMAGE_TAG=$(date +'%Y%m%d-%H%M%S') + echo "image_name=ghcr.io/${OWNER_LOWERCASE}/solid-connection-server" >> $GITHUB_OUTPUT + echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT - - name: Build with Gradle - run: ./gradlew bootJar - - - name: Copy jar file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./build/libs/*.jar" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - - - name: Copy docker file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./Dockerfile" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - - - name: Copy docker compose file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./docker-compose.prod.yml" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - - - name: Copy alloy config file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./docs/infra-config/config.alloy" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - - - name: Copy nginx config to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./docs/infra-config/nginx.prod.conf" - target: "/home/${{ secrets.USERNAME }}/solid-connection-prod/nginx" - rename: "default.conf" + # --- 3. Docker 이미지 빌드, 푸시, 캐시 --- + - name: Build, push, and cache Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: ${{ format('{0}:{1}', steps.image_meta.outputs.image_name, steps.image_meta.outputs.image_tag) }} + cache-from: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache + cache-to: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache,mode=max - - name: Run docker compose and apply nginx config - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - script_stop: true - script: | - sudo cp /home/${{ secrets.USERNAME }}/solid-connection-prod/nginx/default.conf /etc/nginx/conf.d/default.conf - sudo nginx -t - sudo nginx -s reload + # --- 4. Github App으로 임시 토큰 생성 --- + - name: Create installation token + id: app + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + # --- 5. 설정 파일들만 scp로 전송 --- + - name: Copy config files to remote + run: | + echo "${{ secrets.PRIVATE_KEY }}" > deploy_key.pem + chmod 600 deploy_key.pem + + scp -i deploy_key.pem \ + -o StrictHostKeyChecking=no \ + ./docker-compose.prod.yml \ + ./docs/infra-config/config.alloy \ + ./docs/infra-config/nginx.prod.conf \ + ${{ secrets.USERNAME }}@${{ secrets.HOST }}:/home/${{ secrets.USERNAME }}/solid-connection-prod/ + + # --- 6. 서버에서 'docker pull' 및 서비스 재시작 --- + - name: Run docker compose and apply nginx config + run: | + echo "${{ secrets.PRIVATE_KEY }}" > deploy_key_ssh.pem + chmod 600 deploy_key_ssh.pem + + ssh -i deploy_key_ssh.pem \ + -o StrictHostKeyChecking=no \ + ${{ secrets.USERNAME }}@${{ secrets.HOST }} \ + ' + set -e + + # 1. 변수 설정 + export OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + export IMAGE_TAG_ONLY=${{ steps.image_meta.outputs.image_tag }} + export FULL_IMAGE_NAME="ghcr.io/${OWNER_LOWERCASE}/solid-connection-server:${IMAGE_TAG_ONLY}" + + # 2. 서버가 GHCR에 로그인 (pull 받기 위해) + echo "${{ steps.app.outputs.token }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + # 3. docker pull (전체 이미지 이름 사용) + echo "Pulling new image layer from GHCR..." + docker pull $FULL_IMAGE_NAME + + # 4. 작업 디렉토리로 이동 및 Nginx 설정 이동 + cd /home/${{ secrets.USERNAME }}/solid-connection-prod + mkdir -p ./nginx + mv ./nginx.prod.conf ./nginx/default.conf + + # 5. Nginx 재시작 + sudo cp ./nginx/default.conf /etc/nginx/conf.d/default.conf + sudo nginx -t + sudo nginx -s reload + + # 6. Docker Compose 재시작 (안정성 확보 로직 포함) + echo "Restarting Docker Compose with tag: $IMAGE_TAG_ONLY" + echo "Stopping containers gracefully..." + docker compose -f docker-compose.prod.yml stop + + echo "Removing old containers and networks..." + docker compose -f docker-compose.prod.yml down --remove-orphans + + echo "Starting new containers..." + OWNER_LOWERCASE=$OWNER_LOWERCASE IMAGE_TAG=$IMAGE_TAG_ONLY docker compose -f docker-compose.prod.yml up -d + + # 7. 이미지 정리 + echo "Pruning dangling docker images..." + docker image prune -f + + echo "Deploy and Docker Compose restart finished." + ' - cd /home/${{ secrets.USERNAME }}/solid-connect-server - docker compose -f docker-compose.prod.yml down - docker compose -f docker-compose.prod.yml up -d --build + # --- 6. 이미지 정리 --- + - name: Clean up old image versions from GHCR + if: success() + uses: snok/container-retention-policy@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + image-names: solid-connection-server + delete-untagged: true + keep-n-tags: 5 + account-type: org + org-name: ${{ github.repository_owner }} + cut-off: '7 days ago UTC' diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e20302c4b..29aaf5bb1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -18,9 +18,7 @@ services: - redis solid-connection-dev: - build: - context: . - dockerfile: Dockerfile + image: ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev:${IMAGE_TAG:-latest} container_name: solid-connection-dev ports: - "8080:8080" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d805031d0..5b26eecf9 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -18,9 +18,7 @@ services: - redis solid-connection-server: - build: - context: . - dockerfile: Dockerfile + image: ghcr.io/${OWNER_LOWERCASE}/solid-connection-server:${IMAGE_TAG:-latest} container_name: solid-connection-server ports: - "8080:8080" diff --git a/src/main/resources/secret b/src/main/resources/secret index 8300cdeca..2f4a16822 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 8300cdecaebfc28fd657064a00a44815a7bb2eee +Subproject commit 2f4a168223ea81cfe3447d6d95441b1a020fdbfe