레이블이 kubernetes인 게시물을 표시합니다. 모든 게시물 표시
레이블이 kubernetes인 게시물을 표시합니다. 모든 게시물 표시

k8s service account secret

# pod(앱)가 k8s api 로 인증할때 service account(sa) 를 사용하는데
# 모든 pod 가 디폴트로 사용하는 sa default 외 별도로 앱용 sa 를 만들었다.
kubectl get sa
NAME      SECRETS   AGE
default   0         16h
ysoftman1 0         16h

# ysoftman1 pod spec > template > spec > ServiceAccountName: ysoftman1 을 사용하고 있다.
# 그런데 secrects 을 확인해 보면 not found 가 발생한다.
kubectl describe secret
Error from server (NotFound): secrets "ysoftman1" not found

# sa 를 새로 만들어 봐도 not found 가 발생한다.
kubectl create serviceaccount ysoftman2
kubectl describe secret ysoftman
Error from server (NotFound): secrets "ysoftman2" not found

# 찾아보니 1.24 부터(현재 1.26 사용하고 있음) sa 생성시 secret 를 자동 생성해주지 않도록 변경됐다고 한다. 

# token 타입의 secret 를 생성해서 ysoftman sa 에서 사용
cat << zzz | kubectl apply -f -
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: ysoftman1
  annotations:
    kubernetes.io/service-account.name: "ysoftman1"
zzz

# 이제 ysoftman sa 의 secret 이 설정된 것을 볼 수 있다.
kubectl describe secret ysoftman1

# 참고로 새로운 토큰 값만 필요한 경우 다음 명령으로 얻을 수 있다.
kubectl create token ysoftman1 --duration=999999h

observability data pipeline - vector

# kubernetes(k8s) pods stdout,stderr 는 노드의 다음에 경로에 저장된다.
/var/log/pods
/var/log/containers (pods 하위 컨테이너 로그 파일들이 이곳에 링크로 걸려 있음)

# 이런 pod 로그들을 예전에는 fluentd 에서 정재 -> es / kafka 로 보냈는데,
# 요즘에는 fluentd 대신 vector(observability data pipeline - agent & aggregator) 를 많이 사용하는것 같다.
# rust 로 만들어서인지 안정성과 성능이 좋은것 같다. 

# helm 으로 설치
helm repo add vector https://helm.vector.dev
helm repo update

# helm value 설정
# vector 는 다음 3가지 형태(role)로 배포 할 수 있다.
# agent: daemonset 으로 모든 노드의 data(stdout)를 수집
# sidecar: 파드별 pod에 사이드카로 vector 를 띄워 pod에 대해서만 수집
# aggregator: 다른 스트림으로 부터 입력(수집) 
# customConfig 로 디폴트 설정을 대신할 수 있다.
# configmap > data 설정된다.
# kubernetes_logs,host_metrics,internal_metrics(source) -> transform -> prometheus_exporter,console(sink) 로 소비하는 흐름
cat << zzz > values.yaml
role: Agent
customConfig:
  data_dir: /vector-data-dir
  api:
    enabled: true
    address: 127.0.0.1:8686
    playground: false
  sources:
    kubernetes_logs:
      type: kubernetes_logs
    host_metrics:
      filesystem:
        devices:
          excludes: [binfmt_misc]
        filesystems:
          excludes: [binfmt_misc]
        mountpoints:
          excludes: ["*/proc/sys/fs/binfmt_misc"]
      type: host_metrics
    internal_metrics:
      type: internal_metrics
  sinks:
    prom_exporter:
      type: prometheus_exporter
      inputs: [host_metrics, internal_metrics]
      address: 0.0.0.0:9090
    stdout:
      type: console
      inputs: [kubernetes_logs]
      encoding:
        codec: json
zzz

# 설치 하면 Agent 면 daemonset 으로 worker/ingress 노드들에 vector pod 가 설치된다.
helm install vector vector/vector --namespace vector --create-namespace --values values.yaml

# vector 버전업 반영시
helm repo update
helm upgrade vector vector/vector --namespace vector --values values.yaml

# vector 삭제시
helm uninstall vector --namespace vector

# vector 처리 현황 보기
kubectl -n vector exec -it daemonset/vector -- vector top --url http://127.0.0.1:8686/graphql

# k8s log -> filter -> remap -> kafka,elasticsearch,console 로 보내는 경우
# console 은 vector pod log 에서 확인
# vi values.yaml
role: Agent
customConfig:
  data_dir: /vector-data-dir
  api:
    enabled: true
    address: 127.0.0.1:8686
    playground: false
  sources:
    k8s_log:
      type: kubernetes_logs
      # namespace 가 kube_system 아닌것 중 ysoftman 인 것만
      extra_field_selector: metadata.namespace=!kube_system,metadata.namespace=ysoftman
  transforms:
    k8s_transform1:
      type: filter
      inputs:
        - k8s_log
      condition: .level != "debug"
    k8s_transform2:
      type: remap
      inputs:
        - k8s_transform1
      source: |
        # % root of the event metadata
        # . root of the event
        # set es index name
        %custom_type = "sample"
        if .message == r'.*error.*' {
          # % root of the event metadata
          %custom_type = "error"
        }
  sinks:
    kafka_log:
      type: kafka
      inputs: [k8s_transform2]
      bootstrap_servers: logis-kafka-dev.daumtools.com:9092
      topic: kave-dev-sample
      encoding:
        codec: json
    es_log:
      type: elasticsearch
      inputs:
        - k8s_transform2
      endpoints:
        - http://ysoftman.es:9200
      bulk:
        index: "ysoftman-sample-%Y-%m-%d"
    console_log:
      type: console
      inputs: [k8s_transform2]
      encoding:
        codec: json

change grafana pod localtime

# grafana timezone 이 UTC 로 되어 있다.
kubectl exec monitoring-grafana-aaa -it -- date
Wed Mar  6 07:35:45 UTC 2024

# 그래서 로그가 UTC 로 기록된다.
kubectl logs --tail=3 monitoring-grafana-aaa | rg -i "t="
... t=2024-03-06T07:45:00.515393518Z ...

# 이를 KST 로 변경하기 위해 deployment 에서
env > TZ 의 값을 Asia/Seoul 로 변경하면된다.

# 또는 아래와 같이 노드의 timezone 을 container 의 /etc/localtime 을 마운트되도록 설정한다.
kubectl edit deploy monitoring-grafana

spec > template > spec > containers > env > volumeMounts
volumeMounts:
- mountPath: /etc/localtime
  name: localtime-volume

spec > template > spec > containers > env > volumes
volumes:
- hostPath:
    path: /usr/share/zoneinfo/Asia/Seoul
  name: localtime-volume

# pod 가 다시 시작하고 나면 KST 로 변경되어 있다.
kubectl exec monitoring-grafana-aaa -it -- date
Wed Mar  6 16:45:55 KST 2024

# 이제 로그도 KST 로 기록된다.
kubectl logs --tail=3 monitoring-grafana-aaa | rg -i "t="
... t=2024-03-06T16:54:49.939479809+09:00 ...

# k8tz 을 사용하면 pod 에 편한게 적용할 수 있다.
# 배포되면 기본 k8tz 네임스페이스에 service,deployment,pod 등이 뜬다.
# install k8tz
helm repo add k8tz https://k8tz.github.io/k8tz/
helm install k8tz k8tz/k8tz --set timezone=Asia/Seoul

# deploy 등을 재시작해보자.
# 새로 뜨는 파드는 k8tz container 를 사이드카로 해서 locatime 이 반영된다. 

# k8tz 명령어를 사용하는 경우
# install k8tz cli command 
wget -c https://github.com/k8tz/k8tz/releases/download/v0.16.0/k8tz_0.16.0_darwin_amd64.tar.gz -O - | tar zx
./k8tz version

# 수동으로 현재 네임스페이스의 모든 deployment 에 반영하기
kubectl get deploy -oyaml | k8tz inject --timezone=Asia/Seoul -| kubectl apply -f -

# 참고로 grafana dashboard 디폴트 타임존은 다음 값으로 설정하면 된다.
grafana:
  defaultDashboardsTimezone: Asia/Seoul

#####

# argocd 로 등록하기
# --parameter namespace= 를 명시하지 않으면 k8tz 네임스페이스에 pod 가 뜬다.
argocd app create ysoftman-k8tz \
--dest-server https://kubernetes.default.svc \
--sync-option CreateNamespace=true \
--sync-policy automated \
--project ysoftman \
--repo https://k8tz.github.io/k8tz \
--helm-chart k8tz \
--revision "0.16.0" \
--dest-namespace ysoftman-k8tz \
--parameter namespace=ysoftman-k8tz \
--parameter timezone=Asia/Seoul

k8s PersistentVolume 값 변경

# k8s PersistentVolume(pv) > nfs > ip 를 변경하고자 한다.
# patch 로 변경하면 다음과 같이 생성 후에는 변경할 수 없다고 나온다.
kubectl patch pv ysoftmanPV -p '{"spec":{"nfs":{"server":"10.10.10.10"}}}'
Forbidden: spec.persistentvolumesource is immutable after creation

# 참고로 pvc 용량 패치는 되는데, 용량을 줄이면 안된다.
kubectl patch pvc prometheus-1 -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}' -n monitoring
... spec.resources.requests.storage: Forbidden: field can not be less than previous value

# Available 아직 클레임에 바인딩되지 않은 사용할 수 있는 리소스
# Bound 볼륨이 클레임에 바인딩됨
# Released 클레임이 삭제되었지만 클러스터에서 아직 리소스를 반환하지 않음
# Failed 볼륨이 자동 반환에 실패함
status:
  phase: Bound

# claimRef 부분을 삭제하해 Available 상태로 만들 수 있다.
kubectl patch pv ysoftmanPV -p '{"spec":{"claimRef:"null}}'

# 하지만 pv 가 terminating 상태에서 삭제가 안된다.
# finalizers: 오브젝트 삭제 시 충족해야될 조건을 명시하는 곳
# kubernetes.io/pv-protection: pv, pvc 등에서 실수로 오브젝트 삭제를 막기위해 기본적으로 명시되어 있다.
kind: PersistentVolume
metadata:
  finalizers:
  - kubernetes.io/pv-protection

# 다음과 같이 finalizers 조건을 패치(또는 kubectl edit.. 로 해당 부분 삭제)하면, pv 가 삭제된다.
kubectl patch pv ysoftmanPV -p '{"metadata":{"finalizers":null}}'

#####

# 위 내용을 바탕으로 많은 PV 값을 수정해 반영하는 스크립트를 다음과 같이 작성한다.
# 우선 변경할 pv 들을 yaml 로 로컬에 백업해두자.
mkdir -p pv
for name in $(kubectl get pv -A | grep -i aaa | awk '{print $1}'); do 
    echo "backup pv manifest(yaml)... ./pv/$name.yaml"
    kubectl get pv $name -o yaml > ./pv/$name.yaml
done

# pv 삭제
for name in $(kubectl get pv -A | grep -i aaa | awk '{print $1}'); do 
    echo "delete pv manifest(yaml)"
    # delete 하면 terminating 상태가 유지되는데, 이때 finalizers > kubernetes.io/pv-protection 를 삭제해야 완전히 제거된다.
    kubectl delete pv $name & kubectl patch pv $name -p '{"metadata":{"finalizers":null}}'
done

# 백업해둔 pv yaml 에서 ip 만 변경해서 적용
for f in $(ls -1 ./pv); do 
    cat ./pv/$f | sed -e 's/server: 10.10.10.11/server: 10.10.10.12/'g | kubectl apply -f -
done

k8s pod ephemeral storage

# 운영중인 pod 다음과 에러로 evicted(pod 중지) 됐다.
# evicted pod event log describe
Type    Reason   .. From .... Message
Warngin evicted ... kubelet ... Pod ephemeral local storage usage exceeds the total limit of containers 5000Mi
Normal killing ... kubelet ... Stopping containers ysoftman_pod

# 원인은 서버가 pod 내 /tmp/xxx  업로드한 파일을 쓰고 있어 발생했다.

# 테스트를 위해 container 접속해서
# ysoftman_10GB 크기가 커지는것으로 모니터링 하고
watch -n 1 ls -ahl ysoftman_10GB

# container 접속 터미널을 하나 더 열고
# ysoftman_10GB 를 1GB 씩 늘려 본다.
rm -rf ysoftman_10GB
touch ysoftman_10GB
for ((i=1;i<=10;i++)); do
    echo $i
    sleep 1
    dd if=/dev/urandom bs=1000000 count=1000 >> ysoftman_10GB
done

# 참고로 한번에 쓸 경우
# dd if=/dev/urandom of=ysoftman_10GB bs=1000000 count=10000

# 이러면 1GB write 이후 pod 가 evicted 된다.
# ephemeral-storage 값을 늘리면 된다.
pod > spec > containers >  ... 
resources
  requests:
    ephemeral-storage: 10Gi
  limits:
    ephemeral-storage: 10Gi


##########


# 참고
# ephemeral-storage 리소스 종류에는
# [emptydir]
# pod 시작시 생서 pod 내 모든 container 들은 emptydir 볼륨에 동일한 파일을 읽고 쓴다.(컨테이너들간 공유)
# 로컬의 kubelet 베이스 디렉터리(보통 루트 디스크) 또는 램에서 제공
# 외 configmap 등이 있다.
# 다음과 같이 명시해 적용하면 /cache 를 emptyDir 로 사용할 수 있다.
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

# emptydir 는 pod 삭제되면 사라지는 휘발성이지만
# hostpath 를 이용하면 실제 호스트 노드에 마운트해서 pod 가 내려가도 노드에 데이터가 남는다.
# 하지만 pod 다른 호스트 노드에 뜰 수 있기 때문에 완벽한 데이터 보존 방법은 아니다.
# 그리고 hostpath 은 호스트 노드를 접근하는거라 보안 위험이 있어 되도록이면 사용하지 않는것이 좋다.
# 다음과 같이 명시해 적용하면 /test-pd 로 호스트의 /data 에 접근해 사용할 수 있다.
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory

install kubernetes

# kubernetes(k8s) 는 물리적으로 master(control plane 이라고 불린다.) 와 worker 노드로 구성된다.

# k8s master 노드의 컴포넌트들
# kube-apiserver : kubectl, kubelet, kube-scheduler, kube-controller 등 의 api 요청을 처리하는 웹서버 역할
# etcd : key-value 저장소로 k8s 모든 정보가 저장되는 곳이다. kubectl 등의 리소스 명세서등도 여기 저장된다.
# kube-scheduler : 워커노드의 리소스 상태를 파악하고 pod 를 적절한 워커노드에 띄운다.
# kube-controller : pod 를 모니터링하며서 pod 가 특정 상태를 유지하도록 한다.(ex. replica 2 일때 pod 2개중 1개가 죽으면 pod 1개를 새로 띄운다.)

# k8s worker 노드 컴포넌트
# kubelet : master 의 kube-apiserver 와 통신하며 원하는 리소스(ex. pod)들을 관리하는 일종의 에이전트
# kube-proxy : network proxy 역할

# ubuntu 에서 kubeadm(k8s 클러스터 구성 및 관리 툴) 설치

# apt 저장소 추가
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update

# master, worker 모두에 설치하자.
k8s_version="1.19.16-00"
sudo apt-get install -y kubelet=${k8s_version} kubeadm=${k8s_version} kubectl=${k8s_version}

# kubelet kubeadm kubectl 업데이트 되지 않도록 hold 설정
sudo apt-mark hold kubelet kubeadm kubectl

# kubeadm 으로 클러스터를 구성한다.
sudo kubeadm init \
 --apiserver-advertise-address=192.168.104.2 \
 --apiserver-cert-extra-sans=192.168.104.2 \
 --node-name=control-plane \
 --pod-network-cidr=10.224.0.0/16 \
 --service-cidr=10.225.0.0/16

# 설치가 완료되면 다음과 같은 메시지가 출력된다.
# kubectl 사용을 위한 kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
... 생략 ...
# worker -> master 에 조인을 위해  worker 노드에서 실행한다.
... 생략 ...
kubeadm join 192.168.104.2:6443 --token aaaaa --discovery-token-ca-cert-hash sha256:aaaaa
... 생략 ...

# 참고로 kube-apiserver 에서 6443포트를 사용한다.

# master 에서 노드 상태 watching 하고
kubectl get nodes -w

# worker1, worker2 에서 다음 명령을 실행해 master 노드에 join 하도록 한다.
sudo kubeadm join 192.168.104.2:6443 --token aaaaa --discovery-token-ca-cert-hash sha256:aaaaa

# node 에서 돌고 있는 서비스
kubelet
kube-proxy

# master node 돌고 있는 서비스
kube-scheduler
kube-controller-manager
kube-apiserver


#####

# 노드 삭제시
kubectl delete node 노드명

# 노드에 접속해서 kubelet 서비스 비활성화 및 설정 파일 제거
ssh ysoftman@노드 "sudo kubeadm reset -f"

# 클러스터 정보 확인
kubectl cluster-info

# 노드 추가시 
# 노드에서 kubeadm, kubelet, kubectl 설치하고
# token 확인
sudo kubeadm token list

# hash 확인
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

# 마스터 노드에 조인
sudo kubeadm join 마스터노드IP:6443 --token 토큰 --discovery-token-ca-cert-hash sha256:해시

ingress-nginx-controller not found error

# ingress nginx controller(k8s.gcr.io/ingress-nginx/controller:v0.44.0) log 를 확인해 보면
kubectl logs -f $(kubectl get pod -n ingress-nginx | rg -N ingress-nginx-controller --color never  | awk '{print $1}')

# 다음과 같은 에러가 무수히 발생하고 있다.
E1227 03:37:10.227828       7 queue.go:130] "requeuing" err="services \"ingress-nginx-controller\" not found" key="&ObjectMeta{Name:sync status,GenerateName:,Namespace:,SelfLink:,UID:,ResourceVersion:,Generation:0,CreationTimestamp:0001-01-01 00:00:00 +0000 UTC,DeletionTimestamp:<nil>,DeletionGracePeriodSeconds:nil,Labels:map[string]string{},Annotations:map[string]string{},OwnerReferences:[]OwnerReference{},Finalizers:[],ClusterName:,ManagedFields:[]ManagedFieldsEntry{},}"

# nginx 설정에 다음과 같이 --publish-service 를 ingress-nginx-controller 로 설정했는데 ingress-nginx-controller 서비스가 존재하지 않았다.
--publish-service=$(POD_NAMESPACE)/ingress-nginx-controller

# ingress-nginx-controller 를 사용하는 ingress-nginx-controller 이름의 서비스 리소스를 생성하면 더이상 에러가 발생하지 않는다.
cat << zzz | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
zzz

k8s ValidatingWebhookConfiguration 으로 문법 에러 ingress 리소스 생성 방지

# kubectl 과 같은 api 요청을 받는 k8s 서버는 kube-api handler 이후 요청한 리소스에 대해 mutating(리소스 변경),validating(리소스 검증) admission(허용 여부 판단 webhook) 처리를 하여 실제 리소스 설정(etcd 에 저장)될지 말지를 처리한다.

# nginx 에 적용될 server-snippet 등에 문법이 에러가 있는 ingress (리소스)를 적용 요청을 하면 에러 없이 리소스가 생성(등록)되는게 문제다.
kubectl apply -f syntax_error_ingress.yaml
ingress.extensions/ysoftman-test-ingress created

# nginx log 확인해 보면 에러 ingress 리소스 로드 시도가 계속 실패
kubectl logs -f $(kubectl get pod -n ingress-nginx | rg -N ingress-nginx-controller --color never  | awk '{print $1}')
 -------------------------------------------------------------------------------
Error: exit status 1
2021/12/22 17:19:34 [emerg] 15035#15035: invalid number of arguments in "proxy_set_header" directive in /tmp/nginx-cfg062794518:1076
nginx: [emerg] invalid number of arguments in "proxy_set_header" directive in /tmp/nginx-cfg062794518:1076
nginx: configuration file /tmp/nginx-cfg062794518 test failed
-------------------------------------------------------------------------------
W1222 17:19:34.507544       7 queue.go:130] requeuing ysoftman-test/ysoftman-test-ingress, err
-------------------------------------------------------------------------------

# 잘못된 ingress 리소스가 등록되어 계속 nginx 가 리로딩 실패해 문제가 되니 바로 지우자.
kubectl delete -f syntax_error_ingress.yaml

# 잘못된 설정으로 ingress-nginx-controller 가 전체에 영향 주는것을 막기 위해
# validating admission webhook server 를 옵션으로 노출할 수 있다.
# ValidatingWebhookConfiguration 리소스를 등록한다.


#####


# ValidatingWebhookConfiguration 으로 문법 에러 ingress 리소스 생성 방지하기

# (minikube 기준) kube-apiserver enable-admission-plugins 옵션에 ValidatingAdmissionWebhook 가 있는지 확인
kubectl get pod kube-apiserver-minikube -o=json -n kube-system | jq '.spec.containers[0].command' | rg -N "enable-admission-plugins"
  "--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota",

# ValidatingAdmissionWebhook 보이지 않아도 디폴트로 추가되어 있어 별도로 추가하지 않아도 된다.
# master 노드(장비) 마다 접속해 다음 파일에서
# --enable-admission-plugins 값을 추가하면
# kubelet(cluster의 모든 노드에 떠있는 agent)이 변경을 감지해 kube-apiserver(pod)가 자동으로 재시작 된다.
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml


# 방법1 - helm 으로 설치하면 ValidatingWebhookConfiguration 관련 리소스들이 자동 설치된다.


# 방법2 - ValidatingWebhookConfiguration 수동 등록
# 다음 명령 결과가 있다면 admission controller 를 사용할 수 있다.
# k8s 버전에 따라 
# k8s 1.6 이후는 admissionregistration.k8s.io/v1
# k8s 1.9 이후는 admissionregistration.k8s.io/v1beta1
kubectl api-versions | grep admissionregistration

# ingress-nginx-controller 버전 확인
kubectl get daemonset ingress-nginx-controller -n ingress-nginx -o=json | jq '.spec.template.spec.containers[0].image'
"quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.1"

# ValidatingWebhookConfiguration 은 TLS 로 통신해야 한다.
# 다음과 같이 self-singed 로 키를 생성한다.
# service 에서 사용할 이름이 매칭될 수 있도록 CN을 다음과 같이 설정한다. (CN=${SERVICE_NAME}.${NAMESPACE}.svc)
openssl req -x509 -newkey rsa:2048 -keyout validating-webhook-key.pem -out validating-webhook-cert.pem -days 100000 -nodes -subj "/CN=ingress-nginx-controller-admission.ingress-nginx.svc"

# secret 리소스를 등록하자.(base64 인코딩돼 등록되기 때문에 secret 리소스를 보면 LS0... 으로 시작하는 문자열이 된다.)
kubectl create secret tls ingress-validation-tls -n ingress-nginx \
--key validating-webhook-key.pem \
--cert validating-webhook-cert.pem

# ingress-nginx-controller 옵션 --validating-webhook 옵션들 추가
# secret 는 volumes, volumeMounts 로 pod 에서 파일로 접근하도록 한다.
kubectl edit daemonset ingress-nginx-controller -n ingress-nginx
... 생략 ...
      containers:
      - args:
        - /nginx-ingress-controller
        - --enable-ssl-chain-completion=false
        - --configmap=$(POD_NAMESPACE)/ingress-nginx
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
        - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
        - --annotations-prefix=nginx.ingress.kubernetes.io
        - --default-backend-service=default/default-backend-service
        - --report-node-internal-ip-address
        - --validating-webhook=:8443
        - --validating-webhook-certificate=/usr/local/certificates/tls.crt
        - --validating-webhook-key=/usr/local/certificates/tls.key
... 생략 ...
        ports:
        - containerPort: 8443
          name: webhook
          protocol: tcp
... 생략 ...
          volumeMounts:
          - name: webhook-cert
            mountPath: /usr/local/certificates/
            readOnly: true
... 생략 ...
      volumes:
      - name: webhook-cert
        secret:
          secretName: ingress-validation-tls

# ValidatingWebhookConfiguration 과 ingress-nginx-controller-admission service 리소스 등록은 아래 URL에 정리


#####


# 참고 이슈(삽질 엄청함ㅠ)
# 위의 모든 설정을 했는데 invalid ingress 가 아무런 제약없이 created 된다.
# ingrss nginx controller pod 로그를 보면 validationwebhook 은 동작되지만
kubectl logs -f $(kubectl get pod -n ingress-nginx | rg -N ingress-nginx-controller --color never  | awk '{print $1}') | rg "admission" -C 2

# 다음 로그 처럼 accepting 되는 문제가 있었다.
server.go:61] handling admission controller request /extensions/v1beta1/ingress?timeout=10s
main.go:87] accepting non ingress  in namespace ysoftman-test-namespace extensions/v1beta1, Resource=ingresses

# 나와 같이 nginx-ingress-controller:0.25.1 에서
# extensions/v1beta1 를 사용하지 못하는 문제가 있었다.

# syntax_error_ingress apiVersion 을 다음과 같이 변경
extensions/v1beta1 --> networking.k8s.io/v1beta1

# ValidatingWebhookConfiguration 에 networking.k8s.io/v1beta api 추가
  - apiGroups:
    - networking.k8s.io
    - extensions
    apiVersions:
    - v1
    - v1beta1
... 생략 ...
      path: /networking.k8s.io/v1beta1/ingresses

# 이제 server_snippnet 오타가 있는 ingress 등록시 에러가 발생하고 생성되지 않는다.
kubectl apply -f syntax_error_ingress.yaml
namespace/ysoftman-test-namespace unchanged
Error from server: error when creating "syntax_error_ingress.yaml": admission webhook "validate.nginx.ingress.kubernetes.io" denied the request:
-------------------------------------------------------------------------------
Error: exit status 1
2021/12/28 19:11:31 [emerg] 1385#1385: unexpected "}" in /tmp/nginx-cfg650933801:19967
nginx: [emerg] unexpected "}" in /tmp/nginx-cfg650933801:19967
nginx: configuration file /tmp/nginx-cfg650933801 test failed

user namespace 로 kubernetes 와 container 보안 향상

user namespace 로 kubernetes 와 container 보안 향상


!!! Caution 잘못된 번역이나 이해로 틀릴 수 있음!!! ^^;;

[user namespace 란?]
user namespace 는 user IDs, group IDs 를 격리(isolate)한다. 리눅스에서 모든 프로세스는 특정 userid(/etc/passwd에 정의)와 groupid(/etc/group에 정의)로 소유된다. user namespace 는 container 안에서는 host의 userid,groupid 의 일부만 볼 수 있도록 한다.
예를 들어 container 안에서의 root(user id 0)는 사실 호스트의 userid=100000 이다.

[user namespace and capabilities]
container 에서 root 로 보이지만 실제는 user namespace 로 구분되는 사용자다. 이와 관련된 linux kernel 히스토리를 살펴보자.

- linux 2.2 이전에는 실제 root(user id 0)는 1개 였다.
- linux 2.2 이후부터 root 권한이 여러개의 기능 구분되는 capabilities(https://man7.org/linux/man-pages/man7/capabilities.7.html)로 나뉘어졌다. ex) CAP_NET_ADMIN : network 설정권한, CAP_SYS_ADMIN : mount 설정권한
- linux 3.8 (2013년) 에 user namespace 가 소개되고 capabilities 는 더이상 글로벌하게 사용하지 않고 user namespace 의 context 에 의해서 해석된다.

예로 sshfs(fuse 로 ssh filesystem 에 마운트) 프로그램을 실행할때 CAP_SYS_ADMIN capability 가 요구된다. 이건 host 와 container 사이를 격리시키는 목적에 맞지 않게 container 에거 너무 막강한 권한을 부여하게 된다.
(참고로 FUSE:Filesystem in Userspace 로 user 레벨에서 파일시스템을 만들 수 있도록 해준다.)

container 가 새로운 user namespace 없이 기동된 상태에서 sshfs 성공하기 위핸선 CAP_SYS_ADMIN 을 허용할 수 밖에 없어 보안 취약점을 내포하고 있다.(호슽와 같은 name space 를 쓰면 sshfs 프로세스가 CAP_SYS_ADMIN 권한을 가지고 있어 container 안에서 host 로 나쁜짓을 할 수 있음?)

하지만 container 가 새로운 user namespace 로 격리돼 실행된다면 호스트 user namespace 의 CAP_SYS_ADMIN 없이도 container 내에서의 CAP_SYS_ADMIN 를 허용할 수 있다.(host user namespace 가 아닌 container user namespace 의 CAP_SYS_ADMIN 로 container 의 mount namespace 의 filesystem 에 마운트하는것이라 호스트에는 영향이 없다.)
container 내에서의 root 는 호스트의 root 가 아니기 때문에 host 에 영향을 줘서는 안된다.

[user namespace and filesystems]
linux 는 user namespace 에서의 mounting 안전을 고려해 FS_USERNS_MOUNT flag 를 마크해 filesystems 리스트를 유지한다. 초기화되지 않은 user namespace 에 새로운 마운트를 생성하려면 FS_USERNS_MOUNT 타입이어야 한다. linux 저장소에서 다음과 검색해보면 리스트 찾아 볼 수 있다. 
git grep -nw FS_USERNS_MOUNT

초기화 되지 않은 uesr namespace 는 초기에는 profs 와 sysfs 타입의 파일 시스템으로 제한되었는데 지금은 아래와 같은 파일 시스템이 허용된다. 이런 파일 시스템은 권한이 없는 사용자도 안전하게 사용할 수 있기 때문에 허용됐다.

파일 시스템         user namespace 에서 허용됨
procfs,sysfs --> linux 3.8, 2012
tmpfs        --> linux 3.9, 2013
cgroupfs     --> linux 4.6, 2016
FUSE         --> linux 4.18, 2018
overlay      --> linux 5.11, 2020
NFS,ext4,btrfs,etc  --> 허용 안됨

[impact on container security]
user namespace 는 호스트와 container 를 격리하는 보안 레이어다.
호스트의 runc(OCI(Open Container Initiative)를 따른 컨테이너 런타임)가 container 에 의해 overwritten 될 수 있는 취약점이 있었다. 이 취약점은  runc 바이너리는 root 소유이고 container 에 매핑되지 않았는데도, 컨테이너의 프로세스가 /proc/self/exe 를 통해 호스트의 runc 를 참조를 허용한다. 
flatcar container linux 는 호스트의 runc 바이너리를 읽기전용 파일시스템을 두어 완화했다.

[enabling user namespace for FUSE filesystem]
linux 커널은 파일이 변경되었다면 읽거나 쓰기전에 IMA(integrity Measurement Architecture) 서브시스템을 사용해 감지하고 감사(audit)한다.
FUSE 와 같은 파일시스템은 커널이 remeasure, reppraise, reaudit 없이 파일을 서빙할 수 있다. 
memfs FUSE driver 의 패치를 테스트 하기 위해 다음과 같은 시나오리가 있다고 하자.
1. 첫번째 요청에 FUSE 드라이버는 파일의 초기 내용으로 서빙된다.
2. IMA 파일에 대한 측정(measurement)을 제공한다.
3. 두번째 요청에 FUSE 드라이버는 같은 파일의 변경된 내용으로 서빙된다.
4. IMA 내용을 다시 측정하지 않아 내용 변경이 측정되지 않는다.

IMA 에 'force' 옵션을 주어 항상 remeasure, reppraise, reaudit 하도록 하는 패치를 생각해보자.
커널이 아무 변경도 감지하지 못했는데 모든 요청마다 강제로 측정이 수행된다. FUSE 에서 다르게 동작하는 부분이 다른 파일스템들에서 모두 알수는 없어, 이로 인해 'force' 옵션이 잘못된 레이어에 추가될 수 있다. 이 문제를 해결하기 위해 IMA 캐싱 관련 옵션 수행하는 FS_NO_IMA_CACHE(v1,v2,v3,v4) flag 를 제공한다. IMA 는 FS_NO_IMA_CACHE flat 를 체크한 후 'force' 이 있으면 사용한다. 이런 방법으로 IMA 은 다른 파일 시스템들 모두를 알 필요가 없다.
하지만 IMA 'force' 옵션은 모든 문제를 해결하지는 못하고 아직 중요사항들은 검증되지 않았다. IMA 관리자들에 의해 linux 4.17 에는 다음과 같은 flag가 구현되어졌다.
- SB_I_IMA_UNVERIFIABLE_SIGNATURE flag : 중요 검증되지 않았음을 표시하는
- SB_I_UNTRUSTED_MOUNTER flag : 신뢰하지 않는 user namespace 에 마운트 되었을 때

[bring user namespace to kubernetes]
kubernetes(k8s)에서는 user namespace 기능을 사용할 수 없다. 2016년 관련 개선을 논의하기 시작했다.
첫번째 시도로 k8s 는 default 가 아닌 Container Runtime Interface(CRI)를 사용해 kubelet 는 CRI 의 gRPC interface 로 컨테이너와 통신하고 주요 목적으로 user namespace 를 제공한다.

kubelet 이 container runtime 와 통신하는 방법

- kubelet docker api 로 통신 (deprecated)
Kubelet --> docker --> containers

- kubelet CRI의 gRPC 로 통신, CRI shim(CRI protocol 처리)
Kubelet --(CRI)--> CRI shim --> container runtime --> containers

- CRI protocol처리하는 containerd/cri 로 컴파일된 containerd 를 사용
Kubelet --(CRI)--> containerd/cri|containerd --(OCI spec)--> runc --> containers

[CRI changes for user namespaces]
user namespace 지원을 위한 CRI 변경 아이디어

1. kubelet 이 sandbox(infrastructure container) pod 시작을 위해 RunPodSandBox()로 container runtime 에 요청하면 pod 안에 container 들의 namespace 를 공유
2. kubelet 이 sandbox 에 CreateContainer(), pod 의 각 container들은 sandbox 를 참조하고 있음.

이렇게 해서 pod 의 컨테이너들은 use namespace 를 공유한다.

IPC 와 network namespace 도 pod 레벨에서 공유된다. IPC, network 는 pod 의 user namespacce 의 의해 소유되고 이 네임스페이스에서의 capabilities 가 허용된다.
pod 의 각 container 의 mount namespace 도 user namespace 의 의해 소유된다. 
(pod 에 network, ipc, user namespace 를 두면 호스트와는 격리된체 pod 내 container 들은 자유롭게 통신할 수 있다.)

[kubernetes volumes]
container 들이 같은 volume에 접근할때 다음 시나리오를 고려해 보자.

1. container1 에서 NFS 에 파일을 쓴다. 이 파일은 uid=10000(container1 mapping 되있음)
2. container2 에서 NFS 의 파일을 읽는다. container2에 uid=10000이 mapping 되어 있지 않아 uid=65534(nobody)로 보여진다. 

이 문제를 해결하기 위해 다음과 같은 방법을 사용한다.

1. 모든 pod 에 매핑되어 있는 uid 를 사용한다. 컨테이너들간의 격리는 줄어들지만 user namespace 사용하지 않는것 보단 안전하다.
2. 각 pod 에 매핑되어 다른 uid 를 사용하고 실제 파일을 사용할때 마다 uid를 convert 할 수 있는 메커니즘(mechanism)기능을 추가한다. linux 커널에는 shiftfs, fsid mapping, new mout api 와 같은 메커니즘을 제공한다. 하지만 아직까지는 linux upstream 제품에는 없다.

오늘날 volume 사용하지 않는 작업이 많이 있고 이는 user namespace 에 있어서 이익이다. 완벽한 해결책을 찾으려고 k8s 에 구현중인 user namespace 를 막지는 말자.

[conclusion]
user namespace 는 linux 에서 container 들을 더 안전하게 사용할 수 있는 유용한 레이어를 제공한다. 이는 지난 몇몇 취약점들을 완화하는 것으로 입증됐다. volume 제공시 단점으로 어려움을 겪고 있지만 linux kernel 에서 개발중이니 앞으로 많이 개선될것으로 기대한다.
언젠가 kubernetes 가 user namespace 를 지원을 완료하면 kubernetes 는 호스트와 컨테이너들 간에 더욱 안전하게 격리 효과를 볼 것이다. 이는 좀더 많은 권한으로 운영되는 container 들에서의 새로운 상황에도 적용된다. user namespace 없이 뭔가를 한다는것 아주 위험하다.

pid 네임스페이스에 관한 이슈와 해결책

[들어 가기전]
고아(orphan) process : 부모 프로세스가 자식 프로세스보다 먼저 종료되면, 자식 프로세스는 고아 프로세스가 된다. 고아 프로세스는 init(pid 1)에서 거둬 들여, 고아의 부모 프로세스는 init(pid 1)이 된다. 고아 프로세스가 종료되면 init 이 종료 상태를 파악해 좀비 프로세스가 되는것을 막는다.

좀비(zombie/defunct) process : 자식 프로세스는 종료되었지만 부모 프로세스가 wait()등으로 자식 프로세스의 종료 상태를 파악하지 못해 자식 프로세스를 거둬 드릴 수 없는 경우로 process table 에만 남아 있어 kill -9 좀비프로세스PID 로도 삭제되지 않는다.
좀비 프로세스를 삭제 방법
- kill -CHLD PPID (부모 프로세스가 CHLD 시그널을 처리할 수 있는 경우에만 가능)
- kill -KILL PPID (좀비 프로세스의 부모프로세스를 제거한다. 모든 child 가 죽기 때문에 신중해야함)

pid 네임스페이스에 관한 이슈와 해결책을 보고 정리해 봤다

!!! Caution 잘못된 번역이나 이해로 틀릴 수 있음!!! ^^;;


[linux 의 pid(process identifier)]

user space 의 root 프로세스는 init (pid 1, 이것의 부모는 pid 0 으로 커널)이다.
1 번 프로세스가 죽으면 커널이 패닉을 발생해 시스템을 재부팅해야 한다.


[linux namespace 생성]

리눅스 네임스페이스들(uts(host, domain 구분), network, mount, ipc, cgroup(cpu,memory,disk,network 등 리소스 구분), pid)는 unshare 시스템콜로 생성한다. unshare 하면 바로 새로운 네임스페이스로 들어간다.
하지만 pid 네임스페이스의 경우 unshare 로 바로 pid 네임스페이스로 들어가지 않고, fork 로 pid 네임스페이스에 pid 1(init)번를� 만든다. 이 1번 프로세스는 pid 네임스페이스 밖(호스트)에서는 다른 pid 값이다.


[네임스페이스에서의 pid 1]

1. pid 네임스페이스의 init 프로세스는 기본 signal 핸들러가 없어, HUP, INT, QUIT, TERM, KILL .. 등의 시그널 처리를 하지 못한다.(별다른 옵션 없이 docker 컨테이너를 수행하면 ctrl-c 가 동작하지 않는다. 그래서 docker kill 등을 사용한다.)

2. pid 네임스페이스의 init 외 다른 프로세스가 자식 프로세스보다 먼저 죽으면 그 자식 프로세스의 부모는 init (pid 1)이 되고, init 은 프로세스들이 exit 상태를 수집해 커널이 process table 에서 정리할 수 있도록 한다.

3. pid 네임스페이스의 init(pid 1)이 죽으면, 같은 네임스페이스의 모든 프로세스가 강제로 제거되며, pid 네임스페이스는 clean up 된다.


[docker 의 실수]

docker(runc:lightweight docker runtime)는 컨테이너의 entrypoint 를 pid 1 로 하는 새로운 pid 네임스페이스를 만든다. pid 1번에서 signal 처리가 되어 있지 않으면 ctrl-c 와 같은 signal 이 동작하지 않는다. 손자 프로세스가 종료(exit)되기 전에 죽은 자식 프로세스를 fork 하게 되면 zombie(좀비) 프로세스가 컨테이너에 쌓이게 되고, 결국 process table 을 꽉 채우게되는 상황이 발생할 수 있다.
이런 문제를 피하기 위해 fork-exec 를 할 수 있는 특별할 application process 를 실행할 수 있다. 이는 컨테이너가 복잡해지지만 실제 호스트(컨테이너가 아닌)의 init 처럼 만들 수 있다. 사람들은 종속성 격리의 이점을 희생하더라도 여러개의 apt 를 실행(dockerfile 에서 apt install a b c 컨테이너 내에서 패키지를 설치)을 포함시키는 경향이 있다.


[Rkt 해결책]

Rkt(rkt is an application container engine) 로 위 문제를 다소 해결할 수 있다. Rkt 는 사용자가 만든 시작 프로세스(container process)가 init 이 아니라고 가정하고 init (systemd) 를 생성하고 systemd 로 컨테이너에서 동작하는 filesystem 네임스페이스를 만들고 구동시킨다. systemd 는 pid 1번이 되고, (사용자가 만든) container process 는 pid 2번으로 동작한다. 만약 container process 가 init 이라면 이것도 pid 2 로 동작해 또 다른 이슈가 될 수 있다.


[간단한 대체 방법]

fork 후 pid 네임스페이스에서 container process 를 바로 실행하는 대신, spawner 가 다시 fork 한다. 이 두번째 fork 는 container spawner 가 pid 1이 되도록 하고, 모든 child 프로세스들에 signal을 전달할 수 있는 signal handler 를 구성한다. 이렇게 하면 child 프로세스가 죽을때 까지 좀비 프로세스를 거둬들일 수 있고, 이 시점에 container process 의 종료 상태를 수집해 컨테이너화 시스템에 전달한다. 이것은 ctrl-c 와 같은 signal 을 처리할 수 있고 zombie 프로세스도 적절히 거둬들 수 있다는 의미다.
docker 1.13 부터 가능하며 --init 플래그를 사용하면 된다.


[하나의 pod 내에서의 다중 컨테이너]

pod 는 네임스페이스를 공유 하는 컨테이너들의 집합이다. rkt 에서는 파일시스템 네임스페이스를 제외한 모든 네임스페이스들이 공유된다.
앞서 pid 네임스페이스 이슈로 kubernetes(k8s) 는 pod 내 컨테이너들간에 pid 네임스페이스를 공유 하지 않는다. 때문에 같은 pod 내의 container 들 끼리 signal 주고 받을 수 없고, 앞서 언급한 init 문제를 가지고 있어 container processor 는 pid 1로 실행된다.
rkt 방법을 사용하면 컨테이너 안에 init 프로세스를 만들지 않아도 통신이나 signal 을 주고받을 수 있는 컨테이너들을 쉽게 생성할 수 있다. 하지만 이미 존재하는 pod 에 컨테이너를 추가하는 상황에서는 바로 되지는 않는다.


[pod 에 컨테이너 추가하기]

k8s 는 pod sandbox 라는 개념이 있다. k8s 컨테이너 런타임은 컨테이너 시작에 앞서 리소스를 할당하는데, 이는 특히 네트워킹에 유용하고 존재하는 pod 에 컨테이너를 추가하는데도 적용할 수 있다. sandbox pod 를 먼저 생성 후 컨테이너를 하나씩 추가하는 경우, 왜 나중에 컨테이너 추가하는게 허용되지 않을까? 이는 데이터베이스 백업이나 로그 수집과 같은 주기적인 작업에 특히 유용할 것이다.
rkt(에서는 container 를 app 으로 부른다.)는 실험적으로 컨테이너와 독립적으로 pod 생성을 허용하여, 추후에라도 pod 에서 컨테이너를 추가,삭제할 수 있다. 별도 실행되는 유닛(프로세스?)없이 systemd 로 가능하게 한다. 새로운 app(container) 시작 요청이 있으면 pod 의 systemd 와 통신한다. 이러한 모델로 init 프로세스는 다음과 같은 추가적인 특권(privilege)을 가진다.
- 호스트 파일시스템 네임스페이스에 접근할 수 있어서 컨테이너 시작을 위한 파일시스템 네임스페이스를 생성할 수 있다.
- 각 app 시작에 필요한 privilege 들을 알 수 없어 전체 privilege 을 유지해야 한다.
- 컨테이너상의 모든 실행중인 프로세스를 볼 수 있다.
non-sandbox 모델이라면 init 프로세스는 child 프로세스를 시작한 다음 compromise 영향을 최소화 하기 위해 privilege 를 삭제한다.


[sandboxes 와 pid namespaces]

init, sandboxes, pid namespace 를 이용한 방법들 각각 약간의 결점(단점)이 있고, 다음 옵션을 사용할 수 있다.
1. pid namespace 가 sandbox 와 같이 생성되지 않을때, 각 컨테이너는 자신의만의 pid 네임스페이스를 가진다. k8s 에서 사용하는 방식이며, signal 처리를 위한 간단한 init 전략이다. 중요 단점으로는 pod 내 프로세스들간 signal 을 통신을 할 수 없다.
2. pid namespace 가 sandbox 와 같이 생성되지 않을때, sandbox 에서 처음으로 시작되는 컨테이너에 pid namespace 가 생성된다. 이 상태에서는 프로세스간 signal을 주고 받을 수 있다. 단점은 첫번째 프로세스가 pod 의 master 가 되고 이 master 프로세스가 죽으면 컨테이너 내 모든 프로세스는 커널에 의해 종료된다. 때문에 master 프로세스는 pod lifecycle 동안 살아 있어야 한다.
3. pid namespace 가 sandbox와 함께 생성될때, 다른 프로세스를 실행할 수 있는 smart init 프로세스를 포함한다. 이것은 rkt app sandbox 가 동작하는 방식이다. 단점으로는 init 프로세스가 너무 많은 privilege 를 가져 그 만큼 보안 공격에 취약할 수 있다.
4. pid namespace 가 sandbox와 함께 생성될때, sandbox 는 signal을 처리하고 zombie 프로세스를 회수하는 간단한 init 프로세스를 포함한다. 다른 프로세스들은 pid namespace 로 들어갈 수 있지만 부모 프로세스들은 네임스페이스 밖에 있다. init 프로세스는 새로운 컨테이너를 시작하지 않기 때문에, privilege 나 호스트 파일시스템 액세스를 유지할 필요가 없다. 단점은 네임스페이스 내의 각 프로세스들은 pid 0인 부모를 가져 일반적인 process tree 가 깨진다.
5. pid namespace 와 init 이 정확히 위 4번 옵션과 같을때, 다른 프로세스들은 pid namespace 에 들어가고 daemonize 된다. 커널은 위 4번에 pid 0으로 깨진 process tree 에서의 부모를 init 으로 돌릴 것이다. 단점은 daemonize 후 밖에 있는 새로운 프로세스를 모니터링 하기 어렵다. containerizationo system 은 간단하게 프로세스를 waiting 하는 대신 pid 를 통해 프로세스를 추적하도록 강요된다.

(필자는) 위 옵션 중 4번, 5번을 선호한다. 사실 기대하는 프로세스의 lifetime 에 따라 하나를 선택하면 된다. 위 옵션들은 프로세스를 오래 실행하기 위한 좋은 방법들이고 특히 프로세스를 spawner 로 daemonize 프로세스를 종료하는 docker 환경에 좋은 솔루션이다. 만약 간단하게 끝나는 프로세스라면 4번 옵션을 사용하고 프로세스를 pid process tree 와 분리해 유지하면 아주 쉽다. 
어떤 작업들은 k8s 가 init 처럼 동작하는 pause container를 생성하는것과 비슷하다. k8s 에 pid namespace 공유가 지원되면 5번 옵션도 가능할것이다.


[결론]

pid namespace 에는 꽤 숨겨진 복잡도가 존재한다. 오늘날 containerization 시스템은 중요한 단점과 이걸 피할 수 있는 방법들에 따라 선택한다. docker 의 싱글 컨테이너의 단점은 잘 이해되고 적당한 해결방법이 있는 반면, 컨테이너 spawner 가 init 처럼 동작하게 허용되면 컨테이너 빌더 작업이 단순화된다.
그룹 컨테이너의 경우 init 분리 방식이 rkt 가 docker 보다 우수하다. rkt 방식은 현재 k8s pod 모델에서 가능하지 않는 signal 을 통한 프로세스간 통신을 허용한다. 하지만 지연된 시작 컨테이너가 포함되기 때문에 rkt 방식도 몇가지 단점을 보인다.
지연된 시작 컨테이너에 가장 강력한 해결 방법은 pid namespace 와 함께 간단한 init 프로세스를 시작하는것이다. 하지만 새로운 컨테이너를 생성하려면 container spawner 를 통해야 한다. 이는 init 프로세스가 privileges 삭제를 허용하여, 공격을 차단한다. spawner 는 새 프로세스를 daemonize 하고 process tree 를 일관되게 유지하거나 새 프로세스의 부모로 남아 프로세스 관리를 단순화할 수 있다.


k8s 디폴트 서비스로 커스텀 에러 페이지 응답하기

# 다음과 같은 흐름에서
ysoftman.test.com --> 10.10.10.100 (VIP) --> 10.10.10.11,10.10.10.12 (ingress node)

# 도메인으로 접속을 하면 서비스나 nginx controller 의 custom errorpage 로 응답하는데,
curl ysoftman.test.com

# ip 로 요청하면 nginx 기본 404 page(html)을 응답준다.(nginx 정보가 표시되어 보안 수정 사항!)
curl 10.10.10.100
<html>
  <head><title>404 Not Found</title></head>
  <body>
    <center><h1>404 Not Found</h1></center>
    <hr><center>nginx</center>
  </body>
</html>

# nginx pod ssh 접속해 설정을 보면
vi /etc/nginx/nginx.conf 
# backend 설정이 되지 않거나 endpoints 가 없는 경우 기본 404 페이지를 리턴한다.
... 생략 ...
        # backend for when default-backend-service is not configured or it does not have endpoints
        server {
                listen 8181 default_server reuseport backlog=16777216;
                set $proxy_upstream_name "internal";
                access_log off;
                location / {
                        return 404;
                }
        }
... 생략 ...

# 전체 네임스페이스를 찾아보니 디폴트 백엔드가 설정되지 않는 ingress 설정들이 있었다.
kubectl describe ing --all-namespaces | rg 'not found' -B 2
Namespace:        aaaa
Address:          10.10.10.11,10.10.10.12
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
... 생략 ...

# 각 네임스페이스 인그레스에 디폴트 백엔드 설정을 하긴 힘들고
# 새 서비스 추가할때마다 backend servcie 설정을 신경써야 하기 때문에
# nginx controller daemoneset (없다면 deployment)의 yaml에 args 부분에
# 다음과 같이 default-backend-service 로 nginx-controller 자체에 디폴트 백엔드를 명시할 수 있다.
   spec:
      containers:
      - args:
        - /nginx-ingress-controller
        - --enable-ssl-chain-completion=false
        - --configmap=$(POD_NAMESPACE)/ingress-nginx
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
        - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
        - --annotations-prefix=nginx.ingress.kubernetes.io
        - --report-node-internal-ip-address
        - --default-backend-service=ysoftman-namespace/ysoftman-service

# 기본적으로 default-backend-service 는 / 404 와 /healthz 200 만 노출한다.
# default-backend-service 으로 보낼 http 에러 코드를 추가 할 수있다. 
# ingress-nginx-controller -> configmap 에 다음 키와 값을 추가한다
... 생략 ...
data:
  custom-http-errors: 400,401,403,404,405,500,503,505

# 이제 default-backend-service 에서는 X-Code 등의 특정 헤더로 전달 받을 수 있다.

#####

# default-backend-service 는 다음 예제를 사용할수도 있지만
# caddy 도커 이미지로 default-backend-service 를 만들었다.
COPY Caddyfile /etc/caddy/Caddyfile
# index.html 를 에러 페이지로 덮어쓰기
COPY error404.html /usr/share/caddy/error404.html
COPY error404.html /usr/share/caddy/index.html
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

# Caddyfile 은 다음과 같다.
:80
root * /usr/share/caddy
file_server
handle_errors {

#    @404 {
#        expression {http.error.status_code} == 404
#    }

# rewrite @404 /error404.html
    @fromNginxController40x {
        header X-Code 40*
    }
    @fromNginxController50x {
        header X-Code 50*
    }
    @4xx {
        expression "{http.error.status_code} >= 400 && {http.error.status_code} < 500"
    }
    @5xx {
        expression "{http.error.status_code} >= 500 & {http.error.status_code} < 600"
    }
    rewrite @fromNginxController40x /error404.html
    rewrite @fromNginxController50x /error404.html
    rewrite @4xx /error404.html
    rewrite @5xx /error404.html
    file_server
}
log {
    level INFO
    output file /tmp/caddylog.log {
        roll_size 100MiB
        roll_keep 5
        roll_keep_for 48h
    }
}

# caddy 를 로컬 컨테이너로 올려 테스트하면 설정한 에러 페이지가 잘 보인다.
curl http://localhost
curl http://127.0.0.1
curl http://127.0.0.1/aaaaa

# nginx controller --default-backend-service 에 설정하면 에러가 발생한다.
connect() failed (111: Connection refused) while connecting to upstream, 
 client: 10.10.10.11, server: _, request: "GET /aaaaa HTTP/1.1", upstream: "http://10.10.10.12:443/aaaaa"

# 위 에러 로그를 보면 http 인데 443 포트를 사용하고 있다.
# caddy default-backend-service 에 다음과 같이 443 포트가 설정되어 있었다.
apiVersion: v1
kind: Service
metadata:
  namespace: default
  name: default-backend-service
spec:
  ports:
    - name: caddy-https
      port: 443
      targetPort: 443
    - name: caddy-http
      port: 80
      targetPort: 80
  selector:
    app: default-backend-service-deployment

# http 인데도 443 포트를 우선으로 시도 하는 것으로 보인다.
# 443 포트 설정을 제거하고 80 포트만 사용하니 잘된다.
spec:
  ports:
    - name: caddy-http
      port: 80
      targetPort: 80

k8s dashboard 사용하기

# k8s dashboard 사용하기
# 참고로 oauth proxy + github 인증으로 로컬 proxy 서버없이 접근하는 방법도 있다.

# k8s dashboard 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.5/aio/deploy/recommended.yaml

# 로컬에서 k8s api 연결 할 수 있는 프록시 서버 띄우기
kubectl proxy

# 로컬 URL 로 dashboard 접근
http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

#####

# 위의 로컬 프록시는 매번 로컬에 프록시 서버를 띄워 접속해야 돼서 불편하다.
# 고정 도메인으로 접속 하기

# 방법1 - ingress -> service 80 -> pod 9090 으로 연결하는 방식
# kubernetes-dashboard deployment 설정 변경
# 모든 8443 -> 9090 으로 변경
# scheme HTTPS -> HTTP 로 변경
# container arg 기존 설정은 다 삭제하고 다음으로 변경
- --namespace=kubernetes-dashboard
- --insecure-bind-address=0.0.0.0
- --insecure-port=9090
- --enable-insecure-login

# kubernetes-dashboard service 설정 변경
# 80 -> pod 9090 으로 연결되도록 변경
ports:
- port: 80
  protocol: TCP
  targetPort: 9090

# kubernetes-dashboard ingress 추가
# host 는 ingress-nginx controller 설치시 생성되는 aws loadbalaner 사용
cat << EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: kubernetes-dashboard
  name: kubernetes-dashboard-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/backend-protocol: HTTP
    nginx.ingress.kubernetes.io/cors-allow-origin: '*'
    nginx.ingress.kubernetes.io/enable-cors: "false"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
    - host: aaa111.elb.ap-northeast-2.amazonaws.com
      http:
        paths:
          - path: /dashboard/(.*)
            backend:
              serviceName: kubernetes-dashboard
              servicePort: 80
EOF

# 접속 확인
https://aaa111.elb.ap-northeast-2.amazonaws.com/dashboard/#/login

# 구입한 도메인이 있다면 호스트명 변경해서 사용하자
kubectl edit ingress kubernetes-dashboard-ingress -n kubernetes-dashboard
    # 구입한 도메인을 사용
    - host: lemon-apple.ysoftman.com

# 접속 확인
https://lemon-apple.ysoftman.com/dashboard/#/login

# 참고로 위 설정에서 http 를 사용했으니
# 로컬 kubectl proxy 사용시 다음과 같이 http 를 사용해야 한다.
http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/#/login

# 방법2
# 별도 서버가 있다면 aws 클러스터 접속 설정하고 kubectl proxy 실행한다
nohup kubectl proxy

#####

# dashboard service(admin) account 토큰을 파악해 대시보드 로그인에 사용
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | rg admin | awk '{print $1}') | rg -i token: | awk '{print $2}' | pbcopy

