[AWS] SpringBoot 프로젝트 AWS CodeDeploy + S3 + Github Actions를 이용하여 CI/CD 구축, Docker + EC2 + RDS 로 배포하기
프로젝트 아키텍처
1. EC2 서버 만들기
2. RDS 데이터베이스 생성
Docker로 스프링부트 jar 파일과 mariadb를 이미지화 하여 EC2 서버에 배포 하려고 하였으나,
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution [Socket fail to connect to host:address=(host=mariadb)(port=3306)(type=primary). mariadb] [n/a]
mariadb가 연결이 되지 않는 수많은 오류에 마주했었다..
우리가 참고한 자료들은 모두 mariadb 설정 부분이 빠져있었고 팀원들과 상의 후 DB를 RDS에 올리는 게 맞다는 결론을 내렸다.
RDS 프리티어 생성은 다음 글을 참고하였다.
3. RDS 연결
springboot 프로젝트의 application.properties 코드이다.
spring.application.name=<your-app-name>
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb:<RDS 엔드포인트>/<DB 이름>
spring.datasource.username=사용자이름
spring.datasource.password=패스워드
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
application.name, url, username, password는 각자 데이터베이스 설정에 맞게 바꾸어야 한다.
RDS 연결 후에도 Github Actions에서 RDS mariadb 접근을 하지 못하는 오류가 발생했었다.
EC2 인바운드 규칙을 Anywhere-ipv4 로 수정했더니 RDS 접근이 가능해졌다.
Github Actions에서 접근하려면 Github Actions ip주소를 인바운드 규칙에 저장해야 한다고 한다.
https://makethree.tistory.com/19
IAM 정책 권한 설정
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress"
],
"Resource": "arn:aws:ec2:ap-northeast-2:259091037774:security-group/sg-0bbc295bdc0f23bb3"
}
]
}
4. CI/CD 구축 과정
EC2, S3 IAM 역할 설정, CodeDeploy 설정은 위 글을 참고해서 세팅하였다. 내 글은 위 글에서 우리가 수정한 부분들만 거의 담고 있으니 위 글을 참고하길 바란다. 너무 설명이 잘되어있어서 큰 도움이 되었다..
우리는 RDS 데이터베이스와 S3에 이미지를 저장하는 기능을 구현한 웹페이지를 배포해야 했기 때문에 많은 시행착오를 겪었다.
4-1. Github secrets 지정하기
자신의 github repository의 settings에 들어가면 이렇게 secrets and variables를 지정할 수 있다.
우리는 IAM에서 발급한 access key, secret key, aws region를 저장하는 secrets을 생성했다.
또한 따로 S3에 이미지(png, jpg)를 저장하기 위한 AWS S3 계정을 따로 생성해 두었었는데,
기존 프로젝트에서 application-private.properties를 생성하여 gitignore에 저장해 두었던 터라 이 key들을 어떻게 가져올지 고민이 많았었다.
기존에 설정했던 내가 쓴 블로그 정리글 : https://nymagicshop16.tistory.com/110
지피티 선생님이 알려준 방법은 Github secrets에 저장된 key들을 환경변수로 선언하여 application.properties에 전달하는 것이었는데, 작동하지 않았다..
지피티가 알려준 코드였던 것
- name: Set environment variables for S3 Image Bucket
run: |
echo "AWS_IMG_ACCESS_KEY_ID=${{ secrets.AWS_IMG_ACCESS_KEY }}" >> $GITHUB_ENV
echo "AWS_IMG_SECRET_ACCESS_KEY=${{ secrets.AWS_IMG_SECRET_KEY }}" >> $GITHUB_ENV
echo "AWS_IMG_REGION=${{ secrets.AWS_IMG_REGION }}" >> $GITHUB_ENV
echo "AWS_IMG_BUCKET_NAME=${{ secrets.AWS_IMG_BUCKET_NAME }}" >> $GITHUB_ENV
- name: Create application.properties from secrets
run: |
echo "cloud.aws.credentials.secretKey=${AWS_IMG_SECRET_ACCESS_KEY}" >> ./src/main/resources/application.properties
echo "cloud.aws.region.static=${AWS_IMG_REGION}" >> ./src/main/resources/application.properties
echo "cloud.aws.bucket.name=${AWS_IMG_BUCKET_NAME}" >> ./src/main/resources/application.properties
echo "cloud.aws.stack.auto=false" >> ./src/main/resources/application.properties
결론적으로 application-properties를 base64로 인코딩하여 (인코딩 링크 첨부합니다)
https://www.convertstring.com/ko/EncodeDecode/Base64Encode
Github secret에 저장해 두고
- run: touch ./src/main/resources/application.properties
- run: echo ${{ secrets.APPLICATION }}| base64 --decode > ./src/main/resources/application.properties
- run: cat ./src/main/resources/application.properties
이렇게 Github Actions에서 application.properties를 생성하도록 하였다. 이 코드는 아래의 main.yml의 일부이다.
application.properties를 인코딩 한 방법은 이 글을 참고했다.
https://developing-mango.tistory.com/65
우리가 생성한 secrets 들이다.
최종적으로 인코딩했던 application.properties 코드는 이런 형태였다.
4-2. application.properties
spring.application.name=<your-app-name>
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb:<RDS 엔드포인트>/<DB 이름>
spring.datasource.username=사용자이름
spring.datasource.password=패스워드
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
server.tomcat.response-body-encoding-chunked=true
# swagger custom ??(default ??? /swagger-ui/index.html ? ??? ? ??)
springdoc.swagger-ui.path=/swagger-ui
# /api-docs endpoint custom path
springdoc.api-docs.path=/api-docs
cloud.aws.credentials.accessKey=AWS_IMG_ACCESS_KEY_ID
cloud.aws.credentials.secretKey=AWS_IMG_SECRET_ACCESS_KEY
cloud.aws.region.static=AWS_IMG_REGION
cloud.aws.bucket.name=AWS_IMG_BUCKET_NAME
cloud.aws.stack.auto=false
4-3. Github Actions workflow 스크립트 작성
main.yml
Github Actions의 workflow를 정의한 파일이다.
# workflow의 이름
name: Deploy to Amazon EC2 / Spring Boot with Maven
# 환경 변수 $변수명으로 사용
env:
PROJECT_NAME: portfolio
BUCKET_NAME: portfolio-actions-s3-bucket
CODE_DEPLOY_APP: portfolio-codedeploy-app
CODE_DEPLOY_DEPLOYMENT_GROUP: portfolio-codedeploy-deployment-group
# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# workflow는 한개 이상의 job을 가지며, 각 job은 여러 step에 따라 단계를 나눌 수 있습니다.
jobs:
build:
name: CI/CD
# 해당 jobs에서 아래의 steps들이 어떠한 환경에서 실행될 것인지를 지정합니다.
runs-on: ubuntu-latest
steps:
# 작업에서 액세스할 수 있도록 $GITHUB_WORKSPACE에서 저장소를 체크아웃합니다.
- uses: actions/checkout@v3
# (2) application.properties 설정
- uses: actions/checkout@v3
- run: touch ./src/main/resources/application.properties
- run: echo ${{ secrets.APPLICATION }}| base64 --decode > ./src/main/resources/application.properties
- run: cat ./src/main/resources/application.properties
# Spring 구동을 위한 JDK 11을 세팅합니다.
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
# Caching dependencies (디펜던시를 캐싱하여 반복적인 빌드 작업의 시간을 단축할 수 있다.)
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
# Github Action IP
- name: Get Github action IP
id: ip
uses: haythem/public-ip@v1.2
- name: Setting environment variables
run: |
echo "AWS_DEFAULT_REGION=ap-northeast-2" >> $GITHUB_ENV
echo "AWS_SG_NAME=Web-portfolio" >> $GITHUB_ENV
# AWS 인증서비스
# github repository에서 Setting에서 사용할 암호화된 변수
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION}}
- name: Add Github Actions IP to Security group
run: |
aws ec2 authorize-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION}}
# Build
- name: Build with Maven
run: mvn -B package --file pom.xml
# Build한 후 프로젝트 압축
- name: Make zip file
run: zip -r ./$PROJECT_NAME.zip .
shell: bash
# Upload to S3 stroage
- name: Upload to S3
run: aws s3 cp $PROJECT_NAME.zip s3://$BUCKET_NAME/deploy/$PROJECT_NAME.zip --region ap-northeast-2
# CodeDeploy에게 배포 명령을 내린다.
- name: Code Deploy
run: >
aws deploy create-deployment --application-name $CODE_DEPLOY_APP
--deployment-config-name CodeDeployDefault.AllAtOnce
--deployment-group-name $CODE_DEPLOY_DEPLOYMENT_GROUP
--s3-location bucket=$BUCKET_NAME,bundleType=zip,key=deploy/$PROJECT_NAME.zip
- name: Remove Github Actions IP from security group
run: |
aws ec2 revoke-security-group-ingress --group-name ${{ env.AWS_SG_NAME }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION}}
4-4. Codedeploy 배포 명령
appspec.yml
Codedeploy가 Github Actions를 통해 코드 배포 명령을 받았을 때 실행하는 파일이다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/portfolio
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
AfterInstall:
- location: deploy.sh
timeout: 500
runas : root
deploy.sh
Docker를 이용해 배포하는 스크립트이다.
#!/usr/bin/env bash
APP_NAME=shleeb/portfolio_app
REPOSITORY=/home/ubuntu/portfolio
WEB_NAME=portfolio_app
echo "> Check the currently running container"
CONTAINER_ID=$(docker ps -aqf "name=$WEB_NAME")
if [ -z "$CONTAINER_ID" ];
then
echo "> No such container is running."
else
echo "> Stop and remove container: $CONTAINER_ID"
docker stop "$CONTAINER_ID"
docker rm "$CONTAINER_ID"
fi
echo "> Remove previous Docker image"
docker rmi "$APP_NAME"
echo "> Build Docker image"
docker build -t "$APP_NAME" "$REPOSITORY"
echo "> Run the Docker container"
docker run -d -p 3000:8080 --name "$WEB_NAME" "$APP_NAME"
APP_NAME은 도커 이미지 이름, WEB_NAME은 도커 컨테이너 이름이다.
Repository를 처음에 "." 로 지정했었는데, Docker가 dockerfile을 읽지 못하는 이슈가 발생해서
/home/ubuntu 밑에 portfolio 폴더를 두어 repository를 지정해주었다.
4-4. Docker
Dockerfile
# Use an official Maven image to build the application
FROM maven:3-amazoncorretto-17 AS build
# Set the working directory in the container
WORKDIR /portfolio
# Copy the pom.xml and download the dependencies
COPY pom.xml ./
RUN mvn dependency:go-offline
# Copy the source code and build the application
COPY src ./src
RUN mvn clean package -DskipTests
# Use an official OpenJDK image to run the application
FROM amazoncorretto:17.0.11
# Set the working directory in the container
WORKDIR /portfolio
# Copy the built application from the build stage
COPY --from=build /portfolio/target/portfolio-0.0.1-SNAPSHOT.jar /portfolio/portfolio-0.0.1-SNAPSHOT.jar
# Expose the port the application runs on
EXPOSE 8080
# Run the application
ENTRYPOINT ["java", "-jar", "portfolio-0.0.1-SNAPSHOT.jar"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:8080"
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
우리의 프로젝트에는 이런 파일들이 필요했다.
5. Github Actions 구동
구동 성공 확인
6. AWS에서 확인
S3 Bucket에 나의 코드 zip 파일이 올라가 있는지 확인한다.
Codedeploy에서 배포 상태를 확인한다.
배포 상태에서 문제가 발생하면 View Events를 통해 상세히 확인 할 수 있다. 우리는 주로 deploy.sh에서 발생한 오류들을 상세히 확인할 수 있었고, 주로 docker와 관련된 오류를 수정하였다.
7. EC2 접속, Docker 배포 확인
SSH로 EC2의 인스턴스에 접속하여 확인해본다.
발급받은 pem 키를 통해 SSH 클라이언트에 접속할 수 있다.
windows cmd 창에서 명령어를 통해 접속하면 된다.
디렉토리 확인
배포 경로로 들어가서 배포가 잘 되었는지 확인한다.
우리의 배포 경로 : /home/ubuntu/portfolio
도커 이미지 확인
docker image ls
컨테이너 구동 확인
docker ps
위 명령어를 통해 구동중인 컨테이너를 확인한다.
컨테이너 포트가 3000번으로 열려있는 것을 확인한다.
Codedeploy까지 모두 배포 명령이 다 잘 되었는데 배포가 되지 않는다면, docker에서 log를 확인하여 내가 빌드한 코드들 중 문제가 있지 않은지, docker container가 죽어있는지 확인해 보길 바란다.
Github Actions에선 오류가 나지 않았던 부분들이 마지막에 가서 발견되어 수정할 수 있었다.
8. 배포 완료 !!
외부에서 주소로 요청
EC2 인스턴스의 퍼블릭 IPv4 주소에 포트번호로 접속하여 배포가 무사히 잘 되었는지 확인한다.
오늘 배포를 완료하여서 아키텍처와 상세한 설명은 더욱 추가할 나갈 예정이다!