Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
yyy9942
left a comment
There was a problem hiding this comment.
굳 전체적으로 잘 해주셨습니다. 조금만 수정해보시죠. 그리고 conflict 나고있는데 main을 머지받거나 리베이스해서 해결해주세요.
| byte[] bytes = s3Object.readAllBytes(); | ||
| String contentType = s3Object.response().contentType(); | ||
|
|
||
| return new ImageDownloadResponse(bytes, contentType); |
There was a problem hiding this comment.
이미지는 S3 공개 URL을 내려줘서 Client에서 S3를 직접 찌르도록 수정해주세요.
| PARAMETERS=$(jq -n --arg branch "$DEPLOY_BRANCH" --arg image "$IMAGE_URI" --arg region "$AWS_REGION" '{commands: [ | ||
| "set -e", "cd /home/ubuntu/snowthing", | ||
| "git -c safe.directory=/home/ubuntu/snowthing fetch origin \"" + $branch + "\"", | ||
| "git -c safe.directory=/home/ubuntu/snowthing reset --hard \"origin/" + $branch + "\"", |
There was a problem hiding this comment.
여기서는 이미지를 github.sha 기준으로 만들었는데, EC2에서는 다시 현재 브랜치의 HEAD를 받아오고 있네요.
워크플로가 대기하는 사이 새 커밋이 들어오면 검증한 이미지와 실제 실행하는 compose.prod.yml/배포 스크립트의 버전이 달라질 수 있습니다. digest를 고정한 의미가 여기서 깨질 수 있어요.
브랜치명이 아니라 ${{ github.sha }}를 SSM으로 넘기고, EC2에서도 그 SHA를 명시적으로 fetch/reset해서 이미지와 배포 설정을 하나의 커밋 단위로 맞춰주세요. 프론트 워크플로도 같은 부분을 같이 확인해보면 좋겠습니다.
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.parseMediaType(download.contentType())); | ||
| headers.setCacheControl("public, max-age=86400"); |
There was a problem hiding this comment.
여기는 인증된 사용자만 이미지를 내려받게 해두었는데 응답 캐시는 public으로 설정되어 있네요.
public이면 공유 프록시나 CDN이 이 응답을 저장했다가 다른 요청에 재사용할 수 있어서, 현재의 비공개 버킷 + 인증 조회 정책과 서로 충돌합니다.
비공개 이미지가 맞다면 private 또는 no-store로 바꿔주세요. 만약 공개 이미지로 운영하려는 의도라면 인증을 왜 요구하는지부터 정책을 다시 정리하고, 공개 가능한 이미지와 비공개 이미지를 구분하는 편이 좋겠습니다.
| } | ||
|
|
||
| public ImageDownloadResponse getImage(String imageKey) { | ||
| if (imageKey == null || imageKey.isBlank() || imageKey.contains("..")) { |
There was a problem hiding this comment.
여기서는 ..만 막고 있어서, 로그인한 사용자가 키 이름만 알면 images/ 바깥의 객체도 조회할 수 있습니다. 지금 IAM 권한도 버킷 전체 객체에 열려 있어서 애플리케이션 레벨의 경계가 사실상 없는 상태예요.
업로드할 때 만드는 images/{UUID}.{허용 확장자} 형식을 정확히 검증하거나, 더 안전하게는 DB의 이미지 식별자를 받고 서버가 실제 S3 키를 조립하도록 바꿔주세요. images/ 외부 키와 잘못된 UUID/확장자가 거부되는 테스트도 같이 있으면 좋겠습니다.
| GetObjectRequest.builder().bucket(bucketName).key(imageKey).build(); | ||
|
|
||
| ResponseInputStream<GetObjectResponse> s3Object = s3Client.getObject(getRequest); | ||
| byte[] bytes = s3Object.readAllBytes(); |
There was a problem hiding this comment.
이 방식은 S3 객체 크기를 확인하지 않고 readAllBytes()로 전부 힙에 올립니다. 게다가 ImageDownloadResponse 생성 시 한 번, data() 접근 시 다시 한 번 복사해서 요청 하나당 큰 byte 배열이 최소 3개 생길 수 있어요. 동시 요청이 들어오면 메모리 사용량이 급격히 커질 수 있습니다.
먼저 S3의 contentLength 상한을 검증하고, 가능하면 애플리케이션이 byte 배열을 들고 있지 않도록 스트리밍 응답이나 presigned URL 방식을 검토해주세요. 현재 구조를 유지한다면 다운로드 크기 제한과 대용량 객체 거부 테스트는 꼭 필요해 보입니다.
| GetObjectRequest getRequest = | ||
| GetObjectRequest.builder().bucket(bucketName).key(imageKey).build(); | ||
|
|
||
| ResponseInputStream<GetObjectResponse> s3Object = s3Client.getObject(getRequest); |
There was a problem hiding this comment.
ResponseInputStream을 직접 열고 닫지 않고 있습니다. 정상적으로 끝까지 읽은 경우에는 연결이 반환될 수 있지만, 읽는 도중 예외가 발생하면 스트림과 HTTP 연결이 남을 가능성이 있습니다.
try-with-resources로 감싸서 성공/실패와 상관없이 닫히도록 보장해주세요. 예외 발생 시에도 close되는지 테스트까지 있으면 좋겠습니다.
| @RequiredArgsConstructor | ||
| public class ImageService { | ||
|
|
||
| private static final long MAX_FILE_SIZE = 5 * 1024 * 1024L; // 5MB |
There was a problem hiding this comment.
서비스에서는 최대 5MB까지 허용한다고 정의했는데, spring.servlet.multipart.max-file-size와 max-request-size 설정이 보이지 않습니다. 그러면 Spring의 multipart 파서가 서비스 검증보다 먼저 요청을 잘라서 1MB 초과~5MB 이하 파일도 실패할 수 있습니다.
애플리케이션 설정의 multipart 제한과 이 상수를 같은 값으로 맞추고, 프레임워크에서 발생하는 업로드 크기 초과 예외도 FILE_SIZE_EXCEEDED로 변환해주세요. 경계값 통합 테스트도 추가하면 정책이 확실해질 것 같습니다.
| DIGEST=$(aws ecr describe-images --repository-name "$REPOSITORY" --image-ids imageTag="$SOURCE_TAG" --query 'imageDetails[0].imageDigest' --output text) | ||
| MANIFEST=$(aws ecr batch-get-image --repository-name "$REPOSITORY" --image-ids imageDigest="$DIGEST" --query 'images[0].imageManifest' --output text) | ||
| aws ecr put-image --repository-name "$REPOSITORY" --image-tag "$STABLE_TAG" --image-manifest "$MANIFEST" >/dev/null | ||
| mapfile -t RELEASE_TAGS < <(aws ecr describe-images --repository-name "$REPOSITORY" --image-ids imageDigest="$DIGEST" --query 'imageDetails[0].imageTags[?starts_with(@, `release-`)]' --output text | tr '\t' '\n') |
There was a problem hiding this comment.
여기서는 stable로 승격할 때 같은 digest에 붙어 있는 모든 release-* 태그를 삭제하고 있습니다. 서로 다른 커밋이라도 이미지 내용이 같으면 같은 digest를 가질 수 있어서, 다른 커밋의 정상적인 롤백 지점까지 같이 사라질 수 있어요.
우선은 사용자가 선택한 SOURCE_TAG만 제거하는 편이 안전해 보입니다. 여러 release 태그를 정리하려는 목적이라면 digest 기준 일괄 삭제보다는 보관 개수/기간을 포함한 명시적인 retention 정책으로 분리해주세요. 백엔드·프론트에 중복된 stable 처리도 함께 확인하면 좋겠습니다.
| -- ==================================================================== | ||
| -- Snowthing Production Initial Schema (MySQL 8.0) | ||
| -- Version: 001 | ||
| -- Description: 운영 RDS 최초 스키마 생성 스크립트 (비파괴 CREATE TABLE IF NOT EXISTS) |
There was a problem hiding this comment.
운영 프로필은 ddl-auto: validate인데, 현재 배포 흐름에서는 이 SQL을 RDS에 적용하는 단계가 연결되어 있지 않은 것 같습니다. 지금 RDS는 수동으로 구성해서 동작하더라도, 다음 컬럼/인덱스 변경부터는 새 애플리케이션이 스키마 검증 단계에서 바로 종료될 수 있어요.
초기 생성 SQL만 두기보다 Flyway/Liquibase 같은 버전 마이그레이션이나 별도 migration job을 배포 선행 단계로 연결해주세요. 자동 롤백까지 고려하면 이전 이미지와 새 스키마가 잠시 공존할 수 있도록 expand/contract 방식도 같이 정리할 필요가 있습니다.
| echo "=== docker inspect (secrets excluded) ===" | ||
| docker inspect --format 'image={{.Config.Image}} status={{.State.Status}} running={{.State.Running}} exit_code={{.State.ExitCode}} error={{.State.Error}} started_at={{.State.StartedAt}} finished_at={{.State.FinishedAt}} restart_count={{.RestartCount}} network_mode={{.HostConfig.NetworkMode}}' "$CONTAINER_NAME" || true | ||
| echo "=== container logs (last 100 lines) ===" | ||
| docker logs --tail 100 --timestamps "$CONTAINER_NAME" 2>&1 || true |
There was a problem hiding this comment.
여기서 컨테이너 로그와 Nginx 오류 로그 전체를 tee로 stdout에도 내보내고 있습니다. 이 stdout은 SSM 응답으로 수집되고, 워크플로에서 StandardOutputContent를 그대로 출력하므로 장애 시 사용자 정보·토큰·요청값·DB 오류 내용이 Actions 로그까지 올라갈 수 있어요.
원본 진단 로그는 EC2 파일에만 저장하고, Actions에는 저장 경로와 민감정보를 제거한 요약만 출력하는 편이 안전합니다. 로그 파일 권한과 보관 기간도 같이 정해주세요.
📌 개요 (Overview)
EC2에서 소스를 직접 빌드하던 배포 방식을 ECR 이미지 기반으로 변경했습니다.
GitHub Actions가 프런트엔드와 백엔드 이미지를 빌드해 ECR에 저장하고, EC2는 SSM으로 전달받은 image digest를 pull해 실행합
니다. 이전 digest를 이용한 수동 롤백과 RDS 스냅샷 복원도 실제 환경에서 검증했습니다.
🛠️ 주요 변경 사항 (What Changed)
프런트엔드·백엔드 ECR 저장소 분리
GitHub Actions에서 Docker 이미지 빌드 및 ECR push
Git commit SHA 기반 이미지 태그 적용
EC2 배포 대상을 태그가 아닌 image digest로 고정
OIDC로 AWS 배포 역할을 수임하고 SSM Run Command로 배포
EC2에서는 이미지 빌드 없이 ECR pull과 컨테이너 실행만 수행
프런트엔드·백엔드 개별 배포 및 헬스체크 적용
배포 실패 시 직전 실행 이미지로 자동 복구
release- 또는 stable-를 이용한 수동 롤백 추가
배포 실패 시 다음 정보를 같은 UTC 시각으로 보존
공식 GitHub Actions를 Node.js 24 기반 주요 버전으로 갱신
ECR 수명 주기 정책 적용
RDS 수동 스냅샷을 별도 DB로 복원하고 레코드 대조
복원 검증이 끝난 임시 RDS 삭제
Sprint 06 제출 문서와 복구 증거 추가
💡 핵심 기술 의사결정 및 트레이드오프 (Technical Rationale)
기존 방식은 EC2가 Git 소스를 받은 뒤 직접 이미지를 빌드했습니다. 구성이 단순하지만 EC2의 CPU·디스크·빌드 캐시 상태에 영향
을 받고, 과거 커밋을 다시 빌드한 결과가 당시 배포한 이미지와 같다고 보장하기 어렵습니다.
이번에는 GitHub Actions에서 이미지를 한 번 빌드해 ECR에 저장하고, EC2가 해당 이미지를 실행하도록 변경했습니다. 실제 실행
대상은 태그가 아니라 변경할 수 없는 digest로 지정했습니다. 같은 결과물을 재배포하거나 이전 버전으로 되돌릴 수 있고, EC2는
빌드 작업을 하지 않아도 됩니다.
대신 ECR 저장 비용과 이미지 수명 주기 관리, push·pull IAM 권한, 태그 승격 규칙이 추가됐습니다. 저장 비용을 줄이기 위해
candidate·release·stable 보관 정책을 나누고, IAM 권한은 두 ECR 저장소와 대상 EC2에 필요한 범위로 제한했습니다.
이미지 롤백은 애플리케이션만 되돌리고 DB 스키마와 데이터는 되돌리지 않습니다. 따라서 DB 스키마가 바뀐 배포는 이전 애플리
케이션과 현재 스키마의 호환성을 먼저 확인해야 합니다. 호환되지 않으면 자동 롤백을 멈추고 forward fix 또는 검증한 DB 복원
절차를 선택해야 합니다.
🧪 테스트 및 검증 결과 (Verification & QA)
프런트엔드 최초 ECR 배포
백엔드 최초 ECR 배포
새 로그인 UI 배포
이전 프런트엔드 digest 롤백
최신 프런트엔드 digest 재배포
공식 Actions 버전 갱신 후 재검증
stable 이미지 지정
RDS 스냅샷 복원
✅ PR 체크리스트 (Checklist)