개요
GitOps는 Git Repository를 이용해 인프라 및 애플리케이션 구성을 관리하고 프로비저닝 프로세스를 자동화하는 방법을 의미합니다.
구축할 GitOps 워크플로우는 다음과 같습니다.
1. 개발자가 코드를 수정한 후, Application Code를 관리하는 GitHub Repository에 변경 사항을 push합니다.
2. GitHub Repository의 main 브랜치에 push event가 발생하면 GitHub Action이 트리거됩니다.
3. GitHub Action이 코드를 이미지로 빌드하여 Docker Hub에 push합니다.
4. GitHub Action이 K8s manifest를 관리하는 GitHub Repository의 Rollout.yaml 파일의 컨테이너 이미지 태그를 수정합니다.
5. Argocd는 3분마다 주기적으로 GitHub Repository의 manifest와 클러스터의 차이(drift)를 확인합니다.
6. Argocd가 drift를 발견하면 GitHub Repository의 manifest와 클러스터를 동기화합니다.
(로컬 환경에 클러스터를 구축했기 때문에 GItHub Webhooks는 사용하지 못했습니다.)
GitOps 도구 선택
GitHub(Git Repository) - 익숙하고 널리 사용되는 Git 리포지토리 플랫폼으로, 코드 관리 및 협업에 용이합니다.
Github Actions(CI) - GitHub에 내장된 CI 도구로, 간편하게 워크플로우를 설정하고 관리할 수 있습니다.
ArgoCD(CD) - Kubernetes 환경에 특화된 GitOps 도구로, 애플리케이션 배포 및 라이프사이클 관리를 자동화합니다.
Docker Hub(Image Registry) - 널리 사용되는 컨테이너 이미지 레지스트리로, 이미지 저장 및 공유를 편리하게 수행할 수 있습니다.
구축 과정
1. kubeadm으로 Kubernetes 클러스터 구축
kubeadm을 이용해 로컬 환경에 K8s 클러스터를 구축한 방법은 다음과 같습니다.
2. 운영 환경 구성
클러스터 구축을 완료한 후, ArgoCD, Argo Rollouts, Ingress controller, metric-server를 클러스터에 생성하고 정상적으로 동작할 수 있도록 설정했습니다.
ArgoCD
# argocd namespace 생성
kubectl create namespace argocd
# apply argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
sudo curl --silent --location -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64
sudo chmod +x /usr/local/bin/argocd
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'
argocd-server service의 타입을 NodePort로 변경하여, 클러스터에 포함된 노드의 ip 주소를 사용해 argocd-server에 접근 가능하도록 했습니다.
Argo Rollouts
Deployment 대신 Argo Rollout을 사용해 blue/green 배포나 Canary 배포 전략을 구현하기 위해, 필요한 리소스들을 설치하고 배포했습니다.
# namespace 생성
kubectl create namespace argo-rollouts
# apply argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# install kubectl plugin for argo rollout & 설치 확인
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
sudo install -o root -g root -m 0755 kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
kubectl argo rollouts version
Ingress controller
# kubeadm으로 구축한 경우, label을 controlplane에 추가해야 ingress-controller가 controlplane 노드에 생성될 수 있습니다.
kubectl label no controlplane ingress-ready=true
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90s
metric-server
metric-server의 component.yaml을 다운로드합니다.
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
이후 component.yaml에서 args 부분을 다음과 같이 수정합니다.
- args:
- --kubelet-insecure-tls=true # kubelet과의 통신 보안 설정 완화
- --cert-dir=/tmp
- --secure-port=10251 # 기존 10250 port는 kubelet api가 사용하는 port이므로 충돌하지 않는 port로 수정해야 합니다.
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=30s
또한 아래와 같은 위치를 찾아 hostNetwork: true를 추가하여 Pod가 호스트 노드의 네트워크 네임스페이스를 공유하도록 설정합니다. 즉, Pod는 호스트 노드의 IP 주소와 포트를 사용하며, 호스트 노드의 네트워크 인터페이스에 직접 접근할 수 있습니다.
(제가 kubeadm으로 구축한 방법을 사용해 클러스터를 구축한 경우, 아래와 같이 kubelet + kubelet이 생성한 Static Pod와 api server를 통해 생성된 Pod의 IP 주소 범위가 다릅니다. 따라서 metric-server가 kubelet과 통신하기 위해 위와 같은 설정이 필요합니다.)
이후 수정한 components.yaml 파일을 apply 합니다.
kubectl apply -f components.yaml
3. CI
CI 과정을 실습하기 위해, 예전에 개발했던 spring boot 앱과 Repository를 사용했습니다. 애플리케이션을 기능 단위로 분리하여 각각 컨테이너로 배포한다면 더 좋겠지만 GitOps 구축이 목적이므로 애플리케이션은 수정하지 않고 GitHub Action workflow 파일만 작성해 추가했습니다.
.github/workflows/gradle.yml 파일을 다음과 같이 작성하여 생성하였습니다.
name: Java CI with Gradle
# 동작 조건 설정 : main 브랜치에 push request가 발생할 경우 동작한다.
on:
push:
branches: [ "main" ]
jobs:
# Spring Boot 애플리케이션을 image로 빌드한 후 Docker Hub에 Push, Manifest Repository의 rollout.yaml 파일에서 사용하는 이미지의 태그를 새로 Push한 이미지의 태그로 변경
CI:
runs-on: ubuntu-latest
steps:
# Repository Checkout
- name: Checkout code
uses: actions/checkout@v4
# Docker 이미지 빌드
- name: docker image build
run: |
TAG=${{ github.sha }}
docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/ddc:$TAG .
# DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker Hub 이미지 푸시
- name: docker Hub push
run: |
TAG=${{ github.sha }}
docker push ${{ secrets.DOCKERHUB_USERNAME }}/ddc:$TAG
# SSH 설정
- name: Setup SSH key for accessing IaC repo
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_IAC }}
# GitHub 사용자 설정
- name: Set Git config
run: |
git config --global user.name "JinsuYeo"
git config --global user.email "jsyeo97@naver.com"
# IaC Private Repo에 이미지 태그 업데이트 (Argo CD 자동 배포)
- name: Clone IaC Repo and Update Tag
run: |
TAG=${{ github.sha }}
git clone git@github.com:JinsuYeo/ddc-iac.git
cd ddc-iac
sed -i "s|image: .*ddc:.*$|image: ${{ secrets.DOCKERHUB_USERNAME }}/ddc:${TAG}|g" rollout.yaml
git add rollout.yaml
git commit -m "Update product image to ${TAG}" || echo "Nothing to commit"
git push origin main || echo "Push failed"
이후 push event가 발생하면 GitHub Action이 트리거되어 workflow가 실행되고,
성공적으로 수행되었다면 다음과 같이 Docker Hub에 이미지가 push되고, 해당 이미지의 태그로 Manifest Repository의 rollout.yaml 파일의 컨테이너 이미지의 태그가 수정된 것을 확인할 수 있습니다.
4. CD
K8s Manifest를 관리할 GitHub Respository에 Rollout, HPA, Ingress, Service, namespace의 manifest 파일을 작성해 Push했습니다.
rollout.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: backend
namespace: backend
spec:
selector:
matchLabels:
type: backend
app: backend
version: v1.0.0
template:
metadata:
labels:
type: backend
app: backend
version: v1.0.0
spec:
containers:
- name: backend
image: jsyeo97/ddc:a45b82e625b49b282ee13d04e944e0354a2ef129
ports:
- containerPort: 80
resources:
requests:
cpu: 200m
memory: 512Mi
strategy:
blueGreen:
activeService: backend-active
previewService: backend-preview
autoPromotionEnabled: false
hpa.yaml (로컬 클러스터 환경이기 때문에 노드 스케일링이나 CPU, Memory와 같은 리소스에 한계가 있어 max, min replica를 모두 1로 설정했습니다.)
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backend-hpa
namespace: backend
spec:
minReplicas: 1
maxReplicas: 1
scaleTargetRef:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
name: backend
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 60
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
namespace: backend
spec:
rules:
- http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: backend-active
port:
number: 80
service.yaml (rollout의 blue/green 배포를 사용하기 위해 preview와 active service 생성)
---
apiVersion: v1
kind: Service
metadata:
name: backend-active
namespace: backend
spec:
ports:
- port: 80
protocol: TCP
selector:
app: backend
type: backend
---
apiVersion: v1
kind: Service
metadata:
name: backend-preview
namespace: backend
spec:
ports:
- port: 80
protocol: TCP
selector:
app: backend
type: backend
namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: backend
ArgoCD 설정 과정은 NodePort로 ArgoCD 웹 콘솔에 접속한 뒤, 아래 링크의 3번 과정부터 진행하였습니다.
Public Repository를 연결했기 때문에 Repository에서 토큰을 발급받아 인증에 사용하는 과정은 필요 없었고, 로컬 환경이라 Public IP나 도메인 주소가 없기 때문에 GitHub Webhooks도 사용하지 않았습니다.
위 방법으로 ArgoCD 웹 콘솔에서 Manifest GitHub Repository를 연결해 Application을 생성한 후, Manual Sync를 눌러 GitHub Repository의 manifest와 클러스터를 동기화했습니다.
위 화면처럼 Rollout, HPA, Ingress, Service, namespace 모두 성공적으로 클러스터에 생성된 것을 확인할 수 있었습니다.
4. 테스트
ArgoCD를 통해 생성된 K8s 클러스터 환경이 제대로 동작하는지 테스트해보기 위해 Ingress 주소로 요청을 보냈습니다.
(ingress-controller가 생성된 노드의 주소로 접근 가능)
그러나 ingress가 service를 찾지 못하는 문제가 발생해 애플리케이션에 정상적으로 접근할 수 없었고, kubectl describe를 통해 ingress의 namespace가 svc와 다르다는 것을 알아채고 수정하여 해결할 수 있었습니다.
이후 Ingress, service, HPA, rollout 등 모든 리소스가 manifest 대로 정상적으로 생성 및 구성되어, 애플리케이션이 정상적으로 동작하는 것을 확인할 수 있었습니다.
트러블 슈팅
macOS Sequoia 15.0.1로 업데이트 후, kubectl이 제대로 동작하지 않는 문제 (iterm2 이용 시)
controlplane 노드로 curl과 ping을 통한 접근도 안되는 것을 확인했습니다.
$ ping 192.168.10.112
ping: sendto: No route to host
$ curl 192.168.10.112 -v
* Trying 192.168.10.112:80...
* Immediate connect fail for 192.168.10.112: No route to host
* Failed to connect to 192.168.10.112 port 80 after 2 ms: Couldn't connect to server
* Closing connection
curl: (7) Failed to connect to 192.168.10.112 port 80 after 2 ms: Couldn't connect to server
그러나 controlplane에서 host로의 통신은 가능했고, 이후 VM의 네트워크로 어댑터에 브릿지를 사용하는지, routing table 설정이 잘 되어있는지, 노드와 host가 동일한 네트워크 대역에 있는지 확인했으나 문제가 없었습니다.
또한 브라우저에서 argocd로의 연결은 가능했고, iterm2 대신 기본 terminal 이용하거나 sudo로 curl을 날린 경우에는 정상적으로 동작했습니다.
따라서 iterm2에 문제가 있다는 것을 알게 되었고,
iterm2에 로컬 네트워크 접근 권한을 주어 해결할 수 있었습니다.
GitHub Action work-directory 설정 오류
애플리케이션 코드를 관리하는 repository의 디렉토리 구조 상 working-directory 설정이 필요했습니다(Dockerfile과 .gradlew의 위치가 Repository의 root 디렉토리가 아니라 하위 디렉토리에 위치했음). 그러나 working-directory 설정 시 에러가 발생했습니다.
error: an error occurred trying to start process '/usr/bin/bash' with working directory '/home/runner/work/{working-directory 경로}'. no such file or directory
먼저 Dockerfile이 Repository의 root 디렉토리에 위치하도록 디렉토리 구조를 바꾼 후 수정 사항을 모두 push하였습니다.
이때 오류가 발생하였고, 다음과 같은 방법으로 해결했습니다.
# git push 시 아래와 같은 오류 발생
fatal: the remote end hung up unexpectedly
# Git이 HTTP(S) 프로토콜을 통해 데이터를 전송할 때 사용하는 버퍼 크기를 설정합니다. 큰 파일을 푸시하거나 클론할 때 "the remote end hung up unexpectedly"와 같은 오류가 발생하는 경우, 이 값을 늘리면 문제를 해결할 수 있습니다.
git config --global http.postBuffer 1048576000
하지만 그럼에도 Dockerfile을 찾을 수 없다는 오류가 계속해서 발생했고, 다음의 설정을 빠트렸기 때문임을 알게되었습니다.
- name: Checkout code
uses: actions/checkout@v4
위 내용을 workflow에 포함하여 리포지토리를 체크아웃한 후 이미지 빌드 작업을 수행했어야 했음을 깨달았고, 위 내용을 workflow에 추가하고 manifest 레포지토리에 rollout.yaml 파일 추가한 후 github action을 다시 실행하여 정상 동작을 확인할 수 있었습니다.
회고
이전에 진행했던 팀 프로젝트에서는 인프라 구축과 CD 과정을 주로 담당했기 때문에 전체 CI/CD 파이프라인 구축이나 GitOps 구축은 경험하지 못했는데, 이번 기회를 통해 로컬 환경에 간단히라도 구축해볼 수 있었고, GitHub Repository에 commit을 push하는 것만으로 애플리케이션의 배포가 가능한 환경을 로컬에 구축할 수 있었기 때문에 의미 있는 경험이었다고 생각합니다.
그러나 어느 정도는 팀 프로젝트를 통해 경험해봤던 과정이기 때문에 크게 어렵지 않았고, 이후에는 프로젝트에서 경험하지 못했던 모니터링 도구를 클러스터에 배포해보는 경험과 istio를 이용해 트래픽을 라우팅하는 경험을 해봐야겠다고 느꼈습니다.
'Cloud engineering' 카테고리의 다른 글
리눅스 기본 요소들을 이용해 Pod 생성하기 (0) | 2024.11.09 |
---|---|
kubeadm으로 구축한 쿠버네티스 클러스터에 모니터링 시스템 구축하기 (0) | 2024.10.15 |
kubeadm 이용해 K8s cluster 구축하기 (3) | 2024.09.30 |
AWS Innovate Migrate, Modernize, Build 특집 온라인 컨퍼런스 후기 (6) | 2024.09.28 |
CKA 후기 (0) | 2024.09.24 |