From c820eaaddd5d87630d9bcab9bad15e846ef32066 Mon Sep 17 00:00:00 2001 From: Seungho Lee Date: Wed, 7 Aug 2024 00:11:52 +0900 Subject: [PATCH] =?UTF-8?q?ci(ecs):=20AWS=20ECS=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EA=B0=9C=EC=84=A0=EB=90=9C=20Pipeline=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=EB=B0=8F=20=EC=84=A4=EC=A0=95=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개요 - 기존에 EC2만을 사용해서 Pipeline을 구성했는데, ECS로 이전함에 따라, 알맞게 파이프를 구성하자 수정사항 - 기존에 넘겨주었던 많은 Secret 환경 변수들을 AWS에 안전하게 저장하기 때문에 pipeline에 굳이 환경변수를 노출할 필요가 없어져서 삭제한다. - Prod에 Push를 날리면, ECS에서 Task Definition을 새로 만들어서 새로운 컨테이너를 돌린다. - Spring Actuator를 install하여, /health path에서 Health Check가 이루어지도록 한다. 이는, 서버가 다운되었는지 확인하는 Load balancer의 요구 사항이므로 추가한다. - 추가로, 해당 path를 누구나 접근할 수 있게 열어놓는다. - 아마존에서 배포 시에는, 아마존의 secretKey, accessKey등의 키가 없어도 S3 접근이 가능하다. 따라서 이 부분을 삭제하고 로컬에서는 테스트할때 필요하므로, 로컬 환경에서만 필요하도록 설정해놓는다. --- .github/workflows/deploys.yml | 90 ++++--------------- build.gradle | 1 + .../java/com/dife/api/config/AWSConfig.java | 21 +++-- .../com/dife/api/config/SecurityConfig.java | 1 + src/main/java/com/dife/api/jwt/JWTFilter.java | 1 + src/main/resources/application-local.yml | 2 + src/main/resources/application.yml | 9 +- 7 files changed, 46 insertions(+), 79 deletions(-) diff --git a/.github/workflows/deploys.yml b/.github/workflows/deploys.yml index bc2daf01..5f76c4bf 100644 --- a/.github/workflows/deploys.yml +++ b/.github/workflows/deploys.yml @@ -27,6 +27,7 @@ jobs: uses: aws-actions/amazon-ecr-login@v2 - name: Build, tag, and push image to ECR + id: build-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }} @@ -35,77 +36,24 @@ jobs: docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - - name: Deploy to EC2 Instance - uses: appleboy/ssh-action@v1.0.3 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION_CODE: ${{ secrets.AWS_REGION_CODE }} - AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_NAME: ${{ secrets.DB_NAME }} - DB_USER: ${{ secrets.DB_USER }} - REDIS_HOST: ${{ secrets.REDIS_HOST }} - REDIS_PORT: ${{ secrets.REDIS_PORT }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }} - GOOGLE_APP_PASSWORD: ${{ secrets.GOOGLE_APP_PASSWORD }} + - name: Download ECS Task Definition + run: | + aws ecs describe-task-definition --task-definition ${{ vars.AWS_ECS_TASK_DEFINITION_FAMILY }} --query taskDefinition > task-definition.json + + - name: Fill in the new image ID in task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USERNAME }} - key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} - port: ${{ secrets.EC2_SSH_PORT }} - envs: ECR_REGISTRY, ECR_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION_CODE, AWS_S3_BUCKET_NAME, DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, REDIS_HOST, REDIS_PORT, JWT_SECRET, GOOGLE_EMAIL, GOOGLE_APP_PASSWORD - script: | - sudo rm -rf .aws - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID - aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY - aws configure set default.region $AWS_REGION_CODE - aws configure set default.ouput json - - rm env.prod - touch env.prod - echo "ECR_REGISTRY=$ECR_REGISTRY" >> env.prod - echo "ECR_REPOSITORY=$ECR_REPOSITORY" >> env.prod - echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" >> env.prod - echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" >> env.prod - echo "AWS_REGION_CODE=$AWS_REGION_CODE" >> env.prod - echo "AWS_S3_BUCKET_NAME=$AWS_S3_BUCKET_NAME" >> env.prod - echo "DB_HOST=$DB_HOST" >> env.prod - echo "DB_PORT=$DB_PORT" >> env.prod - echo "DB_NAME=$DB_NAME" >> env.prod - echo "DB_USER=$DB_USER" >> env.prod - echo "REDIS_HOST=$REDIS_HOST" >> env.prod - echo "REDIS_PORT=$REDIS_PORT" >> env.prod - echo "DB_PASSWORD=$DB_PASSWORD" >> env.prod - echo "JWT_SECRET=$JWT_SECRET" >> env.prod - echo "GOOGLE_EMAIL=$GOOGLE_EMAIL" >> env.prod - echo "GOOGLE_APP_PASSWORD=$GOOGLE_APP_PASSWORD" >> env.prod - - docker stop myapp || true - docker rm myapp || true - docker rmi -f $(docker images -aq) - aws ecr get-login-password --region $AWS_REGION_CODE | docker login --username AWS --password-stdin $ECR_REGISTRY - docker pull $ECR_REGISTRY/$ECR_REPOSITORY:latest - docker run -d --env-file ./env.prod -p 80:8080 --name myapp $ECR_REGISTRY/$ECR_REPOSITORY:latest - - - name: Check Container Status - uses: appleboy/ssh-action@v1.0.3 + task-definition: task-definition.json + container-name: ${{vars.AWS_ECS_CONTAINER_NAME}} + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy to ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USERNAME }} - key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} - port: ${{ secrets.EC2_SSH_PORT }} - script: | - if docker ps | grep -w "myapp"; then - echo "Container 'myapp' is running." - else - echo "Server check: Container 'myapp' is not running." - docker logs myapp - exit 1 - fi + cluster: ${{vars.AWS_ECS_CLUSTER_NAME}} + service: ${{vars.AWS_ECS_SERVICE_NAME}} + task-definition: ${{ steps.task-def.outputs.task-definition }} + wait-for-service-stability: true diff --git a/build.gradle b/build.gradle index 8317247e..bd66a645 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-actuator' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/dife/api/config/AWSConfig.java b/src/main/java/com/dife/api/config/AWSConfig.java index 36380161..900a6f12 100644 --- a/src/main/java/com/dife/api/config/AWSConfig.java +++ b/src/main/java/com/dife/api/config/AWSConfig.java @@ -4,8 +4,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; @@ -15,10 +15,10 @@ @Profile("!test") public class AWSConfig { - @Value("${spring.aws.access-key}") + @Value("${spring.aws.access-key:#{null}") private String accessKey; - @Value("${spring.aws.secret-key}") + @Value("${spring.aws.secret-key:#{null}") private String secretKey; @Value("${spring.aws.session-token:#{null}}") @@ -42,14 +42,14 @@ public S3Client S3BucketWithSessionToken() { @Profile("!local") public S3Client S3BucketWithoutSessionToken() { return S3Client.builder() - .credentialsProvider( - StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey))) + .credentialsProvider(DefaultCredentialsProvider.create()) .region(Region.of(awsRegion)) .build(); } @Bean - public S3Presigner presigner() { + @Profile("local") + public S3Presigner presignerWithSessionToken() { return S3Presigner.builder() .credentialsProvider( StaticCredentialsProvider.create( @@ -57,4 +57,13 @@ public S3Presigner presigner() { .region(Region.of(awsRegion)) .build(); } + + @Bean + @Profile("!local") + public S3Presigner presigner() { + return S3Presigner.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.of(awsRegion)) + .build(); + } } diff --git a/src/main/java/com/dife/api/config/SecurityConfig.java b/src/main/java/com/dife/api/config/SecurityConfig.java index 3d1bb4c6..9fbab93f 100644 --- a/src/main/java/com/dife/api/config/SecurityConfig.java +++ b/src/main/java/com/dife/api/config/SecurityConfig.java @@ -51,6 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti "/api/members/refresh-token", "/api/members/change-password", "/api/members/login", + "/health", "/ws/**") .permitAll(); requests.requestMatchers("/api/**").authenticated(); diff --git a/src/main/java/com/dife/api/jwt/JWTFilter.java b/src/main/java/com/dife/api/jwt/JWTFilter.java index c5c28eb7..798db233 100644 --- a/src/main/java/com/dife/api/jwt/JWTFilter.java +++ b/src/main/java/com/dife/api/jwt/JWTFilter.java @@ -78,6 +78,7 @@ private boolean isExemptPath(String servletPath) { return servletPath.startsWith("/api/members/register") || servletPath.equals("/api/members/change-password") || servletPath.equals("/api/members/login") + || servletPath.equals("/health") || servletPath.startsWith("/swagger-ui/") || servletPath.startsWith("/api/v1/api-docs") || servletPath.startsWith("/ws") diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7bfd4c3f..1a231c5f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,4 +8,6 @@ spring: ddl-auto: create aws: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} session-token: ${AWS_SESSION_TOKEN} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f9fd017c..5af7c820 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,8 +25,6 @@ spring: auth: true aws: - access-key: ${AWS_ACCESS_KEY_ID} - secret-key: ${AWS_SECRET_ACCESS_KEY} region: ${AWS_REGION_CODE} bucket-name: ${AWS_S3_BUCKET_NAME} @@ -38,6 +36,13 @@ spring: jwt: secret: ${JWT_SECRET} +management: + health: + mail: + enabled: false + endpoints: + web: + base-path: / springdoc: swagger-ui: