저번의 Dockerize의 개요 이후 tomcat의 dockerize가 주된 고민거리였다.
또한 서비스의 특성상 트래픽이 일정하게 들어왔기에 Autoscaling은 비용의 문제로 진행하기 어려웠다. 이에 기존 인스턴스를 활용하면서도 서버의 유연함을 갖출수 있는 방법을 고민하다, 도커 클러스터링을 고안하게 되었다.
일단 OS에서의 유연함을 위해 어떻게 이미지를 배포할지에 대한 고민을 많이 하였다. 또한 기존 스프링세팅에서 오랜시간을 사용한 불편한점이 있었기에 세팅의 간편함을 위해서도 필요하였다
CI/CD로는
1. 기존 사용하던 Jenkins를 계속 사용
2. 마이그레이션에서 사용중인 GithubAction을 도입
Image저장소로는
1. Docker Hub
2. Github Package
3. Docker Registry
등의 여러가지 고민을 하였다.
일단 선택은 Jenkins를 꾸준히 사용, DockerHub를 도입하기로 결정을 하였다.
먼저 Jenkins의 빌드서버가 계속 있는 것에 대해 아쉬움이 있었는데,(Github Action은 따로 서버가 필요없기 때문) 만약 Jenkins에 있는 모든 ci/cd 기능(15개 이상 존재)을 모두 마이그레이션 할려면 ROI가 들것으로 예상이가, jenkins를 계속 이용하기로 하였다.
DockerHub의 경우 고민이 많이 되었다. 왜냐하면 JAR파일과 yml파일을 이용하여 이미지를 빌드하는 것이기에 public으로 저장소를 만들수가 없었다. private은 1개 이후 과금이 부과되므로 최대한 다른 저장소를 활용하려 하였으나, Github Package는 베타버전이후 과금이 될것으로 예상되며, DB서버 안에 Docker Registry를 사용하자니 현재 할당된 30기가의 저장소의 사용량을 넘을것 같아 제외하였다.
요즘 시대에 Docker Swarm이 왠말이냐 라는 의견이 있을수도 있다. 왜냐하면 현재 오케스트레이션 툴의 레퍼런스는 거의 쿠버네티스이기 때문이다. 본인도 k8s의 도입이 정말 하고 싶고 이용을 해보고 싶었지만 너무 오버스펙이라는 생각이 강하게 들었다. 관리해야할 컨테이너가 대용량이라면 충분히 고려해볼만 하겠지만 현 프로젝트의 초점은 컨테이너 관리, 스케일링, 유연한 배포가 목적이기에 Docker Swarm만 적용해도 충분히 매니징이 가능할것이라고 생각하였다.
컨테이너 오케스트레이션의 장점
컨테이너 오케스트레이션의 기능
Kubernetes vs Docker Swarm간의 선택
Docker Swarm은 간단한 컨테이너 오케스트레이터로 설계 되었다. 이미 엄청난 인기를 누리고 있는 Docker Compose에서 네트워크와 서비스의 개념을 가져와 Docker엔진의 일부가 된 오케스트레이터에 구축했다.
Kubernetes는 모든 주요 클라우드에서 관리형 서비스로 제공되기 때문에 더 인기있는 옵션이다. 현재 사용중인 AWS와 같은 클라우드에서 CLI의 단일 명령 또는 웹 포털에서 몇번의 클릭으로 다중 노드 Kubernetes 클러스터를 가동할 수 있다. Kubernetes는 쉽게 확장할 수 있으므로 클라우드 제공업체는 로드밸런서 및 스토리지와 같은 다른 제품과 통합할 수 있으므로 모든 기능을 갖춘 애플리케이션을 쉽게 배포할 수 있다.
DockerSwarm은 클라우드 제공업체의 관리형 서비스로 존재하지 않는다. 부분적으로 움직이는 부품이 적어 다른 서비스와 통합하기 어렵기 때문이다. 클라우드에서 Docker Swarm클러스터를 실행하려는 경우 스스로 프로비저닝하고 관리해야 하는 주요 클라우드 리소스를 보여준다.


