[CI] Spot Instance로 Jenkins Kubernetes cloud agent 구성하기

우노·2024년 11월 2일
0

Practice & Trouble Shooting

목록 보기
18/20

쿠버네티스에서 파드로 에이전트를 만드는 Jenkins가 캡틴 같아서 가져왔습니다.

설계 의도

AWS EKS로 매니지드 쿠버네티스 클러스터를 활용한 안정적인 애플리케이션 운영 환경을 구성을 계획했고 Jenkins까지는 requests, limits를 잘 설정한다면 운영 환경에 큰 영향을 주지 않고 별도의 인스턴스 관리 없이 젠킨스를 활용할 수 있을 것 같아 운영 환경과 같은 클러스터에 Jenkins를 배포했습니다. 운영 환경과 Jenkins는 정해진 수의 온디맨드 노드에 배포해서 안정적으로 구성하고 리소스가 추가로 필요하다면 스팟 인스턴스로 스케일아웃 하도록 노드그룹을 설정했습니다.

    {
      name                = "ondemand_medium"
      spot_enabled        = false
      release_version     = "1.31.0-20241011"
      disk_size           = 20
      ami_type            = "AL2023_x86_64_STANDARD"
      node_instance_types = ["t3.medium"]
      node_min_size       = 2
      node_desired_size   = 2
      node_max_size       = 2 // 온디맨드 고정
      labels = {
        "cpu_chip"  = "intel"
        "node-type" = "ondemand"
      }
    },
    {
      name                = "spot_medium"
      spot_enabled        = true
      disk_size           = 20
      release_version     = "1.31.0-20241011"
      ami_type            = "AL2023_x86_64_STANDARD"
      node_instance_types = ["t3.medium"]
      node_min_size       = 0
      node_desired_size   = 0
      node_max_size       = 2 // 최대 2개까지 스케일아웃
      labels = {
        "cpu_chip"  = "intel"
        "node-type" = "spot"
        "jenkins"   = "true" // Agent nodeSelector Label
      },
    }

Jenkins 서버 자체는 운영 환경에 같이 배포했을 때의 영향을 예측할 수 있지만 빌드 파이프라인을 마스터에서 실행할 때는 운영 환경에 부담이 갈 수도 있을 것 같다는 생각이 들었습니다. 그렇다고 부담이 없을 만큼 인스턴스 크기를 키우기에는 정해진 비용 제한이 있어서 젠킨스 빌드 파이프라인 실행은 무조건 스팟 인스턴스에서 실행하도록 nodeSelector를 설정했습니다.

결론적으로 시나리오는 다음과 같습니다.

agent pod nodeSelector 설정 (jenkins=true) + 빌드 파이프라인 실행
➡️ 스팟 인스턴스 (jenkins=true) 에서 실행

  • 스팟 인스턴스 기본 개수가 0개이므로
    • 현재 0개면 ➡️ 인스턴스 생성 및 node join
    • 현재 1개 이상이면서 리소스를 충분히 활용 가능하다면 ➡️ 해당 노드에서 agent 실행

그리고 Jenkins Master는 온디맨드, Agent는 필요할 때 생성되는 스팟으로 장단점은 다음과 같습니다.

장점

  • Master-Agent 분리로 책임/부담 분리
  • 스팟 인스턴스 활용으로 비용 절감
    • 온디맨드보다 저렴한 스팟 인스턴스
    • 스팟 인스턴스도 필요할 때만 스케일아웃
  • 노드그룹 설정과 플러그인을 활용해서 별도의 AWS 설정없이 스팟 인스턴스 활용

단점

  • 스팟 인스턴스 초기 실행 부담
    • 노드그룹 기본 설정이 0이기 때문에 스팟 인스턴스가 없는 상태면 빌드마다 시간 소요
      • 스팟 인스턴스 Ready까지 1분 소요
    • 노드 Ready까지 기다려야 하므로 Timeout이 길어져야함
  • 이미지 캐시 사용 불가능
  • 기타

Jenkins 설치 및 kubeconfig 추출

먼저 Jenkins를 설치하고 Jenkins가 쓰는 ServiceAccount가 Pod 생성 권한을 가지고 있어야 합니다. 저는 Jenkins Kubernetes Installation을 참고하였고 YAML을 살펴보면 jenkins-admin을 확인할 수 있습니다. jenkins-admin의 kubeconfig를 Jenkins에 등록하여 Pod를 생성할 수 있도록 Credential을 등록해야합니다.

먼저 ServiceAccount를 생성할 때 기본적으로 토큰이 생성되지는 않기 때문에 Secret으로 토큰을 생성했습니다. 아래의 YAML을 apply하고 명령어를 입력하면 토큰을 얻을 수 있습니다.

