Skip to content

Commit

Permalink
ci(ecs): AWS ECS를 활용한 개선된 Pipeline 구성 및 설정
Browse files Browse the repository at this point in the history
개요

- 기존에 EC2만을 사용해서 Pipeline을 구성했는데, ECS로 이전함에 따라,
알맞게 파이프를 구성하자

수정사항

- 기존에 넘겨주었던 많은 Secret 환경 변수들을 AWS에 안전하게 저장하기
때문에 pipeline에 굳이 환경변수를 노출할 필요가 없어져서 삭제한다.
- Prod에 Push를 날리면, ECS에서 Task Definition을 새로 만들어서 새로운 컨테이너를 돌린다.

- Spring Actuator를 install하여, /health path에서 Health Check가
이루어지도록 한다. 이는, 서버가 다운되었는지 확인하는 Load balancer의
요구 사항이므로 추가한다.
  - 추가로, 해당 path를 누구나 접근할 수 있게 열어놓는다.

- 아마존에서 배포 시에는, 아마존의 secretKey, accessKey등의 키가 없어도
S3 접근이 가능하다. 따라서 이 부분을 삭제하고 로컬에서는 테스트할때
필요하므로, 로컬 환경에서만 필요하도록 설정해놓는다.
  • Loading branch information
seungholee-dev committed Aug 5, 2024
1 parent 643eb95 commit 64484a6
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 80 deletions.
92 changes: 20 additions & 72 deletions .github/workflows/deploys.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Deploy 'prod' to Amazon EC2
on:
push:
branches:
- prod
- ci/aws-ecs-pipeline

jobs:
deploy:
Expand All @@ -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 }}
Expand All @@ -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/[email protected]
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/[email protected]
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
21 changes: 15 additions & 6 deletions src/main/java/com/dife/api/config/AWSConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}}")
Expand All @@ -42,19 +42,28 @@ 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(
AwsSessionCredentials.create(accessKey, secretKey, sessionToken)))
.region(Region.of(awsRegion))
.build();
}

@Bean
@Profile("!local")
public S3Presigner presigner() {
return S3Presigner.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.of(awsRegion))
.build();
}
}
1 change: 1 addition & 0 deletions src/main/java/com/dife/api/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/dife/api/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
9 changes: 7 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -38,6 +36,13 @@ spring:
jwt:
secret: ${JWT_SECRET}

management:
health:
mail:
enabled: false
endpoints:
web:
base-path: /

springdoc:
swagger-ui:
Expand Down

0 comments on commit 64484a6

Please sign in to comment.