6) 스프링부트로 웹 서비스 출시하기 - 6. TravisCI & AWS CodeDeploy로 배포 자동화 구축하기
VCS (a.k.a github) 시스템을 통해 새로운/ 변경된 Resource에 대해 자동으로 테스트 또는 빌드 수행 후 안정적인 배포 파일을 생성하는 과정
한 마디로 깃허브 master branch에 새로 pull될 때마다 개발자가 직접 deploy.sh 를 실행시켜 배포하는 것이 아니라 배포를 자동화시키는 것
빌드 결과를 자동으로 운영 서버에 무중단 배포하는 과정
github에서 제공하는 무료 CI 서비스이다.
젠킨스와 같은 CI 툴이 있지만 젠킨스는 설치형이기 때문에 이를 위한 EC2 인스턴스가 하나 더 필요하다. 아직 프로젝트의 규모가 작고 이제 시작하는 서비스이기에 배포를 위한 EC2 인스턴스를 하나 더 추가하는 것이 부담스러워서 오픈소스 웹 서비스인 Traivs CI를 사용하기로 했다.
TravisCi의 상세 설정은 프로젝트에 존재하는 .travis.yml
로 할 수 있다. 이 파일은 build.gradle
과 같은 위치에 놓여야 하며 만약 repository directory가 repository의 root 경로에 있는 것이 아니라 다른 directory 안에 들어가 있는 경우 .travis.yml
은 repository의 root경로에 있어야 한다.
language: java
jdk:
- openjdk11
branches:
only:
- master
# Travis CI 서버의 Home
cache:
directories:
- '$HOME/.m2/repository'
- '$HOME/.gradle'
before_install:
- chmod +x gradlew
script: "./gradlew clean build"
# CI 실행 완료시 메일로 알람
notifications:
email:
recipients:
- xxx@gmail.com #빌드 결과를 받아 볼 이메일 주소
travis.yml
파일은 위와 같이 작성했다.
이 파일을 작성한 뒤 master 브랜치에 commit & push 하고 travis Ci 저장소 웹페이지를 확인하면
다음과 같이 build가 잘되어있는 것을 확인할 수 있다! 위에서 작성한 이메일도 들어가보면 빌드가 성공적으로 이루어졌다는 메일이 와있을 것이다.
참고로 travis.yml 파일을 작성하고 push까지 했는데 travis ci 서비스에서 아예 인식조차 못하는 경우가 있다. 예전에는 travis가 완전한 무료 서비스여서 따로 설정이 필요가 없었지만 지금은 유료로 전환되었기 때문에 꼭 내 Account 의 Setting에 들어가서 유료 plan을 선택해주어야 한다. 지금은 30일 무료 체험 플랜이 있으니 그걸 활용해보면 좋다.
이 과정은 테스트와 빌드만 자동화가 된 것이지 배포까지 자동화된 것은 아니다.
배포 자동화는 AWS의 Code Deploy와 연동해서 해주어야 한다.
Travis CI는 CI툴일 뿐이라 Test와 Build까지 자동화가 되어도 배포가 자동화되진 않는다.
다시 말해, Travis CI가 자동으로 Build 해준 파일이 서버까지 따로 전달되어야 한다는 소리이다.
이 과정을 AWS CodeDeploy가 해준다.
따라서 이번 졸업프로젝트 전체적인 배포의 흐름은
1. IntelliJ에서 프로젝트를 개발하고 github에 새로 개발된 내용을 push 한다.
2. Travis CI에서 github에 새롭게 push된 내용을 인식하여 해당 내용을 test, build하고 메일로 이를 개발자에게 알린다.
3. build 결과를 AWS의 S3에게 전달하고 S3는 AWS CodeDeploy에 해당 내용을 전달한다.
4. AWS CodeDeploy에서 자동으로 전달받은 build 결과를 가지고 ec2를 배포한다.
5. EC2 인스턴스 내부에서 SpringBoot 프로젝트 (서버)가 실행되며 외부 ip에서 user가 해당 서버에 접근할 수 있다.
CodeDeploy에는 저장 기능이 없어서 Travis CI가 build한 결과물을 받아서 CodeDeploy가 가져갈 수 있도록 저장하여 보관하는 공간이 AWS S3이다.
따라서 .travis.yml
파일 하단에 아래와 같은 코드를 추가해서 travis CI가 s3의 버킷에 자신의 build 결과물을 저장할 수 있도록 설정했다.
deploy:
- provider: s3
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
bucket: newsum-deploy-s3-bucket # S3 버킷
region: ap-northeast-2
skip_cleanup: true
acl: public_read
wait-until-deployed: true
on:
repo: NLSushi/server #Github 주소
branch: master
여기서 설정 Accesskey와 Secretkey는 github에 노출되면 안되므로 따로 변수로 설정하고 Travis CI에서 환경변수를 지정해주어야 한다.
다음과 같이 각 AccessKey와 Secretkey의 값을 Travis CI 에서 환경변수 처리 해주었다.
이제 수정된 travis.yml
파일을 push 하고 Travis CI의 빌드 로그를 다시 보려는데,,,
uploading "src/main/java/ewha/nlsushi/newsum/api/ArticleApiController.java" with {:content_type=>"text/x-java-source", :acl=>"public-read"}
/home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/seahorse/client/plugins/raise_response_errors.rb:15:in `call': The bucket does not allow ACLs (Aws::S3::Errors::AccessControlListNotSupported)
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/s3_sse_cpk.rb:19:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/s3_dualstack.rb:24:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/s3_accelerate.rb:34:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/jsonvalue_converter.rb:20:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/idempotency_token.rb:18:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/aws-sdk-core/plugins/param_converter.rb:20:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/seahorse/client/plugins/response_target.rb:21:in `call'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/seahorse/client/request.rb:70:in `send_request'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-core-2.11.632/lib/seahorse/client/base.rb:207:in `block (2 levels) in define_operation_methods'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-resources-2.11.632/lib/aws-sdk-resources/services/s3/file_uploader.rb:42:in `block in put_object'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-resources-2.11.632/lib/aws-sdk-resources/services/s3/file_uploader.rb:49:in `open_file'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-resources-2.11.632/lib/aws-sdk-resources/services/s3/file_uploader.rb:41:in `put_object'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-resources-2.11.632/lib/aws-sdk-resources/services/s3/file_uploader.rb:34:in `upload'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/aws-sdk-resources-2.11.632/lib/aws-sdk-resources/services/s3/object.rb:252:in `upload_file'
from /home/travis/.rvm/gems/ruby-2.4.5/gems/dpl-s3-1.10.16/lib/dpl/provider/s3.rb:114:in `block (2 levels) in upload_multithreaded'
failed to deploy
로그에 다음과 같은 오류가 발생했다.
이 로그를 읽어보니 버킷에 ACL 권한을 안줘서 그런것 같았다.
해당 S3 버킷의 퍼블릭 액세스 차단을 일단 모두 비활성하고
인증된 사용자 그룹의 나열과 읽기 권한을 주면 버킷이 ACL 권한을 갖게 된다!!
버킷 설정을 다시하고 다시 Travis CI에서 빌드하게 하니까 다음과 같이 S3 버킷에 프로젝트 빌드 결과가 담기게 되었다! 야호 🤸♀️
그런데 매번 Travis Ci에서 파일을 하나하나 복사해서 저장하는 것은 시간이 많이 걸리기 때문에 프로젝트 폴더 채로 압축해서 S3로 전달하도록 설정을 조금 추가하는 것이 좋다.
before_deploy:
- zip -r newsum * #현재 위치의 모든 파일을 newsum 이름으로 압축
- mkdir -p deploy #deploy 디렉토리를 Travis CI가 실행중인 위치에 생성
- mv newsum.zip deploy/newsum.zip #deploy 폴더로 이동
deploy:
- provider: s3
...
local_dir: deploy # before_deploy에서 생성한 디렉토리
...
그를 위해 위와 같이 travis.yml
파일을 수정해주었다.
나의 최종 travis.yml 파일은 깃허브에서 확인할 수 있다!
다시 push 하면 이제 S3에서 zip 파일이 생긴 것을 확인할 수 있다. zip 파일 외에는 이제 필요 없어서 전부 지워도 괜찮다
이제 Travis CI와 C3는 연동이 되었으니 CodeDeploy까지 같이 연동하면 된다!
어플리케이션 생성 후 EC2로 접속 (Putty활용)해서 S3에서 zip파일을 받아올 디렉토리를 생성한다.
.travis.yml
과 같은 위치에 appspec.yml
파일을 생성하고 설정한다.
이 파일은 AWS 의 Codedeploy를 설정해주는 파일이다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/travis/build/
appspec.yml
파일을 다음과 같이 설정해주고
travis.yml
파일을 아래와 같이 수정한다.
language: java
jdk:
- openjdk11
branches:
only:
- master
# Travis CI 서버의 Home
cache:
directories:
- '$HOME/.m2/repository'
- '$HOME/.gradle'
before_install:
- chmod +x gradlew
script: "./gradlew clean build"
# CI 실행 완료시 메일로 알람
notifications:
email:
recipients:
- porori0703@ewhain.net
before_deploy:
- zip -r newsum * #현재 위치의 모든 파일을 newsum 이름으로 압축
- mkdir -p deploy #deploy 디렉토리를 Travis CI가 실행중인 위치에 생성
- mv newsum.zip deploy/newsum.zip #deploy 폴더로 이동
deploy:
- provider: s3
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
bucket: newsum-deploy-s3-bucket # S3 버킷
region: ap-northeast-2
skip_cleanup: true
acl: public_read
local_dir: deploy # before_deploy에서 생성한 디렉토리
wait-until-deployed: true
on:
repo: NLSushi/server #Github 주소
branch: master
# 📌수정(추가)된 부분
- provider: codedeploy
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
bucket: newsum-deploy-s3-bucket # S3 버킷
key: newsum.zip # 빌드 파일을 압축해서 전달
bundle_type: zip
application: newsum_codedeploy # 웹 콘솔에서 등록한 CodeDeploy 어플리케이션
deployment_group: newsum-codedeploy-group # 웹 콘솔에서 등록한 CodeDeploy 배포 그룹
region: ap-northeast-2
wait-until-deployed: true
on:
repo: NLSushi/server
branch: master
다시 push하고 Travis CI에서도 build가 pass되면..!
AWS codedeploy 어플리케이션에도 다음과 같이 성공 상태를 볼 수 있다!
이로써 배포가 성공된 것이다.
이제 EC2에 접속해서 zip 파일을 담아두었던 /home/ec2-user/app/travis/build
경로로 가서 파일을 확인해보면
작성한 코드가 git push 만으로 EC2까지 전송된 것을 볼 수 있다!
이제 코드가 전송되었으니 jar 파일을 실행시키기만 하면 된다.
이를 위해서는 EC2에 AWS CodeDeploy로 받은 파일을 실행시키는 배포스크립트를 생성해야 한다.
먼저 jar 파일을 모아둘 디렉토리를 하나 생성하고 jar 디렉토리에 옮겨진 application.jar를 실행시킬 deploy.sh
파일을 하나 생성한다.
vim /home/ec2-user/app/travis/deploy.sh
#!/bin/bash
REPOSITORY=/home/ec2-user/app/travis
echo "> 현재 구동중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -f newsum) //📌jar파일에 들어가는 프로젝트명
echo "$CURRENT_PID"
if [ -z $CURRENT_PID ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> 새 어플리케이션 배포"
echo "> Build 파일 복사"
cp $REPOSITORY/build/build/libs/*.jar $REPOSITORY/jar/
JAR_NAME=$(ls $REPOSITORY/jar/ |grep 'newsum' | tail -n 1) //📌jar파일에 들어가는 프로젝트명
echo "> JAR Name: $JAR_NAME"
nohup java -jar $REPOSITORY/jar/$JAR_NAME &
이 스크립트를 실행시키고 생성된 nohup.out
파일을 열람하여 스크립트가 잘 실행되어 서버가 돌아가는지 로그를 확인한다.
이제 AWS CodeDeploy 배포가 끝나면 deploy.sh를 실행하도록 설정을 변경해주어야 한다.
appspec.yml
에 아래 코드를 추가한다.
hooks:
AfterInstall: # 배포가 끝나면 아래 명령어를 실행
- location: execute-deploy.sh
timeout: 180
CodeDeploy에서 바로 deploy.sh를 실행시킬 수가 없어서 execute-deploy.sh
파일을 실행하는 방식으로 우회해서 설정해주어야 한다.
기타 yml파일들과 마찬가지로 같은 위치에 execute-deploy.sh
를 프로젝트 내부에 생성해서 CodeDeploy가 실행할 수 있도록 한다.
execute-deploy.sh
는 다음과 같이 작성한다.
#!/bin/bash
/home/ec2-user/app/travis/deploy.sh > /dev/null 2> /dev/null < /dev/null &
해당 내용을 push 해주면 이제 모든 설정이 끝났다.
앞으로는 repository의 master branch에 commit+push+ merge 될때마다 자동으로 새 버전이 배포된다~!!!! 🎇💃🎇💃