aws eks 접속 토큰

# aws eks 에 생성한 클러스터 접근시
# 다음과 같이 접속 정보를 설정할 수 있다.
# --kubeconfig 가 없으면 ~/.kube/config 에 추가된다.
aws eks update-kubeconfig --region ap-northeast-2 --name ysoftman-cluster --kubeconfig ~/.kube/aws-ysoftman.yaml

# KUBECONFIG 환경변수 설정
export KUBECONFIG=~/.kube/config:~/.kube/aws-ysoftman.yaml 

# 그런데 실제 접속 정보를 보면 사용자 token: 값 대신
# 다음과 같이 aws eks get-token 으로 새 토큰을 얻는 명령 수행 방식으로 설정된다.
vi ~/.kube/kubeconfig-aws-ysoftman.yaml
... 생략 ...
users:
- name: arn:aws:eks:ap-northeast-2:1111111111:cluster/ysoftman-cluster
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - ap-northeast-2
      - eks
      - get-token
      - --cluster-name
      - ysoftman-cluster
      command: aws

# 에 보면 get-token 으로 얻은 토큰은 TTL(time to live)이 15분이라
# 15분마다 새 토큰으로 갱신이 필요한데,
# 클라이언트가 kubectl 인 경우 위 설정으로 자동 갱신을 해준다.

