8주차 - CICD

개요

이번 스터디 내용은 cicd.
아르고와 젠킨스의 기본적인 사용법을 익히는 시간이다.
사용법을 익히는 것은 좋지만, cicd에서 정말 중요한 건 전략 수립과 이에 맞춘 설정이라고 생각한다.
이번 스터디에서 내가 얻을 게 있다면, 이 부분에 대한 전략을 잘 세워서 웰 메이드 파이프라인을 수립하는 게 아닐까.
그래서 핵심 실습 전략을 세워 나의 작은 프로젝트로 삼을 생각이다.
이전에 정리할 내용은 무엇이 있을까?

ci예시.
https://github.com/argoproj/argo-workflows/blob/main/examples/ci.yaml
이미지 빌드까지
https://kmaster.tistory.com/23#google_vignette
https://medium.com/@mrsirsh/a-simple-argo-workflow-to-build-and-push-ecr-docker-images-in-parallel-a4fa67d4cd60
빌드할 때 도커 엔진없이 빌드할 때 카니코 가능
https://medium.com/@mrsirsh/a-simple-argo-workflow-to-build-and-push-ecr-docker-images-in-parallel-a4fa67d4cd60
이미지 빌드 가능할 듯.

실습 구상

이번 스터디에서, 내 주관을 많이 넣어서 실습을 진행하고자 한다.
많은 고민을 하면서 설계를 하되, 구축 시에는 주관을 강하게 가진 채 진행하겠다.
생각만 하고 어영부영댔던 경험이 조금 있어서, 구축을 하면서 느끼게 되는 부족함은 작업을 완료한 이후 성찰하겠다.
일주일도 안 되는 시간으로 구축해야 하기에, 시간 상 힘들 것 같은 부분들은 추가사항이라고 명시했다.

카나리 배포가 되게 하려면 어떻게 함?
https://nyyang.tistory.com/224
헬름 젠킨스
https://github.com/stakater/Reloader
컨피그 리로더 - 나는 어노 사용하는 게 너어어어어무 싫다.
자동완성도 힘들고, 휴먼 에러내기 딱 좋은 방식이다.
crd를 만드는 게 낫다고 항상 생각한다. - etcd 용량을 대가로 바치지만 상태 관리에도 훨씬 이점이 있다.

로컬 환경에서 아르고 시리즈로 cicd 구축

구상

eks에서 테스트를 하기 이전, 먼저 로컬환경에서 간단하게 cicd를 구축해본다.
기본 환경은 이렇게 구성된다.

기본적으로 클러스터가 구성된 이후 추가적인 세팅들은 전부 HelmTerraform을 이용해 관리한다.
원래는 헬름만 사용했으나, 헬름 yaml 파일을 따로 관리하면서 명령어를 따로 정리해두는 것이 불편하다고 생각됐다.
그래서 테라폼까지 곁들여 활용한다.

사용할 툴들은 다음과 같다.

깃 레포는 개발자용 레포, 운영팀 레포가 분리된다.
실습 플로우는 다음과 같다.

내가 구상한 대로 된다면, 실습을 위해 내가 하는 행위는 일차적으로 끝난다.
나머지는 워크플로우의 흐름에 따라 진행된다.

아르고 롤아웃은 카나리로 배포를 진행한다.
이때 analysis를 진행하여 실제로 새 버전이 제대로 동작하는지 체크한 뒤에 pause 된다.
이 pause는 내가 풀어서 업데이트를 완료시킬 것이다.
그냥 완전자동화도 가능하지만, 항상 책임자가 마지막에 검수는 해야지?

환경 구축

클러스터 구축 세팅은 생략한다.

cert manager로 letsencrypt 인증서 받기

http 통신이 편하기는 하지만, 이미 도메인도 있겠다, 그냥 간단하게 인증서만 받으면 될 걸 굳이 https 통신으로 하지 않을 이유도 못 느끼겠고, 마침 저번에 공부만 하고 결국 실습을 하지 않은 서트 매니저를 조금 다뤄보려고 한다.

image.png
인증서 오브젝트를 만드니 자동으로 챌린지를 수행하는 것이 보인다.
image.png
보다시피 관련 crd가 있기 때문에 진행 상황도 확인할 수 있다.
오더와 챌린지 간의 관계에 대해서는 PKI 참고!
image.png
기다리다 보니 금새 인증서가 발급됐다!
인증서 길이 줄여보고 싶어서 ecdsa 했다가 괜한 에러가 나진 않을까 걱정했는데 다행이다.
image.png
인증서도 성공적으로 나온 것이 보인다.
와일드카드 도메인으로 가져왔으므로, 사실상 이제 내 모든 서버들이 이걸 이용할 수 있다 아ㅋㅋㅋ
대충 찾아봤는데, 클러스터 범위 인증서를 받을 수는 없는 것 같아서 일단 기본 네임스페이스로 받았다.
image.png
실제 사용할 때는 각 네임스페이스 시크릿에 넣어주려고 한다.
인그레스를 만들 때 인증서를 발급 받도록 할 수도 있지만, 그렇게 되면 매번 인증서를 요청하는 상황이 나올 것 같아서 일찌감치 직접 인증서를 받아오는 방향으로 택했다.

tf import -var-file=creds.tfvars  kubernetes_manifest.clusterissuer "apiVersion=cert-manager.io/v1,kind=ClusterIssuer,name=letsencrypt-issuer"
tf import -var-file=creds.tfvars  kubernetes_manifest.certificate "apiVersion=cert-manager.io/v1,kind=Certificate,namespace=default,name=zerotay.com"

처음 써보는 거라 상태 체크를 빠르게 하기 위해 직접 kubectl로 했고, 이후에는 테라폼 코드로 임포트 해주었다.
참고로 var file 인자를 앞단에 둬야 정상 동작하고, 아니면 여러 인자를 줬다고 화낸다.
image.png
이후에 제대로 상황을 반영하기 위해서는 apply를 해줘야만 한다.
제대로 세팅을 옮겼다면 업데이트된다고 나오더라도 실제로 업데이트가 되지는 않는다.

kubectl get secret zero-domain --namespace=default -oyaml | grep -v '^\s*namespace:\s' | kubectl apply --namespace=gitea -f -

이런 식으로 다른 네임스페이스에 시크릿을 쉽게 전파했다.[4]
image.png
근데 막상 해보니 이런 에러가 발생했다.
소유권 때문에 발생하는 에러인데 처음에는 서트 매니저의 인증서 리소스와 실제 인증서 파일이 명확하게 연결돼있는 것이 좋을 것이라 생각해서 설정을 넣었던 것이었다.

kubectl get certificate zerotay.com --namespace=default -oyaml | grep -v '^\s*namespace:\s' | kubectl apply --namespace=gitea -f -

image.png
인증서를 직접 복사하면 다시금 인증서 발급을 위해 챌린지를 하게 될 것이라 생각해서 시도하지 않았던 방법인데, 막상 해보니 알아서 발급이 진행돼서 그냥 이렇게 인증서를 넘겨주는 방식으로 세팅을 진행했다.
image.png
다만 이렇게 한다고 해서 그대로 똑같은 인증서를 받는 것은 아니다.
Kyverno를 이용해 네임스페이스가 만들어질 때 시크릿이 복사되도록 하는 방법도 있지만, 당장은 생략했다.

하지만 사실 이렇게 각 네임스페이스 별로 인증서를 발급 받을 것이라면, 그냥 인그레스에서 어노테이션 달아서 하는 게 가장 편리하다..
그래서 테스트는 저렇게 했으나 다른 것들은 전부 인그레스에 어노테이션을 통해 세팅을 진행한다.

위와 같은 결론을 얻은 채, 다음 실습들을 진행하다가 결국 문제에 직면했다.
image.png
혹시나 했지만 역시나 문제가 발생한 건데, 그냥 너무 많이 인증서 요청해서 렛츠인크립트가 화낸 것이다.
아쉬운 대로 처음에 하고자 했던 방법 그대로 인증서를 다른 네임스페이스에 넣어주는 방식도 활용하기로 마음 먹었다.

local path provisioner -kubectl provider

여러 양식이 담긴 yaml 파일을 한번에 적용하는 것이 기본 모듈에서는 어렵다.[5]
image.png
이런 것을 제공은 해주지만, 막상 해보니 프로바이더가 전파된 서브 모듈에서는 잘 동작하지 않는다.
일반인이 만든 kubctl 프로바이더를 쓰면 그나마 편하게 이런 조작을 할 수 있게 된다.

data "http" "file" { url = "https://raw.githubusercontent.com/rancher/local-path-provisioner/${local.version}/deploy/local-path-storage.yaml" }

data "kubectl_file_documents" "local_path" {
  content = data.http.file.response_body
}
resource "kubectl_manifest" "test" {
    for_each  = data.kubectl_file_documents.local_path.manifests
    yaml_body = each.value
}

local-path-provisioner는 이걸로 세팅했다.

terraform {
  required_providers {
    kubectl = { 
      source = "gavinbunney/kubectl"
      version = "1.19.0"
    }
  }
}

