'Kubernetes'에 해당되는 글 57건
- 2024.09.26 Kubernetes Korea Group Discord Channel
- 2024.08.08 Linux single node 에서 kubeadm 으로 kubernetes 설치 하기 1
- 2024.05.09 minkube, kind 설치하기
- 2024.05.09 (OpenLab 스터디) 어플리케이션 배포 전략에 따른 실제 배포 실습
- 2024.04.30 쿨린이가 Kubernetes(쿠버네티스)를 처음 공부하려면 무엇부터 공부해야 할까?
- 2023.11.20 이것만 공부하세요 - helm chart 만드는 법
- 2023.08.23 Cluster API 다이어그램
- 2023.08.17 RBAC 과 Service Accounts 를 사용하여 사용자 권한 제어하기 (Kubernetes v1.24 이상)
- 2023.08.10 프라이빗 컨테이너 이미지 저장소 HA(High Available) 로 구성하기 (feat. Harbor)
- 2023.07.12 Custom Controller 3 - CronJob 구현하기
1. sudo 사용자 추가, 보안 s/w 내리기, swap off
$ sudo adduser ask
$ cat <<EOF | sudo tee /etc/sudoers.d/sudoers-ask
ask ALL=(ALL:ALL) NOPASSWD:ALL
EOF
$ sudo systemctl stop ufw
$ sudo systemctl disable ufw
$ sudo systemctl stop apparmor.service
$ sudo systemctl disable apparmor.service
$ sudo swapoff -a
2. module load
$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
$ sudo modprobe overlay
$ sudo modprobe br_netfilter
3. network forwarding 설정
$ cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl --system
$ sudo iptables -P FORWARD ACCEPT
4. containerd 설치
$ sudo apt-get update
$ sudo apt-get install -y apt-transport-https ca-certificates curl gpg
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc
$ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update
$ sudo apt-get install -y containerd.io
$ sudo mkdir -p /etc/containerd
$ sudo containerd config default | sudo tee /etc/containerd/config.toml
$ sudo sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml
$ sudo systemctl restart containerd
$ sudo systemctl enable containerd
$ sudo systemctl status containerd
5. crictl 설치
#----------------------------------------------------------
# 서버가 arm64 or amd64 인지 확인하여 설치
#----------------------------------------------------------
$ VERSION="v1.30.0"
$ curl -L https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-${VERSION}-linux-arm64.tar.gz --output crictl-${VERSION}-linux-arm64.tar.gz
$ sudo tar zxvf crictl-$VERSION-linux-arm64.tar.gz -C /usr/local/bin
$ rm -f crictl-$VERSION-linux-arm64.tar.gz
#----------------------------------------------------------
# crictl 이 어느 container 를 접속할 것인지 세팅
#----------------------------------------------------------
$ cat <<EOF | sudo tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
debug: false
pull-image-on-create: false
EOF
$ sudo bash -c "crictl completion > /etc/bash_completion.d/crictl"
$ source ~/.bashrc
#----------------------------------------------------------
# containerd 설정 확인
#----------------------------------------------------------
$ sudo crictl info
6. kubectl 설치
#----------------------------------------------------------
# 서버가 arm64 or amd64 인지 확인하여 설치
#----------------------------------------------------------
$ curl -LO "https://dl.k8s.io/release/v1.30.0/bin/linux/arm64/kubectl"
$ chmod +x ./kubectl
$ sudo mv ./kubectl /usr/local/bin/kubectl
7. Kubernetes 설치
$ mkdir -p ~/kubeadm && cd ~/kubeadm
#-----------------------------------------------
# kubernetes 다운로드 key 와 url 등록
#-----------------------------------------------
$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
$ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update
#========================================================================
# kubelet 설치 (아직 kubelet 이 뜨지는 않음)
#========================================================================
$ sudo apt-get install -y kubelet="1.30.3-*" kubeadm="1.30.3-*"
$ sudo systemctl enable --now kubelet
$ sudo systemctl start kubelet
#========================================================================
# 1. kubeadm 설치
#========================================================================
$ sudo kubeadm config images pull
#------------------------------------------------------------
# 자기 노드의 ip: --apiserver-advertise-address
# multi control-plane 일 경우 L4 ip: --control-plane-endpoint
# cgroup driver 세팅 (https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/)
#------------------------------------------------------------
$ vi kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
criSocket: "/var/run/containerd/containerd.sock"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
apiServer:
certSANs:
- 127.0.0.1
- localhost
- <Node private IP>
- <Node public IP>
networking:
serviceSubnet: 10.233.0.0/18
podSubnet: 10.233.64.0/18
dnsDomain: "cluster.local"
#----------------------------------------------------------------
# kubeadm init 을 하고 나면 /var/lib/kubelet/config.yaml 이 생성되어
# kubelet 이 정상적으로 실행됨
#----------------------------------------------------------------
$ sudo kubeadm init --config kubeadm-config.yaml --v=5
#------------------------------------------------------------
# kubeconfig
#------------------------------------------------------------
$ mkdir -p ~/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
8. Calico 설치
$ mkdir -p ~/calico && cd ~/calico
$ curl -LO https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/tigera-operator.yaml
$ kubectl create -f tigera-operator.yaml
$ curl -LO https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/custom-resources.yaml
#------------------------------------------------------------
# yaml 을 열어서 pod 네트워크를 확인하고 변경
#------------------------------------------------------------
$ vi custom-resources.yaml
...
cidr: 10.233.64.0/18
...
$ kubectl create -f custom-resources.yaml
#------------------------------------------------------------
# calico 설치 확인
#------------------------------------------------------------
$ kubectl get pods -n calico-system
9. kubectl bash completion
$ source <(kubectl completion bash)
$ kubectl completion bash > ~/.kube/completion.bash.inc
$ printf "
# kubectl shell completion
source '$HOME/.kube/completion.bash.inc'
" >> $HOME/.bash_aliases
$ source $HOME/.bash_aliases
10. taint 제거
$ kubectl taint nodes --all node-role.kubernetes.io/control-plane-
11. 설치 테스트
$ mkdir -p ~/sample-yaml && cd ~/sample-yaml
$ cat <<EOF | tee ./nginx-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-deployment
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21.0
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service-nodeport
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 8080
targetPort: 80
nodePort: 30180
type: NodePort
externalTrafficPolicy: Local
EOF
$ kubectl apply -f nginx-service.yaml
$ kubectl get pods
$ curl <Node ip>:30180
Minikube 설치하기
minkube 설치는 아래 사이트를 참고한다.
https://minikube.sigs.k8s.io/docs/start/
$ curl -LO <https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64>
$ install minikube-darwin-arm64 /Users/ask/bin/minikube
start cluster
https://minikube.sigs.k8s.io/docs/drivers/docker/
driver 를 docker 로 사용하기 때문에 docker 를 미리 설치해 놓아야 한다.
$ docker context use default
이후에 minikube 로 cluster 를 생성한다.
$ minikube start --driver=docker --memory=4096
--- output ---
😄 Darwin 13.5.2 (arm64) 의 minikube v1.33.0
✨ 유저 환경 설정 정보에 기반하여 docker 드라이버를 사용하는 중
📌 Using Docker Desktop driver with root privileges
👍 Starting "minikube" primary control-plane node in "minikube" cluster
🚜 Pulling base image v0.0.43 ...
🔥 Creating docker container (CPUs=2, Memory=4096MB) ...
❗ This container is having trouble accessing <https://registry.k8s.io>
💡 To pull new external images, you may need to configure a proxy: <https://minikube.sigs.k8s.io/docs/reference/networking/proxy/>
🐳 쿠버네티스 v1.30.0 을 Docker 26.0.1 런타임으로 설치하는 중
▪ 인증서 및 키를 생성하는 중 ...
▪ 컨트롤 플레인을 부팅하는 중 ...
▪ RBAC 규칙을 구성하는 중 ...
🔗 bridge CNI (Container Networking Interface) 를 구성하는 중 ...
🔎 Kubernetes 구성 요소를 확인...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 애드온 활성화 : storage-provisioner, default-storageclass
❗ /Users/ask/bin/kubectl is version 1.28.2, which may have incompatibilities with Kubernetes 1.30.0.
▪ Want kubectl v1.30.0? Try 'minikube kubectl -- get pods -A'
🏄 끝났습니다! kubectl이 "minikube" 클러스터와 "default" 네임스페이스를 기본적으로 사용하도록 구성되었습니다.
디폴트 메모리를 config 에 세팅할 수 도 있다.
config 세팅은 기존에 만든 minikube node 에는 적용이 안되고 새롭게 만드는 노드에만 적용된다.
$ minikube config set memory 4096
node 추가
minikube 에 worker node 를 추가할 수 있다.
$ minikube node add
node 가 추가된 것을 볼 수 있다.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 8m22s v1.30.0
minikube-m02 Ready <none> 86s v1.30.0
node 에 Role 을 추가하고 싶으면 다음과 같이 지정한다.
$ kubectl label node minikube-m02 node-role.kubernetes.io/node=enabled --overwrite
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 15m v1.30.0
minikube-m02 Ready node 8m12s v1.30.0
minikube 명령어
- 현재 설치된 minkube node 를 조회한다.
$ minkube status
--- output ---
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
minikube-m02
type: Worker
host: Running
kubelet: Running
- minikube 로 만든 kubernetes cluster 의 모든 namespace 를 잠시 멈춘다.
$ minkube pause -A
--- output --
⏸️ Pausing node minikube ...
⏸️ Pausing node m02 ...
⏯️ Paused 16 containers
- minkube 로 pause 한 cluster 를 다시 실행한다.
$ minikube unpause -A
--- output ---
⏸️ Unpausing node minikube ...
⏸️ Unpausing node m02 ...
⏸️ Unpaused 16 containers
- minikube 모든 노드를 중지한다.
$ minikube stop --all=true
--- output ---
✋ "minikube-m02" 노드를 중지하는 중 ...
🛑 "minikube-m02"를 SSH로 전원을 끕니다 ...
✋ "minikube" 노드를 중지하는 중 ...
🛑 "minikube"를 SSH로 전원을 끕니다 ...
🛑 2개의 노드가 중지되었습니다.
- aged 란 이름의 다른 쿠버네티스 버전을 추가로 설치한다.
$ minikube start -p aged --kubernetes-version=v1.28.2
- 모든 minikube 로 만든 kubernetes cluster 를 삭제한다.
minikube delete --all
- minikube addons 목록을 살펴본다.
$ minikube addons list
|-----------------------------|----------|--------------|--------------------------------|
| ADDON NAME | PROFILE | STATUS | MAINTAINER |
|-----------------------------|----------|--------------|--------------------------------|
| ambassador | minikube | disabled | 3rd party (Ambassador) |
| auto-pause | minikube | disabled | minikube |
| cloud-spanner | minikube | disabled | Google |
| csi-hostpath-driver | minikube | disabled | Kubernetes |
| dashboard | minikube | disabled | Kubernetes |
| default-storageclass | minikube | enabled ✅ | Kubernetes |
| efk | minikube | disabled | 3rd party (Elastic) |
| freshpod | minikube | disabled | Google |
| gcp-auth | minikube | disabled | Google |
| gvisor | minikube | disabled | minikube |
| headlamp | minikube | disabled | 3rd party (kinvolk.io) |
| helm-tiller | minikube | disabled | 3rd party (Helm) |
| inaccel | minikube | disabled | 3rd party (InAccel |
| | | | [info@inaccel.com]) |
| ingress | minikube | disabled | Kubernetes |
| ingress-dns | minikube | disabled | minikube |
| inspektor-gadget | minikube | disabled | 3rd party |
| | | | (inspektor-gadget.io) |
| istio | minikube | disabled | 3rd party (Istio) |
| istio-provisioner | minikube | disabled | 3rd party (Istio) |
| kong | minikube | disabled | 3rd party (Kong HQ) |
| kubeflow | minikube | disabled | 3rd party |
| kubevirt | minikube | disabled | 3rd party (KubeVirt) |
| logviewer | minikube | disabled | 3rd party (unknown) |
| metallb | minikube | disabled | 3rd party (MetalLB) |
| metrics-server | minikube | disabled | Kubernetes |
| nvidia-device-plugin | minikube | disabled | 3rd party (NVIDIA) |
| nvidia-driver-installer | minikube | disabled | 3rd party (Nvidia) |
| nvidia-gpu-device-plugin | minikube | disabled | 3rd party (Nvidia) |
| olm | minikube | disabled | 3rd party (Operator Framework) |
| pod-security-policy | minikube | disabled | 3rd party (unknown) |
| portainer | minikube | disabled | 3rd party (Portainer.io) |
| registry | minikube | disabled | minikube |
| registry-aliases | minikube | disabled | 3rd party (unknown) |
| registry-creds | minikube | disabled | 3rd party (UPMC Enterprises) |
| storage-provisioner | minikube | enabled ✅ | minikube |
| storage-provisioner-gluster | minikube | disabled | 3rd party (Gluster) |
| storage-provisioner-rancher | minikube | disabled | 3rd party (Rancher) |
| volumesnapshots | minikube | disabled | Kubernetes |
| yakd | minikube | disabled | 3rd party (marcnuri.com) |
|-----------------------------|----------|--------------|--------------------------------|
- dashboard 를 설치한다.
$ minikube addons enable dashboard
--- output ---
💡 dashboard is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: <https://github.com/kubernetes/minikube/blob/master/OWNERS>
▪ Using image docker.io/kubernetesui/dashboard:v2.7.0
▪ Using image docker.io/kubernetesui/metrics-scraper:v1.0.8
💡 Some dashboard features require the metrics-server addon. To enable all features please run:
minikube addons enable metrics-server
🌟 'dashboard' 애드온이 활성화되었습니다
- metrics-server 를 설치한다.
$ minikube addons enable metrics-server
--- output ---
💡 metrics-server is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: <https://github.com/kubernetes/minikube/blob/master/OWNERS>
▪ Using image registry.k8s.io/metrics-server/metrics-server:v0.7.1
🌟 'metrics-server' 애드온이 활성화되었습니다
- dashboard 에 접속한다.
$ minikube dashboard --url
minikube trouble shooting
minikube 를 설치하고 pod 가 생성안되는 가장 큰 이유는 proxy 때문이다. 회사에서 proxy 를 사용한다면 proxy 세팅을 추가로 해줘야 한다.
아래 내용은 proxy 이외의 문제일 때 해결 방법이다.
- docker hub 으로 부터 이미지를 다운 받지 못하는 문제
Error response from daemon: Get "<https://registry-1.docker.io/v2/>": tls: failed to verify certificate: x509: certificate signed by unknown authority
minikube 의 docker-env 를 확인한 후 세팅한다.
$ minikube -p minikube docker-env
--- output ---
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://127.0.0.1:53131"
export DOCKER_CERT_PATH="/Users/ask/.minikube/certs"
export MINIKUBE_ACTIVE_DOCKERD="minikube"
$ docker context create minikube --description "Minikube" --docker "host=tcp://localhost:53131,ca=/Users/ask/.minikube/certs/ca.pem,cert=/Users/ask/.minikube/certs/cert.pem,key=/Users/ask/.minikube/certs/key.pem"
$ docker context use minikube
위의 경우에는 minikube 를 띄울 때 --insecure-registry 를 추가한다.
$ minikube start --insecure-registry="registry-1.docker.io"
아니면 minikube 로 노드에 접속해서 docker 에 인증서를 추가한다.
$ minikube ssh
$ sudo su -
$ update-ca-certificates --fresh
$ openssl s_client -showcerts -verify 5 -connect k8s.gcr.io:443 < /dev/null 2>/dev/null | openssl x509 -outform PEM | tee ~/k8s.gcr.io.crt
$ openssl s_client -showcerts -verify 5 -connect registry-1.docker.io:443 < /dev/null 2>/dev/null | openssl x509 -outform PEM | tee ~/registry-1.docker.io.crt
$ openssl s_client -showcerts -verify 5 -connect auth.docker.io:443 < /dev/null 2>/dev/null | openssl x509 -outform PEM | tee ~/auth.docker.io.crt
$ cp ~/k8s.gcr.io.crt /usr/local/share/ca-certificates/
$ cp ~/registry-1.docker.io.crt /usr/local/share/ca-certificates/
$ cp ~/auth.docker.io.crt /usr/local/share/ca-certificates/
$ update-ca-certificates
$ systemctl restart docker
Kind 설치
kind 는 아래 설치 사이트를 참조한다.
https://kind.sigs.k8s.io/docs/user/quick-start/
$ [ $(uname -m) = arm64 ] && curl -Lo ./kind <https://kind.sigs.k8s.io/dl/v0.22.0/kind-darwin-arm64>
$ chmod +x kind
$ mv kind ~/bin/kind
kind config 세팅
image 는 아래 사이트에서 확인 가능하다.
https://github.com/kubernetes-sigs/kind/releases
kind config 는 여기를 참조한다.
https://kind.sigs.k8s.io/docs/user/quick-start/#configuring-your-kind-cluster
ingress 를 사용하기 위해서는 아래 내용 처럼 port mapping 을 해야 한다.
https://kind.sigs.k8s.io/docs/user/ingress
$ vi kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
# patch the generated kubeadm config with some extra settings
kubeadmConfigPatches:
- |
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
nodefs.available: "0%"
# patch it further using a JSON 6902 patch
kubeadmConfigPatchesJSON6902:
- group: kubeadm.k8s.io
version: v1beta3
kind: ClusterConfiguration
patch: |
- op: add
path: /apiServer/certSANs/-
value: my-hostname
nodes:
- role: control-plane
image: kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245
# extraPortMappings:
# - containerPort: 80
# hostPort: 10080
# listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0"
# protocol: tcp # Optional, defaults to tcp
- role: worker
image: kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245
#featureGates:
# FeatureGateName: true
kind create cluster
$ KIND_EXPERIMENTAL_PROVIDER=docker && kind create cluster --name kind --config kind-config.yaml
--- output ---
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.29.2) 🖼
✓ Preparing nodes 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Thanks for using kind! 😊
- kind cluster 보기
$ kind get clusters
- context 로 cluster 조회하기
$ kubectl cluster-info --context kind-kind
- cluster 삭제하기
$ kind delete cluster --name kind
스터디에서 준비해야 할 내용 리스트
배포 전략에 대한 개념 설명
- Rolling Update
- Blue/Green
- Canary
- Recreate
Argo CD 설치, Argo Rollout 설치
- Argo CD 로 github 을 연동하여 gitops 구현해 보기
- Argo Rollout 을 활용하여 Blue/Green 구현해 보기
- Kubernetes 에서 Deployment 로 배포된 서비스가 Rolling Update 되는 로직을 설명
- (선택) Nginx Ingress Controller 를 활용하여 Canary 배포를 구현해 보기
Kubernetes 를 세미나를 하거나 모임에서 만나면 쿠버네티스를 알고 싶어 하시는 분들이 제일 문제 물어보는 것이 있다.
"쿠버네티스를 잘 모르는데 이제부터 공부하고 싶어요. 어떤거 부터 하면 좋을까요?"
그 동안은 이는 리눅스를 무엇부터 공부해야 할까요? 와 거의 비슷한 이야기인거 같다. 그래서 키워드로 스스로 공부할 수 있도록 키워드로 리스트를 만들었다.
컨테이너
- Docker 노트북에 설치
- 리눅스 네임스페이스 & cgroup
- dockerfile 작성 및 docker image 만들기
- docker image 실행/종료/삭제 하기
- docker hub 에 가입하여 개인 계정에 image 올리기
Kubernetes
- Kubernetes 는 누가 만들었을까? 어떻게 해서 나오게 된 것일까? (Google Borg 로 검색)
- CNCF (Cloud Native Computing Foundation) - 이 재단이 어떤 재단인지 알아보기
- Kubernetes 는 기능이 어떤 것들이 있을까?
- 기본적으로 Kubernetes 는 컨테이너 이미지를 실행시키는 등 관리하는 역할 수행
- Kubernetes 노트북에 설치 (아래 3개 중에 하나)
- kind
- k3s
- minikube
- Kubernetes client 설치
- kubectl 설치
- Kubernetes Architecture 확인
- Control plane
- etcd (key-value store)
- kube-apiserver
- kube-controller
- kube-scheduler
- Node
- kube-proxy
- kubelet
- Network plugin
- Control plane
- Kubernetes 에서 Pod 란 무엇인가?
- Kubernetes 에 Nginx 이미지를 pod 로 띄우기
- pod yaml 만들기
- kubectl 로 pod yaml 을 Kubernetes 에 배포하기
- Pod 는 어떻게 뜨는 것일까?
- 사용자가 pod yaml 을 작성
- 사용자가 kubectl 로 pod yaml 을 kubernetes 로 보냄
- kubernetes api-server 가 이를 받아서 etcd 에 저장
- kube-scheduler 가 pod 를 원하는 node 로 스케줄링
- 스케줄링된 node 에 실행중인 kubelet 이 pod 를 실행
- Kubernetes Resource 알아보기
- Pod
- ReplicaSet
- Deployment
- Label & Selector
- Service
- ClusterIP
- NodePort
- LoadBalancer
- Ingress & Ingress Controller
- StatefulSet
- DaemonSet
- StorageClass
- Persistent Volume
- Persistent Volume Claim
- ConfigMap
- Secret
- ServiceAccount
- ClusterRole
- ClusterRoleBinding
- Custom Controller (Operator 란 무엇인가?)
- CNCF Projects 알아보기
- Graduated Projects
- Incubating Projects
- Sandbox Projects
helm chart 를 만들기 위해서는 여러 기능들을 알아야 하지만 그 중에서 가장 많이 쓰고 헷갈리는 기능에 대해서 살펴 본다.
기본적으로 실습할 수 있는 환경을 먼저 만들고 하나씩 공부해 본다.
$ helm create flow-control
$ cd flow-control
yaml 형태의 출력을 확인해 본다.
$ helm template .
---
# Source: flow-control/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: release-name-flow-control
labels:
helm.sh/chart: flow-control-0.1.0
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
automountServiceAccountToken: true
---
# Source: flow-control/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: release-name-flow-control
labels:
helm.sh/chart: flow-control-0.1.0
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
---
# Source: flow-control/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name-flow-control
labels:
helm.sh/chart: flow-control-0.1.0
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
helm.sh/chart: flow-control-0.1.0
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
spec:
serviceAccountName: release-name-flow-control
securityContext:
{}
containers:
- name: flow-control
securityContext:
{}
image: "nginx:1.16.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{}
---
# Source: flow-control/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "release-name-flow-control-test-connection"
labels:
helm.sh/chart: flow-control-0.1.0
app.kubernetes.io/name: flow-control
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['release-name-flow-control:80']
restartPolicy: Never
제대로 출력된다면 필요없는 파일은 삭제하고 초기화 하자.
$ rm -rf template/*
$ cat /dev/null > values.yaml
0. yaml 은 들여 쓰기가 중요하다.
가장 간단한 configmap 을 만들어서 value 값을 출력한다.
$ values.yaml
---
favorite:
drink: coffee
food: pizza
$ vi template/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" }}
mug: "true"
{{ end }}
$ helm template .
--- output ---
Error: YAML parse error on flow-control/templates/configmap.yaml: error converting YAML to JSON: yaml: line 8: did not find expected key
Use --debug flag to render out invalid YAML
template 을 제너레이션하면 에러가 발생한다. 왜 그럴까?
configmap.yaml 에 mug: "true"
가 2칸 들여써 있어서 발생하는 에러이다. 이런 에러를 조심하면서 아래 실습을 해보자.
1. 조건문과 빈라인 없애기
configmap.yaml 을 수정해서 제대로 yaml 을 생성해 보자.
$ vi template/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | default "tea" | quote }}
food: {{ .Values.favorite.food | upper | quote }}
{{ if eq .Values.favorite.drink "coffee" }}
mug: "true"
{{ end }}
$ helm template .
---
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
--------------> 빈라인 발생한다.
mug: "true"
--------------> 빈라인 발생한다.
.Release.Name
은 helm 으로 설치할 때 파라미터로 넘기는 값인데 여기서는 설치가 아니므로 기본 값인 release-name
으로 치환 되었고, .Values.favorite.drink
는 values.yaml 에 지정한 키값으로 그에 해당하는 밸류 값이 제너레이트 될 때 출력된다.
함수의 연속적 사용은 |
라인으로 호출 가능하며 default
는 키에 대한 값이 없을 때, quote
는 값을 "
으로 묶을 때 upper
는 값을 대문자로 변환할 때 사용하는 내장 함수이다.
비교 구문은 if eq 값1 값2
와 같이 사용할 수 있다.
출력하지 않는 곳에서는 빈라인이 발생하는데 이 부분을 다음과 같이 없애 줄 수 있다.
{{ if eq .Values.favorite.drink "coffee" }}mug: "true"{{ end }}
하지만 가독성이 떨어지므로 {{-
와 같이 표현하면 빈라인이 없어지면서 윗라인에 나란히 붙는 것과 같은 효과를 낼 수 있다.
{{- if eq .Values.favorite.drink "coffee" }}
mug: "true"
{{- end }}
다시 yaml 을 생성해 보면 아래와 같이 빈라인이 없어졌음을 알 수 있다.
$ helm template .
---
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
myvalue: "Hello World"
drink: "coffee"
food: "PIZZA"
mug: "true"
조건문 대신 다음과 같이 with
를 사용하여 조건문과 키밸류 스쿱을 지정할 수 있다.
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- with .Values.hobby }}
sports: {{ .sports }}
{{- end }}
### values.yaml
hobby:
sports: golf
with 와 함께 사용한 키는 해당 키에 대한 값이 있을 때만 with ~ end
로 감싼 구문이 출력된다. 또한 감싼 구문 안에서는 스쿱이 재정의되어 hobby 아래의 키인 sports 를 .sports
로 바로 사용할 수 있다.
yaml 을 생성하면 다음과 같은 결과가 나온다.
$ helm template .
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
myvalue: "Hello World"
sports: golf
만약 values 에 sports 를 없애면 아래와 같이 출력되지 않는다.
### values.yaml
hobby: {}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
myvalue: "Hello World"
hobby 아래의 키는 키밸류인 디셔너리 타입을 넣기 때문에 아래의 값을 모두 없애기 위해서{}
빈 딕셔너리 값으로 지정했다. 만약 아래의 값이 리스트라면 []
와 같이 빈 리스트 값을 지정할 수 있다.
with ~ end
로 감싼 구문에서 root
영역의 value 를 활용하고 싶을 수 있다. 이 때는 $
를 붙혀서 영역을 최상위 root 로 접근할 수 있다. 아래 예제에서 $.Release.Name
을 참고한다.
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- with .Values.hobby }}
sports: {{ .sports }}
release: {{ $.Release.Name }}
{{- end }}
YAML
은 JSON
의 수퍼셋이기 때문에 JSON 으로 표현하여 가독성을 높혀줄 수 도 있다. pod 를 만들 때 yaml 에 args
를 추가할 수 있는데. 이 때 JSON 을 쓰면 읽기에 편하다.
args:
- "--dirname"
- "/foo"
위의 내용은 JSON 으로 아래와 같이 바꿀 수 있다.
args: ["--dirname", "/foo"]
2. range 함수를 이용한 반복문
range 를 이용하여 반복문을 사용할 수 있다.
### values.yaml
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
toppings: |-
{{- range .Values.pizzaToppings }}
- {{ . | title | quote }}
{{- end }}
pizzaToppings 의 값은 리스트이다(-
기호가 값으로 붙었기 때문에 리스트임을 알 수 있다). 리스트로 값을 가져와서 출력하기 때문에 아래와 같은 결과가 나온다.
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
toppings: |-
- "Mushrooms"
- "Cheese"
- "Peppers"
- "Onions"
한가지 yaml
에서 toppings: 의 뒤에 따라온 |-
기호의 의미는 멀티 라인 스트링을 받겠다는 의미이다.
tuple
을 사용하여 튜플로 만들어 쓸 수 도 있다.
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
sizes: |-
{{- range tuple "small" "medium" "large" }}
- {{ . }}
{{- end }}
### 출력 결과
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
sizes: |-
- small
- medium
- large
$
와 :=
를 이용하여 변수를 지정할 수 있다. 아래는 리스트에서 받은 값을 index 변수와 value 변수로 받아서 활용하는 부분이다.
### values.yaml
pizzaToppings:
- mushrooms
- cheese
- peppers
- onions
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
toppings: |-
{{- range $index, $value := .Values.pizzaToppings }}
- {{ $index }}: {{ $value }}
{{- end }}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
toppings: |-
- 0: mushrooms
- 1: cheese
- 2: peppers
- 3: onions
map 값을 변수로 받아 처리할 수 도 있다.
### values.yaml
favorite:
drink: coffee
food: pizza
### configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
favorite: |-
{{- range $key, $value := .Values.favorite }}
{{ $key }}: {{ $value }}
{{- end }}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
data:
favorite: |-
drink: coffee
food: pizza
3. template 선언 및 활용
부분적으로 사용자 정의 template 을 만들어서 활용 할 수 있다.
define - tempate 사용
template 은 define
으로 선언하고 tempate
으로 활용할 수 있다.
### configmap.yaml
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" }}
data:
myvalue: "Hello World"
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
generator: helm
date: 2023-10-31
data:
myvalue: "Hello World"
위의 예제는 labels 에 date: 날짜
를 추가로 넣는 부분을 named template
을 만들어서 사용한 예제이다.
chat 를 만들 때 configmap.yaml 과 같이 쿠버네티스 리소스들은 template 디렉토리 아래에 위치 시킨다고 했다. 이 디렉토리에 위치한 yaml 파일들은 자동으로 렌더링에 포함되는데 _
로 시작하는 파일은 렌더링에서 제외한다. 그래서 보통 define 으로 정의한 함수들은 _helper.tpl
파일을 만들어서 이곳에 위치 시킨다.
define
으로 정의된 named template (함수) 은 template
으로 호출되기 전까지는 렌더링 되지 않는다. 이제 이 함수를 _helper.tpl 파일로 옮겨서 렌더링 결과를 살펴보자.
# _helper.tpl
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" }}
data:
myvalue: "Hello World"
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
generator: helm
date: 2023-10-31
data:
myvalue: "Hello World"
위의 예제에서 define 함수 내에서 .Values
와 같이 value 를 가져오는 것은 하지 않았다. 아래와 같이 {{ .Chart.Name }}
을 사용한다면 위의 방식으로는 값을 표현할 수 없다.
# _helper.tpl
{{- define "mychart.labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
{{- end }}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
generator: helm
date: 2023-10-31
chart:
version:
data:
myvalue: "Hello World"
이는 template 으로 호출할 때 뒤에 value 를 보내지 않아서 발생한 부분이다. 즉 {{- template "mychart.labels" . }}
과 같이 마지막에 현재의 scope value 인 .
을 넘겨 주어야 제대로 된 값이 출력된다.
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" . }}
data:
myvalue: "Hello World"
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
generator: helm
date: 2023-10-31
chart: flow-control
version: 0.1.0
define - include 사용
template 은 있는 그대로 output 을 보여주기 때문에 들여쓰기의 문제가 있을 수 있다.
# _helper.tlp
{{- define "mychart.app" -}}
app_name: {{ .Chart.Name }}
app_version: "{{ .Chart.Version }}"
{{- end -}}
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{ template "mychart.app" . }}
data:
myvalue: "Hello World"
{{ template "mychart.app" . }}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
app_name: flow-control
app_version: "0.1.0"
data:
myvalue: "Hello World"
app_name: flow-control
app_version: "0.1.0"
app_name 과 app_version 이 출력된 것을 보면 define 에 정의된 들여쓰기 대로 그대로 출력되어 원하는 대로 출력되지 않는다.
include
와 nindent
를 사용하면 원하는 들여쓰기가 가능하다.
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{ template "mychart.app" . }}
data:
myvalue: "Hello World"
{{ template "mychart.app" . }}
# Source: flow-control/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name-configmap
labels:
app_name: flow-control
app_version: "0.1.0"
data:
myvalue: "Hello World"
app_name: flow-control
app_version: "0.1.0"
끝으로 helm install
할 때 들여쓰기가 잘못되면 렌더링 오류가 나서 최종 결과를 볼 수 가 없다. 이를 해결할 수 있는 옵션이 -disable-openapi-validation
이다.
$ helm install --dry-run --disable-openapi-validation mychart ./
Kubernetes 에서 ServiceAccount 를 생성하면 1.22 버전까지는 자동으로 token 을 생성하였다. 그러나 1.23 부터는 토큰을 자동으로 생성해 주지 않기 때문에 수동으로 생성해야 한다.
이 바뀐 기능은 ServiceAcount 와 RBAC 을 연동하여 권한을 제어하고자 할 때 문제가 되므로 수동으로 만드는 방법을 살펴본다.
1. 네임스페이스 - SA 생성
먼저 테스트할 네임스페이스를 만든다.
$ kubectl create ns ask
ask 네임스페이스에 서비스 어카운트를 생성한다.
$ kubectl create sa ask-sa -n ask
1.24 버전부터는 sa 를 생성해도 token 이 자동으로 생성되지 않는다.
token 은 Secret 타입이므로 Secret 을 조회해 보면 token이 자동 생성되지 않았음을 알 수 있다.
참고: https://kubernetes.io/docs/concepts/configuration/secret/#service-account-token-secrets
2. Token 생성
ask-sa 에 해당하는 token 을 수동으로 생성한다.
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: ask-sa
namespace: ask
annotations:
kubernetes.io/service-account.name: ask-sa
type: kubernetes.io/service-account-token
EOF
token 을 생성할 때 어노테이션으로 연결될 서비스 어카운트를 지정한다.
$ kubectl get secret -n ask
NAME TYPE DATA AGE
ask-sa kubernetes.io/service-account-token 3 7s
조회를 하면 service-account-toke 타입으로 secret 이 생성되었음을 알 수 있다.
혹은 token 을 수동으로 생성하는 방법도 있다.
$ kubectl create token ask-sa --bound-object-kind Secret --bound-object-name ask-sa --duration=999999h -n ask
------- output -----------
xxxxxxxxxxxxxxxxxxxxxxx
base64 로 변환하여 secret 에 data.token
값으로 저장한다.
$ kubectl create token ask-sa --bound-object-kind Secret --bound-object-name ask-sa --duration=999999h -n ask | base64 -w 0
------- output -----------
xxxxxxxxxxxxxxxxxxxxxxx
$ kubectl edit secret ask-sa -n ask
...
data:
token: xxxxxxxxxxxxxxxxxxxx
...
3. Role 과 RoleBinding 생성
Role 과 RoleBinding 은 네임스페이스 별로 연결된다. 그러므로 생성한 권한은 해당 네임스페이스에만 권한이 주어진다.
먼저 Role 을 생성한다.
$ cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ask-role
namespace: ask
rules:
- apiGroups: ["", "*"]
resources: ["*"]
verbs: ["*"]
EOF
apiGroup
에서 ""
은 core API group
으로 다음의 출력으로 확인할 수 있다.
APIVERSION 이 v1
인 리소스들이 core API group 이 되면 이들에 대해서 권한을 사용하겠다는 뜻이다.
$ kubectl api-resources -o wide
NAME SHORTNAMES APIVERSION NAMESPACED KIND VERBS
bindings v1 true Binding [create]
componentstatuses cs v1 false ComponentStatus [get list]
configmaps cm v1 true ConfigMap [create delete deletecollection get list patch update watch]
endpoints ep v1 true Endpoints [create delete deletecollection get list patch update watch]
events ev v1 true Event [create delete deletecollection get list patch update watch]
limitranges limits v1 true LimitRange [create delete deletecollection get list patch update watch]
namespaces ns v1 false Namespace [create delete get list patch update watch]
nodes no v1 false Node [create delete deletecollection get list patch update watch]
persistentvolumeclaims pvc v1 true PersistentVolumeClaim [create delete deletecollection get list patch update watch]
persistentvolumes pv v1 false PersistentVolume [create delete deletecollection get list patch update watch]
pods po v1 true Pod [create delete deletecollection get list patch update watch]
podtemplates v1 true PodTemplate [create delete deletecollection get list patch update watch]
replicationcontrollers rc v1 true ReplicationController [create delete deletecollection get list patch update watch]
resourcequotas quota v1 true ResourceQuota [create delete deletecollection get list patch update watch]
secrets v1 true Secret [create delete deletecollection get list patch update watch]
serviceaccounts sa v1 true ServiceAccount [create delete deletecollection get list patch update watch]
services svc v1 true Service [create delete deletecollection get list patch update watch]
다음은 Rolebinding을 생성한다.
$ cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ask-role-binding
namespace: ask
subjects:
- kind: ServiceAccount
name: ask-sa
namespace: ask
roleRef:
kind: Role
name: ask-role
apiGroup: rbac.authorization.k8s.io
EOF
ServiceAcount 인 ask-sa
와 ask-role
Role 을 서로 연결 시킨 다는 의미이다.
이렇게 되면 이제 ask-sa
sa 는 ask-role
role 에 대한 권한만을 사용할 수 있다.
4. kubeconfig 생성
sa 를 만들었으니 이를 연동할 kubeconfig 를 만들어 본다.
token 을 조회해 보자.
$ kubectl get secret -n ask ask-sa -ojsonpath={.data.token} | base64 -d
----- output -----
xxxxxxxxxxxxxxxxxxxxxxxxx
token 값으로 kubeconfig 의 user 접속 token 에 넣는다.
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: xxxxxxxxxxxxxxxxxxxxxxxx
server: https://xxxxxxxxxx.ap-northeast-2.eks.amazonaws.com
name: mycluster
contexts:
- context:
cluster: mycluster
user: ask-sa
namespace: ask
name: mycluster
current-context: mycluster
kind: Config
users:
- name: ask-sa
user:
token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
위의 kubeconfig 로 접속하면 ask 네임스페이스에 대해서 kubectl 명령어를 실행할 수 있다.
$ kubectl --kubeconfig ask.kubeconfig get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:ask:ask-sa" cannot list resource "pods" in API group "" in the namespace "default"
default 네임스페이스에는 권한이 없으므로 권한 없음 에러가 리턴된다.
$ kubectl --kubeconfig ask.kubeconfig get pods -n ask
No resources found in ask namespace.
ask 네임스페이스의 파드는 정상적으로 조회된다.
컨테이너 이미지 저장소를 독립적으로 구성하는 방법을 살펴본다.
일반적으로 컨테이너 이미지는 CNCF 의 프로젝트 중에 하나인 Harbor 를 사용하여 구성한다. Helm chart 가 잘 되어 있어 쿠버네티스 위에서 설치하는 것은 매우 쉬운데 운영 환경에 걸맞는 HA 구성은 여러가지 고려해야 할 사항들이 있다.
그래서 이번에는 Harbor 를 HA 로 구성하는 방법을 알아본다.
Harbor HA Architecture
Harbor 를 HA 로 구성하려면 아래의 전제 조건이 필요하다.
- Kubernetes cluster 1.10 이상
- Helm 2.8.0 이상
- High Available 하게 설치된 Ingress Controller
- High Available 하게 설치된 PostgreSQL 데이터베이스
- High Available 하게 설치된 Redis
- Shared storage 로 된 PVC
이 중에서 1, 2, 3 번은 구성되어 있다고 가정하고 이후를 살펴본다.
또한 Public Cloud 는 AWS 를 사용했고 쿠버네티스 클러스터는 EKS 를 사용했다.
아키텍처는 아래 그림과 같다.
[출처: https://goharbor.io/docs/1.10/img/ha.png]
PostgreSQL 데이터 베이스 구성
AWS 의 RDS 를 사용했으며 Harbor 에서 사용할 필요한 user 생성과 권한을 부여한다.
# psql -U postgres
postgres=# CREATE DATABASE registry;
postgres=# CREATE USER harbor WITH ENCRYPTED PASSWORD 'xxxxxxx';
postgres=# GRANT ALL PRIVILEGES ON DATABASE registry TO harbor;
어드민 권한으로 데이터베이스에 접속하여 Harbor 에서 사용할 registry
database 를 생성한다. user 는 harbor
이고 필요한 password 를 생성한 다음 registry 데이터베이스의 모든 권한을 harbor 유저에 부여한다.
테이블과 스퀀스를 다루기 위해서는 아래 추가적으로 권한을 부여해야 한다. (아래 권한이 추가되지 않으면 harbor 유저로 테이블과 시퀀스에 대한 생성/조회/삭제/수정을 하지 못한다.
postgres=# \c registry
registry=# GRANT ALL ON ALL TABLES IN SCHEMA public TO harbor;
registry=# GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO harbor;
Redis 구성
Harbor 는 캐시로 레디스를 사용하며 이 때 레디스 구성은 독립 혹은 레디스 + 센티널(sentinel) 구성만을 지원한다. 한마디로 클러스터 모드의 레디스는 지원하지 않는다.
AWS Elasticache Redis 서비스는 센티널을 지원하지 않아 굳이 Elasticache 서비스를 사용할 이유가 없다.
Elasticache 서비스의 레디스 구성으로 1개의 컨트롤노드 - 멀티 워커노드 로 하여 데이터 복제는 가능하나 1개의 컨트롤 노드가 무너지면 역시 장애가 발생하므로 서비스를 사용하여 구성하지 않았다.
이 후 살펴볼 Harbor Helm chart 에서 쿠버네티스 위에 레디스를 1개로 띄우는 internal 생성 방식을 사용한다.
레디스 구성을 HA 로 하고 싶다면, 레디스를 멀티 노드 센티널 구성으로 쿠버네티스 위에 띄우는 방법도 있으니 이는 레디스 설치 문서를 참고하면 된다. (센티널 구성일 때 Harbor chart 의 value 값은 코멘트로 적혀있으니 쉽게 이해할 수 있다)
Shared Storage 구성
AWS 에서 지원하는 공유 스토리지는 EFS
가 있다. EFS 는 NFSv4
프로토콜을 지원하니 공유 스토리지로 사용 가능하다.
먼저 AWS EFS 서비스에서 파일스토리지를 생성한다.
생성된 EFS 는 실제로 파일시스템의 스토리지가 생성된 것은 아니다. 일종의 정보를 생성한 것이며 필요에 따라 실제 스토리지를 생성하고 할당 받는 방식이다.
쿠버네티스에서는 Provisioner
, StroageClass
PVC
, PV
라는 스토리지 표준 관리 방법이 있다.
흔히 Provisioner 라고 말하는 CSI Driver
를 설치한다.
EKS 에서는 추가 기능으로 Amazon EFS CSI Driver
를 추가할 수 있다.
이 때 권한에서 중요한 한가지가 있는데 EKS node 에서 사용하는 role 에 (role 명은 eks 의 태그 정보를 확인해 보면 된다) AmazonEFSCSIDriverPolicy
정책이 반드시 추가되어 있어야 한다.
이제 스토리지 클래스를 설치하자.
$ curl -Lo efs-sc.yaml https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml
$ vi efs-sc.yaml
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: taco-efs-storage
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-xxxxxxx # EFS 에서 생성한 fs id
directoryPerms: "700"
$ kubectl apply -f efs-sc.yaml
변경해야 할 것은 fileSystemId
로 앞서 EFS 에서 생성한 파일스토리지의 fs id
값으로 변경해 준다.
스토리지클래스가 잘 작동하는지 확인하기 위해서 아래와 같이 테스트 파드를 생성해 본다.
$ kubectl create ns harbor-ask
$ vi efs-test-pod.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
namespace: harbor-ask
spec:
accessModes:
- ReadWriteMany
storageClassName: taco-efs-storage
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
name: efs-app
namespace: harbor-ask
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: efs-claim
$ kubectl apply -f efs-test-pod.yaml
Pod 가 생성되고 Pod 로 접속하면 /data/out
파일에 시간이 출력되고 있으면 정상적으로 작동하는 것이다.
PVC 를 생성할 때 accessModes
가 ReadWriteMany
인 것도 확인하자.
Harbor chart 로 HA 설치
이제 필요한 사전 구성은 마쳤으니 chart 로 설치를 진행한다.
먼저 chart 를 등록하고 다운 받는다.
$ helm repo add harbor https://helm.goharbor.io
$ helm repo update
$ helm fetch harbor/harbor --untar
차트를 다운 받을 필요는 없으니 관련 values.yaml 을 확인하기 위해서 참고용으로 다운 받았다.
필요한 value 를 설정한다.
$ vi ask-values.yaml
---
harborAdminPassword: "xxxxx"
expose:
type: ingress
tls:
enabled: true
certSource: secret
secret:
secretName: "taco-cat-tls"
ingress:
hosts:
core: harbor.xxx
className: "nginx"
externalURL: https://harbor.xxx
harborAdminPassword
는 Harbor 웹 화면에서 admin
계정으로 접속할 때 필요한 패스워드 이다.
expose
는 ingress 에 노출되는 값이며 도메인이 harbor.xxx
으로 DNS 에 연결되어 있으며 (DNS 가 없으면 로컬 컴퓨터에 /etc/hosts
파일에 등록해서 사용한다), 도메인 인증서는 앞에서 생성한 harbor-ask
네임스페이스에 taco-cat-tls
라는 secret 이름으로 저장되어 있다.
persistence:
enabled: true
resourcePolicy: "keep"
persistentVolumeClaim:
registry:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 1024Gi
jobservice:
jobLog:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 128Gi
redis:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 256Gi
trivy:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 128Gi
imageChartStorage:
disableredirect: false
type: filesystem
filesystem:
rootdirectory: /storage
persistence
는 Harbor 컴포넌트에서 사용하는 스토리지 정보이다. Harbor 차트에서 설치하는 컴포넌트는 registry
, jobservice
, redis
, trivy
, core
, portal
등이 있으며 스토리지가 필요한 컴포넌트만 기술하면 된다.
registry:
replicas: 2
portal:
replicas: 2
core:
replicas: 2
jobservice:
replicas: 2
trivy:
enabled: true
replicas: 2
notary:
enabled: false
cache:
enabled: true
expireHours: 24
각 컴포넌트의 Pod 갯수를 넣는다. HA 구성 이므로 최소 2 이상을 넣는다.
notary
는 이미지 서명 관련 컴포넌트로 이번 구성에서는 설치하지 않았다.
database:
type: external
external:
host: xxxxxx.ap-northeast-2.rds.amazonaws.com
port: "5432"
username: "harbor"
password: "xxxxxxx"
coreDatabase: "registry"
RDS 에 만들어진 외부 데이터베이스 정보를 넣는다.
redis:
type: internal
레디스는 내부에서 단일 Pod 로 생성한다.
전체 values 는 다음과 같다.
$ vi ask-ha-values.yaml
---
harborAdminPassword: "xxxxxx"
expose:
type: ingress
tls:
enabled: true
certSource: secret
secret:
secretName: "taco-cat-tls"
ingress:
hosts:
core: harbor.xxx
className: "nginx"
externalURL: https://harbor.xxx
persistence:
enabled: true
resourcePolicy: "keep"
persistentVolumeClaim:
registry:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 1024Gi
jobservice:
jobLog:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 128Gi
redis:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 256Gi
trivy:
storageClass: "taco-efs-storage"
accessMode: ReadWriteMany
size: 128Gi
imageChartStorage:
disableredirect: false
type: filesystem
filesystem:
rootdirectory: /storage
registry:
replicas: 2
portal:
replicas: 2
core:
replicas: 2
jobservice:
replicas: 2
trivy:
enabled: true
replicas: 2
notary:
enabled: false
cache:
enabled: true
expireHours: 24
database:
type: external
external:
host: xxxxxx.ap-northeast-2.rds.amazonaws.com
port: "5432"
username: "harbor"
password: "xxxxxxx"
coreDatabase: "registry"
redis:
type: internal
쿠버네티스에 배포한다.
$ helm upgrade -i harbor-ask harbor/harbor --version 1.12.3 -n harbor-ask -f ask-ha-values.yaml
설치 확인
Harbor 웹에 접속하여 사용자(tks)와 프로젝트(tks)를 만든다.
해당 프로젝트의 Members
탭에는 사용자가 등록되어 있어야 한다. (그래야 컨테이너 이미지를 올릴 수 있는 권한이 있다)
$ docker login harbor.xxx -u tks
Password:
$ docker pull hello-world
$ docker tag hello-world harbor.xxx/tks/hello-world
$ docker push harbor.xxx/tks/hello-world
Kubernetes 에는 이미 CronJob 이라는 리소스 타입이 있지만, Kubebuilder 을 이용하여 Custom Controller 로 재작성 해보는 연습을 해보도록 하자.
Project 구조 만들기
먼저, Project 구조를 만들기 위해 아래와 같이 kubebuilder init
명령어를 실행한다.
$ mkdir -p cronjob-kubebuilder
$ cd cronjob-kubebuilder
$ kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project
도메인을 tutorial.kubebuilder.io
로 했으므로 모든 API Group 은 <group>.tutorial.kubebuilder.io
방식으로 정해지게 된다. 또한 특별히 프로젝트 이름은 지정하지 않았는데, --project-name=<dns1123-label-string>
과 같이 옵션을 추가하지 않으면 폴더의 이름이 기본적으로 프로젝트 이름이 된다. (여기서 프로젝트명은 DNS-1123 label 규칙을 따라야 한다)
한가지 주의해야 할 점은 cronjob-kubebuilder
디렉토리는 $GOPATH
경로 아래에 있어서는 안된다. 이는 Go modules
의 규칙 때문인데 좀 더 자세히 알고 싶으면 https://go.dev/blog/using-go-modules 블로그 포스트를 읽어보자.
만들어진 프로젝트의 구조는 다음과 같다.
$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── cmd
│ └── main.go
├── config
│ ├── default
│ ├── manager
│ ├── prometheus
│ └── rbac
├── go.mod
├── go.sum
└── hack
└── boilerplate.go.txt
7 directories, 8 files
go.mod
파일은 모듈 디펜던시를 표시하고, Makefile
은 custom controller 를 빌드하고 디플로이 할 수 있다.
config
디렉토리 아래에는 Kustomize 로 작성되어 CustomResourceDefinition
, RBAC
, WebhookConfiguration
등의 yaml 파일들이 정의되어 있다.
특히, config/manager
디렉토리에는 Cluster 에 Custom Controller 를 파드 형태로 배포할 수 있는 Kustomize yaml 이 있고, config/rbac
디렉토리에는 서비스 어카운트로 Custom Controller 의 권한이 정의되어 있다.
Custom Controller 의 Entrypoint 는 cmd/main.go
파일이다.
처음 필요한 모듈을 임포트 한 것을 보면 아래 2개가 보인다.
- core
controller-runtime
라이브러리 - 기본 controller-runtime 로깅인
Zap
package main
import (
"flag"
"fmt"
"os"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
// +kubebuilder:scaffold:imports
)
모든 컨트롤러에는 Scheme
이 필요하다. 스킴은 Kind
와 Go types
간의 매핑을 제공해 준다.
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
main function
에는 아래의 내용들이 들어가 있다.
- 기본 플래그 셋업
manager
를 생성하여 모든 Custom Controller 의 실행을 추적하고, shared cache 세팅하고,scheme
을 아규먼트로 넘기주어 클라이언트를 API 서버에 설정한다.- manager 를 실행하면 manager 가 모든 컨트롤러와 웹혹을 실행한다.
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
manager
생성 시에 컨트롤러가 특정 네임스페이스의 리소스만을 감시할 수 있도록 할 수 있다.
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Namespace: namespace,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
이렇게 특정 네임스페이스를 지정한 경우에는 권한을 ClusterRole
과 ClusterRoleBinding
에서 Role
과 RoleBinding
으로 변경하는 것을 권장한다.
그리고 MutiNamespacedCacheBuilder
를 사용하면 특정 네임스페이스의 묶음의 리소스만을 감시하게 제한할 수 있다.
var namespaces []string // List of Namespaces
cache.Options.Namespaces = namespaces
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
NewCache: cache.MultiNamespacedCacheBuilder(namespaces),
MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort),
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
MultiNamespacedCacheBuilder
는 deprecated api 이므로 cache.Options.Namespaces
를 사용한다. (https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/cache#Options)
Groups, Versions, Kinds and Resources
쿠버네티스에서 API 에 대해서 이야기할 때는 groups, versions, kinds and resources 4개의 용어를 사용한다.
쿠버네티스의 API Group 은 단순히 관련 기능의 모음이다. 각 Group 에는 하나 이상의 Version 이 있으며, 이름에서 알 수 있듯이 시간이 지남에 따라 API의 작동 방식을 변경할 수 있다.
각 API group-version 에는 하나 이상의 API type 이 포함되며, 이를 Kind 라고 부른다. Kind 는 Version 간에 양식을 변경할 수 있지만, 각 양식은 어떻게든 다른 양식의 모든 데이터를 저장할 수 있어야 한다(데이터를 필드 또는 주석에 저장할 수 있음). 즉, 이전 API 버전을 사용해도 최신 데이터가 손실되거나 손상되지 않는다.
Resource 란 간단히 말해서 API 안에서 Kind 를 사용하는 것이다. 종종, Kind 와 Resource 는 일대일로 매핑된다. 예를 들어, Pod Resource 는 Pod Kind 에 해당한다. 그러나 때로는 여러 Resource 에서 동일한 Kind를 반환할 수도 있다. 예를 들어, Scale Kind 는 deployments/scale 또는 replicasets/scale 과 같은 모든 scale 하위 리소스에 의해 반환된다. 이것이 바로 Kubernetes HorizontalPodAutoscaler 가 서로 다른 resource 와 상호 작용할 수 있는 이유다. 그러나 CRD를 사용하면 각 Kind 는 단일 resource 에 해당한다.
resource 는 항상 소문자이며, 관례에 따라 소문자 형태의 Kind를 사용한다.
특정 group-version 에서 어떤 kind 를 지칭할 때는 줄여서 GroupVersionKind 혹은 줄여서 GVK 라고 부른다. 같은 방식으로 resource 도 GroupVersionResource 혹은 GVR 이라고 부른다.
GVK 는 패키지에서 Go type 에 해당한다.
API 는 왜 만들어야 할까?
Kind 에 대해서 Custom Resource (CR) 과 Custom Resource Definition (CRD) 을 만들어야 한다. 그 이유는 CustomResourceDefinitions 으로 Kubernetes API 를 확장할 수 있기 때문이다.
새롭게 만드는 API 는 쿠버네티스에게 custom object 를 가리치는 방법이다.
기본으로 CRD 는 customized Objects 의 정의이며, CR 은 그것에 대한 인스턴스이다.
API 추가
아래 명령으로 새로운 Kind 를 추가하자.
$ kubebuilder create api --group batch --version v1 --kind CronJob
Create Resource 와 Create Controller 를 하겠냐고 물으면 y
로 대답한다.
$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── api
│ └── v1
├── bin
│ └── controller-gen
├── cmd
│ └── main.go
├── config
│ ├── crd
│ ├── default
│ ├── manager
│ ├── prometheus
│ ├── rbac
│ └── samples
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
└── internal
└── controller
이 경우 batch.tutorial.kubebuilder.io/v1
에 해당하는 api/v1
디렉토리가 생성된다.
api/v1/cronjob_types.go
파일을 보면, 모든 쿠버네티스 Kind 에 공통으로 포함된 metadata 를 가리키는 meta/v1
API group 을 임포트 하고 있다.
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
다음으로 Kind 의 Spec
과 Status
에 대한 type 을 정의 한다.
쿠버네티스는 원하는 상태(Spec)를 실제 클러스터 상태(Status) 및 외부 상태와 조정한 다음 관찰한 것(Status)를 기록하는 방식으로 작동한다. 따라서 모든 기능 object 는 spec 과 status 를 포함한다. ConfigMap 과 같은 몇몇 타입은 원하는 상태를 인코딩하지 않기 때문에 이 패턴을 따르지 않지만 대부분의 타입은 이 패턴을 따른다.
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// CronJobSpec defines the desired state of CronJob
type CronJobSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// CronJobStatus defines the observed state of CronJob
type CronJobStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
실제 Kind 에 해당하는 타입인 CronJob 과 CronJobList 를 정의한다. CronJob 은 루트 타입이며, CronJob kind를 설명한다. 모든 쿠버네티스 오브젝트와 마찬가지로, API version 과 Kind 를 설명하는 TypeMeta를 포함하며, name, namespace, labes 과 같은 것을 보유하는 ObjectMeta 도 포함한다.
CronJobList 는 단순히 여러 CronJob 을 위한 컨테이너이다. LIST와 같은 대량 작업에 사용되는 Kind 이다.
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// CronJob is the Schema for the cronjobs API
type CronJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CronJobSpec `json:"spec,omitempty"`
Status CronJobStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// CronJobList contains a list of CronJob
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CronJob `json:"items"`
}
마지막으로 API group 에 Go 타입을 추가한다. 이렇게 하면 이 API group 의 타입을 모든 Scheme 에 추가할 수 있다.
func init() {
SchemeBuilder.Register(&CronJob{}, &CronJobList{})
}
API 설계
쿠버네티스에는 API를 설계하는 방법에 대한 몇 가지 규칙이 있다. 즉, 직렬화된 모든 필드는 camelCase
여야 하며 JSON 구조체 태그를 사용하여 이를 지정한다. 또한, 필드가 비어 있을 때 직렬화에서 필드를 생략해야 한다는 것을 표시하기 위해 omitempty
구조체 태그를 사용할 수도 있다.
필드는 대부분의 기본 유형을 사용할 수 있다. 다만 숫자는 예외이다. API 호환성을 위해 정수의 경우 int32
및 int64
, 소수의 경우 resource.Quantity
와 같이 3가지 형식의 숫자를 허용한다.
Quantity 는 10진수에 대한 특수 표기법으로, 머신 간에 이식성을 높이기 위해 명시적으로 고정된 표현을 가지고 있다.
예를 들어 2m
값은 십진수 표기법에서 0.002
를 의미한다. 2Ki
는 십진수로 2048
을 의미하고, 2K
는 십진수로 2000
을 의미한다. 분수를 지정하려면 정수를 사용할 수 있는 접미사로 전환하면 된다(예: 2.5
는 2500m
).
지원되는 베이스는 두 가지이다: 10과 2(각각 10진수 및 2진수라고 함)이다. 10진수는 "nomal" SI 접미사(예: M
및 K
)로 표시되며, 2진수는 "mebi" 표기법(예: Mi
및 Ki
)으로 지정된다. 메가바이트와 메비바이트를 생각하면 된다.
우리가 사용하는 또 다른 특수 유형이 하나 더 있는데, 바로 metav1.Time
이다. 이것은 고정된 이식 가능한 직렬화 형식을 가지고 있다는 점을 제외하면 time.Time
과 동일하게 작동한다.
package v1
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to b
CronJob 을 세부적으로 살펴보자.
먼저 spec 을 보면, spec 에는 원하는 상태가 저장되므로 controller 에 대한 모든 "입력" 은 여기에 저장된다.
기본적으로 크론잡에는 다음과 같은 요소가 필요하다:
- 스케줄 (CronJob 내의 cron)
- 실행할 Job 에 대한 template (CronJob 내의 job)
편하게 만들어줄 몇 가지 추가 기능도 필요하다:
- job 시작에 대한 deadline (이 deadline 을 놓치면 다음 예정된 시간까지 기다리게 된다)
- 여러 job 이 한 번에 실행될 경우 어떻게 할 것인가(기다릴 것인가? 기존 job 을 중지할 것인가? 둘 다 실행할 것인가?)
- CronJob 에 문제가 있을 경우, CronJob 실행을 일시 중지하는 방법
- 이전 job 기록에 대한 limit
자신의 상태를 읽지 않기 때문에 job 이 실행되었는지 여부를 추적할 수 있는 다른 방법이 필요하다. 이를 위해 적어도 하나의 이전 job 을 사용할 수 있다.
// CronJobSpec defines the desired state of CronJob
type CronJobSpec struct {
//+kubebuilder:validation:MinLength=0
// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
Schedule string `json:"schedule"`
//+kubebuilder:validation:Minimum=0
// Optional deadline in seconds for starting the job if it misses scheduled
// time for any reason. Missed jobs executions will be counted as failed ones.
// +optional
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
// Specifies how to treat concurrent executions of a Job.
// Valid values are:
// - "Allow" (default): allows CronJobs to run concurrently;
// - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet;
// - "Replace": cancels currently running job and replaces it with a new one
// +optional
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`
// This flag tells the controller to suspend subsequent executions, it does
// not apply to already started executions. Defaults to false.
// +optional
Suspend *bool `json:"suspend,omitempty"`
// Specifies the job that will be created when executing a CronJob.
JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"`
//+kubebuilder:validation:Minimum=0
// The number of successful finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"`
//+kubebuilder:validation:Minimum=0
// The number of failed finished jobs to retain.
// This is a pointer to distinguish between explicit zero and not specified.
// +optional
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"`
}
ConcurrencyPolicy 는 실제로는 string 이지만, 재사용과 유효성 검사를 쉽게 할 수 있으므로 타입을 재정의 했다.
// ConcurrencyPolicy describes how the job will be handled.
// Only one of the following concurrent policies may be specified.
// If none of the following policies is specified, the default one
// is AllowConcurrent.
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
type ConcurrencyPolicy string
const (
// AllowConcurrent allows CronJobs to run concurrently.
AllowConcurrent ConcurrencyPolicy = "Allow"
// ForbidConcurrent forbids concurrent runs, skipping next run if previous
// hasn't finished yet.
ForbidConcurrent ConcurrencyPolicy = "Forbid"
// ReplaceConcurrent cancels currently running job and replaces it with a new one.
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)
다음은 관찰된 상태를 저장하는 status 를 디자인해 보자.
현재 실행중인 job 목록과 마지막으로 job 을 성공적으로 실행한 시간을 유지한다. 그리고 직렬화를 위해서 time.Time
대신 metav1.Time
을 사용한다.
// CronJobStatus defines the observed state of CronJob
type CronJobStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// A list of pointers to currently running jobs.
// +optional
Active []corev1.ObjectReference `json:"active,omitempty"`
// Information when was the last time the job was successfully scheduled.
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
}
Controller 구현
컨트롤러는 쿠버네티스와 모든 operator 의 핵심이다.
컨트롤러의 역할은 주어진 오브젝트에 대해 실세계의 실제 상태(클러스터 상태와 잠재적으로 외부 상태(예: Kubelet의 경우 컨테이너 실행 또는 Cloud Provider 의 경우 로드밸런서)가 오브젝트의 원하는 상태와 일치하는지 확인하는 것이다. 각 컨트롤러는 하나의 루트 Kind 에 중점을 두지만 다른 Kind 와 상호 작용할 수 있다.
이 프로세스를 reconciling
이라고 부른다.
controller-runtime 에서 특정 kind 에 대한 reconciling 을 구현하는 로직을 Reconciler
라고 한다.
internal/controller/cronjob_controller.go
파일을 살펴 보자.
기본으로 임포트하는 모듈이 있는데, core controller-runtime 라이브러리와 client 패키지, API 타입 패키지가 있다.
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
컨트롤러의 기본 로직은 다음과 같다.
- 명명된 CronJob을 로드한다.
- 모든 active job 을 나열하고, status 를 업데이트 한다.
- 히스토리 수 제한에 따라 오래된 job 을 정리한다.
- Suspend 값이 세팅되었는지 확인 (값이 세팅된 경우 다른 작업을 수행하지 않음)
- 다음 예약된 실행 가져오기
- 새로운 job 이 스케줄에 맞고, deadline 이 지나지 않았으며, 동시성 정책에 의해 차단되지 않은 경우 실행
- 실행 중인 job 이 보이거나 (자동으로 수행됨) 다음 예약된 실행 시간이 되면 Requeue 한다.
임포트 모듈을 추가한다.
package controller
import (
"context"
"fmt"
"sort"
"time"
"github.com/robfig/cron"
kbatch "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ref "k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
테스트를 위해서 Clock 을 추가한다.
// CronJobReconciler reconciles a CronJob object
type CronJobReconciler struct {
client.Client
Scheme *runtime.Scheme
Clock
}
type realClock struct{}
func (_ realClock) Now() time.Time { return time.Now() }
// clock knows how to get the current time.
// It can be used to fake out timing for testing.
type Clock interface {
Now() time.Time
}
RBAC 을 위해 batch group 의 job 을 핸들링 할 수 있는 권한을 추가한다.
//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get
annotation 을 위한 변수를 추가한다.
var (
scheduledTimeAnnotation = "batch.tutorial.kubebuilder.io/scheduled-at"
)
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the CronJob object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
1. 이름으로 CronJob 을 로드한다
client 를 사용하여 CronJob 을 가져온다. 모든 client 의 메소드에는 취소가 가능하게 context 를 아규먼트로 받는다.
var cronJob batchv1.CronJob
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
log.Error(err, "unable to fetch CronJob")
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}
2. 모든 active job 을 나열하고 status 를 업데이트 한다.
var childJobs kbatch.JobList
if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil {
log.Error(err, "unable to list child Jobs")
return ctrl.Result{}, err
}
active job 을 조회했으면 이를 active, successful, failded job 으로 분류한다.
// find the active list of jobs
var activeJobs []*kbatch.Job
var successfulJobs []*kbatch.Job
var failedJobs []*kbatch.Job
var mostRecentTime *time.Time // find the last run so we can update the status
isJobFinished := func(job *kbatch.Job) (bool, kbatch.JobConditionType) {
for _, c := range job.Status.Conditions {
if (c.Type == kbatch.JobComplete || c.Type == kbatch.JobFailed) && c.Status == corev1.ConditionTrue {
return true, c.Type
}
}
return false, ""
}
getScheduledTimeForJob := func(job *kbatch.Job) (*time.Time, error) {
timeRaw := job.Annotations[scheduledTimeAnnotation]
if len(timeRaw) == 0 {
return nil, nil
}
timeParsed, err := time.Parse(time.RFC3339, timeRaw)
if err != nil {
return nil, err
}
return &timeParsed, nil
}
for i, job := range childJobs.Items {
_, finishedType := isJobFinished(&job)
switch finishedType {
case "": // ongoing
activeJobs = append(activeJobs, &childJobs.Items[i])
case kbatch.JobFailed:
failedJobs = append(failedJobs, &childJobs.Items[i])
case kbatch.JobComplete:
successfulJobs = append(successfulJobs, &childJobs.Items[i])
}
// We'll store the launch time in an annotation, so we'll reconstitute that from
// the active jobs themselves.
scheduledTimeForJob, err := getScheduledTimeForJob(&job)
if err != nil {
log.Error(err, "unable to parse schedule time for child job", "job", &job)
continue
}
if scheduledTimeForJob != nil {
if mostRecentTime == nil {
mostRecentTime = scheduledTimeForJob
} else if mostRecentTime.Before(*scheduledTimeForJob) {
mostRecentTime = scheduledTimeForJob
}
}
}
if mostRecentTime != nil {
cronJob.Status.LastScheduleTime = &metav1.Time{Time: *mostRecentTime}
} else {
cronJob.Status.LastScheduleTime = nil
}
cronJob.Status.Active = nil
for _, activeJob := range activeJobs {
jobRef, err := ref.GetReference(r.Scheme, activeJob)
if err != nil {
log.Error(err, "unable to make reference to active job", "job", activeJob)
continue
}
cronJob.Status.Active = append(cronJob.Status.Active, *jobRef)
}
디버깅을 위해서 log 를 남긴다.
log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs))
status 를 업데이트 한다.
if err := r.Status().Update(ctx, &cronJob); err != nil {
log.Error(err, "unable to update CronJob status")
return ctrl.Result{}, err
}
3. 히스토리 수 제한에 따른 오래된 job 삭제하기
// NB: deleting these are "best effort" -- if we fail on a particular one,
// we won't requeue just to finish the deleting.
if cronJob.Spec.FailedJobsHistoryLimit != nil {
sort.Slice(failedJobs, func(i, j int) bool {
if failedJobs[i].Status.StartTime == nil {
return failedJobs[j].Status.StartTime != nil
}
return failedJobs[i].Status.StartTime.Before(failedJobs[j].Status.StartTime)
})
for i, job := range failedJobs {
if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit {
break
}
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
log.Error(err, "unable to delete old failed job", "job", job)
} else {
log.V(0).Info("deleted old failed job", "job", job)
}
}
}
if cronJob.Spec.SuccessfulJobsHistoryLimit != nil {
sort.Slice(successfulJobs, func(i, j int) bool {
if successfulJobs[i].Status.StartTime == nil {
return successfulJobs[j].Status.StartTime != nil
}
return successfulJobs[i].Status.StartTime.Before(successfulJobs[j].Status.StartTime)
})
for i, job := range successfulJobs {
if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit {
break
}
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); (err) != nil {
log.Error(err, "unable to delete old successful job", "job", job)
} else {
log.V(0).Info("deleted old successful job", "job", job)
}
}
}
4. Suspend 값이 세팅되었는지 확인
CronJob 객체에 suspend 값이 세팅되어 있다면 CronJob 을 일시 중단한다. CronJob 을 삭제하지 않고 잠시 멈추고 싶을 때 사용할 수 있다.
if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
log.V(1).Info("cronjob suspended, skipping")
return ctrl.Result{}, nil
}
5. 다음 예약된 실행 가져오기
잠시 멈춤 상태가 아니라면 다음 스케줄을 가져온다.
getNextSchedule := func(cronJob *batchv1.CronJob, now time.Time) (lastMissed time.Time, next time.Time, err error) {
sched, err := cron.ParseStandard(cronJob.Spec.Schedule)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf("Unparseable schedule %q: %v", cronJob.Spec.Schedule, err)
}
// for optimization purposes, cheat a bit and start from our last observed run time
// we could reconstitute this here, but there's not much point, since we've
// just updated it.
var earliestTime time.Time
if cronJob.Status.LastScheduleTime != nil {
earliestTime = cronJob.Status.LastScheduleTime.Time
} else {
earliestTime = cronJob.ObjectMeta.CreationTimestamp.Time
}
if cronJob.Spec.StartingDeadlineSeconds != nil {
// controller is not going to schedule anything below this point
schedulingDeadline := now.Add(-time.Second * time.Duration(*cronJob.Spec.StartingDeadlineSeconds))
if schedulingDeadline.After(earliestTime) {
earliestTime = schedulingDeadline
}
}
if earliestTime.After(now) {
return time.Time{}, sched.Next(now), nil
}
starts := 0
for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) {
lastMissed = t
// An object might miss several starts. For example, if
// controller gets wedged on Friday at 5:01pm when everyone has
// gone home, and someone comes in on Tuesday AM and discovers
// the problem and restarts the controller, then all the hourly
// jobs, more than 80 of them for one hourly scheduledJob, should
// all start running with no further intervention (if the scheduledJob
// allows concurrency and late starts).
//
// However, if there is a bug somewhere, or incorrect clock
// on controller's server or apiservers (for setting creationTimestamp)
// then there could be so many missed start times (it could be off
// by decades or more), that it would eat up all the CPU and memory
// of this controller. In that case, we want to not try to list
// all the missed start times.
starts++
if starts > 100 {
// We can't get the most recent times so just return an empty slice
return time.Time{}, time.Time{}, fmt.Errorf("Too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.")
}
}
return lastMissed, sched.Next(now), nil
}
// figure out the next times that we need to create
// jobs at (or anything we missed).
missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now())
if err != nil {
log.Error(err, "unable to figure out CronJob schedule")
// we don't really care about requeuing until we get an update that
// fixes the schedule, so don't return an error
return ctrl.Result{}, nil
}
requeue 할 값을 준비만 해 놓는다.
scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())} // save this so we can re-use it elsewhere
log = log.WithValues("now", r.Now(), "next run", nextRun)
6. 새로운 job 이 스케줄에 맞고, deadline 이 지나지 않았으며, 동시성 정책에 의해 차단되지 않은 경우 실행
if missedRun.IsZero() {
log.V(1).Info("no upcoming scheduled times, sleeping until next")
return scheduledResult, nil
}
// make sure we're not too late to start the run
log = log.WithValues("current run", missedRun)
tooLate := false
if cronJob.Spec.StartingDeadlineSeconds != nil {
tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now())
}
if tooLate {
log.V(1).Info("missed starting deadline for last run, sleeping till next")
// TODO(directxman12): events
return scheduledResult, nil
}
// figure out how to run this job -- concurrency policy might forbid us from running
// multiple at the same time...
if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 {
log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs))
return scheduledResult, nil
}
// ...or instruct us to replace existing ones...
if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent {
for _, activeJob := range activeJobs {
// we don't care if the job was already deleted
if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
log.Error(err, "unable to delete active job", "job", activeJob)
return ctrl.Result{}, err
}
}
}
constructJobForCronJob := func(cronJob *batchv1.CronJob, scheduledTime time.Time) (*kbatch.Job, error) {
// We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice
name := fmt.Sprintf("%s-%d", cronJob.Name, scheduledTime.Unix())
job := &kbatch.Job{
ObjectMeta: metav1.ObjectMeta{
Labels: make(map[string]string),
Annotations: make(map[string]string),
Name: name,
Namespace: cronJob.Namespace,
},
Spec: *cronJob.Spec.JobTemplate.Spec.DeepCopy(),
}
for k, v := range cronJob.Spec.JobTemplate.Annotations {
job.Annotations[k] = v
}
job.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339)
for k, v := range cronJob.Spec.JobTemplate.Labels {
job.Labels[k] = v
}
if err := ctrl.SetControllerReference(cronJob, job, r.Scheme); err != nil {
return nil, err
}
return job, nil
}
// actually make the job...
job, err := constructJobForCronJob(&cronJob, missedRun)
if err != nil {
log.Error(err, "unable to construct job from template")
// don't bother requeuing until we get a change to the spec
return scheduledResult, nil
}
// ...and create it on the cluster
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to create Job for CronJob", "job", job)
return ctrl.Result{}, err
}
log.V(1).Info("created Job for CronJob run", "job", job)
7. 실행 중인 job 이 보이거나 (자동으로 수행됨) 다음 예약된 실행 시간이 되면 Requeue 한다.
// we'll requeue once we see the running job, and update our status
return scheduledResult, nil
}
Setup
var (
jobOwnerKey = ".metadata.controller"
apiGVStr = batchv1.GroupVersion.String()
)
// SetupWithManager sets up the controller with the Manager.
func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
// set up a real clock, since we're not in a test
if r.Clock == nil {
r.Clock = realClock{}
}
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kbatch.Job{}, jobOwnerKey, func(rawObj client.Object) []string {
// grab the job object, extract the owner...
job := rawObj.(*kbatch.Job)
owner := metav1.GetControllerOf(job)
if owner == nil {
return nil
}
// ...make sure it's a CronJob...
if owner.APIVersion != apiGVStr || owner.Kind != "CronJob" {
return nil
}
// ...and if so, return it
return []string{owner.Name}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&batchv1.CronJob{}).
Owns(&kbatch.Job{}).
Complete(r)
}
Webhook 생성
$ kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
api/v1/cronjob_webhook.go
파일이 생성된다. 해당 파일에 체크 로직을 추가한다.
// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *CronJob) Default() {
cronjoblog.Info("default", "name", r.Name)
if r.Spec.ConcurrencyPolicy == "" {
r.Spec.ConcurrencyPolicy = AllowConcurrent
}
if r.Spec.Suspend == nil {
r.Spec.Suspend = new(bool)
}
if r.Spec.SuccessfulJobsHistoryLimit == nil {
r.Spec.SuccessfulJobsHistoryLimit = new(int32)
*r.Spec.SuccessfulJobsHistoryLimit = 3
}
if r.Spec.FailedJobsHistoryLimit == nil {
r.Spec.FailedJobsHistoryLimit = new(int32)
*r.Spec.FailedJobsHistoryLimit = 1
}
}
var _ webhook.Validator = &CronJob{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateCreate() error {
cronjoblog.Info("validate create", "name", r.Name)
return r.validateCronJob()
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateUpdate(old runtime.Object) error {
cronjoblog.Info("validate update", "name", r.Name)
return r.validateCronJob()
}
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateDelete() error {
cronjoblog.Info("validate delete", "name", r.Name)
// TODO(user): fill in your validation logic upon object deletion.
return nil
}
func (r *CronJob) validateCronJob() error {
var allErrs field.ErrorList
if err := r.validateCronJobName(); err != nil {
allErrs = append(allErrs, err)
}
if err := r.validateCronJobSpec(); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
r.Name, allErrs)
}
func (r *CronJob) validateCronJobSpec() *field.Error {
// The field helpers from the kubernetes API machinery help us return nicely
// structured validation errors.
return validateScheduleFormat(
r.Spec.Schedule,
field.NewPath("spec").Child("schedule"))
}
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, err.Error())
}
return nil
}
func (r *CronJob) validateCronJobName() *field.Error {
if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
// The job name length is 63 character like all Kubernetes objects
// (which must fit in a DNS subdomain). The cronjob controller appends
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
// a job. The job name length limit is 63 characters. Therefore cronjob
// names must have length <= 63-11=52. If we don't validate this here,
// then job creation will fail later.
return field.Invalid(field.NewPath("metadata").Child("name"), r.Name, "must be no more than 52 characters")
}
return nil
}
Controller 배포 및 실행
CR 과 CRD yaml 을 만드는 명령어를 수행한다.
$ make manifests
CRD 를 배포한다.
$ make install
WebHook 를 로컬에서 다른 터미널로 실행한다.
$ export ENABLE_WEBHOOKS=false
$ make run
config/samples/batch_v1_cronjob.yaml
파일에 값을 추가한다.
apiVersion: batch.tutorial.kubebuilder.io/v1
kind: CronJob
metadata:
labels:
app.kubernetes.io/name: cronjob
app.kubernetes.io/instance: cronjob-sample
app.kubernetes.io/part-of: cronjob-kubebuilder
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: cronjob-kubebuilder
name: cronjob-sample
spec:
schedule: "*/1 * * * *"
startingDeadlineSeconds: 60
concurrencyPolicy: Allow # explicitly specify, but Allow is also default.
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
Reference site
- https://book.kubebuilder.io/cronjob-tutorial/
- cronJob webhook source:
https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook.go - Writing a Kubernetes Operator from Scratch Using Kubebuilder:
https://www.youtube.com/watch?v=LLVoyXjYlYM&list=PL8pIiPkgexmmHAppEre9eHAiYqLxGFbOY&index=7 - PVC Operator Sample Source:
https://github.com/civo/operator-demo/blob/main/controllers/demovolume_controller.go - elastic cloud-on-k8s source:
https://github.com/elastic/cloud-on-k8s/blob/main/pkg/apis/elasticsearch/v1/elasticsearch_types.go
https://github.com/elastic/cloud-on-k8s/blob/main/pkg/controller/elasticsearch/elasticsearch_controller.go - Develop on Kubernetes Series — Operator Dev — Understanding and Dissecting Kubebuilder:
https://yash-kukreja-98.medium.com/develop-on-kubernetes-series-operator-dev-understanding-and-dissecting-kubebuilder-4321d3ecd7d6 - Learning Concurrent Reconciling:
https://openkruise.io/blog/learning-concurrent-reconciling/ - Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- Best practices for building Kubernetes Operators and stateful apps:
https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps - Kubernetes Controllers at Scale: Clients, Caches, Conflicts, Patches Explained:
- https://medium.com/@timebertt/kubernetes-controllers-at-scale-clients-caches-conflicts-patches-explained-aa0f7a8b4332
- Kubernetes API guidelines: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- Golang controller-runtime: https://pkg.go.dev/sigs.k8s.io/controller-runtime
- Extend the Kubernetes API with CustomResourceDefinitions: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/