소스파일을 S3에 올리고, CodeDeploy를 이용하여 EC2에 스프링 도커 컨테이너를 만들어 배포하고자 한다.
과정
1. Github main 브랜치에 Push
2. Github Actions에서 AWS S3에 빌드 파일 및 Dockerfile, deploy.sh 등 업로드
3. Github Actions이 AWS CodeDeploy에 배포 요청
4. CodeDeploy가 배포 실행
5. 도커 빌드 및 실행
소스코드를 저장할 버킷을 생성한다.
IAM > 사용자 > 사용자 추가
사용자 이름은 github-action-s3-codedeploy으로 설정하였다.
github actions에서 AWS S3, CodeDeploy에 접근할 권한이 필요하다. AmazonS3FullAccess
와 AWSCodeDeployFullAccess
를 선택하여 추가한다.
완료하고 Access key ID와 Access Key를 확인한다.
APPLICATION
: application.yml의 내용AWS_ACCESS_KEY_ID
: Access key IDAWS_SECRET_ACCESS_KEY
: Secret access keyGithub Repository > Actions > set up a workflow yourself
main-deploy.yml
파일을 작성한다.
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@master
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Make application.yml
run: |
cd ./src/main
mkdir resources
cd ./resources
touch ./application.yml
echo "${{ secrets.APPLICATION }}" > ./application.yml
shell: bash
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Make zip file
run: |
mkdir deploy
cp ./build/libs/*.jar ./deploy/
zip -r -qq -j ./spring-build.zip ./deploy
- 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: ap-northeast-2
- name: Upload to S3
run: |
aws s3 cp \
--region ap-northeast-2 \
./spring-build.zip s3://tcat-spring
Make application.yml: Github Secrets의 저장된 secrets.APPLICATION
으로 application.yml 파일을 만든다.
Make zip file: 압축을 위한 deploy 디렉토리를 만들어 .jar 파일을 복사한 후 spring-build.zip 파일을 만든다.
Upload to S3: tcat-spring 버킷에 zip파일을 업로드 한다.
버킷에 올라온 zip파일을 확인할 수 있다.
CodeDeploy는 Amazon EC2 인스턴스, 온프레미스 인스턴스, 서버리스 Lambda 함수 또는 Amazon ECS 서비스로 애플리케이션 배포를 자동화하는 배포 서비스이다.
배포하는 환경에 CodeDeploy Agent를 설치해야 하며, 배포 시 정의한 appspec.yml에 따라 동작하게 된다.
EC2에서 CodeDeploy를 사용하기 위해 IAM Role을 생성한다. IAM Role은 주로 AWS 서비스들이 직접 다른 AWS 서비스를 제어하기 위해 사용한다.
IAM > 역할 > 역할 생성
AmazonEC2RoleforAWS-CodeDeploy
권한을 추가한다.
이름은 role-ec2-codedeploy
로 지정하였다.
마찬가지로 CodeDeploy도 IAM Role을 생성한다.
사용사례에서 CodeDeploy를 선택한다.
권한이 자동으로 추가되어있다.
이름을 role-codedeploy
로 지정하고 생성한다.
EC2 > 인스턴스 > 작업 > 보안 > IAM 역할 수정
IAM 역할에 role-ec2-codedeploy
지정하고 인스턴스를 재부팅한다.
CodeDeploy를 이용하려면 배포하는 환경에 CodeDeploy Agent를 설치해야한다.
# 설치
sudo yum -y update
sudo yum install -y ruby
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
chmod +x ./install
sudo ./install auto
# 확인
sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID ~ 가 나오면 정상적으로 작동하는 것이다.
CodeDeploy > 애플리케이션 > 애플리케이션 생성
애플리케이션 이름을 입력하고 컴퓨팅 플랫폼에 EC2를 선택하여 애플리케이션 생성을 완료한다.
CodeDeploy > 애플리케이션 > spring-deploy > 배포 그룹 > 배포 그룹 생성
배포 그룹 이름을 입력하고 서비스 역할에 만들어 둔 role-codedeploy
를 지정한다.
배포 유형은 배포할 서비스가 1대이기 때문에 현재 위치를 선택한다.
환경 구성에 Amazon EC2 인스턴스를 선택하고 태그를 추가한다. 이 태그는 배포하려는 EC2 인스턴스의 태그와 동일하여야 한다.
배포 구성은 다음을 참고한다. 한번 배포할 때, 모든 인스턴스에 배포하도록 AllAtOnce를 선택한다.
로드 밸런싱 활성화는 해제한다.
mkdir ~/app
S3에 있는 소스를 내려받을 app 폴더를 생성한다.
# 도커 설치
sudo yum install docker -y
docker -v
# 도커 시작
sudo service docker start
# 확인
systemctl status docker.service
도커 실행을 위해 도커를 설치한다.
CodeDeploy에서 배포가 실행되면 이 appspec.yml
파일에 따라 동작하게 된다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
ApplicationStart:
- location: deploy.sh
timeout: 60
runas: ec2-user
deploy.sh
를 실행한다.#!/bin/bash
echo "> 현재 실행 중인 Docker 컨테이너 pid 확인" >> /home/ec2-user/deploy.log
CURRENT_PID=$(sudo docker container ls -q)
if [ -z $CURRENT_PID ]
then
echo "> 현재 구동중인 Docker 컨테이너가 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log
else
echo "> sudo docker stop $CURRENT_PID" # 현재 구동중인 Docker 컨테이너가 있다면 모두 중지
sudo docker stop $CURRENT_PID
sleep 5
fi
cd /home/ec2-user/app
sudo docker build -t tcat-api-spring-boot-docker .
sudo docker run -d -p 8080:8080 tcat-api-spring-boot-docker
실행 중인 도커 컨테이너가 있다면 중지하고, 도커 이미지를 빌드하고 컨테이너를 실행하게 된다.
FROM openjdk:11
WORKDIR /tcat
COPY tcat-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]
jar 파일을 실행하는 간단한 Dockerfile을 작성한다.
main-deploy.yml
에 CodeDeploy 설정을 추가한다.
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@master
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Make application.yml
run: |
cd ./src/main
mkdir resources
cd ./resources
touch ./application.yml
echo "${{ secrets.APPLICATION }}" > ./application.yml
shell: bash
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
# 수정
- name: Make zip file
run: |
mkdir deploy
cp ./appspec.yml ./deploy/
cp ./Dockerfile ./deploy/
cp ./scripts/*.sh ./deploy/
cp ./build/libs/*.jar ./deploy/
zip -r -qq -j ./spring-build.zip ./deploy
- 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: ap-northeast-2
- name: Upload to S3
run: |
aws s3 cp \
--region ap-northeast-2 \
./spring-build.zip s3://tcat-spring
# 추가
- name: Code Deploy
run: aws deploy create-deployment --application-name spring-deploy
--deployment-config-name CodeDeployDefault.OneAtATime
--deployment-group-name spring-deploy-group
--s3-location bucket=tcat-spring,bundleType=zip,key=spring-build.zip
appspec.yml
, Dockerfile
, scripts/deploy.sh
을 zip 파일에 추가한다.Route5e > 호스팅 영역 > my-tcat.com > 레코드 생성
값에는 EC2의 IP 주소를 넣어두었다. ALB 설정을 마치면 변경할 것이다.
AWS Certificate Manager > 인증서 > 인증서 요청
인증서를 발급받고 'Route 53에서 레코드 생성' 버튼을 눌러 레코드를 생성한다.
EC2 > 로드 밸런싱 > 로드 밸런서 생성
Application Load Balancer를 선택한다.
Network mapping에서는 가용영역을 선택한다.
Security groupts에서는 EC2와 같은 보안그룹을 선택해준다.
Create target group으로 타켓 그룹을 생성한다.
이름을 지정하고 프로토콜에 HTTP, Port에 80을 입력한다.
include as pending below를 누르고, Create target group으로 타겟 그룹 생성을 완료한다.
생성한 타겟그룹으로 forward 해주고 Create load balancer를 눌러 로드 밸런서 생성을 완료한다.
로드 밸런서 > 리스너
HTTP 리스너의 규칙 편집을 선택하여 규칙을 편집한다.
규칙 THEN 부분에 리다렉션을 설정하고 HTTPS, 443 을 입력하고 업데이트를 완료한다.
HTTPS, 443을 입력하고 forward to에 생성한 타겟 그룹을 지정한다.
생성한 SSL인증서를 지정하고 리스너 추가를 완료한다.
Route5e > 호스팅 영역 > my-tcat.com > 레코드 > 레코드 편집
별칭을 선택하고 Application/Classic Load Balancer에 대한 별칭을 선택하고 등록한다
# 설치
sudo amazon-linux-extras install nginx1
# 동작
sudo service nginx start
EC2에 Nginx를 설치하고 동작시킨다.
sudo vim /etc/nginx/nginx.conf
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
if ($http_x_forwarded_proto != 'https') {
return 301 https://$host$request_uri;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header HOST $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
}
}
server 부분을 위와 같이 변경한다.
80포트로 들어오면 8080으로 전달해주는 설정이다.
sudo service nginx restart
Nginx를 재시작하면 정상적으로 작동되는 것을 확인할 수 있다.
payload가 너무 큰 경우 nginx에서 오류가 발생하기도 한다.
http {
client_max_body_size 50M;
...
}
다음을 추가하여 준다.
기존의 배포 방식은 도커 컨테이너를 중단하고 새로운 컨테이너를 만들어 실행하기까지 서비스 중단이 일어나게 된다. 유저가 서비스 중단을 느끼지 못하도록 Blue-Green 배포 방식을 사용하여 무중단 배포를 진행하려 한다.
배포 전
배포 후
Blue-Green 배포 방식이란 기존 버전을 운영하면서 신규 버전을 준비하여 신규 버전으로 전환하고 기존 버전을 중단하는 방식이다.
docker-compose.blue.yml
#blue
version: '3'
services:
tcat-api:
build: .
ports:
- "8081:8080"
container_name: spring-blue
blue 컨테이너는 port가 8081번으로 설정된다.
docker-compose.green.yml
#green
version: '3'
services:
tcat-api:
build: .
ports:
- "8082:8080"
container_name: spring-green
green 컨테이너는 port가 8082번으로 설정된다.
#!/bin/bash
cd /home/ec2-user/app
DOCKER_APP_NAME=spring
# 실행중인 blue가 있는지
EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep running)
# green이 실행중이면 blue up
if [ -z "$EXIST_BLUE" ]; then
echo "blue up"
docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build
sleep 30
docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
docker image prune -af # 사용하지 않는 이미지 삭제
# blue가 실행중이면 green up
else
echo "green up"
docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build
sleep 30
docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
docker image prune -af
fi
main-deploy.yml
수정
- name: Make zip file
run: |
mkdir deploy
cp ./docker-compose.blue.yml ./deploy/
cp ./docker-compose.green.yml ./deploy/
cp ./appspec.yml ./deploy/
cp ./Dockerfile ./deploy/
cp ./scripts/*.sh ./deploy/
cp ./build/libs/*.jar ./deploy/
zip -r -qq -j ./spring-build.zip ./deploy
docker-compose.blue.yml
, docker-compose.green.yml
파일을 복사하는 과정을 추가한다.sudo vim /etc/nginx/nginx.conf
# 추가
upstream tcat-api-server {
least_conn;
server 127.0.0.1:8081 max_fails=3 fail_timeout=10s;
server 127.0.0.1:8082 max_fails=3 fail_timeout=10s;
}
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
if ($http_x_forwarded_proto != 'https') {
return 301 https://$host$request_uri;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header HOST $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 수정
proxy_pass http://tcat-api-server;
proxy_redirect off;
}
}
proxy_pass
: 요청이 오면 upstrem tcat-api-server로 전달한다.max_fails
: 3만큼 요청이 fail한 경우 다른 서버에게 요청이 넘어간다.fail_timeout
: 30동안 서버가 응답하지 않으면 fail로 생각한다.sudo service nginx restart
Nginx를 재시작한다.
배포를 할 때마다 다음과 같이 port가 변경된다. 또한, 배포 중에 웹사이트 새로고침을 계속 하여도 접속에 이상이 없음을 확인할 수 있다.
https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/welcome.html
https://loosie.tistory.com/424
https://loosie.tistory.com/425
https://galid1.tistory.com/745
https://devlog-wjdrbs96.tistory.com/291
https://medium.com/@vdongbin/aws-elb와-nginx로-https-서버-구축하기-736b8c5ee76
https://saksin.tistory.com/1388
https://webisfree.com/2018-03-29/nginx-413-request-entity-too-large-에러-해결하기-파일-업로드-사이즈
https://subicura.com/2016/06/07/zero-downtime-docker-deployment.html
https://velog.io/@jeff0720/Travis-CI-AWS-CodeDeploy-Docker-로-배포-자동화-및-무중단-배포-환경-구축하기-2
안녕하세요. 글 정말 잘 읽었습니다!
스프링부트를 docker로 실행하고 nginx는 서버에 직접 설치한 상황에서 스프링주트 redirect 주소를 127.0.0.1로 설정해도 되는 건가요?
docker는 127.0.0.1이 아닌 고유의 ip 주소가 있는 걸로 알고 있는데 제가 잘못 알고 있는건지 헷갈려서 여쭤봅니다..