원하는 세팅만 그때그때 할 수 있도록 각 툴들을 서브모듈로 관리하는데, 서브 모듈로 제대로 원하는 프로바이더가 전달되도록 하기 위해서는 각 서브모듈마다 테라폼 블록을 설정해줘야 한다.
이걸 세팅하면 프로바이더 블록을 별도 세팅할 필요는 없다.[6]

프로메테우스, 메트릭 서버 세팅

아르고 롤아웃을 효과적으로 사용하기 위해서는, 메트릭을 수집해야만 한다.
내 로컬 스토리지 용량이 부족해질 것을 감안하고, 수동 삭제할 거 감안하고 프메 그라파나를 설치한다.
image.png
메트릭 서버는 간단하게 설치해서 생략한다.
image.png
이전에 세팅했던 것을 기반으로 프로메테우스도 쉽게 설치에 성공했다.
image.png
세팅을 대충 해서 requests는 별로 안 되는데 실제 메모리 사용량은 60퍼가 넘는 ㄷㄷ
자그마한 내 로컬 클러스터가 고통을 받고 있으니 빠르게 실습을 진행하는 것이 좋겠다.

NAMESPACE=monitoring
kubectl get certificate zerotay.com --namespace=default -oyaml | grep -v '^\s*namespace:\s' | kubectl apply --namespace=$NAMESPACE -f -
kubectl get secret zero-domain --namespace=default -oyaml | grep -v '^\s*namespace:\s' | kubectl apply --namespace=$NAMESPACE -f -

기티아 세팅

기티아를 기본으로 세팅하기 위해서는 postgre, redis를 위한 스토리지 세팅이 필요하다.
비활성화는 못하나 했는데, 그 정도 커스텀을 지원하지 않는다.
image.png
또 한 가지 짜증나는 지점이, 헬름에서 서비스의 포트를 마음대로 커스텀할 수 있도록 템플릿을 짜지 않은 모양이다.
타겟 포트와 서비스 포트를 무조건 일치시키도록 설정된다.
image.png
어려운 건 아니라 그냥 이건 내가 직접 커스텀해서 하기로 마음 먹었다..
또 아쉬운 건 서비스들을 헤드리스 모드로만 실행하도록 하는 것..
기본 세팅이 그러하고, clusterIP: ""와 같은 식으로 헤드리스를 피하고 싶어도 되지 않는다.
사실 크게 문제될 부분은 아니라 이건 그러려니 했다.
image.png
아무튼 조금 귀찮게 세팅해서 기티아 설치 성공.
image.png
tls 설정까지 안전한 통신이 가능하게 만들었다(어차피 로컬에서만 사용해서 크게 상관은 없겠지만..).
image.png
결국 그냥 깃서버이기 때문에 흔히 하듯이 레포지토리를 만들면 된다.

아르고 워크플로우 세팅

이제부터는 아르고 시리즈를 세팅해야 한다.
참고로 아르고 시리즈는 전부 argo 네임스페이스에 만드는 게 권장되는데, 다른 네임스페이스에 둘 거라면 커스텀 세팅을 더 해야 한다.
image.png
워크플로우는 간단하게 성공.
그런데 로그인을 하려면 외부의 인증을 받아야만 한다.[7]
워크플로우가 자체적으로 유저의 신원을 관리하지 않기 때문이다.
SSO를 이용하여 완전히 OAuth를 제공해주는 업체에 인증을 맡겨도 되는데, kubectl로 관리가 가능한 만큼 당연히 클러스터의 신원을 이용하는 것도 가능하다.
image.png
argo-workflows-server가 기본적으로 필요한 권한을 가졌으니, 따로 뭐 안 만들고 이걸 이용해도 될 것이다.

k create token -n argo argo-workflows-server

....
image.png
우히키키키키킼ㅋㅋㅋㅋ
넣을 때.. Bearer를 앞에 꼭 같이 넣어라..
난 당연히 토큰 넣으면 알아서 해줄 줄 알았는데..
image.png
아무튼 이렇게 볼 수 있게 된다.
웹 ui는 확인용으로만 쓸 거고, 간단한 워크플로우를 한번 만들어본다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
spec:
  entrypoint: print-message
  arguments:
    parameters:
    - name: message
      value: hello world
  templates:
  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["{{inputs.parameters.message}}"] 

generateName 필드로 이름이 만들어지는 방식이라 이 양식은 apply할 수 없고, 무조건 create를 해야 한다.
image.png
엔트리포인트를 한번 잘못 잡아서 에러가 났다.
image.png
대충 세팅을 하고 진행하니 또다른 에러도 발생했다.
이건 네임스페이스에서 워크플로우가 실행되는데 이때 default 서비스 어카운트가 사용돼서 생기는 문제이다.
근데 헬름으로 각 네임스페이스에 서비스 어카운트 만들어지게 해뒀는데 기왕이면 어카운트 필드 명시 안 해도 알아서 뮤테이팅돼서 들어가게 좀 해주지..

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
spec:
  serviceAccountName: argo-workflow
  entrypoint: print-message
  arguments:
    parameters:
    - name: message
      value: hello world
  templates:
  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["{{inputs.parameters.message}}"] 

image.png
아무튼 이렇게 하면 제대로 실행이 되는 것을 확인할 수 있다.
image.png
워크플로우는 전부 잡처럼 실행되기 때문에 파드를 조회해보면 끝난 파드들이 보인다.
기본적으로 init 컨테이너가 한번 실행되고, 이후에 실제 워크플로우의 진입점 역할을 하는 컨테이너가 워크플로우 간 각종 변수나 세팅 초기화 및 필요한 설정을 붙잡는 역할로 위치하는 것 같다.
그와 동시에 템플릿에 정의된 컨테이너가 실행된다.
kubectl로도 충분히 workflow를 다룰 수 있지만, 이들이 제공하는 cli를 이용하면 조금 더 이쁜 시각화와 검증 기능을 이용할 수 있다.
개인적으로는 krew에 플러그인 등록을 해줬으면 하는데, 아무도 하는 사람이 없다..[8]
괜히 오기가 발동해서 그냥 kubectl로만 하려다가, 그래도 기왕 쓰는 거 잘 써보자는 마인드로 설치했다.

ARGO_OS="darwin"
if [[ uname -s != "Darwin" ]]; then
  ARGO_OS="linux"
fi
curl -sLO "https://github.com/argoproj/argo-workflows/releases/download/v3.6.5/argo-$ARGO_OS-amd64.gz"
gunzip "argo-$ARGO_OS-amd64.gz"
chmod +x "argo-$ARGO_OS-amd64"
mv "./argo-$ARGO_OS-amd64" /usr/local/bin/argo
argo version

image.png
설치 자체야 간단하니 이제 조금 더 복잡한 워크플로우를 만들어 테스트해보자.
image.png
cli를 써보니까, 이거 안 쓰면 조금 힘들 수도 있겠다 싶다.
이걸로 미리 에러를 잡고 워크플로우를 실행할 수 있어서 꽤나 유용하긴 하다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: http-
spec:
  serviceAccountName: argo-workflow
  entrypoint: http
  onExit: http
  arguments:
    parameters:
    - name: url
      value: "http://nginx.default.svc"
  templates:
  - name: http
    inputs:
      parameters:
        - name: url
    http:
      timeoutSeconds: 20
      url: "{{inputs.parameters.url}}"
      method: "GET" # Default GET
      successCondition: "response.statusCode == 200" # available since v3.3

일단 감을 잡기 위해 조금 더 실습을 진행한다.
image.png
테스트를 해보니 http 템플릿의 경우 레거시 서비스어카운트 토큰을 활용하는 것이 보인다.
그러나 현 버전에서는 이 방식이 사라지고 projected 볼륨으로 짧은 생명 주기를 가진 서비스 어카운트 토큰이 TokenRequest 리소스를 통해 요청되어 붙는 방식으로 바뀌었기 때문에, 마운팅이 진행되지 못하고 펜딩이 걸린다.
버그라면 버그인데, 아마 곧 업데이트가 되지 않을까 싶다.

apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: argo-workflow.service-account-token
  annotations:
    kubernetes.io/service-account.name: argo-workflow

아쉬운 대로 직접 만들었다.
업데이트가 되기 이전까지, http 템플릿은 사용하지 않는 것이 좋겠다.
image.png
그러면 이런 식으로 일단 워크플로우가 진행은 된다.
image.png
조금 수정을 했는데, onExit 필드에 http 템플릿을 쓰면 무조건 context canceled가 뜨는 것 같다.
이건 이유를 잘 모르겠다. - 지금 다시 보니 그냥 내 물리 자원 스펙 문제였을 수도
대체로 봤을 때 종료 핸들러에는 컨테이너 템플릿을 넣어서 아티팩트에 데이터를 올리거나, 웹훅에 메시지를 날리는 식으로 하는 듯하다.
근데 반복적으로 워크플로우를 만들어보았을 때, 성공할 때도 있었고 아닐 때도 있었다.
이건 단순한 내 노드의 사양 문제일 수도 있겠다는 생각이 들었다.
image.png
전부 같은 양식의 워크플로우인데, 저마다 결과가 다르다..ㅋㅋ
중간 워크플로우는 처음부터 context canceled가 뜨고는 이후 단계를 진행하지도 않는다.