# 그런데 서버의 설정에는 고정 토큰 값으로 k8s api 을 사용하고 있다.
# 위의 명령을 다음과 같이 실행해 토큰 값을 파악할 수 있지만,
# 서버 설정 파일에 토큰을 설정하면 15분뒤에는 연결이 끊어진다.
aws --region ap-northeast-2 eks get-token --cluster-name ysoftman-cluster | jq .status.token

# 아래 사이트에서 k8s dashboard 설치시 service-account 를 추가하거나
# 다음과 같이 admin(service-account) 토큰 값을 파악해 
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin | awk '{print $1}')

# ~/.kube/kubeconfig-aws-ysoftman.yaml 를 다음과 같이 고정하면 만료 없이 사용할 수 있긴 하다.
users:
- name: arn:aws:eks:ap-northeast-2:1111111111:cluster/ysoftman-cluster
  user:
    token: "위에서 파악한 토큰값"


k8s service account 생성

# 외부 drone ci 등에 신규 k8s 클러스터에 접근이 필요한 경우
# service account 와 clusterrolebinding(Role-based access control (RBAC)) 리소스를 생성하고
# service account 의 token을 사용하면 된다.

# 다음과 같이 service account 명세를 작성한다.
cat << zzz > service_account_for_droneci.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: drone-ci
  namespace: default

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: drone-ci-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: drone-ci
  namespace: default
