From 06add6ce2ad6caa44bf705b5b1dfd9f7075d5887 Mon Sep 17 00:00:00 2001 From: Sooyoung Yoon Date: Mon, 7 Sep 2026 15:28:54 +0900 Subject: [PATCH] =?UTF-8?q?ci(workflow):=20CI=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1,=20actuator,?= =?UTF-8?q?=20parameter=20store=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 184 ++++++++++++++++++ Dockerfile | 7 + build.gradle | 5 + .../global/security/SecurityConfig.java | 1 + .../resources/application-local.properties | 4 +- .../resources/application-prod.properties | 2 + src/main/resources/application.properties | 2 + .../dropit/DropitServerApplicationTests.java | 6 - 8 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/ci-cd.yml create mode 100644 Dockerfile create mode 100644 src/main/resources/application-prod.properties diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..0fd27a9 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,184 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +# OIDC 토큰 요청 권한 +permissions: + id-token: write # OIDC 토큰 요청용 + contents: read # 코드 체크아웃용 (체크아웃 - Repo에 있는 코드를 Runner에 옮김) + +jobs: + build: + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.vars.outputs.short_sha }} + + steps: + # Step 1: 코드 가져오기 + - name: Checkout code + uses: actions/checkout@v6 + + # Step 2: 커밋 해시 추출 + - name: Set short SHA + id: vars + run: echo "short_sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT + + # Step 3: 스프링 빌드를 위한 Java 21 설치 + - name: Setup Java 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'corretto' + + # Step 4: Gradle 캐시 + - name: Cache Gradle packages + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # Step 5: 테스트 실행 + - name: Run tests + run: ./gradlew test + + # Step 6: JAR 빌드 + - name: Build JAR + run: ./gradlew bootJar + + # Step 7: AWS 자격증명 - OIDC + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + + # Step 8: ECR 로그인 + - name: Login to ECR + uses: aws-actions/amazon-ecr-login@v2 + + # Step 9: Docker 빌드 및 push + - name: Build and push Docker image + run: | + SHORT_SHA=${{ steps.vars.outputs.short_sha }} + IMAGE_URI=${{ vars.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION }}.amazonaws.com/${{ vars.ECR_REPOSITORY }} + + # Docker 빌드 및 push + docker build -t $IMAGE_URI:$SHORT_SHA . +# docker build --platform linux/arm64 -t $IMAGE_URI:$SHORT_SHA . + docker push $IMAGE_URI:$SHORT_SHA + + echo "✅ Pushed: $IMAGE_URI:$SHORT_SHA" + +# deploy: +# needs: build +# if: github.ref == 'refs/heads/main' # main push에서만 배포 (PR에서는 CI만 실행) +# runs-on: ubuntu-latest +# +# steps: +# # Step 1: AWS 자격증명 - OIDC +# - name: Configure AWS credentials +# uses: aws-actions/configure-aws-credentials@v6 +# with: +# role-to-assume: ${{ vars.AWS_ROLE_ARN }} +# aws-region: ${{ vars.AWS_REGION }} +# +# # Step 2: Launch Template 새 버전 생성 (User Data의 IMAGE_TAG를 새 커밋 SHA로 교체) +# - name: Update Launch Template +# env: +# IMAGE_TAG: ${{ needs.build.outputs.image_tag }} +# AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }} +# AWS_REGION: ${{ vars.AWS_REGION }} +# ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} +# LAUNCH_TEMPLATE_ID: ${{ vars.LAUNCH_TEMPLATE_ID }} +# run: | +# ECR_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}" +# +# USER_DATA=$(cat <<'USERDATA' | sed "s|__IMAGE_TAG__|${IMAGE_TAG}|g; s|__AWS_ACCOUNT_ID__|${AWS_ACCOUNT_ID}|g; s|__AWS_REGION__|${AWS_REGION}|g; s|__ECR_REPOSITORY__|${ECR_REPOSITORY}|g" | base64 -w 0 +# #!/bin/bash +# set -e +# AWS_REGION="__AWS_REGION__" +# AWS_ACCOUNT_ID="__AWS_ACCOUNT_ID__" +# ECR_REPOSITORY="__ECR_REPOSITORY__" +# IMAGE_TAG="__IMAGE_TAG__" +# ECR_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}" +# dnf update -y +# dnf install -y docker +# systemctl enable docker +# systemctl start docker +# aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com +# docker pull ${ECR_URI}:${IMAGE_TAG} +# docker run -d -p 8080:8080 --restart unless-stopped --name ci-demo -e SPRING_PROFILES_ACTIVE=prod ${ECR_URI}:${IMAGE_TAG} +# USERDATA +# ) +# +# NEW_VERSION=$(aws ec2 create-launch-template-version \ +# --launch-template-id ${LAUNCH_TEMPLATE_ID} \ +# --source-version '$Latest' \ +# --launch-template-data "{\"UserData\":\"${USER_DATA}\"}" \ +# --query 'LaunchTemplateVersion.VersionNumber' \ +# --output text) +# +# echo "✅ Launch Template v${NEW_VERSION} 생성 완료" +# +# aws ec2 modify-launch-template \ +# --launch-template-id ${LAUNCH_TEMPLATE_ID} \ +# --default-version ${NEW_VERSION} +# +# echo "✅ 기본 버전 → v${NEW_VERSION} 변경 완료" +# +# # Step 3: Instance Refresh 시작 +# - name: Start Instance Refresh +# id: refresh +# env: +# ASG_NAME: ${{ vars.ASG_NAME }} +# run: | +# REFRESH_ID=$(aws autoscaling start-instance-refresh \ +# --auto-scaling-group-name ${ASG_NAME} \ +# --preferences '{ +# "MinHealthyPercentage": 100, +# "InstanceWarmup": 120 +# }' \ +# --query 'InstanceRefreshId' \ +# --output text) +# +# echo "refresh_id=${REFRESH_ID}" >> $GITHUB_OUTPUT +# echo "✅ Instance Refresh 시작: ${REFRESH_ID}" +# +# # Step 4: Instance Refresh 완료 대기 +# - name: Wait for Instance Refresh +# env: +# ASG_NAME: ${{ vars.ASG_NAME }} +# REFRESH_ID: ${{ steps.refresh.outputs.refresh_id }} +# run: | +# echo "⏳ Instance Refresh 완료 대기 중..." +# +# for i in $(seq 1 30); do +# STATUS=$(aws autoscaling describe-instance-refreshes \ +# --auto-scaling-group-name ${ASG_NAME} \ +# --instance-refresh-ids ${REFRESH_ID} \ +# --query 'InstanceRefreshes[0].Status' \ +# --output text) +# +# echo "시도 ${i}/30 - 상태: ${STATUS}" +# +# if [ "${STATUS}" = "Successful" ]; then +# echo "✅ Instance Refresh 성공! 배포 완료!" +# exit 0 +# elif [ "${STATUS}" = "Failed" ] || [ "${STATUS}" = "Cancelled" ]; then +# echo "❌ Instance Refresh 실패: ${STATUS}" +# exit 1 +# fi +# +# sleep 30 +# done +# +# echo "❌ Instance Refresh 타임아웃 (15분 초과)" +# exit 1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d88470b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM eclipse-temurin:21-jre +WORKDIR /app + +COPY build/libs/*.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index c4fcda7..7343007 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation "io.github.openfeign.querydsl:querydsl-jpa:${querydslVersion}" implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation platform('io.awspring.cloud:spring-cloud-aws-dependencies:4.1.0') + implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store' implementation 'io.jsonwebtoken:jjwt-api:0.13.0' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0' @@ -43,6 +47,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' testImplementation 'org.springframework.boot:spring-boot-starter-security-test' + testImplementation 'org.springframework.boot:spring-boot-starter-actuator-test' testImplementation 'org.testcontainers:testcontainers-mysql' testCompileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/dropit/global/security/SecurityConfig.java b/src/main/java/com/dropit/global/security/SecurityConfig.java index dd152d9..d95a3d0 100644 --- a/src/main/java/com/dropit/global/security/SecurityConfig.java +++ b/src/main/java/com/dropit/global/security/SecurityConfig.java @@ -50,6 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti errorResponseSender.send(response, JwtErrorCode.JWT_ACCESS_DENIED)) ) .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.GET, "/actuator/health").permitAll() .requestMatchers(HttpMethod.POST, "/auth/signup", "/auth/login", "/auth/reissue").permitAll() .anyRequest().authenticated() ) diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index e758cb8..2fb7625 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -3,4 +3,6 @@ spring.config.import=optional:file:.env.local[.properties] spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true -logging.level.com.dropit=DEBUG \ No newline at end of file +logging.level.com.dropit=DEBUG + +management.endpoint.health.show-details=always \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..72bd2a3 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,2 @@ +management.endpoint.health.show-details=never +spring.config.import=aws-parameterstore:/dropit-server/prod/ \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 45863b2..5eb7c85 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,3 +12,5 @@ spring.data.redis.port=${REDIS_PORT} jwt.secret=${JWT_SECRET} jwt.expire.access=30m jwt.expire.refresh=14d + +management.endpoints.web.exposure.include=health \ No newline at end of file diff --git a/src/test/java/com/dropit/DropitServerApplicationTests.java b/src/test/java/com/dropit/DropitServerApplicationTests.java index 50f61da..47b0580 100644 --- a/src/test/java/com/dropit/DropitServerApplicationTests.java +++ b/src/test/java/com/dropit/DropitServerApplicationTests.java @@ -1,13 +1,7 @@ package com.dropit; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DropitServerApplicationTests { - - @Test - void contextLoads() { - } - }