1. 학습 계기
기존에 개발해 둔 팀프로젝트들을 배포해서 포트폴리오로 사용하고자 하였다. 직접 AWS server에 올려보니 local환경이 아니기 때문에 발생하는 이슈들이 있었으며, 팀프로젝트 배포당시 발견되지 못했던 이슈들이 있어 수정작업을 하다 보니 매번 수정 후 server에 올려 확인하는 작업이 매우 불편하게 느껴졌다.(기존에는 git repo를 server에 직접 pull 받아 이미지를 build 하는 방법을 사용하다가 용량이 적은 프리티어 서버에서 직접 build를 하는 것에 부담을 느껴 -> build한 파일을 별도의 github repo에 업로드해서 서버에서 pull 받아 사용하던 방법을 사용하고 있었다. 프록시의 경우에는 처음에는 포트포워딩을 설정해 두었다가, nginx를 통해 리버스프록시를 적용하는 방법으로 변경하였다.)
위의 상황을 개선하기 위해 docker를 통해 image를 빌드하고 docker hub에 push 한 뒤 server에서 해당 이미지를 받아와 컨테이너를 생성하는 방법으로 변경하였으나, 여전히 server에 직접 접속해서 이미지를 바꿔줘야한다는 한계가 있었다(ec2 자체에 ssh접속 또한 인바운드 규칙을 설정해 특정 ip에서만 접속 가능하도록 설정하였더니, 작업 장소가 변경되면 매번 aws 콘솔에 접속해 ssh 접속 ip를 추가해주어야 했기 때문에 여간 불편한 게 아니었다.).
이에, 로컬에서 build를 하면 자동으로 server에 반영될수있으면 좋겠다는 판단을 했고, 가장 적은 비용으로 "개인 프로젝트 배포"라는 관점에 맞추어 CD를 구축해보고자 했기에, docker hub의 web hook 기능을 활용해보기러 마음먹었다. 물론 jenkins를 통한 CD가 최근 많이 사용하는 방법이라고 했지만, docker에 대해서 공부 중인 상황에서 docker, docker compose, builder등에 대해 조금은 깊게 공부할 수 있는 계기가 될 것 같아 docker를 활용해 배포한 후 추후에 개선점을 찾아 변경하는 방향으로 공부해보고자 한다.
2. 야매 CD 도안
- 우선 spring 어플리케이션을 build 해서 server에 올릴 image를 위한 jar/war파일을 생성한다.
- 해당 jar 파일을 이용해 multiplaform docker image를 생성한다. docker에서 multiplaform image를 build 하는 경우 local에 image가 생성되는 것이 아닌, 지정한 docker hub repo에 각 os에 맞는 image들이 push 된다.
(해당 image를 build 한 local에서도 해당 이미지를 사용하고 싶다면, pull을 받아와야 사용이 가능하다.) - docker hub의 특정 repo에 image가 push 되면, AWS ec2인스턴스의 특정 port(nnnn)로 webhook 알림을 보내도록 설정한다.
- ec2 인스턴스의 특정포트에 들어온 알림에, push 된 image의 tag가 "prod"인 경우 기존 동작중이던 docker-compose를 down 시키고, 새로운 image를 받아온 뒤, 새로 받아온 image로 다시 애플리케이션을 구동시키도록 한다.
해당 작업은 빠른 구동을 위해 익숙한 스크립트 언어인 js와 node.js를 활용해서 구성하였다.
3. 야매 CD 적용(spring 프로젝트 기준)
1. Spring 어플리케이션을 build 한다.
2. 프로젝트 루트 경로에 Dockerfile을 생성해 build 된 jar/war파일을 기반으로 docker image를 생성하기 위한 설정을 한다.
FROM amazoncorretto:17
COPY build/libs/*.jar app.jar
ENV DB_USERNAME=[DB_USERNAME]\
DB_PASSWORD=[DB_PASSWORD]\
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "/app.jar"]
해당 예시에서는 java의 amazoncorretto 17 버전 image를 이용하면서, 1번 과정을 통해 빌드된 build/libs/경로의 jar파일을 활용해서 도커 이미지를 빌드하는 설정이다. 위처럼 ENV 설정을 통해 애플리케이션 환경변수를 설정해 줄 수도 있으며, ENTRYPOINT설정을 통해 spring.profiles.active를 지정해서 spring 애플리케이션을 구동시킬 수도 있다.
2. docker buildx를 통해 multi platform image(amd, arm)를 build 함으로써, Docker-Hub에 image가 업로드되도록 한다.
docker buildx build -t [dockerhub repo]:[image tag] --platform linux/arm64/v8,linux/amd64 --push .
- 물론 나의 경우엔 개발 환경은 맥북 M1(arm)이고, 배포서버는 ubuntu(amd)였기 때문에 multi platform image를 build 하였는데, 이와 같은 상황이 아니라면, docker 이미지를 build 한 후 docker-Hub에 push 하는 방법으로도 변경이 가능하다.
- Docker의 Multi platform image build에 대해서는 이 글에서 자세하게 다뤄본다.
3. Docker Hub에 webhooks을 추가해 해당 레포에 이미지가 push 되면, 배포서버의 특정 port로 HTTP POST 요청을 날리도록 한다.
4. 아래 node server를 EC2인스턴스의 위에서 지정한 포트에서 구동함으로써, 해당 포트에 webhook 요청이 들어오면 image의 tag를 검사해 "prod"인 경우 서버에서 기존에 run 중이던 docker image를 내리고 새로운 image를 띄우도록 하였다.
const http = require('http');
const {exec} = require('child_process');
const server = http.createServer((req, res) => {
if (req.url === '/hooks/deploy') {
console.log('Webhook received')
let requestBody = '';
req.on('data', (chunk) => {
requestBody += chunk.toString();
})
req.on('end', () => {
const payload = JSON.parse(requestBody);
console.log(payload);
if (payload.push_data.tag === 'prod') {
let step = 'start';
try {
//Stop and Restart compose
step = 'down';
exec('docker compose down', (err, stdout, stderr) => {
console.log('docker compose down: ', stdout, stderr)
});
//Delete images
step = 'cleanup';
exec('docker rmi -f \$(docker images 도커허브레포/이미지명:prod -q) \$(docker images yana94ko/cuokkamap:prod -q) \$(docker images yana94ko/weallriding:prod -q)', (err, stdout, stderr) => {
console.log('docker rmi ... : ', stdout, stderr)
})
step = 'restart';
exec('docker compose up -d', (err, stdout, stderr) => {
console.log('docker compose up -d: ', stdout)
});
//Send Response
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('OK');
console.log('Pull And Restart Container is Sucessfuly Done!!');
} catch (err){
res.statusCode = 500;
res.end('Failed');
console.log('Failed to.. : ', step);
console.log('caused by: ', err)
}
} else {
console.log('Received image tag is not \'prod\', ignored.')
}
})
}
})
server.listen(8283, () => {
console.log('Webhook server is running..')
})
5. server에 docker, docker compose를 설치하고 프로젝트 구동을 위한 docker-compose.yml을 생성해 두었다.
#docker-compose.yml
version: '3'
services:
nginx:
image: nginx:1.23.4
container_name: nginx
ports:
- 80:80
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
- ./logs:/home/logs
cuokkamap:
image: yana94ko/cuokkamap:prod
container_name: cuokkamap
environment:
AWS_ACCESS_KEY: AWS_ACCESS_KEY
AWS_SECRET_KEY: AWS_SECRET_KEY
DB_USERNAME: DB_USERNAME
DB_PASSWORD: DB_PASSWORD
KAKAO_ADMIN_KEY: KAKAO_ADMIN_KEY
KAKAO_REST_API_KEY: KAKAO_REST_API_KEY
tlog:
image: yana94ko/playground:prod
container_name: tlog
environment:
AWS_ACCESS_KEY: AWS_ACCESS_KEY
AWS_SECRET_KEY: AWS_SECRET_KEY
DB_USERNAME: DB_USERNAME
DB_PASSWORD: DB_PASSWORD
weallriding:
image: yana94ko/weallriding:prod
container_name: weallriding
#nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
client_max_body_size 20M;
}
#default.conf
server {
listen 80;
listen [::]:80;
server_name localhost;
}
server{
listen 80;
server_name example1.com;
location / {
proxy_pass http://localhost:[port1];
proxy_set_header host $host;
}
}
server{
listen 80;
server_name example2.com;
location / {
proxy_pass http://localhost:[port2];
proxy_set_header host $host;
}
}
server{
listen 80;
server_name example3.com;
location / {
proxy_pass http://localhost:[port3];
proxy_set_header host $host;
}
}
내 경우에는.. 여러 프로젝트를 하나의 서버에서 관리하고자 3개의 프로젝트를 구동하는 docker-compose파일을 작성하였다. (관련해서 너무 작고 소중한 인스턴스에 nginx까지 4개의 container를 run 시킴으로써 발생한 메모리 이슈는 이곳에서 정리 예정이다.)
4. 문제점과 개선방안
직접 구현해 본 야매 CD는 아래와 같은 문제점들을 가지고 있다.
- 무중단 배포가 아니다.
- 중간에 이슈가 발생하는 경우 어느 곳에서 발생한 이슈인지 트리거하기 어렵다.
- 새로 배포한 이미지에 문제가 있어 롤백을 하기 위해서는 결국 직접 서버에 접속하는 방법밖에 없다.
- docker hub의 webhook를 listen 하기 위해 80 포트 이외에 특정 포트를 추가로 열어야 하기 때문에, 보안 이슈가 있다.
- 또한 docker hub의 webhook를 listen 하기 위한 node server를 별도로 run 시켜야 하기 때문에, 비싼 ec2서버에 리소스 낭비가 발생한다.
- server에 docker compose 사용을 위한 초기세팅을 위해서 서버 생성 초기에는 서버에 직접 접속할 수밖에 없다.
위와 같은 이슈들을 잡기 위해서라도 추후에 jenkins 혹은 AWS code deploy(pipeline)을 공부해 제대로 된 CI, CD를 구현해보고 싶다!
'Project' 카테고리의 다른 글
야나의 코딩 일기장 :) #코딩블로그 #기술블로그 #코딩 #조금씩,꾸준히
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!