Summary
졸업과제 프로젝트를 진행하면서 Kubernetes를 사용해 CI/CD 파이프라인을 구축했던 과정을 정리한 글이다.
쿠버네티스를 어떻게 구축했는지, 어떤 서비스를 구축했는지를 볼 수 있다.main branch
로의 merge
한번으로 테스트, 애플리케이션 빌드, 도커 이미지 빌드, 배포를 진행할 수 있는 파이프라인을 볼 수 있다.
추가로 그 과정에서 필요한 Secret Key들에 관한 관리 방법까지 다뤘다.
- 개발자는 main branch로 pull request 및 merge를 수행한다.
- Github Actions의 workflow가 트리거되면서 애플리케이션의 테스트 및 빌드가 수행된다.
- 빌드가 완료되면 docker image를 빌드하고 docker hub repository에 push가 일어난다.
- docker hub push가 완료되면 argocd-deploy 브랜치의 server 폴더의 git tag 파일이 수정(최신화(main branch 커밋으로))된다.
- GitOps 구현체인 ArgoCD는 argocd-deploy 브랜치의 server폴더를 바라보고 있다가 변화를 감지한다.
- server의 파일에 변경이 생겼기 때문에 kubernetes에게 server 애플리케이션을 재배포하라는 요청을 보낸다.
- docker hub repository에서 최신 이미지를 받아 새롭게 애플리케이션을 배포한다.
1. 인프라 구축 (With Kubernetes)
클라우드 인프라로 마이크로소프트의 Azure를 사용했다.
왜 Azure ??
학생 계정을 인증받으면 Azure는 무려 1년간 사용할 수 있는 100$의 크레딧
을 준다.
따라서 예전에 받았던 크레딧이 남아있는 Azure를 사용하게 되었다.
구축 순서
- 가상 네트워크(vnet) 구성 -> 10.0.0.0/16 의 private IP 공간을 가지는 가상 네트워크를 구성한다.
- 네트워크 보안 설정 -> 사용할 포트(80, 443, 22 … etc) 에 대한 인바운드 규칙을 설정한다.
- 가상 머신 생성 -> Master, Worker1, Worker2 노드를 이전에 생성한 가상 네트워크 위에 생성한다.
- 각 노드에 K3s를 설치하고 Master와 Worker 관계를 구성한다.
서버 각각의 스펙.
- 공통 - Ubuntu 20.04 LTS
- master - 2core 4GB
- worker1 - 1core 2GB
- worker2 - 1core 2GB
쿠버네티스 기반으로 애플리케이션이 동작할 수 있도록 구성했지만 상당히 빈약한 스펙을 가지고 있다.
개발 기간에는 최대한 크레딧만을 사용하고 싶었기 때문에 가격적인 면을 고려하여 선정한 최소한의 서버 스펙이다.
따라서 빈약한 서버 구성에 맞춰서 쿠버네티스의 경량화 버전인 K3s를 사용하기로 결정했다.
K3s
가벼운 Kubernetes라고 생각하면 된다.
적은 메모리/binary 파일을 사용하여 Edge/IoT 환경 혹은 CI/CD 환경에서 k8s를 쉽게 사용할 수 있도록 도와주는 도구이다.
쿠버네티스의 핵심 엔진(api-server, kubelet 등)은 동일하기 때문에 K8s를 대체해서 사용이 가능하다.
K3s 설치는 위 레포지토리를 참고해 직접 설치하였다.
2. Github Actions
CI를 구축하기 위해서 github actions을 사용했다.
public으로 설정된 repository에 대해서는 기본적인 CI 기능을 무료로 제공해준다는 장점이 있다.
그리고 무엇보다 Github를 주로 사용해서 프로젝트를 관리하기 때문에
Pull Request등과 쉽게 연동할 수 있고 build, test등의 로그를 실시간으로 확인이 가능하다는 점이 좋았다.
만약 Jenkins를 사용할 경우엔 최소 1core 2mem 정도의 사양이 필요하다.
그런데 서버 사양을 보면 알겠지만.. 배보다 배꼽이 클 수도 있어서.. 사용하지 않았다.
프로젝트에 사용한 Github Action workflow
Test and Build
main branch를 향해 Pull Request된 코드들이 정상적으로 merge되면 첫 번째 workflow Step이 실행된다.
- submodule로 설정해놓은 secret 파일을 가져온다.
- JDK설정 및 gradlew 실행파일의 권한을 설정해준다.
- gradle을 사용해 애플리케이션의 Test, Build를 진행한다.
- docker image build를 진행한다.
- 연결된 계정의 public repository로 docker image를 push한다.
Update menifest
- argocd-deploy 브랜치로 checkout 한다.
- git-tags.yaml 파일 내부의 tag 값을 main 브랜치의 최신 commit 해시값으로 변경한다.
- 변경된 argocd-deploy 브랜치를 커밋하고 푸시한다.
이후에 WebHook Trigger로 인해 CD 과정을 연속적으로 수행한다.
Github Actions 전체 코드
name: aomdCI
on:
pull_request:
branches:
- main
types: [ closed ]
env:
git_tag: ${{ github.sha }}
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-20.04
steps:
- run: echo The PR was merged
- name: Checkout main branch
uses: actions/checkout@v3
with:
ref: main
token: ${{ secrets.AOMD_PRIVATE }}
submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "temurin"
- name: Grant execute permission for gradlew
shell: bash
run: chmod +x ./gradlew
- name: Build with Gradle
shell: bash
run: |
./gradlew -Dspring.profiles.active=prod clean build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: ./services/portfolio/
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/aomd-server:latest
update-argocd-manifest:
needs: if_merged
runs-on: ubuntu-20.04
defaults:
run:
working-directory: ./prod/aomd-server
steps:
- name: Checkout argocd-deploy branch
uses: actions/checkout@v3
with:
ref: argocd-deploy
token: ${{ secrets.AOMD_PRIVATE }}
submodules: recursive
- name: Change git-tag.yaml
run: |
cat <<EOF > git-tag.yaml
image:
tag: ${{ env.git_tag }}
EOF
- name: New git tag push
run: |
git config --global user.email "GitHub Actions Bot@github.com"
git config --global user.name "GitHub Actions Bot"
git add git-tag.yaml
git commit -m "update git tag"
git pull origin argocd-deploy
git push origin argocd-deploy
참고자료
3. Application Secret Config
Spring 애플리케이션에서 사용하는 secret 정보들을 어떻게 관리할 지 고민이 많았다.
실제 서비스로 배포할 목적은 아니었지만,
아무래도 secret한 정보들을 노출시킨다면 신경이 쓰이기 때문이다.
secret을 관리하는 방법은 다양하며 크게 3가지 방법을 보민해 보았다.
- 설정 값들을 ${...} 환경변수로 지정하고 Github Action에서 빌드 시 환경변수를 넣어준다.
- yml 파일을 privae repo의 git submodule로 사용한다.
- 별도의 Config Server를 사용한다.
그 중에서 가장 먼저 3번(별도 Config Server 사용) 방법을 사용하지 않기로 결정했다.
1. Secret 정보가 그렇게 많지 않을 뿐더러,
2. 추가적인 서버 구성을 위해 Spring Cloud Config라는 서드파티 프레임워크를 익혀야 했다.
3번 방법의 장점은 MSA 환경에서 두드러진다.
여러 서버의 Config 설정을 하나의 서버에서 관리할 수 있고 런타임에 Config를 수정할 수 있다는 장점이 존재한다.
따라서 서버별로 다른 Config를 관리해야하는 MSA의 어려움을 해결해줄 수 있는 방법이다.
하지만 이번 서비스는 모놀리식 아키텍처로 구성되어있다.
그래서 Spring Cloud Config를 적용하기 보다는 좀 더 가벼운 방법을 사용하는게 서버 리소스 면에서나 졸업과제 마감 기한 면에서나 더 알맞을 것 같아서 가장 빠르게 사용을 반려했다.
두 번째로 모든 설정값들을 Git Secret을 사용해 환경변수로 지정하고 Github Action에서 ${}로 넣어주는 1번 방법을 고려했다.
1번째 방법이 가장 쉽고, 가볍고, 빠르게 이해할 수 있었지만,
자동화 CI/CD 파이프라인을 만들자는 모토와 어울리지 않는다고 생각이 들었다.
배포에 필요한 여러 설정 정보들을 직접 Github에 입력해줘야 했기 때문에,
새로운 secret 정보가 생길 때마다 application.yml 파일에서github 자체 페이지에서 모두 설정해야하는.. 귀찮은 일이 있을 것으로 예상되었다.
그리고 수동으로 정보를 입력하고 업데이트가 어렵다는 점(값이 바뀌면 git secret을 삭제하고 새로 생성해야함)이 큰 문제로 여겨졌다.
쉽게 적용할 수 있다는 장점이 있었지만 수동적이라는 단점이 너무 치명적으로 느껴져서 사용을 반려하게되었다.
마지막으로 실제로 선택한 1번 방법은 git submodule을 사용해서 config 파일을 원하는 위치로 가져오는 방법이다.
secret을 관리하는 private repository를 생성하고, 그곳에 secret 파일들을 넣을 수 있다.
나는 아래 그림처럼 secret.yml 파일 하나를 넣어놓았다.
장점으로는 secret 값들을 별도의 환경변수로 설정할 필요가 없고, github와 연동이 쉽다는 점을 들 수 있다.
별도의 리소스를 소모하지 않고, 수동적으로 환경변수를 설정해 줄 필요가 없어서 현재 프로젝트에 가장 적합한 방법이라고 생각이 들었다.
하지만 신경 써 줘야 하는 부분이 2군데 존재했다.
1. Github actions에서 submodule로 연동된 private repo를 찾을 수 있도록 개인 TOKEN을 발급해서 Git secret에 환경 변수로 입력해 줘야한다.
2. submodule을 가져온 후 빌드를 할 때 yml 파일을 찾을 수 있도록 build.gradle 파일을 설정해줘야 한다.
1번은 아래 글을 참고해서 토큰을 발급받은 후 git secret에 입력하면 된다.
그리고 아래 코드처럼 github action내에서 submodule을 가져오도록 설정할 수 있는 옵션을 지정해준다.
- name: Checkout main branch
uses: actions/checkout@v3
with:
ref: main
token: ${{ secrets.AOMD_PRIVATE }}
submodules: recursive
2번은 아래 코드를 build.gradle에 추가해서 해결할 수 있다.
def copyYmlFile = {
copy {
from '../../config'
include '*.yml'
into './src/main/resources/'
}
copy {
from '../../config'
include '*.yml'
into './src/test/resources/'
}
}
task buildTask{
copyYmlFile()
}
문제점과 해결 방안
현재는 Public docker registry를 사용하기 때문에 image를 보면 민감정보들을 확인할 수 있다.
Private docker registry는 이미지를 1개만 허용하고 기타 자잘한 제한사항이 많기 때문에 사용하지 않았다.
추후에 실제 서비스를 하게되어 보안이 중요한 프로젝트를 할 경우에는 private registry를 사용할 것이다.
4. ArgoCD
CD를 수행하기 위해서 2가지 방법을 고려했다.
- github actions에게 클라우드 서버의 권한을 주어 배포 스크립트를 실행할 수 있게 한다.
- ArgoCD를 사용해 GitOps를 적용한다.
1번 방법은 아래 3가지의 단점을 이유로 사용하지 않았다.
- 로그를 확인하기 어렵다
- 배포 상태를 알기 어렵다
- 권한을 github action에게 주게 되므로 보안이 약화된다
따라서 2번 방법의 GitOps 방법론을 사용하기로 결정했고 4가지 원칙을 고수했다.
1. 선언형 배포 작업 정의서를 (YAML) 생성한다.
배포 방식을 명령형으로 정의된 것이 아니라 선언형으로 정의했다.
바라는 상태(desired state)를 선언적으로 Git에 정의하면 그것이 쿠버네티스에 반영되도록 한다.
GitOps를 선택한 큰 이유중 하나로 쿠버네티스가 선언적인 배포방식을 사용하기 때문에 잘 어울릴것이라고 생각했다.
2. Git을 이용해 배포 버전을 관리했다.
Git에 모든 배포에 관련된 정보가 정의되어 있어야 하며, 각 버전이 Git 저장소에 기록이 되어 있어야 한다.
이를 통해 쉽게 예전 버전으로 롤백을 하거나 새로운 버전으로 업그레이드를 할 수 있도록 했다.
3. 변경 사항 반영을 자동화했다.
Git 저장소에 선언형 정의서를 선언하면 실제 배포는 자동으로 이루어져야 한다.
따라서 이것을 책임지는 GitOps 구현체는 ArgoCD와 같은 도구를 사용하였고,
자동화를 통해 인적 오류를 줄이고 지속적 배포를 가능하게 했다.
4. 이상 탐지 및 자가 치유를 적용하였다.
GitOps 구현체는 YAML 파일을 클러스터에 반영하는 것 뿐만 아니라,
배포된 리소스가 이상이 없는지 확인하고 유지시키는 역할도 담당할 수 있도록 했다.
이 부분도 ArgoCD가 쿠버네티스 컴포넌트의 상태를 확인하고 알아서 재가동 시켜준다.
GitOps를 이해하기 위해서는 '단일 진실의 원천' 이라는 단어의 의미를 이해해야 한다.
단일 진실의 원천(Single Source Of Truth, SSOT) : 하나의 진실(결과)은 오직 하나의 원천(이유)에서 비롯된다는 의미
추가 설명
ArgoCD가 바라보는 진실의 원천은 Git으로 설정했다.
정확히 말하자면 Git의 특정 branch를 바라보고 있다.
argoCD는 자신이 가지고 있는 branch의 commit 버전과 실제 저장소의 commit 버전을 동일하게 맞추려고 한다.
쉽게 말해서 항상 특정 repository의 branch를 바라보고있고, 그 branch의 변화에 맞춰서 자신의 상태를 변화시킨다.
클라우드 서비스에 배포해놓은 argoCD가 argocd-deploy라는 브랜치를 바라보도록 설정해 둔 상태이다.
argoCD는 해당 브랜치(argocd-deploy)에 변경사항이 생길 경우 Sync 작업이 일어나고 새롭게 배포를 진행한다.
즉, 배포전략으로 Push Type을 사용했다.
mysql 과 api server 를 따로 배포한 모습이다.
API-SERVER의 배포 현황을 볼 수 있다.
레플리카셋을 통해서 서버들의 로그를 쉽게 볼 수 있다.
MySQL의 배포 현황이다. 서버와 다르게 PVC를 사용하고 있다.
파드나 디플로이먼트에 장애가 생겨 파괴 후 재생성 되더라도 데이터를 유지하기 위해 설정해 주었다.(볼륨)
GitOps와 ArgoCD를 사용하면서 느낀점
- 서버가 어떤 형태로 배포되어있는지, 어떻게 동작하는 중인지를 파악하기위해서 서버의 로그를 찾아보는 등 배포 환경에 직접 찾아갈 필요가 없었다. 단순히 최신 yaml 파일들만 확인하면 되기 때문이다.
- 정말 빠르게 배포할 수 있다. 지금까지 개발하며 진행했던 모든 배포 과정을 merge 한 번으로 할 수 있다.
특히 배포 방식을 단일화 했기 때문에 더욱 안정적이고 지속적인 배포가 가능해졌다. - ArgoCD에서 시원한 UI를 통해 로그를 한 번에 볼 수 있다는 점이 너무 좋았다. 실제로 시연회 직전 MySQL에서 UTF 설정에 에러가 생겨서 MySQL Helm 차트를 수정해 완전히 새로 배포했어야 했다. 이때 트러블 슈팅을 하는 과정에서 ArgoCD의 로그 기능과 UI를 통해 상태를 추적한 것이 큰 도움이 되었다.
생각해본 문제점
- commit 버전에 따른 이미지 관리가 진행되지 않았다. 생각보다 docker image의 tag를 관리하는게 자동으로 하기 쉽지 않았고 아직 정확한 방법을 모르겠다. github action을 사용해 랜덤 해시값을 image 태그로 설정할 수는 있겠지만 랜덤 태그는 가독성이 매우 떨어지기 때문에 이 방법은 굳이 사용하지 않았다.
- 아직 ArgoCD를 잘 사용하지 못하는 것 같다. 매우 일부 기능만을 알고 있으며 ArgoCD 자체에 문제가 생긴다면 트러블 슈팅에 어려움을 겪을 수 있다고 생각한다.
- ArgoCD는 쿠버네티스에 종속적인 기능인 것 같다. 설치를 할 때도 K8s Helm 차트를 사용해서 했기 때문에 잘 사용하기 위해서 익혀야 하는 개념이 배로 많아질 수 있다고 생각한다.
5. Finally
1달간 Azure를 사용하고 서버 가동, 중단을 반복했는데 재밌었다.
근데 가상 머신 3개, 디스크, IP, Vnet등등 다 사용하니까 생각보다 크레딧이 훅훅 나갔다.
100$나 준 Azure에게 정말 감사하다.
최종적으로 배포한 쿠버네티스 컴포넌트 아키텍처이다.
사실 쿠버네티스는 현재 프로젝트 규모에 전혀 적합하지 않다는 문제가 있다.
애플리케이션은 모놀리식인데다 자동 스케일아웃이 필요한 정도의 트래픽이 절대 들어오지 않을 것이기 때문이다.
하지만 효과적으로 CI/CD를 적용해서 안정적인 개발, 배포 프로세스를 구축해보고싶다는 개인적인 바람, 기술적 욕심을 채우기 위해 선택했다.
결과적으로 파이프라인을 구축하는데 성공했고 서버 엔지니어링에 대한 시야를 넓힐 수 있었다고 생각한다.
대부분의 기업이 쿠버네티스를 사용해서 MSA 애플리케이션을 개발하고 CI/CD 파이프라인을 구축하는것으로 알고 있다.
그래서 언젠가 그런 파이프라인을 만나고 관리할 때가 왔을 때 이번 경험이 도움이 되었으면 좋겠다.
긴 글 읽어주셔서 감사합니다.
궁금한 부분 질문은 언제든지 환영입니다!
'DevOps > Kubernetes' 카테고리의 다른 글
Kubeadm 으로 K8s 설치하기 with 자동화 스크립트 (3) | 2022.06.10 |
---|---|
[kubernetes] ubuntu에 kubeadm으로 K8s 설치하기 (cgroup, coredns 해결) (0) | 2022.04.05 |
[kubernetes] Helm으로 nginx ingress controller 설치하기 (1) | 2022.04.04 |
[Kubernetes] No Ingressclass resource with name nginx found 문제 해결하기 (Helm stable repo) (0) | 2022.03.31 |
[kubernetes] #10 쿠버네티스 영속성 데이터와 볼륨 (0) | 2022.03.14 |