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] 한뒤 아래 주소를 넣어주자.
- https://grafana.com/grafana/dashboards/11074
- 직접 만들어도 되긴 하지만, 지금 중요한 건 아니다.
- [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를 먼저 띄워보기로 했다.