zzz

# 이제 적용하면 serviceAccount 가 secret 이 생성된다.
kubectl apply -f service_account_for_droneci.yaml

# drone-ci-token-xxx 를 파악해서 ~/.kube/config 에서 사용하면 된다.
kubectl get secret $(kubectl get secret -n default | grep drone | awk '{print $1}') -n default -o jsonpath="{.data.token}"

# 인증서 확인
kubectl get secrets

# 참고로 A 네임스페이스의 인증서 B 네임스페이스에 적용하기
kubectl get secret -n A ysoftman-test-cert -o yaml > cert.yaml
# cert.yaml 의 namespace=B 로 수정해서 적용한다.
kubectl apply -f cert.yaml

k8s ingress-controller model

k8s 에서 설정된 특정(ysoftman.lemon.com) 호스트에 대해 여러개의 인그레스가 있고
각 인그레스는 default backend (path rule 에 없는 경우 처리할 백엔드 서비스 설정) 가 설정되어 있다.
이 중 A 인그레스의 default backend 의 설정 또는 삭제 변경에 대해서만 ingress-nginx-controller 에 반영되어 처리되는 현상이 있어 찾아봤다.

참고

위 링크를 보면 클러스터 전반에 대한 셋팅으로 nginx model(설정이 메모리에 로딩된 상태를 의미하는것 같다.)이라는 것이 있다. 이 모델은 생성(빌드)에 비용이 많이 들어 특정 작업이 있을때만 빌드(로드된다.)
그리고 모델 생성시 규칙이 있는데 여러 인그레스 중 같은 host, path 등을 가지고 있다면 가장 오래된(최초로 만들어진) 인그레스의 설정이 우선한다.

모델 생성시 오래된 rule 우선 규칙으로 인해 가장 오랜된 A 인그레스의 설정만
ingress-nginx-controller 의 default backend 설정에 반영된다.