# jenkins-admin-token.yaml
apiVersion: v1
kind: Secret
metadata:
  name: jenkins-admin-token
  namespace: devops-tools
  annotations:
    kubernetes.io/service-account.name: jenkins-admin
type: kubernetes.io/service-account-token

kubectl get secret jenkins-admin-token -n devops-tools -o jsonpath='{.data.token}' | base64 --decode

아래 명령어로 .crt 파일의 내용을 base64로 인코드하여 가져옵니다.
kubectl get configmap -n kube-system kube-root-ca.crt -o jsonpath='{.data.ca\.crt}' | base64

획득한 데이터를 YAML에 채우고 나중에 사용하기 위해 저장합니다.

# kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: kubernetes
  cluster:
    certificate-authority-data: <.crt>
    server: https://<kubernetes api-server>
contexts:
- name: jenkins-admin-context
  context:
    cluster: kubernetes
    namespace: devops-tools
    user: jenkins-admin
current-context: jenkins-admin-context
users:
- name: jenkins-admin
  user:
    token: <토큰>

마지막으로 Jenkins deployment의 8080과 50000 포트를 외부에서 사용할 수 있도록 NodePort로 빼주면 됩니다.

Cluster Auto-Scaler

트래픽 부하, nodeSelector 등의 이유로 새로운 Node가 떠야할 때 cluster auto-scaler를 설정해야 합니다. Karpenter도 활용할 수 있지만 아직 EKS와 오토스케일링에 익숙해지는 단계고 구현과 시간에 대한 압박이 있어서 이전에 활용했던 Cluster Auto-Scaler를 활용하게 되었습니다. 스케일 인/아웃 시간 절감부터 노드 프로비저닝까지 EKS에 최적화된 Karpenter도 추후에 적용해보고 비교해보고 싶습니다. 설치는 [DEVOCEAN] [AWS EKS-연재5] AWS EKS 의 Cluster Autoscaler 설정을 참고하였습니다.

[AWS] Karpenter 및 Cluster Autoscaler를 사용하여 클러스터 컴퓨팅 규모 조정 (간단한 비교)

Kubernetes Plugin

Jenkins plugin to run dynamic agents in a Kubernetes cluster.
Kubernetes plugin for Jenkins

Jenkins의 Kubernetes Plugin은 쿠버네티스 클러스터에서 동적 에이전트를 실행하는 플러그인이며 저처럼 빌드 파이프라인을 스팟 인스턴스 노드에 파드를 띄워 실행하고 싶은 경우에 파드를 띄우기 위해 필요한 플러그인입니다. 이 플러그인을 설치했다면 Cloud 설정을 통해 클러스터를 연결하고 파드가 생성될 환경을 설정해야합니다.

Cloud 설정

  1. Name: 추가할 Cloud 이름
  2. Kubernetes URL: api-server 주소
    ex. https://~~.ap-northeast-2.eks.amazonaws.com (eks)
  3. Kubernetes Namespace: Pod가 생성될 네임스페이스
  4. Credentials: Pod 생성/삭제 권한이 있는 ServiceAccount kubeconfig
  5. Jenkins URL: Jenkins 서버 주소 (+8080)
  6. Jenkins Tunnel: Jenkins Tunnel 주소 (host:port)
    기타 등등

🚧 Jenkins URL? Jenkins Tunnel?

Jenkins를 설치할 때 Deployment 파일을 확인하면 8080 포트와 50000 포트를 확인할 수 있다. 여기서 8080 포트는 우리가 쓰는 웹UI의 젠킨스 서버를 이야기하며 Jenkins URL에 작성하면 된다.
Jenkins Tunnel은 50000 포트에 해당하며 master와 agent의 연결을 담당하여 JNLP(Java Network Launch Protocol)로 통신한다. 그래서 http:// 없이 host:port 형태로 작성해야한다. JNLP는 Jenkins 에이전트 연결의 전통적인 방식으로써 JNLP가 아닌 웹소켓으로도 agent 연결이 가능하다.

agent가 master에 연결이 되면 agent에, 정확히는 master 외부에서 빌드 파이프라인이 실행된다. 이를 네트워크를 통해서 관리해야하고 이를 위해 JNLP가 사용된다.

사용자 서버 페이지와 명령 처리가 나뉜 비슷한 예시로 Nexus와 같은 사설 도커 레지스트리를 사용한다면 웹UI로 제공되는 서버 페이지의 80 포트와 도커 명령어로만 통신할 수 있는 5000 포트가 나뉘어있는 것을 들 수 있다.