위는 도커 스웜의 네트워크 구조이다. 이를 위해선 gw_bidge, ingress ip, overlay ip에 대해서 알고 있어야 한다.
스웜모드는 여러개의 도커 엔진에 같은 컨테이너를 분산해서 할당하기 때문에 각 도커 데몬의 네트워크가 하나로 묶인 네트워크 풀이 필요하다. 이뿐만 아니라 서비스를 외부로 노출했을때 어느 노드로 접근하더라도 해당 서비스의 컨테이너에 접근할 수 있게 라우팅 기능이 필요하다.

bridge, host, none 네트워크 외에도 docker_gwbridge와 ingress네트워크가 생성된 것을 볼 수 있다.
gw_bridge는 네트워크 스웜에서 오버레이 네트워크를 사용할때 사용되며, ingress네트워크는 로드밸런싱과 라우팅 메시에 사용된다.
일단 네트워크는 중요한 개념이니 짚고 넘어가도록 하겠다.
Ingress network
Ingress네트워크는 스웜클러스터를 생성하면 자동으로 등록되는 네트워크로서, 스웜모드를 사용할때만 유효하다. 이는 docker network ls명령어(위의 명령어)를 입력했을때 확인할 수 있는 SCOPE 항목에서 Swarm으로 설정된 것에서 알 수 있다.매니저 노드뿐 아니라 스웜 클러스터에 등록된 노드라면 전부 ingress네트워크가 생성된다.
ingress네트워크는 어떤 스웜 노드에 접근하더라도 서비스 내의 컨테이너에 접근할 수 있게 설정하는 라우팅 메시를 구성하고 서비스 내의 컨테이너에 대한 접근을 라운드로빈 방식으로 분산하는 로드밸런싱을 담당한다.
Overlay network
오버레이 네트워크는 여러개의 도커 데몬을 하나의 네트워크 풀로 만드는 네트워크 가상화 기술의 하나로서, 도커에 오버레이 네트워크를 적용하면 여러 도커 데몬에 존재하는 컨테이너가 서로 통신할 수 있다. ingress네트워크 또한 오버레이 네트워크 드라이버를 사용한다.
이해하기 쉽게 보자면 스웜클러스터 내의 컨테이너도 IP를 할당받아야 한다.
eth0는 ingress네트워크와 연결된 네트워크이다. IP주소가 차례로 할당된다고 보면 된다.
예를 들어 eh0 10.255.0.9라면 컨테이너의 ip는 각각 10.255.0.9 10.255.0.10 10.255.0.11... 위와 같이 생성된다.
즉 여러개의 스웜 노드에 할당된 컨테이너는 오버레이 네트워크의 서브넷에 해당하는 IP대역을 할당받고 이 IP를 통해 서로 통신할 수 있다.
docker_gwbridge
오버레이 네트워크를 사용하지 않는 컨테이너는 기본적으로 존재하는 브리지 네트워크를 사용해 외부와 연결한다. 그러나 ingress를 포함한 모든 오버레이 네트워크는 이와 다른 브리지 네트워크인 docker_gwbridge네트워크와 함께 사용된다. docker_gwbridge네트워크는 외부로 나가는 통신 및 오버레이 네트워크의 트래픽 종단점역할을 담당한다. 즉 docker swarm을 init하거나 join할때 사용한다.
docker_gwbridge 네트워크는 컨테이너 내부의 네트워크 인터페이스 카드 중 eth0와 연결된다.
Swarm은 프로덕션 환경에서 컨테이너 오케스트레이션을 소개하고 작업 부하가 아무리 크더라도 쉽게 실행할 수 있는 훌륭한 제품이다. Visa는 Docker의 컨퍼런스에서 Swarm클러스터를 사용하여 블랙프라이데이에 엄청난 급증을 포함하여 시스템을 통해 모든 지불을 지원하는 것에 대해 이야기 하기도 하였다.
현재 사용하는 서버의 자원을 살펴보자





