[캡스톤디자인및창업프로젝트]의 진행 과정에서, Github Actions를 도입함으로써 서버 배포를 자동화하였다. 또한, "일기 훔쳐보기" 기능의 구현 과정에서 DALL-E API를 호출하였는데, 관련 내용에 대해 튜토리얼 형식으로 기술하고자 한다.
기존에는 프로젝트를 수동으로 배포하는 방식을 취했다.
수동 배포의 절차는 다음과 같았다:
1. SSH를 통해 AWS EC2 (서버) 인스턴스에 접속
2. (배포를 위한 파일인) sh 스크립트 파일이 있는 폴더로 이동: cd /home/ec2-user/해당경로
3. sh 스크립트 파일 실행: sh deploy.sh
deploy.sh
REPOSITORY=/home/ec2-user/레포지토리_경로
PROJECT_NAME=프로젝트_이름
cd $REPOSITORY/$PROJECT_NAME/
echo "> Git Pull"
git pull
echo "> 프로젝트 Build 시작"
./gradlew build
echo "> 디렉토리로 이동"
cd $REPOSITORY
echo "> Build 파일 복사"
cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/
echo "> 현재 구동중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -fl wow_server.*.jar | grep java | awk '{print $1}')
echo "> 현재 구동중인 애플리케이션pid: $CURRENT_PID"
if [ -z "$CURRENT_PID" ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | head -n 1)
echo "> JAR Name: $JAR_NAME"
nohup java -jar $JAR_NAME &
➡️ 기존 수동 배포 방식으로는 Github Repository의 main 브랜치에 새로운 커밋이 push 될때마다, 이를 서버에도 반영하기 위해 수동으로 AWS EC2 인스턴스에 SSH를 통해 접속하고, sh 스크립트 파일이 있는 경로로 이동해 해당 파일을 실행하는 번거로운 과정을 거쳤어야했다.
Q: CI/CD란?
A: "CI/CD (Continuous Integration/Continuous Delivery)는 애플리케이션 개발 단계를 자동화하여 애플리케이션을 더욱 짧은 주기로 고객에게 제공하는 방법입니다. CI/CD의 기본 개념은 지속적인 통합, 지속적인 서비스 제공, 지속적인 배포입니다." (출처: RedHat)
Github Actions는 Github에서 직접 제공하는 CI/CD 도구이다. Github 레포지토리(repository)를 기반으로 SW 개발 워크플로우(workflow)를 자동화할 수 있다.
CI/CD를 구성하는 대표적인 툴로 Jenkins가 있는데, Github Actions의 경우 작업하고 있는 Github 레포지토리(Repository)에서 바로 CI/CD를 구성할 수 있기에 상대적으로 CI/CD를 구성하기 더 간단하다는 결론에 이르렀다. 따라서, Github Actions를 선택하여 배포 자동화를 구현하게 되었다.
Github Actions를 사용하기 위해서는, 사용중인 Personal Access Token의 scope에 'workflow'를 포함시켜야한다.
나 같은 경우, Personal access token의 scope에 'workflow'가 선택이 되어있지 않았기에, Github Actions workflows 관련 파일 수정시 아래와 같은 에러가 떴었다:
refusing to allow to a Personal Access Token to create or update workflow `.github/workflows/file.yml` without `workflow` scope
Settings > Developer Settings > Personal access tokens에 들어가서, 사용중인 Personal access token을 누르고, [Select scopes] 메뉴에서 'workflow'가 잘 선택되었는지 확인하고 아직 선택되지 않았다면 체크 표시를 눌러 선택해줘야한다!
- HOST = EC2 인스턴스의 public IP
- USER = 사용자 (보통은 ec2-user)
- SSH_PRIVATE_KEY =
.pem
키 파일 내용
📌 SSH_PRIVATE_KEY 등록시 주의사항 📌
: EC2 서버 접속을 위한 .pem
키 파일을 cat
명령어를 통해 조회하면, 키 파일 내 전체 내용이 나오는데 이를 그대로 복사-붙여넣기하면 된다.
주의할 점은 아래 내용도 포함이 되어야한다는 점이다:
----BEGIN RSA PRIVATE KEY-----
...(키 내용)
...(키 내용)
----END RSA PRIVATE KEY-----
첫번째 line과 마지막 line이 key값이 아니어서 제외하였더니 Github Actions를 통한 SSH 접속이 제대로 이루어지지 않았다! Repository secrets 등록시 이 점을 유의하자. (∗❛⌄❛∗)
Github Actions의 workflow는 Repository 내의
.github/workflows
폴더 아래에 위치한.yml
파일로 설정한다. 하나의 Repository 내에는 여러 개의 workflow, 즉 여러 개의.yml
파일을 생성할 수 있다.
name: WORKFLOW_NAME # workflow 이름 설정
# workflow를 run하는 조건 설정
on:
push:
branches: [ main ] #설정한 workflow run 조건: branch [main]에 push를 할때마다
# workflow가 run할때 실행하는 내용 정의
jobs:
SSH:
runs-on: ubuntu-latest # OS(workflow label)
steps:
- uses: actions/checkout@v3 #Repository 참고
- name: ssh to ec2 #EC2에 접속
uses: appleboy/ssh-action@master #접속 제공 Repository
with:
key: ${{ secrets.SSH_PRIVATE_KEY }} #Repository secrets 사용
host: ${{ secrets.HOST }} #Repository secrets 사용
username: ${{ secrets.USER }} #Repository secrets 사용
script: | #실행할 코드
cd /home/ec2-user/해당경로 #배포 스크립트 파일이 있는 폴더로 이동
sh deploy.sh #배포 스크립트 파일 실행
#기존 deploy.sh 파일에서 nohup 명령어를 포함한 line을 지우고, 아래와 같이 `.yml` 스크립트 파일에 추가
nohup java -jar wow_server-0.0.1-SNAPSHOT.jar > nohup.out 2> nohup.err < /dev/null &
runs-on
같은 경우에 깃허브 Actions 공식 문서에서 Virtual Machine에 따른 .yml
파일의 OS(workflow label)에 대해 안내하고 있으니 이 점을 참고하여 작성하면 된다. 필자의 경우, EC2 서버가 Linux 환경의 VM에 해당되어 ubuntu-latest
레이블을 사용하였다. (아래 이미지 참고)steps
에 언급된 appleboy/ssh-action@master
의 경우, Github Actions를 통한 SSH 접속을 제공해주는 Repository이다.main.yml
이 의미하는 바를 정리하자면, Github Actions를 이용하여 [main] branch에 새로운 push 이벤트가 발생할 때마다, SSH로 EC2에 접속하여 script:
를 실행하라는 것이다. (기존) 프로젝트 수동 배포 방식
1. SSH를 통해 AWS EC2 (서버) 인스턴스에 접속
2. (배포를 위한 파일인) sh 스크립트 파일이 있는 폴더로 이동:cd /home/ec2-user/해당경로
3. sh 스크립트 파일 실행:sh deploy.sh
➡️ 즉, main.yml
에는 기존 수동 배포 방식에서 Github Actions를 통해 workflow를 run시키면서 자동화한 내용이 담겨있다는 것을 알 수 있다.
기존의 deploy.sh
스크립트 파일에는 아래와 같은 nohup 명령어가 포함되어있었다.
deploy.sh
nohup java -jar $JAR_NAME &
➡️main.yml
의 script:
부분에 nohup 명령어를 추가하였기에, 기존 deploy.sh
파일에서 nohup 명령어를 포함한 line을 지워야한다!
nohup 명령어를
deploy.sh
에서main.yml
로 옮기게 된 계기
:main.yml
의script:
부분에 nohup 명령어를 따로 두지 않고, 기존처럼deploy.sh
에서 nohup 명령어를 실행했을때 workflow를 run 시킬때 에러가 발생하여 서버 배포가 제대로 이루어지지 않는 문제가 발생하였다!
원인 분석 결과 nohup 명령어 실행 과정에서 생긴 에러임을 발견하였다. 이에 따라 기존 배포 스크립트 파일deploy.sh
에서는 nohup 명령어를 포함한 line을 삭제하였고,main.yml
에서 실행할script:
에 해당 명령어를 포함한 line을 추가하였다.
이어서, 프로젝트의 핵심 기능인 "일기 훔쳐보기" 기능을 구현하기 위해 Springboot를 활용하여 DALL-E api를 호출한 내용에 대해서도 튜토리얼을 기술하고자한다.
프로젝트를 진행하는 과정에서, 사용자가 원하는 이미지를 생성하여 출력하는 기능을 구현하기 위해 OpenAI사의 DALL-E API를 유료 결제를 통해 사용하게 되었다.
(DALL-E: OpenAI에서 개발한 생성형 이미지 생성형 인공지능)
Springboot로 DALL-E API를 호출하는 레퍼런스가 적어 어려움이 있었는데, 이 튜토리얼을 따라한다면 손쉽게 DALL-E API 호출 구현이 가능하다!
: 이 부분이 가장 중요한 부분이라고 볼 수 있다! ( ・ω・)ノ
build.gradle의 dependencies 부분에 아래 라인들을 추가해주었다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.theokanning.openai-gpt3-java:client:0.17.0'
implementation 'com.theokanning.openai-gpt3-java:service:0.17.0'
implementation 'com.stripe:stripe-java:22.10.0'
implementation 'javax.xml.bind:jaxb-api:2.3.0'
implementation 'sh.platform:config:2.2.2'
cf)
OpenAI 공식 홈페이지에는 python으로 API를 호출하는 내용만 나와있기에, https://github.com/TheoKanning/openai-java 이 레포지토리에서 구현된 java를 이용한 OpenAI 호출 부분을 일부 차용하여 현재 진행하는 프로젝트에서 DALL-E API를 구현하였다. build.gradle 중 아래 부분의 경우, 앞서 언급된 링크의 레포지토리의 버전이 달라질때마다 주기적으로 버전을 바꾸어 명시해줘야한다. 현재 프로젝트에서 사용중인 레포지토리의 버전은 "0.17.0"으로, DALL-E 3 모델을 사용가능하게끔 업데이트된 버전이다.
implementation 'com.theokanning.openai-gpt3-java:client:0.17.0'
implementation 'com.theokanning.openai-gpt3-java:service:0.17.0'
api key 발급 절차:
(1) https://openai.com/에 접속하여 로그인
(2) https://platform.openai.com/api-keys에 접속 (이는 좌측에 있는 menu란에서 '자물쇠 모양 아이콘'을 클릭한 것과 같은 효과)
(3) "Create new secret key" 버튼을 클릭함으로써, 새로운 key 생성!
: 발급받은 api key값을 application.properties에 추가해주었다.
openai.key={값}
DALL-E의 API 문서를 읽어보면, API가 반환하는 이미지의 형태(ResponseFormat)을 총 2가지, URL 또는 Base64 data로 설정할 수 있다는 것을 확인할 수 있다. Default로는 URL이 반환된다. (주의: URL은 1시간 후 만료된다.)
참고: https://platform.openai.com/docs/guides/images/usage
따라서, 코드를 아래와 같이 작성하였다:
import com.theokanning.openai.image.CreateImageRequest;
import com.theokanning.openai.service.OpenAiService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AIService {
@Resource(name = "getOpenAiService")
private final OpenAiService openAiService;
public String generatePicture(String prompt) {
CreateImageRequest createImageRequest = CreateImageRequest.builder()
.prompt(prompt)
.size("1024x1024")
.model("dall-e-3")
.n(1)
.build();
String url = openAiService.createImage(createImageRequest).getData().get(0).getUrl();
return url;
}
}
DALL-E 2의 경우, 512x512도 지원하지만
이번에 새롭게 출시된 DALL-E 3는 1024x1024의 이미지부터 지원한다. 따라서, size에 최소 "1024x1024"이상을 넘겨줘야한다.
또한, 모델이 새로 추가됨에 따라, API 호출시 사용할 모델이 무엇인지 위와 같이 명시해줘야한다. 위의 경우, DALL-E 3 모델을 사용하겠다는 뜻이다.
맨 처음 application.properties에 정의한 openai.key를 Config 파일에도 선언해주었다!
import com.theokanning.openai.service.OpenAiService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ServicesConfig {
@Value("${openai.key}")
private String apiKey;
@Bean
public OpenAiService getOpenAiService() {
return new OpenAiService(apiKey, Duration.ofSeconds(30));
}
}
: 요청 주소는 "/image"로 하여, 아래와 같은 Controller 코드를 작성하였다.
@PostMapping("/image")
public ResponseEntity<?> generateImage(@RequestBody String prompt) {
return new ResponseEntity<>(aiService.generatePicture(prompt), HttpStatus.OK);
}
(위의 이미지 참고) DALL-E API 문서를 확인하면 "Required"되는 항목인 String 형태의 prompt 변수가 있는 것을 알 수 있다. 이를 RequestBody로 담고, 아까 AIService.java에서 작성한 generatePicture() 함수를 호출하여 url인 String 값을 반환하는 API를 위한 Controller 코드를 위와 같이 작성하였다.
결론적으로 이번 포스팅에서 구현한 DALL-E API는
"/image"라는 path로 아래의 RequestBody를 담아 POST 호출하면,
{
”prompt” :”String”
}
ResponseBody로
JSON 객체가 아닌, url의 String 값을 반환한다.
:만약 API 호출시 반환값을 url이 아닌 Base64 Data로 바꾸고 싶으시다면! 아래 코드를 참고하면 된다.ʕo•ᴥ•ʔ✎
public String generatePicture(String prompt) {
CreateImageRequest createImageRequest = CreateImageRequest.builder()
.prompt(prompt)
.size("1024x1024")
.model("dall-e-3")
.n(1)
.responseFormat("b64_json") //b64_json 포맷으로의 반환을 위해 이 코드 추가!
.build();
//이미지 URL 대신 base64 data를 리턴하는 것으로 변경!
String b64 = openAiService.createImage(createImageRequest).getData().get(0).getB64Json();
return b64;
}
차이점은
1. RequestBody에 responseFormat을 b64_json으로 설정해줘야한다.
2. 따라서, .getData().get(0).getUrl(); 이 아닌, .getData().get(0).getB64Json();의 형태로 값을 가져와야한다.
이렇게 DALL-E 3 API를 호출함으로써, 진행중인 프로젝트의 핵심 기능인 "일기 훔쳐보기" 기능을 구현할 수 있었다! :)