Spot NodeGroup으로 인스턴스 띄우기

Cloud 설정을 마쳤다면 마지막으로 Spot NodeGroup의 라벨을 확인합니다. 저는 agent의 nodeSelector에 활용될 라벨로 jenkins=true를 추가했습니다.

이제 파이프라인의 agent에서 pod를 설정해야합니다. Cloud의 PodTemplate 페이지에서도 설정할 수 있지만 저는 명시적으로 어떤 파드에서 띄우는지 팀원과 공유하기 편하게 하기 위해서 yaml 형식으로 작성했습니다.

pipeline {
    agent {
        kubernetes {
            yaml '''
apiVersion: v1
kind: Pod
metadata:
  name: build-env
spec:
  nodeSelector:
    jenkins: "true"
  containers:
  - name: build-env
    image: ubuntu:latest
    command:
    - sleep
    args:
    - infinity
    securityContext:
      runAsUser: 1000
'''
            defaultContainer 'build-env'
            retries 2
        }
    }
    stages {
        stage('Test') {
            steps {
                sh 'hostname'
                echo 'hello world'
            }
        }
    }
}

ubuntu 환경의 agent pod를 실행했고 스팟 인스턴스 노드에서 뜰 수 있도록 nodeSelector를 추가했습니다. 파이프라인이 실행되면 스팟 인스턴스가 생성되거나 기존의 스팟 인스턴스에서 파이프라인을 실행할 기본 환경을 가진 pod가 생성되고 파이프라인 실행이 종료되면 pod는 알아서 삭제됩니다.

실행 결과

스팟 인스턴스 0개에서 1개로 스케일아웃할 때

  • 실행 시작 시각: 09:44:09
  • Spot Node Ready 시각: 09:45:19
  • agent pod 종료까지 소요 시간: 87초

스케일 아웃까지 약 1분이 소요되는 것을 알 수 있습니다. Cluster Auto-Scaler를 활용해서 Karpenter 설정하면 더 빨라지지 않을까 기대하며 추후에 시간적 여유가 있다면 비교해보고 싶습니다.
비용 절감은 지켜봐야겠지만 일단 스팟 인스턴스가 온디맨드보다 70% 비용 절감이 가능하고 min, desired를 0개로 설정해서 유지하며 필요할 때에만 스케일아웃하여 사용하므로 그 이상의 비용 절감 효과를 확인할 수 있을 것으로 기대하고 있습니다.

물론 스팟 인스턴스를 신규 생성할 때 시간이 소요되는 것과 스케일인/아웃에서 시간이 소요되는 점은 개선 방안을 찾아야할 것 같습니다.

주의할 점

Spot NodeGroup 설정 후 한 번도 노드 생성이 되지 않았다면 cluster-autoscaler가 노드를 인식하지 못하므로 최소 하나 이상의 인스턴스가 등록되었어야합니다! 저는 이를 위해 NodeGroup의 min과 desired를 잠시 1로 설정한 후에 다시 0으로 줄여 인스턴스 생성 후 삭제했습니다.

Jenkins X

Karpenter와 마찬가지로 러닝 커브와 시간적 여유 부족으로 찾아보고 도입하지 못했던 Jenkins X를 소개하자면 쿠버네티스에 특화된 Jenkins CLI라고 할 수 있습니다. 추후에 직접 활용하면서 비교해볼 예정입니다.

All In One CI/CD including everything you need to start exploring Kubernetes.
Multi-cluster GitOps, Tekton pipelines, Secrets management, Pull Request ChatOps and Preview Environments.

마무리

이전에 ASG부터 시작 템플릿까지 직접 생성하면서 스팟 인스턴스로 agent를 구성하려고 했을 때 감을 못 잡고 헤맸던 경험이 있는데 노드그룹으로 스팟 인스턴스와 파드로 띄워보면서 동작 방식에 대해서 더 잘 알게 되었다고 생각하고 쿠버네티스를 쓰지 않을 때에도 필요하다면 EC2 인스턴스를 오토스케일링 할 수 있도록 직접 구성해보고 싶습니다!

더하여 비용 청구서를 확인하고 절감 효과를 체감하기 위해 다음 달까지 기다리는 시간이 길게 느껴질 것 같습니다. 빨리 확인하고 싶네요.😆

비슷한 일을 해야되는 분들께 도움이 되는 글이었으면 좋겠고 부족하거나 틀린 부분이 있다면 가감없이 조언 부탁드립니다. 읽어주셔서 감사합니다!🙇🏻‍♀️

profile
기록하는 감자

0개의 댓글