확실히 CPU보단 메모리 자원이 많이 필요함을 알 수 있고, WAS를 운용중인 Stage와 production은 메모리가 많이 필요하다. 반면 Build나 Internal은 메모리가 넉넉하게 있기때문에 자원을 끌어오는 것이 좋겠다는 판단을 하였다.
이러한 점을 고려하여 AWS의 AutoScaling을 적용하는 것보단 Docker Swarm의 클러스터링이 비용적인 면에서 훌륭한 대안이 될거라고 생각한다. 이렇게 되면 현재 https리다이렉션 하는 ELB를 제거하여 비용을 줄일수 있다고 생각하였다.
롤링배포를 통한 안정적인 배포
현재 저번의 NonStopDeploy 를 통해 무중단 배포를 구성하긴 하였지만 배포할때 평시 자원의 두배가 사용되어 서버에 무리를 줄수 있다는 점에 배포시에도 서버자원을 안정적으로 활용할수 있는 롤링배포를 적용하게 되었다.
서비스 장애 복구
replicas를 사용한 복제모드로 설정된 서비스의 컨테이너가 정지하거나 특정 노드가 다운되면 스웜 매니저는 새로운 컨테이너를 생성해 자동으로 복구를 해준다. 이전에는 서버가 다운되면 따로 알림이 안가, datadog을 이용한 health-check를 하고 있었으나, 좀더 근본적인 서비스가 다운되었을 경우 복구를 하는 방법이 있는 장점이 있다.
롤백 가능
현재 마이그레이션을 하면서 기능이 많이 바뀌고 있는데, PR을 무조건 두명이 Approve를 해야 Merge가 되긴 하지만 잘못된 코드가 반영이 될수도 있다. 이전에는 git revert를 사용하여 빠르게 브랜치를 수정하거나 심지어 깃에 미숙한 인원이 사용할 경우 새로 브랜치를 다시 만들어 대응하는 경우도 있었다. 이럴경우 서버의 안정을 위해 빠른 롤백을 통해 장애 복구가 가능하다는 장점이 있다.
기존 Jenkins같은 경우 Build를 하여 Jar파일을 바탕으로 웹서버에 deploy된 Shell을 통해 Blue/Green 배포를 진행하였다.
DockerSwarm을 적용할 경우 Jenkins에서 Build후 이미지를 도커허브에 보내고, 워커노드들이 도커허브에서 이미지를 Pull당겨오고, 매니저 노드가 Rolling배포를 진행하는 방식으로 진행되었다.
DockerSwarm을 이용한 클러스터링
먼저 DockerSwarm을 활용하여 클러스터링을 진행하였다.
ManagerNode로 사용할 서버에 다음과 같은 명령을 내린다.
docker swarm init --advertise-addr 매너저노드의 IP
그러면 docker swarm join ~ 토큰이 주어지는데 이를 이용하여 Join을 할 수 있다.
docker swarm join \ --token 발급된 토큰
이 토큰은 보안상 유출을 하면 안된다.
또한 AWS의 보안그룹 혹은 리눅스Firewall도 열어주어야 한다.

스웜매니저는 기본적으로 2377포트를, 노드사이의 통신에는 7946/tcp, 7946/udp포트를, ingress 오버레이 네트워크에는 4789/tcp, 4789/udp를 사용한다.
docker node ls
를 사용하면 다음과 같이 활성화된 노드들을 확인 할 수 있다.

Docker File추가
Dockerfile은 Docker에서 이미지를 생성하기 위한 용도로 작성하는 파일이기에 기존 프로젝트에 추가를 해줘야 한다.
FROM amazoncorretto:17
WORKDIR /app
COPY ./build/libs/KOIN_API_V2.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
build/deploy스크립트 작성
작업을 하면서 많은 문제가 발생 하였다.

위와같이
인터널 서버는 arm이미지를 사용중이었기에 젠킨스서버(x86)에서 빌드한 이미지의 형식이 달라 컨테이너 실행이 안되었다.

따라서 QEMU에뮬레이터를 사용하여 buildx를 통해 arm64,amd64모두 빌드할 수 있도록 빌드 스크립트를 짰다.