# 참고로 생성된지 오래된 순으로 ingress 파악
kubectl get ingress --sort-by=.metadata.creationTimestamp 

원인 파악하느라 너무 삽질을 많이 했다. 
이제 속이 후련해서 개비스콘 짤 생성~ㅋ (https://gvsc.rajephon.dev/)

k8s ingress-nginx-controller 설정(nginx.conf) 파악

# ingress-nginx(controller) 에서 ysoftman.lemon.com / 에 대한 처리가
# 어떻게 되는지 보기 위해 --v=3 옵션을 주고 ingress-nginx-controller 를
# 다시 시작(deployment 리소스가 없으면 daemonset 리소스를 수정)
# 다음과 같이 실제 nginx 설정이 어떻게 되어 있는지 볼 수 있지만 diff 만 보인다.ㅠㅠ
kubectl logs -f $(kubectl get pod -n ingress-nginx  | rg -v NAME | awk '{print$1}' | head -1) -n ingress-nginx | rg "server_name ysoftman.lemon.com" -C 50

# 실제 running 중인 ingress-nginx-controller 의 nginx.conf 을 덤프해보자.
# nginx 동작하는 노드 접속
ssh ysoftman@인그레스서버

# nginx-controller 도커 컨테이너에 접속
sudo docker exec -it --user=0 --privileged $(sudo docker ps | grep ingress-nginx-controller | awk '{print $1}') bash

# nginx 가 --with-debug 옵션으로 동작 중인지 확인
nginx -V 2>&1 | grep -- '--with-debug'

# nginx master PID 파악해서 gdb 로 열기
gdb -p $(ps -ef | grep "nginx: master" | grep -v grep | awk '{print $2}')

# (gdb) 에 아래 명령 복붙
set $cd = ngx_cycle->config_dump
set $nelts = $cd.nelts
set $elts = (ngx_conf_dump_t*)($cd.elts)
while ($nelts-- > 0)
set $name = $elts[$nelts]->name.data
printf "Dumping %s to nginx_conf.txt\n", $name
append memory nginx_conf.txt \
        $elts[$nelts]->buffer.start $elts[$nelts]->buffer.end
end

# (gdb) 종료
quit

# 덤프된 nginx_conf.txt 확인
cat nginx_conf.txt

# 호스트로 빠져 나온다.
exit

# 컨테이너 -> 호스트(노드)로 nginx_conf.txt 복사
sudo docker cp $(sudo docker ps | grep ingress-nginx-controller | awk '{print $1}'):/etc/nginx/nginx_conf.txt .

# 로컬로 빠져 나온다.
exit

# 노드 -> 로컬로 nginx_conf.txt 복사
rsync ysoftman@인그레스서버:/home/ysoftman/nginx_conf.txt .

# nginx_conf.txt 파일이 너무 크니
# ## start server ysoftman.lemon.com  ~  ## end server ysoftman.lemon.com 만 남기고 지운다.

# 문제가 있는 location 의 라인번호를 보고 다시 nginx_conf.txt 에서 찾아 보자
rg -n "location " nginx_conf.txt


#####


# 좀더 편하게 kubectl ingress-nginx 플러그인을 사용해 파악할 수도 있다.
# nginx-controller 가 daemonset 으로 동작하는 경우 pod 를 찾을 수 없다고 나온다.
# deployment 로 동작하는 경우만 작동하는것으로 보인다.
# 설치
brew install krew
kubectl krew install ingress-nginx
export PATH="${PATH}:${HOME}/.krew/bin"

# 백엔드 정보
kubectl ingress-nginx backends -n nginx-ingress

# 특정 호스트 관련 설정 정보
kubectl ingress-nginx conf -n ingress-nginx --host testaddr.local


k8s configmap 내용 즉시 적용

# kubernetes(k8s) configmap 으로 값을 쓰면 실제 pod 에 전달(적용)까지
# 시간이 좀 걸리는 것 같아 찾아보니

`
As a result, the total delay from the moment when the ConfigMap is updated to the moment when new keys are projected to the pod can be as long as kubelet sync period (1 minute by default) + ttl of ConfigMaps cache (1 minute by default) in kubelet. You can trigger an immediate refresh by updating one of the pod's annotations.
`

# 적용까지 1분+1분(캐시) 걸리 수 있는데,... pod annotation 업데이트하면 pod 에 바로 반영되는것 같다.

# 실제 configmap 적용 후 바로 다음과 같이 refresh 를 위해 pod annotation 을 업데이트하니 바로 pod 에 적용된다.
# 계속 사용해야 하니 유닉스타임(초)와 같은 타임스탬프값을 사용하면 좋을 것 같다.
kubectl annotate --overwrite pods $(kubectl get pods | grep ysoftman | awk '{print $1}') updated="$(date +%s)"



k8s nginx-controller redirect

# k8s 에서 커스텀 에러페이지로 리다이렉트하기
# 각 서비스마다 처리하는게 아니기 때문에 ingress 리소스가 아닌 nginx-controller 에서 처리해야 된다.
# ingress-nginx configmap 적용이 필요하다.
# server-snippet(nginx.conf) 에서 에러코드의 로케이션을 설정한다.

# nginx-controller-configmap.yaml 작성
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: ingress-nginx
  name: ingress-nginx
data:  
  server-snippet: |
    # server 정보등 등과 보안 위험 노출 관련 응답 헤더에서 제거
    more_clear_headers "server";
    error_page 400 /custom_error_400.html;
    error_page 401 /custom_error_401.html;
    error_page 403 /custom_error_403.html;
    error_page 404 /custom_error_404.html;
    error_page 500 501 502 503 504 /custom_error_50x.html;
    location = /custom_error_400.html {
        # return 200 "<h2>Error</h2>\n";
        default_type application/json;
        return 400 '{"code":"400", "message": "bad request"}';
    }
    location = /custom_error_401.html {
        # return 200 "<h2>Error</h2>\n";
        more_set_headers "aaa: lemon" "bbb: apple";
        default_type application/json;
        return 401 '{"code":"401", "message": "unauthorized"}';
    }
    location = /custom_error_403.html {
        # return 200 "<h2>Error</h2>\n";
        more_set_headers "aaa: lemon" "bbb: apple";
        default_type application/json;
        return 403 '{"code":"403", "message": "forbidden"}';
    }
    location = /custom_error_404.html {
        # return 200 "<h2>Error</h2>\n";
        more_set_headers "aaa: lemon" "bbb: apple";
        default_type application/json;
        return 404 '{"code":"404", "message": "not found"}';
    }
    location = /custom_error_50x.html {
        # return 200 "<h2>Error</h2>\n";
        more_set_headers "aaa: lemon" "bbb: apple";
        # nginx 기본 지시자
        add_header test1 "test1";
        add_header test2 "test2";
        default_type application/json;
        return 500 '{"code":"50x", "message": "server error"}';
    }


# 적용
kubectl apply -f nginx-controller-configmap.yaml

# 이제 없는 페이지등의 에러인 경우 위에 설정한 에러 응답을 준다.
http://my.ysoftman.com/non-page.html

docker 이미지 새 repository 에 올리기

# aaa -> bbb repository 로 도커 이미지 복사하기
# aaa 저장소 이미지 다운받기
docker pull ysoftman/aaa/test:master

# aaa 로 부터 bbb 저정소 태그로 생성
docker tag ysoftman/aaa/test:master ysoftman/bbb/test:master

# bbb 태그로 올리기
docker login -u "ysoftman" -p "password123" {ysoftman서버}
docker push ysoftman/bbb/test:master

k8s service external-ip pending

# k8s N 개의 service 에 LoadBalancer 타입의 external-ip 를 설정한 경우
# 몇개의 service 는 external-ip 가 <pending> 상태로 더이상 진행되지 않는 경우가 있다.

# external-ip 가 <pending> 상태인 서비스를 상태를 보면
kubectl describe svc ysoftman-service1

# 다음과 같이 loadbalancer 생성에 실패한 이벤트 기록이 보인다.
 Events:
  Type     Reason                  Age                  From                Message
  ----     ------                  ----                 ----                -------
Warning SyncLoadBalancerFailed 7m3s service-controller Error syncing load balancer: failed to ensure load balancer:  
... 구축된 환경의 커스텀 에러 메시지 표시 ...


# 현재 네임스페이스에서 존재할 수 있는 loadbalancers 의 수가 초과돼 발생할 수 있다.
# services.loadbalancers 의 값을 늘려주면 된다.

# 모든 네임스페이스별 quota 설정 상태
kubectl describe quota --all-namespaces

# qutoa 설정
cat <<EOF > object-counts.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-counts
spec:
  hard:
    configmaps: "10"
    persistentvolumeclaims: "4"
    pods: "4"
    replicationcontrollers: "20"
    secrets: "10"
    services: "10"
    services.loadbalancers: "2"
EOF

# 참고