🚨주의🚨 본 글은 미숙한 개발자가 겪은 시행착오가 많이 들어가 있으므로, 해결방안이 필요하신 분은 목차 부분을, 스크립트가 필요하신 분은 맨 아래를 확인해주시면 감사하겠습니다.
제 배포방식은 이렇게 진화했습니다.
전혀 관계없는 어떤 것들이 비슷한 환경에 맞게 진화했는데, 진화한 모습이 같은 것을 '수렴 진화'라고 합니다.
아마 서버 개발자 분들도 이런 귀찮음을 없애기 위해 위와같은 진화과정을 거쳐 젠킨스나 깃헙액션같은 툴이 탄생하지 않았나 싶습니다.
눈물나는 일대기...
처음 깃허브 액션을 활성화 하시고 액션에 가시면 New workflow가 있습니다.
저는 Java with Gradle을 선택했습니다.
이제 여기서 원하는 스크립트를 작성하시면 됩니다.
간단한 Workflow 입니다.
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
ApplicationTests > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:98
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1804
Caused by: org.hibernate.service.spi.ServiceException at AbstractServiceRegistryImpl.java:284
Caused by: org.hibernate.HibernateException at DialectFactoryImpl.java:100
1 test completed, 1 failed
분명 로컬에선 잘 작동하는 테스트가 갑자기 동작하지 않습니다.
여기서 Workflow는 Github Action Runner가 설치된 도커 인스턴스(러너)에서 실행됩니다.
오류 메세지를 보시면 hibernate란 단어가 보이므로, 100% DB에서 터진 문제입니다.
저와 같은 문제를 해결하신 분의 게시물을 확인해보니, 아니나 다를까 러너에 MySQL이 세팅되지 않아서 발생하는 문제였습니다. 근데 여기서 근본적인 의문점이 있었습니다.
내 프로젝트는 AWS RDS와 연결해서 진행되는데, 굳이 러너에 MySQL을 설치할 필요가 있나...?
아니나 다를까, 오류는 해결되지 않았습니다.
따라서 임시 방편으로 테스트를 끄기로 했습니다.
- name: Build with Gradle
# uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
# with:
# arguments: build
run : ./gradlew clean build -x test
현재 글을 쓰면서 생각나는 문제는 RDS의 인바운드 룰에 러너의 IP가 허용이 안된 것 입니다. 사실 문제는 알았지만 해결방안을 몰라 진행할 수 없었는데, 아래서 임시로 인바운드룰을 설정할 수 있어서 추후 해결하도록 하겠습니다.
이제 빌드된 파일을 전송해주면 되지만, 하나 문제가 있습니다.
Run scp *.jar @:~/cicd
ssh: connect to host *** port 22: Connection timed out
lost connection
Error: Process completed with exit code 1.
어디서 많이 봤던 에러코드죠? 22번 포트가 닫혀있다... 즉 서버에서 인바운드 룰에 ssh로 ip추가를 하지 않았다는 의미입니다. 여기서 전 관문 1에서부터 품고있던, 어쩌면 이번 프로젝트 내내 생각했던 문제를 떠올립니다.
명령어나 코드로 특정 IP에 대해 임시로 포트를 허용하고, 닫을 수 있나?
💡 러너는 고정된 IP가 아닙니다. 왜냐하면 진행될 때 마다 다른 도커 컨테이너를 사용하니까요.
찾아보니까 예전에 S3 업로더를 사용할 때 사용했던 IAM을 사용해서 진행할 수 있더라구요.
- name: Get Github action IP # 액션 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=launch-wizard-2" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }} # IAM 엑세스키
aws-secret-access-key: ${{ secrets.AWS_IAM_SECRET_KEY }} ## IAM 시크릿 키
aws-region: ap-northeast-2
- 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
- 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_IAM_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
AWS_DEFAULT_REGION: ap-northeast-2
💡 여기서 secrets.~~~는 깃허브 시크릿으로 등록한 값 입니다.
이렇게 설정하시면 되시는 분도 있겠지만 저는 안됐는데요, 그 이유는 IAM에서 권한설정을 해주지 않았기 때문입니다.
전 다음과 같은 오류가 발생했습니다.
An error occurred (UnauthorizedOperation) when calling the AuthorizeSecurityGroupIngress operation: You are not authorized to perform this operation.
해결방안을 찾았는데요, 저 권한을 인라인 정책으로 설정해도 똑같은 오류가 발생해서 그냥 EC2 모든 권한을 정책으로 등록했더니 해결됐습니다.
이제 빌드를 완료했고, 아티팩트를 다운받고 배포만 해주면 됩니다.
## 빌드했던거 받기
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: cicdsample
# SCP로 서버로 전송
- name: SCP transfer
uses: appleboy/scp-action@master
with:
username: ${{ secrets.SSH_USER }}
host: ${{ secrets.SSH_ADDR }}
key: ${{ secrets.SSH_KEY }}
rm: true
source: ${{ secrets.SOURCE_PATH }}
target: ${{ secrets.DIST_PATH }}
## 작업에 사용했던 dist 디렉토리를 경로상에서 제거
strip_components: 1
💡 깃허브 액션의 장점으로 사람들이 미리 만들어놓은 액션들을 사용할 수 있습니다.
이전에 다른 SCP 액션을 사용하면 오류가 발생해 appleboy라는 분이 만드신 액션을 사용하도록 변경했습니다.
자세한 옵션 설명
여기서 몇몇 시크릿에 대해 설명드리겠습니다.
SSH_USER
: 터미널로 ec2 접근하실 때 사용하는 id 입니다.
SSH_ADDR
: 터미널로 ec2 접근하실 때 사용하는 주소 입니다. IP도 되고 DNS도 되는 것 같습니다. (현재 DNS 사용중)
SSH_KEY
: ec2를 기준으로 받으셨던 .pem키 파일을 메모장이나 Vim으로 열어서 안에 있는 값 입니다.
SOURCE_PATH
: 다운받은 아티팩트 경로입니다. 저는 /home/runner/work/프로젝트/프로젝트/.jar 이런식으로 작성했습니다. 액션 결과를 확인해보시면 됩니다!!
DIST_PATH
: 서버에 저장될 경로입니다.
마지막으로 서버에 들어가게 해서 제가 만든 스크립트를 실행시켜주고 깃허브 러너 ip를 인바운드 룰에서 제거해주면 끝 입니다.
# 내가 만든 서버 스크립트 실행
- name: Execute Server Init Script
uses: appleboy/ssh-action@master
with:
username: ${{ secrets.SSH_USER }}
host: ${{ secrets.SSH_ADDR }}
key: ${{ secrets.SSH_KEY }}
script_stop: true
script: |
sh action_deploy.sh
# 깃허브 러너 아이피를 인바운드 룰에서 제거
- 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_IAM_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
AWS_DEFAULT_REGION: ap-northeast-2
제가 만든 스크립트는 서버에 들어갔을 때 경로에 만들었고, 다음과 같습니다.
#!/bin/bash
echo “ACTION DEPLOY START”
# 현재 프로세스 가져오기
CURRENT_PID=$(pgrep -f 프로세스 이름)
# 있으면 종료
echo “$CURRENT_PID”
if [ -z $CURRENT_PID ]; then
echo “no such app didn’t find.”
else
kill -9 $CURRENT_PID
echo “killed current pid”
sleep 3
fi
echo “deploy started”
cd /home/ubuntu/cicd/workspace
JAR_NAME=$(ls | grep '자르 이름' | tail -n 1)
echo "> JAR Name: $JAR_NAME"
# 일반 자르로 하면 동작이 안돼서 출력과 오류를 따로 폴더를 만들어 텍스트 파일로 저장했습니다.
nohup java -jar -Duser.timezone=Asia/Seoul $JAR_NAME 1>nohup/stdout.txt 2>nohup/stderr.txt &
sleep 2
여기까지가 완성입니다.
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
# 워크 플로 이름
name: Java CI/CD with Gradle
# 메인에 푸쉬나 피알되면 이벤트 발생
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
##
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
# uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
# with:
# arguments: build
run : ./gradlew clean build -x test
# 아티팩트 업로드
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: cicdsample
path: build/libs/*.jar
deploy:
needs: build
runs-on: ubuntu-latest
steps:
## 빌드했던거 받기
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: cicdsample
# - name: Setup SSH
# uses: webfactory/ssh-agent@v0.5.4
# with:
# ssh-private-key: ${{ secrets.SSH_KEY }}
# 깃허브 액션 러너의 아이피를 얻어온다.
- 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=launch-wizard-2" >> $GITHUB_ENV
# AWS 설정
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
# 아이엠 키 설정
aws-access-key-id: ${{ secrets.AWS_IAM_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_IAM_SECRET_KEY }}
aws-region: ap-northeast-2
# 깃허브 액션의 아이피를 인바운드 룰에 임시 등록
- 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
# SCP로 서버로 전송
- name: SCP transfer
uses: appleboy/scp-action@master
with:
username: ${{ secrets.SSH_USER }}
host: ${{ secrets.SSH_ADDR }}
key: ${{ secrets.SSH_KEY }}
rm: true
source: ${{ secrets.SOURCE_PATH }}
target: ${{ secrets.DIST_PATH }}
## 작업에 사용했던 dist 디렉토리를 경로상에서 제거
strip_components: 1
# - name: Execute remote commands
# run: |
# ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_ADDR }} "sudo fuser -k 8080/tcp"
# ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_ADDR }} "
# 내가 만든 서버 스크립트 실행
- name: Execute Server Init Script
uses: appleboy/ssh-action@master
with:
username: ${{ secrets.SSH_USER }}
host: ${{ secrets.SSH_ADDR }}
key: ${{ secrets.SSH_KEY }}
script_stop: true
script: |
sh action_deploy.sh
# 깃허브 러너 아이피를 인바운드 룰에서
- 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_IAM_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_IAM_SECRET_KEY }}
AWS_DEFAULT_REGION: ap-northeast-2
계속 미루고 미루던 CI/CD를 만들어서 기쁩니다. 졸업작품 때 팀원이 젠킨스를 쓰면서 정말 고생했던 기억이 있는데, 깃허브 액션은 그에 비해 쉬운 것 같아서 나름 재밌었습니다 ㅎㅎ.
코드 저장소에서 직접 제공하는 CI/CD 툴이니 직관적이고 이해, 비교가 쉽습니다. 여러분들도 꼭 써보셨으면 좋겠습니다.
다음에는 실제 배포에서 사용되는 blue/green 무제한 배포를 구현해 실제 배포환경을 구성해보려 합니다.
긴 글 읽어주셔서 감사드리고 좋은 액션 제공해주신 appleboy님 감사합니다.
https://zzsza.github.io/development/2020/06/06/github-action/
https://hoestory.tistory.com/35
https://makethree.tistory.com/19
https://stackoverflow.com/questions/62275740/what-role-allows-an-ec2-user-to-run-aws-ec2-authorize-security-group-egress
https://github.com/appleboy/ssh-action
https://github.com/appleboy/scp-action
https://docs.github.com/ko/actions
안녕하세요
궁금하게 있어 질문 남깁니다!
${{ steps.ip.outputs.ipv4 }} 요건 어디서 주입되는걸까요?