하필 젠킨스의 Docker Version이 낮아 buildx가 실행되지 않았기에 Docker버전을 올려주었다.
여기서 부터 많은 시간을 활용했다. 빌드서버의 도커를 지워보기도 하고, 여러가지 시도롤 해보았지만 항상 로그의 해답이 옳다는 것을 느꼈다. buildx로 빌드를 한다면 권한이 없다고 나오는 것이다. 분명히 로그인을 통해 권한을 얻었지만 쓰기 권한이 없어서인지 DockerHub에서 토큰을 발행받고 로그인을 PW로 하는것이 아닌 토큰을 통해 해야 하는 것이다.
#!/bin/bash
# latest 태그가 없는 이미지와 해당 이미지를 참조하는 정지된 컨테이너를 삭제
sudo docker images -q --filter "dangling=true" | xargs -r sudo docker rmi
sudo docker ps -aq --filter "status=exited" | xargs -r sudo docker rm
# Enable Docker's experimental features for buildx (if not already enabled)
export DOCKER_CLI_EXPERIMENTAL=enabled
# Create and use a new buildx builder which allows multi-architecture builds
docker buildx create --name mymultiarchbuilder --use
# Start up the buildx builder
docker buildx inspect --bootstrap
# Login to Docker Hub (use of password via stdin for security)
#echo"xxxxxxxxx" | sudo docker login -u "유저명" --password-stdin
echo "도커 허브에서 받은 토큰" | docker login --username 유저명 --password-stdin
# Build and push the image as latest, for both amd64 and arm64 architectures
docker buildx build --platform linux/amd64,linux/arm64 -t 유저명/레포지토리명:latest . --push
# Build and save the amd64 image locally without pushing to Docker Hub
docker buildx build --platform linux/amd64 -t 유저명/레포지토리명:latest . --load
위의 주석처리와 같이 원래의 로그인은 주석처리 하고 토큰으로 로그인을 하는 모습을 볼 수 있다.
토큰은 Account Settings/Security에서 받아올 수 있다.


젠킨스 서버에서 빌드후 로컬에 이미지를 저장한 후에는

Send files or execute commands over SSH after build runs를 선택하여 deploy.sh라는 쉘스크립트를 실행할 수 있도록 한다.
쉘스크립트는 아래와 같다.
#bin/bash
# latest 태그가 없는 이미지와 해당 이미지를 참조하는 정지된 컨테이너를 삭제
sudo docker images -q --filter "dangling=true" | xargs -r sudo docker rmi
sudo docker ps -aq --filter "status=exited" | xargs -r sudo docker rm
# Docker Hub에서 pull하기 위해 login 합니다. 비밀번호는 안전하게 전달됩니다.
echo "비밀번호" | sudo docker login -u "유저명" --password-stdin
#tag를 지정하지 않고 pull 하여 docker hub에서 latest를 가져옵니다.
sudo docker pull 유저명/레포지토리명
그리고 매니저 노드는 아래와 같이 구성해야 한다.
#bin/bash
# latest 태그가 없는 이미지와 해당 이미지를 참조하는 정지된 컨테이너를 삭제
sudo docker images -q --filter "dangling=true" | xargs -r sudo docker rmi
sudo docker ps -aq --filter "status=exited" | xargs -r sudo docker rm
# docker service를 업데이트하여 롤링 업데이트를 수행합니다.
sudo docker service update --image 유저명/레포지토리명:latest \
--update-delay 10s \
--update-parallelism 1 \
--force \
서비스이름
force를 붙인 이유는 BuildServer에서 config파일을 관리하고 빌드시 
위와 같이 config파일을 cp명령어를 통해 넘겨준다.
그렇기에 만약 config파일을 수정하고 수동빌드를 하였을 경우 이미지 수정사항이 없어 업데이트가 일어나기 않기에 --force를 꼭 붙여줘야 한다.

빌드 및 업데이트가 잘 되는 것을 볼 수 있다.
만약 아직 서비스를 만들지 않았다면,
docker service create --name 서비스명 --replicas 컨테이너 개수 -p 호스트포트:컨테이너포트 참조할 이미지
위와 같이 서비스를 시작할 수 있으며
docker service scale 서비스명=컨테이너 개수
로 컨테이너의 개수를 조절할수도 있다.
윤곽을 잡게된 인프라는 다음과 같다.



작년부터 고안했던(다량의 트래픽 핸들링) 인프라가 개선된 모습을 보니 뿌듯하다...!
출처:
https://www.notion.so/mrjun/a5052cfcd5164adb8613333257957bd7?pvs=4
https://velog.io/@springer/12-Docker-Swarm-and-Kubernetes
https://chanyoung-dev.github.io/Cloud/CICD/CICDdocker
https://codenoyes.tistory.com/82
https://5equal0.tistory.com/entry/Docker-Registry-%EC%82%AC%EC%84%A4-%EC%9B%90%EA%B2%A9-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0
https://setyourmindpark.github.io/2018/02/06/docker/docker-4/
https://velog.io/@xolgit/%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-Docker
https://froggydisk.github.io/21th-post/