최종적으로 세팅해본 워크플로우는 다음과 같다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: complicated
spec:
  serviceAccountName: argo-workflow
  entrypoint: diamond
  arguments:
    parameters:
    - name: url
      value: "nginx.default.svc"
  volumeClaimTemplates:
  - metadata:
      name: complicated   
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Mi
  templates:
  - name: diamond
    dag:
      tasks:
      - name: A
        template: check-artifact
        arguments:
          artifacts:
          - name: git
            git:
              repo: https://gitea.zerotay.com/admin/develop.git
              revision: "main"
              usernameSecret:
                name: gitea-creds
                key: username
              passwordSecret:
                name: gitea-creds
                key: password
              singleBranch: true
              branch: main
      - name: B
        dependencies: [A]
        template: gen-random-int
        arguments:
          parameters: [{name: message, value: B}]
      - name: C
        dependencies: [B]
        template: print-output
        arguments:
          parameters:
          - name: message
            value: "{{tasks.B.outputs.result}}"
      - name: D
        dependencies: [A]
        template: cat-os-release
        arguments:
          parameters:
          - name: image
            value: "{{item.image}}"
          - name: tag
            value: "{{item.tag}}"
        withItems:
        - { image: 'debian', tag: '9.1' }       
        - { image: 'debian', tag: '8.9' }      
        - { image: 'alpine', tag: '3.6' }     
        - { image: 'ubuntu', tag: '17.10'}   
      - name: E
        dependencies: [D]
        template: print-volume
      - name: F
        dependencies: [C, E]
        template: echo
        arguments:
          parameters: [{name: message, value: F}]

  - name: check-artifact
    inputs:
      artifacts:
      - name: git
        path: /mnt/git
    container:
      image: alpine:latest
      command: [sh,-c]
      args: ["cp -r /mnt/git /mnt/vol/git"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: gen-random-int
    script:
      image: python:alpine3.6
      command: [python]
      source: |
        import random
        i = random.randint(1, 100)
        print(i)

  - name: print-output
    inputs:
      parameters:
      - name: message
    script:
      image: alpine:latest
      command: [sh, -c]
      source: |
        echo {{inputs.parameters.message}}

  - name: cat-os-release
    inputs:
      parameters:
      - name: image
      - name: tag
    container:
      image: "{{inputs.parameters.image}}:{{inputs.parameters.tag}}"
      command: [sh, -c]
      args: ["cat /etc/os-release >> /mnt/vol/test"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: print-volume
    container:
      image: alpine:latest
      command: [sh, -c]
      args: ["cat /mnt/vol/test"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: echo
    inputs:
      parameters:
      - name: message
    container:
      image: alpine:3.7
      command: [echo, "{{inputs.parameters.message}}"]

문서에서 본 내용들을 거의 다 때려박아본 것이다.
아티팩트 세팅 중, 깃 관련 세팅을 어떻게 하는지 나오는 예시 파일을 찾아서 참고했다.[9]
문서에는 아티팩트 관련 내용이 자세히 나와있지 않지만, 예시 파일이 많아서 참고할 만하다.
image.png
엄.
image.png
오? 문서를 보면서 봤던 것들 일단 다 때려박아본 건데 생각보다 잘 된다.
원래 마지막에 http 템플릿을 사용했는데, 위의 실습에서 http 템플릿이 여러 문제가 있다는 것을 알게 돼서 빼버렸다.
image.png
요런 식으로, 이쁘게 나온다!
뭔가 시각적으로 보이니까 알게 모르게 충족감이 있다 ㅋㅋ
image.png
ui로 생각보다 많은 내용들을 확인할 수 있다.
어차피 cli로도 다 할 수 있는 작업들이기는 하다.
혹시 데이터를 스토리지에 남길 수도 있나 싶어서 pvc를 써본 건데 워크플로우가 끝난 이후 pvc도 삭제됐다.
스토리지 클래스 회수 정책을 세팅하면 데이터야 남아있겠다만, 최소한 워크플로우에서는 그런 용도로 쓰라고 만든 게 아닌 건 확실하다.
image.png
파드는 이런 식으로 성공 상태로 남았다.
여러 노드에 배치된 것이 보이는데, 더 상세하게 파보지는 못했으나 실행할 노드를 지정해서 워크플로우 간 지역성을 높여 효율을 도모하는 것도 가능하다고 봤던 것 같다.

argo get 웤플이름

(아니 근데 자동완성 지원을 할 거면 워크플로우에 대해서 자동완성되게 하라고 ㅡㅡ)
image.png
이렇게 더 예쁘게도 볼 수 있다.
쓰면서 느끼는 건데, 다양한 설정을 할 수 있는 것도 좋고, 생각보다 그리 어렵지도 않다.
그러나 아직 많이 발전해야 할 것으로 보인다.
워크플로우를 잘 짜면 같은 동작을 해도 에러가 안 나게도 할 수 있긴 한데, 애초에 에러가 안 나와야 할 상황에서 에러를 내뿜는 것이 퍽 아쉽다는 것이 내 생각이다.
아니면 조금 더 명확한 가이드라인이라도 제공해주면 좋겠다.
복잡하고 긴 파이프라인을 간결하고 가볍게 실행할 수 있다는 점에서, 아르고 워크플로우는 충분히 활용성이 있다고 느꼈다.

아르고 롤아웃 세팅

image.png
아르고 롤아웃은 애초에 대시보드가 로컬 테스트용으로 개발됐기에, 관리 콘솔로서 활용하는 것을 권장하지 않는다고 한다.
image.png
다른 네임스페이스도 웬만해서는 쓰지 않길 바란다..
아르고들은 뭔가 좋으면서도 뭔가 2프로 부족하다..!
(많은 기여자가 생겼으면..)
image.png
아무튼 이 녀석 웹페이지는 정말 아무런 인증과 관련된 기능이 없다.
image.png
롤아웃의 경우 kubectl의 플러그인으로서도 지원을 해주기 때문에 조금 더 통합적인 느낌을 살려서 세팅을 할 수 있다.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: bluegreen
spec:
  strategy:
    blueGreen: 
      activeService: bg-active
      previewService: bg-preview
      autoPromotionEnabled: false
      scaleDownDelaySeconds: 30
  replicas: 2
  selector:
    matchLabels:
      app: bluegreen
  template:
    metadata:
      labels:
        app: bluegreen
    spec:
      containers:
      - name: rollouts-demo
        image: argoproj/rollouts-demo:blue
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP
        resources:
          requests:
            memory: 32Mi
            cpu: 5m

첫 세팅은 이렇게 진행했다.

k argo rollouts create -f 파일이름

image.png
롤아웃은 트래픽을 보내는 서비스가 제대로 설정되지 않으면 애초에 파드를 만들지도 않는다.
세팅을 잘못 해서 서비스의 셀렉터가 같지 않았는데, 그것까지 캐치하여 상태를 보여주는 모습이다.
image.png
처음 리소스가 만들어졌을 때부터 스위칭에 대한 동작을 하는데, 당연히 첫 배포니 구체적으로 뭐가 되는 건 아니다.
image.png
ui로도 이쁘게 확인이 가능한데, 아르고 친구들 ui는 정말 칭찬해줘야 할 것 같다.
image.png
커맨드로도 이쁘게 볼 수 있어서 정말 좋다..!

k argo rollouts set image bluegreen rollouts-demo=argoproj/rollouts-demo:green

이미지 업데이트를 해본다.
명령어 자동완성 잘 돼서 정말 좋다..
image.png
즉각적으로 ui에 표시가 됐다.
image.png
자동 프로모션을 꺼두었기 때문에, 직접적으로 내가 결정을 내리기 전까지 배포는 진행되지 않는다.
image.png
프로모션을 진행하면 설정된 시간 동안까지는 블루 버전이 살아있으며, 그동안 롤백이 가능하다.
물론 그 이후에도 가능은 하지만, 대신 파드가 다시 실행돼야 하기 때문에 시간은 조금 더 소모된다.
image.png
각 서비스에는 알아서 파드 해시 셀렉터가 추가되어 원하는대로 트래픽이 정확한 버전에 전달된다.

시각화 쩔고 간편하다로 롤아웃이 끝나는 거라면 구태여 많이 사람들이 찾을 이유는 없다..
메트릭을 기반으로 카나리 배포를 하고 테스트가 가능하도록 세팅해보자.

image.png
프로메테우스에서 어떤 데이터를 받아오는 게 좋을까 하다가, 일단은 받는 패킷으로 테스트하기로 마음 먹었다.
레디네스 프로브를 설정했기에 내가 별도로 통신을 하지 않더라도 트래픽이 발생하기에 간단하게 테스트는 가능하다.
물론 제대로 하려면 따로 어플리케이션마다 메트릭을 노출하도록 하는 것이 좋을 것 같다.

음. 근데 어차피 그렇게 할 거면 그냥 프로브 자체로 메트릭을 걸어서 해도 될 것 같긴 하다.
음. 근데 어차피 그렇게 할 거면 그냥 자체적인 레디 상태로 배포가 되도록 해도 되긴 하다.
음. 그렇다면 프로브로는 할 수 없는 더 상세한 테스트를 진행할 수 있는 방법을 사용하는 것도 좋겠다.
원래는 프로메테우스를 사용하는 것이 좋겠다고 생각했는데, 직접적으로 어플리케이션의 http 메트릭을 노출하고 반환 코드를 수집해서 측정하는 정도가 아니면 크게 메리트가 없다는 생각이 들었다.
대신 다른 방법으로, 잡을 이용하는 방법이 있다.
말 그대로 컨테이너를 돌려서 특정 동작을 수행하여 분석을 진행하는 것인데, 이 경우에는 내가 원하는 코드를 이용해 간단하게 여러 엔드포인트에 대한 테스트가 가능할 것이다.
원한다면 인증 인가 테스트까지도 수행해볼 수 있겠다.
자료 자체는 역시 공식 레포의 예제를 참고했다.[10]

음. 근데 또 생각이 든 게, 어차피 나중에 eks에서 실습할 때 내 코드를 이용할 것이다.
그러면 내 코드에 처음부터 프로메테우스 메트릭을 넣어주면 되잖아..?
두 가지 방향을 설계 한다.

잠시 - 샘플 앱 만들기

import uvicorn

from fastapi import FastAPI, Query, Header, Cookie, Body, Path, status, Request
from fastapi.responses import JSONResponse,PlainTextResponse

from typing import Annotated, Union, Optional, List, Dict, Any, TypeVar, Generic, Literal, Type, overload
from pydantic import BaseModel, Field
from enum import Enum

from pprint import pprint
import os
from datetime import datetime

from prometheus_fastapi_instrumentator import Instrumentator


app = FastAPI()
instrumentator = Instrumentator().instrument(app)
instrumentator.expose(app, include_in_schema=False)

@app.get("/", response_class=PlainTextResponse)
def read_root(request: Request, x_forwarded_for: Union[str, None] = Header(default=None, convert_underscores=True) , body = Body):
    image_tag = os.getenv("TAG", "unknown")
    now = datetime.now()
    (client_ip, client_port) = request.client
    if x_forwarded_for:
        client_ip = x_forwarded_for
    request_url = request.url

    response= "This is test FastAPI server made by Zerotay!\n"
    response+= now.strftime("The time is %-I:%M:%S %p\n")
    response+= f"TAG VERSION: {image_tag}\n"
    response+= f"Server hostname: {request_url}\n"
    response+= f"Client IP, Port: {client_ip}:{client_port}\n"
    response+= "----------------------------------\n"
    return response

if __name__ == "__main__":
    uvicorn.run(
        "main:app", 
        port=80, 
        host='0.0.0.0', 
        reload=True, 
        # ssl_keyfile= 'pki/webhook.key',
        # ssl_certfile= 'pki/webhook.crt',
    )

이전에 api 서버 인증 인가용으로 만들어둔 웹서버를 조금만 수정했다.
나중에는 코드를 더 정리할 수도 있다.
혹시나 했지만 역시나 다양한 프레임워크들이 프로메테우스 메트릭을 노출할 수 있도록 하는 툴들을 제공하고 있었는데, 나는 fast api를 쓰기 때문에 관련 툴을 이용했다.
이제 이걸 이미지로 말아서 올릴 차례이다.

근래에 어떻게 해야 이미지 빌드를 효율적으로 할 수 있을까, 많은 고민을 했다.
어떻게 캐싱을 하는 게 좋을지, 어떻게 이미지 크기를 줄일지 등.
캐싱 전략은 아직 한참 도전해야 할 게 많은데, 멀티 스테이지 빌드는 이전에도 여러 번 해봐서 그나마 부담감 없이 도전할 수 있을 것이라 생각했다.
다만 poetry를 사용할 때 구체적으로 어떻게 가상환경을 넣어줘야 제대로 실행이 되는지 조금 막막했는데, 이 글들을 보면서 간단하게 성공할 수 있었다.[11]

# syntax=docker/dockerfile:1
# Keep this syntax directive! It's used to enable Docker BuildKit

################################
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.12-slim as python-base

    # Python
ENV PYTHONUNBUFFERED=1 \
    # pip
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PIP_DEFAULT_TIMEOUT=100 \
    \
    # Poetry
    # https://python-poetry.org/docs/configuration/#using-environment-variables
    POETRY_VERSION=2.0.1 \
    # make poetry install to this location
    POETRY_HOME="/opt/poetry" \
    # do not ask any interactive question
    POETRY_NO_INTERACTION=1 \
    # never create virtual environment automaticly, only use env prepared by us
    POETRY_VIRTUALENVS_CREATE=false \
    \
    # this is where our requirements + virtual environment will live
    VIRTUAL_ENV="/venv" 

# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VIRTUAL_ENV/bin:$PATH"

# prepare virtual env
RUN python -m venv $VIRTUAL_ENV

# working directory and Python path
WORKDIR /app
ENV PYTHONPATH="/app:$PYTHONPATH"

################################
# BUILDER-BASE
# Used to build deps + create our virtual environment
################################
FROM python-base as builder-base
RUN apt-get update && \
    apt-get install -y \
    apt-transport-https \
    gnupg \
    ca-certificates \
    build-essential \
    git \
    curl

# install poetry - respects $POETRY_VERSION & $POETRY_HOME
# The --mount will mount the buildx cache directory to where
# Poetry and Pip store their cache so that they can re-use it
RUN --mount=type=cache,target=/root/.cache \
    curl -sSL https://install.python-poetry.org | python -

# used to init dependencies
WORKDIR /app
COPY poetry.lock pyproject.toml ./
# install runtime deps to $VIRTUAL_ENV
RUN --mount=type=cache,target=/root/.cache \
    poetry install --no-root --only main

################################
# DEVELOPMENT
# Image used during development / testing
################################
FROM builder-base as development

WORKDIR /app

# quicker install as runtime deps are already installed
RUN --mount=type=cache,target=/root/.cache \
    poetry install --no-root --with test,lint

EXPOSE 8000
CMD ["bash"]


################################
# PRODUCTION
# Final image used for runtime
################################
FROM python-base as production

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
    apt-get install -y --no-install-recommends \
    ca-certificates && \
    apt-get clean

# copy in our built poetry + venv
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $VIRTUAL_ENV $VIRTUAL_ENV

WORKDIR /app
COPY poetry.lock pyproject.toml ./
COPY . ./

ENV TZ=Asia/Seoul
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ARG TAG
ENV TAG=${TAG}
EXPOSE 8000
CMD ["python", "./main.py"]

실질적으로는 그냥 거의 베껴왔고, 아주 조금의 커스텀만 더했다.[12]
image.png
잘 될까 조금 걱정됐는데, 일단.. 크기를 확실히 줄이면서 정상적으로 빌드가 됐다.
멀티 플랫폼 빌드까지 완료됐으니, 나중에 실제 실습을 할 때 캐싱을 어떻게 할지 조금 더 공부하면서 진행하면 될 것 같다.
현재는 로컬 환경에 캐싱 디렉토리를 두는 방식으로 돼있다.
어차피 디렉토리는 정해져 있으므로, 해당 공간을 어떻게 할지에 대한 전략을 고민하면 된다.

이미지 빌드를 효율적으로 하기 위한 전략은 다양하다.
이 부분은 아래에서 실제 실습을 하면서 구체화할 것이다.
image.png
첫 빌드는 4분이 넘게 걸렸다.
이때 실수로 프로세스의 주소 바인딩에 아무 값도 넣지 않아서 127.0.0.1로만 트래픽을 받길래 수정해줬다.
image.png
로컬 캐싱으로 인해 두번째 빌드는 20초밖에 걸리지 않았다.
그것도 실질적으로 푸시를 하는데 걸린 시간이다.
image.png
이제 제대로 동작한다!
image.png
정상적으로 메트릭도 노출해주고 있으므로, 본격적으로 다시 롤아웃 실습으로 돌아가자.

아르고 롤아웃 테스트

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: canary
spec:
  strategy:
    canary:
      canaryService: canary-service
      stableService: stable-service
      trafficRouting:
        nginx:
          stableIngress: canary-ingress
      steps:
        - setWeight: 10 # 새 버전을 10퍼센트로 맞춘다.
        - pause:
            duration: 1m
        - setWeight: 20
        - pause: {}
        - setCanaryScale:
            replicas: 3
        - setCanaryScale:
            weight: 25
        # 기존의 setWeight에 해당하는 값으로 맞춰짐
        - setCanaryScale:
            matchTrafficWeight: true
        - pause: {}
        - analysis:
            templates:
            - templateName: http-analysis
            args:
            - name: service-name
              value: canary.zerotay.com
  replicas: 10
  selector:
    matchLabels:
      app: canary
  template:
    metadata:
      labels:
        app: canary
    spec:
      containers:
      - name: canary
        image: zerotay/zero-web:0.0.4
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        resources:
          requests:
            memory: 32Mi
            cpu: 5m

사실 analysis 템플릿쪽은 아직 제대로 작성이 안 돼서, 가장 마지막 순서로 밀어넣고 인라인으로 실행되도록 했다.
최소한 내가 pause를 풀지 않는 이상 분석이 실행되지는 않을 것이다.
image.png
이 녀석도 에러를 잘 알려주는 편이라 꽤 편하다.

apiVersion: v1
kind: Service
metadata:
  name: canary-service
spec:
  selector:
    app: canary
  type: ClusterIP
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: stable-service
spec:
  selector:
    app: canary
  type: ClusterIP
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: canary.zerotay.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: stable-service
            port:
              number: 80
  tls:
    - hosts:
      - "*.zerotay.com"
      secretName: zero-domain

인그레스와 서비스도 역시 간단하게 세팅했다.
현재 일차적인 목적은 기본 워크로드를 배포한 이후에 수집되는 메트릭을 확인하고 이 메트릭을 기반으로 analysis 템플릿을 작성하는 것이다.
image.png
기본적인 배포가 완료됐다.
image.png
뭔가 문제가 있는 건가 했는데, 고것은 그저 내 컴퓨터 사양 문제였습니다..
image.png
흐음.. 노드가 죽었다..
image.png
실제 컴퓨터의 용량에는 아직 여유가 있는데, 아무래도 메모리를 2기가만 준 것이 화근이었던 모양이다.
그러나 전체적으로 컴퓨터가 완전히 느려진 것이, 스펙의 한계에 부딪히기 직전이라는 것이 느껴진다.
image.png
한참 기다리니 노드에 접속이 가능해졌는데, 그간 kubelet이 재기동된 흔적이 있다.

전체 클러스터 스펙의 부족으로 발생하고 있는 현상이라 판단하고 새 노드를 추가해보는 게 좋을까?
아니면 워크로드 몇 개를 줄이는 방향으로?
image.png
일단 OOM 문제는 확실해졌다.
그러나 지금 노트북까지 느려진 것으로 보아, 노드를 추가하는 것이 해결점으로 보이지는 않는다.

당장의 실습을 하는 것에 바빠서 안 하려고 했는데, 아무래도 다른 자원을 끌어다써야겠다..

클러스터 자원 부족 해결하기 - 노드 추가

여태 한 노트북으로 클러스터를 운영하고 있었는데, 결국 터질 게 터졌다.
노트북을 장만한 이후로 오랫동안 묵혀뒀던, 내 데스크톱을 구동시킬 필요성이 생겼다.
윈도우인데, 윈도우 노드를 쓰는 것은 익숙치 않기도 하고 트러블슈팅할 자신도 없어서 윈도우에서도 마찬가지로 vm을 띄워서 노드를 추가시키려고 한다.
순식간에 집안 전기세 빌런이 되어버리겠군..
image.png
일단 당장의 문제를 해결하고자 모든 노드를 종료시켰다.
노트북 열을 좀 식히면서, 윈도우 세팅을 할 예정이다.
노트북은 팬을 돌려서 충분히 열이 식고 난 이후에 재부팅을 해야겠다.
이 상태로 argo cd까지 올렸다간 정말 아무것도 못하는 상태가 되어버릴 것이다.

현재 작업 상황 정리.
윈도우 컴퓨터에 가상환경을 어떻게 하고 자시고, 오랜만에 사용하려니 남는 hdmi 케이블도 없고, 키보드도 없고 해서 시간이 많이 지체됐다.
거기에 추가적으로 어떻게 가상환경을 구성하는 것이 좋을까에 대한 고민을 했다.
그나마 현재 가장 익숙한 것은 vagrant라고 할 수 있는데, 원격으로 접근할 수 없다는 것이 아깝다는 생각이 들었다.
그래서 다른 선택지가 없나 찾아봤는데, 일단 virtualbox가 가장 좋은 선택지인 것 같기는 했다.
vmware는 유료 버전을 사용하는 게 아니면 상당히 제한적이고, vsphere가 뭔지도 찾아봤는데 이건 윈도우를 밀어야 해서 기각했다.
앞으로 윈도우는 종종 쓰일 일이 많기 때문에 그냥 밀어버릴 순 없다.
가상환경 자체는 virtualbox로 정하고, 다음은 어떻게 프로비저닝을 할까에 대한 고민 지점이 생겼다.
근데 내가 기대했던 것은 가상환경 툴 중에 원격 관리가 가능한 게 없을까였는데, virtualbox는 또 그런 기능이 없더라고.
결국 내 노트북에서 관리를 하기 위해서는 ssh를 하는 것은 필수적이라는 결론이 나왔다.

그럼 결국 vagrant다..!
간단하게 설치 후 세팅을 해봤는데, 일단 문제 없이 돌아가는 것을 확인했다.
이제 단 한 가지 이슈만이 남았는데, 바로 현재 클러스터를 같은 네트워크 상에서 노출시키는 것이다.
이게 사실 엄청난 문제인 것이, 현재 api서버가 바인딩된 주소가 가상 환경끼리만 사용할 수 있는 주소라는 점이다.
일단 virtualbox차원에서 vm을 밀지 않고 브릿지 네트워크를 추가하는 건 가능하다.
사실 노트북에서 iptables만 살짝 만져주면 노트북으로 들어오는 트래픽을 api서버로 가게 하는 것도 충분히 가능할 것이다.
그러나 노트북 설정은 노트북에서, 클러스터 설정은 클러스터에서, 최대한 설정을 완벽히 분리하고 싶은 내게는 달갑지 않은 방식이다.
그럼 클러스터를 밀지 않고 api서버의 주소를 변경할 수 있는가?
안 될 것까지는 없다고 생각한다.
api 서버 그냥 재기동하면 되는 거니까..
다만 kubelet과의 통신 상의 주소도 바뀌는 꼴이니 이 부분도 수정해야 한다.
다른 컴포넌트들과의 연결성을 위해서는 많은 것들을 다시 설정해야 할 가능성이 높다.
시간이 있다면 이것도 경험이겠거니 하면서 냅다 도전하겠는데, 현재 해야 하는 일이 너무 밀리게 될 가능성이 높다.

당장 내린 결론은, 클러스터를 싹 밀고 다시 설치하는 것이다.
여태 클러스터 설치 한 두번 해본 것도 아니고, 그냥 열심히 만들어둔 양식 파일로 다시 하면 그만이다.
심지어 이번에는 다른 애드온과 툴들을 전부 테라폼 코드로 정리를 해놔서 추가 세팅하는 것도 오래 걸리지 않을 것이라는 게 내 생각이다.
이번주 초부터 괜히 테라폼을 열심히 조진 게 아니니..
근데 한가지 문제가 더 생겼다.
그러고 보니 내 데스크톱은 현재 공유기 아래 놓여있지 않고, 모뎀에 바로 꽃혀있다는 것.
이 상황을 해결할 방법이 있긴 하다.
공유기가 있는 거실에는 랜선 포트가 두 개가 있으니, 공유기에서 나가는 선을 하나 꼽고 이걸 모뎀이 있는 벽 장판에서 내 방으로 연결되도록 꼽는 것이다.
이때 모뎀에 바로 꽂혔던 포트만 뽑아주면 되니 그렇게 어렵지는 않을 것이라 생각한다.
근데 이것을 당장 해결할 수 없는 큰 문제가 있다.
누나 방에 그 판이 있는데, 누나가 평일에 출입을 허락하지 않았다..!
주말에만 하라고 하니, 지금으로서는 내 데스크톱을 노드로 활용할 방법이 없다.

그런 관계로.. 이제부터의 실습은 일단 eks에서 진행하고자 한다.
원래 목표는 내 로컬에서 기본적인 실습을 마치고 eks에서 툴을 조금만 다르게 써서 다시 똑같은 상황을 만드는 것이었다.
로컬에서는 현실적 제약사항에 부딪혔는데, 어차피 eks에서 할 것도 그렇게 다른 종류의 것은 아니기에 그냥 해도 문제될 것은 없다.
그래도 로컬에서 해야 속도도 빠르고 돈도 안 나가서 좋았는데, 어쩔 수 없다.

여담.
나중에 여력이 된다면 앤서블도 공부해두면 좋겠다.
어차피 ssh로 접근해야 할 것들, 조금 더 자동화할 수 있는 것들을 최대한 정리해두면 도움이 될 것 같다.
그리고 오랜만에 윈도우를 켰는데 윈도우 10 지원이 곧 종료된다 해서 후딱 11로 업그레이드했다.
tpm 2.0이 활성화 안 돼 있어서 bios 화면 들어가서 냅다 키니 다른 이슈 없이 업그레이드가 가능했다.

eks에서 cicd 실습 - 아르고 롤아웃부터

지금부터는 다시 eks로 넘어온다.
자원 부족 문제가 없도록, 카펜터를 이용해 클러스터 오토스케일링이 용이하게 만들었다.
그리고 이제부터는 원래 계획에 따라, ecr과 깃헙을 이용하여 실습을 진행하는 방향으로 빠르게 전환한다.
ecr의 경우 이전에도 파게이트에 연결되도록 엔드포인트를 인터페이스 엔드포인트를 뚫은 경험이 있어 이걸 활용할 것이다.
마침 테라폼으로 vpc 엔드포인트를 뚫는 리소스까지 담긴 예제가 있어 이것의 도움을 받을 것이다.[13]

깃헙의 경우에는, 자동화가.. 가능한가?
https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository
오잉? 이왜진? 테라폼 갓..

번외 - 오잉 kube ops view의 상태가

image.png
아까부터 kube ops view가 내가 원하는 네임스페이스 배치되지 않는 현상을 확인했다.
계속 default에 배포가 돼서 확인해봤는데, 일단 템플릿에서는 네임스페이스를 설정하는 게 없긴했다.
내가 이해하는 바에서는 이럴 때 네임스페이스 옵션을 주면 해당 네임스페이스로 배포가 돼야 하는데..
image.png
테라폼 설정이 잘못 되기라도 한 것인가?
잘은 모르겠지만, 일단 그냥 default 네임스페이스에 설치했다.

ecr 세팅

nslookup으로 내부 ip가 나온다면 정상적으로 세팅된 것이다.
이제 간단하게 이미지를 올릴 수 있는 테스트해본다.
image.png
아뿔싸.. 도커 명령어가 없을 것이라 생각하지 못했다.
image.png
기본적으로는 성공적으로 올라가는 것이 보인다.
image.png
이미지가 정상적으로 올라갔다.
image.png
해당 이미지를 이용해 파드를 띄워 정상 실행되는 것까지 확인할 수 있었다.

REGISTRYNAME=134555352826.dkr.ecr.ap-northeast-2.amazonaws.com
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $REGISTRYNAME
TAG=0.1.2
docker buildx build --push --build-arg TAG=$TAG --tag $REGISTRYNAME/zero-web:$TAG .

여유가 없어서 당장은 멀티 플랫폼 빌드와 캐싱은 후순위로 미루고 진행한다.

깃헙 세팅

깃헙도 테라폼으로 세팅할 수 있다는 것을 알게 됐으니 테스트 레포지토리를 자유롭게 만들기 편하겠다.
image.png
일단 깃헙에 접근하기 위한 토큰을 발급 받는다.
사실 어떤 권한들을 줘야 할 지 명확하게 감을 못 잡아서 거의 다 때려박았다..

코드

image.png
인증만 제대로 된다면, 쉽게 만들어진다.
image.png
여담이지만 풀리퀘를 날리거나, 파일을 올리는 작업을 테라폼으로 할 수도 있기는 하다.

아르고 롤아웃 테스트

k argo rollouts dashboard

급하게 테스트하는 관계로 그냥 로컬 포트포워딩을 시켰다.
image.png

image.png
메트릭이 수집되는 것도 확인했다.
조금 헤맸던 게.. 서비스가 셀렉팅하는 라벨을 생각해서 스크래핑을 하는 게 아니라 서비스가 자체로 가진 라벨을 이용해서 먼저 서비스를 찾은 뒤에 스크래핑을 하는 거였다..
사실 서비스란 건 그저 엔드포인트를 만드는데 도움을 주는 리소스이고 실질적으로 파드로 트래픽을 보내는 핵심 리소스는 엔드포인트인데, 왜 굳이 서비스를 한 단계 더 걸쳐서 매핑시키는 걸까?
어차피 엔드포인트를 기반으로 하면 준비 상태가 아닌 파드는 엔드포인트에서 제외되니 결국 서비스를 대상으로 하는 것과 같은 효과를 얻을 텐데 말이다.
이 부분에서 혼동을 겪었는데, 다시 생각해보면 서비스 모니터는 정말 서비스를 대상으로 하는 것이 처음 설계 방향이었을 것이다.
그리고 그것이 대상이 되는 서비스를 확실하게 관리하는 데에는 도움이 되긴 할 것이다.
그래서 실질적으로는 엔드포인트를 기반으로 바로 대상 파드를 추적하는 것이 가능하더라도 구태여 서비스를 한 번 거치는 과정을 둔 것으로 추정된다.

while true; do kubectl argo rollouts get rollout canary;echo ----;sleep 2;done

cli로도 변화를 추적하면서 이미지 업데이트를 진행해본다.
image.png
이미지 업데이트는 어떻게 하던 상관 없다.
어떻게든 파드 템플릿 부분에 변화를 주기만 하면 된다.
image.png
조금씩 업데이트가 진행되고 있다.
가중치 설정에 맞게 카나리 버전이 늘어나고 있다.
image.png
설정해둔 인그레스의 어노테이션을 보면, 롤아웃이 추가시켜준 액션 사항이 보인다.
image.png
어노테이션에 따라 실제로 트래픽을 나눠주는 것은 순전히 alb 컨트롤러의 몫이고, 어노테이션 세팅에 따라 실제로도 적절하게 분산되도록 설정이 됐다.
image.png
setcanaryscale은 트래픽만 늘리는 설정이라고 했던 것 같은데, 막상 해보니 그냥 가중치를 늘리는 것과 똑같은 방식으로 동작했다.
이 부분은 내가 정반대로 이해한 것으로 보인다.
setweight를 할 때 설정되는 것이 트래픽 가중치이고, setcanaryscale 필드는 실제 레플리카를 설정한다.
생각해보니 정직한 필드 이름이네..
image.png
analysis템플릿을 잘못 짜서 에러가 발생하면 이런 식으로 표시된다.
count 값은 실제로 분석을 진행하는 횟수 자체를 말하며, 그렇기 때문에 실패 제한과 연속 성공 최소치의 값보다는 무조건 커야 한다.
분석이 실패하면 abort가 나고, 알아서 롤백이 된다.
image.png
제대로 분석이 진행된다면 위처럼 어떤 메트릭이 수집됐는지까지도 나오는 것을 볼 수 있다.
하고나서야 알았는데, value가 응집되지 않도록 쿼리를 짰다..
아무튼 분석을 진행하는 데에 크게 상관이 있지는 않으므로 계속 실습을 진행했다.
image.png
근데 문득 보다보니 느끼는 건데, 웹으로 시각화한 것도 좋지만 그냥 cli 환경에서보는 것도 나쁘지 않다.
cli에서 아쉬운 것은 한 가지, 현재 스텝을 파악하기 어렵다는 것.
image.png
현 템플릿 상 평가는 5분 간격으로 이뤄지는데, 그 사이에 나온 출력값이 조금 달라졌다.
쿼리는 잘 짜도록 하자 음.
테스트 용으로서는 아예 fail만 나게 하는 경로도 마련했으면 좋겠다는 생각이 든다.
이건 총체적인 cicd를 할 때 엔드포인트를 구현해놔야겠다.
image.png
성공 최소치를 설정하지 않았기 때문에 카운트 개수만큼 분석이 되는 것을 확인할 수 있다.
image.png
모든 스텝이 성공적으로 끝나고, 카나리 버전이 전부 배포됐다.
롤백을 고려하여 이전 버전도 일정 시간 동안 계속 남아있는 것을 확인할 수 있다.

여기에서도 노드를 죽여버렸다..!
카펜터가 제대로 동작하지 않았다.
image.png
생각해보면 이건 내 잘못이긴 하다.
애초에 많은 애드온들을 설치하면서 대충 어느 정도로 리소스를 사용하는지 확인하고 이를 기반으로 최소치를 request 했어야 했다.
결국 한계를 넘은 노드가 펑..
절대 이 노드에는 문제가 없어야 한다고 생각하면서도 남는 공간은 조금 활용하면 어떻겠냐 방심한 잘못이다.
이런 문제를 없애려면 처음부터 자원 요청을 잘하면서, 역시 카펜터를 파게이트에 올리는 식으로 운영하는 것이 좋지 않나 싶긴 하다.
지금이야 내 잘못이지만, 카펜터가 위치한 노드 역시 언제 어떻게 죽을지는 부처님도 모른다.

image.png
문제없이 카펜터가 노드를 스케일링 해주고 있는 모습.
이번에 다시 세팅을 하면서 모든 워크로드에 기본 자원 요청량을 설정했다.
조금 오버해서 자원을 쓰게 된다고 해도 당장 서비스에 장애가 발생하지 않는 것이 더 중요하다고 판단했고, 이후에 적절한 자원 요청량에 대해서는 모니터링을 하면서 맞춰가는 것이 좋다는 것이 내 판단이다.
pdb까지 세팅하진 않았으나, 몇 가지는 Topology Spread Constraints까지 세팅했다.
카펜터가 멋대로 최적화를 하다가 서비스 장애가 나는 불상사를 최대한 줄이기 위함이다.
카펜터는 쿠버 스케줄링을 고려하므로 여러 노드에 분산 배치가 돼야 하는 워크로드의 경우 이를 적절하게 고려하여 노드를 추가 배치해줄 것이다.
image.png
모든 스텝이 성공적으로 완료되자 stable 서비스는 새 버전을 가리키게 된다.
image.png
실제로 stable 서비스의 셀렉터를 보면 롤아웃이 추가시켜준 셀렉터가 새 버전의 레플리카셋 해시를 가리키는 것을 볼 수 있다.
image.png
그리고 scaleDown 30초가 지난 후 이전 버전은 완전히 사라졌다.

아르고 cd 세팅

image.png
기본으로 구성한다고 했을 때, 아르고 cd는 꽤나 많은 워크로드를 필요로 한다.
image.png
아르고 서버의 페이지를 만드는 과정에서 위와 같은 에러가 발생할 수 있는데, 이것은 grpc 설정에 있어서 alb 컨트롤러가 내뱉는 에러이다.
어떤 식으로 세팅해야 하는지는 여기에서 조금 더 자세히 확인할 수 있다.[14]
image.png
이제 기본 아르고 대시보드가 나왔다.

k -n argo-cd get secrets argocd-initial-admin-secret -oyaml

초기 관리자 비번을 확인하고, 이를 통해 들어간다(base64 인코딩 유의!).
image.png
이제 본격적으로 운영팀이 사용할 깃 레포지토리를 등록하자.
image.png
처음 세팅은 간단하게 진행한다.
image.png
단순하게 세팅하면 당연히 프라이빗 레포지토리이므로 argo cd가 접근할 수 없다.
image.png
그래서 먼저 세팅으로 프로젝트에서 사용할 레포지토리를 만든다.
settings 탭에 들어가면 레포지토리를 설정하는 부분이 있어 여기에서 먼저 세팅을 진행할 수 있다.
주의점이 두 가지 있다.

helm create sample-helm

image.png
기본적인 헬름 차트를 만들고 이 상태로 푸시를 했다.
image.png
이제 어플리케이션을 만들 때 레포지토리 url이 표시되며, 내용물이 추적된다.
image.png
아예 내용물이 추적돼서 ui로 설정할 때 관련한 values.yaml 파일이 표시되는 것까지 볼 수 있다.
image.png
어플리케이션이 만들어진 후 10초도 안 돼서 아무것도 없던 cicd 네임스페이스에 워크로드가 배포된 것을 확인할 수 있다.
image.png
ui로 예쁘게 현재 상태를 확인할 수 있다!
image.png
간단하게 변경사항을 만들어 푸시를 해본다.
image.png
깃의 변경사항을 추적하여 argo cd가 업데이트를 진행한 모습이 확인된다.
image.png
깃을 통해서가 아니라 직접적으로 레플리카를 변경하자 OutOfSync 상태가 된 것을 확인할 수 있다.
image.png
변경한 리소스에서 diff를 눌러보면 어떤 부분이 바뀌었는지도 확인할 수 있다.
image.png
원상 복구를 할 수 있도록 syncronize를 진행한다.
image.png
레플리카의 개수를 상태 관리하고 있을 뿐, 어떤 파드가 유지될지를 지정한 것은 아니기에 싱크가 맞춰지며 깃 커밋으로 인해 생겼던 파드가 삭제된 것을 볼 수 있다.
상태 유지가 정말 중요한 어플리케이션이라면 스테이트풀셋을 활용하는 것이 훨씬 안전할 것이다.

k get application -oyaml

image.png
간단하게 웹ui를 통해서 확인했지만, 결국 전부 클러스터 리소스로서도 유지가 되기 때문에 이렇게 cli로 crd를 확인할 수 있다.

app of apps 패턴까지 실습을 하고 싶지만, 일단 cicd 파이프라인을 완성하는 것을 우선 목표로 삼았기 때문에 여기에서는 생략한다.

cicd 실습

ci

처음 생각한 방향대로 구축하기 위해서는, 먼저 개발팀 깃의 변경사항에 따라 이미지를 빌드하여 ecr에 올리는 파이프라인이 필요하다.

buildctl build ... \
  --output type=image,name=docker.io/username/image,push=true \
  --export-cache type=inline \
  --import-cache type=registry,ref=docker.io/username/image

인라인 이미지 캐싱은 이미지에 붙는 이미지 정보에 캐시를 때려박는 식.

buildctl build ... \
  --output type=image,name=localhost:5000/myrepo:image,push=true \
  --export-cache type=registry,ref=localhost:5000/myrepo:buildcache \
  --import-cache type=registry,ref=localhost:5000/myrepo:buildcache

레지스트리 캐싱
export 옵션

buildctl build ... \
  --output type=image,name=docker.io/username/image,push=true \
  --export-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image \
  --import-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image

s3로 캐싱.

image.png

curl $ARGO_SERVER/api/v1/events/argo/my-discriminator \
    -H "Authorization: $ARGO_TOKEN" \
    -H "X-Argo-E2E: true" \
    -d '{"message": "hello events"}'

https://argo-workflows.readthedocs.io/en/stable/webhooks/
image.png
첫 술에 잘 되란 법은 없다.
image.png
처음 argo를 공부할 때 유의하라고 봤던 그 이유가 보인다.
기본적으로 아르고 시리즈들은 아르고가 설치된 네임스페이스에 대해 기본 세팅은 돼 있으나 다른 네임스페이스에 대해서는 어느 정도 커스텀을 해줘야 한다.
image.png
아주 귀찮게도 누구만 aggregate를 안 쓰는 관계로, 워크플로우 서버의 클롤 자체를 수정하는 식으로 간다.
image.png

 - apiGroups:
   - ""
   resources:
   - serviceaccounts
   verbs:
   - get
   - list

서비스어카운트는 코어 그룹이니 대애충 get list 딸깍 주자.
image.png
문제에 진전이 있다.
단순히 서비스어카운트 토큰 이름에 오타를 내서 발생한 문제였고, 다시금 서비스 어카운트 이름에 맞는 시크릿 토큰을 만들어주었다.
image.png
이렇게 200이 나오면 성공이다.
image.png
대충 만든 템플릿이 트리거되어 워크플로우가 만들어졌으니, 이제 본격적으로 템플릿을 수정해보자.

argo template update

템플릿을 그냥 apply하면 변경사항이 제대로 적용되지 않으므로, argo cli를 사용해주는 것이 좋다.

k create secret docker-registry docker --from-file ~/.docker/config.json

이전에 ecr에 로그인한 정보는 위 파일 경로로 저장되기에 이걸 그대로 활용해주면 된다.
요지는 저 파일의 형태따라 시크릿을 만들어주면 된다는 것.
image.png
해보면 이런 식의 데이터가 시크릿에 들어가는 것을 볼 수 있다.
image.png
오예!

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: ci-template
spec:
  serviceAccountName: argo-workflow
  entrypoint: main
  arguments:
    parameters:
    - name: repo
      value: https://github.com/Zerotay/aews-dev.git
    - name: registry
      value: 내ecr
  templates:
    - name: main
      dag:
        tasks:
          - name: parse-tag
            template: parse-tag
            arguments:
              parameters:
              - name: ref-tag
                value: "{{workflow.parameters.ref-tag}}"
          - name: build
            template: build
            arguments:
              parameters:
              - name: registry
                value: "{{workflow.parameters.registry}}"
              - name: tag
                value: "{{tasks.parse-tag.outputs.result}}"
              artifacts:
              - name: git
                git:
                  repo: "{{workflow.parameters.repo}}"
                  revision: "main"
                  usernameSecret:
                    name: github-creds
                    key: username
                  passwordSecret:
                    name: github-creds
                    key: password
                  singleBranch: true
                  branch: main
            depends: "parse-tag"

    - name: parse-tag
      inputs:
        parameters:
        - name: ref-tag
      script:
        image: python:alpine3.6
        command: [python]
        source: |
          tag = "{{ inputs.parameters.ref-tag }}"
          tag = tag.split('/')[-1]
          print(tag)

    - name: build
      inputs:
        parameters:
        - name: registry
        - name: tag
        artifacts:
          - name: git
            path: /mnt/git
      container:
        image: moby/buildkit:v0.9.3-rootless
        workingDir: /mnt/git
        env:
          - name: BUILDKITD_FLAGS
            value: --oci-worker-no-process-sandbox
          - name: DOCKER_CONFIG
            value: /.docker
        command:
          - buildctl-daemonless.sh
        args:
          - build
          - --frontend
          - dockerfile.v0
          - --local
          - context=.
          - --local
          - dockerfile=.
          - --output
          - type=image,name={{inputs.parameters.registry}}/zero-web:{{inputs.parameters.tag}},push=true
        readinessProbe:
          exec:
            command: [ sh, -c, "buildctl debug workers" ]
        volumeMounts:
          - name: docker-config
            mountPath: /.docker
            readOnly: true
      volumes:
        - name: docker-config
          secret:
            secretName: ecr-creds
            items:
            - key: .dockerconfigjson
              path: config.json

워크플로우 템플릿은 이런 식으로 작성했다.
태그 정보는 깃 레포에서 받아온다.
그래서 깃에서 처음부터 태그를 달아야만 제대로 워크플로우가 진행된다.
도커 유저 정보는 docker-registry 유형의 시크릿을 만들 경우 .dockerconfigjson이라는 키를 가지게 되는데, buildctl의 경우 .docker/config.json 파일을 이용하도록 되어 있어 그냥 마운팅을 하면 제대로 값을 읽지 못한다.
애초에 일반 도커도 config.json을 읽는데 내가 제대로 된 활용법을 모르고 있는 것이 아닐까 싶기도 한데, 아무튼 특정 경로로 정확하게 파일을 넣기 위해 추가 설정을 넣어주었다.
태그 정보는 구체적으로 어떻게 가져오는가?

apiVersion: argoproj.io/v1alpha1
kind: WorkflowEventBinding
metadata:
  name: event-consumer
spec:
  event:
    selector: payload.sender.login == "Zerotay" && discriminator == "dev"
  submit:
    workflowTemplateRef:
      name: ci-template
    arguments:
      parameters:
      - name: ref-tag
        valueFrom:
          event: payload.ref

이벤트 바인딩 시 웹훅으로부터 받은 데이터를 인자로 넘길 수 있는데, 이렇게 설정해두면 웹훅이 왔을 때 발동되는 워크플로우에 해당 인자가 들어가게 된다.
https://github.com/argoproj/argo-workflows/blob/main/examples/buildkit-template.yaml
시간이 부족해서 캐싱 방식을 구체화하지는 못했다.
생각으로는 레지스트리 캐싱을 하거나, 아니면 볼륨을 하나 만들어서 캐싱 데이터를 저장한 후에 이걸 마운팅하고, 명령어 상으로는 로컬 캐싱을 하는 식으로 하는 것도 괜찮을 것 같다.
후자의 경우에는 efs와 같이 어떤 노드에서든 붙일 수 있는 스토리지를 활용하는 것이 좋을 텐데, 캐싱되는 데이터가 작지는 않을 것이라 생각이 들어서 비용적으로 좋은 선택인지 더 고민이 필요하다.
이런 고민들을 하다보니 빠르게 하지 못할 것 같아서 후순위로 미뤄둔다.

cd

남은 일은 이미지 레지스트리에 올라간 것을 바탕으로 cd를 하는 것인데, 사실 이건 간단하다.
내 cicd 흐름도에서는 개발팀과 운영팀이 확실하게 나뉘며 단순 버전 업데이트라 하더라도 운영팀이 한번 실제 배포 이전에 상황을 검토해야 한다.
그러므로 실제 배포는 수동으로 이뤄진다.
즉, 그냥 운영팀 레포에서 이미지 버전을 올려서 푸시를 하고, argo cd가 이 정보를 읽어 클러스터에 반영하면 성공이라는 뜻이다.
스테이징 환경에 배포하는 것은 argo image updater를 이용해서 하는 것까지 하려고 했는데, 이것도 시간이 부족해서 당장은 패스..

대신, cd를 할 운영팀은 안전하게 배포가 될 수 있도록 아르고 롤아웃을 이용하는 방식을 채택한다.
그래서 운영팀 레포의 헬름 차트에 이 사항을 구현해보자.
image.png
대충 만들어서 이랬던 놈이지만..
image.png
격변이 불어오기 시작했다!
이제 이미지 업데이트를 했을 때 정상적으로 롤아웃이 진행되기만 하면 성공이다.
image.png
이미지 업데이트를 진행하자 예정대로 두 개의 레플리카셋이 보이는데, 이건 롤아웃이 제대로 작동하고 있다는 뜻이다.
image.png
사진을 제때 못 찍었는데, 롤아웃의 ui로도 업데이트가 순조롭게 완료된 것이 확인된다.
image.png
카나리 배포에서 analysis까지 성공하며 완전히 새 버전으로 배포가 전환되고 있다.
롤아웃이 완료되기 전까지, argo cd에서는 명확하게 업데이트가 진행 중인 것을 인지한다.
이거야 당연하지만, 분석이 진행되며 생기는 analysisrun 리소스까지 정확하게 추적해주어서 상태 모니터링을 하기에 정말 편리하다.
image.png
analysisrun 리소스의 소유권이 롤아웃에 걸려있기에 제대로 추적되는 것을 확인할 수 있다.
image.png
배포가 완료된 이후에는 canary 서비스 자체는 인그레스에서 트래픽을 받지 않으므로 네트워크 구조도를 볼 때 명확히 분리되는 것도 확인할 수 있다.
물론 실제 대상이 되는 파드는 stable이나 canary가 같기는 하다.

스터디

데봅팀과 개발팀의 레포는 보통 다르다.

코드 저장소는 이번에 gogs라는 간단한 것을 사용해본다.
개발, 데봅 레포 둘로 나눈다.

젠킨스의 기본단위는 아이템
image.png
플러그인은 이것들

이풀백시
ecr은 ecr에 설정하는 바업ㅂ도 있다.
도커는 imagepullscrets 에 시크릿 리소스 넣는 것도 가능.
이때 시크릿 타입은 opaque가 아닌 docker-registry

        stage('approve version switching') {
            steps {
                script {
                    returnValue = input message: 'Green switching?', ok: "Yes", parameters: [booleanParam(defaultValue: true, name: 'IS_SWITCHED')]
                    if (returnValue) {
                        sh "kubectl patch svc echo-server-service -p '{\"spec\": {\"selector\": {\"version\": \"green\"}}}' --kubeconfig $KUBECONFIG"
                    }
                }
            }
        }

젠킨스에서, 이렇게 설정하면 젠킨스 웹에서 설정을 하게 할 수있다.
stage view에서 선택 가능.

아르고
쿠버네티스에서 선언적으로 깃 기반의 cd을 도와주는 툴
헬름을 같이 이용한다.

아르고는 클러스터의 모든 오브젝트를 추적하나?
내 맘대로 몇 가지 오브젝트를 더 만들면 어덯게 될까?
이런 것도 추적하나?
아니겠지?

아르고 이미지업데이터?
이미지 레지스트리를 보는데, 그럼 상세설정은?
깃으로 역으로 업데이트 해준다고 한다.
근데 이거 좋은 방법이 맞나?
오히려 책임 분리가 안되는 느낌인데?
새로 이미지 올릴 때 클러스터 상세 설정도 바뀌어야 하면?
어차피 이미지 세멘틱 버저닝할 꺼니까 전략만 잘 수립하면 괜찮다?
그건 그럴 수 있기는 한데, 어차피 다른 방법으로도 가능한데 왜 굳이 그렇게 해야함?

aoa, 앱오앱
최상단은 어플리케이션 오브젝트가 담긴 깃을 관리.
그 오브젝트들은 또 세부 어플리케이션을 관리.
아르고 클러스터부트스트래핑 문서에 있다ㅣ.
클러스터를 여러번 구축할 때 유용.

아르고 롤아웃은 평소 내가 생각해오던 디플의 불편함을 메꿀 수 있는 툴로 보인다.
디플의 업데이트 전략은 너무 제한적이다.
흔히 이야기되는 배포 전략들 막상 하려면 롤링 빼고는 다 직접 해야 한다.
이를 자동화시켜주거나 고급지게 세팅할 방법이 필요하다고 생각했는데 진짜 있네 ㄷㄷ

관련 문서

이름 noteType created

참고


  1. https://medium.com/@t-velmachos/build-docker-images-on-k8s-faster-with-buildkit-3443e36aef2e ↩︎

  2. https://www.slideshare.net/slideshow/kubeconeu-building-images-efficiently-and-securely-on-kubernetes-with-buildkit/146892857 ↩︎

  3. https://docs.docker.com/build/buildkit/ ↩︎

  4. https://stackoverflow.com/questions/46297949/is-there-a-way-to-share-secrets-across-namespaces-in-kubernetes ↩︎

  5. https://medium.com/@danieljimgarcia/dont-use-the-terraform-kubernetes-manifest-resource-6c7ff4fe629a ↩︎

  6. https://stackoverflow.com/questions/75457176/terraform-child-module-does-not-inherit-provider-from-root-module ↩︎

  7. https://argo-workflows.readthedocs.io/en/latest/argo-server/#api-authentication-rate-limiting ↩︎

  8. https://github.com/argoproj/argo-rollouts/issues/294 ↩︎

  9. https://github.com/argoproj/argo-workflows/blob/main/examples/input-artifact-git.yaml ↩︎

  10. https://github.com/argoproj/argo-rollouts/blob/master/examples/analysis-templates.yaml ↩︎

  11. https://github.com/orgs/python-poetry/discussions/1879#discussioncomment-7284113 ↩︎

  12. https://nanmu.me/en/posts/2023/quick-dockerfile-for-python-poetry-projects/ ↩︎

  13. https://alexhladun.medium.com/create-a-vpc-endpoint-for-ecr-with-terraform-and-save-nat-gateway-1bc254c1f42 ↩︎

  14. https://argo-cd.readthedocs.io/en/latest/operator-manual/ingress/#aws-application-load-balancers-albs-and-classic-elb-http-mode ↩︎