From e588f52f5c18146b0fa70e60ec8b81f60696bb4a Mon Sep 17 00:00:00 2001 From: dev4unet Date: Fri, 26 Sep 2025 04:52:13 +0000 Subject: [PATCH] Enhance GitHub Actions workflows to support conditional deletion of Docker images before rebuilding. --- .github/workflows/README.md | 117 +++++++++++++++++++-- .github/workflows/delete-docker-image.yaml | 21 +++- .github/workflows/force-rebuild.yaml | 42 ++++++-- .github/workflows/retag-release.yaml | 44 ++++++-- 4 files changed, 196 insertions(+), 28 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 8278222..f5791f7 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,6 +2,50 @@ 이 디렉토리는 CM-ANT 프로젝트의 GitHub Actions 워크플로우를 포함합니다. +## 배경 설명 + +### Docker Hub 다이제스트 관리 전략 + +Docker Hub에서 **하나의 태그에 여러 digest가 존재하는 것은 의도된 기능**입니다. 이는 멀티 플랫폼 지원, 이력 관리, 롤백 기능을 위한 정상적인 설계입니다. + +**현재 상황**: +- Docker Hub의 `v0.4.0` 태그에 2개 이상의 digest 존재 (정상) +- `pull_policy: always` 설정에도 불구하고 이전 digest의 이미지가 pull될 수 있음 +- Docker 클라이언트가 첫 번째 digest를 우선 선택하지만, 순서가 최신순이 아닐 수 있음 + +**해결 전략**: + +#### 1. 기본 전략 (권장) +- **mayfly의 `--force` 옵션**: Docker Hub API로 digest 정보 확인 후 최신 digest로 명시적 pull +- **digest 기반 설치**: 태그 대신 digest로 설치하여 정확한 버전 보장 +- **기존 호환성 유지**: 기본 동작은 기존과 동일하게 유지 + +**사용법**: +```bash +# 전체 서비스 업데이트 (digest 기반) +mayfly infra update --force + +# 특정 서비스만 업데이트 (digest 기반) +mayfly infra update --force -s cm-ant + +# 기존 방식 (기본 동작) +mayfly infra update -s cm-ant +``` + +**동작 방식**: +1. **docker-compose.yaml 파싱**: 실제 이미지 정보 추출 +2. **Docker Hub API 호출**: 각 태그의 digest 목록 조회 +3. **최신 digest 선택**: `last_pushed` 시간 기준으로 정렬 +4. **digest 기반 pull**: `image@digest` 형태로 명시적 pull +5. **태그 재할당**: 원본 태그로 재태깅하여 호환성 보장 + +#### 2. 보조 전략 (현재 구현) +- **하나의 태그 = 하나의 digest** 원칙 적용 +- 기존 Docker 이미지 삭제 후 새 이미지 생성 +- 깔끔한 다이제스트 관리로 예측 가능한 동작 보장 + +**이러한 배경으로 Docker 이미지 삭제 및 재생성 기능이 보조 수단으로 제공됩니다.** + ## 기존 워크플로우 ### 1. Continuous Integration (CI) @@ -18,7 +62,31 @@ ## 새로운 워크플로우 -### 3. Rebuild Docker Image +### 3. Delete Docker Image +- **파일**: `delete-docker-image.yaml` +- **트리거**: 수동 실행 (`workflow_dispatch`) +- **기능**: Docker Hub에서 특정 태그의 이미지를 삭제 + +#### 사용법: +1. GitHub 리포지토리의 **Actions** 탭으로 이동 +2. **Delete Docker Image** 워크플로우 선택 +3. **Run workflow** 버튼 클릭 +4. 입력값 설정: + - `tag_name`: 삭제할 태그명 (예: `v0.4.0`, `0.4.0`) + - `confirm_delete`: `DELETE` 입력 (확인용) + +#### 특징: +- ✅ **안전한 삭제**: 태그 존재 여부 확인 후 삭제 +- ✅ **삭제 검증**: 삭제 후 검증 단계 포함 +- ✅ **상세한 로그**: 삭제 과정의 모든 단계 표시 +- ✅ **권한 처리**: Personal Access Token 우선 사용 + +#### 사용 시기: +- Docker Hub에서 잘못된 이미지가 업로드되었을 때 +- 태그 충돌을 해결하고 싶을 때 +- 수동으로 특정 태그를 정리하고 싶을 때 + +### 4. Rebuild Docker Image - **파일**: `retag-release.yaml` - **트리거**: 수동 실행 (`workflow_dispatch`) - **기능**: 기존 Git 태그 위치는 그대로 유지하고 Docker 이미지만 재빌드 @@ -42,7 +110,7 @@ - Docker 이미지 빌드 과정에서 문제가 있었을 때 - Git 태그 위치는 유지하되 Docker 이미지만 새로 만들고 싶을 때 -### 4. Move Tag to Latest Commit ⚠️ +### 5. Move Tag to Latest Commit ⚠️ - **파일**: `force-rebuild.yaml` - **트리거**: 수동 실행 (`workflow_dispatch`) - **기능**: Git 태그를 현재 HEAD 커밋으로 이동하고 Docker 이미지 재빌드 @@ -105,9 +173,25 @@ git push origin v0.4.1 다음 Secrets이 설정되어 있어야 합니다: +### 기본 인증 정보 - `DOCKER_USERNAME`: Docker Hub 사용자명 -- `DOCKER_PASSWORD`: Docker Hub 비밀번호 또는 액세스 토큰 +- `DOCKER_PASSWORD`: Docker Hub 비밀번호 (이미지 생성용) + +### Personal Access Tokens (PAT) +- `DOCKER_PAT`: Docker Hub Personal Access Token (이미지 삭제용) - `CR_PAT`: GitHub Container Registry Personal Access Token +- `UPDATE_SWAGGER_DOC_PAT`: Swagger 문서 업데이트용 PAT +- `CB_GITHUB_ROBOT_PAT`: GitHub Robot용 PAT + +### Docker Hub PAT 생성 방법 +1. **Docker Hub 로그인** → **Account Settings** → **Security** +2. **"New Access Token"** 클릭 +3. **권한 설정**: `Read, Write, Delete` 권한 부여 +4. **토큰 생성** 후 GitHub Organization Secrets에 `DOCKER_PAT`로 추가 + +### 권한 차이 +- **`DOCKER_PASSWORD`**: 이미지 생성/업데이트만 가능 (삭제 불가) +- **`DOCKER_PAT`**: 이미지 생성/업데이트/삭제 모두 가능 ## 워크플로우 실행 권한 @@ -116,10 +200,12 @@ git push origin v0.4.1 ## 워크플로우 선택 가이드 -### 🔄 Rebuild Docker Image vs 🏷️ Move Tag to Latest Commit +### 🚀 mayfly --force vs 🗑️ Delete Docker Image vs 🔄 Rebuild Docker Image vs 🏷️ Move Tag to Latest Commit -| 상황 | 권장 워크플로우 | 이유 | -|------|----------------|------| +| 상황 | 권장 방법 | 이유 | +|------|-----------|------| +| **일반적인 최신 이미지 업데이트** | **`mayfly infra update --force`** | digest 기반으로 정확한 최신 버전 보장 | +| 잘못된 이미지 삭제 | **Delete Docker Image** | 특정 태그만 삭제, 안전함 | | Docker Hub digest 충돌 | **Rebuild Docker Image** | Git 태그 위치 유지, 안전함 | | Docker 이미지 빌드 문제 | **Rebuild Docker Image** | 같은 코드로 새 이미지 생성 | | 최신 커밋을 태그에 반영 | **Move Tag to Latest Commit** | Git 태그 위치 변경 필요 | @@ -138,14 +224,25 @@ git push origin v0.4.1 1. GitHub Secrets 설정 확인 2. 리포지토리 권한 확인 3. 입력값 형식 확인 (태그명은 `v0.4.0` 형식) -4. 확인 코드 정확히 입력 (`REBUILD` 또는 `MOVE_TAG`) +4. 확인 코드 정확히 입력 (`DELETE`, `REBUILD` 또는 `MOVE_TAG`) ### Docker Hub API 오류 -1. Docker Hub 계정 권한 확인 -2. API 레이트 리미트 확인 -3. 네트워크 연결 상태 확인 +1. **HTTP 401 오류**: `DOCKER_PAT` 설정 확인 (삭제 권한 필요) +2. **HTTP 403 오류**: Docker Hub 계정 권한 확인 +3. **API 레이트 리미트**: Docker Hub API 사용량 확인 +4. **네트워크 연결**: 연결 상태 및 방화벽 설정 확인 + +### Docker 이미지 삭제 관련 오류 +1. **삭제 권한 부족**: `DOCKER_PAT` 토큰에 `Delete` 권한이 있는지 확인 +2. **태그 존재 여부**: 삭제하려는 태그가 실제로 존재하는지 확인 +3. **Organization 권한**: Docker Hub Organization의 관리자 권한 확인 ### Git 태그 관련 오류 1. 기존 태그가 존재하는지 확인 2. 태그 삭제 권한 확인 3. 원격 저장소 접근 권한 확인 + +### 다이제스트 문제 해결 +1. **여러 digest 확인**: `curl -s "https://hub.docker.com/v2/repositories/cloudbaristaorg/cm-ant/tags/0.4.0/" | jq '.images'` +2. **최신 digest 확인**: `jq '.images[0].digest'`로 첫 번째 digest 확인 +3. **캐시 문제**: Docker Hub API 캐시로 인한 지연 (보통 10-30분) diff --git a/.github/workflows/delete-docker-image.yaml b/.github/workflows/delete-docker-image.yaml index b2191b0..94c3062 100644 --- a/.github/workflows/delete-docker-image.yaml +++ b/.github/workflows/delete-docker-image.yaml @@ -83,11 +83,22 @@ jobs: echo "🗑️ Deleting Docker image: ${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:$TAG_NAME" # Delete the Docker image from Docker Hub - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ - -X DELETE \ - -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ - "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ - -H "Accept: application/json") + # Try with Personal Access Token first, fallback to password + if [ -n "${{ secrets.DOCKER_PAT }}" ]; then + echo "Using Personal Access Token for deletion" + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE \ + -H "Authorization: Bearer ${{ secrets.DOCKER_PAT }}" \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json") + else + echo "Using username/password for deletion" + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE \ + -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json") + fi if [ "$RESPONSE" = "204" ]; then echo "✅ Docker image deleted successfully" diff --git a/.github/workflows/force-rebuild.yaml b/.github/workflows/force-rebuild.yaml index b1fb7da..9596349 100644 --- a/.github/workflows/force-rebuild.yaml +++ b/.github/workflows/force-rebuild.yaml @@ -10,6 +10,14 @@ on: description: 'Tag name to move to latest commit (e.g., v0.4.0)' required: true type: string + delete_existing_docker_image: + description: 'Delete existing Docker Hub image before rebuilding?' + required: true + type: choice + default: 'yes' + options: + - 'yes' + - 'no' confirm_move: description: '⚠️ WARNING: This will move the Git tag to current HEAD and rebuild Docker image. Type "MOVE_TAG" to confirm' required: true @@ -56,20 +64,38 @@ jobs: echo "Git tag $TAG_NAME deleted" - - name: Delete existing Docker Hub tag + - name: Delete existing Docker Hub tag (conditional) + if: github.event.inputs.delete_existing_docker_image == 'yes' run: | TAG_NAME="${{ github.event.inputs.tag_name }}" echo "Deleting existing Docker Hub tag: $TAG_NAME" # Delete the tag from Docker Hub - curl -X DELETE \ - -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ - "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ - -H "Accept: application/json" 2>/dev/null || echo "Docker Hub tag $TAG_NAME not found" + # Try with Personal Access Token first, fallback to password + if [ -n "${{ secrets.DOCKER_PAT }}" ]; then + echo "Using Personal Access Token for deletion" + curl -X DELETE \ + -H "Authorization: Bearer ${{ secrets.DOCKER_PAT }}" \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json" 2>/dev/null || echo "Docker Hub tag $TAG_NAME not found" + else + echo "Using username/password for deletion" + curl -X DELETE \ + -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json" 2>/dev/null || echo "Docker Hub tag $TAG_NAME not found" + fi echo "Docker Hub tag $TAG_NAME deleted" - - name: Wait for cleanup + - name: Skip Docker Hub deletion + if: github.event.inputs.delete_existing_docker_image == 'no' + run: | + echo "Skipping Docker Hub image deletion as requested by user" + echo "⚠️ Note: This may result in multiple digests for the same tag" + + - name: Wait for cleanup (conditional) + if: github.event.inputs.delete_existing_docker_image == 'yes' run: | echo "Waiting 30 seconds for cleanup to complete..." sleep 30 @@ -149,5 +175,9 @@ jobs: echo "- **Tag**: ${{ github.event.inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY echo "- **New Digest**: ${{ steps.docker_build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY echo "- **New Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Docker Image Deletion**: ${{ github.event.inputs.delete_existing_docker_image }}" >> $GITHUB_STEP_SUMMARY echo "- **Status**: ✅ Successfully moved tag to latest commit" >> $GITHUB_STEP_SUMMARY echo "- **⚠️ Warning**: Git tag position has been changed!" >> $GITHUB_STEP_SUMMARY + if [ "${{ github.event.inputs.delete_existing_docker_image }}" = "no" ]; then + echo "- **⚠️ Note**: Multiple digests may exist for the same tag" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/retag-release.yaml b/.github/workflows/retag-release.yaml index 5b98530..520bf4c 100644 --- a/.github/workflows/retag-release.yaml +++ b/.github/workflows/retag-release.yaml @@ -10,6 +10,14 @@ on: description: 'Tag name to rebuild Docker image (e.g., v0.4.0)' required: true type: string + delete_existing_docker_image: + description: 'Delete existing Docker Hub image before rebuilding?' + required: true + type: choice + default: 'yes' + options: + - 'yes' + - 'no' confirm_rebuild: description: 'Confirm Docker image rebuild (type "REBUILD" to confirm)' required: true @@ -42,20 +50,38 @@ jobs: fi echo "Validating tag: $TAG_NAME" - - name: Delete existing Docker image from Docker Hub + - name: Delete existing Docker image from Docker Hub (conditional) + if: github.event.inputs.delete_existing_docker_image == 'yes' run: | TAG_NAME="${{ github.event.inputs.tag_name }}" echo "Deleting existing Docker image for tag: $TAG_NAME" - # Delete the Docker image from Docker Hub (Git tag position remains unchanged) - curl -X DELETE \ - -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ - "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ - -H "Accept: application/json" + # Delete the Docker image from Docker Hub (Git tag position remains unchanged) + # Try with Personal Access Token first, fallback to password + if [ -n "${{ secrets.DOCKER_PAT }}" ]; then + echo "Using Personal Access Token for deletion" + curl -X DELETE \ + -H "Authorization: Bearer ${{ secrets.DOCKER_PAT }}" \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json" + else + echo "Using username/password for deletion" + curl -X DELETE \ + -u ${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }} \ + "https://hub.docker.com/v2/repositories/${{ env.DOCKER_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}/tags/$TAG_NAME/" \ + -H "Accept: application/json" + fi echo "Docker image for tag $TAG_NAME deleted from Docker Hub" - - name: Wait for tag deletion + - name: Skip Docker Hub deletion + if: github.event.inputs.delete_existing_docker_image == 'no' + run: | + echo "Skipping Docker Hub image deletion as requested by user" + echo "⚠️ Note: This may result in multiple digests for the same tag" + + - name: Wait for tag deletion (conditional) + if: github.event.inputs.delete_existing_docker_image == 'yes' run: | echo "Waiting 30 seconds for Docker Hub to process deletion..." sleep 30 @@ -124,4 +150,8 @@ jobs: echo "- **Tag**: ${{ github.event.inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY echo "- **New Digest**: ${{ steps.docker_build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY echo "- **Git Tag Position**: Unchanged (same commit)" >> $GITHUB_STEP_SUMMARY + echo "- **Docker Image Deletion**: ${{ github.event.inputs.delete_existing_docker_image }}" >> $GITHUB_STEP_SUMMARY echo "- **Status**: ✅ Successfully rebuilt Docker image" >> $GITHUB_STEP_SUMMARY + if [ "${{ github.event.inputs.delete_existing_docker_image }}" = "no" ]; then + echo "- **⚠️ Note**: Multiple digests may exist for the same tag" >> $GITHUB_STEP_SUMMARY + fi