개요
`2023 오픈소스 컨트리뷰션`의 `Argo Workflows`에 합류하게 되었습니다. 여러가지 주제 중 `Argo Workflows`를 선택한 이유는 클라우드 엔지니어 또는 devops 엔지니어로 전향하는 것을 목표로 하고있는데, `CNCF`가 관리하는 프로젝트이기도 하고 희망하고자 하는 직무와 너무 잘 맞는다고 생각했기 때문입니다.
하지만 아직 `Argo Workflows`를 사용해 본적이 없기 때문에 클라우드 메이트 기술블로그 Argo를 사용하자 글을 참고하여 `CI/CD Pileline`을 구현해 보며 `Argo`와 친해져 보려 합니다!
아키텍처
`GitHub Source Code Repository`: 배포할 Application 코드 및 컨테이너 이미지를 빌드하기 위한 Dockerfile이 존재합니다.
`Argo Events`: Source Repository의 Push 이벤트가 발생하면 Webhook을 통해 Argo Events를 호출합니다. 호출받은 Argo Events는 정의된 내용에 맞춰 Workflow를 Trigger 합니다.
`Argo Workflows` : Argo Events로부터 Trigger된 Wrokflows는 Source Repository로 부터 Code를 Clone하는 Pull 단계, 그리고 그 코드를 기반으로 새로운 컨테이너 이미지를 빌드하는 Build 단계, 마지막으로 Argo CD가 바라보고 있는 YAML의 컨테이너 이미지 버전을 업데이트 하는 Push 단계로 작동합니다.
`Argo CD` : GitOps Repository에 정의된 YAML 파일의 정보를 확인하며 Kubernetes 클러스터에 Applicaion을 배포합니다.
준비물
`Argo project`를 사용하기 위해 Kubernetes가 필요합니다. 저는 `GCP`(Google Cloud Platform)의 GKE 프리티어를 사용하여 쿠버네티스 환경을 구성했습니다.
설치하기
`kubectl`을 `GKE`와 연결한 상태에서 터미널에 아래 명령어를 붙여넣으면 각 컴포넌트가 설치 됩니다! 설치되는 컴포넌트에 대한 설명은 아래 표를 확인하시면 됩니다.
구분 | 설명 |
`argo-cd` | Kubernetes 애플리케이션 배포 및 관리 도구 |
`argo-workflows` | Kubernetes에서 워크플로우를 정의하고 실행하는 기능 |
`argo-event` | 다양한 소스(Web Hook, S3 등)로 부터 이벤트를 감지하고 트리거 하는 기능 |
설치 쉘 스크립트
설치 쉘 스크립트가 수행되면 각 컴토넌트 설치 및 로그인 할 수 있는 `ID`, `PWD`가 표시 됩니다.
제 환경은 GKE이기 때문에 Service Type을 LoadBalancer로 변경하면 외부 접근이 가능하기 때문에 `kubectl patch service -n argo argo-server -p '{"spec":{"type":"LoadBalancer"}}'`설정을 통해 LoadBalancer로 배포할 수 있습니다.
만약 Minikube, k3d등 Local 환경을 사용중이신 경우 Metallb 설정 혹은 주석 처리된 `kubectl -n argocd port-forward service/argocd-server 8080:443 &` 명령을 통해 서비스를 포트포워딩 해주시기 바랍니다.!
GREEN='\033[0;32m'
RESET='\033[0;37m'
echo -e "${GREEN}
┌───────────────────────┐
│ Install start argo-cd │
└───────────────────────┘
${RESET}"
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/ha/install.yaml
kubectl rollout status deployment -n argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/rollout-extension/v0.2.1/manifests/install.yaml
echo -e "${GREEN}
┌──────────────────────────────┐
│ Install start argo-workflows │
└──────────────────────────────┘
${RESET}"
kubectl create namespace argo
kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v3.4.9/install.yaml
kubectl rollout status deployment -n argo
kubectl patch deployment \
argo-server \
--namespace argo \
--type='json' \
-p='[{"op": "replace", "path": "/spec/template/spec/containers/0/args", "value": [
"server",
"--auth-mode=server"
]}]'
echo -e "${GREEN}
┌───────────────────────────┐
│ Install start argo-events │
└───────────────────────────┘
${RESET}"
kubectl create namespace argo-events
kubectl apply -n argo-events -f https://github.com/argoproj/argo-events/releases/download/v1.8.0/install.yaml
kubectl apply -n argo-events -f https://github.com/argoproj/argo-events/releases/download/v1.8.0/install-validating-webhook.yaml
PASSWORD=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
echo -e "
USERNAME: admin
PASSWORD: ${PASSWORD}
Argo CD: https://localhost:8080
Argo Workflows: https://localhost:2746
"
kubectl patch service -n argo argo-server -p '{"spec":{"type":"LoadBalancer"}}'
kubectl patch service -n argocd argocd-server -p '{"spec":{"type":"LoadBalancer"}}'
#kubectl -n argocd port-forward service/argocd-server 8080:443 &
#kubectl -n argo port-forward deployment/argo-server 2746:2746 &
스크립트 실행 결과
스크립트가 정상적으로 실행되면 아래 코드가 콘솔에 출력됩니다. USERNAME과 PASSWORD는 `Argo CD` 접속 시 사용합니다. `Argo Workflows`는 별도의 계정 정보 없이 접속이 가능합니다.
USERNAME: admin
PASSWORD: zRtb3UQlKvdxV3Ry
접속 해보기
`Argo CD` : https://<loadbalancer-ip>:8080
`Argo Workflows` : https://<loadbalancer-ip>:2746
코드 리포지토리
코드 리포지토리를 생성합니다. 저의 경우 github에 리포지토리를 생성했습니다. `main.go`의 코드는 `Hello, World!`를 출력하는 Go 언어로 된 웹 서비스 이고, 이를 컨테이너 이미지로 빌드하기 위해 `Dockerfile`도 포함되어 있습니다.
이 리포지토리는 `Argo Workflows`에서 CI를 위해 사용됩니다.
tree .
.
├── Dockerfile
└── main.go
main.go
package main
import (
"fmt"
"log"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", index)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Dockerfile
FROM golang:1.20.1-alpine3.17 AS builder
WORKDIR /work
COPY . /work/
RUN go build -o server main.go
FROM alpine:3.14
COPY --from=builder /work/server /work/server
ENTRYPOINT ["/work/server"]
Image Build & Deploy(선택 사항)
# docker image build 하기
docker build -t godummyweb:1.0 .
# localhost 8080 port를 기반으로 배포하기
docker run -dp 8080:8080 --name goweb godummyweb:1.0
접속해보기
`Dockerfile`을 통해 `build`한 이미지로 배포 후 테스트 해봅니다.
GitOps 리포지토리
`GitOps`는 애플리케이션의 배포와 운영에 관련된 모든 요소를 코드화하여 깃(git)에서 관리합니다. 그리고 `Argo CD`는 코드화 된 배포 정보를 깃으로 부터 불러와 `k8s cluster`에 배포합니다. 따라서 이 리포지토리는 코드리포지토리의 소스를 기반으로 `Argo CD`에서 배포를 위해 사용됩니다.
`GitopsDummy`라는 이름의 github 리포지토리를 생성합니다. 그 후 `Argo CD`를 통한 배포를 위해 아래 `yaml`파일을 생성합니다. 해당 파일에는 코드리포지토리의 어플리케이션을 배포하는 `Deployment`와 `LoadBalancer` Type의 `Service`를 배포합니다.
deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-deployment
labels:
app: go
spec:
replicas: 2
selector:
matchLabels:
app: go
template:
metadata:
labels:
app: go
spec:
containers:
- name: go
image: kimhj4270/godummyweb:latest
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: go-service
spec:
selector:
app: go
type: LoadBalancer
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: 30080
Argo CD Application 생성하기
GitOps 리포지토리를 대상으로 `Argo CD`를 통해 `minikube` 클러스터에 배포하도록 설정합니다.
우선 `localhost:8080`주소로 `Argo CD`에 접속합니다.
설치 스크립트실행에 표시 됐던 계정정보로 로그인 합니다.
우선 GitOps 리포지토리 정보를 등록합니다.
`Setting` → `Repositories` → `Connect Repo`
`Repository URL`은 GitOps 리포지토리의 주소이고, `Username`과 `Password`는 Github 계정정보이며 비밀번호는 토큰값을 입력합니다.
`Application`을 생성합니다.
`Applications` → `New App`
결과적으로 GitOps의 deploy.yaml을 참고하여 1개의 Deployment, 1개의 Service 객체가 생성됩니다.
Argo Events, Argo Workflows 정의를 통해 CI/CD Pipeline 구성하기
아래 YAML 파일을 기반으로 CI/CD Pipeline을 구성할 수 있습니다. 하지만 생성 전 변경해야할 부분이 있습니다.
(`더보기`에 코드가 있습니다.)
apiVersion: v1
kind: Namespace
metadata:
labels:
app.kubernetes.io/instance: ci
app.kubernetes.io/name: ci
name: ci
---
apiVersion: v1
data:
.dockerconfigjson: <your-docker-config-base64-encrypt>
kind: Secret
metadata:
name: dockerhub-registry
namespace: ci
type: kubernetes.io/dockerconfigjson
---
apiVersion: v1
data:
gcs-iam.json: <your-GCP-IAM-json-base64-encrypt>
kind: Secret
metadata:
name: argo-gcp
namespace: ci
type: Opaque
---
apiVersion: v1
kind: Secret
metadata:
name: ssh-key-secret
namespace: ci
type: Opaque
data:
ssh-private-key: <your-GIT-SSH-KEY-base64-encrypt>
---
apiVersion: v1
data:
argocd-server: YXJnb2NkLXNlcnZlci5hcmdvY2Quc3ZjLmNsdXN0ZXIubG9jYWw=
password: <your-argocd-admin-password-base64-encrypt>
username: YWRtaW4=
kind: Secret
metadata:
name: argocd-config
namespace: ci
---
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: ci
name: operate-workflow-sa
---
# Similarly you can use a ClusterRole and ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: operate-workflow-role
namespace: ci
rules:
- apiGroups:
- argoproj.io
verbs:
- "*"
resources:
- "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: operate-workflow-role-binding
namespace: ci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: operate-workflow-role
subjects:
- kind: ServiceAccount
name: operate-workflow-sa
---
apiVersion: argoproj.io/v1alpha1
kind: EventBus
metadata:
name: default
namespace: ci
spec:
nats:
native:
replicas: 3
auth: none
---
apiVersion: v1
kind: Service
metadata:
name: ci
namespace: ci
spec:
ports:
- port: 12000
targetPort: 12000
type: LoadBalancer
selector:
eventsource-name: webhook
---
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: webhook
namespace: ci
labels:
eventsource-name: webhook
spec:
service:
ports:
- port: 12000
targetPort: 12000
webhook:
example:
port: "12000"
endpoint: /example
method: POST
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: cicd
namespace: ci
spec:
dependencies:
# 앞에서 정의한 웹훅 이벤트 소스가 발생하면 동작
- name: test-dep
eventSourceName: webhook
eventName: example
template:
serviceAccountName: operate-workflow-sa
triggers:
- template:
k8s:
operation: create
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: input-artifact-git-
namespace: ci
spec:
serviceAccountName: operate-workflow-sa
entrypoint: pipe
templates:
- name: pipe
steps:
- - name: checkout
template: checkout
- - name: delay
template: delay
- - name: image-build
template: image-build
arguments:
parameters:
- name: version
value: "{{steps.checkout.outputs.parameters.gitVersion}}"
artifacts:
- name: source
from: "{{steps.checkout.outputs.artifacts.source}}"
- - name: delay2
template: delay2
- - name: gitops
template: gitops
arguments:
parameters:
- name: version
value: "{{steps.checkout.outputs.parameters.gitVersion}}"
- - name: delay3
template: delay3
- - name: deployment
template: deployment
- name: checkout
inputs:
artifacts:
- name: source
path: /src
git:
repo: https://<your-source-code-git-url>.git
revision: "main"
outputs:
parameters:
- name: gitVersion
valueFrom:
default: "latest"
path: /version.txt
artifacts:
- name: source
path: /src
gcs:
bucket: <your-GCP-Bucket-name>
key: ci/
serviceAccountKeySecret:
name: argo-gcp
key: gcs-iam.json
container:
image: bitnami/git:latest
command: [sh, -c]
args: ["git rev-list --tags --max-count=1 | xargs git describe --tags > /version.txt"]
workingDir: /src
- name: delay
suspend: {}
- name: image-build
inputs:
parameters:
- name: version
artifacts:
- name: source
path: /src
gcs:
bucket: <your-GCP-Bucket-name>
key: ci/
serviceAccountKeySecret:
name: argo-gcp
key: gcs-iam.json
container:
name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: [executor]
workingDir: /src
args:
- "--dockerfile=Dockerfile"
- "--context=./"
- "--destination=<your-image-repository>/<your-image-name>:{{inputs.parameters.version}}"
volumeMounts:
- name: kaniko-secret
mountPath: /kaniko/.docker/
volumes:
- name: kaniko-secret
secret:
secretName: dockerhub-registry
items:
- key: .dockerconfigjson
path: config.json
- name: delay2
suspend:
duration: "5"
- name: gitops
inputs:
parameters:
- name: version
container:
image: bitnami/git:latest
command: [sh, -c]
args:
- |
mkdir -p /root/.ssh
cp /mnt/workspace/ssh-key/id_rsa /root/.ssh/id_rsa
chmod 600 /root/.ssh/id_rsa
ssh-keyscan github.com >> /root/.ssh/known_hosts
git clone git@github.com:<your-git-name>/<your-git-repo>.git /src
cd /src
git config --global user.name "<your-git-name>"
git config --global user.email "<your-git-email>"
sed -E -i 's/(image: <your-image-repository>\/<your-image-name>:)[^ ]+/\1{{inputs.parameters.version}}/g' deploy.yaml
git add deploy.yaml
git commit -m "Update image tag to {{inputs.parameters.version}}"
git push origin main
env:
- name: GIT_SSH_COMMAND
value: ssh -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no
volumeMounts:
- name: ssh-key-volume
mountPath: /mnt/workspace/ssh-key
readOnly: false
volumes:
- name: ssh-key-volume
secret:
secretName: ssh-key-secret
items:
- key: ssh-private-key
path: id_rsa
- name: delay3
suspend:
duration: "5"
- name: deployment
container:
args:
- |-
apk --no-cache add curl
TOKEN=$(curl -s -k $ARGOCD_SERVER/api/v1/session -d "{\"username\":\"admin\",\"password\":\"$PASSWORD\"}" | sed -e 's/{"token":"//' | sed -e 's/"}//')
curl -k -X POST $ARGOCD_SERVER/api/v1/applications/argocicd/sync -H "Authorization: Bearer $TOKEN"
command:
- sh
- -xuce
env:
- name: ARGOCD_SERVER
valueFrom:
secretKeyRef:
key: argocd-server
name: argocd-config
- name: PASSWORD
valueFrom:
secretKeyRef:
key: password
name: argocd-config
image: alpine:3.17
name: ci-workflow-trigger
Docker-Config Secret
.dockerconfigjson secret은 Build과정에서 Container Image 저장소인 Docker Hub에 Image를 Push할 수 있는 권한이 필요하기 때문에 설정해주어야 합니다. 아래 명령어의 $부분을 본인의 상황에 맞게 수정 후 입력하면 .dockerconfigjson을 자동 생성해줍니다.
`$REGISTRY_SERVER`의 주소는 사설 저장소의 경우 사설 저장소 주소를 입력하고, 저처럼 Docker Hub에서 작업하시는 경우 https://index.docker.io/v1/ 를 입력하시면 됩니다.
kubectl create secret \
docker-registry docker-registry \
--docker-server=$REGISTRY_SERVER \
--docker-username=$REGISTRY_USER \
--docker-password=$REGISTRY_PASS \
--docker-email=$REGISTRY_EMAIL
위 명령어를 통해 Secret생성 후 `kubectl edit secrets dockerhub-registry`명령을 실행하면 .dockerconfigjson의 내용을 확인하실 수 있습니다. 해당 내용을 복사하여 secret의 <your-docker-config-base64-encrypt>에 대입하시면 됩니다.
GCS Secret
Argo Workflows의 Job 사이의 데이터 전달과정에서 GCS Bucket Storage를 사용하기 때문에 파일을 생성할 수 있는 권한이 필요한데, GCS Secret에는 이 권한정보를 입력해야 합니다. 우선 GCP의 Strage > Bucket 메뉴에서 Public 저장소를 생성하고, 위 코드의 <your-GCP-Bucket-name>에 생성한 Bucket 이름을 입력합니다.
그 후 GCP의 IAM > Service Account 메뉴에서 Bucket 저장소에 대한 권한을 부여한 서비스 계정을 생성하고, 키 관리 메뉴를 통해 JSON 파일의 키 정보를 다운로드 합니다. 그 후 아래 코드를 입력하여 Secret을 생성합니다.
kubectl create secret generic argo-gcp -n argo --from-file=<your-service-account-auth-file>.json
위 명령어를 통해 Secret 생성 후 `kubectl edit secrets -n argo gcp-service-account` 명령을 실행하면 data 부분에서 <your-GCP-IAM-json-base64-encrypt>에 대입해야 할 내용을 확인할 수 있습니다.
Git SSH Secret
Build된 이미지 정보를 ArgoCD가 바라보는 GitOps Repository에 반영하기 위해 Git SSH를 사용하여 Git Push권한을 부여합니다. 이를 위해서 Github에 SSH Key를 등록하는 과정이 필요합니다. 잘 정리된 글이 있어 링크 첨부합니다.
Github에 SSH Public Key 등록을 완료했으면 아래 절차에 따라 Private Key Secert을 생성합니다. `cat ~/.ssh/<your-private-key-file-name> | base64` 명령을 통해 Private Key 정보를 base64로 인코딩 합니다.
출력된 내용을 복사하여 <your-GIT-SSH-KEY-base64-encrypt>에 붙여 넣습니다.
ArgoCD Secret
GitOps에 Push된 이미지 정보를 반영하기 위해 Sync 버튼을 누르는 POST API 요청을 전달해야 합니다. 이때 토큰 정보와 ArgoCD 주소를 Secret에 저장하여 주소 및 인증 정보를 사용합니다.
설치 스크립트가 완료될때 표기해 주었던 Argocd의 Password 주소를 <your-argocd-admin-password-base64-encrypt>에 대입합니다.
기타 설정값
`https://<your-source-code-git-url>.git` : Source Code Repository 값을 입력합니다. EX) https://github.com/junkmm/WebGoDummy.git
`<your-GCP-Bucket-name>` : GCP Bucket 이름을 입력합니다. EX) argo-proj-junkmm
`<your-image-repository>/<your-image-name>` : 컨테이너 이미지 정보를 입력합니다. EX) kimhj4270/webgodummy
`<your-git-name>/<your-git-repo>` : GitOps 레포 정보를 입력합니다. EX) junkmm/GitopsDummy.git
CI/CD Pipeline 생성하기
모든 설정을 완료했으면 `kubectl create -f <filename.yaml>`을 통해 리소스를 생성합니다.
Github WebHook 등록하기
Source Code Repository에 Push 이벤트가 들어오면 Argo Events의 Event Sensor가 감지하도록 WebHook을 설정해야 합니다. 즉 개발자는 본인의 Code를 Push하기만 하면 배포 과정까지 자동화 할 수 있는 것이죠, 우선 Github에 접속하여 WebHook을 등록해야 합니다.
`Github` -> `Source Code Repository` -> `Settings` -> `Webhooks` 메뉴로 이동하고. Add webhook을 클릭합니다.
아래와 같이 URL 정보를 입력해야 합니다.
저의 경우 Webhook을 감지하는 Service의 Type을 LoadBalancer로 배포하였기 때문에 공인IP로 접근 가능한 상황입니다. `kubectl get service -n ci`를 입력하면 Service의 정보를 확인할 수 있습니다.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ci LoadBalancer 10.60.5.221 34.92.75.33 12000:31398/TCP 4d1h
CI/CD 실행 예시
아래는 Hello, World!-`v5.2`를 표기하는 go web 서버를 Hello, World!-`v5.3`으로 수정 업데이트 하는 예시입니다.
'클라우드' 카테고리의 다른 글
[EKS] Amzaon EKS 설치 및 기본 사용 (0) | 2024.03.09 |
---|---|
[k8s] Pod의 안정적인 유지 - liveness probe (0) | 2023.10.22 |
[Docker] Docker-Compose (0) | 2023.07.14 |
[Docker] Centos이미지 기반 httpd 서비스 구성하기 (0) | 2023.07.14 |
[Docker] Docker로 컨테이너 배포하기 (0) | 2023.07.14 |