k8s-in-rpi
created : Tue, 03 May 2022 02:11:00 +0900
modified : Tue, 24 May 2022 18:03:00 +0900
TODO (우선순위 순으로)
- ui dashboard 에러 처리하기
- grafana 관련:
- nginx log 확인하기
- alert용 email 설정하기
- grafana dashboard를 통해 jupyter notebook 관련하여 모니터링할 수 있는지 확인하기
- wiki 서비스 설치
- rstudio docker image 빌드후 업로드
- github 로 ci/cd pipeline 만들수 있는지 공부
- gitlab 올려서 private repo service 만들기
배경
- 쿠버네티스 이론 공부를 적당히 하고([[kubernetes-in-action]]), 이제 실습을 좀 해보려고 하는데 주어진 장비가 rpi 4 밖에 없다.
- 어쩔수 없이, 있는걸로 공부해보면서 시행착오와 어떤 것들을 적용시켰는지를 적는 메모장이다.
사양
- 생각 외로 적어놔야 하는 것들을 적어두자.
- HW:
- Raspberry Pi 4
- SW:
- kernel
# uname -a Linux <computer name> 5.4.0-1055-raspi #62-Ubuntu SMP PREEMPT Wed Mar 2 14:43:34 UTC 2022 aarch64 aarch64 aarch64 GNU/Linux
- zsh
- kernel
기본 설명
- 아래 서술하는 모든 내용은 github repo 에 기록하고 있다.
- 언제나 repo가 더 최신이며, 이 이유는 하루 작업, 삽질한 내용을 이 문서에 다시 정리하는 식으로 공부하고 있기 때문이다.
- 따라서 repo 내용을 바로 읽을 수 있는 수준이라면 repo를 먼저 흝고 내용을 보는것이 더 효율적이다.
- 아래 내용은 시간 순서가 아니며, 여러 내용이 동시에 진행되고 있다. 하지만 나중에 작업한다면 반드시 선행되어야 하는 순서로 배치하고 있으니 참고하면 된다.
Kubernetes 설치 -> Kind 설치
좀 찾아봤는데, kind 가 그나마 쓸만할 것 같다. 인턴때 잠깐 써봐서 다행이다.
공식 사이트 설치방법을 따라 가려고 했는데 공식 사이트는 amd64이다.
주소를 보니 대충 github release에 arm64이 있을것 같아서 주소를 거기로 바꿨다.
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.12.0/kind-linux-arm64 chmod +x ./kind-linux-arm64 sudo ln -s $(pwd)/kind-linux-arm64 /usr/local/sbin/kind
cluster 만들기
- 사용하다 보니, PersistentVolume 을 사용할때 hostPath를 사용할 일이 있었다. 아래와 같이 설정해주자.
# kind.yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraMounts: - hostPath: /home/lmu/tools/kind/data containerPath: /tmp/data kubeadmConfigPatches: - | kind: InitConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "ingress-ready=true" extraPortMappings: - containerPort: 80 hostPort: 80 protocol: TCP - containerPort: 443 hostPort: 443 protocol: TCP
kind create cluster --name k8s-in-action --config kind.yaml
kubectl 설치
완벽히 정상작동하는지 테스트 해볼려고 kubectl을 실행시키니 명령어를 안깔았었다. 깔아주자
sudo apt install kubectl
kubectl 실행
kind 에서는 context-name 을 주도록 권장하지만 실제로 설정에는 기본으로 current-context로 들어있다. context를 여러개 쓰는게 아니라면 그냥 쳐도 된다.
kubeconfig 정보
kubectl config view #apiVersion: v1 #clusters: #- cluster: # certificate-authority-data: DATA+OMITTED # server: https://127.0.0.1:36987 # name: kind-k8s-in-action #contexts: #- context: # cluster: kind-k8s-in-action # user: kind-k8s-in-action # name: kind-k8s-in-action #current-context: kind-k8s-in-action #kind: Config #preferences: {} #users: #- name: kind-k8s-in-action # user: # client-certificate-data: REDACTED # client-key-data: REDACTED
Troubleshooting
Error: failed to create cluster: failed to init node with kubeadm: command "docker exec --privileged <cluster name>-control-plane kubeadm init --skip-phases=pre flight --config=/kind/kubeadm.conf --skip-token-print --v=6
- 커널 설정이 잘못되어있어인가 싶어서 https://lance.tistory.com/5 내용을 일부(2-4) 적용해본다.
- 3번 과정에서
/boot/firmware/cmdline.txt
로 바꾸었다. - 바로 안되길레 실망했지만, reboot 하니까 kind가 정상 작동한다.
- 아마도 추정은 iptables 문제이지 않을까? 싶다. 에러메시지들을 읽어보니 정상 실행은 됬는데 kubelet에서 api point를 못찾는 메시지가 출력됬었던걸로 기억
- 3번 과정에서
Nginx Ingress 설정
참고자료:
nginx ingress 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
- cert-manager 설치
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.0/cert-manager.yaml
- letsencrypt 관련된 clusterissuer 설정
# cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
# The ACME server URL
server: https://acme-staging-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: <이메일>
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-staging
# Enable the HTTP-01 challenge provider
solvers:
# An empty 'selector' means that this solver matches all domains
- selector: {}
http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# The ACME server URL
server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: <mail address>
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-prod
# Enable the HTTP-01 challenge provider
solvers:
- http01:
ingress:
class: nginx
kubectl apply -f cluster-issuer.yaml
ingress 설정
ingress namespace 생성
kubectl create namespace ingress
ingress 설정 (참고로 이 문서 하단의 jupyter notebook과 grafana 가 포함되어 있다.)
# ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: root-ingress namespace: ingress annotations: ingress.kubernetes.io/ssl-redirect: "true" kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: "letsencrypt-prod" kubernetes.io/tls-acme: "true" spec: tls: - hosts: - kube.makerdark98.dev secretName: letsencrypt-prod rules: - host: kube.makerdark98.dev http: paths: - path: /jupyter pathType: Prefix backend: service: name: headless-svc-base-notebook port: number: 80 - path: /grafana pathType: Prefix backend: service: name: headless-svc-grafana port: number: 80
kubectl apply -f ingress.yaml
nginx ingress controller를 prometheus 에 수동 등록하기
nginx deployment 설정을 고친다.
참고자료 : nginx 공식 문서
kubectl edit deployement ingress-nginx-controller -n ingress-nginx
template: containers: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9113" prometheus.io/scheme: http spec: ports: - name: prometheus containerPort: 9113
servicemonitor 등록
참고자료 : prometheus github issue
# nginx-service-monitor.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: namespace: monitoring name: nginx-ingress-controller-metrics labels: app: nginx-ingress release: prometheus spec: endpoints: - interval: 30s path: /metrics port: "9113" selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx namespaceSelector: matchNames: - ingress-nginx
kubectl apply -f nginx-service-monitor.yaml
grafana 설정
Troubleshooting
- 블로그 글마다 사용하고 있는 apiVersion이 제각각이다. 위 내용은 생각보다 엄청난 삽질을 통해서 얻어진 내용이고, 현재 기준(2022/05/14)으로 공식문서를 따른다.
networking.k8s.io/v1
을 사용해야한다. 많은 블로그들이extensions/v1
이런 류를 사용하고 있는데 점차 바꾸고 있는것 같다.- certificate 또한 마찬가지이다. 블로그마다 내용이 다른데, 반드시 공식 문서를 읽어보자. 블로그는 대부분 과거 표준을 사용하고 있다.
- ingress 라는 이름의 namespace를 생성하여 사용하고 있는데, 이는 나중에 ExternalName 을 사용하는 서비스를 통해서 보내줘야한다.
- nginx ingress는 잘 동작하는 것같은데 내부 서비스로 연결이 안되는 거 같다면 다음 방법을 사용해보자:
k8s 내부에서 curl 을 사용하는 방법:
kubectl run mycurlpod --image=curlimages/curl -i --tty -- sh
- 위 명령어를 사용하면 k8s 내부에서 domain들이 잘 동작하고 있는지 확인할 수 있다 curl 명령어를 직접 날려보자.
- 주의 : 반드시 다 사용했다면 pod 를 지워주자.
nginx ingress log 보는 방법:
# kubectl get pod -n ingress-nginx 를 하였을 때 controller 이름을 확인한다. kubectl logs -n ingress-nginx ingress-nginx-controller-55c69f5f55-txnhj
Node monitoring tool 설치
참고자료
helm 을 통해서 monitoring 툴(prometheus, grafana) 설치
kubectl create namespace monitoring
helm install --namespace monitoring prometheus prometheus-community/kube-prometheus-stack
- 외부접속 가능하도록 ConfigMap 수정
kubectl edit configmap prometheus-grafana -n monitoring
# 생략
apiVersion: v1
data:
grafana.ini: |
[analytics]
check_for_updates = true
[grafana_net]
url = https://grafana.net
[log]
mode = console
[paths]
data = /var/lib/grafana/
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
[server]
domain = kube.makerdark98.dev
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
serve_from_sub_path = true
# 생략
외부 namespace ingress 에서 사용 가능하도록 하자.
apiVersion: v1 kind: Service metadata: name: headless-svc-grafana namespace: ingress spec: type: ExternalName externalName: prometheus-grafana.monitoring.svc.cluster.local
- 이 서비스를 ingress가 바라보도록 하면 된다:
- 사실 효율적인지에 대한 고찰은 아직 끝내지 못했다. 그리고 비효율적일 것이라고 추정한다.
- 하지만, 이 문서가 kind 위에서 rpi를 kube로 세팅하면서 devops를 공부하는데에 목표가 있으므로 아직 효율성은 논하지 않기로 하자.
- 또한, 지금은 전부 하나의 local cluster 이므로 network를 실제로 타지 않고 memory에서만 동작하고 있으므로 이정도 비효율은 잠시 덮어두자.
- 이 서비스를 ingress가 바라보도록 하면 된다:
Dashboard 구성
- [Create]-[Import] 한뒤 아래 주소를 넣어주자.
- 직접 만들어도 되긴 하지만, 지금 중요한 건 아니다.
Troubleshooting
위와 같이 설치했을 때, grafana password 를 모르겠을 때
- 아래 명령어를 실행해서 grafana password를 알아내자.
kubectl get secrets prometheus-grafana -o jsonpath="{.data.admin-password}" -n monitoring | base64 --decode
ConfigMap 설정해도 접속 안될때
- pod를 재시작 시켜줘야하는 거 같은데 나같은 경우에는 그냥 rpi를 껏다 켰다.
- Deployment를 건들여주면 될거 같긴 하다.
Jupyter Notebook 설치
Jupyter Notebook Image 만들기:
- Jupyter notebook 기본이미지에는 git, numpy, matplotlib, latex 이 없다.
- 깔아주자.
FROM jupyter/base-notebook:notebook-6.4.11 USER root ARG DEBIAN_FRONTEND=noninteractive RUN apt update RUN apt install -y texlive-xetex git tig USER 1000 RUN pip install matplotlib numpy
- Makefile 로 업로드 설정 해주자.
REPO=registry.makerdark98.dev IMAGE_NAME=jupyter/notebook VERSION=0.0.1 release: build publish publish-latest build: @echo 'build docker image' docker build -t $(IMAGE_NAME) . publish: @echo 'create tag $(VERSION)' docker tag $(IMAGE_NAME) $(REPO)/$(IMAGE_NAME):$(VERSION) docker push $(REPO)/$(IMAGE_NAME):$(VERSION) publish-latest: @echo 'create tag latest' docker tag $(IMAGE_NAME) $(REPO)/$(IMAGE_NAME):latest docker push $(REPO)/$(IMAGE_NAME):latest
- 실제로 업로드 해주자
make build make publish
아래 설정을 읽어보자, 전부다 해놨다. k8s 에 대한 지식이 있다면 이해 가능
# jupyter.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jupyter-fileshare-pv
spec:
storageClassName: manual
volumeMode: Filesystem
capacity:
storage: 50Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /tmp/data/jupyter
type: Directory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fileshare-pvc
labels:
component: jupyter
spec:
volumeMode: Filesystem
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
volumeName: jupyter-fileshare-pv
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: base-notebook
labels:
app: base-notebook
spec:
replicas: 1
selector:
matchLabels:
app: base-notebook
template:
metadata:
labels:
app: base-notebook
spec:
containers:
- name: base-notebook
image: registry.makerdark98.dev/jupyter/notebook:0.0.1
ports:
- containerPort: 8888
command: ["start-notebook.sh"]
args: ["--NotebookApp.password='<secret>'", "--NotebookApp.
ip='*'", "--NotebookApp.base_url='/jupyter'"]
env:
- name: DOCKER_STACKS_JUPYTER_CMD
value: nbclassic
- name: GRANT_SUDO
value: "yes"
volumeMounts:
- name: storage
mountPath: "/home/jovyan/work"
securityContext:
runAsUser: 0
volumes:
- name: storage
persistentVolumeClaim:
claimName: fileshare-pvc
---
apiVersion: v1
kind: Service
metadata:
name: base-notebook-svc
spec:
type: LoadBalancer
selector:
app: base-notebook
ports:
- protocol: TCP
port: 80
targetPort: 8888
kubectl apply -f jupyter.yaml
- monitoring tool 과 마찬가지로 ingress namespace 에서도 접근가능하도록 service를 만들어주자.
# headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: headless-svc-base-notebook
namespace: ingress
spec:
type: ExternalName
externalName: base-notebook-svc.jupyter.svc.cluster.local
kubectl apply -f headless-svc.yaml
Troubleshooting
위와 같이 persistentvolume을 사용하면, 처음에 마운트된 폴더에 쓰기 권한이 없다는 것을 알 수 있다.
이를 해결하기 위해서 SUDO 권한 옵션을 넣어서 Docker를 실행하고 있다.
sudo chown -R jovyan:users work
arm64용 jupyter notebook docker image는 아직 base 밖에 없는것 같다. 실제로 니즈가 없는 이미지이기도 하니 직접 빌드해서 사용한다고 생각하자.
Docker registry, Web UI 띄우기
참고 :
먼저 namespace 를 만들어준다.
kubectl create namespace registry
PV와 PVC를 만들어 준다.
# registry.yaml apiVersion: v1 kind: PersistentVolume metadata: name: docker-registry-pv spec: capacity: storage: 20Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete storageClassName: docker-registry-local-storage local: path: /tmp/data/registry nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - k8s-in-action-control-plane --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: docker-registry-pv-claim spec: accessModes: - ReadWriteOnce volumeMode: Filesystem resources: requests: storage: 20Gi storageClassName: docker-registry-local-storage
kubectl apply -f registry.yaml -n registry
registry를 설치한다.
helm upgrade --install docker-registry \ --namespace registry \ --set replicaCount=1 \ --set persistence.enabled=true \ --set persistence.size=20Gi \ --set persistence.deleteEnabled=true \ --set persistence.storageClass=docker-registry-local-storage \ --set persistence.existingClaim=docker-registry-pv-claim \ --set secrets.htpasswd=$(cat $HOME/temp/registry-creds/htpasswd) \ --set nodeSelector.node-type=master \ twuni/docker-registry \ --version 1.10.1
headless service를 만들어준다. for ingress
apiVersion: v1 kind: Service metadata: name: headless-svc-registry namespace: ingress spec: type: ExternalName externalName: docker-registry.registry.svc.cluster.local
registry ingress를 만들어준다.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: registry-ingress namespace: ingress annotations: ingress.kubernetes.io/ssl-redirect: "true" kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: "cluster issuer 이름" kubernetes.io/tls-acme: "true" nginx.ingress.kubernetes.io/enable-cors: "true" nginx.ingress.kubernetes.io/cors-allow-origin: "registry dashboard url" nginx.ingress.kubernetes.io/proxy-body-size: "1024m" spec: tls: - hosts: - <registry url> secretName: "secret 이름" rules: - host: <registry url> http: paths: - path: / pathType: Prefix backend: service: name: headless-svc-registry port: number: 5000
registry ui 를 설치한다.
#!/bin/bash helm upgrade --install docker-registry-ui \ --namespace registry-dashboard \ --set registry.external=true \ --set registry.url=https://registry.makerdark98.dev \ --set ui.title="Docker Registry UI" \ --set ui.replicaCount=1 \ --set ui.nodeSelector.node-type=master \ --set ui.image.tag=main \ --set ui.delete_images=true \ --set ui.ingress.enabled=false \ --set ui.proxy=false \ ./docker-registry-ui
headless service를 만들어준다.
apiVersion: v1 kind: Service metadata: name: headless-svc-registry-dashboard namespace: ingress spec: type: ExternalName externalName: docker-registry-ui-ui.registry-dashboard.svc.cluster.local
ingress로 연결해준다.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: registry-dashboard-ingress namespace: ingress annotations: ingress.kubernetes.io/ssl-redirect: "true" kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: "issuer 이름" kubernetes.io/tls-acme: "true" spec: tls: - hosts: - <dashboard url> secretName: <인증서시크릿 이름> rules: - host: <dashboard url> http: paths: - path: / pathType: Prefix backend: service: name: headless-svc-registry-dashboard port: number: 80
Troubleshooting
docker registry 에 이미지 푸쉬할때 인증에러
docker login
k8s에서 private registry 인증가능하게 하기
#!/bin/bash REPO=registry.makerdark98.dev USER=<user> PASS=<password> NAMESPACE=jupyter kubectl create secret docker-registry regcred \ --docker-server=$REPO \ --docker-username=$USER \ --docker-password=$PASS \ --docker-email=makerdark98@gmail.com \ -n=$NAMESPACE
TODO: 실습하기
간단한 Hello World 프로그램 작성하기
Kubernetes를 사용하기에 앞서 당연하게도 안에 들어갈 프로그램과 docker image가 필요하다.
필요할때 만들어본 경험은 있지만 메모해본적은 없다. 한번 처음부터 해보자.
기왕이면 go 공부하는 겸사겸사 go로 작성했다.
인턴때가 생각난다.package main import ( "fmt" "net/http" "github.com/spf13/cobra" ) func main() { cmd := NewDefaultCommand() err := cmd.Execute() if err != nil { fmt.Errorf("command failed %v\n", err) } } func NewDefaultCommand() *cobra.Command { return &cobra.Command{ Use: "helloword", Run: func(cmd *cobra.Command, args []string) { http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello World")) }) fmt.Printf("listen ::5000") http.ListenAndServe(":5000", nil) }, } }
이렇게
helloworld
바이너리를 만들었다.이제 dockerimage를 만들어보자.
FROM alpine:3.15.4 COPY bin/helloword / ENTRYPOINT ["/helloword"]
docker build -t localhost:5000/hello:0.0.1 .
매번 위 과정을 하긴 귀찮으니, Makefile로 만들자.
all: helloworld docker docker: helloworld docker build -t localhost:5000/hello:0.0.1 . helloworld: CGO_ENABLED=0 go build -o bin/helloworld cmd/helloworld.go clean: rm -f bin/**
위 과정을 github에 업로드해두자
docker 이미지로 pod 만들기
일단 node부터 확인하자
kubectl get nodes # NAME STATUS ROLES AGE VERSION # k8s-in-action-control-plane Ready control-plane,master 22h v1.23.4
pod를 만들어보자
kubectl run helloworld --image=hello/alpine --port=5000 kubectl get pods # NAME READY STATUS RESTARTS AGE # helloworld 0/1 ErrImagePull 0 8s
- 이미지를 가져오는데 에러가 난다.
- 확인해보니 registry에 올려둬야한다고 한다. dockerhub에는 올리기 싫어서, 지난번에 해봤던 docker registry를 먼저 띄워보기로 했다.