DevOps
개발개발 ·
aws 배포 ec2 (1) : 모놀리식 배포

우선 aws ec2에 들어간다.

인스턴스 시작을 클릭!

더 많은 AMI 찾아보기를 클릭 후

우분투 리눅스를 둘중 하나 클릭
조금 내려가다 보면 키 페어를 선택하라고 나오는데 이부분은 새 키페어 생성을 클릭하면 된다.

키 페어 이름을 입력하고 키 페어를 생성하면 된다. 키 페어 유형은 RSA, 프라이빗 키 파일 형식은 .ppk로 설정하면 ppk확장자로 다운이 된 것을 알 수 있다. 이것은 putty로 서버에 들어갈 때 사용될 것이니 고이 모셔두자
그 후에 인스턴스 시작을 누르면 인스턴스 제작 완료!
aws 배포 ec2 (2) : 모놀리식 배포
putty를 깔게 되면 이런 화면이 나온다. (설치하는 것은 생략한다)

우선 퍼블릭 IP를 알아야 한다. aws ec2 인스턴스 세부사항에 퍼블릭 IPv4를 복사하자

복사!

먼저 호스트네임을 ubuntu@퍼블릭IPv4를 적는다. 그리고 세션을 저장할 이름을 적고 Save를 누른다. 그리고 SSH에 들어가서 Auth 항목에 들어간다.

브라우저를 클릭하고 아까 우리가 다운 받았던 ppk 파일을 업로드한다. 그리고 Open을 누르면?

접속 완료!
aws 배포 ec2 (3) : 모놀리식 배포
putty로 서버와 연결됐다면 아래의 명령어를 입력하자.
# aws corretto 다운로드
sudo curl -L https://corretto.aws/downloads/latest/amazon-corretto-11-x64-linux-jdk.rpm -o jdk11.rpm
# jdk11 설치
sudo apt-get install openjdk-11-jdk
GitHub Repository에 코드 배포하기
IntelliJ IDEA 기준 GitHub에 Repository로 코드를 배포하는건 매우 간단하다.
위의 메뉴중 VCS - Share Project on GitHub를 누르면 되는데 막상 처음하면 안될것이다. 왜냐면 Git 연결이 안되어있기 때문인데

Ctrl + Alt + S 를 눌러 환경설정에 들어간 다음 github를 쳐서 보면 필자는 이미 등록되어 있지만 아무것도 없으면 Add account가 뜰 것이다. Add account를 눌러서 GitHub 아이디와 비밀번호를 치면 등록이 된다. (GitHub 등록방법은 생략한다.)
암튼 그 후에 GitHub로 들어가서 확인해보면 Repository가 생성되어있는 모습을 확인할 수 있다.

GitHub Deploy key 등록하기
이제 깃헙의 Deploy key를 등록하자
putty에서 연결한 후 다음 명령어를 입력한다.
ssh-keyget -t rsa
이러면 키 이름을 입력하라고 뜨고 password(?)도 입력하라고 한다. 둘 다 입력하면 된다. 그리고 그 후
cat deploykey이름.pub
이렇게 치면 쭉 뭐가 나오는데 이것을 싹 복사한다.

이제 연결해주면 된다 연결 방법은 다음과 같다




여기에 타이틀과 키를 복사하고 동의 후 Add key를 누르면 끝!
이건 Repository에 등록한 것이고 이제 계정에 deploy key를 등록할 차례이다.
ssh-keygen -t rsa -C 깃허브주소
이러면 또 deploy key가 생성된다. 그 후에 다시 cat <deploy key이름>.pub을 입력하고 나오는 것을 쭉 복사해서 이번엔 Settings - SSH and GPG keys 카테고리로 들어가서 - New SSH key를 클릭 - 키 이름 입력후 복붙하고 Add key를 누르면 끝!



위의 과정이 끝났으면 이제 git clone을 이용해 git에 있는 내용을 복사해줘야 한다.
우선 git을 설치해야 한다.
sudo apt-get install git
git clone <리포지토리 주소>
리포지토리 주소는 아래의 사진에서 확인 가능하다.

aws 배포 ec2 (4) : 모놀리식 배포
이번엔 쉘 스크립트를 작성해서 git pull부터 빌드해서 실행하기 까지의 과정을 자동화시키려고 한다.
일단은 이전에 clone에서 받은 프로젝트 폴더로 들어가보겠다. 필자는 chatting이라는 리포지토리 이름을 사용하고 있다.

우선 이 폴더를 sources라는 폴더로 통째로 옮긴다.
# sources 폴더 생성
mkdir sources
# chatting 폴더를 sources 폴더 내부로 옮기기
mv chatting ./sources/
# sources 폴더로 들어가기
cd sources
그리고 chatting 폴더에 들어가서 gradlew의 권한을 바꿔준다.
# chatting 폴더로 들어가기
cd chatting
# gradlew의 권한 변경
chmod + ./gradlew
# 이전 디렉토리로 복귀
cd ..
빌드 스크립트 작성
프로젝트 폴ㄹ더가 있는 위치에서 빌드 스크립트를 작성한다. 일단 먼저 파일을 만든다.
vim deploy.sh
그리고 deploy.sh 라는 파일이 생성되었다면, 다음과 같이 스크립트를 입력한다.
# 프로젝트 폴더가 위치해있는 디렉토리
REPOSITORY=/home/ec2-user/sources
// sources가 있는 리포지토리 파일은 사람마다 다르다
// sources가 있는 루트에서 pwd 명령어를 통해 현재 본인의
// 위치를 파악하고 ec2-user 자리에 넣으면 된다.
# 프로젝트 디렉토리로 들어가기
cd $REPOSITORY/order-example
echo "> GIT PULL"
git pull
echo "> START TO BUILD PROJECT"
chmod +x gradlew
// gradlew의 권한이 자꾸 부족하다고 뜨고 vi 밖에서
// 해결하려고 해봐도 권한이 644에서 755로 바뀌지 않아서
// 스크립트 내부에서 직접적으로 권한을 부여해줬음
./gradlew build
echo "> COPY JAR FILES"
cp ./build/libs/*.jar $REPOSITORY/
echo "> 현재 구동중인 어플리케이션의 PID 확인"
CURRENT_PID=$(pgrep -f devops-test)
if [ -z $CURRENT_PID ]; then
echo "현재 구동중인 APPLICATION이 없으므로 종료하지 않습니다."
else
echo "kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "DEPLOY NEW APPLICATION"
JAR_NAME=$(ls -tr $REPOSITORY |grep jar | tail -n 1)
echo "JAR NAME : $JAR_NAME"
# nohup을 통한 무중단 배포
nohup java -jar $REPOSITORY/$JAR_NAME &
한 줄씩 해석해서 살펴보자
# 프로젝트 폴더가 위치해있는 디렉토리
REPOSITORY=/home/ec2-user/sources
# 프로젝트 디렉토리로 들어가기
cd $REPOSITORY/order-example
일단은 프로젝트가 위치한 폴더 내부로 들어가기 위한 코드이다. REPOSITORY에는 pwd 커맨드를 통해서 획득한 sources의 디렉토리를 복사해서 넣어주면 된다.
git pull
만일 최신의 프로젝트가 존재한다면 pull을 하여 최신화 시켜준다.
./gradlew build
현재 우리의 프로젝트는 gradle 기반이기 때문에 위의 코드를 통해서 프로젝트를 빌드할 수 있다.
cp ./build/libs/*.jar $REPOSITORY/
빌드의 결과로 생겨난 jar파일들을 모두 REPOSITORY로 복사한다.
CURRENT_PID=$(pgrep -f chatting)
if [ -z $CURRENT_PID ]; then
echo "현재 구동중인 APPLICATION이 없으므로 종료하지 않습니다."
else
echo "kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
현재 이 프로젝트를 기반으로한 프로세스가 동작중이라면, 그 프로세스를 죽이고 새 프로세스를 실행하기 위한 코드이다.
우선 CURRENT_PID에는 현재 프로젝트가 프로세스로 올라가있다면, 그 프로세스의 ID 값이 얼마인지 저장한다. 필자의 프로젝트 이름은 chatting으로 정했기 때문에 chatting으로 pgrep -f 옵션을 통해 검색을 한다.
만약 PID 값이 존재한다면 프로세스를 kill 하고 5초를 sleep을 걸어준다.
JAR_NAME=$(ls $REPOSITORY |grep 'chatting' | tail -n 1)
REPOSITORY에서 디렉토리를 탐색하여서 chatting 이라는 이름을 가진 파일을 찾아내되, 이름이 겹치는 경우에는 최신의 파일을 가리키게 JAR_FILE 변수를 선언한다.
nohup java -jar $REPOSITORY/$JAR_NAME &
nohup을 통해서 jar file을 무중단 실행시킨다. &를 통해서 nohup을 통해 배포된 프로젝트가 백그라운드에서 동작하게 한다.
이제 빌드스크립트를 모두 작성했으니, 이제 실행할 차례이다.
그 전에 위의 코드를 실행하다보면 다양한 에러에 직면하게 될텐데 자잘한 문제 말고 해결하는데 꽤 고생했던 에러들을 모아뒀다.
스프링부트 ec2 배포 중 fatal : not a git repository (or any of the parent directories) : .git
https://coding-review.tistory.com/163
스프링 부트 ec2 배포중 gradlew 빌드 후 Execution failed for task ':test' 에러
https://coding-review.tistory.com/164
스프링부트 ec2 배포 중 ./gradlew: Permission denied
https://coding-review.tistory.com/165
deploy.sh를 실행하면 다음과 같은 창이 뜨면서 프로젝트를 빌드하고, 동시에 배포를 진행한다.

배포가 안정적으로 되었는지 체크해보자. 본인의 인스턴스 퍼블릭 IP를 통해 http 요청을 하나 보내보면
잘뜨는것을 확인할 수 있다.
아직 끝나지 않았다
위와 같은 배포 방식에는 문제점이 다소 있다. 대표적인 문제점은 다음과 같다.
• 다수의 트래픽을 감당하기엔 무리가 있다. 왜냐하면, 하나의 인스턴스에 대해서 배포를 진행하고 있기 때문이다.
• 배포 인스턴스의 이미지를 이용해서 여러 인스턴스로 불려내서 Load Balancing을 진행한다고 하더라도 충분히 문제의 소지는 있다. 이 경우에는 각각의 인스턴스를 직접 관리해야한다.
• 어찌저찌 관리한다고 하더라도, 장애 복구와 관련된 문제가 있다. 장애 복구가 자동화 되어있지 않기 때문에 실제 서비스에서 이러한 방식을 채택한다면 크게 문제가 발생할 수도 있다.
그래서 다음 주제는 DevOps의 끝판왕! docker와 관련된 주제로 살펴보겠다.
AWS 클라우드 생태계 알아보기
AWS 클라우드는 정말 많은 서비스를 클라우드 서비스로 제공하고 있습니다.
AWS의 클라우드 생태계는 정말 복잡하고 이름도 왜이리 어려운게 많은건지 뭐부터 알아야할지 답답합니다.
제가 답답했다는 뜻이구요. 답답해서 AWS 클라우드 생태계에 대해서 알아봤습니다. 바로 시작해보죠
AWS Cloud
EC2 (Elastic Compute Cloud)
EC2는 클라우드에서 가상 서버를 제공해주는 서비스라고 생각하시면 됩니다. 이러한 가상 서버는 인스턴스라는 이름으로 더 많이 알려져있습니다. 그리고 이 인스턴스들은 마치 물리적인 서버같이 작동합니다.
EC2는 굉장히 유연하게 서버를 관리할 수 있다는 것이 특징입니다. CPU 파워를 조절할 수 있고, OS도 선택할 수 있으며, 메모리의 양도 정할 수 있고, 저장소의 크기와 같은 것들도 설정할 수 있습니다.
즉, 나만의 커스텀 PC를 클라우드 환경에서 만들 수 있다는 것입니다.
때문에, 정말 PC 조립하듯이 나는 무거운 프로그램을 많이 돌리니까 램이 다른사람보다 많이 필요해 하면 램을 더 추가하고, 난 머신러닝 컴퓨터가 필요하니까 그래픽카드를 좋은거 쓸거야 하면 좋은 그래픽카드 추가하고 이렇게 유연하게 PC를 만들 수 있다는 것입니다.
IAM (Identity and Access Management)
IAM은 내가 배포한 애플리케이션의 유저들을 위해 AWS 자원들에 접근을 보안적으로 컨트롤하는데 도움을 줍니다.
즉, 내가 만들어둔 EC2나 S3와 같은 자원들에 다른 사람들이 접근하는 것에 대해 보안적으로 관리할 수 있게 도와준다는 뜻이죠.
IAM의 대표적인 특징으로는
• AWS 계정에 접근을 공유할 수 있음 : 다른 사람에게 관리자 권한을 이양할 수 있고, 패스워드를 공유하지 않고 나의 AWS계정 안에 있는 자원들을 사용할 수 있게 해줍니다.
• MFA (Multi-Factor Authentication) : 내 계정에 2FA (Two-Factor Authentication) 를 걸어서 내 계정을 안전하게 보호할 수 있습니다.
• 보안 : 아마존의 EC2를 구동한 애플리케이션을 위한 AWS 자원에 접근하는 것을 보호할 수 있습니다.
S3 (Simple Storage Service)
S3는 AWS에서 제공하는 오브젝트 스토리지 서비스입니다. S3는 언제 어디서나 웹의 데이터들을 꺼내오고 저장할 수 있도록 설계되었습니다.
S3는 흔히 백업, 데이터 저장, 데이터 보관, 컨텐츠 분배와 같은 것들을 제공합니다. 또한, S3는 확장성이나, 데이터 가용성, 보안, 성능을 위해서 다른 AWS 서비스를 사용할 때 선택하는 스토리지 서비스입니다.
즉, 정리하면 로컬 저장소는 스토리지의 크기나 보안같은 것들을 신경써야 하는데 클라우드에 올려놓고 저장하면 AWS가 대신 그것들을 관리해준다는 것입니다.
ACM (AWS Certificate Manager)
ACM은 AWS가 SSL이나 TLS핸드쉐이킹 같은 복잡한 기능들을 만들고, 저장하고, 관리해주는 서비스입니다.
SSL이나 TLS인증은 인터넷을 넘어 웹사이트의 신원을 설립하거나 네트워크 커뮤니케이션 (아마 여기선 3 way handshake를 말하는 듯 합니다.) 간에 보안을 위해 사용됩니다.
즉, 정리하면 HTTPS를 이용하기 위해 사용되는 서비스입니다.
Route 53
AWS만의 자체적인 DNS 웹 서비스입니다. Route 53은 개발자나 비즈니스에게 극도로 높은 신뢰성과 효율적인 방법으로 유저들의 인터넷 애플리케이션과 라우팅 하는 방법을 제공합니다.
Route 53은 AWS EC2 인스턴스타 후에 설명할 ELB 혹은 S3 Bucket들과 같이 AWS에서 제공하는 인프라들과의 효과적인 연결을 제공해줍니다.
Route 53의 특징으로는 도메인 저장 서비스, DNS 라우팅, 애플리케이션 장애 체크, 트래픽 관리등을 맡고 있습니다.
ELB (Elastic Load Balancer)
AWS에서 ELB는 자동적으로 애플리케이션에 들어오는 다양한 타겟들의 트래픽들을 분배해주는 서비스입니다. ELB는 트래픽을 다양한 자원들을 통해 효과적으로 분배합니다.
즉, ELB는 AWS에서 만든 로드 밸런서입니다.
ELB에는 CLB (Classic Load Balancer), ALB (Application Load Balancer), NLB (Network Load Balancer) 이렇게 세가지가 존재합니다.
CLB는 레거시 ELB라 지금은 많이 사용되지 않습니다. ALB는 OSI 모델 중 7계층 로드 밸런서입니다. ALB는 HTTP나 HTTPS 트래픽을 로드 밸런싱할 때 적합합니다. NLB는 OSI 모델 중 4계층 로드 밸런서입니다. NLB는 TCP나 UDP의 로드밸런싱을 수행할 때 적합합니다.
ECR (Elastic Container Registry)
아마존의 ECR은 개발자들을 위해 도커 컨테이너 이미지들을 배포하고, 관리하고, 저장하기 쉽게 만들어주는 도커 컨테이너 저장소입니다.
ECR은 아마존의 ECS와 결합되어 있고 우리의 워크 플로우 프로덕션 개발을 쉽게 해줍니다.
ECR은 인프라를 확장하는 것에 대한 걱정이나 우리의 컨테이너 리포지토리를 실행할 필요를 제거해줍니다. ECR은 우리의 애플리케이션을 위해 신뢰성 있게 컨테이너를 배포해주고, 높은 가용성과 확장성있는 아키텍처 안에서 우리의 이미지를 호스트해줍니다.
즉, 각각의 서버를 같은 환경으로 만들기 위해서 도커 이미지, 컨테이너 관리가 힘든 우리들을 위해 ECR로 도커 컨테이너를 쉽게 관리하기위한 서비스입니다.
조금 개념이 애매한데 예를 들어서 설명하자면
만약 Micro Service Architecture에서 각각의 격리된 애플리케이션들은 각각 동일한 서버 환경을 가지고 있어야 할겁니다. 예를 들어 결제 로직에는 MySQL 8.0.17을 쓰고 검색 로직에는 MySQL 8.0.30을 쓰면 안되겠죠?
그렇기 때문에 모든 서버에는 모두 같은 환경으로 구동되어야 하기 때문에 각각의 격리된 애플리케이션이 늘어날 때마다
• 도커 이미지 다운 (이미지가 한두개가 아니겠죠?)
• 도커 컨테이너 생성 (컨테이너도 한두개가 아니겠죠)
• docker-compose 생성
• docker 실행
이 짓을 애플리케이션이 늘어날 때마다 해줘야합니다. 개발자 입장에서 이렇게 되면 확장하기 굉장히 꺼려질겁니다.
ECR을 사용하면 위와 같은 과정이 하나로 압축됩니다.
• ECR에 저장해둔 도커 이미지와 컨테이너 pull
정말 간단해집니다.
ECS (Elastic Container Service)
ECS는 컨테이너 통합 서비스입니다. ECS는 컨테이너를 자동적으로 배포해주고, 확장해주고, 관리해주는 서비스입니다. 만약 내가 도커 이미지를 가지고 있을 때나 이미지를 기반으로 컨테이너를 실행하고 싶을 때 우리는 ECS를 이용할 수 있습니다.
ECS는 도커 컨테이너의 스케쥴링을 관리해주고, 컨테이너를 실행해주고, 컨테이너의 네트워킹이나 보안을 대신 신경써줍니다.
쉽게 설명하면 ECS는 Kubernetes같은 서비스라고 보시면 됩니다. 엄청나게 많이 쪼개져있는 격리된 애플리케이션을 하나로 묶어서 (오케스트리온 이라고 하덥니다) 컨테이너들을 자동적으로 관리하고 배포하는 작업을 진행해줍니다.
마치며
여기까지 AWS의 생태계에 대해서 알아봤습니다. 이거 말고도 정말 많은데요 제가 일단 사용할 것 같은 것들만 위주로 정리해봤습니다.
기존에는 EC2나 Route 53, S3, IAM 정도를 사용했는데요. CI / CD에 대한 파이프라인 설계를 공부하던 중에 앞으로 사용할 수 있을 것 같은 ACM, ELB, ECR, ECS등을 공부해봤습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
AWS ECR에 docker image push하기
이번 포스팅에선 AWS ECR에 도커 이미지를 푸쉬하는 방법에 대해서 자세히 포스팅해보도록 하겠습니다.
이번 포스팅에선 AWS EC2가 있다고 가정하고 docker가 EC2에 설치되어있어야 합니다.
순서를 대략적으로 말씀드리자면
• IAM 생성 및 정책 추가 (IAM 생성 시 주는 csv 파일을 반드시 가지고 있어야 함)
• AWS CLI 설치
• AWS CLI 프로필 설정
• Dockerfile 생성
• ECR로그인, docker image 빌드, docker image 태깅, docker push
그럼 한번 시작해보도록 하죠!
ECR에 docker image push
1. IAM 생성 및 정책 추가
이번 포스팅에선 IAM 생성에 대한 내용은 알려드리지 않습니다. IAM을 생성하는 방법은 구글링하시면 잘 나와있으니 그 방법을 따라 하시면 될 것 같습니다.
IAM을 만드셨으면 이제 정책을 추가해야합니다.

위의 스샷은 IAM 사용자를 클릭하면 볼 수 있는 화면입니다. 이후 권한 추가를 누르고?

직접 정책 연결을 선택하고 검색창에 AmazonEC2ContainerRegistryFullAccess 를 검색합니다.
검색 결과를 선택하시고 디폴트 값으로 다음다음 눌러서 설정해주시면

이렇게 정책이 추가됩니다.
2. AWS CLI 설치
AWS CLI 는 명령 프롬프트에서 명령어를 통해 AWS 자원에 접근하는 방법입니다.
기본적인 AWS 생태계를 구축하고 계시다면 AWS 공식 사이트에서 조작하는 것이 좋지만 정교한 설정을 하기 위해서는 CLI가 있는 것이 더 편할 수 있습니다.
우리는 EC2 즉, 리눅스 환경이기 때문에 리눅스만 보도록 하겠습니다.
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
$ aws --version
aws-cli/2.7.9 Python/3.9.11 Linux/5.13.0-51-generic exe/x86_64.ubuntu.20 prompt/off
$ rm -f awscliv2.zip # 설치 되었으면 zip은 지워주자. 용량이 여유롭지 못하다
만약 이미 설치가 되어있고 업데이트만 진행하고 싶은 경우에는 다음 명령어를 입력하면 됩니다.
$ which aws
/usr/local/bin/aws
$ sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
3. AWS CLI 프로필 설정
이 작업을 거치는 이유는 우리는 ECR 자원에 접근해야 하기 때문에 IAM으로 인증을 거쳐야 ECR 리포지토리에 접근할 수 있을 겁니다.

Access Key와 Secret Key는 IAM을 생성할 때 다운 받았던 csv 파일에 있으니 그걸 그대로 치시면 됩니다.
4. Dockerfile 생성
Dockerfile은 보통 프로젝트 위에 만드는 것이 일반적입니다.
/myproject
Dockerfile
/myapp
app.py
requirements.txt
이런 구조의 프로젝트인 경우 Dockerfile은 저렇게 만드시면 됩니다.
Dockerfile은 아래와 같이 만드시면 됩니다.
FROM ubuntu:22.04
RUN apt-get update && \
apt-get -y install apache2
RUN echo 'Hello World!' > /var/www/html/index.html
RUN echo '. /etc/apache2/envvars' > /root/run_apache.sh && \
echo 'mkdir -p /var/run/apache2' >> /root/run_apache.sh && \
echo 'usr/sbin/apache2 -D FOREGROUND' >> /root/run_apache.sh && \
chmod 755 /root/run_apache.sh
EXPOSE 80
CMD /root/run_apache.sh
보통 vim으로 만들고 위와같이 빈공간을 반드시 만들어주셔야 합니다. 스페이스바로 띄워서 줄을 맞추시면 됩니다.

저 부분을 스페이스바로 띄워주셔야합니다.
5. ECR 리포지토리 생성
AWS에 ECR을 치면

이걸 누르시면 됩니다.

왼쪽 탭에서 Repositories에 들어간 다음 리포지토리 생성 누르시고 프라이빗으로 만드시면 됩니다. 이름은 자유롭게 해세요.
6. 도커 빌드, 이미지 태깅, ECR 로그인, 이미지 푸시
#Docker Image Build
docker build -t ${IMAGE_NAME} . // .(온점) 반드시 치셔야합니다.
#Tagging Image
docker tag ${IMAGE_NAME}:latest ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}
#ECR Login
aws ecr get-login-password --region ${REGION} --profile ${PROFILE_NAME} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
#Image Push
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}
${IMAGE_NAME} : 우리가 빌드할 이미지를 입력합니다.
${ACCOUNT_ID} : IAM의 계정 ID를 입력하는데

이렇게 치시면 account id를 보여줍니다. account id는 숫자로된 12자리 배열입니다.
${REGION} : 자신의 지역을 입력합니다. 저의 경우는 ap-northeast-2 입니다.
${PROFILE_NAME} : 아까 aws configure --profile ${profile_name} 에서 쳤던 프로필 이름을 입력합니다.
${REPOSITORY_NAME} : ECR 에서 생성한 리포지토리 이름을 적습니다.
위의 명령어를 모두 치면?

이렇게 도커 이미지가 들어오게 됩니다.
마치며
이렇게 도커 이미지를 ECR에 저장하는 방법에 대해서 알아봤습니다.
사실 단일 인스턴스에 프로젝트 규모도 크지 않다면 굳이 이용하지 않아도 되는 기능입니다. 물론 제 프로젝트도 단일 인스턴스에 프로젝트 규모가 크지 않아서 굳이 사용하지 않아도 됩니다.
하지만 저는 제 프로젝트를 MSA로 확장하고 싶은 욕구가 있기 때문에 MSA로 전환하기 위해서는 ECR의 존재가 반드시 필요했습니다.
ECR은 반드시 사용하지 않아도 되는 기능이기 때문에 사용하시는데 있어서 잘 선택하시기 바랍니다.
긴 글 마쳐보도록 하겠습니다. 오늘도 즐거운 하루 되세요~
리눅스 기본적인 명령어
이번 포스팅에선 리눅스에서 사용하는 기본적인 명령어에 대해서 정리해볼까합니다.
바로 보시죠
리눅스 명령어
pwd
현재 작업중인 디렉토리 정보를 출력합니다.
$ pwd
/home/itholic
cd
경로 이동, 절대 경로와 상대 경로 모두 가능합니다.
$ cd /home/itholic/mydir
$ pwd
/home/itholic/mydir
ls
디렉토리 목록을 확인합니다.
$ ls
testfile1 testfile2 testfile3
cp
파일 혹은 디렉토리를 복사합니다. 디렉토리를 복사할 때는 -r 옵션을 줘야합니다.
$ cp testfile1 testfile_cp
$ ls
test dir testfile1 testfile_cp
mv
파일 혹은 디렉토리를 이동시킬 때 사용합니다. 진짜 이동시킬 때도 사용하지만 이름을 변경하는 용도로도 사용합니다.
$ ls
testdir/ testfile_mv
$ mv testfile_mv testdir/
$ ls
testdir
혹은
$mv testfile_mv testfile
이렇게 하면 이름 변경 됨
mkdir
디렉토리 생성하는 명령어입니다. -p 옵션을 주면 하위 디렉토리까지 한번에 생성할 수 있습니다.
$ ls
testfile
$ mkdir testdir
$ ls
testdir/ testfile
$ mkdir -p a/b/c/d/e
이렇게 하면 하위 디렉토리까지 전부 만들어줌
rm
파일이나 디렉토리를 삭제하는데 사용합니다. 디렉토리를 삭제할 때는 -r 옵션을 줘야합니다. -f 옵션을 주면 사용자에게 삭제 여부를 묻지 않고 바로 삭제할 수 있습니다.
$ ls
testdir/ testfile1 testfile2
$ rm -f testfile1
$ ls
testdir/ testfile2
$ rm -rf testdir/
$ ls
testfile2
touch
파일이나 디렉토리의 최근 업데이트 일자를 현재 시간으로 변경합니다. 최근 업데이트 일자는 ls -l 명령을 통해 확인할 수 있습니다.
$ touch testfile1
이렇게 하면 현재 일자로 바뀜
cat
단순히 파일의 내용을 출력할 수 있습니다. 파일을 여러개 합쳐서 하나의 파일로도 만들 수도 있죠. 기존 한 파일의 내용을 다른 파일에 덧붙일 수도 있습니다.
$ ls
file1 file2 file3
$ cat file1
1
$ cat file2
2
$ cat file1 file2 > file1_2
$ ls
file1 file1_2 file2 file3
head
파일의 앞부분을 보고 싶은 줄 수만큼 볼 수 있습니다. 옵션을 지정하지 않으면 파일 상위 10줄을 보여줍니다.
$ head -3 testfile
1
2
3
tail
파일의 뒷부분을 보고 싶은 줄 수만큼 보여줍니다. -f 옵션을 주고 실행하면 파일 내용을 화면에 계속 띄워주고 파일이 변하게 되면 새로운 업데이트된 내용을 갱신해줍니다. 주로 실시간 내용이 추가되는 로그파일을 모니터링할 때 유용하게 사용한다고 합니다.
$ tail -f testfile
find [검색경로] -name [파일명]
특정 파일이나 디렉토리를 검색합니다. 파일명은 직접 풀 네임으로 입력해도 되지만 특정 조건을 적용해 검색할 수도 있습니다.
$ find ./ -name 'file1'
./file1
$ find ./ -name "*.jpg"
./picture1.jpg
./picture2.jpg
./picture3.jpg
./picture4.jpg
AWS EC2 용량 부족할 때 대처법
프로젝트를 진행하다가 EC2 용량이 꽉차서 조금 당황했습니다. 어라.. 미디움이면 충분하다고 했는데...
네 생각보다 많은 이미지와 꽤 큰 프로젝트 용량 덕분에 용량이 다 차버려서 진행이 안됐습니다. 때문에 구글링한 결과를 공유하려고 합니다.
EC2 용량 부족시 해결법

먼저 용량이 얼마나 남았는지를 봅니다. 7.6기가가 꽉 찼네요.

EC2 인스턴스에 들어가서 인스턴스 클릭 후 스토리지를 클릭합니다.

볼륨 ID를 클릭해주고 볼륨을 우클릭 후 수정 후 원하는 사이즈로 적습니다.

lsblk 명령어를 치면 xva 스토리지는 12기가로 늘어났지만 우리가 사용하는 xvda1 섹션이 아직 7.9기가입니다. 이걸 늘려줘야합니다.

위의 명령어를 치시구요.

이렇게 치면?

짠 Size가 12기가로 늘어났습니다.
여기까지 EC2 용량 부족시 해결 방법에 대해서 포스팅해봤습니다. 정말 간단하니 용량이 부족하시면 당황하지 마시고 따라해보세요!
출처
https://velog.io/@hyeonseop/ec2-%EC%9A%A9%EB%9F%89-full%EC%9D%BC-%EB%95%8C-%EB%8C%80%EC%B2%98%EB%B2%95
AWS IAM
프로젝트를 진행하면서 AWS에 접근하기 위해서 IAM이라는 것을 설정해본 기억이 한번쯤 있으실 것입니다. IAM은 별 내용이 없어서 지나치기 쉽지만 꽤 심오한 내용을 담고 있었습니다.
이번 포스팅에선 AWS의 권한 책임자 IAM에 대해서 알아보고 직접 만들어보기까지 해보도록 하겠습니다.
IAM
AWS IAM은 Identity and Access Management의 약자로 IAM은 AWS의 서비스나 자원을 안전하게 접근하도록 관리해주는 서비스입니다.
IAM을 사용하면 AWS 유저나 그룹을 생성하고 관리할 수 있으며 AWS 자원에 접근하거나 제한하는 권한을 사용할 수 있습니다.
즉, 인가와 비슷하다고 볼 수 있는데요. 어떤 유저에게는 어떤 서비스는 이용할 수 있고, 어떤 서비스는 이용할 수 없도록 막는 인가와 비슷합니다.
하지만 AWS에선 단순히 우리가 스프링 시큐리티같은 것으로 인가를 구현하는 것보다 더 세세하게 구현해놨습니다.
Authentication vs Authorization (인증과 인가)
인증은 신원을 확인하는 것을 말합니다. AWS에서 IAM 유저의 Access Key나 Secret Key를 이용해서 접근하면 인증이 됩니다.
인가는 권한에 대한 것입니다. 우리가 어떤 것을 할 수 있는지를 정하고 우리가 한번 인증을 하면 IAM 정책은 AWS 자원을 이용하거나 이용할 수 없는 행동인지를 결정합니다.
그나저나 자꾸 IAM 유저 IAM 유저 하는데 IAM 유저가 뭔지 잠깐 짚고 넘어가도록 하겠습니다.
IAM 유저?
한명의 IAM 유저는 AWS와 상호작용하는 서비스를 나타내는 엔티티에 가깝습니다. AWS 안에서 한명의 유저는 AWS에 접근할 수 있는 credential과 연관되어있는 식별자라고 할 수 있죠.
IAM 유저는 Access Key나 Secret Key를 가질 수 있고, AWS 서비스들과 프로그래밍적으로 상호작용할 수 있습니다. IAM 유저들은 패스워드를 가지고 AWS를 관리하는 콘솔에 접근할 수 있습니다.
여기서 말하는 AWS를 관리하는 콘솔이란 AWS 사이트가 될 수도 있고, Linux에서 사용하는 CLI가 될 수도 있고, 애플리케이션에서 사용하는 라이브러리가 될 수도 있습니다.
이제 한번 IAM 유저를 만들어보도록 하겠습니다.
실전

먼저 AWS에서 IAM을 검색해서 들어간 첫 화면에서 사용자 > 사용자 추가를 선택합니다.

사용자 이름을 입력하고 다음을 누릅니다.

디폴트 값은 "그룹에 사용자 추가" 입니다. 우리는 직접 정책을 연결할겁니다.
그리고 검색창에 자신이 사용할 AWS 시스템을 적으면 되는데요. EC2와 S3 정도를 선택해보겠습니다.

ec2를 검색하고 아래 나오는 것 중에 AmazonEC2FullAccess 를 선택! 체크박스 체크하고 다시 검색창으로 돌아와서 s3를 검색합니다.

체크까지 진행하고 "다음"을 클릭합니다.

아무 이상 없는지 확인하고 "사용자 생성" 클릭

이렇게 잘 생성된 것을 볼 수 있습니다.
우리가 만든 정책들을 한번 확인해보겠습니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"s3-object-lambda:*"
],
"Resource": "*"
}
]
}
이건 우리가 선택한 S3의 정책입니다. IAM에서 권한은 정책에 의해 정의됩니다. 정책들은 특정한 일련의 행동, 자원, 효과들을 나타내는 JSON 문서이죠. 우리가 유저, 그룹, 역할들의 정책에 접근할 때 AWS는 이 정책에 근거하여 행동을 허락할지 제한할지를 결정합니다.
그래서 이 정책들을 커스터마이징할 수 있는데요. 자신이 원하는대로 정책을 커스터마이징해서 원하는 행동, 원하는 자원, 원하는 효과를 선택할 수 있습니다.
그래서 S3와 같은 AWS 서비스들은 그들만의 고유하고 세밀한 정책들을 만들기위한 일련의 행동, 자원, 컨디션 키들을 가지고 있습니다.
이 말은 예를 들어서 우리가 특정 S3 버킷안에서 객체들을 저장할 수 있는 유저의 정책을 가지고 있다면, 그 객체들을 저장은 할 수 있지만 삭제는 불가능하도록 설정할 수 있다는 말입니다.
이런 IAM은 인가처럼 특정 권한들을 정책이라는 이름으로 특권을 부여해주는 역할인 것 처럼 보안상으로도 꽤 훌륭합니다. 그리고 이런 상황에서 더 보안적으로 뛰어나게 관리할 수 있죠.
보안상 최고의 행동강령
• 항상 최소한의 특권(권한)만을 가지고 있어야합니다. 유저들이나 애플리케이션이 작동하기위해 필요한 최소한의 권한만 부여해야합니다.
• Access Key나 Secret Key와 같은 credential을 주기적으로 교환해야 합니다.
• 아마존 EC2 인스턴스에서 돌아가는 애플리케이션을 위한 IAM role들을 사용해야합니다.
• 보안을 위해 MFA(Multi Factor Authentication)를 추가해야합니다.
마치며
이렇게 IAM에 대해서 알아봤습니다. 만약 프로젝트를 개발한다고 하면 이 IAM이 생각보다 많이 쓰이는 것을 알 수 있는데요.
프로젝트에서 AWS 라이브러리를 끌어다 사용할 때도 필요하고, AWS CLI를 이용해 리눅스 운영체제에서 AWS 자원에 접근해야 할 때도 그렇습니다.
하지만 저처럼 헬로월드 수준에서는 그냥 root 사용자를 만들어서 돌려쓰기 하면 되겠지만 실전에서는 꽤 중요하게 다뤄질 것 같습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
AWS S3
만약 애플리케이션에서 파일 업로드와 다운로드를 구현할 때 한번쯤 고려해보는 시스템이 바로 S3입니다.
S3란 세개의 S를 말하는데요 Simple Storage Service 이렇게 S3입니다 (갑자기 꽃보다 남자의 F4가 생각나네요).
S3는 그냥 단순하게 클라우드 저장소라고 보면 되는데 구글 드라이브와 다르지 않습니다. 하지만 구글 드라이브와는 다르게 S3는 단순한 저장소 이상으로 다양하게 사용할 수 있습니다.
이번 포스팅에선 S3를 다양하게 사용할 수 있는 방법에 대해서 알아보고 직접 S3를 만들어보고 업로드까지 해보는 시간을 가져보겠습니다.
1. 단순하게 저장소로 사용하는 방법
그냥 단순하게 저장소로 사용하는 방법입니다. 아주 심플하고 간단하죠. 클라우드처럼 저장해놓고 꺼낼 때 꺼내는 방법입니다. 간단하니까 넘어가죠
2. versioning을 하는 방법
여기서 조금 더 나아간 방법이 바로 versioning을 하는 방법입니다. 같은 버킷 안에 여러개의 다른 버전을 가지고 있으면서 버전 관리를 하는 방법이죠.
하지만 이 방법은 그다지 좋은 방법은 아니긴 합니다. 왜냐하면 우리에겐 버전 관리의 끝판왕 깃허브가 있기 때문이죠.
3. 접근 제어와 권한
S3 버킷은 AWS의 IAM을 이용하여 특정한 사람에게만 권한을 부여하여 접근할 수 있도록 설계할 수 있습니다. 이 말은 권한을 받지 못한 사람은 S3에 접근할 수 없다는 말입니다. 이런 인증과 인가를 통해 높은 수준의 보안을 챙길 수 있습니다.
또한 IAM은 굉장히 세부적으로 권한을 컨트롤할 수 있어서 예를 들어 S3 저장소에 업로드는 가능하지만 삭제는 불가능한 IAM을 발급하여 회사에서 결정권한이 없는 직원들에게 부여한다던가 하는 방법이 있습니다.
4. 로깅
애플리케이션을 운영하면서 생기는 로그들을 S3를 이용해서 관리할 수 있습니다.
물론 가격이 부담되긴 하겠지만 보통 로그를 관리하는데 사용하는 ELK 스택들과 다르게 다른 AWS 시스템들(CloudWatch)과 연동이 잘 되기 때문에 애플리케이션을 AWS로 운영한다면 나쁘지 않은 선택일 수도 있습니다.
5. 성능 최적화
만약 애플리케이션에서 아주 무거운 JS파일을 가지고 있어서 이를 로드하는데 큰 부담이 된다면 S3에 무거운 JS파일들을 저장해두고 CloudFront와 같은 CDN을 이용해서 빠르게 JS 파일을 로드해올 수 있습니다.
이건 조금 흥미롭네요. 추후에 CloudFront에 대해서 알아보고 실습도 해볼 생각입니다.
6. AWS 람다와 콜라보
AWS Lambda (이하 람다)는 serverless의 대표주자로서 간단하게 설명하자면 서버와 인프라에 관련된 모든 것들을 AWS에게 전가하고 개발자는 비즈니스 로직에만 신경쓸 수 있도록 하는 것입니다.
이런 람다를 S3의 데이터 저장과 삭제에 대해 트리거삼아 람다에게 명령을 내려줄 수도 있습니다. 예를 들어서 S3에 이미지가 저장되면 그것을 트리거 삼아서 람다를 통해 파이썬 코드로 해당 이미지를 스캔하고 PDF 파일로 변환하는 과정을 거친다거나 하는 방법이 있을 수 있겠네요.
S3 만들어보자!
먼저 S3를 검색하고 나온 페이지에서 "버킷 만들기"를 클릭합니다.



원래 저 부분은 체크가 되어있습니다. 체크를 해제해줍니다.
그리고 "버킷 만들기"를 클릭하면 됩니다.

이름이 중복이라 그래서 새걸로 하나 만들었습니다.
자 이제 우리가 만든 버킷을 클릭해서 들어갑니다.


만든 static 폴더에 다시 들어와서 "업로드"를 누릅니다.

이제 이 화면에 우리가 원하는 업로드 파일을 끌어다 놓고 "업로드" 를 누르면?

이렇게 업로드 된 것을 볼 수 있습니다.
마치며
자 이렇게 우리는 S3를 어떻게 활용하는지 그리고 어떻게 버킷을 만드는지에 대해서 알아봤습니다. S3는 AWS를 이용하는 사람이라면 한번쯤은 써볼만한 중요한 시스템중에 하나라고 생각합니다.
잘만 사용하면 활용도가 굉장히 높으니까 한번쯤 사용해보시는 것도 좋을 것 같습니다.
저같은 경우는 S3를 스프링 프로젝트에서 파일 업로드 다운로드로 많이 사용하고 현재 작업하는 윈도우 컴퓨터에서 EC2인스턴스인 리눅스로 파일을 전송할 때도 S3를 많이 사용합니다.
얼마 전에 올라왔던 "데이터베이스 서버와 애플리케이션 서버를 분리해보자" 포스팅에서도 pem 키를 윈도우에서 리눅스로 옮기는데 이 S3가 사용됐습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
EC2 인스턴스 스토리지 (EBS, EFS)
이번 포스팅에선 EC2의 저장공간에 대해서 알아보도록 하겠습니다. 이번 포스팅은 실습보다는 이론 위주로 공부한 것을 정리해보도록 하겠습니다 (비용이 무서워서...)
AWS의 핵심 기능 중 하나인 EC2의 저장공간 역할을 하는 인스턴스 스토리지는 데스크탑으로 치면 하드디스크에 해당합니다.
하지만 데스크탑과 다른 점은 클라우드 상에서 벌어지는 일인만큼 하드디스크에 접근하기 위해서는 네트워크 I/O가 있어야 한다는 점입니다. 그래서 실제 데스크탑에 물리적으로 연결되어 있는 하드디스크 (HDD, SSD, NVMe) 보다는 속도가 떨어집니다.
하지만 유연하게 저장공간을 설정할 수도 있고 성능도 원한다면 얼마든지 커스텀할 수 있습니다. 이제 본격적으로 EC2 인스턴스 스토리지에 대해서 알아보도록 하죠!
EC2란?
우선 EC2에 대해서 간단하게 짚고 넘어가도록 하겠습니다. EC2는 기본적으로 AWS에게 컴퓨터 한대를 빌리는 것입니다. 가상 컴퓨터라고 해도 좋을 것 같습니다.
우리가 AWS에서 EC2를 선택하고 만들면 선택할 수 있는 것들이 보통 우리가 데스크탑 견적을 짤 때 하는 행동과 비슷합니다. CPU 성능을 고르고 RAM을 고르고 하드디스크를 고르죠.
EC2의 풀네임을 보면 Elastic Compute Cloud로 클라우드 상에서 유연하게 설정할 수 있는 컴퓨터 정도로 해석할 수 있습니다. 그런만큼 정말 유연하게 그때그때 필요할때마다 성능을 바꿀 수 있죠.
나는 작은 프로젝트를 할거라 CPU도 작게 RAM도 작게 설정했다가 갑자기 인기폭발로 많은 사람이 몰리면 더 높은 CPU와 RAM을 설정할 수 있는 것입니다.

EC2는 프리티어에서 제공해주는 micro부터 16xlarge까지 다양한 옵션이 있습니다. 보통 프로젝트를 진행하는 경우에는 medium 이상으로는 가지 않는 것 같지만 필요에 의해 xlarge를 여러개 만들어야 하는 경우 문제없이 컴퓨터를 늘릴 수 있습니다.
EBS
EBS와 EFS는 EC2에 붙어있는 저장공간입니다. EC2에서 CPU와 RAM을 자유롭게 선택할 수 있었던 것 처럼 저장공간도 용량이라던가 IOPS (아이옵스라고 발음합니다. 초당 네트워크 I/O의 줄임말입니다) 를 자유롭게 선택할 수 있죠.
EBS란?
EBS는 USB라고 봐도 무방합니다. EBS는 EC2에 달라붙어서 저장공간을 제공해주지만 필요에 의해 연결을 해제할 수 있습니다.
EBS의 가장 눈에 띄는 특징이라고 한다면 역시 가용영역 (Availability Zone, AZ라고도 합니다) 에 종속되는 것인데요, 예를 들어서 ap-northeast-1에 할당된 EBS는 ap-northeast-2에서 사용할 수 없죠.
그리고 USB처럼 하나의 EBS는 하나의 EC2에만 할당될 수 있습니다. USB도 하나의 USB는 하나의 컴퓨터에밖에 못꼽죠.하지만 반대는 가능하죠. USB도 하나의 컴퓨터에 여러개의 USB를 장착할 수 있듯이 말이죠.
이를 조금 어려운 말로 "EBS는 CCP레벨로써 하나의 EBS는 하나의 EC2에만 연결이 가능하다" 라고 합니다.
우리는 EC2를 만들때마다 스토리지를 구성하게 됩니다.

이렇게 만들어진 볼륨은 루트볼륨이라고 합니다. EC2에서 다운받는 모든 소프트웨어의 저장을 담당합니다.
루트볼륨의 특징은 인스턴스가 사라질때 자동으로 사라진다는 특징이 있습니다. 하지만 이 설정은 종료시 해제 옵션을 비활성화 함으로써 커스텀하게 설정할 수 있습니다.

테스트삼아 루트볼륨과 EBS를 하나 설정했습니다. EC2 인스턴스와 연결된 스토리지들 설정 오른쪽에 보면?

이렇게 종료 시 삭제 옵션이 루트볼륨은 "예" EBS는 "아니요" 로 되어있습니다.
이 옵션은 역시 AWS 콘솔이나 AWS CLI를 이용해 자유롭게 설정할 수 있으며 EBS 또한 EC2 인스턴스가 종료 시 삭제시킬 수도 있습니다.
EBS의 볼륨 유형
EBS는 볼륨 유형이 여섯가지 있습니다. gp2/gp3, io1/io2, st1, sc1 이렇게 여섯가지죠. 각각에 대한 특징을 짚고 넘어가도록 하겠습니다.
gp2/gp3
gp2와 gp3는 일반적인 SSD 성능을 가지고 넓은 범위의 워크로드와 적절한 성능을 보여줍니다. 그리고 가격도 합리적이죠. 이 둘의 차이는 gp2는 스토리지 용량에 따라 IOPS가 오토스케일링 되는 반면 gp3는 용량과 무관하게 커스텀하게 IOPS를 설정할 수 있습니다.
gp2와 gp3의 기본 IOPS는 3천으로 최대 1만6천까지 증가시킬 수 있습니다.
gp3는 커스텀하게 설정할 수 있지만 gp2의 경우 용량에 따라 IOPS가 결정되는 케이스이기 때문에 약 5300GB가 되어야 1만6천까지 성능이 올라갑니다.
io1/io2
io1과 io2는 높은 성능의 SSD 성능을 가지고 있고 높은 수준의 워크로드와 낮은 레이턴시를 위해서 사용하는 SSD 볼륨입니다. 가격은 gp2/gp3와 비교해서 비싼 편이구요. 성능은 당연히 더 좋습니다. io1/io2를 프로비저닝된 SSD 라고도 부릅니다.
만약 IOPS를 1만6천 이상 요구하는 EC2 인스턴스인 경우 고려하면 좋은 볼륨이고 AWS에선 데이터베이스를 위한 볼륨으로 적절한 볼륨이라고 소개하고 있습니다.
왜냐하면 io1과 io2는 민감한 데이터에 적합하고 데이터 일관성에 초점을 맞춘 볼륨이기 때문인데요. 제 생각엔 그냥 대놓고 데이터베이스 서버에 쓰라고 만든 볼륨인 것 같네요.
io1과 io2의 IOPS는 최대 3만2천까지 높일 수 있습니다. 하지만 여기서 EC2 인스턴스의 종류를 Nitro로 설정한다면 최대 6만4천까지 올라가는 것이 특징입니다.
io2 중에서도 Block Express라는 단위의 볼륨이 있는데 이는 최고 IOPS가 25만6천까지 올라가는 최고 성능의 EBS 볼륨입니다.
st1
st1은 낮은 HDD 볼륨으로 디자인 되어있고 자주 접근하거나 높은 수준의 워크로드를 위해 사용하는 볼륨입니다. 즉, 네트워크 I/O는 적지만 많은 용량이 필요한 경우 사용하는 볼륨이라고 생각하시면 됩니다.
sc1
sc1은 가장 적은 비용으로 생성할 수 있는 EBS 볼륨입니다. 비용이 부담되는 경우 선택할 수 있는 볼륨입니다.
하지만 EC2 인스턴스에선 기본적으로 st1과 sc1에 호환되지 않습니다. 때문에 기본적으로 EC2를 생성한다면 gp2로 설정되고 원한다면 gp3, io1/io2로 설정할 수 있습니다.
EBS 인스턴스 스토어
EBS 볼륨에서 선택할 수 있는 IOPS중 가장 높은 수치인 25만6천보다 더 높은 IOPS를 요구하는 인스턴스인 경우엔 어떡할까요?
그 땐 EBS 인스턴스 스토어를 이용하면 됩니다. 인스턴스 스토어는 엄청나게 높은 IOPS를 제공하는데 최대 330만까지 성능을 끌어올릴 수 있습니다.
하지만 데이터 유실의 가능성이 있고 인스턴스가 없어지면 데이터도 같이 사라진다는 특징이 있습니다.
개인적으로 인스턴스 스토어는 좋긴 하지만 이정도로 빠른 IOPS를 원한다면 차라리 ElasticCache를 이용하는게 낫지않나... 하는 생각이 들긴 합니다.
EBS 스냅샷과 다중연결
EBS는 앞서 설명했듯이 다른 가용영역에 있는 EC2 인스턴스에선 사용할 수 없습니다. 하지만 같은 볼륨을 다른 가용영역에서 사용하고 싶은 경우엔 어떻게 할까요?
그 땐 EBS 스냅샷을 이용하면 됩니다. EBS 스냅샷은 말그대로 그 순간 찰나의 순간을 찍어 따로 저장해두는 형태입니다. 이 스냅샷을 이용해 언제든지 EBS 볼륨을 복제할 수 있습니다.
근데 그럴거면 왜 복제하는거지? 그냥 같은 EBS를 여러개의 EC2에서 사용할 수 있게 하면 안되나?
AWS를 만든 개발자들도 그런 니즈를 느꼈는지 EBS를 다중연결할 수 있는 기능을 제공합니다. EBS 다중연결은 기존 CCP레벨에서 하나의 EBS는 하나의 EC2에만 연결 가능하다는 제한조건을 무시할 수 있는 설정으로서 io1/io2에서만 가능한 설정입니다.
하나의 EBS 볼륨을 여러 EC2에 연결해서 사용할 수 있으며 이렇게 하면 EC2의 가용성이 올라가는 효과를 볼 수 있습니다. EBS 다중 연결은 최대 16개의 EC2에 연결해서 사용할 수 있습니다.

이렇게 연결된 EBS 볼륨은 동시 쓰기와 동시 읽기가 가능해 마치 데이터베이스의 클러스터링처럼 사용할 수 있습니다. 데이터베이스 클러스터링과 레플리케이션에 대해서 궁금하신 분은 아래의 링크를 확인해주세요!
https://coding-review.tistory.com/309
물론 이렇게 설정한 EBS 다중 연결은 가용영역에 국한되어있다는 EBS만의 특징을 해결할 순 없었습니다.
EFS
EFS는 EBS와 다른 특징을 가지고 있습니다. EBS는 다양한 소프트웨어나 데이터를 저장할 수 있었다면 EFS는 파일을 저장할 수 있습니다. 워드프레스같은 파일을 여러 EC2에서 접근하고 싶을 때 사용할 수 있습니다.
EBS는 가용영역에 국한되어 있고 다른 가용영역에서 동일한 EBS를 사용하고 싶다면 스냅샷을 이용해야 했습니다. 하지만 EFS는 여러 가용영역의 EC2에서 접근이 가능하고 그 숫자도 수백개나 연결할 수 있습니다.
하나의 가용영역 한해서이긴 하지만 최대 16개의 EC2와 연결할 수 있었던 EBS와 차원이 다른 확장성을 보여줍니다.
때문에 EFS는 높은 확장성과 높은 가용성이 필요한 경우에 사용할 수 있습니다. 하지만 그렇기 때문에 가격이 정말 무시무시한데 gp2의 약 세배정도 되는 가격을 자랑합니다.
또한 EFS는 리눅스 기반 AMI에만 호환이 가능하여 windows인 경우엔 사용할 수 없는 기능이기도 합니다.
비즈니스상 중요한 워드프레스를 저장하기 위해 EBS에서도 제공하는 KMS를 이용한 암호화를 제공해줍니다.
이렇게 가격이 살벌한 EFS를 아무도 사용 안할것 같았는지 AWS 개발자들은 EFS에 EFS-IA라는 스토리지를 추가적으로 제공해줍니다. EFS-IA를 이용하면 파일의 생명주기를 관리해주고 가격을 절감해주는 효과를 가지고 있습니다.
예를 들어 60일동안 접근하지 않은 데이터를 EFS-IA로 이동시켜 비용을 절감하는 효과를 줍니다.
정리
EBS는 네트워크 볼륨을 한번에 하나의 인스턴스에 연결하고 특정 AZ에 국한되어 있는 경우에 사용하면 좋습니다. 또한, 다양한 볼륨 유형으로 애플리케이션 서버를 위한 gp2/gp3, 데이터베이스를 위한 io1/io2 등 다양한 유형을 제공해줍니다.
EFS는 다수의 인스턴스에 걸쳐 연결해야하는 네트워크 파일 시스템에 적합하고 EBS와는 비교도 안될만큼의 높은 확장성과 높은 가용성을 보여주지만 그만큼 가격이 부담된다는 특징이 있습니다.
EBS의 인스턴스 스토어는 기존 EBS 볼륨 유형 중 가장 높은 IOPS 수치인 25만6천보다 더 높은 IOPS를 원하는 경우 사용할 수 있지만 데이터가 유실될 수 있고 인스턴스가 종료되면 데이터가 같이 날아간다는 위험성이 있습니다.
마치며
요즘 클라우드를 공부하면서 가장 기본이 되는 EC2부터 다시 공부해보자는 생각으로 EC2에 대한 기본적인 개념과 EBS, EFS에 대해서 알아봤습니다.
사실 EC2라는 것은 단순히 가상 컴퓨터만을 의미하는 것은 아닙니다. AWS EC2에 들어가서 왼쪽 카테고리를 잘 보시면 다양한 기능들을 선택하고 커스텀하게 설정할 수 있습니다. ①인스턴스, ②이미지 (AMI), ③EBS, ④보안그룹 (방화벽), ⑤로드밸런서, ⑥오토 스케일링 그룹 (ASG)
이렇게 여섯가지를 모두 아우르는 것이 바로 EC2입니다. 하지만 보통 EC2라고 하면 인스턴스를 말하는 것이기 때문에 EC2를 가상 컴퓨터 그 자체로 초점을 맞췄습니다.
사실 인스턴스와 이미지는 딱히 설명할 것도 없고 너무 쉬운 내용이라 따로 포스팅하진 않았습니다. 때문에, 이번 기회에 EC2를 공부하면서 처음으로 EBS에 대해 공부하게 되었습니다.
보안 그룹도 별거 없어서 아마 다음은 로드밸런서, 오토 스케일링 그룹을 차례대로 공부해볼 것 같습니다.
로드밸런서는 몇번 만져봐서 어떤 느낌인지 알지만 오토 스케일링 그룹은 처음이라 어떤 내용이 저를 기다리고 있을지 기대됩니다.
그럼 여기서 긴 글 마쳐보도록 하겠습니다. 오늘도 즐거운 하루 되세요~
Elastic Load Balancer (ELB) : (1)
이번 포스팅에선 AWS의 핵심 시스템 중 하나인 Elastic Load Balancer 줄여서 ELB에 대해서 알아보도록 하겠습니다. ELB는 개발자에게 그리고 AWS에게도 빠질 수 없는 중요한 시스템인데요.
만약 개발자에게 "AWS에서 제공하는 로드밸런서의 기능을 직접 구현해라"라고 하면 구현하는 것도 만만치 않은데다 유지보수 하는것도 쉽지 않을 것입니다.
또한, 기껏 직접 로드밸런서를 만들었다고 하더라도 다른 시스템과 연동하는건 또 다른 이야기이죠. 이 로드밸런서를 각각의 애플리케이션과 연결하고 관리하는건 개발자로 하여금 막대한 기술부채를 안겨다주죠.
AWS도 ELB라는 구현하기 쉽고 유지보수하기 쉬운 시스템을 내면서 다양한 AWS의 시스템과 ELB를 연동시킬 수 있게 함으로써 더욱 다양한 AWS의 시스템을 개발자들이 이용하게끔 유도합니다. 그러면서 수입도 짭짤하게 나오죠.
자 이제 본격적으로 AWS의 ELB에 대해서 알아보도록 하겠습니다.
가용성과 확장성
우선 Availability라고 부르는 가용성과 Scalability라고 부르는 확장성에 대해서 잠시 짚고 넘어가도록 하겠습니다.
먼저 확장성부터 살펴보죠.
확장성
확장성에는 수직적 확장과 수평적 확장이 있습니다. 수직적 확장은 쉽게 설명해서 EC2 인스턴스의 크기를 키우는 것입니다. 만약 인스턴스가 t2.micro로 작동하고 있었다면 이를 t2.large로 바꾸는 것이죠.
이렇게 하면 인스턴스 각각이 수행할 수 있는 능력이 향상될 것입니다. 이것을 예시로 쉽게 설명해보자면 전화업무를 진행하는 A/S 센터에서 신입이 처리할 수 있는 전화업무와 경력직이 처리할 수 있는 전화업무는 크게 차이가 있을건데요.
만약 신입이 5개의 전화업무를 처리할 수 있다면 경력직은 같은시간에 10개의 전화업무를 처리할 수 있을겁니다. 전화업무를 처리하는 개인의 역량이 늘어난 것이죠.

이를 수직적 확장, Scale Up이라고 부릅니다.
수평적 확장은 EC2 인스턴스의 개수를 늘린겁니다. 원래라면 1개에서 처리해야할 부하를 2개, 3개에서 처리하면 그만큼 전체적인 부하가 줄어들겠죠?
전화업무로 다시 예를 들자면 10개의 전화업무를 처리하려면 경력직 한명을 고용하는 것도 방법이지만 신입 두명을 고용하는 것도 방법입니다.

여기까지가 확장성에 대한 내용입니다. 이제 가용성에 대해서 보죠.
가용성
가용성은 확장성과 궤를 같이하는 개념이지만 살짝 다른 핀포인트가 있습니다. 가용성은 장애가 발생했을 때도 시스템이 돌아가는 것에 초점을 둡니다.
만약 전화업무를 처리하는 지부가 서울지부 부산지부 이렇게 두개가 있는 상황에서 문제가 생겨서 부산지부가 먹통이 되는 상황에 놓였다고 해봅시다.
이 때 부산지부는 문제가 생겼지만 서울지부가 아직 살아있기 때문에 전화업무는 그대로 유지할 수 있을 것입니다.
자 가용성과 확장성이 ELB와 무슨 상관이냐 하시겠지만 ELB가 잘하는 업무중에 가용성과 확장성이 들어있기 때문입니다. 이것은 포스팅 마지막 즈음 소개드릴 오토 스케일링 그룹 (ASG)를 설명할 때 본격적으로 등장합니다.
ELB의 종류
AWS에서는 다양한 로드밸런서를 소개하고 있습니다. 대표적으로 소개하는 로드밸런서는
• Classic Load Balancer (CLB) - deprecated
• Application Load Balancer (ALB)
• Network Load Balancer (NLB)
• Gateway Load Balancer (GWLB)
이렇게 네가지를 소개합니다.
하지만 AWS에선 CLB의 사용을 지양하라고 가이드라인이 있는데요. CLB가 버전1이고 뒤에 나온 ALB, NLB, GWLB가 버전 1인 CLB의 속성을 거의 다 포함하고 있기 때문이죠.
그래서 이번 포스팅에서도 CLB는 제쳐두고 ALB, NLB, GWLB에 대해서 설명드리겠습니다.
ALB (Application Load Balancer)
ALB는 말 그대로 애플리케이션을 위한 로드밸런서입니다. 주로 HTTP, HTTPS 트래픽을 분산시키기 위해 만들어진 로드밸런서입니다.
ALB는 OSI 7계층인 애플리케이션 계층에 존재하는 로드밸런서입니다. 7계층 로드밸런서인만큼 어떤 패킷이 오고 가는지 확인해볼 수 있기 때문에 위조/변조된 패킷을 분리해내는데 특화되어 있습니다. 이는 보안이 훌륭하다는 의미이죠.
하지만 뒤에 설명할 NLB인 4계층 로드밸런서에 비해 속도가 느리고 4계층 로드밸런서에 비해 많은 것들을 확인해야하기 때문에 비용이 더 비싸다는 단점이 있습니다.
이 두가지에 대한 차이점은 아래의 링크에 자세히 설명해두었으니 참고해주세요!
https://coding-review.tistory.com/319
NLB (Network Load Balancer)
NLB는 앞서 설명드렸다시피 OSI 7계층 중 4계층 전송계층에서의 트래픽을 담당합니다. 4계층 로드밸런서인만큼 패킷을 확인할 수 없기 때문에 보안에 취약하지만 속도가 매우 빠르고 비용이 저렴하다는 특징이 있습니다.
성능차이는 기본적으로 ALB가 400ms 성능을 보여주는 반면에 NLB는 100ms로 약 네배정도 성능이 뛰어납니다.
때문에 AWS에서도 NLB는 높은 성능을 요구하는 로드밸런서를 위해 만들어진 것이라고 쓰여있습니다.

GWLB
GWLB는 OSI 7계층 중 3계층인 네트워크 계층에서 작동하는 로드밸런서로 AWS에서 소개하는 로드밸런서 중 가장 낮은 계층에서 활동하는 로드밸런서입니다.
참고로 3계층은 IP 패킷이 왔다갔다하는 과정입니다.
GWLB는 들어오는 트래픽에 대해 방화벽을 반드시 통과해야 한다거나 모든 트래픽을 다 잡아내어 분석한다던가 하는 경우에 사용할 수 있습니다.
타겟그룹 (target group)
AWS의 ELB를 사용하면서 가장 많이 보게될 단어인 타겟 그룹에 대해서 잠시 짚고 넘어가도록 하겠습니다. 타겟 그룹은 로드밸런서가 바라봐야할 대상을 가리킵니다.
즉, 타겟그룹은 EC2 인스턴스가 될 수도 있고, 온프레미스 서버가 될 수도 있다는 의미입니다. 보통 저희는 EC2 인스턴스를 바라보게 할것이지만요.
또한, 로드밸런서가 바라보는 대상이 EC2 인스턴스와 온프레미스 그리고 또 다른 로드밸런서일 수 있습니다.
이렇게 로드밸런서를 계층화하면 보안을 더 신경쓸 수도 있고 트래픽을 분석할 수도 있습니다.
실습
간단한 예시와 함께 ELB를 실습해보도록 하겠습니다. 이번 실습은 EC2 인스턴스를 만드는 것에 어느정도 익숙해져있다는 가정하에 진행됩니다. 따라서 생략되는 부분이 있을 수도 있습니다.
1. EC2 인스턴스 생성
우선 EC2 인스턴스를 두개 생성할겁니다. EC2 인스턴스는 다음과 같은 설정을 진행했습니다.
• AMI는 Amazon Linux
• 키페어는 없이 진행
• 보안 그룹은 ssh와 HTTP만 허용
• EBS 루트볼륨은 gp3에 8GB
• User Data에 초기값 설정
다른건 몰라도 User Data는 조금 생소하신 분들이 있을텐데 EC2인스턴스를 만들면 맨 아래 세부사항을 설정할 수 있는 것이 있습니다.


맨 아래 User Data에 해당 내용을 입력하시면 퍼블릭 IP로 들어갔을 때 해당 내용이 출력됩니다.

자 이제 로드밸런서를 만들어봅시다.
2. 로드밸런서 생성
ALB를 선택해주시고 로드밸런서 이름, 네트워크 매핑은 모든 가용영역을 선택해주세요.
그리고 사전작업으로 보안그룹을 만들어줍니다.

HTTP 하나만 인바운드 규칙으로 생성해줍니다.
그리고 우리가 만든 보안그룹을 선택합니다.


그리고 타겟그룹을 만들어줘야합니다. "대상 그룹 생성" 클릭
3. 타겟그룹 만들기

타겟그룹 유형은 인스턴스, 타겟그룹 이름을 입력한 뒤 다음으로 넘어갑니다.

우리가 방금 만든 인스턴스를 모두 선택하고 "아래에 보류중인 것으로 포함" 클릭 그리고 다음을 누릅니다.
이렇게 타겟그룹을 만들었고 다시 로드밸런서 생성창으로 갑니다.

마지막으로 로드밸런서를 생성해주면 끝입니다.
그리고 조금 기다려야하는데 ELB는 타겟그룹이 사용가능한 정상적인 인스턴스인지 확인하기위해 헬스체크 구간을 거칩니다. 이 구간에서 실패가 뜨면 로드밸런서가 정상적인 인스턴스로 판단할 수 없기 때문에 트래픽을 운반하지 않습니다.
헬스체크는 기본값이 루트 주소 (/) 로 가게 되어있고 30초마다 한번씩 헬스체크를 하며 총 5번의 200 상태코드가 내려와야 정상적인 인스턴스로 판단합니다.
이 주기를 짧게 해줄 수 있지만 그냥 기다리면 되는 것이니 일단은 넘어가겠습니다. 그렇기 때문에 조금 기다려야합니다.
이렇게 우리가 만든 타겟그룹을 라우팅해줍니다.

로드밸런서가 프로비저닝 하는 시간도 있기 때문에 조금 기다려주시면 활성화가 됩니다. 그리고 보안그룹에 가보면?

헬스체크까지 완벽합니다.
이제 우리가 만든 로드밸런서 안으로 들어옵니다. 그리고 DNS를 복사합니다.


우리가 작성한 DNS구요 잘 뜹니다. 새로고침을 눌러보면?

네 잘되네요.
포스팅이 너무 길어져서 뒤에 내용은 다음 포스팅에서 진행하도록 하겠습니다. 다음 포스팅에서 봬요!
Elastic Load Balancer (ELB) : (2)
앞선 포스팅과 이어지는 내용입니다.
https://coding-review.tistory.com/460
우리는 인스턴스를 생성했고 로드밸런서를 생성했으며 타겟그룹까지 생성했고 로드밸런서에 타겟그룹을 라우팅하는 작업까지 끝났습니다.
근데 이러고 보니까 조금 문제가 있습니다.
바로 사용자가 로드밸런서에 접근할 때마다 새로운 애플리케이션으로 연결해주는데 로그인을 했는데도 로그인이 풀려버리는 상황이 생겼습니다.
로드밸런서는 잘못이 없습니다. 로드밸런서는 사용자가 로그인을 했건 말건 새로운 애플리케이션으로 보내주는게 정상이니까요.
세션으로 로그인을 유지한다고 한들 A라는 컴퓨터에 세션을 저장해뒀을테지만 B라는 컴퓨터로 로드밸런서가 보내버리면 저장했던 세션은 무용지물입니다.
이런 사용자의 불만을 잠재우기위해 우리는 고정세션이라는 것을 설정해야합니다.
Sticky Session
말 그대로 세션을 고정하는 것입니다. 이건 실습으로 바로 보여드리겠습니다.
우선 타겟그룹 페이지로 이동합니다.

타겟그룹 선택 > 작업 > 타겟그룹 속성 편집
이렇게 누르면 다음과 같은 창이 뜹니다.

그리고 저기 "Turn on stickness"를 클릭하면?

이런 창이 새로 뜹니다.
이 고정세션은 쿠키를 이용해서 세션을 고정시키는 것인데 로드밸런서 생성 쿠키와 애플리케이션 기반 쿠키의 차이는 쿠키 이름을 사용자가 직접 정의해줄 수 있냐 없냐의 차이입니다.
저는 로드밸런서 생성 쿠키로 설정했습니다.
그리고 저장을 누릅니다.
적용되는데 시간이 조금 걸립니다. (1~2분)

이제 아무리 눌러도 하나의 애플리케이션에만 연결된 모습입니다.
참고로 아까 타겟 그룹에 보시면

여기 로드밸런싱 알고리즘을 선택할 수 있는 곳이 있습니다.
번역이 좀 이상한데 최소 미해결 요청은 Least Connection 인 것 같습니다.
라운드 로빈은 정적 로드밸런싱 알고리즘으로서 DNS에 적혀있는 리스트를 보고 순서대로 트래픽을 분산시키는 방법입니다. 때문에 장애가 발생한 애플리케이션에 트래픽을 보낼 수도 있습니다.
Least Connection은 동적 로드밸런싱 알고리즘으로서 트래픽을 애플리케이션에 보내기 전에 미리 헬스체크를 통해 지연시간을 체크합니다. 그리고 그 지연시간이 짧은 애플리케이션 서버쪽으로 트래픽을 보내줍니다.
이런 알고리즘을 바꾸는건 사실 NginX에서도 설정값 하나만으로 바꿀 수 있는 것이지만 ELB는 역시 로드밸런서답게 "딸깍" 한번으로 설정을 바꿀 수 있습니다.
하지만 다양한 로드밸런싱 알고리즘을 자유롭게 커스텀할 수 없는건 조금 아쉽긴 하네요.
로드밸런싱 알고리즘에 대해서 더 자세히 알고 싶으신 분들은 아래의 링크를 참고해주세요!
https://coding-review.tistory.com/319
Cross Zone Load Balancing
이건 말 그대로 가용영역을 뛰어넘는 로드밸런싱을 의미합니다.
ap-northeast-2a에 존재하는 두개의 인스턴스와 ap-northeast-2b에 존재하는 여덟개의 인스턴스가 있습니다.
만약 이 교차 영역 로드밸런싱을 하지 않는다면 ap-northeast-2a에 존재하는 인스턴스에 트래픽이 몰릴 것입니다.
이렇게 되면 한 군데에 더 많은 트래픽이 몰리게 되겠죠.
이를 방지하고자 ALB를 선택하면 가용영역을 뛰어넘는 데이터 이동이 가능해집니다.
ap-northeast-2a로 가야할 트래픽을 ap-northeast-2b로도 보내준다는 것입니다. 사실 AWS에선 가용영역을 넘어서는 데이터 이동은 추가과금이 되도록 설계되어있습니다.
하지만 ALB는 이 Cross Zone Load Balancing이 디폴트 값이기 때문에 과금이 진행되지 않습니다.
물론 다른 로드밸런서인 NLB나 GWLB는 이 설정을 킬 경우 추가 요금이 발생합니다. 물론 추가요금이 발생하는 NLB나 기본적으로 비싼 ALB나 별 차이 없긴 합니다.

로드밸런서를 클릭하고 들어간 다음 속성탭에 들어가보면 위와같이 교차 영역 로드밸런싱이 켜져있는 것을 확인할 수 있습니다.
SSL 인증서
요즘은 SSL이 아니고 TLS라고 해야 맞다고 하던데 저뿐만 아니라 인터넷상의 많은 개발자들이 아직도 이 둘을 묶어 한번에 SSL이라고 많이 부릅니다.
그래서 저도 편의상 SSL이라고 부르겠습니다.
로드밸런서에 SSL 인증서를 설정해두면 이동되는 패킷에 암호화를 진행합니다. 기존 HTTP의 패킷에 SSL을 붙였다하여 우리가 잘 아는 HTTPS 통신이 이루어지는 것입니다.
이 부분의 내용은 해당 포스팅과 너무 벗어난 내용이기 때문에 따로 링크를 첨부해두도록 하겠습니다.
https://coding-review.tistory.com/432
마치며
자 여기까지 EC2의 핵심 시스템 중 하나인 로드밸런서에 대해서 알아봤습니다. 사실 실습을 ALB만 했는데 NLB도 똑같습니다. 조금만 찾아보시면 바로 익숙해질 수 있습니다.
여기서 조금 더 보완을 하자면 기존 EC2의 보안그룹을 HTTP의 80포트를 열어둔 것을 기억하시나요? 이는 EC2 인스턴스의 퍼블릭 IP를 알고있다면 얼마든지 EC2에 접근할 수 있다는 얘기입니다.
이는 보안상으로 그리 좋지못한 방법입니다.
때문에 EC2의 보안그룹에 SSH 통신을 제외하고 보안그룹을 로드밸런서로 설정하게 되면 보안상 더 깔끔해집니다. 이 부분은 따로 실습해보시는 것을 추천드리겠습니다.
이번에 ELB를 본격적으로 공부하면서 원래 알고있었던 내용과 새롭게 알게된 내용으로 알찬 공부가 되었던 것 같습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
AWS로 3 Tier Architecture 고도화하기
이번 포스팅에선 AWS를 다채롭게 사용하여 대부분의 웹을 이루고 있는 3티어 아키텍처를 고도화하는 과정을 포스팅해보도록 하겠습니다.
3 Tier Architecture

3계층 구조라고도 불리는 이 아키텍처는 현재 대부분의 웹의 구조입니다.
이 3티어 아키텍처 말고도 1티어 2티어도 있는데 그냥 티어라는 것은 "서버를 물리적으로 분리했다"라고 생각하시면 됩니다.
1티어 2티어 아키텍처에 대한 내용은 구글링하시면 쉽게 접할 수 있는 내용이기 때문에 이번 포스팅에선 넘어가도록 하겠습니다. 저는 3티어 아키텍처만 집중적으로 파보겠습니다.
클라이언트 계층
흔히 정적 데이터(html, css, js, image)들이 들어가있는 계층으로서 3티어 아키텍처에선 주로 웹서버가 이 자리를 차지하고 있습니다.
웹에선 이 계층을 "프론트엔드"라고도 부릅니다. 프론트엔드에선 UI / UX를 담당하기도 하고 서버로 트래픽을 분산시켜주는 로드밸런싱 역할을 하기도합니다.
그래서 클라이언트계층에서 사용자와 조금 더 가까운 부분과 서버쪽에 조금 더 가까운 부분이 있습니다.
조금 더 잘게 쪼갠다면 이렇게 될 수 있습니다.

서버 계층
흔히 비즈니스 로직이 들어가있는 서버 계층은 클라이언트 계층과 소통하면서 사용자의 요청을 받아 데이터베이스에서 조회하기도하고 데이터를 가공하기도합니다.
이 서버 계층은 보통 WAS (Web Application Server) 라고 부르기도 하고 더 잘 알려져있기를 "백엔드"라고 부릅니다. 제가 주로 공부하는 분야이기도하죠.
클라이언트 계층에서 주로 사용되는 언어는 자바스크립트이지만 서버 계층에서 주로 사용되는 언어는 정말 다양합니다. Java, Node.js, Ruby, Python, C#, Go 등 다양한 언어로 개발할 수 있습니다.
데이터베이스 계층
데이터베이스 계층은 3티어 중 가장 뒤에 위치하고 보통 서버와 소통하면서 데이터를 관리하는 계층입니다.
데이터베이스에는 RDBMS와 NoSQL, IoT라면 RTDB등을 사용하고 어떤 데이터베이스 종류를 사용할 것인지는 팀에서 결정하고 우리의 아키텍처에 가장 잘 어울리는 것을 결정하면 좋을 것 같습니다.
3 Tier Architecture With AWS
3티어 아키텍처에 대해서 대충 훑어봤고 이제 이 포스팅의 핵심 내용인 AWS를 이용해 3티어 아키텍처를 고도화하는 과정을 알아보겠습니다.
하나하나씩 뜯어보면서 고도화해볼까요?
클라이언트 계층

클라이언트 계층에서 AWS는 다양한 서비스들을 제공합니다.
- Route53 -
먼저 제일 앞에는 사용자가 검색창에 google.com을 검색하면 제일먼저 DNS인 Route53을 호출합니다. Route53이 DNS인지 모르시는 분들도 꽤 있더라구요. Route53에서 53이 DNS에서 사용하는 포트번호라서 이름이 Route53입니다.
Route53은 가중치를 두고 동적으로 사용자의 요청을 분배할 수도 있기 때문에 이걸 로드밸런서로 사용할 수도 있다고 들었습니다. 하지만 Route53은 보통 DNS로 사용하는 경우가 일반적이라고 하더군요.
- Web Application Firewall (WAF) -
WAF는 보통은 설계할 때 빼긴 할텐데... DDoS에 위험이 있는 경우에 Route53뒤에 위치하게 해서 DDoS의 위험으로부터 애플리케이션을 지켜줍니다.
하지만 DDoS에 위험이 있는줄 어떻게 아나요 그냥 맞아보기 전까지 모르는거죠. 아무튼 WAF를 이용해서 방화벽을 설치하고 DDoS로부터 안전하게 지켜냅니다.
- CloudFront -
CloudFront는 이름에서도 예측할 수 있듯이 정적 데이터들을 위치시키는 곳입니다. CloudFront는 CDN의 역할도 하기 때문에 빠르게 데이터를 사용자에게 전달할 수 있습니다. CDN에 대해서는 해당 포스팅과 결이 맞지않아 넘어가도록 하겠습니다.
CloudFront는 아까 3티어 아키텍처 설명에서 클라이언트 계층에서 진짜 프론트엔드와 로드밸런서 이렇게 두개로 나눌 수 있다고 말씀드렸는데 바로 프론트엔드에 해당하는 AWS 서비스입니다.
- Application Load Balancer (ALB) -
ALB는 로드밸런서로서 서버로 요청을 분산시키는 역할을 수행합니다. ALB는 로드밸런서 알고리즘 중 Round Robin과 Least Connection 이렇게 두가지를 제공해줍니다. Round Robin은 정적 로드밸런싱 알고리즘, Least Connection은 동적 로드밸런싱 알고리즘이죠. 정적하나 동적하나 이렇게 제공해줍니다.
개인적으로 조금 더 많은 알고리즘을 제공해주면 좋을 것 같은데... AWS의 개발자들이 이거면 충분하다싶어서 이거 두개만 제공해주는거겠죠...
서버 계층

서버계층은 의외로 조금 간단하죠? 하나씩 알아보겠습니다.
- EC2 -
EC2는 우리에게 매우 친숙하죠. 컴퓨터 한대를 AWS에게 빌리는거죠. AMI라는 이미지로 EC2를 언제든지 복제할 수 있다는 장점을 극대화한 ASG를 이용할 수도 있습니다.
- Auto Scaling Group (ASG) -
EC2를 AMI로 이미지화하여 템플릿을 만들어두고 이를 ASG에 등록하면 ASG는 개발자가 설정한대로 EC2를 복제할 수 있습니다.
ASG의 좋은 점은 ALB의 타겟 그룹으로 ASG를 설정할 수 있어서 ASG에 의해 늘어난 EC2를 즉각적으로 ALB에 부착시킬 수 있습니다.
쉽게 설명하자면 ALB가 ASG를 바라보고 있어서 ASG에서 생성되는 EC2를 늘어나는 족족 로드밸런싱할 수 있다는 것입니다.
- Cloud Watch -
Cloud Watch는 AWS의 모니터링 서비스입니다. Cloud Watch에 EC2인스턴스를 등록하고 다양한 메트릭을 모니터링할 수 있습니다. CPU사용량부터 메모리 사용량, 네트워크 I/O 등등 다양한 것들을 모니터링할 수 있죠.
또한 Cloud Watch의 큰 장점은 알람을 보내준다는 것입니다. Cloud Watch Alarm이라는 Cloud Watch속 서브 서비스로서 특정 조건에 EC2 인스턴스가 도달하면 알람을 보내줍니다.
또한 알람뿐만 아니라 이벤트를 걸어줄 수도 있습니다. 이 이벤트를 이용해서 다양한 것들을 할 수 있는데, 예를 들어서, CPU사용량이 80퍼센트에 도달하면 ~행동을 해라 라고 명령할 수 있다는 것이죠.
데이터베이스 계층

데이터베이스도 간단합니다! 한번 알아보죠!
- Relational Database (RDS) -
RDS는 가장 기본적은 RDBMS를 제공해줍니다. 다양한 데이터베이스를 선택할 수도 있고 자동으로 레플리카를 만들어주기도합니다. 또한, 백업본까지 만들어줍니다 (대박)
하지만 제가 생각하는 RDS의 단점은 겁나 비싸다는것... 제가 아직 배움이 부족해 싼 RDS가 있을 수도 있는데 제가 프로젝트 진행하면서 RDS를 알아봤는데 죄다 비싸더군요.
- DynamoDB -
DynamoDB는 AWS에서 제공해주는 NoSQL 데이터베이스입니다. NoSQL 데이터베이스이니만큼 NoSQL의 장점을 그대로 흡수했습니다. 확장성과 유연성이 대단한 데이터베이스입니다.
- Key Management Service (KMS) -
KMS는 AWS에서 키를 관리해주는 서비스입니다. 엥? 키? 하겠지만 이 키를 이용해서 Secret Manager와 함께 사용하면 데이터베이스의 접근이라던가 이런 것들을 더 쉽게 해줄 수 있습니다.
KMS는 AWS서비스 전반에 걸친 키를 관리해주는데 예를 들어서 S3를 보호하는 목적으로도 쓰입니다. 일단 제가 아는건 데이터베이스와 S3와 같은 스토리지에 적용할 수 있는 것으로 알고있습니다. 아마 EFS도 가능할지도?
- Secret Manager -
Secret Manger는 KMS를 이용해서 데이터베이스와 관련된 다양한 비밀정보들을 관리할 수 있게 해줍니다. 예를 들어서 데이터베이스 유저라던가 아니면 다양한 파라미터들을 관리해주는 것이죠.
번외
3티어 아키텍처에서 가장 중요하지만 빠진 것이 있습니다. 바로 네트워크입니다. AWS에선 네트워크를 VPC라는 서비스로 관리하는데 VPC는 private subnet과 public subnet을 이용해서 네트워크를 설정해줄 수도 있습니다.
보통 네트워크는 서버 계층과 데이터베이스 계층을 private subnet으로 두어 외부에서 접근을 막는 것이 일반적입니다. 그래서 ALB를 앞에 두고 이 ALB의 Target Group이 private subnet의 EC2로 향하게 하여 EC2의 접근을 외부에서 막는 것이 좋다고 합니다.
엥? 외부에서 접근을 못하면 서버에 어떻게 들어가요...
바로 바스티온 서버를 두면 됩니다. 바스티온 서버는 외부에 열려있고 내부에 있는 서버로 접근을 위한 용도로 두는 서버입니다. 이렇게 두면 장점이 서버로 들어가는 모든 요청은 ALB를 타고 들어가야한다는 것입니다.
ALB앞엔 앞서 말했다시피 WAF라는 방화벽이 있기 때문에 왠만한 것들은 전부 막을 수 있죠.
정리
자 여기까지 3티어 아키텍처를 AWS를 이용해서 고도화해봤습니다. 제가 써본 것도 있고 안써본 것도 있네요.
개인적으로 AWS의 장점이자 단점이라고 생각하는게 하나부터 열까지 다 세팅해줘야한다는 점입니다. 위의 3티어 아키텍처에서 빠진게 네트워크 설정인데 네트워크 설정까지 직접 개발자가 다 관리해줘야합니다.
3티어 아키텍처만 해도 사용하는 서비스가
• Route53
• Web Application Firewall (WAF)
• CloudFront
• Application Load Balancer (ALB)
• AWS Certificate Manager (ACM)
• Auto Scaling Group (ASG)
• Elastic Cloud Compute (EC2)
• CloudWatch
• Relational Database (RDS) or DynamoDB
• Key Management Service (KMS)
• Secret Manager
우와... 나열하고 보니 엄청 많네요... 여기에 네트워크인 VPC까지 설정해주면 정말 인프라 개발자 죽는 소리가 여기까지 들립니다...
굵직굵직한 서비스만 이정도지 EC2의 Security Group (SG) 라던가, ALB의 Target Group (TG), Listener 등등을 더 추가하면 더 많은 것들을 직접 설정해줘야합니다.
그거까지 나열하면 정말 엄청나게 많네요.
마지막으로 이 세개의 계층을 하나로 보여주는 AWS 아키텍처 그림을 마지막으로 저는 다음 포스팅으로 찾아뵙도록 하겠습니다. 긴 글 읽어주셔서 정말 감사합니다.

클라우드는 어떻게 대세가 되었는가
클라우드는 지난 10년동안 개발자들의 삶에 깊숙히 녹아들었습니다. 이번 포스팅에선 클라우드로 개발되지 않은 환경을 찾는게 힘들게 될 정도로 깊숙히 자리잡은 배경에 대해서 공부한 내용을 공유해보고자합니다.
클라우드의 특징
클라우드의 특징은 크게 성능, 확장성, 가용성, 같은 클라우드 서비스간 통합 이렇게 네가지 키워드로 정리해볼 수 있을 것 같습니다. 하나씩 살펴볼까요?
성능
클라우드가 성능을 논하는게 이상하다고 느끼실 수도 있을 것 같습니다. 보통 클라우드를 떠올리면 확장성과 가용성을 먼저 떠올리곤 하니까요.
하지만 클라우드를 이용함으로써 일반적으로 따로 구성하는 것 보다 더 높은 성능을 끌어올릴 수 있습니다.
1. 하드웨어 최적화
클라우드는 하드웨어를 관리해주다보니 해당 서비스에 최적화된 하드웨어를 가지고 있습니다. 예를 들어서 AWS의 RDS는 데이터베이스를 위한 서버인만큼 IOPS를 프로비저닝할 수 있습니다.
단지 RDS를 고르지 않더라도 EC2인스턴스에서 IOPS에 최적화된 인스턴스를 선택할 수도 있습니다. 이렇듯 하드웨어를 최적화함으로써 성능을 끌어올릴 수 있습니다.
2. Auto Scaling
Auto Scaling 기능은 확장성과 가용성에서 언급되어야할 것 같지만 아시다시피 서버가 늘어나면 하나의 서버가 부담해야할 부하를 분담해서 처리하기 때문에 서버 하나하나의 컴퓨팅 파워가 일정하게 유지되고 이는 성능 향상으로 이어집니다.
3. 네트워크 최적화
VPC와 같은 네트워크 최적화로 같은 AZ (Availability Zone) 안에 있는 서비스들은 네트워크 연결에 필요한 리소스를 최적화할 수 있습니다.
확장성
클라우드의 알파이자 오메가인 확장성에 대해서 말하자면 끝도없죠.
기존 서비스들은 서버를 구하는 비용도 만만치 않았고 유지하는 비용도 만만치 않고, 유연하게 서버를 늘렸다 줄였다할 수 없었기 때문에 스타트업이라는 개념이 쉽게 자리잡을 수 없었습니다.
하지만 클라우드의 등장으로 서버를 유연하게 늘렸다 줄였다할 수 있다는 엄청난 장점과 함께 비용도 쓴만큼만 내면 되기 때문에 새롭게 IT아이템으로 창업하기가 전보다 수월해졌습니다.
그 배경에는 ASG (Auto Scaling Group) 이 있었습니다.
ASG는 CPU의 사용량을 체크하고 있다가 AMI로 똑같은 서버의 이미지를 구워서 복제본을 생성해줄 수 있습니다.
또한, AWS ALB (Application Load Balancer) 의 Target Group에 ASG를 연결할 수 있어서 자동으로 생성되는 서버로 사용자를 유도할 수 있습니다.
우리서버의 성능이 조금 부족한거같다? 그럼 서버의 성능을 한단계 높이면 됩니다. 우리는 A서버는 MySQL을 사용하고싶고 B서버는 PostgreSQL, C서버는 DynamoDB를 사용하고싶다? 그럼 그렇게 하면 됩니다. 유지하는 것도 어렵지 않구요.
가용성
클라우드가 컨테이너와 만나면서 컨테이너 오케스트레이션을 서비스하기 시작한 뒤 많은 기업들이 진입장벽이 굉장히 굉장히 미친놈처럼 높은 쿠버네티스와 같은 오케스트레이션 전략을 사용할 수 있게 되었습니다.
AWS에서는 쿠버네티스를 사용한 EKS (Elastic Kubernetes Service) 와 자체적인 컨테이너 오케스트레이션 서비스인 ECS (Elastic Container Service) 를 이용해서 서버를 더 효율적으로 사용할 수 있게 되었습니다.
16코어 64기가 메모리의 서버에 애플리케이션이 하나만 띄워져 있는 것 보다 4개의 컨테이너로 나누고 이 컨테이너에 각각 4코어 16기가 메모리를 할당하는 것이 성능적으로 훨씬 뛰어납니다.
특히 AWS의 자체 컨테이너 오케스트레이션 전략인 ECS에선 서버를 자동으로 관리해주는 ECS Fargate라는 서비스도 있어서 조금 더 쉽게 접근할 수 있습니다. 물론 가격은 조금 더 나가지만요.
또한, 클라우드는 장애회복에도 특화되어있는데 장애상황에 취약한 데이터베이스의 경우 장애상황에 재시작을 하는 로직이 들어가있습니다.
그리고 백업도 해놓을 수 있죠. 특히 데이터베이스나 인스턴스에 대해서 풀백업과 증분백업을 모두 지원하기 때문에 장애상황에 조금 더 쉽게 대응할 수 있습니다.
다른 서비스와 통합
사실 이게 AWS의 비즈니스 모델인 것 같은데... 하나를 사면 모두 사야하는 기적이 일어납니다.
클라우드로 서비스하는데 규모가 조금 크다?
• EC2는 기본 EBS는 덤
• 데이터베이스는 RDS 혹은 DynamoDB 혹은 EC2에 직접깔기
• 네트워크망은 VPC (살인적인 NAT 게이트웨이의 비용은 덤)
• 장애상황에 서버 죽으면 안되지 안되지 ASG 해야지 (물론 ASG자체는 돈이 들지 않지만 늘어나는 서버의 비용이 있습니다.)
• ASG를 쓰면? ALB도 무조건 따라와야지
• 만약 이미지나 영상을 보관해야한다? S3도 사용
• 데이터베이스 보안 관리 어떻게 할건데? KMS, Secret Manager 사용
• 모니터링과 알람은 CloudWatch
• 컨테이너 오케스트레이션은 EKS혹은 ECS
• 메세지 브로커가 필요하다? SNS혹은 SQS사용
• 영상 스트리밍이 필요하다? Kinesis 사용
간단한것만 적었는데 참 많네요.
아무튼 클라우드를 이용하면 이 모든 서비스와 통합하기 정말 쉽다는 것입니다.
비용이 무지막지한건 어쩔 수 없습니다.
마치며
클라우드를 잘아는 개발자 한명이 있다면 인프라에 관련된 개발자들을 줄일 수 있다는 점에서 대표님들에게도 좋은 솔루션이 될 수 있습니다.
저도 클라우드를 회사에서도 많이 쓰고 공부도 나름 했지만 아직 알아야할게 많아서 좀 막막하긴 합니다.
하지만 공부할게 많이 남았으니 더 열심히 달려봐야죠. 오늘도 긴 글 읽어주셔서 감사합니다. 즐거운 하루되세요!
AWS SQS, SNS
이번에 공부한 것은 AWS SQS인데 SQS를 공부하려면 SNS도 같이 공부할 수 밖에 없더라구요.
이번 포스팅에선 AWS SQS와 SNS를 전체적으로 훑어보면서 다른 메세지 브로커와 어떤 점이 다른지 공부해보고 정리했습니다.
AWS SQS만 가지는 강력한 특징
SQS는 Simple Queueing Service (이하 SQS) 의 약자로 메세지를 큐에 담아서 보내는 여타 메세지 브로커와 비슷합니다. 하지만 SQS가 메세지 브로커냐? 하면 SQS만으로는 메세지 브로커라고 하기 조금 애매할 것 같습니다.
왜냐하면 SQS는 정말 말 그대로 메세지 큐잉 서비스이기 때문입니다. 여타 전통적인 메세지 브로커들처럼 Pub/Sub 구조를 가지고 있지 않다는 얘기입니다.
이게 왜 메세지 브로커와 다르냐면 SQS는 Producer와 Consumer가 1대1 관계만을 가지고 있어서 1대다 구조를 가질 수 있는 Pub/Sub과 다르게 작동하기 때문입니다.
때문에 마이크로서비스에선 하나의 서버에서 여러 서버로 메세지를 전송해야할 일이 잦은데 이때 단순히 SQS만 사용해서는 1:N 연결 구조를 가질 수 없습니다.
그래서 SQS와 SNS를 동시에 사용하는 것인데 SNS는 SQS를 알아보고 바로 다음 챕터에서 정리해보겠습니다.
메세지 브로커
일단 메세지 브로커에 대해서는 짚고 넘어가지 않겠습니다. 간략한 내용은 미약하게나마 아래의 링크에 정리가 되어있습니다.
https://coding-review.tistory.com/504
다른 메세지 브로커와 다르게 SQS만 가지는 강력한 특징에 대해서 한번 읊어보겠습니다.
뭐? 메세지를 무한으로 저장할 수 있다고?
제가 처음 문서를 봤을 때 감정입니다.

제가 읽은 문서인데 간단하게 요약해보면 "SQS의 큐들은 다른 큐에 영향을 받지 않는다. 이 말은 다른 큐에 의해 메세지를 보내는 큐가 느려지는 일이 절대로 없다는 이야기이다. 또한, 하나의 큐가 다른 큐를 막는 현상은 절대로 일어나지 않는다. 이를 AWS SQS 공식 문서에선 SQS는 무한한 개수의 큐를 저장할 수 있다라고 설명한다."
이걸 보고 진짠가..? 싶었습니다. 그렇다고 SQS에 미친 과금을 하면서 테스트를 해볼 수도 없고 참.. 그냥 그렇다니까 그런줄 알고 있어야겠습니다.
아무튼 SQS는 메세지를 무한에 가까운정도로 저장하고 있을 수 있다는 것이 AWS 공식문서의 설명입니다.
SQS는 Serverless의 특징을 가진다.
앞선 특징 때문에 SQS는 Serverless의 특징을 가집니다. 그렇다고 Serverless의 Cold Start 같이 신경써야 하는 것이 있는 것은 아닙니다.
해당 문서에서 SQS가 Serverless의 특징을 가진다는 것은 사용자가 큐잉 서비스를 이용할 때 메세지 브로커의 서버 상태를 고려하지 않아도 괜찮다는 이야기인 것 같습니다.
만약 온프레미스에서 카프카를 이용해서 메세지 브로커를 사용한다면 카프카의 오버헤드나 서버 상태를 고려해야 하는데 SQS는 그렇지 않다는것이죠.
SQS는 자체적인 failover 기능이 있다.
SQS에는 Dead Letter Queue라는 것이 있습니다. 흔히 DLQ라고 줄여 부르곤 하는데 이 DLQ는 요청에 실패한 메세지들을 따로 모아두는 Queue입니다.
AWS는 이 DLQ를 이용해서 요청에 실패한 메세지를 재요청하는 기능을 제공해줍니다. 때문에 SQS에선 자체적인 Fault Tolerance를 가진다는 말이 됩니다.
SQS는 FIFO를 선택할 수 있다.
SQS는 메세지 옵션 중 FIFO를 선택하면 First In First Out을 구현해줄 수 있습니다. 이 설정을 한 SQS 큐는 순서를 보장받을 수 있고 이는 선착순 이벤트같은 용도로 사용될 수 있습니다.
SQS의 Long Polling, Short Polling
SQS는 설정하기에따라 한번에 많은 메세지를 처리할지 조금씩 많이 메세지를 처리할지 선택할 수 있습니다. SQS가 짧게 메세지를 폴링하면 할 수록 데이터를 유실하더라도 최소한으로 유실할 수 있고 그만큼의 데이터 일관성이 보장됩니다.
하지만 짧게 폴링하면 돈이 더 많이 나간다는 사실..!
그럼 어떤 Side Effect가 있을까?
말로만 들으면 정말 엄청난데 어떤 사이드 이펙트가 있을지 살펴보니 SQS는 메세지 크기가 256KB를 넘을 수 없다고합니다. 또한, Pub/Sub 메세징을 지원하지 않고 간단한 메세지 큐잉서비스를 제공해주기 때문에 제약이 많다는 설명도 덧붙였습니다.
하지만 AWS에서도 이를 해결하기위해 다양한 솔루션들을 제공하고 있습니다.
메세지 크기가 256KB를 넘길 수 없는 문제는 S3에 메세지를 등록하고 이 메세지를 전송할 수 있는 기능을 지원합니다. SQS와 S3를 결합하여 해결한 것인데 서비스간 통합이 쉽다는 클라우드의 장점과 맞물리네요.
또한, Pub/Sub 메세징은 SNS를 결합하면서 해결할 수 있습니다. SNS는 가운데 N이 Notification인데 이 Notification은 SMS나 Email서비스를 엔드포인트로 설정하여 다양한 알람을 받을 수 있는 것입니다.
AWS SNS
SQS를 공부하다보면 꼭 SNS얘기가 따라나옵니다. SNS가 뭐길래 SQS와 같이 얘기가 나오는걸까요?
우선 메세지 브로커의 3요소를 먼저 알아볼 필요가 있습니다. 메세지 브로커의 3요소는
• Queue
• Topic
• Event Bus
이렇게가 3요소입니다. 간단하게 살펴보자면
• Queue : 큐는 메세지를 서비스간 이동시키는 큐잉 서비스의 본질이며 메세지를 운반하는 매개체가 됩니다.
• Topic : Topic, 우리말로 주제는 메세지가 어디로 가야할지 방향을 정해줍니다.
• Event Bus : 메세지 브로커가 Event Driven으로 되어있는 경우 다양한 이벤트에 대한 처리를 진행할 수 있습니다.
이런 3요소가 AWS에 모두 존재합니다. 그리고 이 세개의 서비스를 같이 사용할 때 비로소 메세지 브로커의 역할을 충실히 수행할 수 있는것이죠.
Queue = AWS SQS
Topic = AWS SNS
Event Bus = AWS Event Bridge
이렇게 매칭이 됩니다. Event Bridge는 이번 포스팅에서 다루진 않지만 간략하게 짚고 넘어가자면 AWS CloudWatch의 하위 기능으로서 각종 이벤트들을 처리해주는 역할을 합니다.
뭐 아무튼 SNS는 Topic의 역할을 하기 때문에 메세지가 어디로 가야할지 방향을 제시해준다는 특징이 있습니다. 이런 특징말고 SNS만이 가지는 두드러지는 특징을 한번 살펴봤습니다.
SNS는 천만개 이상의 Subscriber를 가질 수 있다.
SNS는 다양한 엔드포인트를 가질 수 있습니다. 그 엔드포인트가 AWS Lambda가 될 수 있고, SMS, Email Service, SQS가 될 수 있습니다.
여기서 핵심은 SQS가 될 수 있다는 것입니다. 즉, SNS를 구독한 어떤 애플리케이션에 메세지를 보내면 SQS를 이용해서 다른 서비스로 메세지를 보낼 수 있다는 말이 됩니다.
이때, SNS는 공식문서에 따르면 1250만개의 Subscriber를 가질 수 있다고합니다.
굉장히 빠른 메세지 처리 능력
SNS는 메세지 로드밸런서라고 부를 수 있을 정도로 굉장히 빠른 메세지 처리 능력을 보여줍니다. 또한, SNS도 SQS와 마찬가지로 FIFO를 지원하기 때문에 데이터의 순서를 보장받을 수 있습니다.
이렇게 좋아보여도 Side Effect는 존재한다.
SNS는 굉장히 좋아보이지만 SQS와 다르게 자체적인 failover가 존재하지 않습니다. 즉, 메세지의 요청이 실패하면 메세지가 사라지게 되는 것이죠.
공식문서에선 이러한 문제에 대해 "SNS는 반복적이고 동일한 메세지를 처리할 때 용이하다." 라는 언급이 있었습니다.
이것에 대한 AWS의 솔루션은 SQS의 DLQ를 이용하라는 것 같은데 사실 복잡해보여서 패스했고... 저는 두 번째 방법인 애플리케이션에서 자체적인 retry 전략을 가져가는 것이 더 편해보였습니다.
스프링 부트의 경우 @Retryable이라는 어노테이션을 통해 SNS 주제로 메세지를 보낼 때 Exception이 발생하면 특정 패턴에 맞춰 재시도를 할 수 있는 기능을 제공합니다. 이 @Retryable 어노테이션은 스프링 AOP를 이용해 재시도 전략을 제공해주죠. (젠장, 또 AOP야)
마치며
확실히 SQS와 SNS는 클라우드라서 가지는 장점을 그대로 계승한 것 같이 보였습니다. SQS의 Serverless적인 특징도 그렇고 무한한 메세지를 저장할 수 있다는 것이 바로 그렇죠.
하지만 만약 애플리케이션이 클라우드 환경에 있고 Kafka 서버를 관리하는 리소스를 다른 곳에 투자하고싶다면 SQS와 SNS를 이용하는 것이 좋은 방안이 될 수 있을 것 같습니다.
오늘도 긴 포스팅 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
출처
https://blog.consol.de/software-engineering/cloud-native/aws-sqs-vs-traditional-message-brokers/
https://medium.com/towards-polyglot-architecture/aws-sqs-sns-event-bridge-when-to-use-what-bc0b9b0e9f4d
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-s3-messages.html
https://stackoverflow.com/questions/31752503/what-are-the-possible-use-cases-for-amazon-sqs-or-any-queue-service
클라우드 데이터베이스의 미래 AWS Aurora DSQL
오랜만에 제가 좋아하는 데이터베이스 분야에서 신기술이 나왔습니다. 처음엔 영상으로 접했는데 그냥 AWS에서 신기술 냈구나 정도로 생각했지만 계속 영상을 보면서 흥미를 가질 수 밖에 없었습니다. Serverless RDBMS라고? 이게 무슨 혼종이지? 싶었죠.
오늘은 AWS Aurora의 후속 버전, 클라우드 데이터베이스의 미래 AWS Aurora DSQL에 대해서 공부한 내용을 정리해보는 포스팅을 작성해보려고합니다.
AWS의 클라우드 데이터베이스 역사
AWS는 처음엔 RDS를 선보였습니다. 이는 단순히 오픈소스로 되어있는 데이터베이스 엔진을 이용해서 RDBMS를 구축한 것에 불과했죠. AWS RDS를 시작으로 점점 사용자들은 다양한 니즈가 생기기 시작했습니다.
2009년에 RDS가 선보인 것을 시작으로 2011년 아이폰의 엄청난 보급으로 어플에 대한 니즈가 생기기 시작하면서 NoSQL이 급격히 부상하게 되었고 이에 맞춰 AWS에서는 DynamoDB라는 Serverless NoSQL을 선보이게 되었죠.
이후 스마트폰과 어플의 보급으로 기존과 비교할 수도 없을 정도의 빅데이터가 쌓이게 되었고 사용자의 패턴을 분석하거나 개인화를 하는 과정에서 통계쿼리가 필요했지만 이미 엄청나게 거대해져버린 데이터베이스들은 통계쿼리에도 엄청난 리소스를 먹기 시작했습니다. 이런 니즈가 AWS Redshift를 런칭하게 되었죠.
NoSQL이 흥하긴 했어도 높은 일관성을 보여주는 RDBMS에 대한 니즈도 착실하게 늘어나기 시작했습니다. 이에 AWS는 운영 복잡성과 안정성을 높이면서도 높은 성능을 요구하는 니즈에 발맞춰 AWS Aurora를 런칭했습니다.
RDBMS의 한계
RDBMS는 기본적으로 CAP중 CP에 가깝습니다. PostgreSQL의 경우 데이터의 일관성이 깨질 것 같으면 아예 노드를 내려버려서 쓰기를 아예 막는 경우도 있고 대부분의 RDBMS 진영에선 마스터 노드가 죽으면 재빠르게 슬레이브 노드가 승격되면서 쓰기 작업을 이어나가야했죠.
또한, 트랜잭션으로 일관성을 보장하기 때문에 데이터의 일관성을 위해 행단위의 락을 걸어야했고 뒤에 도착한 트랜잭션은 이 락이 해제될 때까지 기다려야했습니다. 이는 쓰기 작업에서 지연시간도 생기고 처리량도 높아지기 힘들었죠.
그래서 RDBMS를 사용한다면 읽기 처리량은 슬레이브 노드를 확장하거나 캐싱을 통해서 낮은 지연시간과 높은 처리량을 보여줬지만 쓰기 작업은 읽기에 비해 무겁고 인덱싱이 되어있다면 더 무거워지죠. 그리고 앞서 언급했듯이 락을 획득해야했기에 지연시간도 높고 처리량도 낮았죠.
그래서 보통 이런 대규모 서비스에선 쓰기 작업은 비동기로 처리함으로써 클라이언트 입장에선 금방 작업이 된 것처럼 보이지만 사실 뒤에선 Kafka나 RabbitMQ, Redis Stream같은 비동기 메세지 큐 (이벤트 드리븐 아키텍처) 를 이용해야했습니다.
하지만 이는 운영 복잡도를 엄청나게 늘리는 계기가 되었죠. KRaft를 지원하기 이전의 Kafka는 Zookeeper의 관리도 필요했을 뿐더러 KRaft로 높은 처리량을 가져갈지라도 partition과 Consumer Group의 샤딩이 필수였고 샤딩으로 인해 복잡성이 추가로 증가하게 되면서 운영 복잡도는 말도안되게 높아진 상황이 되었죠.
AWS팀에선 이런 상황이 싫었던 것 같습니다. 그리고 RDBMS를 사용하던 개발자들도 니즈가 확실히 생긴 것 같습니다.
아니 쓰기 작업도 읽기처럼 할 수 없어? 읽기 작업은 확장하기도 용이하고 락에 의한 지연시간도 높지 않고 좋던데? RDBMS는 너무 읽기 성능을 올리는데 진심이야. B- tree부터가 읽기 성능을 최적화하기 위한 알고리즘이고 인덱싱도 그렇고 Optimizer도 읽기 성능을 위한거잖아?
그랬더니 결과가 이게 뭐야. 쓰기 작업이 많아지면 TPS가 급격하게 떨어지고 인덱스까지 걸려있으면 성능이 더 떨어지니 쓰기 작업이 많은 서비스들은 죄다 NoSQL로 갈아타고 있잖아. 쓰기 작업이 많아지면 비동기 메세지 큐를 도입해야하고 운영 복잡도도 너무 높아져.
AWS Aurora DSQL은 여기서 출발합니다.
AWS Aurora DSQL

일단 아키텍처를 던져보고 시작하겠습니다. 기존 Query Processor 와 Storage 사이에 뭔가 덕지덕지 많은게 추가됐습니다.
하나씩 풀어보겠습니다.
• Query Processor : 쿼리를 실질적으로 실행하기위한 엔진이 여기 들어가게 됩니다. 쿼리를 해석하고 실행하기위해 다음 단계로 넘기는 역할이죠.
• Adjudicator : 이친구는 트랜잭션이 충돌인지 아닌지를 판단하는 간단한 컴포넌트입니다. 해당 트랜잭션이 충돌이 아니라면 트랜잭션의 커밋을 확정짓고 Journal이라는 객체에 값을 전달해줍니다.
• Journal : Journal은 트랜잭션의 단위이고 Crossbar로 쿼리를 전달하게 되는데 이 Journal에 해당하는 우리가 잘 아는 개념이 바로 Kafka에 대응되는 개념입니다. 물론 정확히 100퍼센트 일치하는 개념은 아니지만 Adjudicator가 판단한 커밋된 쿼리가 날아가면서 어떤 쿼리가 실행됐는지 적는 "로그 시스템" 이기 때문에 중간 다리로서의 분산된 형태의 로그 기반 시스템이라는 것이죠.
• Crossbar : Crossbar는 스토리지와 연결되어있고 Crossbar는 Range 기반 샤딩이 되어있고 각 범위에 키를 구독하고 있는 스토리지에 쓰기 작업을 넣어주는 역할을 합니다.
DSQL은 동시성 문제를 읽는 시점이 아닌 커밋 시점에 판단하고 이를 뒤로 미룸으로써 성능을 가져오는 아키텍처를 선택했습니다.
이건 흔히 RDBMS가 MVCC (Multi Version Concurrency Control / 다중 버전 동시성 제어) 를 채택하는 것과 다르게 OCC (Optimistic Concurrency Control / 낙관적 동시성 제어) 를 채택한다는 것이 특이한 점인데요.
AWS측에선 OCC를 활용하는만큼 애플리케이션 레벨에서 트랜잭션 충돌로 인한 예외 발생시 재시도하는 로직이 반드시 필요하다고 말하고 있습니다.
이렇게 아키텍처를 설계하면 어떤 이점을 가지게 될까요?
• 낮은 지연시간, 높은 처리량이 가능하다는게 확실히 두드러지는 장점입니다.
• 보통 읽기와 쓰기 중 하나의 성능을 높이고 싶은 경우 각각을 확장시킬 수 없고 양쪽 다 확장하는 수직적인 확장만이 이를 가능케 했지만 읽기와 쓰기를 나눔으로써 각각 다른 확장이 가능하다는 것이 큰 장점입니다.
• Serverless라면 cold start의 문제가 있는 것을 미리 Firecracker 기반의 Mircro VM을 미리 만들어두어 cold start를 최소화했기에 스파이크성 트래픽이 아니라면 성능이 유지된다는 것이 특징입니다.
• 이미 Adjudicator에서 트랜잭션의 충돌을 검사하기에 커밋이 확실한 것들만 Journal로 넘어가고 Crossbar를 구독하고 있는 스토리지는 커밋이 확정된 쿼리만 스토리지에 전달하면 되기 때문에 분산 합의 알고리즘 (Paxos, Raft 등) 이 필요 없습니다.
• 비동기 인덱스를 설정할 수 있다. (이 이야기는 조금 뒤에서 풀겠습니다.)
Serverless RDBMS
여기서 Serverless에 대한 내용을 얘기를 하지 않았는데 기존 Micro VM은 Lambda에서 활용되었던 기술을 사용하는 것이고 기존 Lambda에서 문제였던 cold start 문제는 로직이 실행될 때 VM이 실행되는 과정에서 구동되는데까지 걸리는 시간 (애플리케이션이 뜨는 시간) 을 의미하죠.
DSQL도 Micro VM을 사용하지만 쿼리한번에 VM이 뜨는 cold start가 걸리면 성능에 지대한 영향을 미치기에 미리 VM을 만들어두어 쿼리 성능을 유지시키는 방향을 생각했다는 것이죠.
이는 우리에게도 흔한 개념인 커넥션 풀을 클라우드식으로 구현했다고 볼 수 있을 것 같습니다.
Serverless인 것과 별개로 full managed 모델이기 때문에 원래라면 개발자가 직접 Auto Vacuum을 설정해줘야 하지만 DSQL에서는 자동으로 Vacuum이 동작합니다. 물론 원래의 PostgreSQL처럼 유연하게 Auto Vacuum을 몇초에 한번씩 할건지 설정할 수는 없겠지만 Auto Vacuum에 대해 신경쓰지 않아도 되니 굉장히 편할 것 같습니다.
또한, 그렇기 때문에 인덱싱 과정도 자동으로 관리해주는데요. 기존의 PostgreSQL은 CLUSTER 명령어를 이용해서 테이블을 주기적으로 정렬해주는 작업이 필요했습니다. CLUSTER 명령은 딱 한번만 수행되기 때문에 이후에는 정렬되지 않은 값들이 들어오게 되는데 이 때문에 지속적으로 CLUSTER 명령어를 실행시켜야만 했습니다.
하지만 DSQL은 새로운 데이터가 들어올 때마다 자동으로 정렬해주기 때문에 테이블을 주기적으로 정렬해줄 필요가 없습니다.
이것이 장점인지 단점인지는 확실히 알 수는 없지만 AWS팀에서 선택한 것이니 합리적인 의사결정에 의해 내려진 판단이라고 생각하고 저는 넘어갈 생각입니다.
분산 합의 알고리즘
분산 합의 알고리즘이 필요한 이유가 쓰기 작업만 전문으로 하는 마스터 노드가 장애 발생 시 슬레이브 노드가 이를 빠르게 감지하고 바로 마스터 노드로 승격해 쓰기 작업을 이어나가야 하기 때문에 반드시 필요한 개념이었습니다.
하지만 애초에 DSQL은 Adjudicator에서 충돌을 감지하고 커밋이 확실한 쿼리만 Crossbar로 넘어가고 이를 구독한 스토리지가 그냥 받아서 쓰기만 하면 되기 때문에 분산 합의 알고리즘이 필요가 없는 것이죠.
비동기 인덱스
보통 RDBMS에서 인덱스를 설정할 때는 테이블 단위의 락이 걸립니다. 이 때 트랜잭션이 쓰기 작업을 혹은 읽기 작업을 위해 트랜잭션이 접근하면 락을 놓을 때 까지 (인덱스가 설정될 때 까지 / 인덱스를 건 컬럼이 정렬될 때까지) 트랜잭션이 접근하지 못하기 때문에 곧바로 성능 저하가 생깁니다.
하지만 Aurora DSQL의 경우 OCC를 기반으로 하고 있기 때문에 락을 굳이 걸 필요가 없습니다. 만약 인덱스를 설정하고 있는 과정에서 트랜잭션이 접근하면 그냥 예외를 던져버려서 재시도를 유도하면 되기 때문이죠.
물론 이 과정에서 애플리케이션은 재시도를 해야 하기 때문에 부하가 생길 수도 있는 것이 단점이겠네요.
AWS Aurora DSQL의 단점
와 진짜 DSQL 만능이네요 무조건 써야겠네
확실히 신기하고 매력적이긴 하지만 DSQL에도 단점이 있습니다.
1. 외래키 제약조건
DSQL은 외래키 제약조건을 사용할 수 없다는 것이 단점입니다. 물론 서비스를 운영할 때는 외래키는 논리적으로만 설정하지 실제로 물리적인 데이터베이스에 외래키를 직접적으로 맺지 않는 것이 보통인 경우가 많습니다.
그 이유는 외래키 제약조건이 생기면 데이터베이스의 무결성은 유지되겠지만 관리하기가 매우매우 까다로워져 보통 외래키를 직접적으로 맺지 않는 것입니다.
너무 깨끗한 물에는 물고기가 살지 않는 것과 같은 이치이죠.
하지만 그렇다고 외래키 제약조건이 쓸모없는 것은 아닙니다. 상황에 따라선 물을 깨끗하게 유지해야하는 경우도 있기 때문이죠. 깨끗한 물에는 물고기가 안살지만 사람은 깨끗한 물을 마시는것이 좋은 것을 예로 들 수 있겠네요.
이런 점은 아쉽게 느껴지는 부분인 것 같습니다.
2. Auto Increment의 부재
PK에 대해 Auto Increment가 불가능하다는 것이 두번째 단점입니다. AWS측에선 UUID를 PK를 정하는 것을 권장하는데 그 이유가 분산 시스템에 최적화된 데이터베이스를 지향하고 쓰기 작업에도 OCC의 철학을 접목시키기 위해 쓰기 작업에 조금 더 초점이 맞춰진 데이터베이스입니다.
보통 NoSQL진영 혹은 RDBMS에서도 분산 환경이라면 쓰기 작업이 많은 테이블의 경우 랜덤한 PK를 사용하는 것이 일반적인 경우입니다. 이 때 랜덤한 PK를 사용하게 되면 인덱싱하는 과정에서 성능 하락이 불가피하지만 그럼에도 불구하고 분산 환경에서는 한쪽 노드로 요청이 쏠리는 hotspot 문제를 피할 수 있기 때문에 이 방법이 많이 사용되곤 합니다.
만약 단조 증가하는 PK라면 반드시 다음 데이터가 맨 마지막에 위치할 수 밖에 없고 이런 것들이 hotspot을 야기한다는 것이죠.
때문에 분산 환경에서는 쓰기 작업이 많은 테이블의 경우 PK를 랜덤한 값으로, 읽기 작업이 많은 테이블의 경우 인덱싱의 효과를 제대로 볼 수 있게 Auto Increment를 설정하는 편이죠.
Aurora DSQL에서는 Auto Increment를 사용할 수 없는게 단점으로 꼽힙니다. 아무래도 언급했듯이 쓰기 작업에 더 초점이 맞춰져 있기 때문에 분산 환경에 적합한 방식을 채택한 것이죠.
Aurora DSQL에서 Auto Increment를 사용하고싶다면 DynamoDB에 단조 증가하는 넘버링을 적어주고 해당 값을 끌어다 저장하는 방식을 사용할 수 있습니다. 사용해본 사람들의 말로는 꽤나 유용한 방법이라고 하니 이렇게 사용하는 것도 방법일 수 있겠네요.
하지만 개인적으로 아쉬운건 PostgreSQL에 기반하고 있지만 Auto Increment가 안되게 설정한 것은 아쉽습니다. 차라리 개발자가 유동적으로 선택할 수 있게 했으면 어땠을까 하는 아쉬움이 있습니다.
3. PostgreSQL의 호환성 문제
기본적으로 DSQL은 PostgreSQL을 기반으로 만들어졌습니다. 하지만 PostgreSQL과 100퍼센트 호환이 안되기 때문에 만약 PostgreSQL로 이미 서비스를 하고 있는 상황이라면 DSQL로 마이그레이션 하는 것을 조금 진지하게 생각해봐야할 문제입니다.
PG 어드민도 사용할 수 없어서 AWS는 DBeaver같은 GUI 외부 툴을 이용하라고 권장하고 있죠.
이외에도 어떤 부분이 호환되지 않는지에 대해서 공식 문서에서 자세히 다루고 있기 때문에 해당 문서를 보시는 것을 추천드립니다.
https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html
4. JSON, JSONB 타입 저장 불가능
이건 꽤나 아쉬운 점이라고 생각하는데 PostgreSQL이 다른 RDBMS와 다른 점이 JSON타입을 저장할 수 있는 점이라고 생각했는데요. AWS Aurora DSQL의 경우 PostgreSQL 기반이긴 하지만 JSON, JSONB 타입의 데이터를 저장할 수 없습니다.
전체적인 Aurora DSQL의 방향성이 쓰기 작업도 읽기 작업같이 높은 확장성과 높은 유연성을 가지고 싶어하는 철학을 접목시켜서 쓰기 작업과 읽기 작업을 분리해 쓰기 작업에 대한 성능 최적화를 위한 것처럼 보이기 때문에 읽기 작업에서 큰 활약을 보여주는 JSON타입은 저장할 수 없는 것으로 보입니다.
또한, 만약 JSON타입을 저장할 수 있게 해주면 Range 기반의 샤딩을 처리할 때 파티셔닝이 제대로 이루어지지 않는 문제도 있을 것 같긴 하네요.
아니면 개발하는 과정에서 Adjudicator가 커밋할 쿼리를 판단하는 과정에서 JSON타입은 큰 성능 저하와 같은 문제가 있었기 때문에 넣지 않은 것일 수도 있겠네요.
아무튼 AWS 팀에서 밝히지 않았으니 정확한 이유는 알 수 없지만 Aurora DSQL과는 철학이 맞지 않는 것은 확실해보입니다. 하지만 그래도 아쉽네요...
마치며
제가 데이터베이스를 공부하는 것을 좋아하기도 하고 클라우드도 관심이 있는지라 이번엔 정말 재밌게 공부했습니다. 특히 AWS에서 DSQL을 직접 개발했던 개발자의 블로그까지 찾아가서 비하인드 스토리까지 보면서 씹고 뜯고 맛보고 즐길 수 있었습니다.
특히 100퍼센트 JVM기반으로 기획하고 개발하던 도중 100퍼센트 Rust로 변경하게 되는 과정은 정말로 흥미로웠고 역시 AWS가 클라우드의 1등 자리를 지키는 이유가 있구나 싶었습니다.
물론 단점이 없는 것은 아니기에 아쉬운 점도 있었지만 대부분 Aurora DSQL과 철학적으로 맞지 않아 납득이 가는 단점들이라 크게 신경이 쓰이진 않았습니다.
백엔드 개발이라는 것이 보수적이고 격변이랄게 딱히 없는 분야이기 때문에 이것이 장점이기도, 단점이기도 합니다. 이번에 DSQL로 인해 클라우드 네이티브 데이터베이스에 격변이 크게 일어난 것 같은 느낌이 들었습니다.
저도 기회가 되면 한번 사용해볼 기회가 있으면 좋을 것 같습니다. 이번 포스팅은 여기서 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
함께 보면 좋을 글
https://www.allthingsdistributed.com/2025/05/just-make-it-scale-an-aurora-dsql-story.html
> AWS Aurora DSQL 팀의 회고 글
https://brooker.co.za/blog/2024/12/03/aurora-dsql.html
> AWS Aurora DSQL 핵심 개발자의 개인적인 회고
https://brooker.co.za/blog/2024/12/04/inside-dsql.html
> AWS Aurora DSQL 핵심 개발자의 읽기 쿼리 관련 포스팅
https://brooker.co.za/blog/2024/12/05/inside-dsql-writes.html
> AWS Aurora DSQL 핵심 개발자의 쓰기 쿼리 관련 포스팅
Docker 로 MySQL 생성 후 외부 연결
이번 포스팅에선 Docker를 이용해 MySQL을 설치하고 외부 연결까지 해보도록 하겠습니다.
사전 준비로 ec2가 준비되어있어야 합니다. 또한 ec2의 크기를 medium으로 설정하셔야합니다. 그래야 docker를 돌렸을 때 렉이 안걸린다고 하네요.
ec2 설치는 아래의 링크를 확인해주세요.
https://coding-review.tistory.com/160
이제 시작해보죠!
1. Docker 설치
먼저 도커를 설치해야합니다. 그리고 도커 설치하는 김에 docker-compose도 같이 설치하겠습니다.
sudo apt-get install docker
sudo apt-get install docker-compose
2. MySQL 이미지 다운로드
아래의 명령어를 입력해 MySQL 8.0.17 태그 이미지를 다운로드합니다.
docker pull mysql:8.0.17
권한 없다고 뜨면 맨 앞에 sudo 붙여서 해보기
정상적으로 다운로드가 되었다면 아래의 명령어를 입력했을 때 MySQL이 잘 보일겁니다.
docker images
안되면 sudo 붙여보기
3. Docker MySQL 컨테이너 생성 및 실행
이제 Docker에 MySQL 컨테이너를 생성하고 실행할건데 여기서 주의할 점은 docker에 MySQL과 같은 DB를 설치하는 경우 컨테이너 삭제와 함께 데이터도 날아가므로 저장소는 반드시 외부 저장소를 사용합니다.
(sudo) docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --name capston-mysql -v /Users/capston/datadir:/var/lib/mysql mysql:8.0.17 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
조금 긴데 한번 잠깐 설명해드리자면
• MYSQL_ROOT_PASSWORD 이 뒤에는 자신이 원하는 DB의 패스워드를 입력합니다.
• --name 뒤에는 자신의 데이터베이스 이름을 적습니다.
• -v 뒤에는 외부 저장소를 만들고 그 저장소에 /var/lib/mysql 의 데이터를 바인딩하겠다는 것입니다.
• --character-set-server=utf8mb4 --collation-server-utf8mb4_unicode_ci 는 적지않으면 한글이 깨지기 때문에 반드시 적어줍니다.
이렇게 적는게 역시 난잡하고 힘들기 때문에 우리는 미리깔아둔 docker-compose를 이용해서 적는 방법도 보여드리겠습니다.
먼저 아무 디렉토리나 들어가서 docker-compose.yml 파일을 만듭니다.
mkdir docker-compose.yml
그리고 안에 내용을 집어넣습니다.
version: "3" # 파일 규격 버전
services: # 이 항목 밑에 실행하려는 컨테이너 들을 정의
db: # 서비스 명
image: mysql:8.0.17 # 사용할 이미지
container_name: capston-mysql # 컨테이너 이름 설정
ports:
- "3306:3306" # 접근 포트 설정 (컨테이너 외부:컨테이너 내부)
environment: # -e 옵션
MYSQL_ROOT_PASSWORD: "password" # MYSQL 패스워드 설정 옵션
command: # 명령어 실행
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- /Users/jmlim/datadir:/var/lib/mysql # -v 옵션 (다렉토리 마운트 설정)
yml에 왜 - 가 들어가있지? 싶지만 저렇게 적는게 맞습니다.
저장 후 빠져나온 다음 (esc 누르고 :wq 엔터) docker-compose를 실행시키기만 하면 위의 복잡한 명령어를 줄여줄 수 있습니다.
(sudo) docker-compose up -d
-d 는 백그라운드로 실행 시 옵션
4. docker 컨테이너 목록 출력
(sudo) docker ps -a
5. MySQL 컨테이너에 bash 셀 접속
docker exec 명령을 사용해서 docker 컨테이너에 접근한 다음 MySQL에 로그인합니다.
(sudo) docker exec -it capston-mysql bash
-it 뒤에는 아까 docker-compose에서 적었던 container_name에 있는 것을 적으면 됩니다.
6. MySQL 서버 접속
root@f3af78fa6428:/# mysql -u root -p
mysql>
패스워드는 docker-compose에서 설정한 자신의 패스워드를 입력하면 됩니다.
7. 데이터베이스와 사용자를 생성하고 MySQL에서 권한을 부여한다.
mysql> CREATE USER 'capston'@'%' IDENTIFIED BY 'password';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT ALL PRIVILEGES ON *.* TO 'capston'@'%';
Query OK, 0 rows affected (0.00 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
컨테이너 외부에서도 MySQL에 로그인이 되어야 하기 때문에 원래 'capston'@'localhost' 에서 localhost 부분을 %로 바꿔줍니다.
8. MySQL 설정파일에서 외부 연결 설정 해제하기
bash 셀 상태에서 아래의 명령어를 입력합니다.
vi /etc/mysql/my.cnf
만약 command not found가 뜬다면
apt-get update
apt-get install vim
이렇게 쳤는데도 안된다. 그럼 apt-get update자체의 문제입니다.
E: Failed to fetch http://security.debian.org/debian-security/dists/stretch/updates/main/binary-amd64/Packages 404 Not Found
보통 이런 에러가 뜰텐데 이건 오래된 버전의 stretch를 사용하기 때문입니다.
이때는
sed -i 's/stretch/buster/g' /etc/apt/sources.list
이렇게 명령어를 입력하시고 다시 apt-get update를 하시면 될겁니다.
그렇게 my.cnf 파일에 들어가보면

bind-address << 요게 있으면 주석처리를 해주세요. 알아보니 없는 사람도 있더라구요. 없으면 그냥 빠져나오시면 됩니다.
9. AWS EC2 인스턴스 보안그룹 변경
거의 다왔습니다. 이제 AWS의 인스턴스 보안그룹을 변경해줘야합니다. 모든 지역에서 인스턴스에 접근할 수 있어야하기 때문이죠.

자신의 인스턴스 오른쪽 버튼 > 보안 > 보안 그룹 변경에 들어가보면

자신의 인스턴스와 연결된 보안 그룹이 나옵니다.

왼쪽에 보안 그룹에 들어가면 위와같은 모습인데 아까 확인했던 보안 그룹 이름과 일치하는 보안 그룹을 선택하고 아래의 인바운드 규칙 편집을 눌러줍니다.

그리고 규칙 추가를 두번 해서 하나는 ::/0, 하나는 0.0.0.0/0 을 선택하고 저장 후 나와줍니다.
10. 외부 연결

이렇게 연결해주시면?

이렇게 연결이 된 것을 볼 수 있습니다.
마치며
이번에 도커를 손 대봤는데 정말 어렵네요... 하지만 앞으로 자주 봐야할 것 같습니다.
도커를 이용해 최종적인 목표는 CI / CD 에서 Blue / Green 배포를 진행하는 것입니다. 아직 프로젝트도 못올렸고 elasticsearch도 연결해야 하지만 첫 걸음이 가볍게 마무리되어서 기분이 좋습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
도커 주요 명령어 (컨테이너와 이미지 관련)
이번 포스팅에선 도커의 주요 명령어에 대해서 알아보도록 하겠습니다. 아주 간단한 명령어들만 있으니 후에 추가할 수 있을 것 같습니다.
바로 시작해보죠
컨테이너 관련 명령어
현재 정지된 컨테이너 확인
docker ps -a
현재 동작중인 컨테이너 확인
docker ps
컨테이너 삭제
docker rm ${컨테이너ID}, ${컨테이너ID}
이렇게 복수개도 삭제할 수 있음
컨테이너로 접속
docker exec -it ${컨테이너 이름} bash
이미지 관련 명령어
이미지 보기
docker images
이미지 삭제
docker rmi ${이미지ID}
컨테이너를 삭제하기 전에 이미지를 삭제할 경우
docker rmi -f ${이미지ID}
Docker로 Redis 배포하기
이번 포스팅에선 도커를 이용해서 Redis를 배포하는 방법에 대해서 알아보도록 하겠습니다.
배포는 아래와 같은 순서로 진행됩니다.
• docker, docker-compose 설치
• Redis 이미지 다운받기
• Redis configuration 깃 클론
• docker compose 작성
• docker container 띄우기
• 잘 접속되는지 확인
이번 포스팅은 앞선 포스팅과 마찬가지로 EC2 인스턴스가 있다고 가정합니다. 또한 도커를 돌리기 위해서는 EC2를 medium으로 만드시는 것이 렉 없이 편안하게 구현하실 수 있습니다.
medium은 한달에 5만원정도의 과금이 있습니다.
Redis 배포
1. docker, docker-compose 설치
apt-get update
sudo apt-get install docker
sudo apt-get install docker-compose
설치하는거야 뭐 간단합니다.
2. Redis 이미지 다운받기
docker pull redis:latest
뒤에 latest는 반드시 붙여주셔야합니다. 그렇지 않으면 태그가 none이 되어서 컨테이너 실행이 안됩니다.
아마 태그가 none이 되는 것이 버저닝이 안된 redis 껍데기만 가져오는 것이 아닐까 생각됩니다.
3. Redis Configuration 깃 클론
https://github.com/gingaminga/docker-redis
여기있는 redis.conf를 가져올겁니다. 위의 깃허브를 이용하는 이유는 다른 깃 리포지토리보다 가벼워서입니다.
git clone https://github.com/gingaminga/docker-redis.git
4. docker compose 작성
docker-compose를 작성하기 전에 redis-server를 다운받아줍니다.
sudo apt-get install redis-server
그리고 파일들을 정리해주고 필요한 정보를 얻어냅니다.
$ ls
docker-redis
$ mkdir redis
$ ls
docker-redis redis
$ mv docker-redis redis
$ cd redis
$ cd docker-redis
$ pwd
여기서 경로를 확인해줍니다. 저의 경우는 /home/ubuntu/redis/docker-redis 입니다.
$ cd ..
$ vim docker-compose.yml
yml 파일의 이름은 원하는대로 설정하시면 됩니다. 저는 가시성을 위해 docker-compose로 작성했습니다.
version: "3.8"
services:
redis:
image: redis
container_name: redis_container
command: redis-server /usr/local/etc/redis/redis.conf
hostname: redis
ports:
- 6379:6379
volumes:
- /home/ubuntu/redis/docker-redis/redis.conf:/usr/local/etc/redis/redis.conf
위의 코드는 docker-compose.yml의 내부입니다. 하나씩 설명해보도록 하겠습니다.
version : 도커 엔진에 따라 달라지는 버전입니다. 최신 버전은 3.8을 적으시면 됩니다.
image : 우리가 다운받은 이미지의 이름입니다. 컨테이너가 실행될 때 해당 이미지를 기반으로 컨테이너를 만들어줄겁니다.
container_name : 컨테이너 이름입니다. 이걸 설정해주면 컨테이너를 관리할 때 컨테이너 ID로 귀찮게 하지 않아도 됩니다.
command : 도커 컨테이너가 실행될 때 /usr/local/etc/redis/redis.conf 의 configuration 파일을 기반으로 컨테이너를 만듭니다.
hostname : 호스트 이름입니다.
ports : 해당 포트를 열어줌으로써 통신이 가능하게 합니다.
volumes : 우리가 아까 준비해뒀던 docker-redis의 conf 를 우리가 띄울 redis 컨테이너의 /usr/local/etc/redis/redis.conf에 바인딩시킵니다.
이렇게 docker-compose가 작성되었는데 컨테이너를 바로 띄우지는 않고 우리가 아까 받았던 conf 파일을 조금 수정해줄겁니다.
$ cd redis/docker-redis
$ vi redis.conf
여기 들어가시고 순서대로
• bind 127.0.0.1 이라고 되어있는 부분을 bind 0.0.0.0 으로 바꿔줍니다.
• protected mode yes => protected mode no 이렇게 바꿔주세요.
• requirepass 를 자신이 원하는 패스워드로 변경해주세요. (이 작업을 해야 외부 툴에서 접근이 가능한 것으로 추정됩니다.)
requirepass를 검색할 때 :/requirepass 이렇게 하시면 커서 아래쪽에서 requirepass가 검색됩니다. 반대로 위쪽에서 검색은 :?requirepass 입니다. 앞에 콜론 있습니다!
5. docker container 띄우기
이제 docker-compose를 띄워줍니다.
$ docker-compose -f ./docker-compose.yml up -d
경로 뒤에있는 docker-compose.yml은 자신이 만든 이름으로 넣어주세요.
다음 아래의 명령어로 컨테이너를 확인합니다.
$ docker ps
6. 잘 접속되는지 확인

이렇게 redis-cli 로 접속이 잘 되면 성공한겁니다. 이제 데이터를 넣고 조회까지 해보면 완료되는 모습입니다.
자 이제 모두 마무리되었습니다.
저는 Redis를 이용할 GUI 툴로 Medis를 선택했는데요. 처음부터 이걸 써서 Medis가 친숙하네요. Medis를 설치하는 방법은 포스팅에서 다루지 않습니다. 구글링을 통해 설치해주세요!


우리가 방금 redis-cli에서 넣은 데이터가 잘 들어있네요.
마치며
여기까지 Redis를 도커로 띄워서 배포하고 외부 접속까지 완료했습니다. 제 프로젝트의 가장 핵심이 되는 성능 개선이 Redis를 이용한 캐싱이기 때문에 반드시 필요한 작업이었습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
EC2 Docker + Nginx를 이용해 스프링 프로젝트 배포하기
이번 포스팅에선 EC2 인스턴스를 Docker와 Nginx를 이용해 배포하는 방법에 대해서 포스팅해보도록 하겠습니다.
사실 해당 부분이 전체적인 CI / CD의 파이프라인을 놓고 봤을 때 그리 큰 부분을 차지하진 않습니다. 하지만 배포가 되는 것이야말로 CI / CD의 알파이자 오메가일 것입니다.
사실 저는 모놀리식으로 배포를 한 경험이 있습니다. 이에대한 노고에 대해서는 아래의 링크에 잘 수록되어있습니다.
https://coding-review.tistory.com/160
저때를 회상해보면 정말 고생했습니다. MySQL을 EC2 인스턴스에 설치 했는데 root 권한으로 들어갈 수가 없어서 별에별 삽질은 다했던 기억이 있네요.
MySQL Server를 깔았다가 지웠다가 MySQL Client도 깔았다가 지웠다가 MySQL도 깔았다가 지웠다가 아주 별 x랄을 다했습니다.
결국 프로젝트 자체를 배포하는 것은 포기하고 EC2 인스턴스만 배포하고 프로젝트를 종료했습니다. 정확히 10개월 전인데 약 1년전의 저와 다르게 지금은 또 많이 성장했기 때문에 이번엔 반드시 내 프로젝트를 전부 띄워보겠다. 라는 일념하나로 이번 프로젝트에 임했던 것 같습니다.
서론이 길었군요 바로 시작해보죠.
참고) 이번 포스팅은 EC2 인스턴스가 이미 존재한다는 전제로 진행됩니다.
Docker + Nginx로 배포
이번 포스팅은 아래와 같은 순서로 진행됩니다.
• Docker, Docker-compose 설치
• jdk 11 설치
• 내 프로젝트 깃 레포지토리에서 클론
• 깃 리포지토리에서 Git Bash를 이용해 테스트 케이스 삭제
• Dockerfile 작성
• Nginx 설정파일 작성
• docker-compose 작성
본격적으로 시작해보겠습니다.
1. Docker, Docker-compose 설치
$ apt-get update
$ sudo apt-get install docker
$ sudo apt-get install docker-compose
도커는 항상 하던대로 설치합니다. 만약 깔려있으시면 다운 안받으셔도 됩니다!
2. jdk 11 설치
# aws corretto 다운로드
sudo curl -L https://corretto.aws/downloads/latest/amazon-corretto-11-x64-linux-jdk.rpm -o jdk11.rpm
# jdk11 설치
sudo apt-get install openjdk-11-jdk
3. 내 프로젝트 깃 레포지토리에서 클론
$ sudo apt-get install git
우선 깃을 설치해주시고
$ git clone ${레포지토리 주소}
클론을 해줍니다.
다음 파일들을 좀 정리할건데요. 먼저 루트 폴더에 깃 클론이 들어왔을겁니다.
$ ls
capston
*capston = 깃 클론 폴더
$ mkdir project
$ ls
capston project
$ mv capston project
$ ls
project
$ cd project
이렇게 프로젝트 폴더에 제 프로젝트를 넣어줄겁니다.
그리고 해당 프로젝트로 들어가서 gradlew의 권한을 변경해줍니다.
$ sudo chmod 755 gradlew
반드시 gradlew가 있는 디렉토리안에서 진행해야합니다.
저의 경우 /project/capston 으로 들어간 상태에서 진행했습니다.
그리고 빌드를 진행합니다. 근데..?
$ ./gradlew build

이렇게 뜨는 분들 분명 있을겁니다. 테스트케이스가 실패한 경우!
저도 테스트가 잘못된줄 알고 로컬에서 테스트를 돌려본 결과 잘 통과하는 것을 확인했습니다. 하지만 gradle로 빌드했을 때 테스트케이스에서 오류가 발생했습니다.
이런저런 해결방법을 찾아봤는데 결국 해결하지 못했구요... 결국 모든 테스트케이스를 gitignore 하기로 결정했습니다.
어차피 실제 배포때는 테스트케이스는 필요없으니까요.
4. 깃 레포지토리에서 Git Bash를 이용해 테스트케이스 삭제
저의 경우 windows 10 환경에서 진행했습니다.
바탕화면에 아무 폴더나 하나 생성해주세요. 저는 Git이라는 이름으로 폴더를 생성했습니다.
만약 자신의 컴퓨터에 Git을 설치했다면 Git Bash라는 명령 프롬프트가 다운받아져 있을겁니다. 혹시 없으시다면 구글링으로... (포스팅의 결이 다르기 때문에.. 이해부탁드립니다)
그리고 폴더를 여시고 맨 바닥에 오른쪽 클릭 > Git Bash 클릭!
# 깃 시작
$ git init
# 깃 리포지토리와 연결
$ git remote add origin ${깃 리포지토리 주소}
# pull
$ git pull
# src로 이동
$ cd src
# test 폴더에 있는 캐시 삭제, test 폴더 삭제
$ git rm --cached -r test
$ git rm -rf test
# "test delete" 라는 커밋 메시지와 함께 커밋
$ git commit -m "test delete"
# 깃 push
$ git push origin master
이렇게 명령어 쭉 따라 치시면 테스트폴더가 삭제되고 깃 리포지토리에 저장됩니다!
그리고 스프링 부트 프로젝트의 .gitignore 파일에
/src/main/resources/application.yml
/src/main/resources/application-google.properties
/src/main/resources/application-kakao.properties
/src/main/resources/application-naver.properties
/src/test/*
이렇게 추가해주시면 됩니다.
5. Dockerfile 작성
이제 모든 전처리 과정이 끝났습니다. 본격적으로 배포를 시작해보죠.
Dockerfile은 프로젝트 폴더 옆에 만들어 주시는게 국룰이랍니다. (Chat GPT 피셜)
$ ls
capston
# 이렇게 자신의 프로젝트가 옆에 있는 상태인 디렉토리에서
$ vim Dockerfile
$ ls
capston Dockerfile
# 모두 마무리되면 이런 형태로
자신의 프로젝트 옆에다 Dockerfile을 만들어줍니다.
FROM openjdk:11-jdk
# JAR_FILE 변수 정의 -> 기본적으로 jar file이 2개이기 때문에 이름을 특정해야함
ARG JAR_FILE=./capston/build/libs/DevopsTestKotlin-0.0.1-SNAPSHOT.jar
# JAR 파일 메인 디렉토리에 복사
COPY ${JAR_FILE} app.jar
# 시스템 진입점 정의
ENTRYPOINT ["java","-jar","/app.jar"]
JAR_FILE 은 자신의 SNAPSHOT.jar 이 있는 곳을 가리켜야합니다. 아니면 절대경로로 적어주셔도 됩니다.
6. Nginx 설정파일 작성
# nginx 디렉토리 생성
$ mkdir nginx
# nginx 디렉토리로 이동
$ cd nginx
# conf.d 디렉토리 생성
$ mkdir conf.d
# conf.d 디렉토리로 이동
$ cd conf.d
# app.conf 파일 생성
$ vim app.conf
이렇게 nginx의 설정파일을 만들어주고
server {
listen 80;
access_log off;
location / {
proxy_pass http://spring:8080;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
app.conf 내용물은 위와같이 적어주시면 됩니다.
그렇게 하고 Dockerfile이 있는 곳에서 디렉토리를 확인해보면 이렇게 되어있겠죠.
$ ls
capston Dockerfile nginx
7. docker-compose.yml 작성
거의 다왔습니다. 이제 docker-compose.yml을 Dockerfile 옆에다 생성해줍니다.
version: "3.8"
services:
web:
image: nginx
ports:
- 80:80
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
depends_on:
- spring
spring:
build: .
ports:
- 8080:8080
하나씩 설명해보자면
image : 이미지는 nginx를 쓰겠다는 의미입니다. 이렇게 적어두면 알아서 이미지를 받아옵니다.
ports : 80포트로 들어오는 nginx를 로드밸런싱하여 8080 포트로 라우팅시키겠다는 의미입니다. 즉, nginx는 딱히 아무것도 하지않고 포트만 샥 하고 바꿔주는 역할을 합니다.
volumes : 우리가 방금 만들었던 nginx의 설정파일을 컨테이너의 /etc/nginx/conf.d 폴더로 바인딩시키겠다는 의미입니다.
build : 옆에 온점 반드시 찍으셔야합니다!
ports : 스프링 프로젝트의 포트를 8080으로 설정하겠다는 의미입니다.
이제 이렇게 모두 설정하시면 프로젝트 구조는 이렇게 됩니다.
project
- capston
- build.gradle
- gradlew
- build
- gradle
- gradlew.bat
- settings.gradle
- src
- Dockerfile
- docker-compose.yml
- nginx
- conf.d
- app.conf
조금 복잡한데요. 잘 따라오셨는지요!
마지막으로 컨테이너를 띄워보겠습니다.
$ docker-compose -f ./docker-compose.yml up -d
# 위와같은 명령어를 쓰기 위해서는 반드시 docker-compose가 있는 디렉토리에서 명령어를
# 실행하셔야 합니다.
그러면 nginx의 이미지를 내려받고 컨테이너로 띄워줍니다. 그리고 저희 프로젝트도 띄워지죠.
$ docker ps

모든 컨테이너가 올라갔습니다.
이제 EC2의 퍼블릭 IP 뒤에 8080포트를 달아주면?
http://${EC2 퍼블릭 IP}:8080

짜잔 완벽하게 배포된 것을 확인할 수 있습니다.
마치며
이번 여정은 정말 복잡하고 길었습니다. 음... 솔직히 1년전의 저보고 하라고 하면 절대 못했을 듯 한 시도였습니다. 지금이야 여유도 생기니 리눅스도 공부하고 도커도 공부했지만 1년전의 저는 스프링 시큐리티도 잘 못다루던 애송이였으니까요.
이제 첫 걸음마... 는 아니고 세번째 걸음마를 뗐습니다. 첫 번째는 mysql 도커로 배포후 외부연결, 두 번째는 redis 도커로 배포 후 외부연결, 그리고 세번째가 도커를 이용해 스프링 프로젝트 배포
정말 긴 여정이었습니다. 하지만 앞으로 해야할게 정말 많습니다. Route 53으로 도메인 달아줘야하고, ACM으로 HTTPS 통신도 해야하고 ACM을 ELB를 이용해 로드밸런싱 해야합니다.
그리고 대망의 Nginx를 이용해 Blue / Green 배포까지!
이 모든 과정을 거쳐야 온라인 쇼핑몰 ver.2 카테고리에 포스팅할 수 있을 것 같습니다.
혹시나 이 긴 여정에 도움이 필요하신 분들을 위해 자세히 포스팅할 예정이니까요. 앞으로 잘부탁드립니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
Docker Hub
이번 포스팅에선 Docker Hub (이하 도커 허브) 리포지토리를 만들고 사용하는 방법에 대해서 알아보도록 하겠습니다.
도커 허브는 도커 생태계에서 이미지를 따로 보관할 수 있는 저장소와 같은 역할을 합니다. docker 만 설치하면 어디서든 (linux 환경 한정이긴 합니다만..) 이미지를 pulling 할 수 있습니다.
그럼 한번 알아보도록 하죠!
Docker Hub 시작하기
https://hub.docker.com/
1. 도커 허브에 들어가고 Register를 선택합니다.

2. 사용할 아이디와 이메일 주소, 패스워드를 입력합니다.

3. 저희는 공짜로 쓸거니 Free 버전을 눌러줍니다.

4. 이메일 인증을 진행합니다.

5. 도커 저장소 생성하기

위의 Repositories를 클릭하고 create repository를 클릭합니다.
6. 저장소 이름 작성

이렇게 생성하면 끝!
이제 이용방법에 대해서 알아봅시다. 이것도 정말 간단한데요
$ docker login
Username: garlicpollpoll
Password:
Login Succeeded
우선 로그인을 하는데 아까 우리가 생성했던 계정과 똑같이 입력하면 됩니다.
그리고 도커 이미지를 확인하고?
$ docker iamges

이미지를 푸시할 때 규칙이 있는데요
$ docker push garlicpollpoll/capston:1
이렇게 계정이름 / 리포지토리 이름 : tagname 이런 식으로 입력해야 합니다.
그리고 실제로 가보면?

저는 테스트때문에 2까지 넣었는데 이렇게 완료된 것을 볼 수 있습니다.
이걸 이용해서 무중단 배포를 진행할겁니다. 다음 포스팅도 기대해주세요!
Docker + Nginx 를 이용해 스프링 프로젝트 무중단 배포하기 : 방법
이번 포스팅에선 Docker + Nginx를 이용해 무중단 배포에 대해 포스팅해보도록 하겠습니다.
무중단 배포는 CI / CD를 통틀어 웹 서비스에 모두 적용되어 있는 가장 기본이라고 할 수 있습니다. 도커 컨테이너를 띄워 보신 분들은 아시겠지만 도커 컨테이너를 내렸다가 다시 올리는 과정이 길게는 1분정도 걸립니다.
그 때 우리의 애플리케이션을 이용하고 있는 사용자가 있다면 갑자기 서비스를 이용할 수 없는 상황이 될겁니다. 만약 회원수가 정말 많아서 1분에 결제가 수백 수천만원이 왔다갔다하는 상황이라면 회사 입장에서 당연히 꺼려질 수 밖에 없습니다.
그래서 이번엔 Docker 와 Nginx를 이용해 무중단 배포에 대해서 알아봅니다!
무중단 배포
무중단 배포에는 두가지 배포가 있습니다.
• Rolling update 배포 : 새로 배포되어야 하는 버전을 하나씩 순차적으로 적용시키면서 배포하는 방식입니다. 한 번에 모두 배포되는게 아니기 때문에 배포가 되는 과정에서 옛날 버전과 새로운 버전이 공존합니다. 그렇기 때문에 잘못하면 배포하는 과정 중 호환성 문제가 발생할 수 있습니다.
• Blue / Green 배포 : Blue 혹은 Green 버전 둘 중 하나로 배포되어 있는 상태에서 새로운 버전을 동시에 띄우고 로드 밸런서를 통해서 스위칭하는 방식이며, 한 번에 두 개의 버전을 동시에 띄우기 때문에 시스템 자원이 두배로 든다는 단점이 있습니다.
이 배포 말고도 Canary 배포도 있는데 일부에게만 새로운 버전을 배포하고 테스트하는 배포라고 합니다. 제가 생각하는 무중단 배포와 다르기 때문에 소개해드리진 않습니다.
블루 그린 배포에 대해서 이미지로 도식화하면 다음과 같습니다.




개념은 정말 간단하죠?
새로 배포할 때마다 새로운 컨테이너를 띄우고 Nginx 연결을 새로 띄운 컨테이너 포트로 연결한 뒤 이전 포트의 컨테이너는 버립니다.
이제 본격적으로 과정에 대해서 알아볼까요? 순서는 다음과 같이 진행됩니다. 이번 포스팅역시 EC2 인스턴스가 존재한다고 가정하고 시작합니다.
• docker, docker-compose 설치
• docker hub 리포지토리 생성
• docker-compose-green.yml / docker-compose-blue.yml 생성
• deploy.sh 작성
• nginx-green.conf / nginx-blue.conf 작성
• deploy.sh 실행
순서대로 알아보죠!
1. docker, docker-compose 설치
$ apt-get update
$ sudo apt-get install docker
$ sudo apt-get install docker-compose
2. docker hub 리포지토리 생성
도커 허브는 아래의 링크에서 자세히 확인할 수 있습니다. 어떻게 만드는지에 대해서는 포스팅하지 않습니다.
https://coding-review.tistory.com/407
3. docker-compose 작성
먼저 블루부터 만들어보겠습니다.
version: "3.8"
services:
api:
image: docker.io/garlicpollpoll/capston:1
container_name: garlicpollpoll-capston-blue
environment:
- LANG=ko_KR.UTF-8
- UWSGI_PORT=8080
ports:
- "8080:8080"
이미지는 도커 허브에서 이미지를 가져와서 컨테이너로 띄울겁니다. 하나씩 코드를 설명해보겠습니다.
image : docker.io/${도커허브 계정명}/${도커허브 리포지토리명}:${태그명} 도커 허브에서 이미지를 가져오겠다는 의미입니다.
container_name : 컨테이너 이름을 블루 버전이라고 명시해줍니다.
environment : UTF-8의 한국어를 사용하겠다고 적어줍니다.
ports : 블루의 포트는 8080입니다. 라고 컨테이너를 만들 때의 속성으로 적어줍니다.
다음 그린을 만들어보죠.
version: "3.8"
services:
api:
image: docker.io/garlicpollpoll/capston:2
container_name: garlicpollpoll-capston-green
environment:
- LAN=ko_KR.UTF-8
- UWSGI_PORT=8081
ports:
- "8081:8080"
달라진건 blue를 green으로 바꾸고 포트를 8081로 바꾼것입니다.
4. nginx.conf 작성
이제 nginx의 설정파일을 생성해보죠.
블루부터 보겠습니다.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
upstream backend {
server 43.201.47.110:8080; # blue
}
access_log /var/log/nginx/access.log;
server {
listen 80;
location / {
include /etc/nginx/uwsgi_params;
proxy_pass http://backend;
}
}
}
블루 버전입니다. 문법은 대충 보면 느낌이 오실겁니다.
그리고 그린 버전입니다.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
upstream backend {
server 43.201.47.110:8081; # green
}
access_log /var/log/nginx/access.log;
server {
listen 80;
location / {
include /etc/nginx/uwsgi_params;
proxy_pass http://backend;
}
}
}
그린 버전은 포트만 다르고 나머지는 똑같습니다.
5. deploy.sh 작성
이제 알파이자 오메가인 deploy.sh 를 적어봅시다.
#!/bin/bash
DOCKER_APP_NAME='garlicpollpoll-capston'
EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f ./docker-compose-blue.yml ps | grep Up)
if [ -z "$EXIST_BLUE" ]; then
echo "blue up"
docker-compose -p ${DOCKER_APP_NAME}-blue -f ./docker-compose-blue.yml pull
docker-compose -p ${DOCKER_APP_NAME}-blue -f ./docker-compose-blue.yml up -d
BEFORE_COMPOSE_COLOR="green"
AFTER_COMPOSE_COLOR="blue"
else
echo "green up"
docker-compose -p ${DOCKER_APP_NAME}-green -f ./docker-compose-green.yml pull
docker-compose -p ${DOCKER_APP_NAME}-green -f ./docker-compose-green.yml up -d
BEFORE_COMPOSE_COLOR="blue"
AFTER_COMPOSE_COLOR="green"
fi
sleep 10
EXIST_AFTER=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} -f ./docker-compose-${AFTER_COMPOSE_COLOR}.yml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then
cp ./nginx-${AFTER_COMPOSE_COLOR}.conf ./nginx.conf
nginx -s reload
docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f ./docker-compose-$BEFORE_COMPOSE_COLOR.yml down
echo "$BEFORE_COMPOSE_COLOR down"
else
echo "Failed to start $AFTER_COMPOSE_COLOR"
fi
좀 깁니다. 오타가 나지 않게 잘 작성해주세요!
자세한 내용은 차후에 쉘 스크립트 문법에서 설명하도록 하겠습니다. 간단하게 흐름만 알아보도록 하죠.
• 먼저 blue버전의 컨테이너가 있는지 확인합니다.
• 도커 허브를 풀링해서 이미지를 업데이트하고 블루가 없으면 블루를 띄우고 있으면 그린을 띄웁니다.
• 새로운 컨테이너가 제대로 떴으면
• 그 전 컨테이너를 지웁니다.
이제 deploy.sh를 실행해줘야 하는데... 권한이 없다고 뜰겁니다.
그땐 쿨하게 파워권한을 주면 됩니다.
$ sudo chmod 777 ./deploy.sh
그리고 실행해줍니다.
$ ./deploy.sh

로그를 잘 보시면 그린버전을 올리고 블루 버전을 멈춘뒤 블루 버전을 삭제하는 것을 볼 수 있습니다.
그리고 컨테이너를 확인해보면?
$ docker ps

잘 안보이는데... 8081로 잘 업로드 된 것을 볼 수 있습니다.
마치며
처음으로 무중단 배포를 진행했는데 정말 신기하고 새롭네요. 실제로 위와 같은 방법으로 무중단 배포를 진행하시면 느끼시겠지만 여간 귀찮은게 아닙니다.
그것에 대한 포스팅은 추후에 자세하게 적도록 하겠습니다.
그래서 Jenkins와 같은 툴을 이용해서 배포 파이프라인을 만드는걸로 알고있습니다. 이것도 추후에 공부해서 포스팅해보도록 하겠습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
docker-compose 주요 문법
이번 포스팅에선 docker-compose 에서 사용되는 주요 문법들에 대해서 포스팅해보도록 하겠습니다.
요즘 docker에 빠져있는데요. docker는 작은 프로젝트에 한해선 단점이 하나도 없는 좋은 기술인 것 같습니다.
docker-compose는 docker의 다양한 설정들을 보기쉽게 정리하고 나중에 설정이 바뀌었을 때 유지보수도 쉽게 도와주는 docker의 좋은 파트너입니다.
보통 docker-compose는 yml (야믈) 파일로 저장되는데요. 그렇기 때문에 우리가 스프링에서 application.yml 을 써봤다면 조금은 익숙하실겁니다.
이제 본격적으로 시작해보죠!
docker-compose 문법
1. image
docker-compose 안에서 이미지를 선택할 수 있습니다. 만약 로컬에 있는 커스텀 이미지를 불러오고싶다면 해당 이름을 넣으면 됩니다. 만약 로컬에 없는 이미지를 입력하면 이미지를 내려받아줍니다.
services:
proxy:
image: nginx:latest # nginx 이미지
2. build
build는 docker-compose에서 Dockerfile을 이용해 자동으로 docker build를 실행하고 생성된 이미지를 베이스 이미지로 사용합니다.
servies:
server:
build: # Dockerfile 빌드
보통 저는 docker-compose 파일과 Dockerfile을 같은 디렉토리에 넣기 때문에 위와같은 문법을 사용하면 되지만 혹여 Dockerfile과 docker-compose가 다른 위치에 있는 경우 그리고 Dockerfile의 이름이 Dockerfile이 아닌 경우에는 아래의 문법을 사용하시면 됩니다.
servies:
server:
build: # Dockerfile 빌드
context: ./src/servers # Dockerfile 빌드 경로
dockerfile: dockerfile-server # dockerfile의 파일명이 dockerfile이 아닐 경우 파일명을 입력해야합니다.
3. command / entrypoint
command 명령어는 Dockerfile의 entrypoint와 굉장히 비슷합니다. 다만 Dockerfile의 entrypoint보다 docker-compose의 entrypoint가 더 우선순위가 높습니다.
Dockerfile과 docker-compose의 entrypoint는 실제로 image가 빌드되고 컨테이너로 올라갈 때 실행할 명령을 적을 수 있다는 점입니다.
#docker-componse.yml
services:
nginx:
build: .
entrypoint: cat /etc/hosts
# 또는
entrypoint:
- /bin/cat
- /etc/hosts
이렇게 코드를 작성하면 docker-compose가 실행될 때 cat /etc/hosts의 명령어가 실행됩니다.
Dockerfile의 CMD 쓰면 되는거 아냐? 이렇게 생각하신 분들도 있을텐데요. Dockerfile의 CMD와 ENTRYPOINT는 엄연히 다릅니다. 이에 대해서는 추후에 포스팅해보도록 하겠습니다.
4. ports
ports는 호스트 OS와 컨테이너의 포트를 바인딩 시키는 포트 포워딩의 역할을 합니다. ports아래 옵션을 추가할 수 있는데 target은 컨테이너 내부 포트고, published는 호스트 OS의 포트입니다. protocol 옵션은 프로토콜을 지정할 수 있습니다.
ports는 <호스트 머신의 포트번호>:<컨테이너의 포트 번호> 와 같은 순서로 바인딩이 이루어집니다. 만약 8080:8080이 아닌 8080만 지정했을 경우 4631(랜덤):8080 이렇게 바인딩 됩니다.
# docker-compose.yml
version: "3"
services:
nginx:
build: .
ports:
- "8080:8080"
#또는
- target: 8080 ## 컨테이너 내부 포트
published: 8080 ## 호스트OS에서 공개할 포트
protocol: tcp ## 포트 프로토콜
5. expose
expose는 호스트 OS에 포트를 공개하지 않고, 컨테이너만 포트를 공개합니다. 즉, 호스트 OS와 직접적으로 연결되지 않고 링크등으로 연결된 컨테이너-컨테이너간의 통신만 필요한 경우 사용합니다.
일반적으로 로그 서버와 같이 호스트 머신에서 직접 액세스 하지 않고, 우베 애플리케이션 서버 기능을 갖고 있는 컨테이너를 경유해서만 액세스 하고 싶은 경우등에 사용됩니다.
# docker-compose.yml
version: "3"
services:
server:
build: # Dockerfile 빌드
expose:
- 8080 # 도커 내부적 포트
즉 정리하자면, expose는 호스트 쪽에서 접근이 불가능하고, 컨테이너끼리만 통신이 가능하도록 지정하는 것입니다.
6. volume
Dockerfile에서 사용되는 volume과 같이 컨테이너에 디렉토리 혹은 파일을 마운트할 때 사용됩니다. <호스트>:<컨테이너> 방식으로 사용하고 마지막에 :ro를 추가해 볼륨을 읽기 전용으로 사용할 수도 있습니다.
# docker-compose.yml
volumes:
- ./mysql/mysql_data:/var/lib/mysql
- ./mysql/sqls/:/docker-entrypoint-initdb.d/
이렇게 만든 docker-compose 를 실행하고 싶다면 아래와 같은 명령어를 작성하면 됩니다.
$ docker-compose -f ./docker-compose.yml up -d
해석하자면 현재 디렉토리에 있는 docker-compose.yml 파일을 기반으로 컨테이너를 백그라운드로 띄워줘 가 되겠습니다.
마치며
이렇게 docker-compose문법에 대해서 알아봤습니다. docker-compose를 실제로 자신이 작성할 수 있을만큼 익히는 것도 중요하겠지만, 누군가 작성한 docker-compose 파일을 똑같이 옮겨 적을 때 의미없이 옮겨 적는 것 보다 알면서 옮겨 적는게 아무래도 조금 더 도움이 될 듯합니다.
이제 다음은 docker-compose의 명령어에 대해서 알아보도록 하고 이어서 Dockerfile에 대해서 알아보도록 하겠습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://seokhyun2.tistory.com/61
https://narup.tistory.com/207
Docker + Nginx를 이용해 스프링 프로젝트 무중단 배포하기 : 과정
저번 포스팅에선 docker + nginx를 가지고 무중단 배포를 하는 방법에 대해서 설명한 것이구요. 이번 포스팅에선 전체적인 과정에 대해서 알아보도록 하겠습니다.
이번 포스팅은 아래와 같은 순서로 이뤄집니다.
• 스프링 프로젝트에서 무언가 변경 (ver.2)
• git commit / push
• EC2 에서 프로젝트 폴더에 들어간 뒤 git pull
• ./gradlew build
• 내 프로젝트를 docker image로 build
• 도커 허브에 방금 만든 이미지 push
• docker-compose.yml 파일에서 버전 변경
• deploy.sh 실행
결과부터 말씀드리자면 이 과정 굉장히 복잡합니다. 그리고 굉장히 귀찮습니다. 일단 이번 포스팅에선 과정을 보여드리고 다음엔 jenkins를 이용해 CI / CD 파이프라인을 자동화하는 과정을 포스팅해보도록 하겠습니다.
우선 시작해보죠!
무중단 배포 과정

우선 우리는 지금 "최신상품" 이라고 써있는 것을 "최신상품 NEW!!!" 로 바꿀겁니다.
1. 스프링 프로젝트에서 무언가 변경 (ver.2)

화질이 좋지않군요... 영상을 스샷찍었더니 이런 불상사가 발생했습니다. 아무튼 NEW!!!로 바꾼 모습입니다.
2. git commit / push

깃과 연결된 프로젝트를 커밋후 푸시합니다.
3. EC2에서 프로젝트 폴더에 들어간 뒤 git pull

발표용 자료이기 때문에 돋보기를 사용했습니다.
$ ls
$ git pull
4. ./gradlew build

프로젝트 디렉토리에서 gradlew 를 빌드해줍니다. 만약 gradlew에 권한이 없다고 뜨면 이 디렉토리에서 슈퍼권한을 주면 됩니다.
$ sudo chmod 777 ./gradlew
$ ./gradlew build
4. 내 프로젝트를 docker image로 build

이번엔 Dockerfile과 docker-compose등이 있는 디렉토리로 한칸 올라와서 이미지로 만들어줍니다.
$ cd ..
$ docker build -t garlicpollpoll/capston:2 .
도커 허브에 넣기 위해서 ${계정이름}/${리포지토리 이름}:${버전 혹은 태그} 순서로 이미지를 만들었습니다.

아까 없던 버전2가 이미지로 생성된 모습입니다.
$ docker images
6. 도커 허브에 방금 만든 이미지 push

도커 허브에 방금 만든 이미지를 푸시합니다.
$ docker login
Username: garlicpollpoll
Password:
Login Succeeded!
# 이미 한번이라도 로그인 했으면 아이디 비밀번호 치는 과정은 생략됨
$ docker push garlicpollpoll/capston:2

우리가 올린 버전2가 도커 허브에 잘 들어갔습니다.
7. docker-compose.yml 파일에서 버전 변경

docker ps 명령어로 현재 올라와있는 컨테이너의 버전을 확인합니다. 작아서 잘 안보이시겠지만 이미지는 버전1을 사용하고 있고 블루 그린 중 블루와 8080포트를 사용하고 있습니다.
그럼 우리는 그린의 docker-compose 파일로 들어가서 버전을 바꿔줘야합니다.

이렇게 태그를 2로 바꿔줍니다. 이렇게 바꾸면 도커 허브에서 태그가 2인 이미지를 가져와서 컨테이너로 띄워줄겁니다.
8. deploy.sh 실행

정상적으로 완료되었고 로그를 보시면 그린버전의 컨테이너를 만들고 블루버전의 컨테이너를 중지시킨 뒤 컨테이너를 삭제시켰습니다.
이제 그린버전의 포트인 8081로 접속해보면?

짜잔 바로 바뀌었습니다.
이렇게 블루 그린 배포를 진행하면 원래 컨테이너가 내려갔다 올라가는 시간인 1분에 비해 1초~2초 정도로 짧게 줄일 수 있습니다.
마치며
저번 포스팅에서 배포 방법을 알려드렸고 이번 포스팅에선 배포 과정에 대해서 알려드렸습니다. CI / CD 생각보다 재밌으니 여러분도 한번 도전해보세요!
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
윈도우에서 Docker 사용하기
이번 포스팅에선 윈도우에서 도커를 사용하는 방법에 대해서 포스팅해보도록 하겠습니다.
흔히 알려지기로는 도커는 리눅스 환경에서만 동작하기 때문에 신입 기준 러닝커브가 있는 편이라고 하죠.
어? 그럼 윈도우에선 어떻게 동작하지? 바로 WSL 이라는 기술을 이용하면 됩니다. WSL은 Windows Subsystem for Linux 2 의 약자로 쉽게 말해서 윈도우에서 리눅스를 사용할 수 있게 해주는 기술입니다.
이제 WSL을 이용해서 윈도우에서 도커를 사용해봅시다.
윈도우에서 도커 설치
우선 WSL2를 다운받아야합니다. 그러기 위해선 윈도우 터미널인 power shell이라는 것을 다운받아야 하는데요.
파워 쉘은 다운받기 정말 간단합니다. 마이크로소프트 스토어에서 다운받으면 됩니다.
파워 쉘 다운


저는 이미 다운받아서 열기로 되어있는데 다운로드 버튼이 저 위치입니다. 다운로드 받으시고 관리자 권한으로 실행해주세요!

이렇게 정상적으로 떴다면 성공!
그리고 터미널에 다음과 같은 명령어를 입력해주세요.
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
두 명령어 모두 "작업을 완료했습니다." 라는 메세지가 떴다면 성공입니다.
그리고 이제 윈도우를 재부팅합니다.
재부팅 후에 다시 터미널을 관리자 권한으로 실행하고 다음과 같은 명령어를 입력해줍니다.
wsl --set-default-version 2
이렇게 하면 WSL의 버전의 디폴트 값을 2로 설정하고 실행하게 됩니다.
이제 도커 데스크탑을 설치해봅시다.
도커 데스크탑 설치
https://www.docker.com/products/docker-desktop/

다운로드 받고 가이드라인대로 진행하시면 됩니다.
설치에 3분~5분가량 소요가 됩니다.
자 이제 설치가 되었으면 도커 데스크탑의 설정이 잘 되어있는지 확인해야합니다.

먼저 설정 누르고 Use the WSL 2 based engine 에 체크가 되어있는지 확인합니다.
그리고 왼쪽에 Resources탭을 클릭하고

WSL integration을 클릭합니다.

Enable integration with my default WSL distro 에 체크가 되어있는지 확인합니다. 만약 체크가 되어있지 않다면 체크해주고 Apply & restart를 클릭해주세요!
테스트
이제 도커가 제대로 실행되는지 테스트해봐야죠?

저는 MySQL과 카프카를 설치해서 확인해봤습니다.
마치며
이렇게 윈도우에서 도커를 사용하는 방법에 대해서 알아봤습니다.
조금 사용해본 소감으로는 조금 익숙하지 않다고 해야할까요... 워낙 리눅스에서만 도커를 사용했다보니 윈도우에서 사용하기가 여간 불편한게 아니었습니다.
리눅스에서 당연하듯 써온 명령어인 vim 이나 apt-get같은 명령어가 들지않아서 참 곤란했지만 익숙해져야겠죠.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://www.lainyzine.com/ko/article/a-complete-guide-to-how-to-install-docker-desktop-on-windows-10/
Docker로 Prometheus, Grafana 배포하기
실제 환경을 모니터링하는 것은 매우 중요한 것으로 알려져있습니다. 물론 항상 모니터링 하진 않겠지만 중요한 서비스를 배포해야 한다거나 버그를 고치고 난 뒤에 모니터링을 통해 실제 환경에 잘 녹아들었는지 확인해야합니다.
이번 포스팅에선 실제 배포된 환경을 프로메테우스와 그라파나로 연동하여 CPU 사용량, 메모리 사용량, 디스크 사용량, HTTP 트래픽 등을 모니터링하는 환경을 구축해보도록 하겠습니다.
모니터링 환경 구축
우리가 최종적으로 목표하는 것은 바로 아래와 같은 환경을 구축하는 것입니다.

도커를 이용해 프로메테우스를 띄우고 그라파나도 띄운 다음 모니터링 하는 것이죠. 그리고 이것이 모두 이루어지면 나만의 모니터링 시스템을 만들 수 있습니다.

이렇게 만들기 위해서 먼저 우리는 준비물이 필요합니다.
• 도커 (Docker)
• 도커 컴포즈 (Docker-compose)
이렇게 두가지가 준비 되었다면 프로메테우스와 그라파나 시작해보죠!
1. Docker-compose 작성
먼저 Docker-compose를 작성해보도록 하겠습니다.
현재 위치 : /prometheus-grafana/docker-compose.yml
version: '3.8'
services:
prometheus:
user: "root"
image: prom/prometheus
container_name: prometheus_container
volumes:
- ./prometheus/config:/etc/prometheus
- ./prometheus/volume:/prometheus/data
ports:
- 9090:9090
command:
- '--web.enable-lifecycle'
- '--config.file=/etc/prometheus/prometheus.yml'
restart: always
networks:
- promnet
grafana:
user: "root"
image: grafana/grafana
container_name: grafana_container
ports:
- 3000:3000
volumes:
- ./grafana/volume:/var/lib/grafana
restart: always
networks:
- promnet
networks:
promnet:
driver: bridge
yml (야믈) 파일은 오타 하나라도 나면 전부 실행이 안되기 때문에 주의해서 작성하셔야합니다.
docker-compose.yml 파일에서 알 수 있듯이 volumes 속성을 통해 컨테이너 밖에 있는 우리의 AWS linux 환경과 마운트를 해줄 파일을 정해줬습니다. 이제 이 파일들을 하나씩 만들어야겠죠?
2. prometheus.yml 파일 생성
프로메테우스를 실행하기 위해서는 prometheus.yml 파일이 필요합니다.
현재 위치 : /prometheus-grafana/prometheus/config/prometheus.yml
global:
scrape_interval: 15s
scrape_timeout: 15s
evaluation_interval: 2m
external_labels:
monitor: 'codelab-monitor'
query_log_file: query_log_file.log
rule_files:
- "rule.yml"
scrape_configs:
- job_name: 'prometheus'
scrape_interval: 10s
scrape_timeout: 10s
metrics_path: '/metrics'
honor_labels: false
honor_timestamps: false
scheme: 'http'
params:
user-id: ['[email protected]']
static_configs:
- targets: ['{본인 IP주소}:9090']
labels:
service: 'monitor-1'
- job_name: 'node'
static_configs:
- targets: ['{본인 IP주소}:9100']
프로메테우스는 일종의 데이터베이스입니다. 실제 배포된 환경에 요청을 보내 배포 환경의 상태를 저장해놓고 있는 것이죠. 그리고 저장된 데이터들을 시각화하여 보여주는 것이 바로 그라파나입니다.
prometheus.yml 에서 주의깊게 봐야할 점을 짚고 넘어가도록 하겠습니다.

프로메테우스가 실제 배포 환경에 요청을 보내게 될 주기입니다. 그리고 배포 환경의 정보를 로그 파일을 통해 저장하겠다는 의미입니다.

프로메테우스는 특별한 규칙에 근거하여 돌아갑니다. 추후에 rule.yml 파일을 작성할 예정입니다.

프로메테우스가 어떤 환경을 타겟으로 할 것인지 정하는 부분입니다. 9090포트는 프로메테우스 자신을 뜻하구요, 9100은 후에 다운받을 node-exporter에 해당합니다.
node-exporter는 다운받을 때 추가적으로 더 설명해드리도록 하겠습니다.
3. rule.yml 작성
현재 위치 : /prometheus-grafana/prometheus/config/rule.yml
groups:
- name: example
rules:
- alert: InstanceDown
expr: up == 0
for: 5m
labels:
severity: page
annotations:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes."
- alert: APIHighRequestLatency
expr: api_http_request_latencies_second{quantile="0.5"} > 1
for: 10m
annotations:
summary: "High request latency on {{ $labels.instance }}"
description: "{{ $labels.instance }} has a median request latency above 1s (current value: {{ $value }}s)"
rule.yml은 어떤 상황에선 어떤 행동을 취해라 라고 가이드라인을 정해주는 것입니다.
인스턴스가 내려가면 어떤 메세지를 띄워줘라, API 요청이 너무 길어지면 어떤 메세지를 띄워줘라 같은 내용이 담겨있습니다.
4. Docker 실행
Docker-compose를 실행해주면?
$ docker-compose -f ./docker-compose.yml up -d

완료후 컨테이너가 잘 떴는지 확인해봅시다.
$ docker ps

포트까지 떠야 정상입니다! 포트가 안뜨고 status에 Restarting이라고 뜬다면 오타가 났다는 얘기입니다.
그리고 이제 프로메테우스에 들어가봅시다.

들어와서 status에 target을 클릭합니다.

처음엔 이부분이 Down으로 되어있을 겁니다. 이제 node-exporter를 다운받아줘야죠
5. node-exporter 다운 받기
우선 node-exporter가 뭔지 간단하게 짚고 넘어가도록 하겠습니다.
node-exporter는 CPU, 메모리, 디스크, I/O, 네트워크 등 시스템에 대한 데이터를 보아주는 프로메테우스 exporter 중 하나입니다.
node-exporter의 존재 의의는 하드웨어나 OS 시스템의 메트릭을 모니터링하기 위함입니다. 즉, 쉽게 말하면 모니터링하기 위한 프로메테우스가 가진 도구라는 것입니다.
node-exporter를 다운받고 실행까지 시켜보겠습니다.
$ wget https://github.com/prometheus/node_exporter/releases/download/v1.3.1/node_exporter-1.3.1.linux-amd64.tar.gz
$ tar xvfz node_exporter-1.3.1.linux-amd64.tar.gz
$ cd node_exporter-1.3.1.linux-amd64
$ nohup ./node_exporter > node_exporter.log 2>&1 &
이렇게 node-exporter를 백그라운드로 실행하면 완료됩니다.
여기서 한 가지 의문이 들 수 있습니다. 왜 nohup을 사용해서 백그라운드로 쓰지? 도커로 띄우면 안되는건가?
맞습니다. 도커로 node-exporter를 띄우게 되면 node-exporter가 내부적으로 컨테이너를 격리시키려고 하기 때문에 도커로 띄우면 골치아파진다고 하니 그냥 백그라운드로 실행하시는게 좋습니다.
다시 프로메테우스 status > targets 로 이동했을 때 Up이라고 떠있는 것을 확인할 수 있을 것 입니다.
이제 모니터링 툴을 사용해야겠죠? 포스팅이 길어져서 그라파나 사용법은 다음 포스팅에서 진행하도록 하겠습니다.
https://coding-review.tistory.com/437
Grafana 시작하기
https://coding-review.tistory.com/436
이전 포스팅과 이어집니다.
우리는 앞서 프로메테우스, 그라파나, node-exporter까지 준비를 완료했습니다. 이제 그라파나를 어떻게 사용하는지 천천히 알아보도록 하죠.
그라파나 시작하기

그라파나를 처음 시작하면 다음과 같은 화면이 보입니다.
디폴트 계정은
ID : admin
PW : admin
이렇게 되어있으니 로그인 하시면 됩니다.
로그인 후 비밀번호를 바꾸라고 하는데 바꾸고싶으시면 바꾸시고 바꾸기 싫으시면 안바꾸셔도 됩니다.

초기화면에서 왼쪽 위 메뉴를 누릅니다.

Administration > Data sources 클릭!

Add new data source 를 클릭합니다.

그리고 프로메테우스를 클릭!

우리에게 필요한 것은 단 두개입니다. data source 이름, 그리고 프로메테우스가 배포된 주소 저희는 실제 퍼블릭 IP를 가지고 운영하고 있기 때문에 IP 주소 포트까지 적어주시면 됩니다.
맨 아래 save & test를 클릭후 초록색이 뜨면 성공입니다.

메뉴를 누르고 이번엔 대쉬보드로 들어갑니다.

오른쪽에 New 버튼 누르시고 New Dashboard를 눌러줍니다.

Add visualization을 누르고 디폴트 값인 Prometheus도 클릭해줍니다.

그럼 이런 화면이 뜨는데 먼저 ① Builder > Code로 변경해주고 ② 프로메테우스 쿼리를 입력해줘야합니다. ③ 그리고 Run quries 를 누르면 그래프가 짠 하고 뜹니다.
먼저 간단하게 CPU 사용량을 확인해볼까요?

프로메테우스 쿼리는 구글링 하시면 잘 나와있습니다.
마치며
자 우리는 이렇게 그라파나까지 완료해서 시스템 모니터링 환경을 구축했습니다. 정말 뿌듯하고 좋긴 하네요. 어려움도 있었지만 이제 문제 해결에 도가 터서 그런건지 나름 쉽게 해결했습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://www.devkuma.com/docs/prometheus/docker-compose-install/
https://grepper.tistory.com/68
https://ttl-blog.tistory.com/1366
도커 네트워크 (with 아키텍처)
이번 포스팅에선 도커, 그 중에서 도커 네트워크에 대해서 알아보도록 하겠습니다. 도커 네트워크에 대해서는 구글링하면 bridge니 host니 overlay니 많이 나와있으니 흔히 구글링하면 나오는 내용 말고 좀 더 딥한 내용을 다뤄보려고합니다.
제가 도커 네트워크를 공부해야겠다고 생각하게 된 계기는 제가 너무 도커에 대해서 아무것도 모르고 도커를 사용하고 있던 것을 발견했습니다.
그냥 Dockerfile만들고 build하고 docker-compose에 등록해서 관리하면 끝!
사실 이게 끝일 정도로 도커 자체는 정말 간단하긴 하지만 도커를 조금 더 깊이있게 공부해야겠다는 마음으로 도커 카테고리를 약 1년만에 부활시켰습니다.
이번엔 도커 네트워크에 대해서 딥다이브하는 시간을 가져보겠습니다.
도커 아키텍처
도커 네트워크를 먼저 알아보기 전에 도커 아키텍처에 대해서 짚고 넘어가도록 하겠습니다. 왜냐하면 이 아키텍처를 이해하고 있어야 도커 네트워크에 대한 내용이 이해가 될 것 같아서 먼저 공부했습니다.
이참에 도커 docs를 봤는데 이야... 정말 잘 되어있더군요 어려분도 정독할 기회가 있다면 한번 읽어보시는 것을 추천드립니다.

도커는 클라이언트, 도커 API, 도커 호스트, 도커 데몬, 이미지, 컨테이너, 레지스트리로 이루어져있습니다. 하나씩 알아보죠.
• 도커 클라이언트 : 우리가 실행하는 API들이 호출되는 장소입니다. 리눅스가 될 수도 있고 맥이 될 수도 있고 윈도우가 될 수도 있죠.
• 도커 호스트 : 도커가 실제로 실행되는 곳입니다.
도커 데몬 : 우리가 호출한 API를 실제로 컨테이너에게 전달하는 역할을 합니다.
• 이미지 : 도커 컨테이너를 생성하기 위해 만들어놓은 틀 같은 것입니다.
• 컨테이너 : 실제로 운영되는 애플리케이션입니다.
• 레지스트리 : 도커 이미지가 저장되는 정적 공간입니다.
도커가 정말 잘 만들었다고 생각되는 부분 중 하나가 누구나 이해하기 쉬운 아키텍처로 되어있다는 것입니다. 도커를 한시간정도만 사용해보면 바로 저 아키텍처가 이해가 됩니다.
제가 감명깊게 받는 포인트는 정말 쉬운데 정말 파워플한 기능을 가졌다는 것입니다. (물론 깊이 파면 다르지만요)
자 이제 본격적으로 네트워크에 대해서 알아보도록합시다.
도커 네트워크
도커 네트워크를 알아보기 전에 OSI 7계층이 먼저 선행되어야할 것 같습니다.
Layer 1 (L1)
L1에 대표적인 기기는 뭐니뭐니해도 케이블이죠. 하지만 케이블은 너무 우리에게 멀리있는 존재고 우리 가까이 있는 L1기기중 우리에게 익숙한 것이 NIC ("닉" 이라고 읽습니다)입니다.
엥? 안익숙한데요?
NIC은 우리에게 익숙한 랜(LAN)카드입니다. NIC은 외부에서 들어오는 네트워크를 OS로 연결시켜주는 역할을 합니다.
Layer 2 (L2)
L2에 대표적인 기기 하나 꼽아봐라 하면 저는 당연 스위치입니다. 스위치는 하나의 네트워크로 들어온 요청을 MAC주소를 이용해 어디로 가야할지 정해주는 기기입니다. 보통 NIC에는 MAC주소가 적혀있고 스위치는 NIC에게 붙어있는 MAC주소를 보고 연결하는 것입니다.
예를 들어서 공유기를 타고 들어온 저희집 네트워크가 제 컴퓨터로 들어오기 위해서는 스위치를 거쳐서 제 컴퓨터로 들어가게 됩니다.
Layer 3 (L3)
L3에서 가장 대표적인 기기는 라우터입니다. 이 라우터도 우리 일상속에 가까이있습니다. 바로 공유기가 라우터입니다.
우리집에서 요청한 네트워크 통신이 서버로 도달하고 서버가 응답해주는 요청에 우리집 IP와 포트가 적혀있습니다. 인터넷에 흘러다니던 요청들이 정확한 IP와 포트로 들어가게 되는데 이때 요청을 받아주는 것이 라우터인 공유기의 역할이죠.
번외1 Layer 4 (L4)
번외로 L4에서 우리에게 익숙한 것은 TCP 통신입니다. 원래 패킷은 L3에서 사용하는 것이지만 L3에선 할 수 없는 것들을 L4에게 위임함으로써 이뤄낼 수 있습니다.
예를 들어 L3에서의 패킷은 혼잡제어나 흐름제어, 장애회복과 같은 것들을 지원해주지 않습니다. 이를 L4에 위임하여 통신의 신뢰성을 높이는 것이죠.
번외2 Layer 7 (L7)
L7 애플리케이션 계층은 우리가 사용하는 모든 애플리케이션이 위치하고 있는 곳입니다. DNS같은 서비스나 HTTP같은 프로토콜까지 폭넓게 존재하는 영역이죠.
보통 우리가 보낸 요청이 이 L7을 찍고 다시 우리에게 전달되는 것이니 엔드포인트라고 할 수도 있겠습니다.
흐름
OSI 7계층의 가장 좋은 공부 방법은 물리계층이니 데이터링크계층이니 이런식으로 외우는 것보다 OSI 7계층에서 우리 주변에 있는 것들을 잘 조합해서 이해하는 것이 좋다고 생각합니다.
정리하자면
애플리케이션 (L7) => TCP 프로토콜 (L4) => 공유기 (L3) => 스위치 (L2) => 랜카드 (L1) => OS (여기에 네트워크 드라이버가 있습니다.)
이렇게 구성된다고 볼 수 있습니다.
이게 뭐..?
도커 네트워크 얘기하다 왜 이 얘기를 하냐... 바로 도커가 네트워크를 구성할 때 위와같은 방식을 사용하기 때문입니다.
애플리케이션이 작동하기 위해서 세가지 요소가 반드시 필요한데 바로 OS, 네트워크, 애플리케이션 이렇게 세가지입니다.
보통 서버는 리눅스 위에서 동작하기 때문에 도커의 전략은 "OS? 호스트가 쓰는 리눅스 그대로 써! 네트워크? 그것도 호스트거 그대로 써! 우린 애플리케이션만 남긴다."
이렇게 전략을 구성했기 때문에 도커 내부적으로 OS는 리눅스 커널을 사용하더라도 네트워크는 가상으로 구현을 해줘야합니다.
도커의 네트워크 아키텍처는 다음과 같습니다.

도커 네트워크는 다음과 같은 흐름을 가집니다.
• 도커 호스트의 NIC : 도커가 동작하는 호스트 서버의 NIC을 그대로 이용하면서 사용자가 호스트의 NIC을 타고 들어갑니다.
• 도커 데몬의 NAT Gateway : 도커 데몬은 컨테이너의 사설 IP와 포트를 알아내기 위해 NAT을 사용하는데 이 NAT 게이트웨이가 스위치의 역할을 하기도 합니다.
• 도커 데몬의 veth0 : veth0는 가상 이더넷의 약자입니다. 가상 이더넷은 컨테이너마다 부여가 되고, 컨테이너의 MAC주소를 가지고 있다고 생각하시면 됩니다.
• 도커 컨테이너의 NIC : 도커 컨테이너에 NIC이 애플리케이션의 연결을 도와줍니다.
이 상태에서 도커 컨테이너에 있는 네트워크 드라이브를 설정함으로써 더 효율적인 네트워크 관리가 가능합니다.
도커의 네트워크 모드
도커의 네트워크 모드는 몇개 존재하지만 중요한 것 네개만 짚도록 하겠습니다.
bridge
bridge는 가장 기본이 되는 네트워크 모드로 bridge를 이용하면 각종 이점을 얻을 수 있습니다.
• 내부적으로 컨테이너 이름을 DNS처럼 사용할 수 있다.
• 네트워크를 하나로 묶어서 네트워크를 격리시킬 수 있다.
• 내부망을 사용하기 때문에 네트워크 I/O를 "조금" 줄일 수 있다.
이렇게 크게 세가지 이점이 있는데 하나씩 자세하게 살펴보도록 하겠습니다.
- 내부적으로 컨테이너 이름을 DNS처럼 사용할 수 있다. -
예를 들어서 이런 경우를 가정해봅시다.
상황)
서버의 비용을 줄이기 위해 웹서버와 WAS서버를 하나로 묶어서 사용할 것입니다. 웹서버는 nginx를 사용하고 있고 443으로 들어오는 HTTPS요청을 WAS의 포트로 연결해주는 역할을 합니다.
server {
listen 443 ssl;
server_name www.google.com;
location / {
proxy_pass {컨테이너이름}:8080
proxy_header 불라불라
}
}
보통같았으면 proxy_pass를 원래였으면 실제 도메인인 google.com을 입력해야 하지만 네트워크를 bridge로 설정해놓음으로써 컨테이너 이름을 도메인처럼 사용할 수 있습니다.
- 네트워크를 하나로 묶어서 네트워크를 격리시킬 수 있다. -
위의 예시를 그대로 사용하자면
nginx의 80포트와 443포트를 Security Group으로 열어두고, 8080포트를 열어두는 것이 아니라 nginx를 이용해서만 들어가게 함으로써 실제 도커 호스트의 네트워크와 도커 컨테이너의 네트워크를 격리시켜서 보안을 더 높일 수 있다는 이야기입니다.
- 내부망을 사용하기 때문에 네트워크 I/O를 "조금" 줄일 수 있다. -
bridge는 내부망을 사용하기 때문에 네트워크 I/O가 발생하지 않아 조금 더 빠른 성능을 보여줍니다.
하지만 제가 "조금"이라고 쓴 이유는 그 효과가 미미해서 큰 이득을 얻을 수는 없기 때문입니다.
host
네트워크 모드 host는 말 그대로 호스트와 컨테이너 사이의 네트워크 격리를 이루지 않는 것을 의미합니다.
이렇게 host로 설정하면 도커 데몬과 네트워크 통신을 하지 않기 때문에 성능적으로 매우 뛰어나다는 장점이 있습니다.
엥? 위에서도 내부망을 사용한다고 하지 않았나요?
내부망을 사용하더라도 네트워크 아키텍처에서 봤듯이 도커 호스트와 도커 컨테이너간 네트워크 통신이 필요합니다. 그렇게 네트워크 통신이 한번 이뤄지면 그 다음부턴 다리를 놓듯이 내부망을 사용한다는 것이고, host는 내부망으로 타지도 않고 바로 호스트의 네트워크를 그대로 사용한다는 것입니다.
그래서 호스트의 네트워크를 그대로 사용하기 때문에 보안이 조금 떨어진다는 단점이 있습니다.
그럼 host는 언제 사용할까요?
예를 들어서 미디어 서버를 사용해서 WebRTC를 구현한다면 미디어 서버는 엄청나게 넓은 범위의 포트를 열어둬야합니다. 또한, 미디어 서버 뿐만 아니라 Turn 서버를 이용할 때도 엄청나게 넓은 범위의 포트가 필요하죠.
그때 만약 컨테이너로 포트를 열어두면 네트워크 통신에 비용이 많이 들기 때문에 이 때 host로 설정해두면 성능적인 이점을 많이 가져올 수 있습니다.
overlay
overlay는 여러개의 호스트가 있을 때 유용한데, 여기서 호스트란 위에서도 언급했지만 도커 데몬을 실행시키는 물리적 서버 혹은 클라우드 인스턴스를 의미합니다.
Docker Swarm이나 Kubernetes와 같은 컨테이너 관리 툴을 사용할 때 주로 사용되는 네트워크입니다. 일반적으로 사용되는 것은 아닌 것 같습니다.
none
none은 말 그대로 네트워크를 설정하지 않는 것입니다. 개인적으로는 이게 왜 필요한가 싶긴한데... 도커 네트워크 스펙으로 있으니까 일단 가져와봤습니다.
마치며
이렇게 도커 네트워크에 대해서 알아봤습니다. 도커 네트워크에 대해서 알아보면서 일반적인 네트워크 통신도 잠깐 알아봤습니다.
도커 아키텍처는 그림 한장 그것도 쉬운 그림으로 도커를 쓰는 누구나 이해시킬 수 있고 도커 네트워크조차 일반적인 CS지식이 있다면 이해하기 정말 쉬운 구조로 되어있는 것 같습니다.
다시한번 도커의 매력적인 능력에 감탄을 하며 이번 포스팅 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다.
출처
https://docs.docker.com/network/
CI / CD 자동화 (1) : Jenkins vs Github Action
이전 포스팅을 보고 오시면 더 이해하기 쉽습니다!
https://coding-review.tistory.com/410
이번 포스팅에선 Jenkins를 시작하는 방법에 대해서 포스팅해보도록 하겠습니다. 이전 포스팅에서 블루 그린 배포를 성공적으로 완료했지만 문제가 있었습니다.
그것은 바로 엄청나게 복잡한 배포 과정인데요.
잠깐 이전 포스팅 내용을 정리하자면 우리의 블루 그린 배포는 다음과 같은 과정으로 이뤄집니다.
• 스프링 프로젝트에서 업데이트
• git commit / push
• git pull
• gradlew build
• docker image build
• docker hub push
• docker-compose.yml 파일 변경
• deploy.sh 실행
이걸 도식화하면 다음과 같습니다.

이 과정을 한번 겪어보고 나니 한숨이 쉬어지더군요.

이 과정을 자동으로 처리하고 싶은 욕구가 마구 샘솟았습니다. 물론 그전에 Jenkins의 존재에 대해 알고 있었습니다. 때문에 Jenkins를 사용해야겠다고 마음 먹었습니다. 알아보니 Jenkins와 같이 CI 를 자동화해주는 Github Action이라는 기술도 있었습니다.
그래서 각각의 장단점에 대해서 알아봤는데요.


저는 여기서 Jenkins를 선택했습니다. 그 이유는 다음과 같습니다.
• 제 프로젝트는 실무에서 사용할 정도로 복잡한 CI / CD를 요구하지 않으므로 설정과 관리가 어려워 봤자 얼마나 어렵겠냐는 마인드로 결정했습니다.
• 또한 Github Action에서 발생하는 Job Scheduling Issue는 개인적으로 CI / CD에선 치명적이라고 생각합니다.
• Jenkins와 Github Action을 어떻게 사용하는지 구글링을 조금 해봤는데 Jenkins를 시작하는게 그리 어려워보이지 않았습니다.
복잡한 CI / CD인 경우 Jenkins를 사용하는게 유지보수가 힘들다는 말이 많았는데 저는 복잡한 프로젝트가 아니기 때문에 전혀 상관 없을 것 같았습니다.
그리고 또 결정적인 이유가 바로 이것입니다.

구글 트랜드에서 검색한 결과인데요. Jenkins가 압도적으로 많았습니다.
제가 CI / CD 자동화가 처음이었어서 아무래도 많이 선택하는 기술을 선택해야 초기에 학습하기 쉬울 것이라고 생각했고 트러블이 생기더라도 해결하기위한 레퍼런스가 많을 것으로 생각했습니다.
이런 탄탄한 생태계를 갖고있는 기술은 처음 시작하는 저같은 사람들에게 최적이라고 생각했습니다.
그래서 이어서 쓰게 될 포스팅은 모두 Jenkins에 대한 포스팅입니다. 다음 포스팅에서 바로 Jenkins를 시작해보도록 하겠습니다.
https://coding-review.tistory.com/414
CI / CD 자동화 (2) : Jenkins 시작하기
앞선 포스팅과 이어지는 내용입니다.
https://coding-review.tistory.com/413
저는 EC2 환경에서 Linux 22.04 LTS 버전을 이용해 Jenkins를 구축했습니다. 이 점 참고하시기 바랍니다.
Jenkins를 우분투 환경에서 시작하는 방법에는 두가지가 있습니다.
• 직접 설치
• docker를 이용해 설치
Jenkins를 직접 우분투에 설치하면 방화벽 설정을 비롯해 여러가지 설정을 만져줘야 하기 때문에 굉장히 귀찮은데요.
그래서 저는 아무런 설정도 할 필요가 없는 도커를 이용해서 Jenkins를 시작했습니다. 도커를 이용하면 사전 준비가 상대적으로 편해서 진입장벽이 굉장히 낮아지는 특징이 있습니다.
그럼 이제 본격적으로 시작해보겠습니다.
Jenkins 시작하기
1. 도커, 도커 컴포즈 다운
그럼 먼저 도커와 도커 컴포즈를 다운받습니다.
$ apt-get update
$ apt-get install docker
$ apt-get install docker-compose
2. Jenkins 이미지 내려받기
$ docker pull jenkins/jenkins:lts
이미지를 다운 받고 이제 도커 컴포즈를 작성해야겠죠.
3. docker-compose.yml 파일 작성
version: "3.8"
services:
jenkins:
image: jenkins/jenkins:lts
container_name: jenkins_container
user: root
volumes:
- ./volumes:/var/jenkins_home
- /home/ubuntu/project:/var/jenkins_home/project/deploy
- /home/ubuntu/project/capston:/var/jenkins_home/project/capston
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8082:8080
제가 작성한 도커 컴포즈입니다. 여기서 가장 중요한 것이 바로 마운트하는 volumes 부분입니다.
• ./volumes:/var/jenkins_home : 이 코드는 혹여 Jenkins 컨테이너가 혹시모를 상황에 죽어버려도 다시 docker-compose를 이용해 띄울 때 설정 정보를 그대로 가져가기 위해서 적는 코드입니다.
• /home/ubuntu/project:/var/jenkins_home/project/deploy : 왼쪽에 있는 /home/ubuntu/project는 제 디렉토리인데요. 이 디렉토리에는 실제로 실행해야하는 make-docker-compose.sh 파일이나 deploy.sh 그리고 각종 docker-compose 파일들이 들어있습니다. Jenkins를 띄운 다음에 파이프라인을 작성할 때 반드시 필요한 디렉토리이죠.
• /home/ubuntu/project/capston:/var/jenkins_home/project/capston : 위의 명령어와 같습니다.
• /var/run/docker.sock:/var/run/docker.sock : 이것도 만만치않게 중요한 코드입니다. 지금 우리는 도커로 Jenkins를 띄워서 사용하고 있죠. 그리고 파이프라인은 Jenkins 컨테이너 안에서 docker-compose 나 docker 명령어를 실행합니다.
그런데 문제가 있습니다. 컨테이너 안에서 docker-compose를 실행하면 컨테이너 안에 컨테이너가 또 만들어질 것 아니에요? 이렇게 되면 문제가 발생할 수 있습니다.
그래서 docker는 컨테이너 내부에서 소켓을 열어서 컨테이너 외부에 있는 Docker와 소통하는 방법을 제공합니다. 그게 바로 이 방식이구요. 이 방식을 DooD (Dock out of Docker) 라고 합니다.
• ports : 마지막으로 포트인데 제가 지금 프로젝트를 블루 버전을 8080 그린 버전을 8081로 쓰고 있어서 Jenkins는 8082로 설정했습니다.
이렇게 도커 컴포즈를 작성하고 Jenkins 컨테이너를 띄워보겠습니다.
$ docker-compose -f ./docker-compose-jenkins.yml up -d
정상적으로 컨테이너가 띄워졌다면 EC2 퍼블릭 IP:8082 를 주소창에 치고 들어가면 이와 같은 모습이 나옵니다.

이는 Jenkins를 사용하기 위해서 초기 비밀번호를 입력하라는 뜻입니다. 암호를 푸는 방법은 두가지입니다.
• 로그를 확인해서 들어가기 (제일 간단함)
• 컨테이너 내부에서 확인하기 (조금 귀찮)
두가지 모두 소개해 드릴건데 개인적으로 1번을 추천합니다. 가장간단하거든요.
$ docker container logs ${컨테이너 이름}
# 저는 컨테이너 이름이 jenkins_container여서 jenkins_container를 치면 됩니다.
# 컨테이너 이름은 우리가 아까 만들었던 docker-compose에 명시할 수 있습니다.
# 혹은 docker ps 명령어를 치고 나와있는 컨테이너 이름을 확인하시거나 컨테이너 ID를 입력하시면 됩니다.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 부분이 암호입니다. 저부분 그대로 복사하셔서 입력하시면 됩니다.
두 번째 방법은 컨테이너 내부로 들어가는 방법인데요.
먼저 컨테이너 내부로 들어가줍니다.
$ docker exec -it jenkins_container bash
$ cat /var/jenkins_home/secrets/initialAdminPassword
이렇게 치고 나온 결과를 복사하시면 됩니다.
그럼 이제 Jenkins에 들어갈 수 있습니다.

Jenkins에 들어가면 이런 창이 뜨는데 Install suggested plugins 를 클릭해줍니다. Jenkins에서 추천해주는 플러그인을 자동으로 다운받을 수 있습니다.
이제 기본 설정을 해봅시다.
4. Jenkins 기본 설정
우리는 우선 AWS의 private key와 git hub을 credential로 등록할겁니다. 그러기위해선 아래 보이는 스샷처럼 Jenkins 관리를 들어가줍니다.




Scope : Global로 냅두세요.
ID : 이 credential의 이름입니다.
Description : 써도 되고 안써도 되는데 저는 간단하게 설명을 써놨습니다. ssh credential 이라고 적었습니다.
Username : Jenkins에서 만들라고 하는 Username을 적었습니다. 저는 garlicpollpoll 이었구요.

Enter directly를 누르고 Add 버튼을 누릅니다.
그럼 아래 입력창이 뜨는데 여기다가 aws ssh private key를 넣을겁니다.
EC2 콘솔로 돌아와서요. 아래의 명령어를 작성합니다.
$ cat /home/ubuntu/.ssh/id_rsa
아마 github와 연동할 때 ssh 키를 만들었다면 존재하겠지만 없으신 분들은 아래와 같이 하시면 됩니다.
$ ssh-keygen -t rsa
패스워드 입력하라고 하면 원하는 패스워드 입력하시고 엔터 치시면 위와같은 디렉토리에 저장이됩니다.
cat 명령어로 쳐서 나온 어마어마하게 긴 문자열을 반드시 처음부터 끝까지 복사해줍니다. 이 말은 BEGIN OPENSSH PRIVATE KEY 라고 써있는 부분 전부 다를 의미합니다.
마지막으로 Passphrase는 생략하고 create 하시면 하나 완성!
이제 Github도 만들어보죠.
똑같이 Add Credential하시고 이번엔 SSH 가 아니고 Username with password를 누르셔야합니다.
Username : github 아이디를 적으시면 됩니다.
Password : github 패스워드 적으시면 됩니다.
ID : 이 Credential의 고유 ID입니다. 저는 Github Credential이라고 적었습니다.
Description : 적으셔도 되고 안적으셔도 됩니다. 저는 Github이라고 적었습니다.
자 이제 모든 준비가 끝났습니다.
엄청 길었습니다. 긴 글 읽어주셔서 감사합니다. 다음 포스팅에선 Jenkins를 이용해 파이프라인을 만드는 방법에 대해서 알아보죠!
https://coding-review.tistory.com/415
CI / CD 자동화 (3) : Jenkins 파이프라인 작성하기
이전 포스팅과 이어집니다.
https://coding-review.tistory.com/414
이제 본격적으로 파이프라인을 만들어보겠습니다.



Github project를 클릭하시고 아래에 자신의 github 리포지토리 주소를 입력합니다.
주의해야하는 점은 맨 마지막에 / (슬래쉬) 반드시 넣으셔야 오류가 안납니다.
그리고 오래된 빌드 삭제를 누르시는데 이건 해도 되고 안해도 됩니다만 예전 로그까지 볼 필요는 없을 것 같아서 선택했습니다.

저는 그 숫자가 10개가 적당해보여서 10개로 입력했습니다.

Definition을 Pipeline script로 선택해주시고 맨 아래 Use Groovy Sandbox를 선택 해제합니다.
그리고 이제 Script안에 파이프라인을 적어야합니다.
pipeline {
agent any
stages {
stage('Github') {
steps {
git branch: 'master',
url: 'https://github.com/garlicpollpoll/capston.git'
}
}
stage('Build') {
steps {
sh '''
chmod 777 ./gradlew
./gradlew build
'''
}
}
stage('DownloadDockerAndDockerCompose') {
steps {
sh '''
apt-get update
apt-get install docker -y
apt-get install docker-compose -y
usermod -aG docker jenkins
'''
}
}
stage('MakeDockerImage') {
steps {
sh '''
cp /var/jenkins_home/workspace/capston-pipline/build/libs/capston-0.0.1-SNAPSHOT.jar /var/jenkins_home/project/deploy
cd /var/jenkins_home/project/deploy
chmod 777 ./make-docker-image.sh
./make-docker-image.sh
'''
}
}
stage('Deplolyment') {
steps {
sh'''
cd /var/jenkins_home/project/deploy
chmod 777 ./deploy.sh
./deploy.sh
'''
}
}
}
}
문법은 대충 어떤 느낌인지 감이 오실거라고 생각합니다.
제 파이프라인을 설명해보자면
• Github : 제 리포지토리 url의 master branch를 가져옵니다. 이 말은 git pulling을 하겠다는 의미입니다.
• Build : gradlew 파일의 권한을 슈퍼권한을 준 다음 gradlew를 빌드합니다. 한번 슈퍼 권한을 주면 다음엔 안줘도 되는데 혹여 Jenkins 컨테이너가 죽는다던가 새로 올린다던가 하는 상황에서 언제든지 같은 동작을 하게끔 멱등성을 유지하고싶어서 넣었습니다.
• DownloadDockerAndDockerCompose : 이것도 위의 Build 스텝이랑 비슷한 느낌인데 컨테이너가 다시 끼워지더라도 같은 동작을 하기 위해서 넣은 파이프라인입니다. 참고로 apt-get 명령어는 이미 패키지가 존재하면 다운로드를 하지 않는다는 특징이 있습니다.
• MakeDockerImage : Github 스텝에서 git pulling이 잘 되었다면 pulling한 파일들은 /var/jenkins_home/workspace/${pipeline 이름} 에 저장됩니다. 이 디렉토리에 있는 build/libs/capston-0.0.1-SNAPSHOT.jar를 /var/jenkins_home/project/deploy 에 복사합니다.
그리고 이 복사한 JAR파일을 기반으로 Docker Image를 만드는데 make-docker-image.sh 의 로직은
1. blue 버전의 컨테이너가 띄워져있는지 확인합니다.
2. 띄워져 있다면 garlicpollpoll-capston-green 이라는 이름의 이미지를 만듭니다.
3. 띄워져 있지 않다면 garlicpollpoll-capston-blue 라는 이름의 이미지를 만듭니다.
• Deployment : 이 스텝에선 deploy.sh 가 실행됩니다. 제가 적은 deploy.sh에 대해 궁금하신 분은 아래의 링크를 확인해주세요!
https://coding-review.tistory.com/408
그리고 이제 한번 확인해봐야겠죠?
잘 동작합니다.
마치며
Jenkins가 유지보수 극악이라고 얘기해서 살짝 겁먹었는데 역시 저처럼 헬로월드 수준의 CI / CD는 문제없네요.
이렇게 Jenkins를 이용해서 CI / CD 자동화를 알아봤습니다. 저는 단계적으로 배포를 진행해서 그런가 더 뿌듯했습니다.
• Cafe 24로 WAR 파일 배포
• AWS EC2 배포
• EC2 + Docker 배포
• EC2 + Docker + Nginx 무중단 배포
• EC2 + Docker + Nginx + Jenkins 무중단 배포 자동화
정말 뿌듯한 프로젝트였습니다. 한 주동안 정말 행복했구요 제 생각에 신입이 할 수 있는 가장 마지막 단계가 CI / CD 자동화가 아닌가 생각합니다. 마지막을 장식하는데 이정도면 충분해보입니다.
여러분도 마지막 대미를 장식하는데 Jenkins를 이용해 자동화를 진행해보면 좋겠습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
Graceful Shutdown (feat. Hard Shutdown)
여러분들이 배포를 진행하는데 어떤 배포 방법을 사용하는지에 따라 다르겠지만 기존 배포했던 서버를 내려야 하는 상황이 생길 것입니다.
만약 블루그린 배포를 진행했다고 가정하면 새로 올릴 서버를 띄운 다음 기존 서버를 죽여야하는 상황이 생기겠죠? 그 때 그냥 서버를 죽여버리면 사용자는 굉장히 난처한 상황에 놓일 것입니다.
혹시라도 그 사용자가 결제를 진행하고 있다가 서버가 내려가서 결제가 취소된다면 부정적인 사용자 경험으로 이어질 수 있습니다.
하지만 그런 상황이라면 조금 기분 나쁘고 말겠지만 결제가 진행되고 있는 도중 (API에 요청을 보내서 처리하고 있던 도중) 서버가 내려가버리면 이건 예외상황에 의해 트랜잭션이 롤백되는 것이 아니기 때문에 개발자 입장에서도 처리하기 난처한 상황입니다.
그렇기 때문에 Graceful Shutdown 우리말로 우아한 종료를 진행해야 합니다. 이제 Graceful Shutdown이 어떤 것인지 한번 알아보도록 하죠.
Graceful Shutdown
Graceful Shutdown은 아주 간단한 개념입니다. 쉽게 설명하자면 천천히 종료하는 것 입니다. 구현하는 것도 정말 간단합니다. 스프링부트에 내장되어 있는 웹 서버들 (e.g Tomcat) 이 Graceful 을 지원하고 있기 때문이죠.
이 Graceful Shutdown이 작동하는지 직접 눈으로 확인해보도록 하겠습니다.
@RestController
@Slf4j
public class TestController {
@GetMapping("/log/test/{id}")
public String test(@PathVariable("id") Long id) throws InterruptedException {
log.info("Process Start " + id);
Thread.sleep(20000);
log.info("Process End " + id);
return "ok";
}
}
테스트를 위해 컨트롤러를 만들고 프로세스가 시작되고 20초뒤에 종료되는 로직을 준비해봤습니다.
정상적으로 종료가 된다면 마지막 Process End 로그가 찍히겠죠?
저는 Graceful 을 테스트하기 위해 테스트 API에 GET 요청을 보내자마자 kill 명령어를 통해 해당 포트를 죽일것입니다. 그 때 Graceful 이 적용되기 전과 후를 비교해보겠습니다.
우선 로그가 잘 찍히는지 확인해봐야겠습니다. 서버를 띄우고 해당 API로 요청을 보내보겠습니다.

정상적으로 잘 작동합니다.
이제 요청을 보내자마자 kill 명령어를 통해 8080포트를 죽이겠습니다.

시작 로그만 찍히고 종료 로그가 찍히지 않았습니다.
자 이제 Graceful 을 구현해봅시다.
Spring Boot 에서 Graceful Shutdown
스프링 부트에서는 정말 간단하게 설정 하나만으로 Graceful을 구현할 수 있습니다.
먼저 application.yml 파일에

이렇게 적어주면 끝입니다.
자 이제 다시 시작해서 똑같이 요청을 보내고 kill로 죽여보겠습니다.

오오... Graceful Shutdown이 적용됐습니다.
※주의!
이 작업을 윈도우 환경에서 실행하면 원하는 결과가 나오지 않습니다!
윈도우에서 만약 8080포트를 죽이려고 한다면 강제종료하는 수밖에 없기 때문에 무슨 수를 써도 시작 로그만 찍히고 강제종료 해버립니다.
때문에 리눅스 환경에서 kill -15 {pid} 명령어를 통해 작업을 종료해야 원하는 결과를 얻을 수 있습니다.
마치며
블루 그린 배포를 진행했을 때 주의해야 하는 점을 공부하면서 Graceful Shutdown을 구현해야 한다는 얘기를 듣고 조금 겁먹었지만 생각보다 아무것도 아니라 조금 당황했습니다.
하지만 간단하다고 중요하지 않은 건 아니죠. Graceful Shutdown은 정말 중요한 옵션이니 배포를 진행했다면 반드시 진행해야 할 것입니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://bravenamme.github.io/2020/10/06/graceful-shutdown/
클래스 로더 warm up / JIT 컴파일러 warm up
이번 포스팅에선 warm up에 대해서 알아보도록 하겠습니다. 자바에선 JVM의 존재 때문에 항상 느린 언어 취급을 받아야 했습니다 (실제로도 느린건 사실입니다).
하지만 그런 느린 언어라는 누명을 벗기위해 다양한 노력을 시도했습니다. 언어적으로 노력한 부분도 있고 자바를 사용하는 개발자들의 꼼수(?)로 자바는 생각보다 컴파일언어 (C, C++, Rust) 와 견줄만한 속도를 가지게 되었습니다.
우리는 이번 포스팅에선 warm up 이라는 개념이 왜 등장했는지 클래스 로더와 JIT 컴파일러의 관점에서 알아보고, warm up을 어떻게 진행하는지 방법에 대해서 알아보도록 하겠습니다.
클래스 로더
자바에서는 클래스를 읽어오기 위해 클래스 로더를 이용합니다. 클래스 로더는 클래스 파일을 찾고, 메모리에 로드해 실행 가능한 상태로 만드는 역할을 합니다.
클래스 로더는 아래와 같은 작업을 통해 동작합니다.
• Class Loading : 클래스 파일을 가져와 JVM 메모리에 적재합니다. 이 단계도 JVM 기본 클래스와 자바 코드를 로딩하는 Bootstrap Class Loading, 자바 핵심 라이브러리를 로딩하는 Extension Class Loading, 개발자가 직접 작성한 classpath를 로딩하는 Application Class Loading으로 나뉩니다.
• Class Linking : 클래스가 참조하는 다른 클래스, 메서드, 필드 등을 확인하고 필요하면 메모리 상에서 연결하는 단계입니다. 이 단계도 크게 Verification, Prepare, Resolution 단계로 나뉩니다.
• Class Initialization : 클래스 변수를 초기화 하거나 static 블록 내의 코드를 실행하는 등의 클래스 초기화 작업을 수행합니다.
이런 자세한 부분까지는 알 필요 없고 그냥 클래스 로더가 동작하기 위해서는 많은 자원이 소모되는 큰 작업이라는 것만 아시면 됩니다.
실제 애플리케이션에 있는 모든 클래스를 클래스 로더를 통해 JVM에 올리게 되면 굉장한 부하가 생기기 때문에 클래스 로더는 독특한 방식을 사용합니다.
바로 Lazy Loading 이죠 지연 로딩이라고도 불리는 이 작업은 JPA를 공부해보셨다면 한번쯤 들어보셨을 익숙한 단어일 것입니다.
간단하게 설명해서 JVM에서 모든 클래스를 한번에 로드하는게 아니라 필요할 때마다 로드해서 성능을 최적화시켰다는 얘기입니다.
이 말은 맨 처음 호출할 때는 Latency가 발생할 수 있다는 얘기입니다.
JIT 컴파일러
왜 클래스 로더 얘기하다가 갑자기 JIT 컴파일러로 넘어왔나 싶을 수도 있습니다. 하지만 JIT 컴파일러 역시 클래스 로더와 비슷한 문제를 겪고 있기 때문에 바로 다음에 소개해보도록 하겠습니다.
우선 JIT 컴파일러가 뭔지 간단하게 설명하고 자세히 들어가도록 하죠.
JIT 컴파일러는 자주 실행되는 부분의 코드를 바이트코드에서 네이티브 코드로 컴파일합니다. 이렇게 자주 실행되는 부분을 HotSpot이라고 부릅니다.
때문에 이런 JIT 컴파일러를 통해 자바는 컴파일 언어들 (C, C++, Rust) 와 비슷한 성능을 보여줍니다.
간단하게 설명하면 JIT 컴파일러가 없는 인터프리터 언어는 영어 원서를 한줄 한줄 해석해서 읽는 것이고 JIT 컴파일러가 있는 인터프리터 언어는 번역본을 읽는 속도입니다.
여기까지는 기본적인 얘기였고 여기까지만 아셔도 사실 상관 없습니다. 더 자세한 내용은 아래의 링크를 확인해주세요!
https://coding-review.tistory.com/123
이제 좀 딥한 내용으로 들어가보죠
C1 컴파일러, C2 컴파일러
JIT 컴파일러는 C1 컴파일러 C2 컴파일러로 나뉘어져 있습니다.
C1 컴파일러는 클라이언트 컴파일러라고도 불리고 JIT 컴파일러가 시작되는 초반에 성능을 끌어올리기 위해 만들어진 컴파일러입니다.
C2 컴파일러는 서버 컴파일러라고도 불리고 JIT 컴파일러의 전반적인 성능을 최적화하기 위해 만들어진 컴파일러입니다. C2 컴파일러는 C1에 비해 더 오랜 기간 남아있는 코드들을 관찰하고 분석합니다. 이는 C2를 더 나은 컴파일된 코드로 최적화합니다.
JIT 컴파일러는 Tiered Compilation 이라고 부르는 단계를 거쳐서 성능을 최적화합니다.
애플리케이션이 시작할 때 JVM은 초기에 모든 바이트코드를 해석하고 이 바이트코드르에 대한 프로파일링 한 정보를 모읍니다.
처음에 JIT 컴파일러가 자주 실행되는 코드들을 C1 컴파일러가 컴파일해서 빠르게 성능을 끌어올리고 이후 C2가 더 자주 사용되는 코드들을 모아서 다시 재컴파일합니다. 그렇게 함으로써 성능이 한 단계 더 끌어올려지는 것이죠.

여기서 드는 의문은 얼마나 자주 사용되어야 C1, C2 컴파일러가 반응할까? 에 대한 내용입니다.
https://if.kakao.com/2022/session/35
위의 링크는 카카오에서 겪었던 배포 후 Latency 문제를 해결한 내용입니다. 해당 영상에서는 C1 컴파일러는 250회 반복되었을 때, C2 컴파일러는 1000회 반복되었을 때 반응했다고 발표했습니다.
Warm Up
클래스 로더와 JIT 컴파일러의 공통점에 대해서 눈치채셨나요?
바로 어느정도 시간이 있어야 본 성능을 보여준다는 것입니다.
클래스 로더의 지연 로딩, JIT 컴파일러의 최소 250회 반복조건은 애플리케이션이 초기에 배포되는 시점에 큰 장애물이 됩니다.
위의 카카오 If 카카오 발표에서 문제에 대한 분석을 말해주는데 CPU, 메모리, 외부 데이터베이스 호출 어떤 곳에서도 오버헤드가 발생하지 않았다는 내용이 나옵니다.
때문에 카카오 모빌리티 팀은 애플리케이션 내부에 문제가 있다고 판단, JVM의 문제로 귀결시켰죠.
결국 JVM의 존재 때문에 한번 배포가 진행된 다음엔 제 성능을 보여주지만 맨 처음엔 10초가까이 되는 Latency가 발생했다는 것이 카카오 모빌리티 팀의 내용입니다.
때문에, 배포를 할 때 warm up 의 중요성에 대해서 한번 더 강조됩니다.
빠르게 뛰어야하고 폭발적인 근육을 사용하는 단거리 운동에서 러닝이나 가벼운 근력운동으로 근육을 예열시키는 것이 warm up에 해당하는 내용이라고 할 수 있습니다.
JVM도 이와 마찬가지로 클래스 로더의 지연 로딩, JIT 컴파일러의 반복 조건을 만족하기 위해 배포되기 전에 warm up 하는 과정이 필요합니다.
이제 각각 어떻게 warm up 하는지에 대해서 알아보죠.
클래스 로더 warm up
클래스 로더를 warm up 하기 위해서 꽤 오랜 시간이 걸리는 로직을 준비했습니다.
바로 100만행의 데이터를 풀스캔해 맨 마지막 100만번째 데이터를 JPA로 호출하는 것입니다. 클래스 로더를 warm up 하지 않고 순수한 지연 시간을 확인해보도록 하겠습니다.
참고) 지연시간 확인은 스프링의 StopWatch를 사용했습니다.

100만행 기준 맨 마지막 로우를 검색한 결과 0.03초라는 꽤 긴 시간이 걸렸습니다.
이제 클래스 로더를 예열시켜봅시다.
@Component
@RequiredArgsConstructor
@Slf4j
public class WarmupRunner implements ApplicationRunner {
private final ItemRepository itemRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
try {
itemRepository.findById(1L);
} catch (Exception e) {
log.error("Warm Up Error");
}
}
}
스프링에선 간단한 방법으로 예열을 시켜줄 수 있는데요. 바로 ApplicationRunner를 구현하는 것으로 예열할 수 있습니다.
이 구현체의 run 메서드에선 스프링이 가동될 때 한번 실행해주는 것이 전부입니다. 이렇게 한번 실행하게 되면 지연 로딩이었던 클래스가 JVM에 로드되면서 속도를 높일 수 있죠.
이제 다시 테스트해보도록 하겠습니다.

자세히 보시면 JPA 로그가 두개 찍힌 것을 볼 수 있습니다. 처음 스프링이 로드될 때 해당 메서드를 한번 실행함으로써 JVM에 클래스를 로드하였다는 증거입니다.
그리고 실제로 실행했을 때는 앞서 봤던 0.03초보다 훨씬 상회하는 0.002초로 약 150배 성능이 향상된 것을 볼 수 있습니다. 퍼센트로 본다면 약 6퍼센트의 성능 향상이 있었습니다.
JIT 컴파일러 warm up
JIT 컴파일러의 warm up은 JIT 컴파일러의 최소 반복 조건인 250회를 반복하는 것이 우리의 최종 목표입니다.
그렇다고 애플리케이션 내부적으로 250회 반복문을 돌리는 것은 의미없습니다. JVM의 부하만 있을 뿐이죠.
왜냐하면 JVM과 JIT 컴파일러는 실제 workload만 최적화하기 때문입니다. JVM과 JIT 컴파일러는 프로그램의 실제 실행을 모니터링하고 실제로 사용되는 코드들만 최적화를 진행합니다.
그렇기 때문에 애플리케이션 내부에서 여러번 실행하는 것은 의미가 없습니다. 직접적으로 JIT 컴파일러의 warm up을 진행하고 싶다면 리눅스의 curl 명령어를 통해 실질적인 API 호출이 있어야 warm up 효과를 받을 수 있습니다.
때문에 이를 해결하기 위해선 Jenkins와 같은 CI 자동화 툴에서 파이프라인을 구축해 warm up 을 진행하는 것이 올바른 방법입니다.
하지만 여기서 의문이 들 수 있습니다.
애플리케이션이 완전히 뜨고 warm up을 해야하는데 그러는 도중에 사용자의 요청이 들어오면 CPU가 터질듯이 부하가 생기지 않을까?
맞습니다. 때문에 카카오 모빌리티 팀에서도 warm up을 하는 도중에는 HTTP status 400을 내려줌으로써 아직 서버가 활성화되지 않았다는 것을 외부에 알렸다고 합니다.
그리고 정상적으로 warm up이 완료 되었으면 HTTP status 200을 내려줌으로써 실제 배포를 진행했다고 합니다.
이를 한번 구현해보도록 하겠습니다.
먼저 curl 명령어를 이용해 250번 예열을 해보겠습니다.
1. warmup.sh 파일 생성
#!/bin/bash
DOCKER_APP_NAME='garlicpollpoll-capston'
EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f ./docker-compose-blue.yml ps | grep Up)
if [ -z "$EXIST_BLUE" ]; then
for var in 250
do
curl -X POST 3.38.103.33:8081/warmup/item/list
sleep 10
done
curl -X POST 3.38.103.33:8081/warmup/completed
else
for var in 250
do
curl -X POST 3.38.103.33:8080/warmup/item/list
sleep 10
done
curl -X POST 3.38.103.33:8080/warmup/completed
fi
echo "warm up is done"
jenkins 로 자동화를 위해서 linux 서버를 오랜만에 띄웠습니다. 위의 코드처럼 warmup.sh 파일을 만들어줍니다.
중간에 저 sleep은 warm up 테스트를 위해 집어넣은 것입니다. 실제 코드에서는 빼야합니다.
2. jenkins 파이프라인 구축
pipeline {
agent any
stages {
stage('Github') {
// get in github stage
}
stage('Build') {
// build stage
}
stage('DownloadDockerAndDockerCompose') {
// download docker and docker compose stage
}
stage('MakeDockerImage') {
// make docker image stage
}
stage('Deplolyment') {
// deploy stage
}
stage('Warm Up') {
steps {
sh'''
cd /var/jenkins_home/project/deploy
chmod 777 ./warmup.sh
./warmup.sh
'''
}
}
}
}
jenkins 파이프라인에 대해서 궁금하신 분들은 아래의 링크를 참고해주세요!
https://coding-review.tistory.com/415
3. 인터셉터 만들기
@Component
public class ReadinessInterceptor implements HandlerInterceptor {
private boolean isReady = false;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!isReady && !request.getRequestURI().equals("/warmup/completed")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Application is not ready yet");
return false;
}
return true;
}
public void setReady(boolean ready) {
isReady = ready;
}
}
이 인터셉터는 warm up이 진행되는 도중에는 400을 내려주고 warm up이 끝나면 200을 내려줄겁니다.
4. warm up 전용 API 만들기
@RestController
@RequiredArgsConstructor
@Slf4j
public class WarmUpController {
private final ItemRepository itemRepository;
private final ReadinessInterceptor interceptor;
@PostMapping("/warmup/item/list")
public ResponseEntity<?> warmupItemList() {
PageRequest page = PageRequest.of(0, 9);
List<Item> findAllItem = itemRepository.findAllItem(page);
itemRepository.count();
return new ResponseEntity<>(new WarmupDto("warmup execute"), HttpStatus.ACCEPTED);
}
@PostMapping("/warmup/completed")
public ResponseEntity<?> warmupComplete() {
interceptor.setReady(true);
return ResponseEntity.ok(new WarmupDto("warmup success"));
}
}
먼저 /warup/item/list 라는 API로 warm up을 진행합니다. 그리고 모든 warm up이 끝나면 /warmup/completed 라는 API로 curl 명령어를 쏴서 이전 인터셉터의 isReady 부분을 true로 바꿔주어 이후 모든 요청을 200으로 내려줄겁니다.
5. 인터셉터 등록
@Component
@RequiredArgsConstructor
public class WarmupConfig implements WebMvcConfigurer {
private final ReadinessInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).excludePathPatterns("/css/**", "/image/**", "/js/**", "/warmup/completed");
}
}
보통 인터셉터를 등록할 때 addInterceptor 안에 new 연산자를 이용해 인터셉터를 등록하지만 저희는 그렇게 하면 isReady 값이 항상 false가 되어버려 warm up이 종료되고 나서도 400 코드로 응답합니다.
때문에 인터셉터를 스프링 빈으로 등록하고 인터셉터를 등록할 때 주입을 받아서 추가해주면 이후 isReady 값이 true로 변경되어도 싱글톤으로 빈이 등록되었기 때문에 이후 요청은 200으로 응답받게 됩니다.
이제 테스트 해봐야겠죠?

Jenkins 스케줄링을 보시면 warmup이 진행되는 중입니다. 이 때 API 요청을 보냈을 때 보시다시피 400코드로 내려주는 것을 확인할 수 있습니다.

모든 스케줄링이 끝나고 다시 요청한 결과 200코드로 내려주는 것을 확인했습니다.
마치며
이번에는 warm up에 대해서 알아봤습니다. 이전 Graceful Shutdown과 마찬가지로 warm up은 배포 과정에서 반드시 짚고 넘어가야할 과정이기 때문에 한번 공부해봤습니다.
제 프로젝트는 단순히 서버가 내려갔다 올라갈 뿐이고 그렇게 해도 상관없는 프로젝트이지만 컨셉이 사용자 수가 1000만명이 된다면 어떻게 될까를 가정하고 만드는 프로젝트이기 때문에 반드시 고려해야할 사항이라고 생각합니다.
여기까지 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://hudi.blog/jvm-warm-up/
https://www.baeldung.com/jvm-tiered-compilation
https://velog.io/@dbwogml15/JIT-COMPILER-JVM-WARM-UP
쿠버네티스 기초 (키워드, 아키텍처, 흐름)
개인적으로 쿠버네티스에 관심이 있어서 표면적인 지식을 학습하려고합니다. 쿠버네티스는 구글에서 개발한 오픈소스 컨테이너 오케스트레이션 도구이며 구글에서 가이드라인을 주기로는 행성급 애플리케이션(약 10억개의 컨테이너)에 어울리는 기술이라고 합니다.
때문에, 웬만한 기업에선 쿠버네티스를 사용하는 것이 오히려 유지보수적인 측면에서 독이 될 정도로 사용하기 까다로운 기술 중 하나입니다.
신입인 저는 공부하는 것이 오버스펙이라고 주변에서 뜯어말리지만 공부는 그냥 자기가 하고싶은거 하는게 최고이지 않겠습니까 공부해서 손해보는 공부는 없으니까요. 그래서 사람들 말 무시하고 제가 하고싶은 공부 하기로 했습니다.
쿠버네티스란?

위의 사진은 개발 단계를 표현한 그림입니다.
맨 처음 전통적인 개발 방식은 하드웨어에 OS를 설치하고 그 위에 바로 애플리케이션을 설치하는 과정을 거쳤습니다.
하지만 가상화 기술이 발달하고 OS위에 가상 머신을 두어 그 가상 머신에 애플리케이션을 설치하게 되었죠. 하지만 이 가상 머신들은 구현하기위해 자원을 많이 소모해야 하기 때문에 비효율적이라는 말이 있었습니다.
이를 해결하기위해 컨테이너 기반의 개발방식이 개발되었고 이는 많은 사람들이 컨테이너를 사용하게 되는 계기가 되었습니다.
잠깐! 컨테이너?
컨테이너는 각각이 격리된 환경이라고 생각하시면 됩니다. 이 컨테이너에는 우리의 애플리케이션이 놓이게 되고 실행됩니다.
컨테이너들은 경량화 되어있고 애플리케이션을 구동하는데 필요한 모든 것(코드, 런타임, 라이브러리 등등)을 가지고 있습니다.
이제 본격적으로 쿠버네티스에 들어가보도록 하겠습니다.
쿠버네티스의 키워드
• Pod (팟) : 쿠버네티스는 컨테이너들을 직접적으로 다루지 않습니다. 대신 쿠버네티스는 팟이라고 부르는 것을 이용해 컨테이너들을 관리합니다. 팟은 쿠버네티스 안에서 배포할 수 있는 가장 작은 유닛이며 스토리지나 네트워킹같은 자원들을 공유하는 하나 혹은 더 많은 컨테이너를 가지고 있을 수 있습니다.
• Node (노드) : 팟은 노드 위에서 동작하는데 노드는 쿠버네티스에서 워커 머신입니다(워커 머신, 워커 노드에 대해서는 뒤에서 더 서술하도록 하겠습니다.). 그리고 이 워커 머신은 물리적 머신일 수도 있고 가상 머신이 될 수도 있습니다. 각각의 노드들은 팟들을 실행하고 마스터에 의해 관리됩니다.
• Master (마스터) : 마스터는 쿠버네티스의 컨트롤 플레인 (컨트롤 타워라고 보시면 됩니다.) 마스터는 클러스터에 대한 전역적인 결정을 내리고 클러스터 이벤트를 감지하고 응답합니다. 여기서 클러스터 이벤트란 장애 상황에서 팟이 내려갔다가 다시 생성되는 상황같은 것을 말합니다.
• Deployment (배포) : Deployment는 우리가 원하는 만큼 동작시킨 팟들을 말합니다. Master 안에 있는 Deployment Controller는 원하는 수의 팟들을 항상 이용 가능하게 동작시키는 것을 보장합니다.
• Services (서비스) : 쿠버네티스 서비스들은 논리적으로 정의한 일련의 팟들이거나 그 팟들에 접근할 수 있는 정책을 추상화한것입니다. 이 서비스들은 마이크러 서비스로 생각할 수 있고 서비스에 의해 타겟팅 된 일련의 팟들은 보통 Label Selector에 의해 결정됩니다.
• Kubectl : Kubectl은 우리의 쿠버네티스 클러스터들에 대하여 명령을 실행하는 커맨드라인 툴입니다. 우리는 kubectl을 사용해서 애플리케이션을 배포하고 클러스터 자원들을 관리하고 로그를 볼 수도 있습니다.
• Namespace : 쿠버네티스는 여러개의 가상 클러스터들을 지원하는데 이것들은 같은 물리적 클러스터를 지원해줍니다. 이러한 가상화 클러스터는 namespace라고 부르고 이것들은 리소스를 논리적으로 이름이 지정된 그룹으로 분할할 수 있습니다.
• Storage & Config : 쿠버네티스는 우리의 팟을 위한 저장 공간을 관리할 수 있습니다. 그리고 우리의 팟으로 설정 정보들을 주입할 수도 있습니다.
• Self-healing : 쿠버네티스는 장애 상황에서 컨테이너를 재실행할 수도 있고 노드가 죽었을 때 팟을 리스케줄링하거나 대체할 수 있고 유저 (여기선 개발자)가 정의한 헬스체크에 제대로 응답하지 않는 컨테이너를 죽여버릴 수도 있습니다.
• Scaling & Load Balancing : 쿠버네티스는 애플리케이션을 필요한만큼 스케일 업하거나 스케일 다운할 수 있습니다. 그리고 안정적인 애플리케이션 퍼포먼스를 위해 네트워크 트래픽을 분산시킬 수도 있습니다.
키워드가 좀 많네요 ㅎㅎ..
자 이제 쿠버네티스에 우리가 컨테이너를 올리고 애플리케이션을 배포했을 때 어떤 과정이 내부적으로 돌아가는지 간단하게 살펴보도록 하겠습니다.
쿠버네티스 흐름
1. Deployment Request
먼저 우리가 애플리케이션을 배포하기 위해서는 일반적으로 Deployment 설정을 만들어야합니다. 이 설정은 우리의 애플리케이션이 어떤 상태를 원하는지 묘사하는데 예를 들어서 어떤 컨테이너 이미지를 사용할 것인지, 얼마나 많은 레플리카 애플리케이션을 구동할 것인지 등등이 포함되어 있습니다.
그리고 이제 내부적으로 API를 요청하게 됩니다. 이 때 사용되는 것이 바로 API Server라고 합니다. AWS로 치면 API Gateway같은 것입니다.
API Server는 Master Node위에 존재하고 API Server는 요청 과정을 검증하고, etcd 데이터 스토어 안에서 원하는 상태를 업데이트 하는 과정이 포함됩니다.
2. Scheduling
자 이제 본격적으로 시작입니다. 쿠버네티스는 내부적으로 노드의 상태를 확인하는 스케줄러가 있습니다.
우리의 Deployment 설정으로 인해 컨테이너가 만들어지고 노드가 만들어지고 팟이 만들어지면서 애플리케이션의 컨테이너가 캡슐화 되고 스케줄러가 반응합니다.
스케줄러는 Master Node위에 존재하고 있고 스케줄러는 노드가 할당되어있지 않은 새로운 팟을 주의깊게 살펴보고 있다가 가동중인 노드 중 리소스가 사용가능한지, 제약사항은 없는지 같은 요인을 확인하고 선택해서 할당해줍니다.
3. Running the Pods
클러스터 안에 있는 각각의 노드들은 Kubelet을 구동시킵니다. Kubelet은 팟 안에 있는 컨테이너가 정상적으로 작동하고 있는지 확인하는 것에 책임이 있습니다. Kubelet은 팟 안에 컨테이너를 구동시키기 위한 컨테이너 런타임을 작동시킵니다.
그리고 도커, CRI-O같은 각각의 노드 위의 컨테이너 런타임은 필요한 컨테이너 이미지를 풀링하고 컨테이너를 만들고 컨테이너를 구동시킵니다.
4. Networking and Service Discovery
Kube-Proxy는 팟들 각각 서로 커뮤니케이션 하기 위한 노드들에 대해 네트워크 규칙을 관리합니다. 이는 내부적인 로드 밸런싱이나 네트워크 라우팅, 외부 소스로부터 팟들을 연결하는 작업을 거치죠.
또한 쿠버네티스는 내부적으로 DNS 서비스를 제공해주는데 그래서 팟들은 서로서로를 잘 찾을 수 있게 해줍니다.
5. Health Checks and Self-healing
개발자가 정한 헬스체크를 Kubelet이 보고 헬스체크를 동작시킵니다. 만약 컨테이너에 문제가 생기는 경우 재시작을 진행합니다.
그 과정에서 Deployment에 의해 관리되는 ReplicaSet들은 원하는 개수의 레플리카 팟들을 항상 작동가능하도록 담당합니다.
6. Scaling and Load Balancing
쿠버네티스는 내부적으로 Replication Controller가 들어있어서 CPU 사용량에 기반하여 안에 있는 많은 수의 팟들을 자동적으로 수평적으로 확장해줍니다.
그리고 Service는 이 타이밍에 많은 수의 팟의 네트워크 트래픽을 분산시키기 위해 로드밸런서를 가동해줍니다.
7. Updates and Rollbacks
쿠버네티스는 우리의 애플리케이션을 롤링 배포를 통해 배포합니다. 하지만 우리가 설정해준다면 카나리 배포나 블루그린 배포도 가능합니다.
또한, 만약 문제가 생긴다면 쿠버네티스는 우리의 애플리케이션의 이전 버전으로 롤백을 지원해줍니다.
8. Logging and Monitoring
쿠버네티스는 디버깅 목적으로 로그를 유지하고 있어서 개발자들은 애플리케이션 퍼포먼스나 상황이 정리되어 있는 각각의 컨테이너 로그들에 접근할 수 있습니다.
그리고 쿠버네티스는 자체적인 모니터링 솔루션을 갖고 있진 않습니다만 모니터링이나 알람을 위한 프로메테우스같은 서드 파티 툴과 결합할 수 있습니다.
9. Storage Management
쿠버네티스는 Persistent Volumes를 통해 저장공간을 관리합니다. 이는 데이터를 영원히 보관하기 위해 stateful한 애플리케이션으로 제공되고 팟 개개인의 라이프사이클에 기반합니다.
10. Cluster Management
개발자는 kubectl을 이용해서 클러스터들과 상호작용할 수 있고 쿠버네티스 대쉬보드 같은 GUI도 이용할 수 있습니다.
가만...
얘기를 쭉 듣다보니 쿠버네티스는 우리가 도커를 사용할 때 항상 하던 컨테이너 생성과 삭제 그리고 장애 상황시 대처, 로드밸런싱, 헬스체크, 셀프힐링, 스케일링, 배포, 롤백, 로깅, 모니터링, 스토리지 관리까지 자동으로 해준다는거야?
이런 끝내주는 기술이 있는데 왜 행성급 규모에서만 사용하라고 하는거지?
아서라 아서
중요한 부분은 저걸 다 할 수 있다는 것이 아닙니다.
중요한 것은 바로 저 모든 것을 개발자가 직접 개발해야 하는 것들이 많다는 것이죠.
쿠버네티스가 자체적으로 가지고 있는 것은 셀프힐링 정도밖에 없습니다. 나머지는 개발자가 일일히 관리해줘야 하는 것입니다.
모든 것을 유지보수 할 수 있는 인력이 없는 상황에서는 오히려 쿠버네티스가 독이 될 수 있다는 것입니다. 그래서 많은 사람들이 MSA는 신의 선물이 아니라 신이 내린 저주라고 말하는 것이죠.
정리
저도 표면적인 지식만 습득해서 아직 자세히는 잘 모르겠습니다만 정말 흥미로운 기술입니다.
많은 개발자 선배님들이 쿠버네티스는 신입에게 오버스펙이다 라고 말씀하시는 것이 왜인지 알 것 같습니다. 하지만 정말 재밌어보이는 기술인 것은 틀림 없습니다.
요즘 자소서 쓰느라 블로그 포스팅이 조금 뜸했는데 신입 기준에서 앞으로 뭘 공부해야 할지도 막막해서 더 뜸했던 것 같습니다. 이제 쿠버네티스라는 재밌는 장난감을 조금씩 만지면서 놀아보도록 하겠습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
출처
https://potato-yong.tistory.com/100
https://devocean.sk.com/blog/techBoardDetail.do?ID=163578
kubernetes의 정책 관리 - Istio
오늘은 kubernetes의 정책 관리를 맡고 있는 Istio에 대해서 포스팅 해보도록 하겠습니다.
DevOps 엔지니어 모집 공고를 보면 "Istio를 기반으로한 서비스 메쉬에 대한 이해가 높으신 분" 같은 문구를 종종 볼 수 있습니다.
이것이 제가 쿠버네티스 아키텍처에서 Istio를 제일 먼저 공부하게 된 계기입니다. 어떤 놈이고 어떤 일을 하는지 공식 문서와 GPT를 이용해 공부해봤습니다.
Istio
우선 쿠버네티스 아키텍처를 먼저 짚고 넘어가겠습니다.

여기서 서비스 메시라고 적혀있는 Istio가 오늘의 주인공입니다.
서비스 메시가 뭘까요?
서비스 메시란 마이크로 서비스로 이루어진 네트워크망을 뜻합니다. 즉 쿠버네티스 내부적인 네트워크 망이라고 보시면 되겠습니다.
이제 본격적으로 Istio에 대해서 알아보도록 하겠습니다. 아, 참고로 읽는 방법은 '이스티오' 입니다. 처음에 아이에스티오라고 불렀다가 면접관이 못알아먹길래 찾아봤습니다...
Istio의 키워드
Istio의 키워드는
• 트래픽 관리
• 보안
• 관찰
• 회복
이렇게 네가지 입니다. 이제 이것에 대해서 한번 알아보죠.
트래픽 관리
Istio의 트래픽 라우팅 규칙은 서비스 사이에서 API 호출이나 트래픽 흐름을 컨트롤 하기 쉽게 해줍니다. Istio를 사용하면 A/B테스팅이나, 카나리 배포나, 블루그린 배포같은 중요한 작업을 간단하게 처리할 수 있게 해주죠.
우선 이 말만 들으면 아하 배포할 때 네트워크 관리를 해주는 거구나 싶지만 하는 일이 더 있습니다.
Istio의 service registry를 사용해서 만든 Envoy 프록시는 마이크로 서비스 베이스인 애플리케이션에서 여러개의 인스턴스를 연결하는 로드밸런싱의 역할을 하기도 합니다. Istio의 Envoy 프록시의 특이한 점이라면 동적으로 로드밸런싱을 진행하여 많은 요청을 받고 있는 호스트에겐 트래픽을 보내지 않습니다.
Envoy 프록시에 대해서 잠깐 짚고 넘어갈까요?
Envoy 프록시
Envoy 프록시란 모든 서비스에 대하여 인바운드 혹은 아웃바운드 트래픽을 완화하기 위해 사용되는 Istio의 high-performance를 위한 프록시입니다.
우리의 메시 서비스에서 보내거나 받는 모든 트래픽은 Envoy를 통해 프록시화 되고 이것들은 우리의 서비스에서 어떠한 변화도 없이 트래픽들을 컨트롤하기 쉽게 만들어줍니다.
이제 간단하게 Istio가 로드밸런싱 하는 방법에 대해서 알아보도록 하겠습니다.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- bookinfo.com
http:
- match:
- uri:
prefix: /reviews
route:
- destination:
host: reviews
- match:
- uri:
prefix: /ratings
route:
- destination:
host: ratings
이렇게 어떤 API가 어떤 호스트로 연결될지 결정할 수 있습니다. 이를 라우팅 규칙이라고도 하죠.
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
weight: 75
- destination:
host: reviews
subset: v2
weight: 25
이런식으로 퍼센테이지를 줄 수도 있습니다.
destination rule을 설정해서 Envoyu의 트래픽 정책을 커스터마이징 할 수도 있는데 예를 들어서 개발자가 선호하는 로드밸런싱 모델 (알고리즘) 을 선택할 수도 있고 TLS 모드도 원한다면 추가할 수 있으며 서킷 브레이커 세팅도 할 수 있습니다.
이 부분에 대해서는 뒤에서 더 자세히 알아보도록 하죠.
TMI로 로드밸런싱 알고리즘은 공식문서에선 Random, Weighted Least Connection, Round Robin이 있습니다. 몇개 더 있던데 유명한 알고리즘은 아니라서 제외했습니다.
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-destination-rule
spec:
host: my-svc
trafficPolicy:
loadBalancer:
simple: RANDOM
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
- name: v3
labels:
version: v3
이렇게 trafficPolicy 밑에 보시면 ROUND_ROBIN 이라고 적혀있는 것을 볼 수 있습니다. 참고로 대소문자를 구별하니까 적으실 땐 주의하셔야합니다.
보안
모놀리식 아키텍처와 다르게 마이크로 서비스 아키텍처에선 다양한 보안적 문제를 검토해야합니다. man-in-the-middle에 대응하기 위해 트래픽 암호화도 필요하고, 유연한 서비스 접근 제어를 위해 상호 TLS를 설정하거나 섬세한 접근 정책이 필요하고 누가 무엇을 얼마나 어떻게 사용할지도 정해줘야하죠.
Istio의 보안 아키텍처는 다음과 같습니다.

잘 보시면 기본적으로 Istio에서 CA도 제공해주고 Authentication Policy나 Authorization Policy도 제공해주는 것을 볼 수 있습니다.
그리고 서비스 간에 통신은 상호 TLS로 이뤄지죠. 이에 대해서 자세히 알아봅시다!
우리는 보안에서 가장 중요한 인증과 인가에 대해서 알아볼겁니다.
인증 (Authentication)
Istio의 신원 확인은 클라이언트 단에선 서버의 신원은 인가된 것인지 보기 위해 Secure Naming정보를 기반하여 체크합니다. 반대로 서버 단에선 클라이언트가 인가 정책에 기반하여 서버의 자원에 접근할 수 있는지를 결정합니다.
Istio에선 두 가지 인증 방식을 제공해줍니다.
• 상호 인증 (Peer Authentication) : 커넥션을 이루는 클라이언트를 증명하기 위해 service-to-service 인증을 사용합니다. Istio는 인증을 제공하기위한 풀스택 솔루션으로 상호 TLS를 제공해줍니다.
그리고 이것은 service 코드를 변경하지 않아도 사용할 수 있습니다. 네트워크 트래픽 중 TLS 트래픽과 plain-text를 둘 다 허용하는 가장 유연한 단계인 PERMISSIVE와 TLS만 허용하는 STRICT 그리고 아무것도 허용하지 않는 DISABLE이 있습니다.
• 요청 인증 (Request Authentication) : 요청에 대한 credential을 증명하기 위해 유저 끝단에서 인증을 사용합니다. Istio는 request-level 인증인 JWT 검증을 사용합니다.
아마 마이크로 서비스 아키텍처에선 stateless한 인증 방식이 필요하기 때문에 JWT를 사용하는게 아닐까 싶네요.
여기서 잠깐 짚고 넘어갈 개념이 바로 상호 TLS (Mutual TLS) 입니다.
Mutual TLS는 기존의 TLS와 다르게 몇가지가 추가됩니다.
• 클라이언트가 서버에 ClientHello를 보낸다.
• 서버는 ClientHello 안에 있는 Cipher Suite를 선택하고 TLS 프로토콜을 정한다음 인증서를 클라이언트에게 보낸다.
• 클라이언트가 서버의 인증서를 복호화해서 서버가 인증되면 클라이언트가 서버에게 본인의 인증서를 보낸다. (이 부분이 기존 TLS와 다른 부분입니다.)
• 서버가 클라이언트의 인증서를 복호화하고 session key를 생성한 뒤 사용한다.
기존 TLS에는 없던 클라이언트가 서버에게 인증서를 보낸다는 점이 추가되어 보안적으로 더 뛰어나다고 할 수 있습니다.
인가 (Authorization)
인가에 대해선 깊이있게 공부하지 않아도 될 것 같아서 깊이있게 공부하진 않았습니다. 표면적인 정보만 알려드리고 넘어가겠습니다.
기본적으로 Istio는 RBAC (Role-Based Access Control)을 따르고 있습니다. 하지만 원한다면 누가 어떤 서비스에 접근할 것인지 동적으로 정해줄 수도 있습니다.
예를 들어서 service name에 따라서 정해줄 수도 있고, service namespace에 따라서도 정해줄 수 있습니다. 또한, method, path에 따라서도 접근할 수 있는 권한을 부여할 수 있습니다.
관측 가능성 (Obervability)
Istio는 메시 안에서 모든 서비스 커뮤니케이션에 대한 세부적인 측정이 가능합니다. 이러한 측정은 서비스의 동작이나 트러블 슈팅, 유지보수, 애플리케이션의 최적화를 할 수 있고 이를 관측 가능성이라고 부릅니다.
이 관측 가능성을 위해서 서비스 개발자에겐 추가적인 짐이 지워지진 않습니다.
여기서 잠깐!
관측 가능성을 Istio가 지원하긴 하지만 하드웨어 메트릭이나 소프트웨어 메트릭은 프로메테우스와 그라파나를 주로 사용하기 때문에 그냥 짚고만 넘어가겠습니다.
Istio는 관측 가능성을 위해 메트릭을 제공해주는데 해당 메트릭은 모니터링을 하기 위해 사용되는 메트릭이며 프록시 레벨에서의 메트릭을 지원해준다는 특징이 있습니다.
프록시 레벨에서의 메트릭 뿐만 아니라 서비스 레벨에서도 메트릭을 지원합니다. 이를 통해 지연시간, 트래픽, 에러 등의 메트릭을 수집하고 대쉬보드로 만들 수 있습니다.
이건 개인적으로 놀라웠던 부분인데 Istio는 secure naming을 이용해 인증을 진행하기 때문에 전적으로 kubernetes의 control plane위에서 동작합니다. 때문에 control plane의 메트릭도 잡아서 보여줄 수 있습니다.
마치며
여기까지 kubernetes의 정책을 관리하는 Istio에 대해서 알아봤습니다.
공식 문서 정말 길더라구요 다 읽는데만 거의 네시간 가까이 걸렸습니다.
이번엔 GPT에 도움을 많이 받지 않았습니다. 왜냐하면 GPT는 깊이있게 공부할 땐 좋은데 이렇게 표면적으로 공부할 땐 그닥 좋은 것 같진 않습니다.
기존 CS 공부나 이런 것들은 깊이있게 공부해도 상관 없을정도로 쉬운 지식들이 많아서 GPT로 공부했지만 이번에는 공식 문서를 이용해봤습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
출처
https://istio.io/latest/docs/concepts/traffic-management/
https://istio.io/latest/docs/concepts/security/
https://istio.io/latest/docs/concepts/observability/
쿠버네티스 이론 : 파드 (Label, Node Scheduler)
이번 포스팅에선 쿠버네티스의 파드에 대해서 공부한 내용을 정리해보도록 하겠습니다.
파드 (Pod)
파드란 쿠버네티스가 관리하는 최소 배포 단위입니다. 최소 배포 단위라는 것은 파드를 기준으로 애플리케이션을 구동한다는 의미입니다.
파드 안에는 여러개의 컨테이너가 들어갈 수 있고 이 컨테이너 하나하나가 애플리케이션이 됩니다.
쿠버네티스가 파드를 이용해서 컨테이너를 묶음으로 관리하는 것은 사용자의 니즈를 파악한게 아닐까 싶네요. 어떤 애플리케이션이던 혼자서 단독으로 돌아가는 애플리케이션은 실 사용 서비스에선 있을 수 없는 일이죠.
하다못해 스프링부트만 하더라도 단독으로 내장톰캣을 이용해서 사용할 수 있지만 결국 HTTPS를 사용하려면 NGINX같은 웹서버가 있어야하고 정상적인 서비스라면 데이터베이스와 연결이 되어있을테니 MySQL같은 데이터베이스도 필요하죠.

쿠버네티스는 노드라는 더 큰 범위에 파드를 놓고 그 파드 안에 컨테이너를 두어 컨테이너들을 관리합니다.
파드는 생성될 때마다 IP가 자동으로 부여되고 관리되는데요. 우리는 이 파드에 연결하기 위해 {pod IP}:{pod Port} 이렇게 들어갈 수 있습니다.
하지만 문제가 있는 것이 파드는 언제든지 삭제되고 다시 재생성될 수 있다는 것입니다. 재생성되면 파드의 IP가 바뀌게 되어 저런 방법으로는 연결할 수 없습니다.
또한, 파드의 IP로 들어갈 수 있는건 쿠버네티스 클러스터 내부에서만 해당합니다. 만약 외부 사용자들이 파드에 접근하게 하려면 어떻게 해야할까요?
서비스 (Service)
여기서 나오는 것이 서비스입니다. 물론 이번 포스팅에선 서비스에 대해서 간략하게만 짚고 넘어갈것입니다. 다음 쿠버네티스 이론인 "쿠버네티스 이론 : 서비스 (A to Z) 에서 자세히 다루도록 하겠습니다.
사실 위에서 만든 쿠버네티스 클러스터의 모습은 많은 것이 빠져있습니다. 파드만 강조해서 보여드리기 위해서 저렇게 만들었지만 사실 위에 서비스라는 오브젝트가 하나 더 있습니다.

이렇게 되어있으면 서비스의 IP를 이용해서 노드에 접근할 수 있고 노드에 배포되어있는 파드에 연결할 수 있습니다.
그럼 서비스가 어떤 파드를 사용자에게 보여줘야하는지 알아야합니다. 저 노드에 배포되어있다고 하여 모든 컨테이너를 외부로 노출할 수는 없으니까요.
여기서 잠깐 파드 설정파일을 한번 보도록 하겠습니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-2
labels:
type: web
log: dev
spec:
containers:
- name: container
image: tmkube/init
우리가 주목해서 봐야할 부분은 metadata에 labels 부분입니다. 쿠버네티스는 이 라벨을 이용해서 각 오브젝트들을 연결합니다. 비단 서비스 - 파드 뿐만 아니라 컨트롤러 - 노드, 컨트롤러 - 파드, 파드 - 볼륨 등등 다양한 오브젝트들이 라벨을 이용해서 연결합니다.
라벨은 key와 value로 되어있습니다. 이렇게 되어있는 이유는 키-벨류 쌍으로 지정할 수도 있지만 키만 가지고도 매칭할 수 있도록 하기 위해서이죠.
아무튼 라벨을 이용해서 서비스와 파드를 연결한다는 것이 중요한 것이죠. 여기서 서비스 설정 파일을 한번 확인하겠습니다.
apiVersion: v1
kind: Service
metadata:
name: svc-1
spec:
selector:
type: web
ports:
- port: 9000
targetPort: 8080
type: LoadBalancer
selector에 type: web 이렇게 적음으로써 key가 type, value가 web인 파드를 연결하겠다는 의미입니다.
서비스와 관련된 내용은 다음 쿠버네티스 이론 서비스편에서 더 자세히 다루도록 하겠습니다.
이제 파드에 대해서 조금 더 자세히 깊숙하게 들어가보도록 하겠습니다.
Node Scheduler
파드의 한 부분을 차지하는 그리고 쿠버네티스가 오브젝트를 관리하는 라벨에 대해서는 위에서 언급했으니 다른 파드의 특징에 대해서 알아보도록 하겠습니다.
Node Scheduler (개요)
노드스케줄러는 파드가 생성될 때 어느 노드에 들어갈 것인지 자동으로 정해주는 쿠버네티스의 오브젝트 중 하나입니다. 물론 자동으로 선택되게 하는 것이 개발자가 손을 덜 타게 하지만 수동으로 어떤 노드에 들어갈 것인지 정해줄 수도 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-3
spec:
nodeSelector:
hostname: node1
containers:
- name: container
image: tmkube/init
이렇게 nodeSelector 속성을 이용해서 어떤 노드에 들어갈지 골라주면 됩니다. hostname 속성에 노드를 만들 때의 노드 이름을 적어주면 해당 파드는 node1에만 생성됩니다.
하지만 앞서 언급했듯이 이렇게 하는 것은 조금 번거롭죠. 때문에 자동으로 노드스케줄러를 이용해서 자동으로 넣는 방법도 쿠버네티스가 제공해줍니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-4
spec:
containers:
- name: container
image: tmkube/init
resources:
requests:
memory: 2Gi
limits:
memory: 3Gi
노드스케줄러에의해 자동으로 파드를 배포되게 하려면 resources에 대한 정보가 있어야합니다. 왜냐하면 이 정보를 바탕으로 들어갈 수 있는 노드를 찾기 때문이죠.
위와 같은 파드를 배포하려고 하는데 요청된 메모리가 2기가입니다. 그럼 최소 2기가가 필요하다는 의미인데, 만약 어떤 노드에 메모리가 1기가밖에 남아있지 않다면 이 노드에는 넣을 수 없기 때문이죠.
혹시라도 메모리가 초과되는 노드에 배치되면 어떤일이 벌어질까요?
컴퓨터는 같은 메모리 주소를 프로세스가 참조하는 것을 절대로 허용하지 않습니다. 즉, requests에서 요청한 메모리가 초과되면 모든 프로세스가 종료됩니다. 하지만 반대로 CPU가 request를 초과하는 것은 허용하죠. 왜 이런 일이 벌어지는지 운영체제의 입장에서 생각해보겠습니다.
CASE1. 메모리가 초과되는 상황
만약 프로세스가 같은 메모리 주소를 참조하게 된다면 데이터의 정합성이 깨질 수 있습니다. 또한, race condition 상태가 되어 데이터가 꼬여버릴 수도 있죠. 그리고 서로의 데이터를 덮어쓰면서 문제가 생기기 때문에 프로세스들은 같은 메모리 주소를 바라보지 못하도록 OS레벨에서 이를 막습니다.
CASE2. CPU가 초과되는 상황
반대로 CPU가 초과되는 상황에서는 조금 다릅니다. 프로세스는 CPU자원을 할당받을 때 고유값인 CPU time이라는 것을 부여받고 프로세스 자신이 처리해야 하는 일에 따라 CPU레지스터들이 할당됩니다.
이 말이 어떤 말이냐하면 프로세스가 실행될 때 CPU자원을 고유하게 받는다는 것입니다. 때문에, 프로세스는 CPU자원을 사용함에 있어서 충돌이라는 개념이 존재하지 않습니다.
이런 케이스를 잘 보여주는 상황이 있는데 바로 메모리 스왑입니다.
OS는 자신에게 장착된 메모리를 전부 다 써버리게 되면 HDD나 SSD에서 스토리지 자원을 끌어다가 메모리처럼 사용하게 됩니다.
이때 실제로 메모리는 아니고 메모리처럼 동작하도록 하는 것이기 때문에 메모리에 비해 속도가 안나오는 것은 사실이지만 메모리가 초과되었다고 서로 다른 프로세스에게 같은 메모리 주소를 주면 위에서 언급한 문제가 발생할 수 있습니다.
때문에 OS는 메모리 스왑으로 메모리를 할당해주는 특징이 있습니다.
다시 노드 스케줄러로 돌아와서, 이때문에 노드 스케줄러가 자동으로 파드를 할당하는 기준은 바로 노드에 남아있는 자원의 양입니다. 여기서는 CPU와 메모리가 주된 자원이 되겠죠.
메모리나 CPU가 많이 남아있는 노드에 더 우선적으로 파드를 배치하는 것이죠. 쿠버네티스는 자체적으로 노드에 점수를 매겨서 높은 점수인 노드에 파드를 배치한다는 특징이 있습니다.
여기까지 노드스케줄러에 대한 개요였습니다. 이후 나올 내용들은 조금 딥한 내용입니다. 한번 더 깊이있게 알아보도록하죠.
Node Scheduler (심화)
노드 스케줄러는 앞서 언급했듯이 아무런 설정도 되어있지 않으면 자원이 널널한 노드를 자동으로 선택해서 파드를 배치합니다. 하지만 다양한 설정으로 노드 스케줄러에게 어떤 노드를 선택해야하는지 지시할 수 있습니다.
이런 설정이 필요한 이유는 파드가 노드에 배치될 때 아무 노드에나 배치되면 안되는 상황이 있을 수 있기 때문입니다.
즉, 어떤 노드는 데이터베이스 노드이고 어떤 노드는 WAS노드인데 WAS와 관련된 파드가 데이터베이스 노드에 들어가면 관리하기가 힘들기 때문이죠.
노드 스케줄링에는 세가지 정책이 있습니다. 노드 직접 선택, Pod간 집중 / 분산 선택, Node에 할당 제한 이렇게 세가지인데요. 한번 자세히 알아보도록 하겠습니다.
Node 선택
노드를 직접 선택해야 하는 방법으로는 NodeName, NodeSelector, Node Affinity 이렇게 세가지로 또 나뉩니다. 하나씩 천천히 알아보죠.
• NodeName : 노드의 이름을 직접 선택하는 방법이지만 많이 사용되지는 않습니다. 왜냐하면 파드와 마찬가지로 노드 또한 삭제되고 생성되는 과정이 일어날 수 있고 생성되는 과정에서 노드의 이름이 바뀔 수 있기 때문입니다.
• NodeSelector : 노드의 라벨을 지정해줘서 해당 라벨이 붙어있는 노드에 파드를 배치하는 방법입니다. 이 방법이 다좋은데 하나 불편한 점이 key와 value가 정확히 일치해야한다는 점입니다. 만약 일치하는 노드가 없는 경우 파드가 생성되지 않습니다.
• NodeAffinity : 노드의 key값만 있으면 해당 키가 있는 노드로 파드를 자동으로 배치해주는 특징이 있습니다.
NodeName과 NodeSelector는 간단하니 NodeAffinity에 대해서 자세히 알아보겠습니다.
Node Affinity는 노드의 키를 보고 규칙에 의해 파드를 배치한다는 특징이 있습니다. 때문에 우리는 matchExpressions라는 속성으로 어떤 노드에 배치되어야하는지 쿠버네티스에게 알려줘야합니다.
여기서 잠깐 관련된 속성 한번 보고 가시죠!
apiVersion: v1
kind: Pod
metadata:
name: pod-match-expressions1
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoreDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- {key: kr, operator: Exists}
containers:
- name: container
image: tmkube/app
terminationGracePeriodSeconds: 0
위의 설정에서 자세히 봐야하는 부분이 matchExpressions 부분입니다. 위의 설정은 노드의 key가 kr인 노드가 존재하면 그곳에 파드를 생성하라는 의미입니다.
여기서 requiredDuringSchedulingIgnoreDuringExecution 속성을 넣으면 반드시 kr노드에 들어가게 되고 kr노드가 없으면 파드를 생성하지 않습니다.
반대로 preferredDuringSchedulingIgnoreDuringExecution 속성을 넣으면 kr노드를 선호하지만 kr노드가 없으면 다른 노드에 생성하겠다는 의미입니다.
matchExpressions에는 operator라는 속성이 있는데 이 속성은 규칙을 부여하는 것입니다. 여러가지 operator가 있지만 몇개만 소개하겠습니다.
• Exists : 키가 존재하는 경우에 라벨을 매치시켜줍니다. 위의 예제라면 kr노드에 배치해주는 것이죠.
• DosesNotExists : 키가 존재하지 않는 경우에 라벨을 매치시켜줍니다. 위의 예제에선 kr이라는 키가 없는 노드에 배치해줍니다. 이 경우엔 en, jp 등이 될 수 있겠네요.
• In : 키와 벨류가 모두 일치하는 경우에 매치시켜줍니다. 위의 예제에선 나오지 않았지만 예를 들어서 key: kr, value: seoul 이렇게 매치해준다는 의미입니다.
• NotIn : In속성과 완전 반대되는 특징을 가집니다.
Pod간 집중 / 분산
이 정책은 파드를 배치할 때 여러개의 파드가 하나의 노드에 무조건 할당되어야하거나 여러개의 노드에 분산되어 배치되어야할 때 사용할 수 있습니다.
Pod간 집중과 분산 두가지 관점에서 알아보고 어떤 상황에서 사용될 수 있는지 정리해봤습니다.
• Pod Affinity : 이 속성은 파드에 집중 배치되어야할 때 사용할 수 있습니다. 주로 HostPath로 설정된 볼륨과 연결된 파드들을 배치할 때 사용합니다.
여기서 HostPath로 설정된 볼륨이란, 쿠버네티스에는 여러가지 볼륨(저장소)의 형태가 있는데 HostPath로 설정된 볼륨은 노드에 국한된 볼륨입니다. 즉, 노드를 벗어나면 이 볼륨을 사용할 수 없다는 말입니다. 이외에도 모든 곳에서 사용할 수 있는 PV (Persistant Volumes) 가 있지만 다다음 포스팅 주제인 쿠버네티스 이론 : 볼륨 (A to Z) 에서 자세히 다루도록 하겠습니다.
즉, Pod Affinity의 경우 특정 노드에서만 사용 가능한 HostPath볼륨이 설정되어있는 노드에 파드를 배포해야 하는 경우에 사용할 수 있다는 것입니다.
• Pod Anti Affinity : 이 속성은 Pod Affinity와 완벽히 반대되는 속성으로서 서로 다른 노드들이 한 노드에 배포되면 안되는 경우에 사용할 수 있습니다.
보통 마스터 슬레이브 구조에서 마스터가 다운되면 슬레이브가 승격되어야 하는 구조에서 사용할 수 있습니다.
마스터 슬레이브 구조에서는 마스터가 죽을 수도 있는 특정한 상황을 비관적으로 가정하기 때문에 마스터 파드가 죽어버려 노드 전체를 사용할 수 없을 때 만약 슬레이브도 같은 노드에 배포되어있다면 문제가될 수 있습니다.
때문에 마스터 슬레이브 구조에선 서로 다른 파드들이 서로 다른 노드에 들어가야 하는 것이죠.
Pod Affinity는 Node Affinity와 마찬가지로 matchExpressions로 노드를 설정해주는데 보통 여러개의 파드를 동시에 하나의 노드에 배포되어야할 때 사용하기 때문에 In속성을 주로 사용하게 됩니다.
Pod Anti Affinity는 Pod Affinity와 완전히 반대로 동작합니다.
apiVersion: v1
kind: Pod
metadata:
name: server1
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoreDuringExecution:
- topologyKey: a-team
labelSelector:
matchExpressions:
- {key: type, operator: In, value: [web1]}
containers:
- name: container
image: tmkube/app
terminationGracePeriodSeconds: 0
여기서 주의해야 하는 점이 matchExpressions로 설정되는 key와 value들은 노드의 라벨이 아닙니다. 파드의 라벨입니다.
노드 스케줄러는 이 속성을 보고 파드의 라벨을 확인합니다. 그리고 키와 벨류가 맞는 파드를 발견하면 그 파드가 있는 노드에 새로운 파드를 배치해줍니다.
Pod Affinity에선 topologyKey라는 속성을 추가할 수 있는데 이 속성은 노드 스케줄러가 이 키값을 가지고 있는 노드에서 마땅한 노드를 찾는 것을 도와줍니다.
Node에 할당 제한
이 정책은 어떤 노드에 특정 파드만 들어가야 하는 경우가 있는 경우에 사용합니다. 예를 들어서 GPU를 많이 필요로하는 파드만 모아서 배치하는 경우가 그러한데 이럴 때 사용할 수 있는 속성입니다.
Node에 할당을 제한하기 위해서는 Taint와 Toleration에 대해서 짚고넘어가야합니다.
먼저 특정 파드만 들어와야하는 노드에 Taint속성을 집어넣습니다. 이렇게 하는 경운 해당 노드에는 파드를 정상적인 방법으로는 배치할 수 없습니다.
노드 스케줄러에 의해 자동으로 배치되지도 않고, 심지어 노드의 이름을 직접 지정하는 경우도 배치되지 않습니다.
이 노드에 배치되기 위해서는 Toleration이라는 속성이 추가된 파드만 해당 노드에 위치할 수 있습니다.
한번 설정파일을 한번 보겠습니다!
그 전에 노드에 라벨을 설정하고 Taint설정을 해보겠습니다.
kubectl label nodes k8s-node1 hw=gpu:NoSchedule
kubectl taint nodes k8s-node1 hw=gpu:NoSchedule
apiVersion: v1
kind: Pod
metadata:
name: pod-with-toleration
spec:
nodeSelector:
gpu: no1
tolerations:
- effect: NoSchedule
key: hw
operator: Equal
value: gpu
containers:
- name: container
image: tmkube/app
terminationGracePeriodSeconds: 0
그리고 위와같이 노드를 Taint로 설정하면 어떤 방법으로도 파드를 배치할 수 없습니다.
위와같이 tolerations 속성을 달아주면 Taint속성이 부여된 노드에 파드가 배치됩니다.
이때 effect, key, operator, value 모든 값이 일치하여야 Taint속성의 노드에 배치됩니다. 이때 effect에 대해서 잠깐 짚고 넘어가겠습니다.
• NoSchedule : 우리가 노드를 만들 때 hw=gpu:NoSchedule 이렇게 설정할 때의 속성을 말합니다. hw=gpu 는 키와 벨류인 라벨을 말하는 것이고 NoSchedule은 effect를 말하는 것입니다.
NoSchedule은 해당 노드가 노드스케줄러에 의해 자동으로 배치되지 않는 것을 의미합니다.
• NoExecute : NoSchedule의 특징은 해당 노드에 NoSchedule이 적히면 노드스케줄러에 의해 파드가 배치되지 않긴 하지만 이미 해당 노드에 파드가 배치되어 있는 경우에는 이 파드들은 계속 돌아갑니다.
하지만 NoExecute를 설정하면 이 설정이 부여되는 즉시 그 노드에 존재하는 toleration이 없는 파드들이 전부 삭제됩니다.
모든 파드가 삭제되긴 하지만 모든 파드들이 삭제되고 다른 노드에 배치되기 때문에 아예 이용이 불가능한 것은 아닙니다.
마치며
원래 파드에 대한 모든 내용을 한 포스팅에 담으려고 했는데 너무 많아졌네요... 노드 스케줄링만 거의 모든 포스팅을 차지하네요. 다음 포스팅은 이번 포스팅과 이어지는 내용입니다. 파드에 대해서 계속 자세히 다뤄보도록 하겠습니다.
쿠버네티스 이론 : 파드 (Life cycle, Probe, QoS classes)
이번 포스팅에는 파드에 대해서 더 자세히 알아보도록 하겠습니다. 이번 포스팅은 생명주기, Probe, QoS class에 대해서 정리해봤습니다.
파드의 생명주기
파드의 상태에는 파드의 전체적인 상태를 나타내는 Phase가 있고 세부적인 상태를 나타내는 Condition이 있습니다. 그리고 파드안에는 컨테이너가 여러개 들어가있고 이 컨테이너들의 상태를 볼 수 있는 State라는 속성이 있습니다.
파드의 전체적인 상태를 나타내는 Phase에는 Pending, Running, Succeeded, Failed, Unkown 이렇게 다섯가지가 있습니다.
• Pending : Pending은 파드의 최초 상태입니다. 이때 파드를 구동하기위해 필요한 작업을 진행합니다.
1. 컨테이너의 초기 상태를 세팅하기 위해 initContainer가 작동합니다. initContainer의 성공과 실패 여부에 따라 initialized 속성이 True가 되기도 하고 False가 되기도 합니다.
2. 이후 파드가 어느 노드에 올라갈지 수동으로 정해주거나 노드스케줄러에 의해 자동으로 올라간다면 PodScheduled 속성이 True 혹은 False가 됩니다.
3. 이후 파드가 올라갈 노드가 정해지면 파드를 구동하기 위해 템플릿에 정해놓은 이미지를 다운로드합니다. 이때, 컨테이너의 State는 Waiting이 되고 Reason은 ContainerCreating이 됩니다.
• Running : Running은 파드가 실행되는 과정입니다. 파드가 실행되는 과정에서 다양한 문제가 발생할 수 잇꼬 이를 상태값으로 관리합니다.
1. 파드에 컨테이너가 올라가면서 생기는 다양한 문제들을 Reason에 적습니다. 이 때, 파드가 재시작될 수 있고 상태는 Waiting, Reason은 CrashLoopbackup이 됩니다.
2. 파드들이 정상적으로 동작하면 ContainerStatus의 State는 Running이 되고 이에 따라 속성들을 모두 True로 바꿉니다.
• Failed / Succeeded : 만약 Job이나 CronJob에 의해 파드가 생성되는 경우 파드의 할 일이 끝나고 나면 해당 상태로 돌입하게 됩니다. 파드가 하나라도 에러에 의해 종료되는 경우 Failed가 되고 정상적으로 모든 파드가 종료되는 경우 Succeeded가 됩니다.
만약 파드가 정상적으로 동작한다면 생명주기는 Pending -> Running (계속 동작해야 하는 파드) -> Failed or Succeeded 가 됩니다.
하지만 만약 Pending 상태에서 파드를 생성할 수 없어 바로 Failed로 넘어갈 수도 있고 네트워크 장애가 발생해서 Unkown으로 변경될 수도 있습니다.
Probe
Probe에는 ReadinessProbe가 있고 LivenessProbe가 있습니다. 이 두개가 왜 사용되는지 언제 사용되는지 먼저 짚고 넘어가도록 하겠습니다.
만약 서로 다른 두 노드에 파드가 생성되어있고 서비스가 이 두파드를 바라보고 있다고 했을 때, 정상적인 상황에선 양쪽에 트래픽이 50:50 으로 흐르게 될 것입니다.
이때 하나의 파드에 장애가 생겼고 쿠버네티스가 자동으로 이 파드를 재시작한다면 파드가 생성되자마자 곧바로 서비스에 바로 파드가 연결됩니다.
하지만 이 파드의 앱이 구동중이라 파드는 생성되었지만 컨테이너 내부의 앱은 접근할 수 없다면 500 Server Internal Error가 뜨게 됩니다. 스프링부트만 하더라도 서버에서 구동되는 동안 외부에서 접근할 수 없는 것과 같은 이치입니다.
그럼 이 컨테이너의 앱이 부팅중일 때 사용자는 에러 페이지를 보게 되겠죠.
이를 해결하기 위해 ReadinessProbe가 사용됩니다. ReadinessProbe는 파드 내부의 컨테이너에 정상적으로 앱이 이용 가능한지 체크합니다. ReadinessProbe가 컨테이너 내부에 헬스체크를 해서 외부에 연결해도 되는지 체크하는 역할을 합니다.
그리고 이후 상황에서 파드가 정상적으로 작동은 하고 있지만 앱이 다운되는 경우도 Probe가 필요합니다.
파드는 정상인데 앱이 다운되는 경우는 어떤 경우가 있을까요?
흔치 않지만 Tomcat은 정상 작동 중인데 내부의 앱이 정상적으로 작동하지 않을 때 생길 수 있는 문제입니다. 이때 우리는 LivenessProbe를 이용하면 이 앱이 살아있는지 죽어있는지 체크해서 파드 전체를 재시작해줄 수 있습니다.
ReadinessProbe, LivenessProbe의 설정들
ReadinessProbe와 LivenessProbe는 하는 역할은 다르지만 기본적으로 비슷하게 동작합니다. ReadinessProbe와 LivenessProbe 모두 파드내 컨테이너안에 앱의 상태를 확인한다는 점에서 같은 설정을 공유합니다.
ReadinessProbe와 LivenessProbe는 세가지 설정 중 하나는 무조건 있어야합니다.
• httpGet : 컨테이너 내부 앱에 HTTP요청을 보냄으로써 앱의 상태를 확인합니다. httpGet의 속성으로 가질 수 있는 값들은 Port (어떤 포트로), Host (어떤 주소로), Path (어떤 API로), HttpHeader (어떤 HTTP header를), Scheme(HTTP 혹은 HTTPS로) 보낼지 설정할 수 있습니다.
• Exec : 앱에 직접 HTTP요청을 보내는 httpGet과 다르게 컨테이너 내부의 특정 파일이나 명령어가 동작하는 경우 사용할 수 있ㅅ브니다. 사용할 수 있는 속성은 Command 하나뿐이지만 cat /tmp/ready.txt 처럼 어떤 파일이 있는지 확인하는 용도로 확인할 수 있습니다.
• tcpSocket : tcpSocket은 Host와 Port를 설정해 소켓통신으로 지속적으로 앱의 상태를 확인합니다.
ReadinessProbe와 LivenessProbe는 위의 세가지 설정 중 반드시 하나는 있어야하고 세부적으로 아래와 같은 설정을 가질 수 있습니다.
• initialDelaySeconds : Probe들이 파드 구동 후 몇초부터 시작할지 정하는 설정입니다. 디폴트 값은 0초입니다.
• periodSeconds : Probe들이 어느정도의 기간을 가지고 앱의 상태를 확인할지 설정합니다. 디폴트 값은 10초입니다.
• timeoutSeconds : 만약 타임아웃이 난다면 몇초동안 몇초동안 타임아웃이 나면 죽었다고 판단할지 정하는 설정입니다. 디폴트 값은 1초입니다.
• successThreshold : 주요 세가지 설정들이 몇 번 성공해야 이 앱이 살아있다고 판단할 것인지 정하는 설정입니다. 디폴트 값은 1번입니다.
• failureThreshold : 주요 세가지 설정들이 몇 번 실패해야 앱이 죽었다고 판단할 것인지 정하는 설정입니다. 디폴트 값은 3번입니다.
예시로 Probe 체험하기
ReadinessProbe와 LivenessProbe는 주요한 세가지 설정 중 최소 하나와 나머지 부가 설정들에 의해 앱의 상태를 확인합니다. 이번 섹션에선 한 가지 예시로 ReadinessProbe와 LivenessProbe를 정리하도록 하겠습니다.
ReadinessProbe
• 주요 설정은 Exec로 Command는 cat /tmp/ready.txt입니다.
• initialDelaySeconds : 5
• periodSeconds : 10
• successThreshold : 3
LivenessProbe
• 주요 설정은 httpGet으로 path에 /health입니다.
• initialDelaySeconds : 5
• periodSeconds : 10
• failureThreshold : 3
이 설정대로 ReadinessProbe 시나리오를 가정해보겠습니다.
• 파드를 생성한다.
• 컨테이너가 생성된다. 컨테이너의 상태가 ContainerReady: False, Ready: False로 설정된다.
• intialDelaySeconds에 의해 컨테이너 생성 5초 뒤 상태를 체크한다.
• Exec로 컨테이너 내부로 들어가 cat /tmp/ready.txt를 입력한다.
• periodSeconds에 의해 10초 간격으로 재실행한다.
• successThreshold에 의해 3번 성공하면 성공적으로 앱이 구동됐다는 것으로 인지한다.
• 앱의 구동이 완료되기 전까지 ContainerReady와 Ready 속성은 False로 되어있다가 완료가 되면 두 속성을 모두 True로 바꾼다.
• 서비스는 그제서야 해당 파드를 엔드포인트로 설정한다.
다음 LivenessProbe 시나리오를 가정해보겠습니다.
• LivenessProbe는 initialDelaySeconds에 의해 컨테이너 생성 후 5초뒤부터 앱의 상태를 점검한다.
• periodSeconds에 의해 앱의 상태를 점검하기 시작한 뒤로부터 10초간격으로 지속적으로 점검한다.
• 만약 200 OK라면 넘어간다.
• 앱이 죽어버려서 헬스체크가 500 Server Internal Error로 변경된다.
• periodSeconds에 의해 10초간격으로 지속적으로 요청한다.
• failureThreshold에 의해 3번 실패하는 경우 파드를 재실행한다.
• 이후 ReadinessProbe의 상황을 다시 반복된다.
QoS classes
QoS는 Quality of Service의 약자로서 파드별로 어떤 파드가 중요하게 관리해야하는지 쿠버네티스에게 알려주는 용도로 사용됩니다.
QoS class는 Guaranteed, Burstable, BestEffort가 있고 오른쪽으로 갈수록 중요하지 않은 파드입니다.
만약 노드의 자원을 동일하게 사용하고 있는 세게의 파드가 있고 각각 Guaranteed, Burstable, BestEffort일 때 Guaranteed 파드에서 자원이 더 필요한 경우 BestEffort파드를 죽이고 BestEffort에 할당된 자원을 반납합니다. 그리고 그 자원을 Guaranteed에 분배해줍니다.
Burstable의 파드는 BestEffort만큼은 아니지만 노드의 자원보다 더 많은 자원을 사용하는 경우나 Guaranteed파드가 다른 파드에 분배된 자원보다 더 많이 사용해야하는 경우 차선책으로 파드를 내리고 자원을 회수합니다.
QoS는 따로 설정하는 것이 아니라 컨테이너의 resource 속성에 적힌 requests와 limits의 설정값에 의해 결정됩니다.
• Guaranteed : 쿠버네티스 입장에서 가장 안정적으로 운영되어야한다고 판단되는 파드입니다. 쿠버네티스가 Guaranteed파드로 인식하기 위해서는 다음과 같은 조건이 충족되어야합니다.
1. 모든 컨테이너에 requests와 limits가 설정되어 있어야합니다.
2. requests와 limits에는 모두 Memory와 CPU가 설정되어있어야합니다.
3. 각 컨테이너 내의 Memory와 CPU의 requests와 limits의 값이 같아야합니다.
• Burstable : 쿠버네티스 입장에서 두번째로 안정적으로 운영되어야한다고 판단되는 파드입니다. 쿠버네티스가 Burstable 파드로 인식하기 위해서는 requests와 limits의 크기가 다르거나, requests는 설정되어있는데 limits는 설정이 되어있지 않거나, 파드안에 컨테이너 중 어떤 것은 requests와 limits가 설정되어있지만 어떤 것은 설정 되어있지 않은 경우 (불규칙한 경우) 에 해당 파드는 Burstable 파드로 인식합니다.
• BestEffort : BestEffort는 컨테이너 내부에 자원 설정을 전혀 하지 않는 경우 쿠버네티스가 해당 파드를 BestEffort로 인식합니다.
마치며
원래였으면 쿠버네티스 파드편으로 묶어서 포스팅하려고 했지만 양이 너무 많아 두개로 나누게 되었습니다. 다음 포스팅에선 쿠버네티스 실전 : 파드 편이 포스팅됩니다.
쿠버네티스 이론 : 서비스 (Cluster IP, Node Port, LoadBalancer)
이번 포스팅은 쿠버네티스의 꽃인 파드와 짝을 이루는 서비스입니다. 서비스는 파드를 외부로 노출시켜주는 역할을 하게 되는데, 앞선 쿠버네티스 이론 : 파드 편에서 잠시 언급했지만 라벨을 이용해서 파드와 연결합니다.
이는 노드에 들어있는 파드들 중에 노출되면 안되는 파드도 있기 때문에 (e.g. 데이터베이스) 보안상 지정된 파드만 외부와 연결할 수 있습니다.
서비스에는 크게 Cluster IP, Node Port, LoadBalancer 이렇게 세 가지 타입이 있습니다. 각 타입은 특징이 뚜렷해서 어느 것이 어떤 상황에서 쓸지 명확하게 구분할 수 있습니다.
그럼 본격적으로 시작해보죠!
Service (서비스)
서비스에는 Cluster IP, Node Port, LoadBalancer 이렇게 세 가지 타입이 있는데 하나씩 살펴보도록 하죠.
Cluster IP
Cluster IP는 쿠버네티스 클러스터 내부에서만 접근이 가능한 타입입니다. 만약 Cluster IP 타입으로 서비스를 만든다면 외부에서는 어떤 방법을 동원해서도 연결될 수 없습니다.
이런 특징 때문에 쿠버네티스를 관리하는 관리자만 접근할 수 있도록 열어두는 용도로 주로 사용합니다.
서비스의 타입을 아무것도 설정하지 않으면 기본적으로 설정되는 타입이기도 한데, 사실 많이 사용될 것 같지는 않습니다.
한번 yaml 설정파일 보시죠.
apiVersion: v1
kind: Service
metadata:
name: svc-1
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
type: ClusterIP
apiVersion: v1
kind: Pod
metadata:
name: pod-1
labels:
app: pod
spec:
containers:
- name: container
image: tmkube/app
ports:
- containerPort: 8080
파드와 서비스 이렇게 두가지 설정이 있습니다. 파드의 라벨은 app: pod 이고 이를 서비스가 selector를 이용해 연결하고 있습니다.
이때, 서비스의 IP로 접근을 요청하면 연결이 되지 않는 것을 알 수 있는데 이에 대한 자세한 실습 내용은 쿠버네티스 실습 : 서비스 편에서 제대로 다뤄보도록 하겠습니다.
Node Port
Node Port는 기본적으로 Cluster IP의 성질을 가지고 있는 상태에서 모든 노드에 동일한 포트를 설정하고 해당 포트로 들어오는 모든 요청을 모두 분산시켜준다는 특징이 있습니다.
Node Port에는 한가지 특징이 있는데 바로, 다른 노드로 들어오더라도 모든 요청을 분산한다는 특징이 있습니다. 즉, 서로 다른 노드에 서로 다른 파드가 배포되어 있다면 어떤 파드로 트래픽이 흐를지 모른다는 의미입니다.
이 이얘기는 노드1에 파드1이 배포되어있고 노드2에 파드2가 배포되어있다면 노드1로 온 요청을 파드2가 처리할 수도 있다는 의미입니다.
하지만 경우에 따라선 노드1로 들어온 트래픽 요청을 파드1에서 처리하고 싶을 수도 있습니다. 이럴 때는 externalTrafficPolicy를 local속성으로 설정하면 해결할 수 있습니다.
apiVersion: v1
kind: Service
metadata:
name: svc-2
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
nodePort: 30000
type: NodePort
Node Port의 또다른 특징은 Node Port에 해당하는 포트는 30000~32767중 하나로 설정된다는 것입니다. 때문에 만약 NodePort로 타입을 설정하고 nodePort를 설정하지 않는다면 30000~32767중 아무 포트가 하나 정해집니다.
Node Port는 내부망에서 다른 클러스터끼리 소통하는 경우 사용할 수 있고, 데모 버전이나 임시 연결용으로 포트포워딩을 하여 외부로 잠시 연결해야 하는 경우에 사용할 수 있습니다.
Load Balancer
Load Balancer타입은 외부로 노출된 IP를 이용해 외부 요청을 처리하는 타입으로서 기존 Node Port의 속성을 가지고 있는 상태에서 시작합니다.
만약 이 타입을 설정하는 경우 모든 노드 포트와 연결되는 로드밸런서가 앞에 위치하게 됩니다. 이 로드밸런서는 노드로 들어오는 요청을 분산시키는데 Node Port와 다른 점은 내부망에서 사용했던 Node Port와 다르게 외부 인터넷으로 연결될 수 있다는 것이 특징입니다.
주의) 이 로드밸런서의 External IP는 쿠버네티스 플러그인을 설치해야 자동으로 부여해줍니다. 하지만 만약 GCP나 AWS, Azure, OpenStack등을 사용해서 쿠버네티스를 사용한다면 이를 자동으로 부여해줍니다.
apiVersion: v1
kind: Service
metadata:
name: svc-3
spec:
selector:
app: pod
ports:
- port: 9000
targetPort: 8080
type: LoadBalancer
로드밸런서는 실제 운영이 되어야하는 파드들을 연결할 때 사용할 수 있습니다.
Service 심화
서비스에 대해서 간단하게 다시 짚어보고 서비스를 조금 더 깊이있게 파보겠습니다.
서비스란?
서비스는 파드의 외부 접속을 도와주는 오브젝트로서 설저에 따라 접속할 수 있는 범위를 조절할 수 있습니다.
Cluster IP는 외부에서 접속이 불가능하고 쿠버네티스 클러스터 내부에서만 접근이 가능한 Admin 전용 서비스입니다.
Node Port는 내부망에서 접속이 가능하고 Node Port로 서비스를 설정하면 임의의 포트가 노드에 부여되고 노드의 IP와 포트를 알고 있으면 파드로 접근도 가능합니다.
Load Balancer는 외부에서 접근할 수 있게 하는 상용서비스에서 주로 사용됩니다.
DNS Server
만약 파드와 파드를 연결하고 싶은 경우엔 어떻게 해야할까요? 파드에서는 파드의 IP만 알고 있으면 연결이 가능하지만 현실적으로 이는 불가능합니다.
왜냐하면 파드의 IP는 생성될 때마다 새롭게 동적으로 부여되기 때문에 만약 파드가 삭제되면 더는 해당 IP를 사용할 수 없기 때문이죠.
이 때 서비스를 DNS 서버에 올려두면 서비스의 IP가 등록되고 이를 이용해서 서비스 이름으로 서비스를 찾아갈 수 있습니다.

서비스와 파드는 생성과 동시에 IP를 부여받게 되는데 DNS서버는 서비스와 파드의 IP를 관리하고 있습니다. 우리가 흔히 아는 DNS 서버와 같은 역할을 하죠.
쿠버네티스가 DNS서버를 운영할 때는 FQDN이라는 방식을 사용합니다.
FQDN이란 DNS서버가 도메인을 관리하는 방식을 이야기하는 것입니다.
서비스의 경우 {서비스명}:{네임스페이스명}.svc.{DNS서버이름} 이렇게 관리하죠. 이를 예시와 함께 살펴보면,
service1.default.svc.cluster.local 이렇게 짓는다는 의미입니다.
파드의 경우는 {파드의 IP}.{네임스페이스명}.pod.{DNS서버이름} 이렇게 관리합니다. 이를 예시와 함께 살펴보면,
20-109-5-11.default.pod.cluster.local 이렇게 관리한다는 의미입니다.
DNS에 등록된 서비스에 접근할 때는 서비스명으로만 접근이 가능하지만 파드에 접근하기 위해서는 DNS에 적힌 FQDN의 모든 이름을 입력해야 접근이 가능합니다. 하지만 앞서 언급했듯이 파드의 IP는 지속적으로 변경되기 때문에 사실상 사용할 수 없습니다.
Headless
headless는 파드와 파드를 연결할 때 서비스를 통하지 않고 직접 연결하고 싶은 경우에 사용할 수 있는 속성으로서 headless는 DNS서버를 이용합니다.
headless는 서비스에 clusterIP속성을 None으로 주게 되면 자동으로 생성되는데 headless는 DNS에 다음과 FQDN으로 적습니다.
{파드명}.{headless 서비스명}.{네임스페이스명}.svc.{DNS서버명}
그리고 접근할 때는 파드명과 headless 서비스명만 적으면 접근이 가능합니다. 즉, pod1.headless1 이런식으로 접근할 수 있다는 의미입니다.
apiVersion: v1
kind: Service
metadata:
name: headless1
spec:
selector:
svc: headless
ports:
- port: 80
targetPort: 8080
clusterIP: None
Endpoint
보통 서비스와 파드는 label과 selector로 연결됩니다. 하지만 실제로는 label과 selector만으로는 이 둘이 연결될 수 없습니다. 이는 사용자의 관점에서 쓰기 쉬우라고 만들어 놓은것이지 실제로 쿠버네티스는 label과 selector가 있다고 그래서 이 둘을 연결해주지 않습니다.
쿠버네티스는 서비스와 파드를 실제로 연결하기 위해 Endpoint라는 것을 만듭니다. Endpoint는 서비스의 이름과 똑같은 이름으로 만들어지고 Endpoint에는 파드의 IP와 포트가 적혀있습니다.
때문에, 이 Endpoint의 존재를 알고 있다면 실제로 selector가 없는 서비스와 label이 없는 파드도 연결할 수 있습니다.
서비스의 이름과 똑같은 Endpoint오브젝트를 만들고 이 Endpoint에 파드의 IP와 포트를 입력하면 이 서비스와 파드는 실제로 서로 연결이 됩니다.
하지만 앞서서 계속 언급했다시피 파드의 IP는 언제든 바뀔 수 있기 때문에 이렇게 사용할 수는 없습니다. 이런 Endpoint는 쿠버네티스가 서비스와 파드를 어떻게 연결하는지 정도만 알고 있으면 될 것 같습니다.
쿠버네티스는 이런 Endpoint를 사용자 레벨에서 사용할 수 있도록 하는 오브젝트가 있는데 바로 ExternalName입니다.
ExternalName
ExternalName은 Endpoint를 사용자가 쉽게 사용할 수 있도록 제공해주는 오브젝트입니다.
서비스에서 externalName속성에 도메인 이름을 넣게 되면 쿠버네티스는 쿠버네티스 내부의 DNS를 먼저 뒤지고 없다면 사우이 DNS에 쿼리를 날려 도메인에 해당하는 IP를 가져옵니다.
이렇게 ExternalName을 설정해주면 파드가 접속해야하는 곳의 IP주소가 변경되더라도 파드를 재생성하거나 설정을 변경해주지 않아도 된다는 장점이 있습니다.
마치며
서비스는 파드와 짝을 이루는 쿠버네티스의 강력한 기능 중에 하나인 것 같은데... 사실 쿠버네티스를 공부하면 할수록 이런 오브젝트가 수십개가 있다고..? 싶은 생각이 듭니다.
다음 포스팅은 볼륨입니다. 볼륨도 재미난 기능들이 많더라구요. 한번 제대로 파보겠습니다! 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 이론 : 볼륨 (EmptyDir, HostPath, PV / PVC)
이번 포스팅은 쿠버네티스의 저장소 볼륨입니다. 파드가 컨테이너를 가지고 애플리케이션을 구동한다면 볼륨은 그 애플리케이션이 사용할 각종 파일들이나 정보들을 저장해놓는 공간입니다.
파드, 서비스만 하더라도 굉장히 굵직한 기능이었는데 다른 기술이었으면 파드와 서비스로 실서비스를 할 수 있는 정도일겁니다. 그런데 쿠버네티스 이놈은 이런 오브젝트들이 수십개가 있습니다. 정말 미X놈이 아닐 수가 없습니다.
이번 포스팅에선 그 중에서 볼륨에 대해서 공부하고 정리해보겠습니다.
Volume (볼륨)
볼륨에는 EmptyDir, HostPath, PV / PVC 이렇게 세가지가 존재합니다. 하나씩 한번 살펴보도록 하겠습니다.
EmptyDir
EmptyDir은 파드 안에서 각각의 컨테이너가 공통으로 바라보는 저장소로서 파드안에 생성되기 때문에 파드가 사라지면 데이터도 같이ㅏ 삭제된다는 특징이 있습니다.
이렇게 파드안에 컨테이너가 같은 볼륨을 바라보는 경우 컨테이너끼리 특별히 다른 설정을 해주지 않아도 서로 파일을 주고 받을 수 있게 됩니다. 도커의 볼륨과 비슷하네요.
파드가 사라지면 데이터도 같이 삭제되기 때문에 휘발성 데이터를 넣는 것이 일반적입니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-1
spec:
containers:
- name: container1
image: tmkube/init
volumeMounts:
- name: empty-dir
mountPath: /mount1
- name: container2
image: tmkube/init
volumeMounts:
- name: empty-dir
mountPath: /mount2
volumes:
- name: empty-dir
emptyDir: {}
위의 설정에서는 각각 컨테이너들이 다른 path를 바라보고 있는데 name이 empty-dir로 일치하기 때문에 실질적으로는 같은 저장소를 바라본다고 볼 수 있습니다.
HostPath
HostPath는 파드별로 생성되던 EmptyDir과 다르게 노드마다 생성되는 볼륨입니다.
파드별로 생성되는건 정없죠. 파드는 언제든지 삭제될 수 있으니까요. 조금 더 오래 유지되는 데이터를 저장하고 싶은 니즈가 있고 이를 HostPath로 실현할 수 있습니다.
이렇게 노드별로 생성된 볼륨은 그 노드에 들어있는 파드가 사용할 수 있으며 이렇기 때문에 파드가 죽어도 사라지지 않는다는 특징이 있습니다.
하지만 HostPath도 문제가 없는 것은 아닌 것이 노드스케줄러에 의해 파드가 재생성되고 노드에 재배치될 때 같은 노드에 재생성된다는 보장이 없습니다.
때문에, HostPath는 노드가 구동되기위해 사요오디는 시스템 파일들을 저장해야하는 경우에 사용됩니다.
주의해야 하는 점은 파드의 볼륨을 HostPath로 설정하는 경우 실제로 노드에 볼륨이 존재해야 파드를 생성할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-2
spec:
containers:
- name: container
image: tmkube/init
volumeMounts:
- name: host-path
mountPath: /mount1
volumes:
- name: host-path
hostPath:
path: /node-v
type: Directory
위 설정처럼 파드를 생성할 때만약 hostPath설정을 주어 볼륨을 설정하면 host-path라는 볼륨이 존재해야 이 파드가 생성된다는 의미입니다.
PVC / PV
PVC (Persistant Volume Claim)와 PV (Persistant Volume)는 파드가 영속적인 볼륨에 접근할 수 있도록 도와줍니다.
볼륨의 종류는 로컬이 될 수도 있고 Git이 될 수도 있고 AWS, NFS등이 될 수도 있습니다.
쿠버네티스 관리자는 PV를 만들고 사용자는 PVC를 만들어서 파드와 연결해서 사용해야합니다.
그런데 왜 PV를 만들고 이를 곧바로 파드에 연결하지 않고 PVC를 통해서 연결하게 되는 것일까요?
그 이유는 쿠버네티스는 모든 개념을 사용자의 관점과 관리자의 관점으로 나누어서 관리하고 있기 때문입니다. 때문에 볼륨 생성과 PV생성은 관리자, 파드 생성과 PVC생성은 사용자로 나눌 수 있습니다.
apiVersion: v1
kind: PersistentVolume
meatadata:
name: pv-01
spec:
capacity:
storage: 1G
accessModes:
- ReadWriteOnce
local:
path: /node-v
modeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: node, operator: in, value: [node1]}
위의 PV설정은 로컬 스토리지를 사용하는 경우에 해당하는 설정입니다. 만약 AWS나 StorageOS와 같은 온프레미스 스토리지 솔루션을 사용한다면 다른 설정을 가질 수 있습니다. 추후에 실습을 할 때 한번 본격적으로 다뤄보도록 하겠습니다. 자세한 내용은 쿠버네티스 실습 : 볼륨 편에서 진행하도록 하겠습니다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-01
spec:
accessModes:
- ReacWriteOnce
resources:
requests:
storage: 1G
storageClassName: ""
위의 PVC 설정에서 눈여겨 봐야 할 것이 storageClassName을 "" 이렇게 빈칸으로 냅둔 것인데, 빈칸으로 냅두지 않으면 다르게 동작하기 때문입니다. 이에 대해서는 다음 섹션인 볼륨 (심화) 에서 다루도록 하겠습니다.
apiVersion: v1
kind: Pod
metadata:
name: pod-volume-3
spec:
containers:
- name: container
mountPath: /volume
volumes:
- name: pvc-pv
persistentVolumeClaim:
claimName: pvc-01
이렇게 PVC와 PV를 만들고 파드에 이를 연결하면 영속적인 볼륨을 가질 수 있습니다.
여기까지 볼륨에 대한 간단한 개요를 알아봤습니다. 이제 뒤이어 심화버전을 알아보도록 하겠습니다.
볼륨 (심화)
볼륨 심화편을 들어가기에 앞서 볼륨에 대해서 간단하게 정리하고 넘어가겠습니다.
볼륨이란?
볼륨은 EmptyDir, HostPath, PV / PVC 이렇게 세 가지 타입이 있습니다.
EmptyDir은 파드에 한정된 볼륨입니다.
HostPath는 노드에 한정된 볼륨이구요.
때문에 우리는 PV / PVC를 많이 사용하게 됩니다.
먼저 쿠버네티스 관리자가 PV를 만듭니다. PV를 만들 때 스토리지 용량과 AccessMode를 설정하고 PVC를 만들면 AccessMode에 맞는 PV를 선택해서 사용할 수 있습니다.
AccessMode에는 ReadWriteOnce (RWO), ReadOnlyMany (ROM), ReadWriteMany (RWM) 이렇게 세 가지가 존재합니다. 이에 대해서는 자세히 짚고 넘어가지 않겠습니다. 그냥 읽기 쓰기에 대한 권한을 부여한다고 간단하게 짚고 넘어가겠습니다.
하지만 이 대목에서 관리자가 PV가 필요할 때마다 PV를 만들어서 PVC와 연결해주기는 굉장히 번거롭습니다.
이를 위해 쿠버네티스에선 Dynamic Provisioning이라는 기술이 볼륨을 동적으로 할당해줍니다.
이 기능을 사용하면 PVC만 만들어주면 자동으로 PV를 할당해줍니다.
이때 PV는 온프레미스 솔루션인 StorageOS일 수도 있고, NFS일 수도 있고, 클라우드 환경에 존재한느 스토리지일 수도 있습니다.
그럼 쿠버네티스는 PVC만 만들었는데 어떻게 그에 걸맞는 PV를 자동으로 연결해줄 수 있는 것일까요? 바로 StorageClass라는 이름을 지어주면 됩니다. 우리가 위에서 잠깐 언급했던 그 StorageClass 맞습니다.
만약 StorageClass를 fast라고 적어주고 PVC를 만들 때 스토리지 용량과 AccessMode, StorageClassName을 fast로 연결해주면 설정한 스토리지 용량과 AccessMode를 할당해주고 스토리지 클래스를 이용해서 PV를 이어줍니다.
볼륨의 상태에서는, 정확히는 PV의 상태, Available, Bound, Released, Failed 이렇게 네 가지가 있습니다.
PV가 생성되고 맨 처음에는 Available이라는 상태값을 가집니다. 이후 PVC가 생성되면 Bound상태가 됩니다. 이렇게 생성된 PVC에 파드가 연결되면 이 파드가 볼륨을 사용할 수 있게 됩니다.
이 상황에서 만약 파드가 죽어버리더라도 PVC, PV가 살아있기 때문에 파드의 상태에 영향을 받지 않는 것입니다. 이 PV는 PVC가 없어져야 비로소 반응하게 되는데 이 때의 상태값이 다시 세개로 나누어집니다. 이때의 상태값을 ReclaimPolicy라고 합니다.
• Retain : StorageClass를 사용하지 않았을 때의 기본 정책이고 PVC가 삭제되더라도 데이터는 보존됩니다. 하지만 PVC가 삭제되더라도 PV로 삭제가 전파되지 않아 데이터는 보존되지만 다른 PVC를 연결해서 사용할 수는 없습니다.
Retain의 경우 PVC가 삭제되면 다른 PVC를 연결해서 사용할 수 없기 때문에 PV를 직접 생성했던 것과 마찬가지로 직접 PV를 삭제해줘야합니다.
• Delete : StorageClass를 사용할 때의 기본 정책이고 PVC가 삭제되면 PV가 같이 삭제됩니다. 때문에, 데이터가 보존되지 않고 재사용할 수도 없습니다.
• Recycle : 이 정책은 PVC가 삭제되면 PV는 데이터를 전부 지워버리고 다시 재사용 가능한 PV로 만들어버립니다.
마치며
이번 포스팅에선 쿠버네티스의 볼륨에 대해서 알아봤습니다. 쿠버네티스를 공부하면 할수록 왜 이 기술이 표준이 되었는지 알게되었습니다. 오브젝트들 하나하나의 기능이 정말 강력하네요.
다음 포스팅은 Authentication, Authorization입니다. 인증과 인가에 관련한 내용도 풍부하게 있으니 다음 포스팅에서 뵐게요! 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 이론 : 인증, 인가
쿠버네티스는 쿠버네티스와 관련된 명령어를 사용할 때 마스터 노드에 존재하는 API Server를 지나가게 되어있습니다.
하지만 어떤 곳에서든 kubectl같은 명령어를 사용할 수 있다면 이는 보안적으로 좋지 못하겠죠?
이번 포스팅에선 쿠버네티스가 어떤 인증 체계를 가지고 있으며 어떻게 권한에 접근하게 하는지에 대해서 공부한 내용을 정리해보도록 하겠습니다.
Kubernetes API Server
쿠버네티스의 API Server에 접근하기 위해서는 HTTPS로 접근해서 인증서가 존재하는 경우에만 접근하게 할 수 도 있고, 마스터 노드에서 사용할 수 있는 kubectl에 프록시를 연결해서 HTTP로 연결하는 방법도 있습니다. 만약 쿠버네티스 클러스터를 여러개 운영하고 있다면 모든 쿠버네티스 API Server에 접근할 수 있도록 설정파일을 만드는 방법도 있죠.
또한, 파드에서도 kubectl과 같은 명령어를 사용할 수 있는데 파드에서 네임스페이스가 나눠져 있는 경우 API Server에 네임스페이스별로 접근할 수도 있고 접근하지 못할 수도 있습니다.
이렇듯 쿠버네티스는 API Server에 접근할 때 사용자의 관점인 User Account와 파드와 같은 오브젝트 관점인 Service Account를 제공합니다.
이때, 어떤 사용자가 API Server에 접근할 수 있을지를 정하는 Authentication이 있고, 사용이 가능하다면 어디까지 사용할 수 있는지에 대한 권한을 부여할 수 있는 Authorization이 있습니다.
이뿐만 아니라 예를 들어서 쿠버네티스 관리자가 볼륨을 생성할 때 최대 용량을 10기가로 설정해놓으면 10기가를 넘는 용량을 스토리지로 만들 때 생성할 수 없도록 제한할 수 있는 Admission Control이 있습니다.
다음 섹션에선 Admission Control을 제외한 Authentication과 Authorization에 대해서 정리해보았습니다.
Authentication
Authentication (이하 인증) 은 API Server에 어떤 사용자가, 어떤 서비스를 이용 가능할지 쿠버네티스에게 알려주는 용도입니다.
API Server에 접근하는 상황을 HTTPS, kubectl proxy, kubeconfig, Service Account 이렇게 네 가지 상황으로 나누어 자세히 알아보도록 하겠습니다.
API Server에 HTTPS로 접근
API Server에 접근하는 첫 번째 방법은 인증서를 이용한 HTTPS로서 쿠버네티스는 맨 처음 클러스터가 만들어지면 6443포트로 API Server를 열어둡니다. 이 포트로 접근해 API Server에 인증 받기 위해선 kubeconfig에 적혀있는 클라이언트 개인키와 인증서를 모두 가지고 있어야합니다.
kubeconfig란 쿠버네티스가 구동될 때 이 파일을 기반으로 동작하는데 그 때 인증기관의 인증서와 개인키, 인증서를 모두 가지고 있습니다.
인증 순서에 대해서 잠깐 짚고 넘어가자면
• 인증기관의 개인키 : 인증 요청서인 csr파일이 있으면 바로 인증서로 바꿔줍니다.
• 클라이언트 개인키 : 인증 요청서인 client csr이 있으면 인증기관의 개인키와 인증기관의 인증서와 인증요청서를 가지고 클라이언트 인증서를 만들어줍니다.
이렇게 만들어진 클라이언트 인증서와 클라이언트 개인키가 kubeconfig에 등록되게 되고 이 인증서와 개인키를 사용자가 들고 API Server에 접근하면 사용자가 인증되고 API Server에 자유롭게 접근할 수 있습니다.
우리가 기본적으로 kubectl을 설치하면 이때 만들어진 kubeconfig를 그대로 복사해서 설치가 되기 때문에 우리는 별다른 설정 없이 바로 kubectl을 사용할 수 있는 것입니다.
kubectl proxy로 API Server에 접근
kubectl proxy로 API Server로 접근하는 것은 사실 보안적으로 문제가 있을 수 있기 때문에 많이 사용되지 않고 권장하는 방식도 아닙니다.
때문에 간단하게만 짚고 넘어가도록 하겠습니다.
kubectl을 설치할 때 kubeconfig에 프록시 설정 (host, port)을 해두면 사용자가 인증서 없이 곧바로 kubectl로 접근할 수 있습니다.
이 방법은 외부망으로 연결되어 있으면 굉장히 위험하고 내부망으로 연결해도 보안상 좋지 않기 때문에 잘 사용되지 않습니다.
kubeconfig로 멀티 클러스터 연결
만약 멀티 클러스터를 이용하는 경우, 사용자의 kubectl을 설치할 때 여러개의 kubeconfig를 이용해서 접근할 수 있습니다.
kubeconfig에는 클러스터의 정보와 유저의 정보, 그리고 이 클러스터와 유저를 이어주는 컨텍스트가 존재합니다.
클러스터엔 클러스터 이름, 접근 정보 (URL등), 그리고 인증기관의 인증서가 존재합니다. 유저엔 유저이름, 유저의 개인키, 유저의 인증서가 존재하죠. 마지막으로 컨텍스트엔 컨텍스트이름, 클러스터이름, 유저이름 이렇게 적혀있어서 어떤 유저가 어떤 클러스터와 연결되어있는지 알 수 있습니다.
이런 여러개의 컨텍스트를 가지고 있다면 유저는 kubectl을 사용할 때 kubectl config 명령어를 이용해서 클러스터에 접근하고 API Server에도 접근할 수 있는 것입니다.
Service Account에서 API Server에 접근
마지막으로 파드와 같은 서비스들이 API Server에 접근하는 것입니다.
네임스페이스를 만드렉 되면 default라는 이름의 Service Account가 자동으로 생성됩니다. 이 Service Account는 자체적인 Secret을 가지고 있으며 여기에는 인증기관의 인증서가 들어있고 토큰이 들어있습니다.
만약 파드가 이 네임스페이스에 생성되면 파드가 Service Account에 연결되고 이 파드는 API Server에 접근할 수 있는 것입니다.
Authorization
쿠버네티스에서 Authorization (이하 인가)에서 가장 많이 쓰이는 것이 RBAC입니다. RBAC란 Role Based Access Control의 약자로 역할을 기반으로 접근을 통제하는 것입니다.
쿠버네티스는 클러스터단위로 관리된느 Node, PV, Namespace같은 것들이 있고, 네임스페이스 단위로 관리되는 Pod, Svc같은 것들이 있습니다. 간단하게는 클러스터 자원과 네임스페이스 자원으로 나눌 수 있습니다.
즉, 파드가 클러스터 자원에 접근하기 위해서 네임스페이스를 만들면 Service Account가 만들어지고 이 Service Account (이하 SA)에 역할을 연결하면 됩니다.
네임스페이스 자원에 대한 인가를 위해서는 역할을 생성하고 이를 SA와 연결하면 되는데 이 가운데 RoleBinding이라는 친구가 이둘을 연결해줍니다.
먼저 자원에 대한 인가를 위해 Role을 만들고 RoleBinding을 이용해서 SA와 연결하면 이 네임스페이스에 들어와있는 파드와 서비스에게 이 역할이 부여되고 네임스페이스 자원을 사용할 수 있습니다.
만약 SA가 클러스터 자원에 접근하고 싶은 경우 ClusterRole과 ClusterRoleBinding을 이용해서 똑같이 바인딩해주면 됩니다.
그냥 Role과 RoleBinding이랑 다른 점은 클러스터 자원을 각각 다르게 지정해줄 수 있다는 것입니다. 즉, Node나 PV, Namespace를 직접 지정해서 역할을 부여해줄 수 있습니다.
굉장히 객체지향적이야..
만약 네임스페이스 내부의 자원들이 클러스터 자원에 접근하기를 원하지 않고 네임스페이스 내부 자원만 사용하고 싶으면 어떻게 할까요?
그럴 때는 RoleBinding과 Role을 SA로 연결하지 않고 바로 ClusterRole과 연결하면 해당 네임스페이스의 자원만 접근이 가능합니다.
왜 이렇게 하는걸까요? 이렇게 하면 그냥 Role을 연결하는 것과 뭐가 다르죠?
만약 모든 네임스페이스에서 Role을 관리해야한다면 Role이 조금 변경되면 모든 네임스페이스에 연결된 모든 Role을 변경해줘야하고 그런 경우 매우 관리하기가 힘들기 때문에 ClusterRole을 RoleBinding과 연결해서 해당 네임스페이스는 클러스터 자원과 연결될 수 없다는 것을 쿠버네티스에게 알려주는 것입니다.
Role, RoleBinding의 디테일
Role, RoleBinding을 조금 더 디테일하게 보면, Role에는 어떤 API그룹을 허용할 것인지 정하는 apiGroups가 있고, 어떤 자원을 허용할 것인지에 대한 resources가 있습니다. 또한, 어떤 역할을 수행할 것인지에 대한 verbs가 있습니다.
만약 Job에 대한 Batch작업을 조회할 수 있는 읽기 권한을 부여한다고 가정하면
apiGroups: [batch]
resources: [jobs]
verbs: [get, list]
이렇게 설정하면 됩니다. 이 부분에 대한 더 자세한 내용은 쿠버네티스 실습 : 인증, 인가 포스팅에서 다루도록 하겠습니다.
RoleBinding은 roleRef와 subjects가 있고 roleRef에는 어떤 Role을 지정할지 Role의 이름을 적으면 되고, subjects에도 SA를 적으면 SA와 Role이 연결됩니다.
만약 어떤 네임스페이스에서 모든 클러스터 자원에 접근하기를 원한다면 ClusterRole을 등록하고
apiGroups: [""]
resources: [""]
verbs: ["*"]
이렇게 설정하고 ClusterRoleBinding에서 roleRef에 ClusterRole을 등록하고 subjects에 SA를 등록하면 됩니다.
마치며
인증과 인가 부분도 흥미롭게 공부했습니다. 하지만 인가 부분을 공부할 때 조금 의아했던 것이 쿠버네티스의 철학입니다.
Role과 SA를 바로 연결하는게 아니라 RoleBinding을 통해서 연결하는게 뭔가 어디서 봤다 싶었는데 파드와 서비스를 연결하는게 비슷하더라구요.
파드와 서비스도 곧장 둘이 연결되는게 아니라 Endpoint라는 오브젝트를 통해서 연결이 되던 것이 Role과 SA를 바로 연결하지 않고 RoleBinding이라는 오브젝트로 연결하는것과 동일했습니다.
조금 느낌이 다르긴 하지만 멀티클러스터링에서의 인증에서도 kubeconfig내부에서 컨텍스트로 클러스터와 유저를 엮은 것과 비슷하기도 합니다.
이것이 한두개면 그러려니 하겠는데 거의 대부분의 연결이 이런 방식으로 이루어져있어서 이게 쿠버네티스가 추구하는 철학과 관련된 것인가 하는 느낌이 들었습니다.
조금씩 더 공부하면서 확신을 가지게 되겠지만 우선은 이대로 궁금증만 남기고 마무리지으려합니다.
이번에도 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 이론 : 컨트롤러 (ReplicaSet, StatefulSet)
이번 포스팅에선 쿠버네티스의 꽃인 (대체 꽃이 몇송이야) 컨트롤러에 대해서 공부한 내용을 정리해보도록 하겠습니다.
컨트롤러는 파드를 관리하면서 애플리케이션을 안정적으로 운영할 수 있도록 도와줍니다. 컨트롤러는 크게 네 가지 기능을 제공하는데 순서대로 한번 보시죠!
• Auto Healing : 만약 파드가 죽어서 애플리케이션을 사용할 수 없으면 자동으로 살려주는 역할을 합니다.
• Auto Scale Out / Scale In : 컨트롤러는 파드의 개수를 자유자재로 조절하면서 스케일아웃, 스케일인을 구현할 수 있습니다.
• Deployment : 애플리케이션의 버전을 업데이트 하는 과정에서 자연스러운 무중단 배포를 진행해주고 만약 문제가 생기는 경우 롤백까지 진행해줍니다.
• Job : 특정 파드가 잠깐 수행해야하는 일이 이싸으면 해당 파드에 자원을 할당해주고 일이 끝나면 자원을 회수하는 역할을 수행합니다.
이번 포스팅에선 설정만 해놓으면 별일 없이 잘 수행되는 Auto Healing과 Job을 제외하고 Auto Scale Out / In과 Deployment를 중점적으로 살펴보도록 하겠습니다.
Controller (컨트롤러)
컨트롤러에서 쿠버네티스는 기존에 Replication Controller를 사용했습니다. 하지만 이제 Deprecated 됨으로써 ReplicaSet을 사용해야하는데요.
Replication Controller와 ReplicaSet에서 모두 지원하는 template과 replicas에 대해서 짚고 넘어가고 ReplicaSet에서만 지원하는 selector에 대해서 짚고 넘어가겠습니다.
template / replicas
apiVersion: v1
kind: ReplicationController
meatdata:
name: replication-1
spec:
replicas: 3
selector:
type: web
template:
metadata:
name: pod-1
labels:
type: web
spec:
containers:
- name: container
image: tmkube/app.v2
template은 어떤 파드를 기준으로 레플리카를 만들 것인지 설정하는 것입니다. 보통 파드의 메타정보와 스펙에 대해서 입력하면 그 파드를 기준으로 레플리카를 만들어주죠.
apiVersion: v1
kind: Pod
metadata:
name: pod-1
labels:
type: web
spec:
containers:
- name: container
image:
tmkube/app.v1
라벨로 선택할 파드가 존재해야 이 Replication Controller가 작동한다는 특징이 있습니다.
selector
selector는 ReplicaSet에서만 존재하는 특징으로 Replication Controller는 라벨로 파드를 관리했던 것과 다르게 조금 더 세세하게 파드를 설정할 수 있다는 특징이 있습니다.
ReplicaSet의 selector는 기존 라벨 방식의 matchLabels속성과 조금 더 세세한 설정이 가능한 matchExpressions 속성이 있습니다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: replica-1
spec:
replicas: 3
selector:
matchLabels:
type: web
matchExpressions:
- {key:ver, operator: Exists}
template:
metadata:
name: pod
operator에는 Exists말고도 세 가지 속성이 더 있는데 한번에 정리해보죠!
• Exists : Exists는 key에 적혀 있는 값이 존재하면 해당 파드와 컨트롤러를 연결하는 역할을 합니다.
• DoesNotExist : DoesNotExist는 Exists와 반대로 key에 적혀있지 않은 파드들을 연결합니다.
• In : In속성은 key말고도 value를 설정하여 더 세세하게 파드를 연결할 수 있게 합니다. 예를 들어서 key: A, value: 1이렇게 설정하면 A:1로 설정된 파드와 연결됩니다.
• NotIn : NotIn은 In과 반대되는 특징을 가집니다.
이 상황에서 컨트롤러가 사라지면 파드가 같이 사라진다는 특징이 있는데 이를 kubectl로 cascade 속성을 false로 주면 컨트롤러만 삭제시킬 수 있습니다.
selector를 사용할 때 주의사항은 matchLabels에 적혀있는 라벨과 템플릿에 있는 라벨이 일치해야 ReplicaSet이 생성됩니다.
마찬가지로 matchExpressions를 사용할 때도 템플릿에 있는 값들을 모두 포함해야합니다.
Stateful vs Stateless
ReplicaSet이 파드를 복제해서 안정적인 서비스를 이뤄내는 것과는 별개로 조금 더 신경써야 하는 부분이 있습니다.
바로 Stateful과 Stateless입니다.
앞서 언급한 ReplicaSet은 사실 Stateless 애플리케이션에 적합한 오브젝트입니다. 여기서 Stateless 애플리케이션과 Stateful 애플리케이션을 짚고 넘어가도록 하겠습니다.
Stateless 애플리케이션
Stateless한 애플리케이션에 적합한 예시는 Web Server들이 있는데 Apache Web Server나 Nginx같은 서비스가 바로 그것이죠.
이런 서비스들의 특징은 각각의 파드들이 공통된 역할을 가지고 있다는 것입니다. 즉, 만약 레플리카를 만들더라도 이전에 배포했던 파드와 똑같은 파드를 만들어도 크게 문제될 것이 없다는 것입니다.
이런 특성이 있어서 만약 파드에 볼륨을 만들어주려고 해도 모든 파드들이 동일한 볼륨을 바라봐도 상관없습니다.
또한, Stateless 애플리케이션들의 네트워크 특징을 보면, 유저가 네트워크를 이용해 애플리케이션에 접근하고 이 트래픽은 한쪽에 쏠리면 안되기 때문에 동일한 비율로 로드밸런싱이 됩니다.
Stateful 애플리케이션
반면 Stateful 애플리케이션은 Stateless 애플맄레이션과 반대로 각각의 파드들이 각자의 역할을 가지고 있고 이 때문에 볼륨도 다른 볼륨을 가지고 있어야하며 심지어 같은 노드에 배포되지 않아야 하는 경우도 있습니다.
또한, 보통 Stateful 애플리케이션은 내부 시스템들이 접근하는 경우가 많고 이 트래픽도 역할에 따라 부하가 분배되는 곳이 다른 목적지를 가지는 경우가 많습니다.
이런 Stateful 애플리케이션의 대표적인 예시는 바로 데이터베이스입니다.
데이터베이스가 배포된 파드들은 읽기, 쓰기가 가능한 마스터와 읽기만 가능한 슬레이브가 있고 만약 마스터가 죽어버리면 슬레이브를 마스터로 승격시키기 위해 감시하고 있는 Arbiter (아비터)가 있습니다.
일반적인 데이터베이스를 생각해보면 각 파드에서 접근해야하는 저장소도 다른 것이 당연하고 마스터가 들어있는 노드 전체가 죽어버리는 경우를 대비해 무조건 다른 노드에 배포되어야합니다.
이런 특징 때문에 Stateless 애플리케이션과 Stateful 애플리케이션을 다르게 관리해줘야합니다.
Stateless 애플리케이션은 이전에 봤던 ReplicaSet을 이용해 관리하고 이와 반대되는 Stateful 애플리케이션은 StatefulSet이라는 오브젝트로 관리됩니다.
ReplicaSet과 StatefulSet의 차이 : 파드의 생성과 삭제
StatefulSet은 ReplicaSet과 비슷하지만 두드러지게 다른 특징들이 있는데 하나씩 살펴보도록 하겠습니다.
replicas가 1인 경우
• ReplicaSet : 만약 replicas 속성이 1인 경우 ReplicaSet은 랜덤한 이름으로 파드의 이름을 짓습니다. ReplicaSet은 이렇게 해도 상관이 없는 것이, 어차피 레플리카들이 같은 역할을 하고 있는 복제품이기 때문입니다.
• StatefulSet : StatefulSet은 replicas 속성이 1인 경우 파드의 이름을 인덱싱해서 지어줍니다. Stateful한 애플리케이션들은 파드의 이름조차 고유한 역할을 부여받기 위해 사용되기 때문에 이 이름이 막 지어지면 뒤에서 언급할 Headless Service를 이용할 때 불편하기 때문에 정직하게 인덱싱하여 이름이 지어집니다.
replicas가 3인 경우
• ReplicaSet : replicas를 늘리게 되면 ReplicaSet은 동일한 역할을 하는 파드를 동시에 생성합니다. 어차피 동시에 생성되어도 상관없기 때문에 동시에 생성됩니다. 또한, 만약 파드가 죽어서 재생성 되더라도 랜덤하게 새로운 이름을 부여해주면서 파드를 재생성합니다.
• StatefulSet : StatefulSet은 replicas가 3이 되면 파드를 동시에 생성하지 않고 순차적으로 생성합니다. 이름도 인덱싱을 해주면서 순차적으로 생성됩니다. 또한, 만약 파드가 죽으면 죽은 파드와 동일한 이름으로 재생성해줍니다. 이 이유도 뒤에 언급할 Headless Service를 이용할 때 필요하기 때문입니다.
replicas가 0인 경우
• ReplicaSet : 만약 replicas를 0으로 바꾸면 ReplicaSet으로 관리되는 파드들은 모두 동시에 삭제됩니다.
• StatefulSet : StatefulSet은 replicas를 0으로 하더라도 파드가 삭제되는 것이 생성되었던 것의 역순으로 삭제됩니다.
ReplicaSet과 StatefulSet의 차이 : PVC와 Headless Service
ReplicaSet과 StatefulSet의 차이는 파드의 생성과 삭제뿐만 아니라 볼륨을 연결할 때와 Headless Service의 관점에서도 두드러지는 차이를 보여줍니다.
replicas가 0인 경우
• ReplicaSet : ReplicaSet으로 생성된 파드들은 기존에 PV와 PVC가 생성되어있어야 파드와 볼륨이 적절하게 매칭이됩니다. 이 때문에 PVC와 PV를 직접 생성해야한다는 특징이 있습니다.
• StatefulSet : StatefulSet은 PV template이 존재해 이 템플릿을 기반으로 PVC가 동적으로 바인딩됩니다.
replicas가 3인 경우
• ReplicaSet : ReplicaSet으로 관리되는 파드들은 동일한 PVC를 바라보고 있어도 전혀 문제가 되지 않습니다. 왜냐하면 애플리케이션 자체가 상태를 공유하고 있을 필요가 없기 때문입니다. 이러한 특징 때문에 PV가 한 노드에 생성되면 모든 파드들이 PVC와 동일한 노드에 배치되어야합니다. 만약 다른 노드에 파드가 배치되면 이 PVC와 연결될 수 없습니다.
• StatefulSet : StatefulSet이 관리하는 파드들은 각자가 가지고 있는 고유한 볼륨이 있어야하고 이 때문에 파드가 전혀 다른 노드에 배치되더라도 새로운 PV를 할당해줍니다.
replicas가 0인 경우
• ReplicaSet : ReplicaSet은 replicas가 0이 되면 PV와 PVC가 static으로 만들어진만큼 삭제되지는 않습니다. 하지만 이후 이 PV와 PVC가 다시 재활용되지는 않습니다. 이 내용은 쿠버네티스 이론 : 볼륨 포스팅에 자세히 나와있습니다. 참고해주시면 감사하겠습니다.
• StatefulSet : 반면 StatefulSet으로 만들어지는 PV와 PVC는 파드가 삭제된다고 하더라도 삭제되어서는 안됩니다. 그 볼륨 자체가 이 파드들의 상태를 보관하고 있는 또 하나의 기준이 될 수 있기 때문입니다.
또한, StatefulSet이 Headless Service와 연결하면 내부 시스템인 파드가 해당 파드를 바라볼 때 DNS Server의 FQDN에 의해 관리되는 도메인 이름으로 접근할 수 있습니다. 이렇기 때문에 이 파드가 죽어서 재생성되더라도 해당 파드의 이름으로 그대로 살려줘야 쿠버네티스 내부 시스템들이 이 파드를 바라볼 때 동일한 도메인 명으로 관리될 수 있습니다.
마지막으로 ReplicaSet과 StatefulSet의 yaml파일을 비교해보고 해당 섹션을 마무리지어보도록 하겠습니다.
ReplicaSet
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: replica-web
spec:
replicas: 1
selector:
matchLabels:
type: web
template:
metadata:
labels:
type: web
spec:
containers:
- name: container
image: kubetm/app
terminationGracePeriodSeconds: 10
StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: replica-web
spec:
replicas: 1
selector:
matchLabels:
type: db
template:
metadata:
labels:
type: db
spec:
containers:
- name: container
image: kubetm/app
terminationGracePeriodSeconds: 10
Deployment
ReplicaSet을 이용해서 버전을 업데이트하는 방법은 네 가지가 있습니다. Recreate, Rolling Update, Blue / Green, Canary 이렇게 네 가지를 하나씩 살펴보도록 하겠습니다.
Recreate
Recrate는 기존에 동작하던 파드를 삭제시키고 다시 띄우는 방법으로서 다운 타임이 존재한다는 단점이 존재합니다.
Recreate는 기존의 ReplicaSet의 replicas 속성을 0으로 만들고 난 뒤 새로운 ReplicaSet의 템플릿을 v2로 설정하고 다시 만들면 됩니다.
Recreate는 여러개의 ReplicaSet을 가지고 잇고 이 많은 ReplicaSet을 구별하기 위해 따로 label과 selector를 부여받습니다.
Rolling Update
Rolling Update는 기존의 동작하던 파드와 새로운 버전의 파드를 동시에 Service와 연결시켜 하나씩 새로운 버전으로 넘어가도록 하는 전략으로서 다운타임이 발생하지 않지만 새로 추가하는 파드만큼의 리소스가 필요하다는 단점이 있습니다.
Rolling Update는 기존의 ReplicaSet에서 동작하던 v1을 냅둔 상태에서 v2의 새로운 ReplicaSet을 만드고 새로운 파드의 라벨을 현재 운영중인 Service와 맞춥니다. 그리고 ReplicaSet의 replicas를 조절하면서 배포를 진행합니다.
Blue / Green
블루그린 배포는 쿠버네티스의 Deployment에서 기본적으로 제공해주는 배포방식은 아닙니다.
기존의 존재하던 파드와 새로운 버전의 파드를 순식간에 바꿔치기 하면서 트래픽을 이동시켜 다운타임이 발생하지 않지만 현재 운영중이던 파드의 개수만큼의 리소스가 필요해서 여타 다른 배포 방식보다 리소스가 많이 든다는 단점이 있습니다.
블루그린 배포는 ReplicaSet을 두개 만들고 Service가 바라보고 있는 라벨을 ver1에서 ver2로 바꿔줌으로써 순식간에 트래픽을 움직인다는 특징이 있습니다.
이런 특징 때문에 만약 ver2에서 문제가 발생하면 Service의 라벨만 다시 ver1으로 바꿔주면 되기 때문에 롤백이 쉽다는 장점이 있습니다.
Canary
카나리 배포는 먼저 배포중이던 파드와 새로운 버전의 파드를 동시에 배포해 새로운 버전의 안정성을 검사한 뒤에 모든 파드를 새로운 버전으로 옮기는 방법으로서 다운타임이 발생하지 않고 자원이 적게든다는 장점이 있습니다.
카나리 배포는 여러개의 Service를 바라보는 Ingress Controller가 있고 Ingress 트래픽을 url로 나눠 안정성을 체크합니다. 모든 안정성 테스트를 마치게 되면 새로운 버전으로 모두 바꿔 배포를 완료합니다.
이 카나리 배포는 바로 뒤에 언급할 Ingress Controller에서 자세히 다루겠습니다.
나머지 배포 방식은 쿠버네티스 실습 : 컨트롤러 편에서 더 자세히 다루도록 하겠습니다.
Ingress Controller
MSA를 위해서라면 서로 다른 서버에 들어오는 트래픽을 API로 관리해야하고 이를 관리하기위해 Ingress 라는 오브젝트가 필요합니다.
하지만 Ingress는 오브젝트 그 자체로는 뭔가 할 수 있는게 없어서 Ingress를 구현하는 구현체가 있어야합니다.
이를 위해 각기 다른 파드를 만들고 이 파드들과 연결되는 서비스를 만들어 외부로 노출합니다. 그리고 Ingress를 구현하는 구현체가 배포된 파드를 만들면 그 파드가 Ingress rule과 맞는 Ingress를 매치합니다.
이런 Ingress 구현체들이 있는 것을 Ingress Controller라고 하며 보통 Ingress Controller는 Nginx같은 웹서버가 됩니다.
Ingress Controller를 만들기 위해서는 Nginx를 위한 네임스페이스가 만들어져야하고 이 네임스페이스에 Deployment를 만들어야합니다. 이를 위해 ReplicaSet을 만들고 template을 Nginx이미지로 설정합니다.
보통 Ingress Controller의 역할은 크게 세 가지가 있는데 Service LoadBalancing, Canary Upgrade, HTTPS입니다.
• Service LaodBalancing : 말 그대로 서비스들을 로드밸런싱하기 위해 사용하는 것으로서 파드와 그 파드를 연결하는 서비스를 만듭니다. 그리고 이 서비스를 Ingress 오브젝트에 등록합니다. 이후 Nginx같은 Ingress Controller를 만든 뒤 이 파드와 연결되는 서비스를 NodePort로 배포합니다. 이때 배포된 포트번호와 Ingress 오브젝트에서 설정해둔 path를 이용해 매칭이됩니다.
• Canary Upgrade : 이런 Ingress Controller를 이용하면 카나리 배포를 진행할 수 있습니다. 기존에 운영되던 파드와 서비스에 대한 Ingress가 있을 것이고 이를 NodePort 서비스로 다시 연결하여 외부로 연결한 상태에서 새로운 버전의 파드를 생성하고 이를 서비스와 연결합니다. 이 새로운 파드와 연결된 서비스를 새로운 Ingress 오브젝트에 등록하고 @weight 어노테이션을 이용해서 가중치를 둡니다. 그리고 이 테스트가 끝나면 weight를 100퍼센트로 바꾸어 배포를 완료합니다.
• HTTPS : Ingress Controller에서 HTTPS를 설정하는 경우는 파드에서 자체적으로 인증서를 이용해서 HTTPS를 해결할 수 없는 경우에 사용합니다. HTTPS를 위해 Ingress 오브젝트에 host와 service를 연결하는 rules를 만들고 tls라는 속성에 secretName속성을 이용해 우리가 따로 만든 Secret오브젝트와 연결할 수 있습니다. Secret오브젝트에는 인증기관의 개인키와 인증서가 들어있으며 이를 Ingress Controller가 사용하게 하며 HTTPS를 적용할 수 있습니다.
Ingress Controller는 쿠버네티스 실습 : 컨트롤러 편에서 더 자세히 다뤄보도록 하겠습니다.
Autoscaler
컨트롤러의 주요 기능 중 하나는 Auto Scale이 있습니다. Auto Scale에도 똑같은 파드를 복제하는 Horizontal Pod Autoscaler (HPA), Vertical Pod Autoscaler (VPA), Cluster Autoscaler (CA)가 있습니다.
HPA는 파드가 사용하는 자원이 임계치를 넘어갈 때 컨트롤러와 연결된 HPA가 있다면 이 HPA가 파드의 자원 상태를 모니터링하고 있다가 컨트롤러의 replicas를 늘려줍니다.
이렇게 파드의 개수를 증가시키는 것을 Scale Out, 반대로 자원을 보고 파드의 개수를 다시 줄이는 것을 Scale In이라고 합니다.
VPA는 파드가 사용하고 잇는 자원이 임계치를 넘어갈 때 컨트롤러와 연결된 VPA가 있다면 VPA가 파드의 자원 상태를 모니터링 하고있다가 파드의 자원 임계치를 높여줍니다.
이렇게 파드의 자원의 양을 늘려주는 것을 Scale Up, 반대로 낮추는 것을 Scale Down이라고 합니다.
HPA의 주의사항
HPA는 주로 Stateless 애플리케이션에서 사용해야합니다. 그 이유는 Stateful 애플리케이션의 경우 각각의 파드가 정해진 역할이 있기 때문에 무작정 막 늘릴 수 없기 때문입니다.
HPA가 봤을 때 어떤 역할의 파드를 어떻게 늘려야하는지 감지할 수 없기 때문에 HPA는 기동이 빠르게 되는 애플리케이션과 Stateless 애플리케이션에 사용하는 것을 권장하고 있습니다.
VPA의 주의사항
VPA는 HPA와 반대로 Stateful 애플리케이션에 잘 어울리는 확장 방식입니다. Stateful 애플리케이션의 경우 무작정 파드를 늘릴 수 없기 때문에 이렇게 자원의 양 자체를 늘려버리는 것을 선택하는 것이 권장됩니다.
보통 Stateful 애플리케이션의 경우 우리 비즈니스의 상황에 맞게 미리 레플리카를 준비하는 경우가 많아 무작정 파드의 개수를 늘리는 것이 아닌 자원의 양을 늘려주는 것이 일반적입니다.
CA의 특징
CA는 현재 클러스터 내부에 존재하는 노드의 자원이 없는 경우 노드를 새로 생성해서 그 노드에 파드를 배치해주는 것입니다.
보통 노드 = 서버이기 때문에 쿠버네티스 입장에선 이 서버를 직접 늘려주지는 못하지만 각종 클라우드 서비스들이 Auto Scaling Group을 지원하고 있기 때문에 ASG와 CA가 연결되어 노드를 하나 늘려주는 역할을 합니다.
이 상황에서 만약 노드의 자원이 안정적으로 돌아오는 경우 CA가 새롭게 만들었던 노드를 없애고 그 노드 안에 들어있던 파드를 로컬 노드에 다시 재배치하면서 마무리됩니다.
HPA, VPA, CA가 어떻게 파드를 늘릴까? : 아키텍처의 관점에서
쿠버네티스 아킽넥처에서 크게 쿠버네티스를 관장하는 마스터노드와 실제 애플리케이션이 들어있는 워커노드로 분리할 수 있습니다.
마스터노드에는 파드의 형태로 쿠버니테스를 관리할 수 있는 여러개의 오브젝트들이 만들어져있습니다. 그 중 Controller Manager라는 파드는 Deployment, RepliicaSet, HPA, VPA같은 오브젝트들을 관리합니다.
그리고 이 오브젝트들이 쿠버네티스 클러스터의 워커노드에 명령을 내릴 때는 kube-apiserver를 거쳐 이 API Server가 다른 워커노드에 쿠버네티스 명령어를 실행시킵니다.
워커노드에 쿠버네티스를 설치하면 kubelet이라는 것이 설치가 되는데 쿠버네티스는 파드단위로 관리하지만 실제로 노드에 배포되어있는 것은 컨테이너이기 때문에 이 컨테이너들을 런타임시 관리해주는 도커같은 플러그인을 사용합니다.
그래서 우리가 replicas같은 값을 변경해주면
ReplicaSet -> kube-apiserver -> kubelet -> docker -> container 생성
이런 흐름도를 가지게 됩니다.
이 상황에서 이 kubelet들의 메트릭들을 모니터링하는 파드를 생성하면 이 메트릭 정보를 받아서 특정 상황이 되면 Control Plane (마스터 노드) 에 있는 kube-apiserver에 자원을 요청하는 API를 날리게 되고 이 API가 다시 HPA를 호출하면 HPA가 ReplicaSet에 있는 replicas를 늘려주는 작업을 진행합니다.
이 kubelet들의 메트릭을 모니터링하는 파드는 메모리와 CPU뿐만 아니라 프로메테우스같은 플러그인을 설치하면 좀 더 세세한 설정을 통해 (파드로 들어가는 패킷의 양이라던가, Ingress Controller로 들어가는 트래픽의 양이라던가) kube-apiserver에 자원을 요청할 수 있습니다.
cf) HPA의 AutoScale 공식
HPA가 replicas를 늘리는 공식이 있는데 HPA를 설정할 때에는 target속성에 실제 관리해야하는 Controller를 적고 (ReplicaSet) maxReplicas와 minReplicas를 적습니다.
그리고 Metrics속성으로 type을 Resource로 넣어준 뒤 averageUtilization 속성으로 자신이 원하는 자원의 양을 퍼센티지로 넣어주면 됩니다.
• averageUtilization이 50이고 (파드들의 자원이 50퍼센트를 유지했으면 좋겠다는 설정)
• minReplicas가 2 (최소로 유지되는 파드의 개수가 2이면 좋겠다는 설정)
• maxReplicas가 10 (최대 유지되는 파드의 개수가 10이면 좋겠다는 설정)
• CPU request자원이 200m, limits가 500m
• 현재 파드의 평균 자원이 300m
이런 상황이라면 목표 자원은 {파드의 requests} X {averageUtilization} = 100이 됩니다. 이 상황에서 파드의 limits가 500이기 때문에 현재 파드의 평균 자원이 300이어도 파드가 다운되지는 않습니다.
하지만 이 상황이라면 Scale Out을 해야하는 상황이고 이때는 다음과 같은 공식을 따릅니다.
{현재 레플리카 수} X {현재 파드의 평균 자원} / {목표 자원} 이런 계산 공식을 가지고 따라서
2 X 300 / 100 = 6이 되므로 늘어나는 레플리카는 5개입니다.
반대로 Scale In의 경우도 같은 공식을 이용해서 레플리카를 줄여줍니다.
마치며
컨트롤러는 쿠버네티스의 꽃인만큼 공부해야할게 정말 많았습니다. 하지만 공부하고나니 쿠버네티스의 큰 축을 하나 공부했다는 뿌듯함이 남다르네요.
파드와 서비스도 굉장히 강력하다고 생각했는데 컨트롤러는 정말 미쳤다는 말밖에 할 게 없네요.
정말 긴 글이었네요. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 이론 : 아키텍처 (Component, Network, Logging / Monitoring)
이번 포스팅에선 쿠버네티스 아키텍처에 대해서 Component와 Network, Logging / Monitoring의 관점에서 공부한 내용을 정리해보도록 하겠습니다.
쿠버네티스의 아키텍처를 공부해보니까 각각의 기능들이 어떻게 돌아가는지 조금 더 잘 보이고 앞으로 공부할 때도 더 좋을 것 같네요.
2024-09-08 초안
우선 아키텍처에 대한 글을 쓰고 그림은 나중에 추가할 예정입니다.
쿠버네티스 컴포넌트 아키텍처
쿠버네티스의 컴포넌트들은 kube-apiserver, etcd, kube-scheduler, controller manager, kubelet, kube-proxy등이 있습니다. 사실 컴포넌트에 더 많은 오브젝트들이 있는데 일단 이것만 짚고 넘어가겠습니다.
• kube-apiserver : kube-apiserver는 유저가 kubectl로 날리는 API와 ReplicaSet같은 오브젝트들이 replicas를 늘려 파드를 생성할 때에도 kube-apiserver에 요청합니다. 모든 명령의 가운데서 명령을 받고 이행하는 존재입니다.
• etcd : etcd는 kube-apiserver로 들어온 API가 어떤 내용을 포함하고 있는지 쿠버네티스 내부적으로 저장해놓는 데이터베이스입니다. 예를 들어서 CPU와 Memory를 어느정도로 요청하는 파드인지 저장해 놓고 있다가 kubelet에게 전달해주면서 kubelet이 파드를 만들 때 어떻게 파드를 만들어야하는지 참고하는 용도로 쓰입니다.
• kube-scheduler : 노드의 자원 상태를 모니터링하고 있는 kube-scheduler는 자신이 수집한 메트릭을 보고 쿠버네티스에 어떤 명령을 내려야하는지 관장합니다.
• controller manager : controller manager는 컨트롤러와 관련된 다양한 오브젝트들을 관리하고 있습니다. ReplicaSet이나 StatefulSet, DaemonSet, Deployment, HPA, VPA, CA등등을 관리하죠. controller manager는 kube-scheduler가 수집한 메트릭을 보고 replicas를 늘릴지 말지 결정하고 늘려야한다면 파드를 늘리기위해 kube-apiserver로 요청하는 역할을 수행합니다.
• kubelet : kubelet은 워커노드에 포함되어있는 스펙이고 kube-apiserver에 들어온 파드를 늘려달라는 명령을 받아서 etcd에 있는 정보를 바탕으로 파드를 생성해주는 역할을 합니다. 이 과정에서 파드는 쿠버네티스에 있는 개념이지 실제로는 컨테이너로 동작하기 때문에 도커와 같은 런타임에 컨테이너를 관리해주는 플러그인을 이용해서 kubelet이 받은 명령을 도커로 전달해서 컨테이너를 생성합니다.
• kube-proxy : kube-proxy는 도커가 만든 컨테이너를 쿠버네티스 네트워크와 연결하는 역할을 수행합니다. 이렇게 만들어진 컨테이너들은 파드의 형태로 쿠버네티스의 관리를 받게되고 쿠버네티스의 네트워크인 Service나 파드간 연결등에 사용할 수 있는 네트워크를 구성해줍니다.
쿠버네티스의 컴포넌트들에 대해서 대충 알게 되었으니 이제 흐름에 대해서 언급해볼까합니다.
CASE1. 유저가 kubectl 명령어로 파드를 만드는 경우
• 파드를 만들라는 API를 kube-apiserver로 보낸다.
• etcd가 kube-apiserver를 모니터링하고 있다가 파드를 만들겠다는 명령을 보고 파드를 어떻게 만들 것인지 확인하고 그에 걸맞는 파드 정보를 저장한다.
• etcd가 kube-apiserver에 파드를 만들 준비가 되었다고 알려주면 (편의상 준비됐다고 알려준다는 표현을 사용했습니다.) 그 요청을 kubelet이 모니터링하고 있다가 etcd의 설정을 기반으로 파드를 만들고 내부의 컨테이너를 만들기위해 도커에게 명령을 내린다.
• 도커는 kubelet이 내린 명령을 받고 컨테이너를 만든다.
• 만들어진 컨테이너는 kube-proxy에 의해 쿠버네티스 클러스터 네트워크의 일부분이 된다.
CASE2. 파드의 자원이 부족해서 replicas를 늘려야하는 상황
• 파드의 자원이 부족해진다.
• kube-scheduler가 각 노드의 상황을 모니터링하고 있다가 파드의 자원이 부족한 것을 보고 replicas를 늘려달라고 kube-apiserver에게 요청한다.
• replicas에 대한 요청을 모니터링하고 있던 ReplicaSet이 파드를 만들어달라고 kube-apiserver에게 요청한다.
• 그 요청을 보고 있는 etcd는 어떤 파드를 생성해야할지에 대한 정보를 저장한다.
• kube-apiserver가 kubelet에게 파드를 생성하라는 명령을 내린다.
• kubelet이 파드를 생성하고 필요한 컨테이너를 만들기위해 도커에게 명령을 내린다.
• 도커가 컨테이너를 생성한다.
• 생성된 컨테이너를 쿠버네티스 네트워크에 편입시키기 위해 kube-proxy가 명령을 수행한다.
쿠버네티스 네트워크 아키텍처
파드가 하나 생성되고 kube-proxy에 의해 네트워크에 편입되게 되면 이 다음 네트워크는 어떻게 구축되고 서로 연결이 어떻게 되어있을까요?
이번 섹션에선 쿠버네티스의 네트워크 아키텍처를 공부해보고 정리해보려합니다.
※ 주의 CS지식인 네트워크 지식을 다루지 않습니다!
Pod Network
파드가 생성되면 Pause Container가 컨테이너 형태로 생성이 되고 이 컨테이너는 자신만의 NIC을 가지고 있고, 자신만의 IP를 가지고 있습니다. 그리고 이 컨테이너는 파드 내부의 네임스페이스에 들어있는 다른 컨테이너들과 연결되어있습니다.
이때 워커노드의 호스트 네트워크에 가상 네트워크를 만들고 이 가상 네트워크가 Pause Container와 연결되어 있어 파드 외부에서 해당 파드에 연결될 수 있습니다.
Pause Container는 본인이 포함된 파드 내부의 컨테이너를 연결시킬 때 포트로 연결시켜준다는 특징이 있습니다.
즉, Pause Container의 IP가 20.111.156.66이고 파드 내부 컨테이너들이 8080, 8081이라면 20.111.156.66:8080 이렇게 연결될 수 있는 것입니다.
위에서 언급한 내용은 같은 노드에서 파드끼리 통신하는 내용이고 만약 노드가 다르다면 어떻게 될까요?
kubenet이라는 쿠버네티스에서 기본적으로 제공해주는 네트워크 플러그인은 워커노드에 Pause Container로 NIC이 만들어지면 호스트 노드의 가상 NIC을 만들어서 모든 파드끼리 묶습니다.
이 가상 NIC들은 Bridge 네트워크로 묶이게 되고 이렇게 묶은 Bridge네트워크는 묶었던 파드보다 더 낮은 대역의 CIDR를 가지고 있습니다.
이 Bridge네트워크는 파드 네트워크의 대역을 참고해서 파드에 IP를 부여해주는데 이때 한 노드에 255개 밖에 만들어지지 않는다는 특징이 있습니다. 이것이 kubenet의 가장 큰 단점이라고 볼 수 있죠.
그리고 kubenet은 Bridge네트워크 위에 라우터 레이어를 만드는데 이 라우터 층에서는 NAT이 파드로 들어오는 네트워크면 Bridge로 내려주고 그 외의 네트워크는 위로 올려줍니다.
Pod Network : Calico
Calico는 네트워크 구현체로서 기본적으로 제공해주는 kubenet보다 더 많은 기능을 가지고 있어서 쿠버네티스에서 많이 사용되는 네트워크 구현체입니다.
Calico를 사용하게 되면 기본 아키텍처에서 조금 달라지게 되는데 파드 네트워크에서 Pause Container가 생성되고 가상 NIC이 호스트 네트워크로 연결되는 것은 동일합니다.
그 이후에 이 가상 NIC을 라우터로 묶어주는데 이 때 라우터의 IP는 파드의 네트워크보다 더 큰 대역폭을 부여받습니다.
만약 서로 다른 노드에 존재하고 있는 파드A와 파드B가 통신하려고 할 때 파드B에서 파드A로 IP요청을 보내면 이 트래픽이 가상 NIC을 통해 Calico의 네틍뤄크로 들어갑니다.
그리고 이 라우터에서 자신이 담당하고 있는 파드들의 IP를 검사해서 있는지 없는지 확인하고 없으면 라우터 위의 계층인 Overlay계층으로 넘겨줍니다.
이때 Overlay계층은 IP Encapsulation이 일어나면서 파드의 IP를 목적지 워커노드의 IP로 바꿔줍니다. Calico는 파드의 IP대역이 어느 노드에 들어있는지 알고 있기 때문에 이런 기능이 가능한 것입니다.
이렇게 다른 노드로 이동된 트래픽이 Overlay계층에서 다시 Decapsulation이 일어나고 이 트래픽이 원하는 목적지 파드까지 연결시켜줍니다.
Service Network
만약 파드와 서비스를 연결한다면 이 둘은 Endpoint라는 오브젝트에 의해 연결이 되어있습니다.
이때 마스터 노드의 kube-apiserver는 Endpoint를 감시하고 있다가 Endpoint가 생성되면 이 정보를 모든 노드에 DaemonSet으로 떠있는 kube-proxy에게 이 정보를 넘겨줍니다. 이 정보에는 서비스의 IP와 파드의 IP가 적혀있습니다.
그리고 워커노드의 iptables에 해당 서비스 대역으로 들어노는 모든 트래픽을 kube-proxy로 들어가라고 지정되어있고 만약 다른 파드에서 이 서비스 대역의 IP로 요청을 보내게 되면 iptables가 kube-proxy로 이 트래픽을 보내주고 kube-proxy는 서비스 IP를 파드의 IP로 바꿔주면서 파드 네트워크 영역으로 보내줍니다.
하지만 이 방식의 단점은 모든 트래픽이 kube-proxy를 지나게 되어있고 kube-proxy는 모든 트래픽을 감당하기엔 성능상 좋지않아서 기본적으로 쿠버네티스는 kube-proxy를 타지 않고 곧바로 iptables에 서비스 IP와 파드 IP를 적어서 보내줍니다.
리눅스에서는 IPVS라는 L4로드밸런서가 제공하는데 쿠버네티스는 설정에서 기본설정인 iptables 대신 IPVS로 설정하게 되면 iptables와 똑같은 역할을 수행해주지만 트래픽이 많아지면 많아질수록 IPVS가 iptables에 비해 더 좋은 성능을 보여준다는 차이점이 있습니다.
Service Network : Calico
만약 이 상태에서 Calico를 사용하게 된다면 서비스를 파드와 연결했을 때 ClusterIP로 연결하게 되면 다른 노드에 존재하는 파드에서 해당 서비스를 요청하게 되었을 때 이 요청이 라우터 계층에 있는 NAT 게이트웨이를 지나게 되고 이 NAT에서 이 요청의 서비스IP를 파드의 IP로 바꿔서 트래픽을 전송합니다.
이 다음의 흐름은 위에서 언급했던 서로 다른 노드간 파드 네트워크와 동일합니다.
하지만 여기서 NodePort로 서비스를 파드와 연결하게 된다면 조금 다른 흐름이 됩니다.
만약 NodePort로 서비스와 파드를 연결하면 모든 노드에 떠있는 kube-proxy가 자신의 노드에 3만번대의 포트를 부여해주고 외부에서 트래픽 요청이 들어오면 iptables가 NAT게이트웨이로 트래픽을 보내고 NAT이 패킷을 까서 어느 목적지로 가야하는지 확인한 다음 쿠버네티스 클러스터의 네트워크를 탱눠서 목적지로 트래픽을 전송합니다.
쿠버네티스 Logging / Monitoring 아키텍처
쿠버네티스는 로그와 모니터링을 위한 아키텍처를 가지고 있습니다. 쿠버네티스에서 제공하는 플러그인으로 로깅 / 모니터링을 구현할 수도 있고 외부 플러그인을 이용해서 더 세세하게 모니터링을 구축할 수도 있습니다.
이 두가지 상황을 모두 알아보도록 하겠습니다.
Core Pipeline
Core Pipeline은 일반적으로 쿠버네티스에서 기본적으로 제공해주는 모니터링 시스템입니다.
먼저 시작은 kube-apiserver에서 파드를 만들라는 명령을 받은 kubelet에서 시작됩니다.
kubelet이 명령을 받고 파드를 만들면서 컨테이너를 만들기위해 컨테이너 런타임 플러그인인 도커에게 명령을 내립니다.
이 컨테이너들은 워커 노드에 기본적으로 존재하는 자원을 사용하고 워커 노드의 디스크에 로그를 기록합니다.
도커는 자원을 사용해서 컨테이너를 돌리고 디스크에 로그를 기록하면서 Resource Estimator인 cAdvisor에게 메트릭 정보를 보냅니다. 그럼 이 cAdvisor가 컨테이너 정보들을 모아서 kubelet정보로 정리합니다. (편의상 정리한다고 표현했습니다.)
이 kubelet에 대한 정보를 마스터 노드에 존재하는 metrics-server로 보내주고 kube-apiserver가 이 metrics-server에 접근해서 kubectl log / kubectl top 명령어로 노드와 파드의 메트릭을 볼 수 있게 됩니다.
Service Pipeline
쿠버네티스가 기본적으로 제공해주는 메트릭 정보 말고 플러그인을 이용해서 더 자세한 내용을 메트릭으로 받을 수 있습니다.
이 플러그인들은 로그를 수집하는 Agent와 로그를 조회하고 분석하는 Metric Aggregator / Analytics가 있습니다. 그리고 이를 UI로 볼 수 있는 Web UI가 존재합니다.
Agent는 도커와 노드의 자원으로부터 로그를 수집해서 Metric Aggregator로 보내고 이를 UI로 보는 것이 일반적인 흐름입니다.
플러그인을 이용한 Service Pipeline은 Elastic 재단에서 나온 ELK스택과, 쿠버네티스에 맞춤형으로 갭라된 Agent인 Fluent-bit을 이용한 EFK스택, 프로메테우스를 이용한 메트릭 모니터링, 그리고 쿠버네티스에서 가장 많이 사용하는 Promtail - Loki - Grafana로 이어지는 PLG스택이 있습니다.
PLG스택은 각 노드에 Promtail이 파드의 형태로 배포되어 있고 이 Promtail이 워커노드에 존재하는 파드에 대한 로그를 참조해서 이 로그들을 Loki로 푸시합니다.
이 때 Loki는 파드의 형태로 존재하고 Loki와 연결된 서비스가 ClusterIP로 되어있습니다. 그리고 이 Service로 Grafana가 API콜을 하면서 메트릭 정보를 UI로 뿌려주는데 이 Grafana도 NodePort로 만들어진 Service와 연결되어 있어서 쿠버네티스 클러스터 내부망으로 이 Grafana에 접근할 수 있습니다.
마치며
이번 포스팅에선 쿠버네티스의 아키텍처를 컴포넌트, 네트워크, 로깅 / 모니터링로 나누어 알아봤습니다.
개인적으로 컴포넌트, 네트워크 아키텍처를 공부하면서 굉장히 뿌듯한 느낌이 들었습니다. 조금 더 쿠버네티스를 잘 알게 된 것 같아서 너무 좋았습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 실습 : 환경 구축 (feat. EC2)
지금 kubectl 명령어를 이용해서 실습을 하고있는데 minikube로 쿠버네티스를 간단하게 설치하고 몇번 만져봤습니다.
현재 진행정도는 MySQL을 배포하고 DBeaver로 붙어보는것, Spring Boot를 배포하고 API로 접근해보는 것 까지입니다.
왜 진행상황을 언급했냐면 이 과정이 순탄하지 않았기 때문인데 무엇보다 minikube라는 것이 쿠버네티스 클러스터 환경을 한 컴퓨터에 로컬에 컨테이너 형태로 구축해주는 것입니다.
즉, 쿠버네티스는 마스터 노드와 워커 노드로 구분할 수 있는데 이 노드들은 사실 원래 각각 물리적으로 분리된 컴퓨터여야합니다. AWS로 치면 각각 다른 EC2인스턴스여야 한다는 것입니다.
하지만 minikube는 이 노드들 (컴퓨터)을 한 컴퓨터에 컨테이너 형식으로 추상화해놓아서 쿠버네티스를 연습하기엔 정말 좋지만 실전에서 사용한다거나 하는 것은 어느정도 제한이 됩니다.
예를 들어서 Spring Boot를 배포하고 외부에서 붙게하려면 External IP를 받을 수 있게 Ingress Controller (Nginx)를 만들어줘야 하는데 Ingress를 만들고 연결했는데 External IP가 안보여서 왜인가 하고 찾아보니 minikube는 로컬환경이라 External IP가 안만들어진다는 것이었습니다.
쓰읍...
물론 공부가 목적이라면 그냥 그렇구나 하고 넘어가면 되지만 실제로 동작하는걸 두 눈으로 봐야 직성이 풀리는 저로서는 굉장히 불만족스러웠습니다.
해결책은?
우선 첫번째로 Virtual Box같은 프로그램으로 컴퓨터에 리눅스 환경을 설치해서 그곳에 쿠버네티스 클러스터를 구축하는 것입니다.
두번째로 EC2를 만들어서 인스턴스로 쿠버네티스 클러스터를 구축하는 것입니다.
첫번째는 제가 맥북으로 공부하고 있어서 실패했습니다. 맥은 Virtual Box를 지원하긴 하지만 테스트 버전만 지원해서 굉장히 안좋다고 맥은 Virtual Box를 사용하지 않는 것을 권장하더군요.
그래서 VMWare같은 가상화 서비스를 찾아봤는데 이건 여러개를 어떻게 띄우는지 몰라서 이것도 패스했습니다.
결국 AWS EC2 인스턴스에 쿠버네티스를 직접 설치하자는 결론에 이르렀고 수시간의 삽질끝에 클러스터를 만드는 것 까지는 성공했습니다. 이제 이 포스팅 이후부터 제가 원했던 Ingress Controller를 이용해서 스프링 부트 애플리케이션을 외부에서 붙어보도록 하겠습니다.
쿠버네티스 환경 구축
일단 쿠버네티스 환경 구축에 앞서...
왜 선배 개발자분들이 쿠버네티스는 환경 구축부터 진입장벽이라고 말했는지 알 것 같았습니다.
뭔놈에 설치하는게 이렇게 그지같은지 참...
일단 시작 전에 EC2 인스턴스 세대를 준비해야합니다. 한대는 마스터 노드로, 두대는 워커 노드로 활용될 예정입니다. 참고로 EC2인스턴스는 t2가 디폴트지만 t3a가 일반적인 t2보다 저렴하니 그것을 선택하는 것이 좋습니다.
또한 t3a.medium (2Core, 4GB) 정도는 되어야 도커가 렉 없이 돌아가기 때문에 이정도 인스턴스를 준비하시는 것을 추천드립니다.
본격적으로 환경을 구축해보겠습니다.
쿠버네티스 환경 구축 전 사전 지식
쿠버네티스를 위해서는 인스턴스에 아래의 프로그램이 설치되어야합니다.
• 컨테이너 런타임 (containerd, docker)
• 쿠버네티스 (kubelet, kubeadm, kubectl)
• Calico
본격적으로 시작해보죠.
저는 우분투에서 시작했습니다.
도커설치
1. 우분투 패키지 업데이트
sudo apt-get update
2. 패키지 설치
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
3. 도커의 공식 GPG키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
4. 도커의 공식 apt 저장소를 추가
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
5. 시스템 패키지 업데이트
sudo apt-get update
6. 도커설치
sudo apt-get install docker-ce docker-ce-cli containerd.io
7. 도커 설치 확인
sudo systemctl status docker
kubelet, kubeadm, kubectl 설치 (모든 노드에 설치해야합니다.)
1. 메모리 스왑 끄기
sudo swapoff -a
2. etcd 설치
sudo apt-get update && sudo apt-get install -y apt-transport-https curl etcd
3. 패키지 다운로드
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
4. GPG키 추가
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://pkgs.k8s.io/core:/stable/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
5. 패키지 업데이트
sudo apt-get update
6. kubelet, kubeadm, kubectl 설치
sudo apt-get install -y kubelet kubeadm kubectl
7. apt-mark 홀드
sudo apt-mark hold kubelet kubeadm kubectl
8. 설치 잘 됐는지 확인
kubectl version --client && kubeadm version
9. containerd 설치
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
10. containerd 설정 변경
sudo vi /etc/containerd/config.toml
이 파일을 열고 들어가면 이 부분을 찾아서 바꿔주시면 됩니다.
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = false
false를 true로
11. socat 설치
sudo apt-get install -y socat
sudo systemctl restart socat
sudo systemctl enable socat
12. 메모리 스왑 및 추가 설정
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
sudo modprobe br_netfilter
echo 'br_netfilter' | sudo tee /etc/modules-load.d/k8s.conf
sudo tee /etc/sysctl.d/k8s.conf<<EOF net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 EOF
마스터 노드 초기화 (마스터 노드에만 해주세요!)
1. 패키지 업데이트
sudo apt-get update
2. etcd 종료
sudo systemctl stop etcd
sudo systemctl disable etcd
이 작업을 해야하는 이유가 etcd는 깔려있어야하지만 마스터 노드를 초기화할 때 etcd를 자동으로 세팅해주기 때문입니다.
3. 포트확인
sudo lsof -i :2379
sudo lsof -i :2380
이 두개 포트가 없으면 정상적으로 완료된 것입니다. 만약 있다면 kill -9 {PID} 이 명령어를 이용해서 프로세스를 죽여주세요.
4. 노드 초기화
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
5. 방화벽 제거
sudo ufw disable
6. 설정 파일 세팅
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
7. calico 설치
kubectl apply -f [calico](https://docs.projectcalico.org/manifests/calico.yaml)
8. 설정 추가
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
sudo modprobe br_netfilter
sudo sysctl --system
9. 토큰 생성
kubeadm token create --print-join-command
이렇게 명령어를 치면 아래와 같은 결과물이 출력됩니다.
kubeadm join {마스터노드의 private IP}:6443 --token {TOKEN} --discovery-token-ca-cert-hash sha256:{HASH VALUE}
이제 이 명령어를 그대로 복사해서 워커 노드에다 실행시키면?
끝!
이제 마스터 노드에서 kubectl get nodes 로 확인해보시면 됩니다.
이때 모든 보안그룹의 인바운드 규칙에 6443 포트를 열어줘야합니다.
마치며
진짜 쿠버네티스는 미친놈이 아닌가 싶을정도로 세팅하는게 정말 힘들었습니다. 하지만 지금 안되어있는게 kube-proxy가 설치가 안되어있는데 calico가 설치되어있어서 괜찮으려나? 싶기도 합니다.
다음 포스팅에선 MySQL 배포해보고 DBeaver 연결하고 Spring Boot까지 배포해보겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 실습 : 환경 구축 (워커 노드에 권한 부여)
이번 포스팅은 쿠버네티스 환경을 구축하는 과정에서 생긴 실습 과정을 정리해보고자 작성하게 되었습니다.
쿠버네티스는 워커 노드를 설정하면 kubectl 명령어를 곧바로 사용할 수 없습니다. 이를 사용하기 위해서는 쿠버네티스의 인증, 인가 과정을 거쳐야 합니다.
이에대한 자세한 내용은 아래의 링크에 정리되어있습니다!
https://coding-review.tistory.com/549
워커 노드에서 kubectl 명령어를 사용하기 위해서 저는 kubeconfig와 Service Account (이하 SA) 를 조합해서 워커 노드에 권한을 부여했습니다.
워커 노드에 권한 부여

제가 실습했던 내용을 도식화해봤습니다.
흐름은 이렇게 진행됩니다.
• SA를 만든다.
• ClusterRole을 만든다. 이때 API Server에 어떤 명령만 허용할 것인지를 정의한다.
• ClusterRoleBinding으로 SA와 ClusterRole을 이어준다.
• SA를 이용해서 토큰을 만든다. 이는 kubeconfig의 user에 넣어준다.
• admin.conf에서 Certificate Authority Data를 뽑아서 이는 kubeconfig의 certificate-authority-data에 넣어준다.
• 워커 노드에서 worker-kubeconfig라는 이름으로 (제가 임의로 지은 이름입니다.) 파일을 만들고 이를 워커 노드의 KUBECONFIG 변수로 정해준다.
• Optional (옵션) : 다른 워커노드에 이 worker-kubeconfig 파일을 옮겨서 적용시킨다.
한번 본격적으로 시작해보도록 하겠습니다! 순서대로 진행할 것이고 순서 옆에 어떤 노드에서 진행되고 있는 것인지 적겠습니다!
1. SA를 만든다. (마스터노드)
kubectl create serviceaccount worker-sa
2. ClusterRole을 만든다. (마스터노드)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: worker-node-access
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
임의로 파일 이름을 지어주고 오브젝트를 만들어보겠습니다. 저는 worker-cr 이라고 지었습니다.
kubectl apply -f ./worker-cr
이렇게 오브젝트를 만들어줍ㄴ디ㅏ.
3. ClusterRoleBinding을 만든다. (마스터노드)
kubectl create clusterrolebinding {ClusterRoleBinding 이름} --clusterrole={ClusterRole 이름} --serviceaccount=default:{Service Account 이름}
4. SA에서 토큰을 뽑아낸다. (마스터노드)
kubectl create token worker-sa
이 토큰 값을 잘 적어주시길 바랍니다.
5. admin.conf에서 CA-data를 뽑는다. (마스터노드)
sudo cat /etc/kubernetes/admin.conf | grep certificate-authority-data
이 값 또한 잘 적어주시길 바랍니다.
6. 워커노드에서 Config 오브젝트를 만든다.
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://{control-plane의 내부아이피}:6443
certificate-authority-data: {admin.conf에서 뽑은 CA-data값}
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: worker-sa
name: worker-sa-context
current-context: worker-sa-context
users:
- name: worker-sa
user:
token: {SA에서 뽑은 토큰 값}
이렇게 만든 다음 주의해야할 점은 contexts > context > user 에 있는 worker-sa라는 값과 users > name 에 있는 worker-sa라는 값이 동일해야합니다.
7. 워커노드에서 이 값을 바탕으로 KUBECONFIG 환경변수로 설정해준다.
저는 위의 Config파일 이름을 worker-kubeconfig라고 지어줬습니다.
export KUBECONFIG=./worker-kubeconfig
이제 kubectl get nodes를 입력해보면?

pods는 조회할 수 없는 것을 확인할 수 있습니다.
그런데 이 방식은 문제가 있습니다. 사실 함정이 있습니다.
Short Term Token VS Long Term Token
위에서 언급한 방식은 Short Term Token 즉, 토큰이 금방 만료된다는 특징이 있습니다.
보통 이런 짧은 만료시간을 가진 토큰을 사용하는 이유는 보안의 위험때문인데요. JWT에서 Access Token과 Refresh Token이 따로 존재하는 것이고 우리는 지금 Access Token을 발급받아서 사용한 것입니다.
그럼 Refresh Token처럼 오랫동안 사용할 수 있는 토큰을 발급받아서 오래 사용할 수 있는 방법도 있을까요?
당연히 있습니다.
바로 Secret 오브젝트를 만들어서 토큰을 발급하는 방법입니다.
맨처음 우리가 토큰을 만들 때 사용한 방식은 SA를 토큰으로 만들었습니다. 이렇게 SA로 토큰을 만들면 짧은 만료기간을 가진 토큰이 발급됩니다.
하지만 Secret으로 발급하면 우리가 이 Secret을 삭제하지 않는한 계속 이 토큰을 사용할 수 있습니다.

이 그림에서 SA가 Secret으로 바뀌기만 한다고 생각해주시면 될 것 같습니다.
그럼 Secret을 만들어야겠죠? 한번 만들어보겠습니다. 이 작업은 모두 마스터 노드에서 진행해야합니다.
apiVersion: v1
kind: Secret
metadata:
name: worker-node-access-secret
annotations:
kubernetes.io/service-account.name: worker-sa
type: kubernetes.io/service-account-token
여기서 주의하셔야할 점은 우리가 만든 SA를 사용하지 않는 것은 아니라는 것이고 우리가 만든 worker-sa를 그대로 사용해야한다는 것입니다.
이름이 틀리면 적용되지 않으니 주의해주시면 감사하겠습니다.
저는 이 파일이름을 worker-node-access-secret.yml로 지었습니다.
kubectl apply -f ./worker-node-access-secret.yml
이렇게 배포하고 토큰을 만들어줍니다.
kubectl get secret {Secret 이름} -o jsonpath="{.data.token}" | base64 --decode
이제 나온 토큰을 다시 워커 노드로 가져가서 user > token에 넣어주시면 됩니다. 위에서 언급했으니 내용을 다루진 않겠습니다.
그리고 다시 kubect get nodes를 해보면?

이렇게 잘 나오는 것을 확인할 수 있습니다.
마치며
이렇게 쿠버네티스 워커 노드에 권한을 부여해봤습니다. 사실 모든 권한을 다 주는 것이 편하지만 실제 환경을 구축해봄으로서 조금 리얼한 실습을 할 수 있었습니다.
저번 포스팅에선 본격적으로 Spring Boot를 띄워보겠다고 했었는데 kubectl 명령어가 워커 노드에서 동작하지 않아서 이 부분을 해결하느라 시간을 조금 썼습니다.
이제 진짜진짜 본격적으로 pod나 apply에 대한 권한도 워커 노드에 부여하고 본격적으로 스프링 부트를 외부에서 붙을 수 있도록 도전해보겠습니다.
오늘도 긴 글 읽어주셔서 감사합니다. 즐거운 하루 되세요!
AWS EKS Cluster 구축
저는 정말 어리석은 선택을 하고 2주라는 시간을 날려버렸습니다. 바로 쿠버네티스를 온프레미스로 구현하는 것이었는데요. 초심자에겐 정말 지옥같은 경험이었습니다.
근데 그 클러스터를 EC2인스턴스에서 구축하려고 했다는 것이 정말 어리석었죠. EC2 인스턴스는 비용절감을 위해 껐다 켰다 하고 있었는데 어느날 EC2를 켜보니 Calico가 안되더군요. 그걸로 하루를 날려먹고 쿠버네티스 클러스터를 재구축하면서 마무리지었습니다.
그리고 쿠버네티스 서비스의 LoadBalancer 타입을 시험해보고싶어서 Metallb를 구축하려다가 이것도 안되서 포기했습니다...
결국 EC2인스턴스에서 온프레미스처럼 쿠버네티스를 구축하려 했던 저의 미련한 실수였습니다.
온프레미스로 쿠버네티스를 구축하면 뭔가 한단계 성장할 것 같았거든요. 마치 저에게 있어서 바닐라JS로 리액트를 만드는 것과 같은 난이도였습니다.
모든 것을 포기하고 EKS로 노선을 틀었습니다. 이번 포스팅에선 EKS 클러스터 구축과 간단하게 EKS의 특징을 정리해보도록 하겠습니다.
EKS란?
EKS는 쉽게 얘기해서 AWS에서 쿠버네티스를 조금 더 쉽게 사용할 수 있게끔 도와주는 완전 관리형 클라우드 서비스입니다.
보통 쿠버네티스 클러스터를 구축하면 kube-system 네임스페이스를 가진 파드들 (kube-apiserver, kube-proxy 등등) 을 관리하는 Control Plane이 있기 마련입니다.
하지만 EKS는 EKS Cluster 그 자체가 Control Plane으로서 API 서버도 존재하고 클러스터를 전반적으로 관리해줍니다.
이후 EKS에 대해서 자세히 간단하게만 정리하자면
• 마스터 노드 (Control Plane) 관리할 필요 없음
• 노드간 네트워크 신경쓰지 않아도 됨
• 파드의 외부 노출을 위한 External IP 관리 할 필요 없음
• 노드에 자원 부족하면 ASG를 이용해서 자동으로 노드 늘어남
• IAM으로 EKS 클러스터에 접근 권한 세세하게 관리 가능
• CloudWatch로 메트릭 서버 관리할 필요 없음
• 이외에도 각종 AWS 서비스들과 통합 가능 (SNS, SQS, DynamoDB, S3, EBS, EFS)
클라우드 서비스들이 다 그렇지만 클라우드로 모든것을 관리하기 시작하면 정말 믿도끝도없이 사용할 수 있는게 클라우드인 것 같습니다.
그만큼 EKS와 다른 AWS서비스들간의 통합은 정말 무서운 것 같습니다.
EKS 클러스터 구축
우선 EKS 클러스터를 만들기 위한 IAM을 만들어줘야합니다.

신뢰할 수 있는 유형을 AWS 서비스로, 그리고 서비스에 EKS를, 사용 사례에는 EKS -Cluster를 선택합니다.
다음다음 하다보면 결국 AmazonEKSClusterPolicy (https://us-east-1.console.aws.amazon.com/iam/home?region=ap-northeast-2#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonEKSClusterPolicy) 정책을 선택하게 되고 이를 생성하면 됩니다.
이제 EKS를 선택해서 클러스터 생성을 누르고 진행하면 됩니다.

이름에 원하는 이름을, 클러스터 서비스 역할에 우리가 방금 만든 IAM Role을 넣고 쿠버네티스 버전을 설정합니다.
제가 언급하지 않는 것들은 디폴트 값으로 설정해도 전혀 상관 없습니다!

네트워크에 VPC를 선택하고 그 VPC안에 있는 서브넷을 선택해주면 됩니다. EKS를 위해서라면 최소 2개의 서브넷이 있어야합니다. 그리고 쿠버네티스 클러스터에 접근을 위한 보안 그룹을 설정해줘야합니다.
저는 22번, 80번, 443번 포트를 열어줬습니다.

지표에 대한 설정은 추후에 CloudWatch와 연동할 수 있기 때문에 일단은 선택하지 않았고 Control Plane 로깅은 모두 선택해줍니다.

추가 기능을 선택할 때는 디폴트로 위의 사진처럼 네개가 선택되어있습니다. EKS Pod Identity는 파드에 접근하는 권한을 IAM으로 관리할 수 있도록 해주는 것인데 저는 일단 로컬PC인 맥북에서 클러스터에 접근할 거고 그 클러스터에 대한 접근을 IAM으로 관리할 것이지 파드에 관해서는 IAM이 필요없어서 선택하지 않았습니다.

추가기능에 대한 버전을 선택할 수 있는데 저는 일단 다 디폴트로 설정했습니다.
다음은 검토단계이고 모든 것을 확인한 뒤 생성을 누르고 5분정도 기다리면 생성이 됩니다.

짠! 이렇게 설정이 됐습니다. 이제 맥북에서 붙어보겠습니다.
윈도우에서 AWS CLI를 설치하는 방법은 다루지 않도록 하겠습니다.
터미널을 열고 homebrew를 이용해서 aws-cli를 설치합니다.
brew install aws-cli
설치가 완료되면 버전을 확인하여 정상적으로 설치됐는지 확인합니다.
aws --version
그리고 이제 IAM으로 붙어볼건데요. 이제 이 클러스터에 접근해서 AWS의 서비스들을 이것저것 조작하기 위해서 IAM 사용자를 만들어줄겁니다.
IAM 사용자를 만들어보겠습니다.

먼저 사용자 이름을 입력하고 다음을 누릅니다.

직접 정책 연결을 선택하고 권한 정책에서 검색을 통해 하나씩 체크박스를 선택해주면 되는데요.
• AmazonEKS_CNI_Policy
• AmazonEKSServicePolicy
• AmazonEKSWorkerNodePolicy
• AmazonEKSVPCResourceController
• AWSElasticLoadBalancingFullAccess
• AmazonEC2ContainerRegistryFullAccess, AmazonEC2ContainerRegistryReadOnly (ReadOnly만 해도 되는데 저는 로컬에서 ECR로 직접 이미지를 전송할거라 FullAccess로 해줬습니다.)
이렇게 일곱가지를 추가해줍니다.
그리고 이렇게 만든 IAM 사용자를 csv로 관리해주시면 좋습니다! csv 다운로드를 눌러서 관리하도록 하겠습니다.
그리고 다시 맥북으로 와서 AWS IAM 사용자를 내 PC에서 등록해보겠습니다.
aws configure
여기서 Access Key와 Secret Key를 입력하라고 하는데 csv에 있는 정보를 그대로 적으시면 됩니다.
region name에는 ap-northeast-2를, format은 json으로 설정해줍니다.
거의 다왔습니다. 이제 EKS클러스터에서 IAM 사용자를 등록해보겠습니다.

클러스터 내부에서 "엑세스" 탭에 "엑세스 항목 생성"을 선택합니다.

아까 우리가 만든 IAM 사용자를 선택해줍니다. 나머지는 디폴트로!

저는 우선 제가 어드민이 되어야하기 때문에 모든 권한이 들어있는 정책을 선택했습니다. 이거 말고도 읽기 권한만 부여하는 ViewPolicy라던가 다양한 정책들이 있습니다.
그리고 생성을 누르면 끝!
진짜 마지막! 워커 노드를 생성해보겠습니다.

"컴퓨팅"탭에서 "노드 그룹 추가" 버튼을 클릭합니다. 저는 이미 만들어뒀습니다. 원래 이곳은 비어있는 공간입니다.
노드 = EC2이기 때문에 EC2에게도 IAM 역할을 부여해줘야합니다.

아까 EKS 역할을 만들었던것처럼 IAM > 역할 > 역할 생성을 누르고 EKS와 마찬가지로 신뢰할 수 있는 엔티티를 AWS 서비스로, 서비스, 사용 사례는 EC2를 선택해줍니다.
그리고 AmazonEC2FullAccess를 정책으로 선택하면 역할과 정책이 연결되는 것입니다. 이렇게 IAM 역할을 만들고 나서 다시 노드 그룹으로 넘어가겠습니다.

이름을 입력하고 노드 IAM역할에 방금 만든 역할을 선택하고 모두 디폴트로 설정합니다.
이 화면에서 Kubernetes 레이블이라는 부분이 있는데 이 노드가 만들어질 때 어떤 라벨을 달고 있을지 결정하는 것입니다. 뒤에서 더 나오겠지만 노드그룹에는 몇개의 노드를 유지할 것인지 정할 수 있는데 이 노드마다 전부 라벨을 붙인 상태에서 노드가 추가되는 것입니다.
즉, 우리는 노드의 자원이 부족하면 노드를 늘려서 파드가 배치되게 해야하는데 보통 파드에는 node selector가 존재합니다. 이 파드가 어떤 노드에 배치되어야 하는지 정하는 속성이죠.
하지만 원래같으면 노드가 생성되고 라벨을 수작업으로 붙여줘야합니다. 때문에 즉각적인 대응이 어렵고 유연성이 떨어지죠. 항상 개발자가 컴퓨터 앞에 그것도 노드의 자원을 체크하면서 노드가 생성되는 것을 모니터링할 수 없으니까요.
때문에, 이 라벨을 달아주는 것이 꽤나 도움이 됩니다.

이어서 인스턴스 유형을 설정하고 디스크 크기를 설정합니다. 저는 가격이 저렴한 t3a로 설정했습니다. AMI는 아마존 리눅스로 설정했구요.

아래로 내리면 노드를 몇개씩 관리할 것인지 설정할 수 있습니다. 저는 두개의 노드가 필요하기 때문에 두개로 설정하겠습니다.

뒤이어 네트워크 구성을 선택해야하는데 맨처음 EKS 클러스터를 만들 때 VPC를 선택했기 때문에 어떤 서브넷에만 노드를 생성할 것인지 정하면 됩니다.
저는 디폴트 서브넷에 모두 들어갈 수 있게 설정했습니다.
이제 생성을 누르면 짜잔!

맨처음 사진을 그대로 사용했습니다! 이 화면이 뜨게 되는 것이죠. 그리고 EC2로 가보면?

이렇게 인스턴스가 생성된 것을 볼 수 있습니다.
이제 진짜진짜 마지막으로 우리의 개인 PC에 연결한 IAM사용자를 eks 사용자로 인식해야합니다.
다음과 같은 명령어를 입력합니다.
aws eks update-kubeconfig --name {EKS 클러스터 이름}
이렇게 하면 kubeconfig가 자동으로 설정됩니다.
그리고 kubectl 명령어를 날려보면?

와우 노드가 연결됐습니다.
마치며
이렇게 EKS 클러스터를 만들어봤습니다. IAM이랑 연결하는 것 때문에 꽤 많은 시간을 잡아먹었지만 일단 해결됐습니다. 저는 IAM 사용자에게 어드민 권한을 주었지만 다른 IAM사용자를 만들고 이 사용자는 View권한만 준다던가 하는 방식으로 활용할 수도 있을 것 같네요.
EKS가 편리한 것이 로컬에서 쿠버네티스 클러스터에 접근할 수 있으니 마치 minikube로 실습하듯이 쿠버네티스를 공부해볼 수 있습니다. minikube에선 불가능했던 External IP를 부여하는 것이나 다양한 것들을 직접 실환경처럼 실습해볼 수 있겠네요.
앞으로 다양한 EKS의 포스팅들이 업로드될 것이니 많은 기대바랍니다.
정말 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
AWS EKS로 스프링 프로젝트 배포하기
이번 포스팅에선 EKS를 이용해서 스프링 프로젝트를 배포해보도록 하겠습니다.
온프레미스로 k8s를 구축할 땐 말도 드럽게 안듣더니 EKS로 하니까 역시 돈이 좋다는 것을 체감하고 있습니다. 개발 난이도를 엄청나게 낮춰주네요.
이번 실습에선 담백하게 스프링 + MySQL 이렇게 구성했습니다. 이번 실습은 실제로 파드간 연결이 되는지 확인하는 것이 목적이었기 때문에 복잡성을 높이지 않았습니다. 이번 실습으로 실제로 파드간 연결이 되는 것이 확인되었으니 다음 포스팅에서 조금 고도화해서 다뤄보도록 하겠습니다.
이번 포스팅의 중점은 다음과 같습니다.
• Headless Service를 이용해서 파드간 연결이 가능한지 테스트 하기위한 실습입니다.
• 복잡성을 최대한 줄이기 위해 Deployment (ReplicaSet, StatefulSet등), PV / PVC를 적용하지 않았습니다.
• 파드 메트릭이나 HTTPS와 같은 고도화는 다음 포스팅에서 진행할 예정입니다.
• 노드의 최소한의 자원만 사용했습니다.
그럼 바로 시작해보죠!
EKS로 스프링 프로젝트 배포하기
스텝바이스텝으로 하나씩 파헤쳐보겠습니다.
• 네임스페이스 생성
• ResourceQuota, LimitRange 생성
• MySQL 구동을 위한 Secret생성 후 MySQL 구동
• MySQL 사용자 만들고 데이터베이스 만들기
• 스프링 프로젝트 간단하게 만들기
• 스프링 프로젝트 외부에서 접속해보기
이 순서로 진행됩니다!
1. 네임스페이스 생성
apiVersion: v1
kind: Namespace
metadata:
name: database
apiVersion: v1
kind: Namespace
metadata:
name: was
우선 두개의 네임스페이스를 생성했습니다. 데이터베이스와 WAS를 위한 네임스페이스입니다.
kubectl apply -f ./{데이터베이스 네임스페이스 yaml파일 이름}
kubectl apply -f ./{WAS 네임스페이스 yaml파일 이름}
2. ResourceQuota, LimitRange 생성
네임스페이스를 만들었으니 네임스페이스에 대한 제한조건을 걸어두겠습니다. 이건 사실 필요없는거긴 한데... 어렵지 않은 것이라 그냥 해봤습니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: db-quota
namespace: database
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "1"
limits.memory: 1Gi
MySQL의 ResourceQuota는 요청 CPU는 1코어 요청 메모리는 1기가입니다.
apiVersion: v1
kind: LimitRange
metadata:
name: db-limits
namespace: database
spec:
limits:
- max:
cpu: "1"
memory: 1Gi
min:
cpu: 500m
memory: 500Mi
type: Container
이어서 MySQL의 LimitRange입니다. 별로 특별한 것은 없으니 넘어가겠습니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: was-quota
namespace: was
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "1"
limits.memory: 1Gi
apiVersion: v1
kind: LimitRange
metadata:
name: was-limits
namespace: was
spec:
limits:
- max:
cpu: "1"
memory: 1Gi
min:
cpu: 500m
memory: 500Mi
type: Container
WAS의 ResourceQuota와 LimitRange까지 모두 작성한 다음 생성해주시면 됩니다.
3. MySQL 구동을 위한 Secret 오브젝트 생성과 MySQL 구동
apiVersion: v1
kind: Secret
metadata:
namespace: database
name: mysql-secret
type: Opaque
data:
MYSQL_ROOT_PASSWORD: cm9vdA==
우선 Secret을 만들어줍니다. 여기서 주의해야하는 점이 data에 들어가는 value는 반드시 Base64로 인코딩 해줘야한다는 것입니다.
Secret을 작성하고 생성해주시면 됩니다.
apiVersion: v1
kind: Pod
metadata:
name: mysql-pod1
labels:
type: db
log: dev
namespace: database
spec:
nodeSelector:
node: db
containers:
- name: mysql-container1
image: mysql:8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: MYSQL_ROOT_PASSWORD
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
hostPath:
path: /data/mysql
type: DirectoryOrCreate
이제 볼게 조금 많아졌는데 위에서부터 천천히 살펴보겠습니다.
• labels는 이 파드에 어떤 라벨을 붙일 것인지를 나타냅니다. 후에 이 라벨은 서비스가 파드를 바라볼 때 사용됩니다.
• nodeSelector는 어떤 노드에 배치되었으면 좋겠는지 적는 것입니다. 이 nodeSelector가 설정되어있으면 우리가 정한 노드로 들어가지만 설정되어있지 않다면 아무 노드에나 들어가게됩니다. 만약 이 설정을 사용하고싶다면 반드시 노드에 라벨링이 되어있어야합니다.
kubectl label nodes {노드이름} {라벨키}={라벨값}
이렇게 붙일 수 있고 노드 이름은 kubectl get nodes 로 확인할 수 있습니다.
• ports 부분에서 우리는 현재 Deployment를 만들 생각이 없기 때문에 컨테이너 포트만 정해주면 상관없어서 우선 저렇게 작성했습니다.
• 이제 우리가 만든 Secret을 사용할 수 있는 env 설정입니다. name 에는 환경변수의 키값을 적고 valueFrom에서 우리가 작성한 Secret을 바라보게 만들어줬습니다.
여기서 중요한 것은 Secreta을 만들 때의 name과 secretKeyRef 에서 적을 name이 동일해야 바인딩이 됩니다.
• volumeMounts는 hostPath의 볼륨을 만들어줬습니다. hostPath 볼륨으로 만들어주면 노드가 사라지는 순간 없어지는 볼륨이어서 제 실습에 딱 적당합니다.
만약 노드가 사라져도 볼륨이 유지되는 것을 원한다면 PV / PVC를 만들어서 연결해야합니다. 나중에 포스팅할 것 같지만 일단 지금은 넘어가도록 하겠습니다.
이제 MySQL 파드를 생성해보겠습니다.
잠깐!
쿠버네티스는 데이터베이스를 배포하기에 적합한 기술이 아닙니다. 만약 실제로 데이터베이스를 사용해야할 일이 있다면 쿠버네티스 클러스터 환경이 아닌 별도의 서버를 두는 것이 합리적입니다.
쿠버네티스는 stateless 애플리케이션에 적합하도록 설계되었습니다.
쿠버네티스는 설계될 때부터 애플리케이션의 생명주기가 짧도록 설계했습니다. 즉, 쿠버네티스 입장에서 애플리케이션이라 함은 언제든지 죽을 수 있고 언제든지 되살릴 수 있는 존재입니다.
이는 stateful의 특징이 강력한 애플리케이션인 데이터베이스에는 적합하지 않습니다.
물론, 쿠버네티스에서도 이런 stateful 애플리케이션을 위한 StatefulSet이라는 오브젝트를 지원해주긴 합니다만 이는 stateful한 애플리케이션 중에서도 적당히 stateful한 애플리케이션에 적합한 오브젝트입니다.
따라서, 만약 실제로 데이터베이스를 쿠버네티스와 연결하고 싶다면 별도의 서버를 두는 것이 좋습니다. 이에 대한 내용은 아래의 링크에 자세히 설명되어있습니다.
https://coding-review.tistory.com/451
다시 본론으로 돌아와서 파드를 생성하고 상태를 확인해보겠습니다.

잘 배포가 되었네요.
4. MySQL 사용자를 만들고 데이터베이스 생성하기
우리는 사용자를 만들기 위해서 우선 파드 내부로 접근해야합니다. 현재 파드에는 하나의 컨테이너밖에 없기 때문에 아래의 명령어가 통하는것이지 만약 여러개의 컨테이너가 있다면 컨테이너 이름을 특정지어줘야합니다.
kubectl exec -it -n {네임스페이스 명} {파드 명} -- /bin/bash
이제 MySQL로 접근하면?
mysql -u root -p
우리가 Secret에서 정한 패스워드를 입력하면 접속 완료!

이제 사용자를 만들어봅시다.
create user 'ks'@'%' identified by 'root';
grant all privileges on *.* to 'ks'@'%';
flush privileges;
exit
이렇게 명령어를 작성하고 나오면 됩니다.
5. 스프링 프로젝트 간단하게 만들기
스프링은 제 포스팅에서 너무 많이 얘기해서 간단하게 짚고 넘어가겠습니다.
MySQL과 JPA에 대한 의존성을 추가해주고 entity를 만들고 repository만들고 controller만들었습니다.
package com.example.transaction.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
private int age;
}
package com.example.transaction.repository;
import com.example.transaction.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByName(String name);
}
package com.example.transaction.controller;
import com.example.transaction.entity.Member;
import com.example.transaction.repository.MemberRepository;
import com.example.transaction.service.MemberServiceA;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/hello-k8s")
public String getMember() {
Member findMember = memberRepository.findByName("garlicpollpoll").orElseThrow(() -> new IllegalStateException("존재하는 회원이 없습니다."));
return "hello-k8s I'm " + findMember.getName();
}
}
준비 끝!
그리고 스프링 프로젝트에서 가장 중요한 application.yml을 보면
spring:
datasource:
url: jdbc:mysql://mysql.database.svc.cluster.local:3306/transaction?serverTimezone=UTC&characterEncoding=UTF-8
username: ks
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: create
open-in-view: false
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
logging:
level:
org:
springframework:
transaction:
interceptor: trace
역시 데이터베이스와 연결하는 것이 가장 중요하죠.
이것을 짚고 넘어가기 전에 우선 MySQL의 서비스부터 만들어보겠습니다.
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: database
spec:
ports:
- port: 3306
name: mysql
clusterIP: None
selector:
type: db
이 서비스를 생성하면 이제 MySQL파드에 접근할 수 있는것인데요. 저는 clusterIP를 None으로 주었습니다. 이것이 의미하는 것은 Headless Service를 만들겠다는 의미입니다.
일단 이것에 대한 자세한 내용은 아래의 링크에 나와있습니다!
https://coding-review.tistory.com/547
우선 서비스를 만들면 DNS Server에 이 서비스의 이름으로 올라가게됩니다. 위 포스팅에서도 언급되지만, 네임스페이스가 같다면 서비스 이름으로만으로도 파드에 접근할 수 있게 됩니다.
제 서비스의 예시로는 mysql:3306이렇게 붙을 수 있다는 것이죠. 하지만 네임스페이스가 다르다면 이렇게 붙을 수는 없습니다.
Headless Service를 만들면 DNS Server에 다음과 같은 규칙으로 DNS가 만들어집니다.
{Headless Service명}.{네임스페이스명}.svc.{DNS서버 명}.local
이렇게말이죠. 때문에 MySQL 파드에 붙고싶을 땐 Headless Service를 만들고 이렇게 붙으면 됩니다.
mysql.database.svc.cluster.local:3306
그럼 스프링에서 MySQL로 붙을 수 있습니다.
이제 마지막인 스프링을 외부에서 붙어보겠습니다.
6. 스프링 프로젝트 외부에서 붙어보기
우선 스프링 파드를 띄우려면 이미지가 있어야겠죠? bootJar를 이용해서 JAR파일을 만들어주고 Dockerfile을 만들어줍니다.
FROM openjdk:17-jdk
ARG JAR_FILE=./transaction-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
그리고 ECR에 이미지를 푸시할겁니다.
그러려면 ECR에 접근해야겠죠? ECR에 접근하는데 필요한 IAM 정책은 이미 전 포스팅에서 설정했으니 넘어가고 바로 ECR로 이미지를 푸시해보겠습니다.
ECR 레파지토리를 만들면 (프라이빗으로 만들어야합니다.) "푸시 명령어 보기"라는 버튼이 있습니다. 그걸 그대로 복사해서 쉘 스크립트 파일을 만들어줄겁니다.
저는 이렇게 됩니다.
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com
docker build --platform linux/amd64 -t k8s/ecr .
docker tag k8s/ecr:latest {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
docker push {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
사용자 ID에는 AWS 서비스를 아무데나 들어가도 나오는 ARN에 적혀있습니다.
저는 IAM에서 보는 것을 선호하는데 IAM에 들어가서 아래로 내려보시면 숫자로 적혀있는 일련번호가 있습니다. 그걸 적어주시면 됩니다.
자 이제 이 쉘스크립트를 실행하면 이미지가 저절로 푸시됩니다.
그리고 파드를 만들어보겠습니다.
apiVersion: v1
kind: Pod
metadata:
name: spring-pod
namespace: was
labels:
type: was
log: dev
spec:
nodeSelector:
deploy: was
containers:
- name: was-container
image: {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
ports:
- containerPort: 8080
이미지에는 ECR로 들어가서

요 URI를 입력하면 됩니다.
그리고 파드를 생성해줍니다.
이후에 서비스를 만드는데 MySQL때와는 다르게 LoadBalancer 타입으로 만들어줘야합니다.
apiVersion: v1
kind: Service
metadata:
name: spring-svc
namespace: was
spec:
type: LoadBalancer
selector:
type: was
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30000
externalTrafficPolicy: Cluster
이렇게 적고 서비스를 생성하면?

로드밸런서가 생성됩니다.
참고로 이 로드밸런서는 ALB가 아닌 NLB입니다. 때문에 패킷의 TCP 헤더에 적힌 곳으로 날아간다는 특징 때문에 HTTPS가 적용이 안됩니다. 때문에 우리가 내부적으로 NGINX같은 웹서버로 적용시켜줘야합니다.
이 로드밸런서로 들어가면 리스너도 설정되어있고 헬스체크까지 설정되어있는 것을 볼 수 있습니다.
저는 80포트로 열어줬기 때문에 보안그룹도 80포트가 열려있네요.
그리고 서비스를 조회해보겠습니다.

저기 적혀있는 EXTERNAL_IP가 로드밸런서에서도 볼 수 있는 그 DNS값입니다. 이제 저걸 검색창에 치면?

이렇게 스프링 페이지가 뜹니다!
저는 80포트로 열어줘서 포트를 적지 않았지만 8080포트로 열었다면 뒤에 포트를 적어야합니다.
그리고 우리가 만든 API로 접근해보면?

이렇게 잘 나오는 것을 확인할 수 있습니다.
마치며
이렇게 스프링 프로젝트를 EKS로 실습해봤습니다. 온프레미스로 진행할 때와는 전혀 다른 속도감이네요. 정말 맘에듭니다.
아! 참고로 쿠버네티스 클러스터 안에 있는 로드밸런서나 노드그룹은 삭제하는 것이 지갑 건강상 좋습니다!
다음 포스팅에선 이 실습을 조금 강화시켜보겠습니다.
오늘도 정말 긴 포스팅이었네요. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
AWS EKS PV로 EBS볼륨 바운드하기
이전 포스팅에서 어느정도 이어지는 포스팅입니다. 물론 완전히 이어지진 않아서 따로 봐도 상관없습니다.
https://coding-review.tistory.com/555
이전 포스팅에서 단순하게 MySQL 파드, 서비스 / 스프링 파드, 서비스를 배포해서 외부에서 접속했던 것이라 어느정도 고도화가 필요했습니다.
PV / PVC를 EBS볼륨과 바운드하는 것을 시작으로 Nginx로 웹서버 - WAS 서버 구축하기 등을 천천히 이어나가겠습니다.
이번 포스팅에선 EBS를 PV로 바운드하는 방법에 대해서 정리해보도록 하겠습니다.
PV로 EBS볼륨 바운드
우선 저는 스프링 프로젝트에 PVC를 붙여서 PV와 연결해보도록 하겠습니다. PV는 저장소인만큼 데이터베이스와 잘 어울릴 것 같지만 이전 포스팅에서도 언급했듯이 데이터베이스같은 강한 stateful 애플리케이션은 쿠버네티스와 어울리지 않습니다.
때문에 제 프로젝트를 MSA로 전환하는 과정에서 필요한 발판으로서 스프링 프로젝트와 PV를 연결해보았습니다.
PVC 생성
우선 EKS 클러스터를 구축했다면 StorageClass가 자동으로 생성되어있습니다.

이렇게 StorageClass가 만들어져있고 이를 열어보면 다음과 같습니다.
kyoungsuk@imgyeongseog-ui-MacBookPro was % kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
gp2 kubernetes.io/aws-ebs Delete WaitForFirstConsumer false 4d3h
kyoungsuk@imgyeongseog-ui-MacBookPro was % kubectl get sc gp2 -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"gp2"},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs","volumeBindingMode":"WaitForFirstConsumer"}
creationTimestamp: "2024-09-23T11:03:12Z"
name: gp2
resourceVersion: "284"
uid: 728a001e-d21e-4c81-bc3b-3d0cf9d9c1cc
parameters:
fsType: ext4
type: gp2
provisioner: kubernetes.io/aws-ebs
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
이렇게 되어있는걸로 봐선 EBS볼륨을 io1이나 io2로 설정해서 만들 수 있는 StorageClass도 만들 수 있을 것 같긴 하군요. 이건 따로 실습해봐야겠습니다.
이제 본격적으로 PVC를 만들어보겠습니다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spring-pvc
namespace: was
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: gp2
PVC를 다음과 같이 생성했고 스토리지 용량은 5기가 StorageClassName은 앞서 언급했던 gp2로 설정했습니다. AccessMode에 대해서는 이전 쿠버네티스 이론 : 볼륨 편에서 자세히 다뤘으니 넘어가도록 하겠습니다.
그리고 이 PVC를 배포해주시면 됩니다. 그럼 반은 성공한 것이지요.
kubectl apply -f ./was-pvc.yml
그리고 이제 중요한 스프링 프로젝트에 PVC를 붙이는 것인데요.
우선 파드를 만드는 곳에선 PVC를 붙일 수 없다는 것을 먼저 말씀드리고싶네요. 방법이 있을진 모르지만 우선 제가 실습했던 다양한 상황에선 파드를 만드는 파일에선 PVC를 붙일 수 없다는 결론을 냈습니다.
그래서 저는 ReplicaSet을 만들고 그곳에 PVC를 붙였습니다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: spring-rs
namespace: was
spec:
replicas: 2
selector:
matchLabels:
type: was
template:
metadata:
labels:
type: was
spec:
volumes:
- name: was-storage
persistentVolumeClaim:
claimName: spring-pvc
containers:
- name: was-container
image: 192021811870.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
volumeMounts:
- name: was-storage
mountPath: /was/storage
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 500m
memory: 1Gi
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: deploy
operator: In
values:
- was
하나씩 풀어서 해석해보겠습니다.
• 우선 replicas는 2로 설정했습니다. 사실 노드의 자원 상황을 고려하면 1이 적당하지만 억지로 2개를 띄워서 PV가 제대로 연결되었는지 확인하고 싶어서 2로 설정했습니다.
• ReplicaSet에서 어떤 템플릿의 파드를 사용할 것인지를 정의하는 부분에서 PVC를 연결했습니다. name은 volumeMounts에서의 name과 동일하게 맞춰줘야하고 PVC를 만들 때 설정했던 name과 persistentVolumeClaim의 claimName과 같아야합니다.
• volumeMounts에선 PV가 연결되었다는 것을 알아보기 쉽게 /was/storage로 설정했습니다.
• resources는 replicas를 두개 띄우기 위해서 조금 빡빡하게 설정했습니다. 실제로 스프링 프로젝트를 돌리기 위해서는 CPU 1코어에 2GB 메모리가 적당하긴합니다.
• 저는 PodAffinity를 사용해서 노드를 선택했지만 nodeSelector로도 충분히 대체가능합니다.
이제 ReplicaSet을 배포해보면?

이렇게 배포가 된 것을 볼 수 있습니다. 그리고 pv도 조회를 해보면

이렇게 잘 만들어진 것을 볼 수 있습니다. Reclaim Policy가 Delete로 되어있는 것은 이 ReplicaSet을 삭제하면, 정확히는 PVC를 삭제하면 PV도 자동으로 삭제하겠다는 의미입니다.
이 내용도 쿠버네티스 이론 : 볼륨 편에서 자세히 다뤘으니 해당 포스팅을 참고해주세요!
그리고 EBS볼륨이 실제로 만들어졌는지 확인해보겠습니다.

보면 우리가 만든 5GB짜리 볼륨이 생성된 것을 볼 수 있습니다.
그래도 믿을 수 없다! 직접 파드에 exec 명령어로 들어가서 폴더를 확인해보겠습니다.

우선 첫번째로 들어간 파드에서 실제로 was폴더가 만들어져있었고 안에 들어가서 lalala 폴더를 하나 만들었습니다.
이제 다른 파드로 들어가서 똑같은 폴더가 만들어졌는지 확인해보겠습니다.

다른 파드로 들어가서 직접 lalala폴더를 확인했습니다.
추가적으로 어떤 스토리지가 마운트 되었는지 명령어를 날려서 확인해보면

이렇게 /was/storage로 5GB의 볼륨이 마운트된 것을 확인할 수 있습니다.
마치며
이렇게 스프링 프로젝트에 PVC를 연결하고 PV로서 EBS볼륨을 마운트해봤습니다. 실제로 서비스를 개발하다보면 다른 서버에서 동일한 파일을 바라봐야하는 경우가 종종 있는데요.
보통 클라우드로 인프라를 구성했다면 EBS볼륨을 이용해서 이렇게 같은 볼륨을 마운트해 같은 파일을 바라보게 하는 것이 가능합니다.
이것을 EKS 클러스터는 조금 더 쉽게 사용하게 만들어주는 것 같네요. 실제로 EBS볼륨도 자동으로 만들어지고 마운트하는 작업까지 자동으로 되니까 너무 편하네요.
여기까지 포스팅을 마쳐보도록 하겠습니다. 긴 글 읽어주셔서 감사하고 오늘도 즐거운 하루 되세요!
AWS EKS 스프링 프로젝트에 NGINX 붙이기
이번 포스팅은 이전 포스팅과 어느정도 이어집니다. 이전에 스프링 프로젝트를 간단하게 배포했는데 저는 추후 HTTPS를 붙여서 실제 도메인을 붙여볼 예정이기 때문에 Nginx에 스프링 프로젝트를 붙여야했습니다.
이번 포스팅에선 Nginx를 붙이기만 하고 다음 포스팅에선 HTTPS를 적용해보겠습니다.
바로 시작해보겠습니다!
스프링에 Nginx 붙이기
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/aws/deploy.yaml
우선 Ingress Controller를 설치해줍니다.
위의 명령어를 쓰면 namespace, Service Account, Role, RoleBinding, ClusterRole, ClusterRoleBinding, configmap, controller service, controller deploy 이렇게 모든 것들을 전부 배포해줍니다.
그리고 서비스를 확인해보면

이렇게 NLB가 만들어집니다.
이 상태에서 EXTERNAL-IP에 적힌 것을 그대로 복사해서 검색창에 넣으면 Nginx창이 뜨지만 아직 Nginx를 배포하지도 않았고 서비스로 연결하지도 않아서 그냥 404페이지가 뜹니다.
이제 한번 연결해보겠습니다.
server {
listen 80;
server_name a35a928c5918d4f46ad7a6206e6fffaa-b98369e8eaa8a657.elb.ap-northeast-2.amazonaws.com;
server_tokens off;
location / {
proxy_pass http://spring-svc.was.svc.cluster.local:8080;
}
}
우선 이건 제가 만든 default.conf 파일입니다. 우리의 Nginx conf 파일이 되어줄겁니다.
server_name에는 NLB의 external IP를 적어주고 proxy_pass에는 headless service의 도메인을 적어주면 됩니다. 추후 만들것이니 일단 넘어가죠!
이제 이걸 ConfigMap으로 추출할겁니다.
kubectl create configmap nginx-conf --from-file=default.conf -o yaml
제가 만든 default.conf를 ConfigMap으로 추출해서 생성했습니다. 저는 일단 yaml 파일로 ConfigMap도 가지고 있으려고 따로 추출했습니다.
그리고 간단하게 서비스도 만들어주고
apiVersion: v1
kind: Service
metadata:
namespace: ingress-nginx
name: nginx-svc
spec:
ports:
- name: nginx-port
port: 80
targetPort: 80
selector:
app: nginx
저는 서비스를 ClusterIP로 만들었습니다. 왜냐하면 클러스터 내부 (Ingress) 에서 접근할 것이기 때문이죠.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: ingress-nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
volumes:
- name: nginx-storage
configMap:
name: nginx-conf
items:
- key: default.conf
path: custom-default.conf
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: nginx-storage
mountPath: /etc/nginx/conf.d
readOnly: true
이건 Nginx Deployment입니다. 여기서 주의해야 하는 점은 볼륨 마운트인데 우선 volumes에 들어있는 configMap 속성에서 items를 주목해서 봐야합니다.
key에 해당하는 것은 아래의 명령어로 확인할 수 있습니다.

밑줄친 저부분이 key에 해당합니다. 그리고 path부분은 우리가 원하는 이름으로 정해주면 됩니다.
volumeMounts에선 /etc/nginx/conf.d 바로 하위에 우리의 conf파일을 두겠다고 선언하면 됩니다.
이렇게 하면 준비 끝! Deployment를 배포해봅시다!
kubectl apply -f ./nginx-deployment.yml

잘 배포가 됐습니다.
혹시 우리가 만든 custom-default.conf를 직접 확인하고 싶다면 직접
파드 안으로 들어가서 확인해보시면 됩니다. 명령어는 아래와 같습니다.
kubectl exec -it -n {네임스페이스명} {파드명} -- /bin/bash
이제 마지막 단계로 Ingress를 만들어주면 됩니다. Ingress를 만들면 우리가 이미 배포한 Ingress Controller를 알아서 찾아가기 때문에 별다른 설정 없이도 Ingress Controller와 연결할 수 있습니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
namespace: ingress-nginx
annotations:
kubernetes.io/ingress.class: "nginx" # 사용할 Ingress Controller
spec:
rules:
- host: a35a928c5918d4f46ad7a6206e6fffaa-b98369e8eaa8a657.elb.ap-northeast-2.amazonaws.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-svc
port:
number: 80
우선은 host에 도메인을 입력하지 않고 external IP를 주었습니다. 이 것은 추후에 도메인으로 바뀔 예정입니다. 우선은 넘어가도록 하죠.
그리고 Ingress를 배포해보면?
kubectl apply -f ./ingress.yml

이렇게 잘 생성된 것을 확인할 수 있습니다.
이 상태에서 이제 이전 포스팅에서 만들었던 API를 호출해보겠습니다.

잘 호출되네요!
마치며
Nginx를 붙이고 HTTPS까지 진행하는 것은 시간관계상 그리고 포스팅 분량상 어쩔 수 없이 여기서 마무리 짓도록 하겠습니다. 점점 진도가 나가는 것이 끝이 보이는 것 같네요!
이렇게 오늘도 조금씩 성장했습니다! 내일도 화이팅하기를 바라면서 오늘은 여기서 마무리짓도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
AWS EKS 스프링 + Nginx + SSL
저번 포스팅에서 이어지는 내용입니다!
https://coding-review.tistory.com/557
저번 포스팅에선 Nginx를 설치하고 스프링 프로젝트를 붙여서 로드밸런서를 이용해서 외부에서 접속해봤습니다.
이번엔 Nginx에 SSL을 붙여서 HTTPS를 적용시켜봤습니다. 거의 4일내내 이거만 한거같네요. 본격적으로 시작해보겠습니다!
Nginx와 SSL 근데 이제 Kubernetes를 곁들인
저도 Nginx에 SSL을 붙인게 한두번이 아닌지라 제가 익숙한 방식대로 했습니다. 처음엔 이렇게요.
apiVersion: v1
data:
default.conf: |-
server {dd
listen 443 ssl;
server_name studyk8swithaws.shop;
server_tokens off;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass http://spring-svc.was.svc.cluster.local:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
kind: ConfigMap
metadata:
creationTimestamp: "2024-09-30T13:15:21Z"
name: nginx-conf
namespace: ingress-nginx
resourceVersion: "1565754"
uid: ffe6bda7-017c-4a85-a7c4-1b2f8e1bbb14
conf파일을 configmap으로 만들어서 이걸 volumeMount하는 작전이었는데...
사실 이렇게 하면 될 줄 알았는데 실패했습니다. 조금 더 깊이있게 파봤으면 성공했을지도 모르지만 nginx ingress controller에서 SSL을 리다이렉트 시켜주는 것을 지원해준다고 하길래 곧장 선회했습니다.
1. 도메인 사기
도메인을 구입했는데 저는 .shop이 붙은 도메인으로 550원주고 샀습니다. 저렴한 도메인이 많으니 한번 찾아보시는 것을 추천드려요!
2. Route53과 도메인 연결하기
이 부분은 빠르게 넘어가겠습니다. Cafe24던 가비아던 네임서버를 Route53의 네임서버로 변경해주는 것입니다. 그리고 만들어진 로드밸런서를 A레코드를 이용해서 붙여줘야합니다.
그리고 letsencrypt에서 발급한 TXT레코드도 등록해야하니 이부분은 정확히 짚고 넘어가겠습니다.
certbot certonly --manual --preferred-challenges=dns \
--email {내 이메일} \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos \
--domain {내 도메인 www 빼고} \
--domain "*.{내 도메인}" \
--work-dir ./certbot \
--config-dir ./certbot \
--logs-dir ./certbot
이렇게 진행하면 다음과 같은 문구가 뜹니다. 엔터를 누르라고 하는데 우선 엔터를 누르지 말아야합니다!

이걸 Route53에서 TXT를 생성하고 붙여넣기 해야합니다.
Route53에서 새 레코드 생성을 누르시고 TXT타입으로 만들어줍니다.

레코드 이름에 정확히 _acme-challenge라고 넣어주시고 값에는 위에서 빨간 밑줄 친 부분을 넣어줍니다.
그리고 상태 확인을 통해 DNS에 전파되는 것을 확인하고 엔터를 눌러줍니다.

여기로 들어가면 fullchain과 privkey가 생깁니다. 해당 디렉토리로 이동한 다음 다음과 같은 명령어를 작성해줍니다.
kubectl create secret tls nginx-ssl-secret \
--namespace ingress-nginx \
--cert=fullchain.pem \
--key=privkey.pem \
--dry-run=client -o yaml | kubectl apply -f -
이렇게 secret을 만들어줍니다. 그럼 일단 letsencrypt는 준비 끝!
3. cert-manager 설치
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
이걸 설치해주시면 Certificate 오브젝트를 관리할 수 있습니다.
그리고 cert-manager는 ClusterIssuer를 이용해서 인증서를 지속적으로 발급해줍니다. 이어서 ClusterIssuer까지 만들겠습니다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-nginx
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-nginx
solvers:
- http01:
ingress:
class: nginx
그리고 이제 ingress를 업데이트해줘야합니다.
4. ingress 수정
저번 포스팅에서 조금 많이 수정되는 ingress입니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
namespace: ingress-nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-nginx
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- studyk8swithaws.shop
secretName: nginx-ssl-secret
rules:
- host: studyk8swithaws.shop
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spring-svc
port:
number: 80
issuer로 letsentrypt-nginx (<< 이건 ClusterIssuer에서 작성한 이름이어야합니다.) 를 작성하고 tls 부분을 추가해줍니다. 그리고 서비스를 원래는 nginx의 서비스를 붙였었는데 이걸 스프링 서비스로 바꿔줍니다.
이전 포스팅들과 달라지는 점이라면 네임스페이스가 다르면 인식할 수 없을 수도 있어서 스프링 replicaset이랑 service를 모두 ingress-nginx와 같은 네임스페이스를 사용하도록 했습니다.
서비스의 포트는 80포트입니다. 아래에서 바로 서비스 yaml 파일을 보시죠.
apiVersion: v1
kind: Service
metadata:
name: spring-svc
namespace: ingress-nginx
spec:
selector:
type: was
ports:
- protocol: TCP
port: 80
targetPort: 8080
서비스 포트는 80, 타겟 포트는 8080을 설정해주고 replicaset에서도 containerPort를 8080으로 변경해줍니다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: spring-rs
namespace: ingress-nginx
spec:
replicas: 2
selector:
matchLabels:
type: was
template:
metadata:
labels:
type: was
spec:
volumes:
- name: was-storage
persistentVolumeClaim:
claimName: nginx
containers:
- name: was-container
image: 192021811870.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
ports:
- containerPort: 8080
volumeMounts:
- name: was-storage
mountPath: /was/storage
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 500m
memory: 1Gi
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: deploy
operator: In
values:
- was
이렇게 준비를 모두 끝마치고 ingress controller를 재시작해줍니다.
kubectl rollout restart deployment -n ingress-nginx ingress-nginx-controller
혹시 이때 안된다면 로그를 확인해서 어떤 부분이 문제인지 확인하셔야합니다! 저는 보안 그룹에서 8080포트를 열어주지 않아서 502 에러가 떴었습니다.
그리고 도메인으로 입력해보면?

성공적으로 띄웠습니다! 옆에 빨간경고도 안뜨고 정말 보기 좋네요.
마치며
4일이 넘게 걸려서 해결한 것을 블로그로 정리하려니 30분도 안걸렸네요. 그래도 성공하니까 기분은 좋습니다!
다음 포스팅은 실제로 파드에 부하를 줘서 파드가 늘어나는지 확인해볼겁니다. Kubernetes Auto Scaler, 그중에서도 HPA를 이용해서 파드를 늘려보고 실제로 파드가 늘어나는지 그리고 노드의 자원이 부족하면 노드도 늘어나는지 테스트 해보려고합니다.
그럼 이번 포스팅도 여기까지 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
AWS EKS HPA로 확장성 높이기
저번 포스팅에서 이어지는 내용입니다!
https://coding-review.tistory.com/558
저번 포스팅에선 Nginx Ingress Controller 에 letsencrypt로 발급한 키로 secret을 만들어서 SSL을 연결했습니다. 이번 포스팅에선 HPA와 CA를 이용해서 파드에 무리가 가면 파드를 늘리고 파드를 늘리는 과정에서 노드의 자원이 부족하면 노드까지 늘리는 것을 실습해보고 정리해보겠습니다.
저번 포스팅에서 추가되는 것은 HPA와 CA를 만드는 것 뿐입니다.
HPA만들기
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: spring-hpa
namespace: ingress-nginx
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: ReplicaSet
name: spring-rs
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
이렇게 HPA를 만들면 레플리카를 우리가 설정한 개수내에서 늘렸다 줄였다할 수 있습니다.
HPA에서 주목해야 하는 점은 scaleTargetRef에서 name을 우리가 생성한 Deployment (혹은 ReplicaSet, StatefulSet) 의 이름과 연결해줘야한다는 것입니다.
또한, metrics에서 어떤 자원을 기반으로 스케일링을 할 것인지 정해줄 수 있습니다. 쿠버네티스 v2 (1.18버전 이후) 에는 Scale Out, Scale In 을 어떤 전략을 가지고 할 것인지도 정해줄 수 있습니다.
하지만 저는 그런 전략까지는 필요없어서 우선 간단하게 만들었습니다.
만약 스케일 전략을 가지고 있다면 100퍼센트로 스케일한다고 가정했을 경우 레플리카가 2 -> 4 - >8 이렇게 늘어나고 줄어들 때도 8 -> 4 -> 2 이렇게 줄어듭니다. 스케일 전략의 퍼센트는 커스텀하게 설정할 수 있습니다.
실전에서는 이렇게 스케일 전략을 가져가는 것이 더 나은 선택이 될 수 있을 것 같습니다.
우선 저는 CPU의 평균을 50퍼센트로 유지하도록 설정했습니다.
Metric Server 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
이 명령어를 통해 메트릭 서버를 배포할 수 있고 배포된 메트릭 서버는 파드의 정보를 읽어와서 모니터링할 수 있게 해줍니다.
메트릭 서버는 추후에 파드의 상태를 확인할 때 사용하도록 하겠습니다.
CA 배포
CA란 Cluster Autoscaler의 약자로 HPA만 존재하는 경우 파드가 늘어날 뿐 노드의 자원이 부족하다면 Pending상태가 됩니다. 이 때 클라우드 서비스들은 Auto Scaling 서비스를 가지고 있기 때문에 이를 연결해서 자동으로 노드를 늘려주는 역할을 합니다.
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
name: cluster-autoscaler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-autoscaler
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
resources: ["events", "endpoints"]
verbs: ["create", "patch"]
- apiGroups: [""]
resources: ["pods/eviction"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["update"]
- apiGroups: [""]
resources: ["endpoints"]
resourceNames: ["cluster-autoscaler"]
verbs: ["get", "update"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["watch", "list", "get", "update"]
- apiGroups: [""]
resources:
- "namespaces"
- "pods"
- "services"
- "replicationcontrollers"
- "persistentvolumeclaims"
- "persistentvolumes"
verbs: ["watch", "list", "get"]
- apiGroups: ["extensions"]
resources: ["replicasets", "daemonsets"]
verbs: ["watch", "list", "get"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["watch", "list"]
- apiGroups: ["apps"]
resources: ["statefulsets", "replicasets", "daemonsets"]
verbs: ["watch", "list", "get"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"]
verbs: ["watch", "list", "get"]
- apiGroups: ["batch", "extensions"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "patch"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["create"]
- apiGroups: ["coordination.k8s.io"]
resourceNames: ["cluster-autoscaler"]
resources: ["leases"]
verbs: ["get", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
verbs: ["delete", "get", "update", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-autoscaler
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-autoscaler
subjects:
- kind: ServiceAccount
name: cluster-autoscaler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cluster-autoscaler
subjects:
- kind: ServiceAccount
name: cluster-autoscaler
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
app: cluster-autoscaler
spec:
replicas: 1
selector:
matchLabels:
app: cluster-autoscaler
template:
metadata:
labels:
app: cluster-autoscaler
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '8085'
spec:
priorityClassName: system-cluster-critical
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
serviceAccountName: cluster-autoscaler
containers:
- image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.2
name: cluster-autoscaler
resources:
limits:
cpu: 100m
memory: 600Mi
requests:
cpu: 100m
memory: 600Mi
command:
- ./cluster-autoscaler
- --v=4
- --stderrthreshold=info
- --cloud-provider=aws
- --skip-nodes-with-local-storage=false
- --expander=least-waste
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/{클러스터 이름}
volumeMounts:
- name: ssl-certs
mountPath: /etc/ssl/certs/ca-certificates.crt # /etc/ssl/certs/ca-bundle.crt for Amazon Linux Worker Nodes
readOnly: true
imagePullPolicy: "Always"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumes:
- name: ssl-certs
hostPath:
path: "/etc/ssl/certs/ca-bundle.crt"
이건 깃허브에 돌아다니는 CA 예시를 그대로 가져와서 사용한 것입니다. 맨 아래가 CA이고 자세히 보시면 클러스터 이름을 적어야 하는 곳이 있습니다. 이곳에 AWS EKS의 클러스터 이름을 적어주면 됩니다.
저는 EKS의 노드그룹의 설정으로 최대 노드 수를 3으로 설정해 실습을 위한 최소 비용만을 사용하도록 했습니다.
HPA, CA 실습
우선 파드 상태를 확인합니다.

Nginx 파드 두개, 스프링 파드 두개, Nginx Ingress Controller 한개가 생성되어있습니다.
이 상태에서 exec 명령어로 스프링 파드 내부로 들어갑니다.

그리고 CPU에 부하를 줘보겠습니다. 위의 명령어를 입력하면 됩니다.
while true; do :; done

저는 스프링 파드의 limit을 0.5 코어로 주었기 때문에 더이상 올라가지 않는 것을 확인할 수 있었습니다.

CPU가 100퍼센트에 도달하자 파드를 두개 늘리는 모습입니다.

노드도 하나 생성됐네요. AGE를 보면 지금 막 생성된 것을 볼 수 있습니다.
EC2 인스턴스 상황을 확인해보겠습니다.

이렇게 하나가 늘어난 것을 확인할 수 있습니다.
이제 부하를 멈춰보겠습니다.

부하가 줄어들었고 파드가 줄어드는지 확인해봐야합니다.

파드가 네개였다가 두개로 줄어들었습니다.
그리고 노드도 확인해보면... 줄어들겠지만 EKS의 CA를 사용하는 경우 AWS ASG가 노드를 늘리는건 금방 늘리는데 죽일때는 정말 느리게 죽입니다.
상식적으로도 트래픽이 몰려 서버가 간당간당한 상태에선 빠르게 노드를 추가해서 무리가 되는 파드의 부담을 줄여야하지만 부하가 점점 빠지는 경우에는 언제 또 증가할지 모르는 부하를 대비해 정말 이 노드가 사용되지 않는구나 판단될 때까지 노드를 살려놓습니다.
체감상 노드가 추가되는 것은 1~2분이면 추가가 되는데 사라지는건 10분 넘게 기다려도 사라지지 않는 경우가 있더군요.
CA사용시 주의사항!
CA를 사용하는 경우, 특히 AWS와 같은 클라우드 서비스를 사용하는 경우 문제되는 것이 바로 PV와 PVC바운딩이 안돼서 파드가 Pending 상태에 무한히 빠지는 경우입니다.
AWS를 예시로 들자면 EBS를 PV로 사용하는 경우 그에 맞는 PVC를 생성하고 volumeMount를 하는 경우 자동으로 PV를 선택해서 바운드 해준다는 특징이 있습니다.
하지만 이런 경우 문제가 되는 것이 EBS는 하나의 리전에서만 동작하기 때문에 노드가 추가되면 PV가 연결되지 않아 CA에 의해 생성되는 노드가 PV를 찾을 수 없어 무한히 Pending 상태로 이어지고 파드가 생성되지 않습니다.
즉, EBS를 PV로 사용한다면, 한국의 경우 ap-northeast-2a 부터 ap-northeast-2d까지 생성되는데 만약 EBS가 2a에 생성되고 CA에 의해 생성되는 노드가 2b라면 이 2b에 생성된 노드는 2a에 생긴 EBS와 연결될 수 없다는 이야기입니다.
때문에, CA를 사용한다면 EBS를 PV로 사용하는 것 보다는 EFS를 PV로 사용하는 것이 바람직합니다. EFS는 EBS와는 다르게 모든 리전에서 사용가능하기 때문이죠.

이렇게 Pending 상태에서 무한히 돌고있는 파드를 describe 명령어를 이용해서 확인해보면 volume node affinity conflict 라는 에러 메세지를 볼 수 있습니다.

PVC와 PV에 대한 describe도 확인해보면 해당 PV는 ap-northeast-2c에 생성되어있고 새롭게 생성된 노드가 만약 2c가 아닌 다른 리전에 생성되었다면 해당 PV가 노드와 연결되지 않아 파드 자체가 생성되지 않는 것이죠.

아무튼 결국 노드도 하나 줄어들어서 최소 노드 개수인 2개에 맞춰진 모습입니다.
EC2 인스턴스도 확인해보면 두개로 줄어든 것을 확인할 수 있습니다.

마치며
이렇게 HPA와 CA를 이용해서 파드를 부하에 따라 유동적으로 생성되도록 설계하였습니다. 이렇게 설계하게 되면 노드 그룹으로 설정한 최대 노드 개수에 도달하기 전까진 파드가 안정적으로 동작할 수 있습니다.
이제 다음 포스팅에선 데이터베이스 서버를 쿠버네티스와 분리하는 작업을 진행할 예정입니다.
제가 이전 포스팅에서도 종종 언급했지만 데이터베이스는 stateful 애플리케이션이고 이 정도가 매우 심해 쿠버네티스와 어울리지 않는 애플리케이션 중 하나입니다.
때문에 쿠버네티스 파드로 데이터베이스를 생성하는 것은 안전한 방법이 아니라고 볼 수 있죠.
쿠버네티스에선 이런 stateful 애플리케이션을 관리하기 위한 StatefulSet 이라는 오브젝트를 제공해주긴 하지만 stateful의 정도가 너무 심한 것들은 쿠버네티스에서 분리할 것은 권장하고 있죠.
그럼 다음 포스팅에서 뵙도록 하겠습니다! 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
AWS EKS 모니터링 환경 구축하기 (Prometheus, Grafana)
이전 포스팅에서 이어집니다!
https://coding-review.tistory.com/559
이전 포스팅에선 HPA와 CA로 파드와 노드를 동적으로 늘리는 실습을 했습니다. 이번엔 모니터링 환경을 구축해보도록 하겠습니다!
이전 포스팅에서 다뤘던 내용은 다루지 않겠습니다!
모니터링 환경 구축
프로메테우스 설치
우선 프로메테우스 yaml 파일을 하나 만들어줍니다.
defaultRules:
rules:
alertmanager: true
etcd: true
alertmanager:
service:
type: NodePort
kubeApiServer:
enabled: true
kubelet:
enabled: true
prometheus:
thanosService:
enabled: false
serviceMonitorSelectorNilUsesHelmValues: false
service:
type: NodePort
retention: 5d
retentionSize: "10GiB"
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp2
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 15Gi
여기서 중요한건 serviceMonitorSelectorNilUsesHelmValues를 false로 하는 것인데 만약 프로메테우스와 다른 네임스페이스를 가지고 있더라도 모니터링할 수 있도록 해주는 옵션입니다.
그리고 Helm을 이용해서 프로메테우스를 설치해주도록 하겠습니다.
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
이렇게 두개를 먼저 입력하고 이제 본격적으로 설치해보겠습니다.
helm install prometheus prometheus-community/prometheus --namespace ingress-nginx -f prometheus.yml
그리고 파드를 확인해보면?

이렇게 잘 되는 것을 확인할 수 있는데 이때 prometheus-server와 prometheus-alertmanager가 Pending 상태일 수 있습니다. 이건 PVC와 연결이 안되서 그런 것이므로 PVC를 확인해보면 됩니다.

저는 이미 연결을 다 해놨지만 Pending 상태에 있을 것입니다. 이때 edit으로 직접 파일을 수정해주도록 합시다.

이 부분을 확인해주시고 만약 gp2가 아니라면 gp2로 바꿔줍니다.
그럼 정상적으로 Running 상태에 들어가게됩니다.
이어서 그라파나를 설치해보겠습니다.
그라파나 설치
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install grafana grafana/grafana --namespace ingress-nginx
네임스페이스는 본인의 네임스페이스로 적절하게 바꿔주세요!
그리고 파드와 서비스를 확인해보죠

그라파나가 잘 들어가있죠?
이제 외부에서 붙어보겠습니다.

이렇게 포트포워딩을 해놓고 다른 터미널을 띄워줍니다.

다른 터미널에서 secret을 확인해주면 이렇게 빨간 부분이 패스워드입니다.
우린 3000번 포트로 포트포워딩을 했으니 http://localhost:3000으로 들어가줍니다.
아이디는 admin 패스워드는 위의 빨간 부분입니다.

이렇게 들어왔습니다!
왼쪽에 Connections -> Datasource으로 가줍니다.

저는 이미 하나 등록해놨는데 일단 없다고 가정하고 오른쪽 위에 Add new data source를 클릭합니다.

프로메테우스를 클릭하면 아래의 Connection부분이 우리가 입력해야할 부분입니다. 다시 터미널로 오겠습니다.

Service를 검색해서 나오는 prometheus-server의 포트와 아이피를 잘 적어두고 다시 UI로 넘어옵니다!

연결하고 Dashboard로 가줍니다.

제가 미리 하나 등록해놨는데 그건 무시하시고 오른쪽 위에 New를 클릭한 뒤 New Dashboard를 클릭합니다.

Add New Visualization 을 클릭하면 다음과 같은 창이 나오는데 디폴트로 되어있는 것을 선택합니다.

처음 화면인데 오른쪽 아래 Builder를 Code로 바꿔줍니다.

적절한 PromQL을 넣어주면 끝!
저는 네임스페이스가 ingress-nginx이면서 파드의 이름이 spring-rs-로 시작하는 것들을 넣어줬습니다.
현재 저는 스프링 파드를 두개 띄워놨고 이에 대한 CPU사용량을 모니터링 한 것입니다. PromQL은 찾아보시거나 GPT한테 물어보면 기깔나게 대답해주니 그것을 참고하면 되겠습니다.
마치며
이렇게 모니터링 환경을 구축해보았습니다. 추후에 포스팅으로 부하테스트를 거치고 파드가 늘어나는 것과 슬랙으로 알람을 보내는 것 까지 한번 시도해보려고합니다.
그럼 오늘 포스팅은 여기서 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다! 오늘도 즐거운 하루 되세요!
AWS EKS 모니터링 환경 구축에 슬랙 알람 추가하기
이전 포스팅과 이어집니다!
https://coding-review.tistory.com/560
이전엔 프로메테우스 + 그라파나로 모니터링 환경을 구축해봤습니다. 이번 포스팅에선 이전 포스팅에다 슬랙을 추가해보도록 하겠습니다.
이전 포스팅 내용은 담지 않았습니다!
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-state-metrics prometheus-community/kube-state-metrics --namespace kube-system
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
우선 메트릭 서버를 설치해야하는데 저는 kube-state-metrics와 metrics-server를 동시에 설치했습니다.
그리고 슬랙을 설치하고 로그인 한 뒤 webhook을 연결해줘야합니다.
슬랙 webhook을 연결하는 것은 구글에 많이 나와있어서 이 부분은 생략하도록 하겠습니다.
만약 프로메테우스를 helm으로 설치하였다면 alertmanager가 같이 들어가있을텐데요. 이전 포스팅과 다른 점이 바로 이부분입니다.
defaultRules:
rules:
alertmanager: true
etcd: true
alertmanager:
service:
type: NodePort
config:
global:
resolve_timeout: 5m
route:
group_wait: 30s
group_interval: 12h
receiver: 'slack-notifications'
routes:
- receiver: 'slack-notifications'
match:
- alertname: "InfoInhibitor|Watchdog"
receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '{채널이름}'
api_uri: {slack webhook api}
send_resolved: true
kubeApiServer:
enabled: true
kubelet:
enabled: true
prometheus:
thanosService:
enabled: false
serviceMonitorSelectorNilUsesHelmValues: false
service:
type: NodePort
retention: 5d
retentionSize: "10GiB"
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp2
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 15Gi
이렇게 변경하고 다시 프로메테우스를 배포하면 됩니다.
다시배포할 땐
helm uninstall prometheus --namespace ingress-nginx
helm install prometheus prometheus-community/prometheus --namespace ingress-nginx -f prometheus.yml
혹은 configmap만 따로 뽑아서 변경한 다음 alertmanager를 삭제하면 deployment가 알아서 다시 만들어줍니다.
그리고 그라파나로 들어가줍니다.

이전 포스팅의 그 대시보드 오른쪽에 Alert가 있는데 Alert는 대시보드를 저장하면 사용할 수 있습니다. New alert rule을 선택하면?


새로운 폴더와 evaluation group을 만들어줍니다.
그리고 맨 아래로 보면?

View or create contact points를 선택합니다.

제가 테스트한다고 하나 만들어놨는데 무시하시고 Add contact point를 선택합니다.

여기서 슬랙을 선택하고 webhook URL을 입력하면 끝!
오른쪽 위에 Test를 한번 눌러보시면 슬랙에 테스트 메세지가 전송됩니다. 이제 맨 아래 저장을 누르고 우리가 만든 슬랙을 선택하면 끝입니다.
그리고 CPU의 부하를 줘보겠습니다.

그리고 슬랙을 보면!

이렇게 알람이 오는 것을 확인할 수 있습니다.
마치며
이번 포스팅에선 모니터링에 알람까지 설정을 해봤습니다. PromQL로 동접자 쿼리를 날릴 수 있는지 확인해보고 모니터링은 마쳐야겠네요. 아 동접자 쿼리는 포스팅할 예정은 없습니다. 아마 구글에 많이 나와있지않을까 싶은데 저도 테스트만 해보고 포스팅은 안할 생각입니다.
이제 점점 인프라가 완성되고 있네요. 드디어 끝이 보입니다! 다음 포스팅에서 뵙도록 하죠!
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
쿠버네티스 클러스터에 RDB배포 해? 말어? 에 대한 논쟁
요즘 사이드 프로젝트 때문에 실습만 주구장창 했는데 오랜만에 이론 포스팅입니다. 쿠버네티스를 공부하다보면, 그리고 DevOps에 대해 공부하다보면 한번쯤은 보게 되는 논쟁 바로
"쿠버네티스 클러스터에 RDB배포? 시기상조다" VS "쿠버네티스 클러스터에 RDB배포 안해봤어? 겁나 편한데"
이 논쟁을 구글링하면 마땅한 답이 없습니다. 누가 시원하게 쓰면 좋아! 쓰면 나빠! 이걸 적어놓은걸 찾기 힘들었습니다. 그래서 GPT와 토론을 하면서 주장 -> 반박 -> 재반박 -> 반박에 반박 을 거듭하여 나름 정리해봤습니다.
우선 이 논쟁에 대해 짚어야 하는 부분은 크게 다섯가지입니다.
• 트랜잭션 롤백에 대한 관점
• 마스터 슬레이브 노드 선출에 대한 관점
• 네트워크 망분리로 인한 split-brain 상황에 대한 관점
• 쿠버네티스 클러스터로 로드밸런싱 되는 과정에서 생기는 네트워크 지연 이슈에 대한 관점
• CAP정리에 대한 관점
하나씩 톺아보겠습니다.
쿠버네티스 RDB배포 논쟁
토론에 들어가기 앞서 짚고 넘어가야 하는 점이 있는데요. 쿠버네티스는 초기 목표는 stateful한 애플리케이션에 대한 관리이지만 버전을 거듭하면서 점점 stateless 애플리케이션에 적합해졌다는 것입니다.
제가 오늘 언급할 RDBMS는 그 자체로 하나의 거대한 stateful 애플리케이션으로서 stateless가 조금이라도 묻으면 장애를 발생시킬 수도 있는 무서운 친구입니다. (NoSQL은 전혀 다릅니다!)
때문에 쿠버네티스에서도 이런 Stateful 애플리케이션을 관리할 수 있도록 StatefulSet이라는 오브젝트를 이용해 네트워크도 stateful하게 관리해주고 PV, PVC로 스토리지도 stateful하게 관리해주는 여러 케어가 있었지만 제가 나름대로 직접 발로 뛰어가며 여러 시니어 개발자분들한테 물어봐도 여전히 아직 시기상조라는 말이 많습니다.
그 점을 미리 알고 가시면 좋을 것 같습니다.
1. 트랜잭션 롤백에 대한 관점
쿠버네티스 = 분산시스템이죠.
분산시스템에서 트랜잭션 롤백에 대한 처리가 힘들다는 것이 첫번째 논점입니다. 만약 A라는 요청이 들어왔을 때 1단계, 2단계, 3단계에 걸쳐 커밋을 해야하는데 1단계를 파드1, 2단계를 파드2, 3단계를 파드3에서 처리하는 경우 2단계에서 실패하면 3단계로는 넘어가지 않아야하고 1단계는 롤백이 되어야하는 상황이 생기고 이를 관리하기가 힘듭니다.
"이의있소"
그런 분산 시스템에서의 트랜잭션 롤백을 위한 2PC와 SAGA패턴이라는 것이 준비되어 있습니다. 기본적으로 분산시스템에서 많이 쓰이는 것은 2PC보단 SAGA패턴인데 그 이유는 2PC의 경우 롤백을 확인하기위한 첫번째 커밋과 실제 데이터베이스에 적용되는 두번째 커밋 사이에 락이 걸리기 때문에 데드락 위험이 굉장히 높아진다는 특징이 있기 때문입니다.
SAGA패턴에서는 이런 단점을 보완해서 조금 더 유연한 트랜잭션 롤백 전략을 가지고 있는데 SAGA패턴에서는 전위회복, 후위회복이라는 두가지 개념을 이용해서 유연한 트랜잭션 롤백 전략을 가지고 있습니다.
SAGA패턴에 대해서 아래의 링크에 자세히 설명되어있습니다! 한번 들러주셔서 개념잡고 오셔도 괜찮을 것 같습니다. 물론 몰라도 전혀 상관없습니다!
https://coding-review.tistory.com/484
짧게 짚고 넘어가자면 전위 회복은 롤백시 특정 부분으로 돌아가서 트랜잭션을 재실행하는 것이고 후위 회복은 최초 시점으로 롤백하는 것입니다.
전위 회복에서 중요한 점은 트랜잭션을 재실행하는 로직이 있어야 한다는 것이고 후위 회복은 최초 시점에 대한 상태를 저장하고 있어야 한다는 것이 중요한 점입니다.
아무튼 분산 시스템에서 트랜잭션 롤백에 대한 전략은 이미 유명하고 이를 관리하는 것 또한 어느정도 보편화된 상황이죠.
"재반박 해보겠소"
쿠버네티스 클러스터에서 파드는 쉽게 종료되고 쉽게 재시작되는 존재입니다.
만약 트랜잭션이 실패한 것이 그냥 비즈니스 로직 중간에 발생한 예외 때문에 롤백되는 것이 아니라 파드가 죽어버리는 상황 때문에 발생한 것이라면 어떨까요?
일단 듣기만해도 복잡성이 엄청나게 올라갈 것 같은데요. 문제는 또 있습니다.
Istio같은 서비스 메쉬를 사용하고 있다면 네트워크 타임아웃에 따른 circuit breaker가 작동할 수도 있습니다. 이 경우에도 마찬가지로 네트워크가 제대로 흐르지 않아 예외 상황이 발생하게 되고 이 것을 관리하는 것이 또 다른 복잡성을 유발할 수 있습니다.
2. 마스터 슬레이브 노드를 선출하는 것에 대한 관점
기본적으로 RDBMS는 마스터 슬레이브 노드 선출에 대한 전략을 가지고 있습니다.
만약 이것이 분산 시스템으로 넘어오게 된다면 마스터 노드가 죽었을 경우 새로운 마스터를 선출하는 과정에서 어떤 파드를 마스터로 올려야하는지에 대한 어려움이 생깁니다.
데이터베이스가 마스터노드1개 슬레이브노드4개 이렇게 존재한다고 가정해봤을 때 마스터 노드가 죽으면 어떤 슬레이브 노드를 선출해야할까요? 이런 문제가 생깁니다.
"반박!"
그럼 흔한 RDBMS의 레플리케이션 전략을 그대로 파드로 올려버리면 되지 않나요?
그러니까 Galera 클러스터같은 동기 레플리케이션 전략을 그냥 묶음으로 올려버리면 되는 것 아닌가 하는 말입니다.
기본적으로 삼중화 전략을 선택하는 Galera클러스터는 멀티 마스터 아키텍처를 이용해서 만약 하나의 마스터가 죽더라도 Galera클러스터는 안정적으로 돌아갈 수 있습니다.
"재반박!"
데이터베이스가 마스터 노드를 선출하는 과정, 즉, 마스터 노드가 죽어있다는 그 "상태"도 stateful의 범위에 들어가있습니다.
노드가 죽어있다는 그 상태마저도 상태를 공유해야하는 것이 바론 stateful 애플리케이션의 대명사 RDBMS가 해야하는 것입니다. 만약 마스터 노드가 죽어버린 경우 Galera클러스터는 내부 로직에 따라 마스터를 재선출하는 과정을 거치게 됩니다.
만약 그 상황에서 죽어있던 노드 (혹은 파드)가 살아난다면 이 상태를 깨는 즉, stateless한 성격이 나오는 것이기 때문에 Galera클러스터 입장에서는 "선출 해? 말아? 누구를? 원래 마스터였던 애? 아니면 기존에 있던 애?" 이런 복잡성이 생기고 이것에 대해 따로 전략을 가지고 있어야합니다.
3. 네트워크 망분리로 인한 split-brain에 대한 관점
분산 시스템에서는 majority node와 minority node로 네트워크 장애가 발생해서 분리되는 경우 자칫 잘못하면 하나의 클러스터에 여러개의 마스터노드가 생기는 상황을 야기할 수 있습니다. 이것을 split-brain 상황이라고 하고 이것에 대한 자세한 내용은 아래의 링크에 정리되어있습니다!
https://coding-review.tistory.com/542
"반박 포인트!"
이는 쿠버네티스에 국한된 문제가 아닙니다. 즉, 분산 시스템에 레플리케이션 전략을 사용하는 경우 어떤 상황이던지 split-brain 상황이 올 수 있습니다.
또한, 네트워크망을 자체적으로 구축하고 있는 쿠버네티스 클러스터에서는 split-brain에 대한 문제가 완화되기 때문에 쿠버네티스에서 네트워크 장애로 인한 split-brain의 관점은 틀립니다.
이게 끝이 아니고 Galera 클러스터와 같은 레플리케이션 전략들은 majority node에서만 마스터를 선출하여 최종적으로 데이터베이스에 적용되는 커밋은 majority node에서만 일어날 수 있도록 설계되어 있기 때문에 split-brain이 일어나도 상관없을 정도로 안전합니다.
"과연 그럴까요?"
그런 split-brain 상황에서 커밋이 일어나는 노드를 선출하는 과정에서 파드가 재생성 되었을 때 문제가 되는 것입니다. 이 선출 과정이 의미가 없을 정도로 관리가 안되는 것이죠.
이 이유는 이전에 언급했던 것과 일맥상통하네요.
4. 쿠버네티스 클러스터로 로드밸런싱 되는 과정에서 생기는 네트워크 지연 이슈에 대한 관점
이건 반박할 여지가 없는 쿠버네티스의 압도적인 패배입니다.
왜냐하면 네트워크를 한 껍데기로 한 번 더 감싼 구조이기 때문에 네트워크 지연 이슈가 있을 수 밖에 없습니다.
외부 요청이 쿠버네티스 클러스터로 들어오는 과정에서 Istio나 그것과 연결된 Ingress Controller들, 그리고 그것과 연결된 Service들, 그리고 그것과 연결된 Pod들 그리고 그 안에 있는 Container들 이렇게 분리된 네트워크는 지연시간을 높일 뿐이죠.
또한, 네트워크 구현체인 calico의 경우 네트워크망을 내부망으로 만들기 위한 래핑 과정이 있고, 만약 A파드로 가야하는 트래픽이 B파드로 갔다면 B파드로 간 트래픽을 다시 DNS lookup으로 A파드를 찾아내서 A파드로 돌려보내주는 역할을 하기 때문에 트래픽이 여기갔다 저기갔다 두번 세번 이동하게 됩니다.
이 논제는 "도커 컨테이너에 데이터베이스를 띄우는 것이 맞냐?"라는 논쟁과 거의 흡사한데 도커 컨테이너로 단순하게 띄우는건 양반입니다. 쿠버네티스 클러스터는 여러겹으로 네트워크 망을 감싸고 있기 때문에 데이터베이스의 요청이 많은 일반적인 요즘 애플리케이션에서는 적합하지 않은 것이죠.
심지어 어떤 개발자분은 이 이유 하나만으로도 쿠버네티스 클러스터에 RDB를 띄우면 안된다고 주장하시는 분도 있습니다.
그만큼 이 문제는 데이터베이스 요청이 많아지는 요즘 애플리케이션에서 치명적인 약점이라고 볼 수 있습니다.
5. CAP정리에 대한 관점
CAP정리는 Partition Tolerance (장애 허용) 상황에서 C와 A를 둘 다 만족시킬 수 없다는 정리이고 이미 증명이 끝난 이론입니다. CAP정리에 대해서는 아래의 링크에 자세히 정리되어있으니 참고해주시면 감사하겠습니다!
https://coding-review.tistory.com/312
보통 CAP정리는 NoSQL을 이야기할 때 많이 등장하는 개념인데 그 이유는 NoSQL이 분산시스템에 특화되어있기 때문입니다. 하지만 RDB를 분산시스템에 올려놓고 쓰는 경우 CAP를 피해갈 수 없죠.
장애 (Partition) 상황에서 Availability를 만족시키려면 모든 노드가 쓰기작업이 가능해야합니다. 하지만 모든 노드가 쓰기 작업이 가능해지면 Consistency를 만족할 수 없습니다.
왜냐하면 모든 노드가 쓰기 작업이 가능하니까 서로 다른 데이터를 입력할 수 있기 때문에 만약 장애상황에 A노드에서 A작업을, B노드에서 B작업을 하는 경우 A에는 B작업에 대한 결과가 없고 B노드에는 A작업에 대한 결과가 없기 때문에 Consistency를 만족하지 않죠.
반대로 Consistency를 챙기는 순간 Availability를 만족할 수 없습니다. 위의 경우와 완벽히 반대죠. Consistency를 만족시키려면 모든 노드에서 쓰기 작업이 있으면 안됩니다. 하지만 그 순간 Availability를 만족시킬 수 없습니다.
위의 포스팅에서도 언급되어있지만 CP, AP 데이터베이스라고 그 반대급부에 있는 C, A를 무조건 만족시키지 않는 것은 아닙니다. 장애상황이 아니라면 C, A, P를 모두 만족하는 데이터베이스는 널리고 널렸죠.
때문에 CAP정리는 장애 상황일 때만 C, A가 동일선상에 있지 그 외에 상황에선 C, A가 동일선상에 놓여있지도 않습니다. 대표적으로 CP데이터베이스라고 알려진 HBase는 장애 상황일 때 A를 포기하고 그 외에 상황에선 A를 만족합니다. 또한, 대표적인 AP데이터베이스인 Casandra는 장애 상황에서 C를 포기하고 그 외에 상황에선 C를 만족합니다.
그렇기에 Brewer (저는 "브루어"라고 읽는데 사람 이름입니다.) 가 말한 이상적인 분산 데이터베이스는 일반적인 상황에선 C를 만족하지만 장애 상황일 땐 C를 포기하는 데이터베이스라고 말하더군요.
마치며
마지막 CAP에 대한 관점은 사실 억지인 것 같은데 어느정도는 맞는 말이니 적어놨습니다. 쿠버네티스에서 RDB를 배포하면 되냐? 안되냐? 에 대한 논쟁은 DevOps진영에서 아직까지 마땅한 답이 나오지 않은 뜨거운 감자인 것 같습니다.
우선 제 입장은 선배님들 말대로 아직은 시기상조인게 맞는 것 같습니다. 물론 MongoDB나 Redis, Elasticsearch같은 NoSQL 데이터베이스들은 띄우는 것이 가능하다고는 하지만요.
그럼 이번 포스팅도 여기서 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
https://chatgpt.com/share/67111153-aea0-8004-b123-cf0d85ec06fd
쿠버네티스의 분산 합의 알고리즘 (Raft 알고리즘)
최근에 사이드 프로젝트를 하느라 코딩을 많이해서 그런가 요즘은 이론 공부에 집중하게 되더군요. 조금 딴소리지만 처음 공식문서를 읽게 된게 Baeldung의 로컬 트랜잭션과 글로벌 트랜잭션인데 이걸 처음 읽고 충격에 빠지지 않을 수 없었습니다. 이론공부를 한다는게 이렇게 재밌는 일인가? 싶은 생각이 들었었죠.
그 때가 벌써 2년전이네요. 그 이후로 공식 문서를 보는걸 좋아했는데 짧은 영어로 한글자 한글자 해석하면서 알게되는 변태같은 심연의 지식들이 너무 좋더라구요.
이번 포스팅에선 분산 시스템에서 합의를 이뤄 리더를 선출해내는 분산 합의 알고리즘을 공부해보고 정리해봤습니다.
분산 합의 알고리즘
Raft 알고리즘을 본격적으로 들어가기 전에 분산 합의 알고리즘이 어떤 것이며 왜 필요한지 정리하고 넘어가겠습니다.
분산 합의 알고리즘은 말 그대로 분산 시스템에서 합의를 이루어 리더를 선출해내는 알고리즘입니다. 우선 합의를 한다는 말이 가지는 의미는 분산 시스템에서 각 노드들이 투표권을 가지고 리더를 선출한다는 의미입니다.
분산 합의 알고리즘은 분산 시스템에서 상태를 저장해야하는 기술들에서 주로 볼 수 있습니다. 오늘 소개드릴 Raft 알고리즘은 아니지만 MySQL Orchestrator나 Redis Sentinel, Redis Cluster 같은 리더선출 기반 기술들에서 볼 수 있는 알고리즘입니다.
많은 분산 합의 알고리즘이 있지만 그 중에서도 Raft는 이해하기 쉬운 구조로 되어있고 합리적인 선출 방식 덕분에 많은 곳에서 사용중입니다.
보통 분산 시스템에서 상태를 저장해야하거나 리더가 특정 행동을 주도해야하는 경우 유용하게 사용됩니다. 그래서 Zookeeper나 Zookeeper의 의존성을 없애기 위해 Kafka에서 3.3부터 안정화된 KRaft가 바로 Raft를 이용해서 분산 합의를 이뤄낸 경우입니다.
https://www.usenix.org/system/files/conference/atc14/atc14-paper-ongaro.pdf
위의 링크는 2014년에 발표된 Raft 알고리즘의 논문인데요. 이해하기 쉽게 배려넘치는 글은 아니지만 그래도 Raft 알고리즘을 이해하는데 도움이 많이 됐습니다. 논문을 다 보실 필요는 없고 5장만 보시면 될 것 같습니다. 3페이지 정도 되는 분량이라 가볍게 읽기에도 좋을 것 같네요.
Raft 알고리즘
Raft 알고리즘에선 세 가지 중요한 개념이 등장합니다.
• Leader Election
• Log Replication
• Safety
하나씩 정리해보겠습니다.
Leader Election
Raft 알고리즘은 기본적으로 리더를 선출하는 것이 베이스입니다. 이 때 중요한 개념이 바로 term인데요. 한국어로 번역하자면 "임기" 정도가 될 것 같습니다.
이 임기 동안 서버들이 가질 수 있는 상태는 Leader, Fallower, Candidate 이렇게 세 가지입니다. 임기 동안 각 서버들은 랜덤한 timeout을 가지고 있고 이 timeout이 지나면 Candidate이 되며 리더가 되기위한 투표를 진행합니다.
한번 다시 정리해보겠습니다.
• Leader : 리더는 임기 (term) 동안 지속적으로 팔로워들에게 지속적으로 Heartbeat을 보내면서 자신이 아직 리더로서 건재하다고 요청을 보냅니다.
• Follower : 팔로워는 임기 동안 리더로부터 지속적으로 Heartbeat를 받고 만약 Heartbeat를 받지 않은 팔로워는 timeout이 지나면 Candidate으로 격상합니다.
• Candidate : 후보자로 격상된 팔로워가 다른 서버에게 투표를 요청하고 특정 알고리즘을 기반으로 투표를 진행한 뒤 과반수가 리더로 선출하는데 동의하면 리더로 격상됩니다. 이 특정 알고리즘은 뒤에서 소개해드릴 Log Replication에서 자세히 다루도록 하겠습니다.
보통 Heartbeat와 리더 선출을 위한 투표는 수 밀리초에서 수백 밀리초 사이에 벌어집니다. 즉, 리더는 자신이 살아있다는 것을 서버에 알리기 위해 수~수백 밀리초동안 지속적으로 요청을 보낸다는 말인데요. 그럼 이게 서버에 부하가 가진 않을까 걱정이 됩니다.
그래서 Raft 알고리즘에선 RPC 통신을 함으로써 리더 선출과 Heartbeat를 진행할 때 네트워크 부하와 서버 부하를 줄일 수 있게 됩니다.
이에 관해서는 gRPC를 소개한 제 포스팅에서 자세히 설명이 되어있습니다. HTTP/2 기반의 통신으로 헤더 압축, 바디 압축, 멀티플렉싱으로 동기 통신에서 엄청난 성능을 보여주게 됩니다.
물론 Raft 알고리즘에서 사용하는 RPC 통신은 gRPC는 아니지만 RPC 통신이라는 점에서 일맥상통하기에 추천드리는 포스팅입니다.
https://coding-review.tistory.com/589
Raft에선 두 가지 RPC 통신을 하게 됩니다. 바로 RequestVote RPC와 AppendEntries RPC 이렇게 두 가지의 RPC 통신입니다.
논문에서도 이에 대한 자세한 내용은 없고 RequestVote는 리더 선출을 위한 투표용 RPC이고, AppendEntries는 Heartbeat를 위한 RPC 통신입니다.
AppendEntries RPC가 보통 수십 밀리초단위로 Heartbeat를 체크하고 RequestVote는 200밀리초 정도 걸리는 조금 긴 RPC 통신입니다.
Log Replication
Log Replication은 어떤 논리를 기반으로 리더를 선출할 것인지에 대한 Raft 알고리즘의 알파이자 오메가입니다.
리더는 팔로워에게 Log (이하 로그) 라는 정보를 전파하게 되는데 이 로그는 우리가 아는 백트래킹용 로그가 아니고 해당 시스템이 어떤 임무를 수행해야하는지 적혀있는 TODO list같은 느낌의 무언가입니다.
이렇게 로그를 관리하고 전파하는 이유는 후에 리더에 장애가 생겨 죽어버리면 다른 팔로워가 리더가 되면서 리더의 임무를 이어서 해야하는데 이전 리더가 하던 일을 이어받아야 하기 때문에 반드시 필요합니다.
리더는 과반수의 팔로워들이 최신 로그를 가지게 되면 해당 로그를 커밋함으로써 로그 상태를 관리합니다.
Raft 알고리즘에서 Log Replication으로 로그를 관리하지만 그 과정에서 일관성이 매우 중요한데요. Raft에선 이 일관성을 관리하기 위해 로그를 특정 규칙에 의해 관리가 되어야합니다.
로그를 관리하는 과정을 이제 소개하려고 합니다. 그러기 위해선 앞서 설명한 term과 새로 등장하는 index에 대한 깊이있는 이해가 필요합니다.
Safety
• term : 0부터 시작하는 term은 현재 리더의 임기를 보관하는 정보입니다. 이 정보를 기반으로 해당 로그가 최신 상태인지 구버전의 상태인지 확인할 수 있습니다.
예를 들어서 리더 서버가 현재 3이고 이 상태를 공유하는데 만약 2인 서버가 있다면 이 서버는 아직 로그가 전파되지 않았다는 의미이므로 구버전이라는 의미입니다.
• index : 이 index는 kafka로 치면 offset과 비슷한 개념입니다. 현재 리더가 처리하고있는 명령의 순서이죠. 이 index가 작다는 이야기는 구버전이라는 의미입니다. 왜냐하면 현재 자신이 보유한 index가 높으면 그만큼 최신이라는 의미이기 때문이죠.
이를 기반으로 투표는 다음과 같은 순서로 진행됩니다.
• 리더에 장애가 발생해 다운되었다.
• timeout이 끝난 팔로워가 후보자가 된다.
• 후보자는 다른 팔로워에게 투표를 권고하는 요청을 보낸다 (RequestVote RPC)
• 팔로워가 후보자의 term과 index를 확인한다.
• term과 index를 기반으로 팔로워가 투표한다.
이제 term과 index를 기반으로 투표하는데 어떤 기준으로 투표하는지에 대해서 정리해보겠습니다.
팔로워는 다음과 같은 규칙을 따릅니다.
• 자신보다 term이 같거나 높은 후보자에게 투표를 던진다.
• 만약 term이 같다면 index가 높은 후보자에게 투표한다.
• term이 낮거나 term과 index모두 자신보다 낮은 후보자에게는 투표하지 않는다.
이로써 리더가 되는 후보자들은 모두 최신의 로그를 가지고 있어야 리더로 선출될 수 있습니다.
이 과정을 틀린 가정을 기반으로 모순을 도출해내 이 틀린 가정이 어떻게 틀렸고 어떻게 일관성을 유지하고 있는지 정리해봤습니다.
틀린 가정 : 리더T가 있고 리더T에 장애가 발생한 상황에서 후보자U가 새로운 리더로 선출되었다는 과정에서 후보자U는 리더T보다 로그가 뒤쳐져있으면서 리더로 선출되었다는 가정입니다. 우리는 앞서 term과 index로 리더로 선출될 후보자들은 항상 최신의 로그를 가지고 있다고 이야기했습니다. 이 가정 속에선 후보자U가 리더T보다 로그가 뒤쳐져 있는 상태에서 리더가 될 수 없는 것을 증명하려고 합니다.
• 리더T가 term이 3이고 index가 10인 상태에 대한 로그A를 커밋하고 장애가 발생해 죽어버린 상황.
• 리더T가 로그A를 커밋했다는 것은 기존 서버들 중 과반수가 로그A를 가지고 있다는 뜻이 됩니다.
• 이 상태에서 후보자U가 리더로 당선 되었다는 것은 과반수로 후보자U에게 투표를 던졌다는 뜻입니다. 이 때 후보자U를 뽑은 서버들을 voter라고 칭하겠습니다.
• 리더T가 과반수에 로그A를 복제했기 때문에 voter는 로그A를 가지고 있습니다.
• voter는 로그A를 지우지 않습니다. 왜냐하면 팔로워는 로그를 쓰지 않고 리더 또한 기존 로그를 덮어쓰지 않기 때문입니다.
• voter가 후보자에 투표하기 위해선 후보자가 voter보다 높은 term을 가지고 있거나 같다면 높은 index를 가지고 있어야합니다.
• 그러므로 후보자U는 낮은 버전의 로그를 가지고는 리더에 당선될 수 없습니다.
이 과정은 논문에서 설명하는 과정을 제가 조금 쉽게 풀어서 설명한 것인데 논문에서 설명한 방식 자체가 이해하기 쉬운 버전이 아니라 나름 쉽게 만들었는데도 이해하기가 쉽지 않네요..
쿠버네티스가 Raft 알고리즘을 활용하는 방법
Raft 알고리즘은 리더가 무언가를 해야하는 Zookeeper나 Kafka같은 시스템에서 주로 사용될 것 같지만 분산 시스템에서 상태를 일관성있게 유지해야하는 쿠버네티스에서도 Raft 알고리즘을 사용합니다.
쿠버네티스는 etcd에서 Raft 알고리즘을 사용하는데 여기서 의문이 들었습니다. Raft 알고리즘은 분산 시스템에서 상태를 일관성 있게 유지해야하는데 etcd는 Control Plane에 하나만 존재하는데 어떻게 분산으로 관리한다는거지? 라는 생각이요.
처음엔 멀티 클러스터에서의 etcd를 생각했지만 그게 아니고 etcd는 쿠버네티스 클러스터 내에 물리적으로는 하나만 존재하지만 논리적으로는 여러개의 노드에 나눠져 존재하고 있습니다.
이 etcd에서는 쿠버네티스 클러스터 내부의 리소스 상태나 어떤 파드가 생성되어있는지 어떤 노드에 파드를 배치하면 안되는지와 같은 상태를 반드시 일관성있게 관리해야합니다.
때문에 논리적인 etcd가 하나 죽더라도 물리적인 etcd에 영향을 끼치면 절대절대 안되는 상황이죠. 만약 물리적인 etcd에 영향을 끼친다면 쿠버네티스 클러스터 전체가 영향을 받아 온전하게 동작하지 않는 상태가 발생하기 때문에 etcd가 반드시 쿠버네티스 클러스터 내부에서 살아있어야합니다.
이 때 Raft 알고리즘이 도움이 됩니다. etcd가 Raft 알고리즘으로 쿠버네티스 클러스터 내부의 상태를 일관성 있게 관리하여 쿠버네티스 클러스터를 죽지않도록 관리하는데 Raft 알고리즘을 사용하는 것이죠.
etcd는 쿠버네티스에서 뇌같은 존재입니다. 단순히 쿠버네티스 내부의 데이터베이스가 아니고 etcd에 적재된 정보를 기반으로 쿠버네티스가 어떻게 행동할지에 대한 기준이 됩니다.
그렇기에 etcd는 논리적으로 반드시 세개 이상 존재하는 것이 권장되고 과반수 투표를 위해 홀수개로 유지하는 것이 권장됩니다.
마치며
사실 Raft 알고리즘에 대한 포스팅 내용인데 쿠버네티스 카테고리에 넣은 것은 Raft 알고리즘을 어디 따로 빼둘 카테고리가 없었기 때문입니다.
쿠버네티스가 Raft 알고리즘을 이용해서 etcd를 관리하고 있기 때문에 명분이 생겨 쿠버네티스 카테고리에 Raft 알고리즘을 소개해봤습니다.
근데 내용이 진짜 어려웠습니다. Raft 알고리즘 말고 Paxos라는 알고리즘은 이론적으로는 완벽에 가까운 합의 알고리즘이지만 굉장히 어렵다고 소문이 났는데 저는 Raft 알고리즘도 머리 터질뻔 했습니다... Paxos는 어느정도일지 쳐다도 보기 싫네요.
이런 깊이있는 내용은 지피티가 헛소리를 많이 하기에 논문을 바탕으로 쓰게 되었습니다. 확실히 논문을 읽으니 제대로 공부했다는 기분도 들고 좋네요.
이번 포스팅은 여기서 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
Apache Kafka (with Chat GPT)
요즘 MicroService Architecture (이하 MSA) 에 관심이 생겨서 이것저것 알아보던 중에 Kafka라는 서비스를 알게되었습니다.
쉽게 얘기하면 각각 떨어져있는 서비스들 사이에서 메시지를 주고받을 수 있도록 하는 서비스입니다.

기존 카프카가 없을 때 메시지를 보내려면 위와 같이 모든 컴포넌트가 메시지를 쏴줘야 하는 문제가 있었습니다. (딱봐도 문제가 있게 생겼습니다) 이렇게 데이터를 보내게 되면 데이터 복잡성이 증가하겠죠.

하지만 카프카가 도입되면서 메시지를 보내는쪽 (Publisher) 과 메시지를 받는쪽 (Subscriber) 을 나누고 카프카를 통해 메시지를 주고받으면서 조금 더 편하게 데이터를 주고 받을 수 있게 되었습니다.
이번 포스팅에선 메시지 브로커 (메시지 큐잉 서비스) 가 뭔지, 카프카란 대략적으로 어떤 것을 말하는 것인지, 카프카와 비슷한 서비스는 뭐가 있는지, 그리고 이후에 동기 비동기 네트워킹을 채택하고 있는 다양한 서비스들을 알아봅니다.
※ GPT야 이것좀 알려줘 카테고리는 원래 제가 작성하는 주제 (자바, 스프링, CS, 알고리즘) 와 다른 하지만 평소에 궁금했던 내용을 작성합니다. GPT가 답변한 내용은 ~이다 체로, 제가 작성한 부분은 ~습니다 체로 작성됩니다.
0. 메시지 브로커란? (메시지 큐잉 서비스)
메시지 브로커 서비스는 메시지의 교환을 용이하게 함으로써 분산 시스템이나 컴포넌트들 사이에서 커뮤니케이션을 가능하게 하는 미들웨어의 타입 중 하나이다. 메시지 브로커 서비스의 목적은 메시지의 생산자와 소비자를 서로 타이트하게 coupled 되는 것 없이 독립적으로 작동하게 되면서 decoupling 하는 것이다.
여기서 잠깐! decoupling이 뭐야?
decoupling (이하 디커플링) 이란 소프트웨어 시스템의 서로 다른 컴포넌트들이나 서브시스템들 사이의 의존성을 줄이는 방법이라고 할 수 있습니다.
디커플링은 전형적으로 깨끗한 인터페이스나 컴포넌트들 사이에서 깔끔한 커뮤니케이션 프로토콜을 정의하거나, 컴포넌트들 사이에서 직접적인 의존성을 최소화함으로써 달성할 수 있습니다.
디커플링의 목적은 시스템간 영향을 주는 것 없이 새로운 컴포넌트를 추가하거나 교체하거나 변경하기 더 쉽게 만듦으로써 유연하게, 유지하기 쉽게, 확장성있게 향상시키는 것입니다.
디커플링은 또한 시스템 장애에 대한 리스크를 줄여주는데 그 이유는 하나의 컴포넌트에서의 장애가 다른 컴포넌트들로 전파되는 것이 적기 때문입니다.
디커플링은 흔히 소프트웨어 아키텍처 설계에서 주로 사용됩니다. microservice와 같은 곳에서 주로 사용되는데 그곳에서 각각의 서비스들은 잘 정의된 인터페이스를 통해 다른 서비스와 독립적으로 커뮤니케이션하거나 동작하도록 설계됩니다.
디커플링은 또한 Object-Oriented Programming (OOP) 같은 소프트웨어 엔지니어링 분야에서 사용되기도 합니다. OOP에서 캡슐화 원리에 의해 정보를 숨기는 것이 클래스 사이의 의존성을 줄이기위해 사용됩니다.
다시 메시지 브로커로 돌아와서 메시지 브로커 서비스에서 메시지들은 보내는 사람 (sender) 에 의해 생산되고 브로커에 의해 받게 된다. 브로커는 또한 메시지 전송이나 필터링, 라우팅, 그리고 보안같은 특징들을 제공할 수 있다.
메시지 브로커 서비스는 다양한 방법으로 구현될 수 있는데 예를 들어서 소프트웨어 컴포넌트를 혼자 내버려 둠으로써 설계할 수도 있고, 애플리케이션 안에 embedded 한 라이브러리를 만들 수도 있고, vendor에 의해 제공되는 클라우드 베이스의 서비스로도 구현할 수 있다.
메시지 브로커 서비스는 흔히 MSA나 event-driven 시스템들이나, 데이터 프로세싱 시스템같은 컴포넌트들 사이에서 비동기 커뮤니케이션을 요구하는 분산시스템에서 사용된다.
메시지의 생산자와 소비자를 디커플링 함으로써 메시지 브로커 서비스는 시스템을 더 유연하게, 신뢰성있게, 확장성있게 설계할 수 있다.
1. 카프카란?
Apache Kafka는 실시간 데이터 파이프라인을 설계하거나 애플리케이션을 스트리밍 하기 위해 사용되는 오픈소스 분산 스트리밍 플랫폼이다.
카프카는 큰 볼륨의 데이터를 핸들링하거나 낮은 레이턴시의 프로세싱을 제공하거나, 데이터 스트림을 전달하기 위해 설계되었다. 카프카의 아키텍처는 서버나 브로커의 클러스터를 통해 쪼개진 분산 커밋 로그에 기반하고 있다.
생산자는 카프카 클러스터에 데이터를 입력하고, 소비자는 클러에터에서 데이터를 읽는다. 카프카는 message retention, pub/sub messaging, stream processing과 같은 특징들을 제공한다.
카프카는 이벤트 프로세싱이나 로그 통합, 메시징 서비스와 같은 실시간 데이터 스트리밍 애플리케이션을 설계하는데 탁월한 선택이 될 수 있다.
2. 카프카와 바슷한 서비스
• RabbitMQ : RabbitMQ는 Advanced Message Queuing Protocol (AMQP) 이나 Streaming Text Oriented Messaging Protocol (STOMP), Message Queuing Telemetry Transport (MQTT) 와 같은 다양한 메시징 프로토콜을 지원하는 분산된 오픈소스 메시지 브로커이다. (무료)
• Azure Event Hubs : 초당 수백만개의 이벤트를 핸들링할 수 있거나 파티셔닝, 자동적으로 확장하는 것과 같은 특징을 제공하는 마이크로소프트 에저 (Azure) 에 의해 제공된 실시간 데이터 스트리밍 플랫폼이다. (유료)
• Google Cloud Pub/Sub : 애클리케이션 사이에서 확장성있고 지속가능한 메시징을 위해 설계된 Google Cloud Platform (GCP) 에 의해 제공되는 메시징 서비스이다. (유료)
• AWS Kinesis : 확장에 있어서 실시간 데이터 프로세싱을 위해 설계된 AWS에 의해 제공되는 실시간 데이터 스트리밍 플랫폼이다. 키네시스는 스트리밍 프로세싱, 실시간 분석, 다른 AWS 서비스들과 통합과 같은 특징들을 제공한다. (유료)
• Apache Pulsar : 높은 수율, 낮은 레이턴시 데이터 스트리밍을 핸들링하기위해 설계된 오픈소스 분산 pub/sub 메시징 서비스이다. Pulsar는 geo-replication, multi-tenancy, 유연한 데이터 retention 정책들과 같은 특징들을 제공한다. (무료)
3. 동기 네트워킹, 비동기 네트워킹
Kafka와 같은 메시지 큐잉 서비스에는 동기 네트워킹, 비동기 네트워킹으로 돌아가는 서비스들로 나눠져있습니다. 이번 섹션에서는 동기 네트워킹과 비동기 네트워킹의 특징과 차이에 대해서 서술합니다.
• 제어의 흐름 (Flow of Control) : 동기 네트워킹에서는 제어의 흐름이 클라이언트에 의해 지시된다. 클라이언트가 요청을 보내면 다음 일을 진행하기 전에 응답을 기다려야한다. 반대로 비동기 네트워킹에선 제어의 흐름이 클라이언트에 의해 지시되지 않는다. 클라이언트는 요청을 보내면 응답을 기다릴 동안 다른 일을 진행할 수 있다. (제어의 흐름이 클라이언트에 의해 지시된다는 의미는 아마도 클라이언트를 통해 제어되는 것인지 아닌지를 말하는 듯 합니다. (Blocking / Non Blocking))
• 성능 : 비동기 네트워킹은 잠재적으로 더 좋은 성능을 제공할 수 잇는데 그 이유는 비동기 네트워킹이 클라이언트의 blocking 없이 동시에 진행될 수 있는 많은 요청을 수행할 수 있기 때문이다. 이러한 것은 특히 네트워크 레이턴시가 높은 상황에서 이익이 될 수 있다. 혹은 레이턴시가 높은 곳에서 클라이언트가 큰 볼륨의 요청을 진행하기 위해 필요한 상황에서 주로 사용될 수 있다.
• 복잡성 : 비동기 네트워킹은 동기 네트워킹보다 훨씬 더 복잡하다. 왜냐하면 비동기 네트워킹이 콜백을 관리하기 위한 추가적인 로직을 요구하고 비동기 실행 동안 일어날 수 있는 에러를 핸들링하기위한 추가적인 로직을 요구하기 때문이다. 반면에 동기 네트워킹은 이해하기 더 쉽고 관리하기 더 쉬운 선형적인 제어의 흐름을 포함하고 있기 때문에 더 쉽다.
• 확장성 : 비동기 네트워킹은 전형적으로 동기 네트워킹보다 더 확장성이 뛰어난데 그 이유는 클라이언트의 blocking 없이 다수의 요청을 동시에 수행하는 것을 허락하기 때문이다. 이러한 상황은 네트워크의 전반적인 성능을 향상시킬 수 있다.
4. 메시지 큐잉 서비스 중 동기 서비스인 것과 비동기 서비스인 것
• Asynchronous Message Broker Service
Apache Kafka
• RabbitMQ
• Apache Pulsar
• Synchronous Message Broker Service
ActiveMQ
• Google Cloud Pub/Sub
• AWS Simple Queue Service (SQS)
5. 동기 비동기를 선택했을 때의 장단점
• 비동기 메시징 장점
확장성 : 비동기 메시지 브로커들은 큰 볼륨의 데이터를 핸들링하기 위해 설계되었고 수평적으로 추가적인 로드를 핸들링하기 위한 확장을 할 수 있다.
• 장애 대응 (fault tolerance) : 비동기 메시지 브로커들은 전형적으로 데이터 레플리케이션이나 자동적으로 failover (장애가 발생했을 때 자동적으로 시스템이 파악하는 것 혹은 서버가 내려가는 것) 하는 거소가 같은 특징을 가지고 장애를 대응하기 위해 설계되었다. 이러한 특징은 장애가 난 상황에서 데이터를 잃어버리지 않는 것을 보장해줄 수 있다.
• 디커플링 (Decoupling) : 비동기 메시지 브로커는 소비자나 생산자를 독립적으로 실행하는 것을 도와주면서 데이터의 소비자나 생산자를 디커를링하는 방법을 제공한다. 디커플링은 시스템을 유연하게 향상시킬 수 있고 컴포넌트 (요소) 간에 의존성을 줄여줄 수 있다.
• 성능 : 비동기 메시지 브로커는 데이터를 처리하는 것에 대해 특히 데이터의 생산자나 소비자가 많은 경우 높은 성능을 제공해줄 수 있다.
• 비동기 메시징 단점
복잠성 : 비동기 메시지 브로커는 동기 메시지 브로커에 비해 특히 많은 데이터 생산자나 소빚자가 있는 경우에 관리하거나 구축하는데 더 복잡할 수 있다.
• 레이턴시 : 비동기 메시지 브로커는 특히 데이터가 빠른 속도로 진행될 필요성이 있는 상황에서 추가적인 레이턴시를 야기할 수 있다.
• 신뢰성 : 비동기 메시지 브로커는 요소들간 뎅터 통신을 위한 메시지 패싱 (message passing) 에 의존한다. 그리고 그것은 잠재적인 메시지 유실과 불일치를 야기할 수 있다.
동기 메시징의 장단점은 비동기 메시징 장단점의 완전 반대입니다.
마치며
여기까지 Chat GPT에게 Kafka에 대해서 전체적으로 물어봤습니다. MSA를 구현하기 위해 필요한 기술이라는 것만 알고 있었지 이렇게 자세하게는 잘 몰랐던 것 같습니다.
아직 이 분야는 제가 몰라도 되는 부분이긴 하지만 평소에 MSA에 관심이 많았던 터라 이번 기회에 전반적인 내용에 대해서 공부하게 되었습니다.
추후에 Kafka를 이용해 MSA를 구현해보는 실습을 꼭 해보고싶습니다. 그러기위해선 docker라던가 kubernetes와 같은 기술들도 잘 알아야겠죠.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 보내세요~
출처
Chat GPT
아파치 카프카 (개념)
저번 포스팅에서 파트2를 시작하면서 메세지 브로커의 장을 열었습니다. 메세지 브로커를 이용해서 서버간 통신을 조금 더 매끄럽게 진행할 수 있다는 것을 알았고 어떤 모델이 있는지 알았습니다.
이번엔 Pub/Sub 메세징의 대표주자 아파치 카프카 (Apache Kafka) 에 대한 개념을 잡아보도록 하겠습니다.
구글 트렌드

위의 그래프는 구글 트렌드에서 검색한 Kafka와 RabbitMQ에 대한 트렌드 추이입니다.
기본적으로 Kafka에 대한 검색량이 높고 특징으로는 2022년에 들어오면서 갑자기 70퍼센트대로 올라왔다는 것이 눈에 띕니다.
이는 한국에서 보이는 추이이고 이번엔 글로벌로 보겠습니다.

글로벌도 상황은 비슷해보이네요. 역시 2022년에 카프카에 대한 수치가 올라간 것이 특징입니다. RabbitMQ도 소폭 상승했네요.
여기에 Redis까지 넣으면 Redis는 Pub/Sub 메세징으로도 쓰이지만 캐싱으로 더 많이 쓰이기 때문에 혼란을 줄 우려가 있어 넣지 않았습니다.
이렇게 카프카가 많이 쓰이는 이유가 뭘까요? Pub/Sub 메세징이라서? 그 이유에 대해서 알아보도록 하죠.
Apache Kafka
카프카의 특징은 구글링해보시면 지겹도록 보실 수 있습니다. 비동기 통신을 지원한다느니 논블락킹 I/O라느니 복사 붙여넣기 한 수준으로 다 똑같은 내용을 포스팅해서 저는 그 내용은 넣지 않겠습니다.
저는 그럼 어떤 내용을 포스팅 할 것이냐.
바로 카프카에 대한 용어 완전 정복과 특징, 그리고 아키텍처를 얘기해볼까합니다.
카프카에서 사용하는 용어
카프카에서 주로 어떤 용어를 사용하는지 간단하게 개요를 확인하고 이후 예시를 통해 딥하게 들어가보도록 하겠습니다.
• producer or publisher : producer 혹은 publisher (이하 publisher)는 메세지를 보내는 주체입니다. 간단하게 "메세지 보내는 놈"이라고 생각하시면 편합니다.
• consumer or subscriber : consumer 혹은 subscriber (이하 subscriber)는 메세지를 받는 주체입니다. 간단하게 "메세지 받는 놈"이라고 생각하시면 좋습니다.
• topic : topic은 메세지를 소비하는 consumer (혹은 subscriber) 의 마빡에 붙여놓는 라벨입니다. 어떤 종류의 메세지를 소비할 것인지 구별해놓는 용도로 사용합니다. 일단 이정도로 설명하고 뒤에 예시에서 더 자세하게 설명하도록 하겠습니다.
• consumer group : consumer group은 consumer를 관리하기위한 더 큰 라벨입니다. consumer group은 카프카에서 관리하고 뒤에 설명할 로드 리밸런싱에 대해서 자주 등장하게 될 내용입니다.
• partition : partition은 메세지가 이동하는 하나의 queue라고 생각하시면 됩니다.
이제 하나씩 뜯어볼까요?
이 용어 중 중요한 것은 바로 topic입니다. 왜냐하면 다른 것들은 이해하기 쉬운데 topic은 직관적으로 이해하기 힘들거든요.
publisher, subscriber, topic, consumer group을 예시를 들어 설명해보겠습니다.
상황)
우리가 뉴스를 구독한다고 가정해볼까요? 저는 개인적으로 IT기사를 보는 편입니다. IT기사들이 올라오면 그때그때 알람을 받으면 좋죠.
기자가 뉴스 기사를 쓰고 수많은 기자분들 중에 IT 뉴스 기사를 쓰시는 분이 있으면 작성과 동시에 저한테 알람이 올겁니다. 이는 우리 생활에 흔히 있는 일이죠.
상황은 끝입니다. 이 두 문단으로 publisher, subscriber, topic, consumer group을 다 설명했습니다.
이제 하나씩 매치업해보면서 용어를 이해해봅시다.
• publisher는 기자입니다. 뉴스 기사를 쓰는 사람이죠.
• subscriber는 저 자신입니다. 뉴스 기사를 받는 사람이죠.
• topic은 IT 뉴스입니다.
• consumer group은 뉴스입니다.
자 이렇게 하면 이해하시기 편할겁니다.
publisher는 기자입니다. 뉴스 기사를 쓰는 사람인데 publisher는 특정 subscriber 즉 특정 구독자를 향해서 뉴스 기사를 쓰는게 아닙니다.
기자가 저만을 위해서 뉴스 기사를 쓰는게 아니라는 말이죠. 수많은 기자들 중 특정 topic으로 기사를 씁니다. 즉, publisher는 topic만 알고 누구한테 이 뉴스 기사가 소비되는지 모릅니다.
subscriber는 저 자신이죠. 구독자는 특정 topic만 구독해놓습니다. 그럼 그냥 저절로 topic에 해당하는 뉴스기사가 저한테 옵니다. 저는 어떤가요? 누가 썼는지 관심이 없죠. 그냥 IT 뉴스 기사이기만 하면 됩니다.
topic은 IT뉴스, 경제뉴스, 정치뉴스, 스포츠뉴스 이런식으로 각 주제를 이야기합니다. 구독자는 이 topic만 구독할 뿐이죠. 즉, topic은 publisher가 생성한 메세지가 어떤 subscriber로 갈지 정합니다.
카프카는 consumer group이라는 정보를 바탕으로 consumer 즉 메세지 소비자들을 구분해놓습니다.
이렇게 구분해놓으면 뭐가 좋을까요? 어디로 어떤 메세지가 가는지 라벨링을 해놓을 수 있습니다. 그럼 카프카 입장에서도 관리하기 편하겠죠?
이를 그림으로 설명해보겠습니다.

이 그림은 카프카의 아키텍처를 간단하게 도식화한 것입니다.
제가 partition에 대한 설명은 생략했는데, 카프카에서 각 subscriber로 이어지는 이 선들이 하나의 partition이라고 보시면 됩니다. 메세지가 이동하는 통로 내지는 queue라고 보시면 됩니다.
partition의 특징으로는 하나의 partition은 하나의 subscriber를 가집니다. 반드시!
반대로 subscriber는 "하나 이상"의 partition을 가질 수 있죠. 즉 1:N관계이지만 0...1이 아닌 그냥 1입니다. 0일수는 없습니다. 이 관계는 바로 직후에 이야기할 로드밸런싱과 장애회복에서 주요한 내용이 됩니다.
이제 카프카에서 주로 사용하는 용어에 대해서 대략적인 이해가 있으니 카프카의 주요한 특징에 대해서 알아보도록 하겠습니다. 바로 장애회복과 로드밸런싱입니다.
상황)
우리의 서비스는 주문과 결제 서버를 가지고 있습니다. 한번 상황을 가정해보도록 하죠.
*주문
서버 : 2대
consumer group : Order
topic : CustomerOrder
consumer : C1, C2 (C1, C2는 각각의 서버이자 메세지를 처리하는 consumer가 됩니다. 위에선 subscriber라고 했지만 여기서 consumer group이라는 이름을 위해 consumer로 통일하겠습니다.)
partition : P1, P2 (각 파티션은 숫자에 맞는 consumer와 연결되어있습니다.)
*결제
서버 : 2대
consumer group : Payment
topic : CustomerPayment
consumer : C3, C4 (이하 동일)
partition : P3, P4 (이하 동일)
장애회복
카프카에는 장애상황에 대처할 수 있는 능력을 가지고 있습니다. 예를 들어서 이런 상황을 가정해보도록 하죠.
상황속 상황)
갑자기 주문이 몰려 주문 서버 한대가 죽어버렸다.
이때 생기는 문제는 뭘까요?

메세지가 유실되면 publisher가 보낸 데이터는 날아가게 되고 아무런 응답도 받지못해 붕뜬 상태가 될 수 있습니다.
주문을 위해 주문요청을 넣었는데 아무런 응답이 없게 되는것이죠.
이걸 조금 더 단순화하면 다음과 같습니다.

이 상황에서 카프카는 장애가 난 서버에게 가는 partition을 즉시 살아있는 다른 consumer group에 붙입니다. 즉, partition이 통합되는 것이죠.
이제 위의 상황은 장애시 이렇게 바뀌게 됩니다.

이렇게 바뀜으로써 장애상황에 안전하게 대응할 수 있게 됩니다.
이런 장애상황에 즉시 partition을 리밸런싱할 수 있는 이유는 바로 consumer group이 있기 때문입니다. consumer group은 groupId로 관리하는데 이 groupId를 보고 즉시 다른 consumer로 partition을 할 수 있는 것이죠.
로드밸런싱
메세지 브로커도 엄밀히 따지면 하나의 서버이고 때문에 이 미들웨어 서버로 들어오는 메세지들을 각기 다른 consumer에게 전달하는 것은 일종의 로드 밸런싱으로 보일 수 있죠.
하지만 카프카의 로드밸런싱은 조금 다릅니다.
상황속 상황)
우리는 장애 상황을 한번 맛본 뒤로 확장성의 필요성을 깊이있게 깨달았습니다. 우리 서비스가 커짐에 따라 결제는 괜찮은데 주문 서버가 좀 힘들어합니다. 그래서 우리는 고심끝에 주문 서버를 네대로 증설하기로 결정했습니다.
이제 상황은 이렇게 바뀌었습니다.
*주문
서버 : 4대
consumer group : Order
topic : CustomerOrder
consumer : C1, C2, C5, C6
partition : P1, P2, P5, P6
*결제
서버 : 2대
consumer group : Payment
topic : CustomerPayment
consumer : C3, C4
partition : P3, P4
이 상황에서 카프카에게 파티션이 늘어났음을 알려줘야합니다. 이는 카프카의 설정에서 바꿔줄 수 있습니다. 기존 partition을 두개에서 네개로 늘려주면? 카프카가 늘어난 consumer의 groupId를 보고 해당 consumer group으로 늘어난 partition 두개를 새로 편성해줍니다.
아키텍처 설계 with 클라우드
우리는 카프카의 장애회복과 로드밸런싱을 이용해서 안전하게 서비스를 운영할 수 있게 되었습니다. 하지만 사장님이 매우 화나신 것 같네요.
사장님 : 아니 지금도 서버 비용이 만만치않은데 서버를 두대나 늘리겠다고? 그 돈은 땅파면 나오나?
음... 맞는 말입니다. 트래픽을 분석해보니 주문이 폭주하는 시간대는 오후 7시에서 9시사이이고 다른 시간에는 평범하게 서버 두대로도 수용가능할 것으로 보입니다.
이걸 클라우드 아키텍처로 설계해봅시다.
이 내용은 실습에서 한번 해볼 예정이라 간단하게 개요만 설명하도록 하겠습니다.
준비물
• ECS or EKS : 서버를 유연하게 증설하고 폐기할 수 있는 시스템인 ECS와 EKS를 준비합니다. (저는 실습에서 ECS fargate를 이용할 예정입니다.)
• ASG : ECS와 EKS가 어떤 서버를 늘려야하는지 알 수 없으므로 Auto Scaling Group을 이용해서 EC2 인스턴스를 준비해둡니다.
• Cloud Watch Event and Lambda : 서버의 CPU 사용률이 90퍼센트가 넘으면 이를 감지하고 다른 서비스로 전파해주는 역할이 필요합니다. Cloud Watch Event를 이용해서 CPU의 사용량을 모니터링하고 90퍼센트가 넘으면 Lambda로 API를 보내도록 해야합니다.
• EC2 : 그냥 서버죠 뭐... 적당한 사양으로 준비해줍니다.
• API : 이 API는 카프카의 설정을 바꿔주는 API입니다. partition은 수동으로 늘려줘야하기 때문에 이 API를 받으면 카프카의 partition을 두개에서 네개로 늘려줍니다.
자 이제 준비물은 끝났습니다.
상황속 상황)
오후 7시가 되고 CPU사용량이 점점 늘어납니다.
60퍼...70퍼...80퍼...90퍼!!!
Cloud Watch가 서버의 CPU 사용량이 90퍼센트가 넘었다는 알람을 보내면서 Lambda를 호출합니다.
Lambda는 카프카의 partition을 네개로 변경하는 API를 호출함과 동시에 ASG에 트리거를 작동시킵니다.
카프카의 partition이 네개가 되면서 주문 서버의 groupId인 Order를 확인하고 partition을 새로 증편해줍니다. 동시에 ASG가 ECS의 클러스터와 인스턴스를 증가시키면서 서버를 두대 증설합니다.
오후 10시가 되어서야 서버의 CPU가 30퍼센트 아래로 내려가면서 상황은 종료됩니다.
서버의 CPU가 30퍼센트가 되면서 Cloud Watch Event가 발동되고 Lambda를 호출하여 카프카의 partition을 다시 두개로 줄여주는 API를 호출합니다.
동시에 ECS는 CPU가 30퍼센트가 된 것을 보고 서버 두개를 폐기해버립니다.
이를 그림으로 표현하면 다음과 같습니다.

이왕 그리는김에 클라우드 아키텍처도 한번 볼까요?

여기서 카프카를 SQS로 바꿀 수도 있을 것 같은데... 이 부분은 일단 카프카를 실습해보고 실험해보겠습니다.
마치며
이렇게 카프카의 용어인 publisher, subscriber, topic, consumer group, partition에 대해서 알아보고 카프카의 특징인 로드밸런싱과 장애회복도 알아봤습니다.
그리고 이후 실습으로 이 아키텍처가 가능한지 테스트도 해볼 예정입니다.
다음 포스팅에선 카프카를 가지고 실제로 실습을 해보도록 하겠습니다.
메세지 브로커의 근심과 걱정
마이크로 서비스와 메세지 브로커는 뗄 수 없는 사이입니다. 서로 다른 도메인이 여러개의 서버로 나눠져 있는 상황에서 모든 서버에 동일하게 데이터를 전달해야 하는 경우에 메세지 브로커만한게 없죠.
그렇기에 모놀리식에서는 메세지 브로커를 사용하는게 오버엔지니어링이 되기도 합니다.
이번 포스팅에서는 메세지 브로커의 근심과 걱정에 대한 내용을 공부해보고 정리해봤습니다.
메세지 브로커
https://coding-review.tistory.com/504
메세지 브로커는 위의 포스팅에 간략하게나마 정리가 되어있습니다. 저는 카프카에 대해서 공부했지만 대부분의 메세지 큐잉 서비스들이 비슷한 느낌이 있어서 대략적인 내용을 공부하기엔 좋았습니다.
이번 포스팅에선 메세지 브로커에 대해서 간략하게만 짚고 넘어가도록 하겠습니다.
Pub/Sub 구조와 Topic
메세지 브로커에서 빼놓고 등장하는 개념이 Pub/Sub 메세징인데요. 메세지를 보내는 입장인 Publisher와 메세지를 받는 입장인 Subscriber가 메세지를 주고받는 아키텍처를 말합니다.
이 때 메세지를 보내는 사람과 메세지를 받는 사람이 알고 있어야 하는 것이 바로 Topic입니다.
이는 간단한 예시로 설명이 가능한데 바로 뉴스기자와 구독자입니다.
다른 얘기지만 저는 요즘 AI와 관련된 뉴스기사를 재밌게 보고 있는데 3~4일에 한번씩 기사를 읽으면서 트렌드를 따라가고 있는 편입니다.
하지만 뉴스 기사는 여러가지 "토픽"이 있죠. 스포츠, 게임, 경제, 연예, 기술 등등이요. 뉴스 기자분들은 열심히 본인의 분야에서 기사를 쓸겁니다. 여기서 기자분들이 바로 Publisher이죠.
독자 (Subscriber) 는 말그대로 구독자이고 이 독자들은 자신이 원하는 "주제" (Topic) 의 뉴스기사만을 보고싶어합니다.
저만하더라도 AI만 보고싶으니 다른 주제들은 보고싶지 않거든요. 그럼 저는 AI라는 주제를 구독합니다. 그럼 AI에 관련된 뉴스기사가 뜰때마다 저에게 알람이 올 수 있겠죠? 그럼 독자들 입장에서 굳이 계속 휴대폰을 열어보지 않아도 되니까요.
자 여기까지 Pub/Sub 구조와 Topic에 대해서 모두 설명이 끝났습니다.
메세지 브로커의 Publisher, Subscriber, Topic와 모두 치환 가능합니다.
Publisher는 기자, Subscriber는 독자, Topic은 주제, 그럼 메세지 브로커는? 네이버뉴스나 AI타임즈같은 플랫폼이 되겠네요.
메세지를 어떻게 주고받는지는 위에 링크에 자세히 설명되어있으니 참고해주세요!
메세지 브로커의 근심
메세지 브로커가 가진 근심이 뭘까요? 보통 메세지 브로커를 사용한다면 이런 경우일 가능성이 높습니다.
• 메세지를 보낸다
• 메세지를 받는다
• 데이터베이스에 어떤 작업 (INSERT, UPDATE, DELETE) 을 진행한다
• 이후 다시 메세지를 보낸다.
• 메세지를 받는다
• ....
그런데 이때!
데이터베이스 작업하던 도중 예측 가능한 혹은 예측 불가능한 예외가 터져서 롤백이 됐습니다. 그럼 메세지는 어떡하죠?
물론 당연히 보내면 안됩니다.
어떻게요?
OutBox Pattern
메세지를 보내기 전에 성공했다는 것을 확인할 수 있는 정보를 어딘가에 저장하고 있다가 성공했으면 그 때 메세지를 보내면 됩니다.
의외로 간단하네요.
OutBox 패턴을 구현할 수 있는 방법이 두 가지 있습니다. 바로 Polling Publisher Pattern과 Transaction Log Tailing Pattern입니다.
Polling Publisher Pattern
이 방법은 구현하기는 간단하지만 몇가지 단점이 있는 방법입니다. "어딘가에 저장"이라는 단어에서 직관적으로 느낄 수 있는 것인 데이터베이스 테이블에 저장하는 방법입니다.
데이터베이스에 값을 쓰면서 동시에 이 임의의 테이블 (이하 OutBox 테이블) 에 값을 쓰는겁니다. 이 값이 큐에 들어있는 메세지가 될 수도 있고, 아니면 실제로 저장되는 데이터일 수도 있죠.
그리고 메세지를 보내기 전에 이 OutBox 테이블에 있는 값을 읽어서 있으면 (성공하면) 메세지를 보내면 됩니다.
이 방법은 굉장히 구현하기 간단한 반면 큰 단점이 존재합니다. 바로 메세지를 보내기 전에 두번의 쿼리가 발생한다는 것입니다.
두번의 쿼리가 뭐 어때서 그냥 시원하게 보내면 되지.. 싶다가도 메세지가 한두개가 오는게 아닌걸 생각해보면 리소스가 꽤나 많이 들어가는 작업이라는 것을 알 수 있습니다.
이 단점을 해결한 것이 Transaction Tailing Pattern입니다.
Transaction Tailing Pattern
이 패턴의 이름에서 유추할 수 있듯이 트랜잭션을 따라가면서 어떤 값이 바뀌었는지만 확인하는 방법입니다.
데이터베이스에는 테이블을 업데이트하기 전 (INSERT, UPDATE, DELETE 전) 트랜잭션 로그라는 것을 기록합니다. 우리에겐 WAL로 잘 알려진 이것은 다른 말로 트랜잭션 로그라고도 불립니다.
이 로그를 추적하면서 변경된 값만 보면서 변경된 값이 있으면 그것을 보고 메세지를 보내는 방식입니다.
이를 그림으로 표현하면 다음과 같겠네요.

그런데 이 단점이 없어보일 것 같은 방법에도 단점이 있는데 바로 구현하기가 굉장히 힘들다는 것입니다. 트랜잭션 로그를 추적해야한다는 것을 듣기만해도 난이도가 헬일 것 같은데 추가적인 작업이 남아있습니다.
메세지가 정상적으로 전송되면 메세지를 지워줘야하고 트랜잭션 로그를 추적해서 겨우 메세지를 보내놨더니 굉장히 빈번하게 발생하는 네트워크 오류때문에 메세지가 전송되지 않으면 재전송하는 로직도 필요합니다.
이처럼 메세지 브로커와 트랜잭션을 묶는다는 것은 굉장히 복잡도가 올라갑니다.
이를 구원해줄 해결사가 등장합니다. 바로 PostgreSQL입니다.
젠장, 또 PostgreSQL이야
뜬금없이 다른 RDB도 많은데 PostgreSQL이냐면 위의 많은 패턴에서 겪는 높은 복잡도를 해결해주는 키를 가지고 있기 때문입니다.
바로 PGMQ (Postgres Message Queue) 입니다.
https://github.com/tembo-io/pgmq
PGMQ의 깃허브 문서를 보면 이게 진짜 되나싶을정도로 놀랍긴한데... (실습을 해볼 예정입니다) 이 OutBox 테이블의 역할을 PGMQ가 맡아서 함으로써 메세지를 관리하기 쉽게 해줍니다.
또한, 쿼리로 이루어져있어서 딱히 관리할 것도 없구요. 거기다가 딱히 Citus처럼 난이도 높은 Plugin이 필요한 것도 아닙니다.
위의 아키텍처가 살짝 수정돼서 아래의 그림처럼 됩니다.

오늘도 PostgreSQL이 1승을 추가하네요.
그럼 다른 RDB는 어떻게 해야하냐?
뭐.. 트랜잭션 로그 추적하시던가 OutBox 테이블 만드시던가..
정 안되겠으면 PostgreSQL로 마이그레이션 하는 방법도 있겠네요.
마치며
다음 포스팅에서는 이 내용을 직접 실습해보려고합니다. 사실 마이그레이션은 농담이고 MySQL인 경우에는 어떻게 트랜잭션 로그를 추적해야하는지도 실습해볼겁니다.
메세지 브로커와 데이터베이스가 협업해야 하는 과정에서 생기는 마찰들 때문에 꽤나 시끄러워졌습니다. 이정도로 대공사일 줄은 몰랐는데말이죠.
트랜잭션 로그를 추적한다는 것 자체만으로도 굉장히 겁먹은 상태인데... 시도는 한번 해보도록 하겠습니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!
2024 11/26 업데이트
해당 내용에 대한 실습 포스팅이 업로드되었습니다!
https://coding-review.tistory.com/571
아파치 카프카 (심화)
이번 포스팅에서는 아파치 카프카에 대해서 개념만 잡았던 이전 포스팅에 이어 조금 딥한 내용을 공부해보고 정리해보았습니다. 처음엔 카프카의 구성 요소를 짧에 짚고 넘어가고 이후 카프카의 주요 개념에 대해서 알아보도록 하겠습니다.
아파치 카프카
카프카의 구성요소로는 Broker, Topic, Partition, Producer, Consumer, Consumer Group이 있습니다. 하나씩 간단하게만 정리해보겠습니다.
• 브로커 : 브로커는 카프카가 설치되어있는 물리적인 서버에 해당합니다. 이런 브로커들이 모여서 카프카 클러스터가 되는데 이 브로커는 카프카 그 자체라고 볼 수도 있습니다.
• 토픽 : 메세지가 저장되어있는 "논리적인" 공간입니다. 메세지를 생산하는 프로듀서는 이 토픽을 바라보고 생산하고 컨슈머는 이 토픽을 구독하게 됩니다. 하지만 데이터가 논리적인 공간이니만큼 실제 데이터가 이동하는 것은 아닙니다.
• 파티션 : 파티션은 메세지가 저장되어있는 물리적인 공간입니다. 컨슈머가 토픽을 구독하여 메세지를 받긴 하지만 실제 데이터는 파티션에서 보내줍니다.
• 프로듀서 : 프로듀서는 토픽으로 메세지를 전송하는 객체 혹은 애플리케이션이 됩니다.
• 컨슈머 : 컨슈머는 메세지를 수신하는 객체 혹은 애플리케이션입니다.
• 컨슈머 그룹 : 컨슈머 그룹은 컨슈머들이 모여있는 논리적인 공간입니다. 컨슈머가 특정 토픽을 구독하면 해당 토픽에 존재하는 파티션과 컨슈머 내부에 있는 컨슈머들을 자동으로 연결해줍니다. 이 때의 대원칙은
1. 컨슈머는 직접 파티션을 선택할 수 없다.
2. 컨슈머는 반드시 리더 파티션에서 메세지를 읽는다. (최신 버전에서는 팔로워 파티션에서도 읽는다고 하더군요)
리더 파티션, 팔로워 파티션 이런건 추후에 더 깊이있게 정리할 예정입니다.
간단하게 구성 요소에 대해서 알아봤으니 이제 주요 개념에 대해서 정리해보겠습니다.
복제 (Replication)
카프카의 파티션은 높은 가용성, 높은 내구성을 위해 복제되는데 이 레플리케이션은 데이터베이스 레플리케이션과 매우 흡사합니다.
파티션은 읽기와 쓰기를 담당하는 리더 파티션과 장애 상황에 대응하기 위한 팔로워 파티션으로 나눠집니다. 팔로워 파티션은 장애 상황에 대응하기 위해 리더 파티션의 데이터를 복제하면서 스탠바이하고 있습니다.
앞서 언급했지만 컨슈머는 리더 파티션에서만 메세지를 읽을 수 있지만 최신 버전에서는 리더 파티션에 대한 부하 분산을 위해 팔로워 파티션에서도 메세지를 읽을 수 있다고 하더군요.
파티션의 복제는 개발자가 직접 설정해줄 수 있는데 이 때 파티션의 복제는 카프카 클러스터의 수 (브로커 수) 를 넘지 못합니다.
오프셋 (Offset)
오프셋은 각 파티션마다 어느정도 메세지가 진행되었는지 확인할 수 있는 인덱스이고 이 오프셋을 이용해서 컨슈머의 메세지 처리에 대한 추적이 가능합니다. 때문에 굉장히 중요한 개념 중 하나입니다.
오프셋의 종류로는
• Current Offset : 현재 컨슈머가 처리하고 있는 오프셋입니다.
• Log-End Offset : 현재 큐에 들어간 가장 마지막 오프셋입니다.
• Lag : 현재 큐에 있는 가장 마지막 오프셋과 현재 컨슈머가 처리하고 있는 오프셋의 차이입니다. 이 정보가 카프카를 모니터링할 때 굉장히 중요한 지표가 됩니다. 왜냐하면 현재 카프카가 어느정도로 메세지가 밀려있는지 확인할 수 있기 때문이죠.
현재 컨슈머가 처리하고 있는 Current Offset은 컨슈머가 보통 업데이트를 하게 됩니다. 이 때 오프셋을 업데이트하는 것을 오프셋 커밋이라고 부르고 이 오프셋 커밋에는 자동 커밋, 수동 커밋 이렇게 두가지가 존재합니다.
오프셋 커밋을 자동으로 하게 되면 설정이 간단해서 구현하기가 쉽다는 장점이 있지만 메세지가 중복처리 될 수 있다는 단점이 있습니다. 만약 오프셋 커밋을 수동으로 하게 된다면 구성이 복잡하지만 중복 처리 가능성을 낮춰준다는 장점이 있습니다.
여기서 잠깐!
메세지가 중복처리 된다는 개념이 어떤 개념인지 잘 와닿지 않는데 어떤 상황인지 살펴보겠습니다.
만약 컨슈머가 메세지를 처리하던 도중 예상치못한 장애가 발생하게 되면 어떻게 될까요? 이 정보가 카프카로 되돌아오고 이 때 카프카는 Current Offset을 기준으로 메세지를 재시도하도록 유도합니다.
그럼 같은 메세지가 중복으로 컨슈머에게 도달하게 되는 것이죠. 만약 이 과정에서 데이터베이스 쓰기 연산이라도 있었다면 메세지가 중복으로 돌아와서 데이터베이스 쓰기 연산을 한번 더 진행하게 됩니다.
이 과정이 바로 메세지 중복이 된다는 개념입니다.
그럼 수동으로 오프셋을 커밋하면 중복이 해결되느냐? 그건 또 아닙니다.
케이스는 두가지가 있을 수 있습니다.
• 메세지를 처리하고 오프셋 커밋 : 메세지를 처리하고 오프셋을 커밋하는 경우 메세지가 처리되는 과정에서 장애가 발생하면 중복 처리 가능성이 매우 높아집니다.
• 오프셋을 커밋하고 메세지를 처리하는 경우 : 오프셋을 먼저 커밋하면 메세지를 처리하는 과정에서 장애가 발생하면 메세지를 재시도 해야하는데 오프셋이 이미 커밋되어서 메세지가 유실되어버립니다.
보통 이런 경우 메세지를 처리하는 것을 카프카 트랜잭션 API를 이용해서 처리합니다. 하지만! 카프카 트랜잭션 API도 만능은 아닙니다.
https://coding-review.tistory.com/566
만약 데이터베이스 연산이 끼어있으면 어떻게 할까요? 위의 링크에 잘 정리되어 있습니다. 그 얘기는 본 포스팅과 맞지 않으니 생략하도록 하겠습니다.
간단하게 정리하자면 메세지 처리와 데이터베이스 연산을 카프카 트랜잭션으로 묶을 수 없어서 Outbox Pattern을 이용해야하고 Polling Publisher Pattern, Log Tailing Pattern 이렇게 두가지 방식으로 구현할 수 있습니다.
저장과 압축
카프카는 내부적으로 컨슈머가 메세지를 소비하더라도 일정시간동안 가지고 있는 특징이 있습니다. 이렇게 하는 이유는 컨슈머가 내부적인 장애로 인해 메세지를 다시 소비해야하는 경우를 위한 것이죠. 카프카의 저장 정책에는 시간 기반, 크기 기반 이렇게 두 가지가 있으니 한번 천천히 살펴보죠.
시간 기반은 특정 시간이 지나면 메세지가 자동으로 삭제되는 정책입니다. 반대로 크기 기반은 메세지의 크기가 특정 바이트를 넘어가면 자동으로 삭제하는 것이죠. 물론 이 둘을 혼용해서 사용할 수도 있습니다.
카프카가 메세지를 삭제하는 것도 나름의 정책이 있습니다. 가장 기본적인 정책은 시간기반, 크기기반의 삭제 정책입니다. 이는 앞서 언급했듯이 시간이 지나면, 크기가 크다면 삭제하는 정책입니다.
두 번째 삭제 정책은 압축해서 삭제하는 방법입니다. 파티션 내부 메세지에는 여러가지 메타데이터가 존재하는데 여기에는 오프셋도 있고 실제 메세지 데이터도 있고 "키"라는 것도 있습니다.
카프카는 이 키를 기반으로 메세지를 관리하게되죠. 압축 정책은 키가 중복되면 이전 메세지를 곧바로 지우게 되는 것입니다. 이렇게 함으로써 메세지를 항상 최신 상태를 유지하는 것이죠. 이러한 정책은 데이터가 항상 최신으로 유지되어야하는 IoT 서비스에서 주로 사용됩니다.
리밸런싱
카프카의 리밸런싱은
https://coding-review.tistory.com/504
여기에 자세히 설명이 되어있습니다. 요약하자면 특정 파티션에 연결된 컨슈머 그룹 중 하나의 컨슈머에서 장애가 발생해서 메세지가 유실될 위험에 처했다면 즉시 파티션이 다른 컨슈머를 바라봄으로써 파티션에 존재하는 메세지가 유실되지 않도록 처리하는 것입니다.
리밸런싱은 로드밸런싱과 결이 조금 다른데 로드 밸런싱은 부하를 분산시키는 것에 초점이 맞춰져 있다면 리밸런싱은 메세지가 유실되는 것을 방지하기 위함에 초점이 맞춰져있습니다.
ISR과 ACK
ISR은 In-Sync Replication의 약자로 복제한 파티션들의 집합입니다. 만약 ISR설정을 2로 지정한다면 파티션이 리더 파티션을 포함하여 2개가 복제되어 유지된다는 말입니다.
만약 ISR을 설정할 때 최대 응답시간을 넘어가는 복제본 파티션들은 ISR에 등록되지 않고 넘어간다는 특징이 있습니다.
앞선 개념들은 컨슈머에 초점이 맞춰져 있었다면 ISR과 ACK은 프로듀서에 초점이 맞춰져있는 개념입니다. 프로듀서가 카프카에게 메세지를 전달할 때 카프카에 메세지가 제대로 도착했는지 여부를 확인해 프로듀서가 메세지를 재전송할 것인지를 확인하는 프로세스이기 때문입니다.
ISR과 ACK은 서로 짝꿍처럼 붙어다니는데 ACK을 먼저 살펴보겠습니다.
ACK에는 세가지 단계가 존재합니다. 0, 1, all 이렇게 세가지이죠. 0인 경우 ISR에 메세지가 제대로 복제되었는지 여부를 확인하지 않고 종료해버립니다. 이 때문에 0인 경우 응답속도는 매우 빠르지만 카프카 내부적으로 복제가 이루어지지 않은 경우 메세지 손실의 위험이 있습니다.
1의 경우 ISR에 있는 리더 파티션만 메세지를 전달받은 경우 잘 받았다는 응답을 프로듀서로 보냅니다. 만약 이 응답이 오지 않으면 프로듀서는 재전송하지 않습니다.
마지막으로 all의 경우 ISR에 등록된 모든 복제본에 메세지가 적힌 이후에 프로듀서에게 응답을 보냅니다.
프로듀서의 경우 카프카로부터 응답이 오지 않으면 메세지를 재전송하지 않기 때문에 적절한 ACK설정을 해주어야합니다.
메세지가 유실되어도 상관없고 엄청난 퍼포먼스가 필요하다면 0을, 최소한의 메세지 일관성이 필요하다면 1을, 강력한 메세지 일관성이 필요하다면 all을 선택해야합니다.
근데.. 이거 어디서 많이 보던건데..?
맞습니다. 바로 데이터베이스 레플리케이션 비동기 복제, 반동기 복제, 동기 복제의 개념과 정확히 일치합니다. 컴퓨터 공학은 여기서 쓰던 개념이 저기서도 쓰이고 저기서 쓰이던 개념이 여기서도 쓰이고 모든게 이어져있다는 느낌이 듭니다.
마치며
오늘은 카프카에 대해서 조금 심화과정을 공부해봤습니다. 지금 제가 해보고싶은건 카프카 파티션을 동적으로 설정하고 컨슈머도 동적으로 늘려서 확장하는 방법인데 조만간 AWS를 활용해서 실습해보고싶습니다.
오늘 포스팅은 여기서 마치도록 하겠습니다. 오늘도 즐거운 하루 되세요~
카프카는 어떻게 수십만 TPS에 도달할 수 있었을까?
요즘은 카프카와 NoSQL을 공부하고 있는데 카프카는 특히 공부하면 공부할수록 왜 개발자들이 많이 사용하는지 알 것 같습니다. 공부하면 공부할수록 왜 쓰는지 이해가 된달까요..
이번 포스팅에선 카프카와 AWS SQS, SNS와 비교하면서 왜 카프카가 대용량 메세지 처리에 능하게 되었는지에 대해서 정리해봤습니다.
카프카 vs SQS, SNS
왜 다른 메세지 큐도 아니고 SQS냐하면 RabbitMQ는 메세지를 소비할 곳을 세밀하게 조정하고 싶을 때 사용하는 것이 바람직하지 대용량으로 비동기 메세지 처리를 하는데 특화되어있지는 않습니다. 다른 메세지 큐인 Redis의 Pub/Sub 또한 대용량 비동기 메세지 처리에 특화되어있지 않기도 하구요.
그런 의미에서 SQS와 SNS를 조합한 메세지 큐잉 서비스는 완전 관리형 메세지 큐잉 서비스를 표방하는만큼 카프카와 비교할만하죠.
이번 섹션에서는 카프카와 SQS, SNS를 여러가지 측면에서 비교해보고 최종적으로 하고싶었던 얘기인 "카프카는 어떻게 수십만 TPS에 도달했는지" 에 대해서 얘기해보겠습니다.
메세지 실패에 대한 재시도 전략
메세지 큐잉 서비스에서 실패한 메세지에 대한 재시도 전략은 반드시 필요하다고 볼 수 있습니다. 카프카에선 현재 컨슈머가 소비하고 있는 offset (이하 오프셋) 을 stateful하게 관리하고 있어 컨슈머가 오프셋을 커밋해주지 않으면 카프카는 실패로 간주, 컨슈머에게 다시 메세지를 전송해줍니다.
SQS는 기본적으로 이런 재시도 전략이 없습니다. 그런데 실패한 메세지만 따로 관리하는 DLQ (Dead Letter Queue) 가 존재하죠. DLQ로 들어간 메세지는 메세지를 한 번 재시도해서 전송할 수 있습니다.
하지만 아직까진 DLQ에서마저도 실패한 메세지에 대해서 자동으로 처리해주진 않습니다. 애초에 DLQ는 종착점이기 때문에 일정 횟수 이상 컨슈머가 처리에 실패한 메세지를 저장하는 곳이지 여기서 자동으로 다시 메인 큐로 이동하는건 불가능합니다.
이를 해결하기 위해선 CloudWatch를 이용해서 모니터링하다 Lambda같은 서비스로 DLQ에서 실패한 메세지에 대해서 SQS로 다시 올려주는 작업이 필요합니다. 즉, 수동으로 해줘야한다는 의미이죠.
Retention 정책
카프카에서는 다양한 Retention 정책이 있습니다. 이런 메세지 보관 정책들은 주로 메세지를 재시도할 때 사용하고 어떤 메세지가 처리되었는지에 대한 로그 기록으로도 사용됩니다.
카프카에선 몇초가 지나면 삭제할 것인지, 크기가 어느정도 되면 삭제할 것인지에 대한 "삭제 정책"과 메세지 키를 중복해서 관리하지 않고 최신 상태로만 관리하는 "압축 정책"이 존재합니다.
그럼 SQS는 어떨까요? SQS는 기본적으로 메세지가 14일동안 유지됩니다. 그리고 삭제되죠. 그럼 삭제된 메세지는 어떻게 하냐? 어떻게 못하죠 그냥 삭제된겁니다.
흠.. 여기서도 카프카 승리일까요? 조금 더 지켜보죠
메세지 관리
SQS에서 할 말이 있는 섹션이 등장했습니다. AWS의 SQS부분 공식 문서를 보면 SQS는 무한에 가까운 큐를 가질 수 있다고 나와있습니다.
진짜 무한은 아니겠지만 그만큼 그런거 확장하는데 신경쓰지 않아도 된다고 얘기하는 것 같습니다. "이게 클라우드야." 라는 느낌을 강하게 주죠.
카프카는 어떨까요?
카프카는 ZooKeeper부터 시작해서 관리해야할게 정말 많죠. 사실 위에서 Retention 정책에 대해서도 카프카가 시간단위, 공간단위 삭제 정책과 압축 정책이 있긴 하지만 이만큼 자유롭다는건 그만큼 관리를 개발자가 직접 해줘야한다는 것이죠. 이것도 다 자원이 들어가는 것이고 이런건 무시하지 못하죠.
만약 카프카의 메세지 브로커를 모니터링 해야하고 상황에 따라서 브로커를 추가해줘야할 수도 있고 컨슈머 그룹, 파티션, 토픽 이런걸 다 관리해줘야하죠.
물론 AWS에서 완전 관리형 MSK라고 카프카를 자동으로 관리해주는 서비스가 있긴 합니다만... 카프카를 쓸 정도 되는 큰 서비스를 관리하는데 이거 쓸바엔 그냥 사람한테 시키는게 더 싸게 먹힐수도 있습니다.
메세지 처리
SQS의 TPS는 평균 300TPS, 배치처리를 하는 경우 3000TPS입니다. 반면 카프카는 수만~수십만 TPS까지 나오게 됩니다.
이게 사실이라면 대규모 비동기 메세지 처리에 SQS를 쓰는걸 진지하게 고려해봐야할 것 같은데... 왜 이런 차이가 생기게 된걸까요?
먼저 SQS는 컨슈머가 메세지를 처리할 때 HTTP Polling을 사용하기 때문에 레이턴시가 존재하게 됩니다. 그리고 SQS가 뭐 무한한 큐를 가질수 있네 없네를 따지고 있지만 SQS는 단순한 메세지 처리, 높은 확장성의 메세지 큐, 다양한 AWS 서비스와 연동에 초점이 맞춰져있습니다.
그럼 카프카는 어떻게 수십만 TPS를 달성할 수 있었을까요?
zero copy
운영체제에 대한 로우레벨 얘기를 하지 않을 수 없겠습니다. 결국 카프카도 "메세지"라는 상태를 보관해야하는 입장에서 어느정도 stateful하고 데이터베이스정도는 아니지만 상태관리를 해줘야합니다.
카프카는 로그의 형태로 메세지를 데이터로 관리하고 있고 이를 결국 디스크에 써야합니다. 우리는 운영체제에서 파일시스템이 접근해 파일에 접근하는 경우를 봐야합니다.
https://coding-review.tistory.com/563
https://coding-review.tistory.com/565
제가 작성한 두개의 포스팅을 먼저 읽고 오시면 앞으로 할 얘기가 쉽게 다가오실 것 같습니다. 물론 읽지 않으셔도 상관없습니다.
파일시스템이 파일에 접근하기 위해서 Buffered I/O를 진행하게 됩니다. Buffered I/O란 메모리에서 메모리로 데이터가 이동하면서 순차적으로 데이터를 로우레벨에서 하이레벨로 이동시키는 것을 이야기합니다.
이를 운영체제에선 copy라고 부르고 최상단 하이레벨인 애플리케이션부터 최하단 로우레벨인 디스크에 있는 데이터가 이동하기까지 여러번의 copy가 일어납니다.
운영체제에 커널모드, 유저모드, 애플리케이션 이렇게 세 단계에서 copy가 일어나게 되는데 이를 순서로 표현하면 다음과 같습니다.
• 애플리케이션에서 디스크에 있는 데이터에 접근하려고 합니다.
• 운영체제가 유저모드에서 커널모드로 전환됩니다.
• 커널모드에서 시스템콜을 호출해서 파일시스템에 접근합니다.
• 파일시스템이 추상화된 VFS가 디스크에 접근해 디스크에 들어있는 데이터를 메모리에 적재합니다.
• 메모리에 적재된 데이터가 커널모드에 copy됩니다. (1)
• 커널모드에서 다시 유저모드로 전환될 때 또 다시 copy 됩니다 (2)
• 유저모드에서 애플리케이션에 다시 copy 됩니다 (3)
이때서야 비로소 애플리케이션이 디스크에 있던 데이터를 사용할 수 있게됩니다.
그럼 zero copy가 뭔지 직관적으로 이해할 수 있을 것 같습니다. 앞에서 언급한 copy가 일어나지 않게 되는것이죠.
메모리에 적재하는 순간! 바로 데이터를 전송해버리는것이죠.
엥? 그래도 되나?
사실 애플리케이션이 디스크에 있는 데이터를 사용해야할 때 이런 copy가 필요한데 카프카 입장에선 디스크에 있는 정보가 카프카에게 필요가 없습니다. 그냥 consumer에게 던지기만 하면 되죠.
때문에 운영체제에서 이런 zero copy를 이루기위해 sendFile()이라던가 mmap()같은 시스템콜을 만들어두어 카프카와 같은 서비스들이 이용할 수 있도록 준비되어있습니다.
PageCache
여기서 한 단계 더 나아가 운영체제에 PageCache가 존재합니다. 이건 파일시스템이 자주 사용되는 데이터에 대해 캐싱해둔 공간으로 주로 메모리에 위치하게됩니다.
운영체제는 기본적으로 20퍼센트 정도 되는 메모리를 dirty page로 지정하는데 dirty page는 디스크에서 데이터를 메모리에 적재한 뒤 flush하기 전 "변경된 데이터이지만 디스크에 반영되지 않은 데이터"로서 캐시되어잇습니다.
카프카는 이 dirty page에 접근해서 캐싱된 데이터가 있으면 그걸 바로 컨슈머에게 던져버리는거죠.
그럼 순서가 이렇게 변합니다.
• 애플리케이션 (카프카) 가 디스크에 접근하려고 합니다.
• 운영체제가 유저모드에서 커널모드로 전환됩니다.
• 커널모드에서 시스템콜을 호출해서 파일시스템에 접근합니다.
• dirty page를 조회합니다. 만약 있으면 바로 consumer에게 데이터가 전송됩니다.
• 만약 없다면 파일시스템이 추상화된 VFS가 디스크에 있는 데이터를 메모리에 적재합니다. 이후 바로 consumer에게 전송합니다.
mmap과 DirectByteBuffer
mmap은 디스크에 있는 데이터를 메모리로 올리는 역할을 수행하고 이를 통해 메모리에서 직접 데이터를 가져오는 효과가 생겨 속도가 빨라집니다.
즉, 카프카가 Producer로부터 받은 데이터를 디스크에 넣기 전에 mmap으로 메모리에 올리고 곧바로 sendFile (zero copy) 시스템콜로 컨슈머에게 던져버리는 것이죠.
그 과정에서 DirectByteBuffer라는 개념이 나오는데, 카프카는 JVM 기반이기에 GC에 부담을 주는 것이 싫어 Off-Heap 즉, JVM에 할당된 Heap 메모리가 아닌 외부 메모리 (OS에 할당된 메모리) 를 이용하고 싶었습니다.
이런 니즈가 있었기에 java 1.4에서 DirectByteBuffer라는 개념이 생겨 JVM 기반 애플리케이션이 직접 외부 메모리에 붙을 수 있게 되었습니다.
하지만 이렇게 Non Blocking으로 데이터에 붙기 시작하니까 메모리 누수가 생겨 메모리를 관리하기 힘들어졌습니다. 이를 또 개선한 것이 java 9에 생긴 cleaner이고 이를 호출해 지속적으로 메모리를 청소해줄 수 있는 API가 생기게 되었습니다.
이게 뭐 얼마나된다고 수십만 TPS까지 나오겠어?
이게 꽤 얼마가 됩니다.
CPU가 메모리에 접근해서 데이터를 처리하는 과정이 수백번 수천번의 이동이 있기에 그 데이터가 처리되는 것인데 이런 순환이 하나하나 쌓이기 시작하면 정말 커지게 되는 것이죠.
그래서 카프카의 성능은 CPU의 성능보다 메모리의 크기와 PageCache의 hit 비율에 따라 성능 차이가 크게 달라집니다. 카프카의 성능이 정말정말 중요하다면 커널의 PageCache비율을 줄이는 것도 고려해볼만한 튜닝 방식이라고 생각합니다.
마치며
이렇게 카프카가 어떻게 수십만 TPS에 도달하게 되었는지 로우레벨에서 정리해봤습니다. 카프카는 정말 팔방미인에 다재다능하지만 유지보수 측면에서는 최악이라는 사실을 오늘도 깨닫게 됩니다. 역시 완벽한 기술은 없다는게 이번에도 증명되었네요.
이번 포스팅은 여기서 마치도록 하겠습니다. 오늘도 즐거운 하루 되세요!
아파치 카프카 (응용)
카프카에 대한 이론적인 내용은 아마 이게 끝일 것 같습니다. 아파치 카프카 카테고리는 개념, 심화, 응용을 마지막으로 이제 실습을 해볼 예정입니다.
이번 응용편에선 이렇게 진행됩니다.
• Unclean Leader Election의 위험성과 사용 여부 판단
• Offset 관리 전략
• 성능 최적화 및 튜닝
• 카프카 스토리지와 OS 레벨 연계
이렇게 네가지 섹션으로 이루어집니다. 한번 천천히 정리해보겠습니다.
Unclean Leader Election
카프카는 장애시 ISR에서 리더 파티션을 새로 뽑아 장애 상황에 최적화된 움직임을 보여줍니다. 그런데 만약 ISR에 뽑을만한 리더 파티션이 없으면 어떻게 될까요?
이럴 때 개발자는 Unclean Leader 즉, ISR에 없는 파티션 중 리더로 선출할 수 있도록합니다. 이렇게 ISR에 없는데 리더 파티션으로 선출되면 메세지가 잘못 전달될 가능성이 생깁니다.
이런 가능성이 존재함에도 개발자는 이 메세지가 전달되도록할 수 있는데 일관성 (Consistency), 가용성 (Availability) 중 하나를 선택할 수 있습니다.
C를 선택하게 된다면 (false로 설정한다면) unavailability 에러가 발생하긴 하지만 메세지가 안정적으로 유지됩니다. 반대로 만약 A를 선택하게 된다면 (true로 설정한다면) ISR에 존재하지 않는 파티션을 리더로 선출해 가용성을 높일 수 있습니다.
이런 점 때문에 카프카는 CAP정리를 완벽하게 따르게 되었습니다. 메세지를 stateful하게 관리하는 점이나 브로커가 물리적으로 독립적인 환경이라는 것 때문에 카프카 또한 NoSQL의 반열에 오르게 되는건가..? 싶었습니다.
일단 기본적으로 카프카를 사용한다는 것은 마이크로서비스일 확률이 높고 그렇다면 메세지가 일관되게 처리하는 것이 일반적이기에 false로 설정하는 것이 올바른 방법입니다. 하지만 로그나 모니터링에 카프카를 이용하는 것이라면 true로 놓고 사용해도 상관 없겠습니다.
Offset 관리 전략
카프카의 컨슈머는 본인이 어떤 메세지를 처리하고 있었는지에 대한 정보인 Offset을 알고 있어야 다음 메세지를 처리해도 되는지, 현재 메세지를 재처리해야하는지에 대한 정보를 알 수 있습니다.
Offset은 컨슈머 쪽에서 제어권을 가지고 있고 기본적으로 자동 커밋과 수동 커밋을 지원합니다.
자동 커밋 vs 수동 커밋
자동 커밋은 컨슈머가 메세지를 받자마자 바로 커밋하는 것이고 이것이 카프카의 기본 설정입니다. 하지만 이렇게 하게 되면 컨슈머에 장애가 발생했을 때 메세지를 재처리할 수 없어서 메세지가 유실되는 상황이 발생합니다.
수동 커밋은 컨슈머가 자동으로 커밋을 처리하는게 아니고 개발자가 직접 커밋 시점을 정해주는 것으로써 메세지가 유실되지 않게 관리할 수 있습니다.
하지만 수동 커밋의 경우 메세지를 재처리하는 과정에서 데이터베이스 연산이 메세지 처리와 묶여있는 경우 중복처리가 일어날 수 있습니다. 예를 들어서 INSERT문을 처리해야하는데 두 번 INSERT문이 실행될 수 있다는 뜻이죠.
EOS vs Outbox 패턴
이런 문제를 해결하기 위해서 보통 EOS라고 불리는 카프카 트랜잭션을 사용하거나 Outbox 패턴을 사용합니다. EOS는 카프카 메세지를 트랜잭션 처리해 원자성을 보장하는 것입니다.
하지만 EOS의 경우 굉장히 쌈뽕한 로직으로 깔끔하게 처리할 수 있지만 데이터베이스 연산에 대한 트랜잭션까지 묶어서 처리할 수 없기에 Outbox 패턴을 주로 사용하게 됩니다.
Outbox 패턴에 대한 자세한 내용은 아래의 링크에 설명되어있습니다.
https://coding-review.tistory.com/566
요약하자면, 데이터베이스 연산이 완료되고 Offset을 커밋한다는 개념입니다.
결론은?
결론은 수동 커밋에 Outbox 패턴을 잘 버무려서 사용하면 될 것 같습니다.
성능 최적화 및 튜닝
카프카의 성능을 최적화하는 방법에는 Producer 최적화, Consumer 최적화, 마지막으로 Broker 최적화가 있습니다.
Producer 최적화
Producer를 최적화하는 방법에는 Batching, Compression 이렇게 두 가지가 있습니다. Batching 처리를 하는 것은 높은 처리량이 요구되는 서비스인 경우 필수적으로 사용하게됩니다.
한 번 메세지가 도착하면 한 번 처리하는게 아닌 배치처리를 하는 것인데 이렇게 하면 네트워크 비용도 아끼고 처리량도 올라가게됩니다. 기본적으로 메세지가 도착하자마자 처리하는게 디폴트 값인데 이 값을 설정해주면 배치처리에 용이해지고 처리량이 올라가게됩니다.
또한, 메세지를 한번에 처리할 때 압축해서 처리하는 것을 Compression이라고 부르고 이렇게 처리해주면 또 한 번의 네트워크 비용이 감소되고 처리량도 더 올라가게 됩니다.
GPT피셜이긴 하지만 참고할만한 사진을 가져왔습니다.

Consumer 최적화
Consumer를 최적화하는 방법은 polling 시간을 짧게 설정해주어 리밸런싱을 최소한으로 일어나게 하거나 메세지를 묶음으로 처리해서 처리량을 높이는 방법이 있습니다.

이것도 GPT피셜이긴 하지만 참고가 될 것 같아서 가져왔습니다.
카프카 스토리지와 OS 레벨 연계
카프카는 단순한 메세지 큐잉 서비스가 아니라 디스크 기반의 로그 시스템에 가깝습니다. 심지어 JVM 기반인데 어떻게 속도가 그렇게 빠르게 나올까요?
해답은 Off-Heap 메모리에 있습니다. Off-Heap은 JVM에서 사용하는 Heap 메모리 외부의 메모리 즉, OS에 할당된 메모리입니다.
카프카는 JVM기반이기에 GC를 최소화시키고 싶은 욕구가 있었습니다. 실제로 Elasticsearch의 경우 JVM 기반이기에 대용량 로그를 처리할 때에는 반드시 JVM 튜닝이 필요합니다. 제가 알기로 디스코드였나 트위터였나 초반엔 Elasticsearch를 쓰다가 너무 크기가 커지고 성능을 최적화하기에 JVM이 거슬려서 다른걸로 갈아 탔다고 들었습니다.
카프카는 JVM에서 사용하는 Heap 메모리를 이외의 외부 메모리를 적극 활용하는데 mmap, DirectByteBuffer, sendFile()과 같은 시스템 콜을 적극 활용합니다.
mmap
mmap은 디스크에 있는 데이터를 메모리로 올리는 역할을 하고 이를 통해 메모리에서 직접 데이터를 가져오는 효과가 생겨 속도가 빨라집니다.
DirectByteBuffer
DirectByteBuffer는 네트워크와 메모리 사이에서 처리해주는 버퍼 메모리로 JVM에선 ByteBuffer라는 것이 있어서 내부의 Heap 메모리에 접근하기 용이하게 설계되어있습니다.
JVM기반 애플리케이션에서 직접 외부 메모리에 접근하고싶은 요구가 생겨 DirectByteBuffer라는 외부 메모리에 접근할 수 있는 기능을 추가했는데 직접 메모리에 붙어서 작업할 수 있어서 속도가 굉장히 빠르지만 GC같은 cleaner가 없어서 메모리 누수가 발생할 수 있다는 단점이 있습니다.
때문에 JVM기반 애플리케이션에서 지속적으로 cleaner를 호출해서 메모리를 정리해줘야합니다.
zero copy
zero copy란 데이터를 유저모드로 가져오지 않고 직접 메모리에서 컨슈머에게 전송하는 방법으로 주로 sendFile() 시스템 콜을 사용합니다.
zero copy에 대한 내용은 아래의 링크에 자세하게 설명이 되어있습니다.
https://coding-review.tistory.com/585
즉, 카프카는 실상 메모리와 Page Cache의 hit 비율이 카프카 전체 성능에 큰 영향을 준다고 볼 수 있습니다.
때문에, dirty page의 크기를 관리한다거나 메모리를 관리하는 것이 카프카의 성능을 튜닝하는 방법이라고 볼 수 있습니다..만!
운영체제는 웬만하면 건드리지 않는게 좋겠죠..? ㅎㅎ..
마치며
이번 포스팅을 끝으로 카프카에 대한 이론 정리를 마쳤습니다. 이제 직접 카프카 클러스터를 구축해보면서 실제로 독립적인 (도커로 독립시키면 그게 독립이죠 ㅎㅎ.. 절대 클라우드 비용 내기 싫어서 그런거 아닙니다.) 애플리케이션에서 카프카에 메세지를 Producing해보고 Consuming해보겠습니다.
이번 포스팅은 여기서 마치도록 하겠습니다. 오늘도 즐거운 하루 되세요!
MSA 사고실험 시작
MSA 사고실험 시작
이번 포스팅을 시작으로 MSA에서 아키텍처 설계를 해보려고 합니다. 제 개인적인 생각이지만 k8s정도는 한번 구축해보는게 이해하는것도 편하고 여러모로 쓸모 있지만 MSA라는 거대 아키텍처는 하나하나가 모여 거대한 아키텍처를 이루기에 깔짝깔짝 해본다고 해봤다고 하기 좀 뭣 한 것 같습니다.
당연히 Hello World 수준에서는 쉽겠지만 실제 MSA를 적용하고 있는 여러 기업들에서 생기는 문제들은 이런 수준이 아닐 것이므로 Hello World 수준으로 실습하는건 적절하지 못하다고 판단했습니다.
근데 구인공고 여기저기서 "MSA에 대한 이해가 있는 분" 이라고 써놓으니 공부를 안할 수도 없고 곤란하던 차에 든 생각이 바로
"아인슈타인도 상대성 이론을 사고실험으로 만들었는데 나라고 못할거 있나"
이름하여 MSA 사고실험입니다. 이 사고실험으로 MSA를 직접 실습해보진 않고 MSA에서 생길 수 있는 문제를 추상적으로 공부하면서 기반을 다지는 것이 이 카테고리를 시작하는 목표입니다.
MSA 사고실험에선 아키턱체 위주의 설계를 진행할 예정이고 그 과정에서 생길 수 있는 다양한 가능성 중에 제 기준 최적화가 잘 되었다고 판단되는 것들을 공부해보면서 다듬을 예정입니다.
AI 시대에 살아남기 위해 필요한 것
AI 시대가 어느정도 자리를 잡으면서 과도기에 접어든 것 같습니다. 이제 더이상 우와 대박! 하는 와우 모먼트는 줄어든 것 같고 AI 성능도 더이상 늘어나진 않는 것 같습니다.
물론 성능이 좋아졌다고 마케팅은 하지만 사실 그놈이 그놈같고 뭐가 달라졌는지 체감하기가 쉽지않죠. 특히 저처럼 둔한 사람은 더 그렇고요. 그래서 늘 쓰던 것에서 잘 바뀌지 않는 것 같습니다.
AI가 저보다 코딩을 엄청나게 잘하는건 옛저녁 일이었고 프롬프트만 정밀하게 잘 넣어주면 엇나가는 일도 줄어들었죠. 예전엔 개발자가 코딩을 잘 하는 것이 큰 자산이었지만 이제 AI로 인해 모두가 상향평준화 된 상황에서 살아남으려면 필요한 것은 무엇인가를 고민하게 되었습니다.
사실 이 생각은 AI가 막 태동하던 2년전부터 가지고 있던 생각인데요. 바로 비즈니스의 이해와 아키텍처 설계 능력만큼은 아직 개발자만의 산물이고 예전에도 그랬으며 지금까지도 여전합니다.
AI가 발전하면서 아이러니하게도 인간만이 할 수 있는 일이 뚜렷해지고 AI가 할 수 있는 일이 뚜렷해지면서 더더욱 인간이 필요한 상황이 되고 있는 것 같습니다.
이런 상황에서 쭉정이는 걸러지고 정말 능력있는 사람만 살아남게 되었는데 경영자들은 이제 더이상 코더를 필요로 하지 않고 도메인에 대한 이해가 높은 개발자나 설계를 할 수 있거나 전체 개발 프로세스의 맥락을 이해하고 장애를 빠르게 개선할 수 있는 개발자를 필요로하고 있습니다.
저는 이 세개의 역량이 하나로 이어져있다고 생각하고 그 전초전으로 설계능력을 키울 생각입니다.
제가 원래도 다양한 애플리케이션들의 아키텍처를 그려보는 것을 좋아했는데요. 직접 설계해본 적은 AWS 3 Tier Architecture 고도화하기 라는 포스팅으로 처음 해보고 이후 해본 적이 없습니다.
https://coding-review.tistory.com/505
여기서도 사실 AWS로 클라우드 네이티브를 하려면 어떻게 해야하는지에 대해서 정리한 것 뿐이지 제가 직접 설계했다고 하기도 뭣한 상황이긴 합니다.
이제 아키텍처를 직접 설계해보고 문제 상황을 가정하여 최적화하는 과정을 통해 설계능력을 키워보도록 하겠습니다.
MSA 사고실험 많은 관심 바랍니다. (_ _)
MSA에서 데이터베이스 아키텍처 설계해보기 (RDBMS)
가트너의 MSA 아키텍처에서 보면 MSA를 3티어 아키텍처로 두고 여러개의 파트로 분리해놓은 것을 보고 일단 데이터베이스부터 설계를 해야겠다는 생각을 했습니다.
MSA에서 데이터베이스를 설계할 때 제가 중요하게 생각한 관점은 다음과 같습니다.
• RDBMS는 거대한 상태 덩어리이므로 stateless 애플리케이션에 적합한 k8s에 적합하지 않다. 때문에 데이터베이스는 클러스터 외부 베어메탈 머신에서 돌아가야한다. 이런 상황에서 쿠버네티스 클러스터 내부에 있는 애플리케이션과 상호작용 해야한다.
• 마스터 - 슬레이브 아키텍처로 마스터 노드에선 쓰기 작업을, 슬레이브 노드에선 읽기 작업을 해야하며 마스터 노드의 장애시 슬레이브 노드가 마스터 노드로 승격되어야한다. 되도록이면 Automatic Failover를 지향한다.
• 반드시 수평 확장이 가능해야한다.
이 전제 조건을 바탕으로 한번 본격적으로 시작해보겠습니다.
MSA 에서 데이터베이스 아키텍처

우선 제가 설계한 아키텍처 먼저 살펴보고 왜 선택하게 되었는지를 첨언하도록 하겠습니다.
• 앞에 전제조건대로 쿠버네티스 클러스터 내부엔 데이터베이스를 넣지 않습니다. 앞서 언급했듯이 거대한 상태 덩어리인 데이터베이스 (특히 RDBMS) 를 stateless 애플리케이션에 최적화된 쿠버네티스 클러스터에 넣지 않기로 결정했습니다. 베어메탈 머신에서 데이터베이스를 직접 설치해서 사용하는걸로 정했습니다.
• 데이터베이스는 MySQL (혹은 MariaDB) 을 선택했습니다. 이건 그냥 제가 MySQL을 많이 써봐서 생태계도 잘 알고 있고 친숙해서 사용했습니다.
• 데이터베이스 HA 솔루션으로는 VTOrc를 선택했습니다. Vitess와 Orchestrator를 결합한 VTOrc는 서로의 단점을 보완하는 관계이기에 제가 생각하는 아키텍처에 잘 맞을거라고 생각했습니다.
• 쿠버네티스 클러스터 내부에 있는 파드에서 MySQL 클러스터에 연결하기 위해 라우터를 하나 두어야하는데 맨 처음엔 ProxySQL을 생각했으나 조금 더 찾아보니 VTOrc에 VTGate라는 비슷한 기능이 있어서 Vitess와 통합을 생각해서 VTGate를 사용했습너ㅣ다.
• VTGate는 etcd에 등록된 MySQL 프록시인 VTTablet을 찾아가는 라우터이기에 Write / Read 쿼리에 따라서 마스터, 슬레이브로 요청을 나눠줄 수 있고 지금 누가 마스터고 누가 슬레이브인지 상태를 관리하기위해 etcd를 적극 활용합니다. 이 etcd는 절대 죽으면 안되기 때문에 3중화를 진행했습니다.
하나씩 풀어보겠습니다.
쿠버네티스 클러스터에 RDB 넣어 말아 논쟁
쿠버네티스 커뮤니티에선 이 논쟁이 아직도 뜨거운 감자입니다. 누구는 galera 클러스터를 10개씩 (3중화가 기본인 galera를 10개 넣는다는건 30개의 파드를 띄운다는것) 쿠버네티스에 띄워서 사용했다느니 누구는 도커로 데이터베이스를 띄우는것도 반대인데 쿠버네티스 클러스터에 어떻게 띄우냐느니 말이 많습니다.
저도 마땅한 답을 찾지 못했기에 선배님들 의견을 따라가려고 합니다. 컨퍼런스에서 연사자로 나오셨던 여러 분들과 대화해보고 제 멘토분도 아직은 쿠버네티스 클러스터에 RDB를 띄우는건 이르다. 라는 판단이기에 저도 그 판단에 따르도록 하겠습니다.
이에 대해서 쓴 블로그 글이 있으니 그걸 참고해보시는 것도 괜찮을 것 같습니다.
https://coding-review.tistory.com/562
InnoDB 진영 HA 솔루션 VTOrc
InnoDB 진영에선 HA 솔루션으로 Vitess와 Orchestrator를 조합한 VTOrc를 주로 사용합니다. 실전에서 한번도 사용해본적은 없지만 그래도 대세를 따르려합니다.
Vitess, Orchestrator를 단독으로 사용할 수도 있지만 VTOrc가 서로의 단점을 잘 보완해주어 장점만 뽑아먹을 수 있게 설계되어있어서 VTOrc를 선택했습니다.
Orchestrator만 단독으로 사용한다면 레플리카 토폴로지에 등록된 노드들의 정확한 메타데이터를 알지 못하는 경우가 있고 Vitess만 사용하는 경우 마스터 - 마스터 레플리카를 절대 금지하고 있는 상황이라 Write 연산의 부하를 분산시켜주기 위해서 Orchestrator를 사용합니다.
이 덕분에 Orchestrator는 Vitess로 인해 더 정확한 메타데이터를 확보해 마스터 - 슬레이브 아키텍처에서 승격에 필요한 확실한 정보를 알게 되었고 샤딩까지 가능하게 되었습니다. Vitess는 Orchestrator의 마스터 슬레이브 승격 매커니즘과 마스터 - 마스터 레플리카 전략도 가져갈 수 있게 되었죠.
물론 둘 다 누가 지금 마스터고 누가 슬레이브인지에 대한 상태정보를 알아야하는데 가끔 Automatic Failover를 Orchestrator가 진행하는 과정에서 순식간에 끝나거나 정상적으로 종료되지 않으면 Orchestrator가 마스터라고 인식하고 있지만 Vitess는 슬레이브로 인식하는 경우가 있긴합니다.
하지만 이 경우 Orchestrator가 지속적으로 싱크를 맞춰주기 때문에 큰 문제는 없다고 판단하였습니다.
Orchestrator와 Vitess의 합작에 대한 포스팅도 준비되어있으니 아래의 링크를 참고해주시면 감사하겠습니다.
https://coding-review.tistory.com/543
쿼리 라우터 VTGate
쿼리가 읽기면 슬레이브로 쓰기면 마스터로 이동시키는 VTGate를 앞에두고 모든 파드들이 VTGate를 바라보게 하였습니다. 일단 기본적으로 MySQL 클러스터가 이리저리 흩어져있는데 이를 한군데 모아줄 라우터가 필요하다고 판단했기 때문이죠.
처음에 바로 떠오른 것은 ProxySQL이었습니다. 왜냐하면 ProxySQL이 MySQL 전용 라우터이고 복잡한 라우팅도 가능하고 자체적으로 데이터베이스 커넥션을 관리해주기 때문에 MySQL진영에서 많이 사용한다고 들었습니다.
그럼에도 제가 VTGate를 채택한 이유는 Orchestrator와 통합하기 위해서였습니다.
Orchestrator의 마스터 - 슬레이브 아키텍처가 마스터 노드 장애시 슬레이브 노드의 승격으로 HA솔루션을 제공하는만큼 꼭 필요하다고 생각했고 Orchestrator를 사용하려니 Vitess를 사용하는것이 맞겠다고 판단하였습니다.
처음엔 ProxySQL - Galera cluster - Orchestrator 로 이어지는 아키텍처를 구상했는데 ProxySQL의 문제가 뭐였냐면 만약 마스터 노드의 장애로 슬레이브 노드가 승격시 ProxySQL이 이를 직접적으로 알 수 있는 방법이 없다는 것이었습니다.
때문에 이를 알기 위해선 Orchestrator에서 제공하는 API로 현재 마스터 - 슬레이브 정보를 가져왔어야했고 이를 crontab으로 확인해야했죠.
하지만 쿼리가 쏟아지는 와중에 crontab과 마스터 - 슬레이브 승격간에 시간차이가 생겨버리면 데이터 유실이 심히 우려되어 이 방법은 사용하지 않았습니다.
etcd 삼중화
etcd가 VTGate의 상태관리 역할을 하는데 단일 노드로는 잘 활용하지 않습니다. 때문에 노드를 세개 두어 삼중화 작업을 진행했습니다. 또한 etcd는 내부적으로 HA 솔루션을 가지고 있는데 Raft라는 분산 합의 알고리즘을 이용해서 마스터가 죽어도 정상적으로 작동할 수 있기 때문에 안전하다고 판단했습니다.
etcd는 비교적 삼중화 작업이 간단하고 Raft 덕분에 잘 죽지도 않아서 매우 만족스러운 설계라고 생각했습니다.
Raft에 대한 내용도 작성한 포스팅이 있으니 아래의 링크를 참고해주시면 감사하겠습니다.
https://coding-review.tistory.com/590
마치며
MSA 사고실험은 일단 3 tier 에서 가장 뒤쪽부터 시작할 생각입니다. 왜냐하면 기반이 튼튼해야 뒤에서 설계할 아키텍처에도 힘이 실리기 때문이죠. 일단 사용자로부터 가장 멀리 떨어져있는 Data Access Tier 부터 처리하고 천천히 앞으로 나아가보겠습니다.
이번 데이터베이스 아키텍처는 제가 평소에 꾸준하게 공부하던 데이터베이스 관련 지식들이 많은 도움이 되었습니다. 딱히 공부할게 많지 않아서 금방 끝난 것도 있는 것 같습니다.
다음 포스팅은 애플리케이션 레벨에서 이 데이터베이스에 어떻게 접근할 것인지 설계해보려고합니다. 다음 포스팅도 많은 기대 부탁드립니다.
긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
MSA에서 Kafka 유연하게 설계하기 (쓰기 연산 부하 편)
MSA 사고실험 첫번째 포스팅인 데이터베이스 설계에 이어 두번째는 Kafka를 유연하게 설계해보았습니다. 보통 MSA에서 이벤트 기반 아키텍처를 설계할 때 카프카를 주로 사용하게 되는데 카프카에 대한 이론적인 내용보다 설계에 초점을 맞추게 되다보니 두 가지 관점에서 카프카를 바라볼 수 있을 것 같습니다.
바로 쓰기 연산이 주를 이루는 애플리케이션과 읽기 연산이 주를 이루는 애플리케이션 이렇게 두 가지 관점에서 볼 수 있을 것 같은데요. 보통 쓰기 연산에 부하가 많이 가는 채팅앱, 읽기 연산이 자주 일어나는 쇼핑몰과 SNS처럼 두 가지 관점에서 설계를 해보려 합니다.
주의!!
제 얄팍한 지식으로 설계하는만큼 실제 아키텍처를 공부하는게 아니고 직접 설계하는 능력을 기르기 위해서 쓰는 포스팅임을 알립니다. 실제로 설계를 먼저 해보고 이후 트위터나 디스코드같은 글로벌 대규모 서비스들의 기술 블로그를 보고 아키텍처를 다시 공부하는 작업이 이루어질 예정입니다.
이번 쓰기 부하 편에 이어 읽기 부하 편까지 포스팅을 작성하고 실제 벤치마킹을 할 애플리케이션의 아키텍처를 직접 공부할 예정이니 지금 포스팅은 단순하게 봐주시면 감사하겠습니다.
아키텍처

일단 아키텍처의 흐름도를 이야기하고 왜 그렇게 설계하게 되었는지 이야기해보겠습니다. 채팅을 예시로 서술해보겠습니다.
• 클라이언트에서 채팅을 입력하는 순간 백엔드 서버에 요청이 들어갑니다.
• API 게이트웨이를 타고 들어온, 혹은 다른 백엔드 서버에서 클라이언트가 되어 Redis Stream을 생산하는 서버로 요청이 들어옵니다.
• 해당 서버는 Redis Stream 메세지를 생산하는 Producer가 되면서 Redis Stream에 메세지를 밀어넣습니다.
• Redis Stream의 메세지를 소비하는 Consumer Group인 다른 서버들이 요청을 받아서 특정 Consumer Group은 클라이언트로 WebSocket을 이용해서 메세지를 뿌려줍니다. 그리고 다른 Consumer Group은 메세지의 보존을 위해서 데이터베이스 write 요청을 비동기로 처리하기 위한 Kafka 클러스터 Producer가 됩니다.
즉, 해당 서버는 Redis Stream을 소비하는 Consumer이면서 Kafka 클러스터에 메세지를 다시 밀어넣는 Producer이기도 한 것이죠.
• Kafka에서는 들어온 메세지를 다시 해당 Topic을 구독하는 Consumer Group에 메세지를 보내고 최종적으로 이 Consumer Group이 데이터베이스 직접 쓰기 연산을 수행합니다.
• Kafka에서 브로커의 관리나 리밸런싱은 Cruise Control이 도맡아서 처리하게 됩니다.
이렇게 설계한 이유는 카프카의 특징때문인데요, 우선 채팅은 쓰기 연산이 많아 비동기로 처리해야하지만 카프카는 스파이크성 트래픽에 취약하기 때문에 앞에 버퍼를 하나 두어 카프카의 부하를 최소화하기 위함입니다.
카프카는 뛰어난 처리량을 보여주지만 스파이크성 트래픽에 부하가 심하고 특히 Zookeeper를 사용하는 KRaft 이전 버전들은 Topic이 늘어나면 늘어날수록 Zookeeper에 부하가 심해져 카프카 클러스터의 전체적인 성능하락에 큰 영향을 줍니다.
이렇게 버퍼를 둠과 동시에 Redis Stream에서 메세지를 소비하는 서버의 경우 바로 WebSocket으로 채팅을 상대방에게 전송해 UX적인 측면에서 메세지가 빠르게 전송된다는 느낌을 줄 수도 있어 해당 방식을 채택하게 되었습니다.
이 아키텍처에서 꼭 고려해야하는 점은 다음과 같습니다.
• 카프카는 메세지가 실패하는 경우 자동으로 Retry하는 로직이 있지만 Redis Stream은 자체적인 Retry전략이 없어 직접 DLQ를 구현해야 한다는 점
• Redis Standalone은 Redis Stream을 사용할 때 1000TPS 밖에 나오지 않아 클러스터링이 가능하면서 수평확장에 능한 Redis Cluster를 사용해야하지만 그렇게 되면 Retry전략을 사용할 때 샤드로 나누어진 슬롯으로 인해 DLQ를 구현하기 까다롭다는 점
• Redis Cluster는 클러스터 하나에 마스터, 슬레이브 노드가 들어가지만 모종의 이유로 클러스터 내부에 있는 모든 마스터, 슬레이브가 죽으면 해당 클러스터는 다시 복구할 수 없기 때문에 완벽한 HA는 구축할 수 없다는 점
• Topic이 늘어남에 따라 Zookeeper에 부하가 생기기 때문에 Topic 또한 샤딩이 되어야하고 그로인해 Consumer Group 쪽에서도 Group ID를 샤딩해야하기 때문에 구현, 운영 복잡성이 올라간다는 점
• 카프카의 메세지를 소비하는 Consumer Group에서 메세지와 데이터베이스 쓰기 연산을 트랜잭션으로 묶을 수 없어 Outbox Pattern이나 Transaction Tailing 방식으로 하나로 묶어야하지만 이는 구현, 운영 복잡성이 올라간다는 점
Redis Stream 재시도 전략
Redis Stream은 DLQ를 직접 구현해 재시도를 해야합니다. Redis Stream에서는 처리되지 않은 메세지를 로그로 가지고 있게 되는데 이 로그를 지속적으로 조회하면서 실패한 메세지를 DLQ에 넣어놓고 DLQ에 들어간 메세지를 지속적으로 Redis Stream으로 다시 밀어넣는 작업을 통해 직접 구현해야한다는 단점이 있습니다.
또한, Redis Cluster는 해시 슬롯으로 Automatic Sharding이 가능하기에 자체적으로 샤딩이 되지만 이렇게 샤딩이 되는 순간 Redis Stream의 재시도 전략을 수행하기 힘들어 Producer -> Cosumer -> Retry 에 이르는 이 한 사이클에서 같은 슬롯 넘버를 사용해야한다는 점이 또 하나의 고려할 점인데요.
그렇기 때문에 Redis Stream으로 메세지를 밀어넣을 때 어떤 슬롯 넘버를 사용하는지 Retry하는 쪽에서 알아야하기에 샤딩의 장점이 조금은 희석된다는 단점이 있습니다.
Redis Cluster의 HA
기본적으로 Redis Cluster는 HA를 지원하지만 하나의 클러스터 내부에 있는 모든 노드가 죽어버리면 이 클러스터를 살릴 수 있는 방법이 없기 때문에 완벽한 HA라고는 할 수 없습니다.
이건 뭔가 해결책이 있는건 아니지만 만약 클러스터 내부에 있는 모든 노드가 죽어버린다면 다른 클러스터에 부하가 심해져 전체적인 장애로 이어질 수 있어 각별히 주의해야합니다.
Topic, Consumer Group Sharding
Zookeeper를 사용하는 경우 Topic이 1만개 이상이 되면 Zookeeper에 부하가 생겨 카프카 클러스터의 전체적인 성능 하락 이슈가 생길 수 있습니다. 때문에 Topic을 분산시켜 부하를 줄여야하는데요.
기본적으로 Producer -> Topic -> Partition -> Consumer Group -> Consumer 로 이어지는 이 흐름에서 Topic과 Consumer Group을 샤딩하게 되면 전체적인 부하를 분산시킬 수 있어 해당 방법이 크게 도움이 될 것입니다.
하지만 해당 방법은 메세지가 어디로 흐르는지 정확하게 추적할 수 없다는 점 때문에 디버깅을 하기 매우 까다롭다는 문제가 생긴다는 단점이 있습니다.
때문에 KRaft를 사용하도록 버전을 업그레이드 하는 방법도 생각해볼 수 있습니다. KRaft는 수만개의 Topic을 수용할 수 있기 때문에 만약 Topic을 샤딩하는 것이 운영 복잡도를 높일 수 있고 이것이 싫다면 KRaft로 업그레이드하는 방법이 좋은 해결책이 될 것입니다.
카프카 메세지와 데이터베이스 연산의 트랜잭션
카프카 메세지는 자체적으로 트랜잭션으로 메세지를 묶을 수 있지만 (EOS) 이 작업이 데이터베이스의 연산과 묶을 수는 없기에 EOS만으로는 부족하고 추가적으로 Outbox Pattern이나 Transaction Tailing전략으로 카프카 메세지와 데이터베이스 연산을 묶어야합니다.
하지만 이 과정에서 개발, 운영 복잡도가 올라가고 만약 데이터베이스도 여러개의 노드로 분리되어 있다면 Transaction Tailing을 구현하기 매우 까다로워 복잡도가 올라가게 됩니다.
때문에 구현하기 편한 Outbox Pattern을 생각해볼 수 있지만 Outbox Pattern은 트랜잭션의 원자성을 보장하기 위한 테이블에 UPDATE쿼리가 지속적으로 날아가야하기에 쿼리가 여러번 날아가고 쓰기 연산이 많은 채팅 앱에서는 부담되는 것도 사실입니다.
때문에 구현, 운영 복잡도가 매우 올라가게 되어 들어가는 리소스가 많아질 우려가 있다는 단점이 있습니다.
마치며
MSA가 개발, 운영 복잡도가 올라가니까 함부로 시작하지 말라는 선배님들의 말처럼 복잡도가 정말 끝도없이 올라가는 모습을 보면서 정말 함부로 시작할게 아니라는 생각이 다시금 들게 하는 설계였습니다.
저는 단순히 설계만 해보는 입장에서 실제 구현하고 실제 운영은 안해봤지만 이 설계가 개발, 운영 복잡도가 매우 높다는건 굳이 먹어보지 않아도 알겠더군요.
하지만 이렇게 설계하면 정말 쓰기 연산에 부하가 아무리 심해도 크게 보완할 수 있어 대규모 서비스에서 빛을 발하게 될 것으로 예상됩니다.
이번 쓰기 부하 편에 이어 다음 포스팅은 읽기 부하 편으로 이어집니다. 읽기 연산이 주를 이루는 SNS, 쇼핑몰 등에 빚대어 아키텍처를 설계해보고 고려해야하는 점은 어떤 것인지 한번 학습해보도록 하겠습니다.
이번에도 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~