Prometheus Operator로 etcd 모니터링 하기

etcd 모니터링 하기

etcd는 /metrics 라는, 프로메테우스가 수집할 수 있는 매트릭 엔드 포인트를 제공한다. 하지만, Secure Etcd 클러스터인 경우에는 해당 엔드 포인트에 접근하기 위해서는 인증서가 필요하다.

(다른 방법으로는 /metrics 엔드 포인트를 다른 포트로 분리하여, 인증서 없이 접근할 수도 있다. --listen-metrics-urls 옵션을 참고 바란다.)

환경

helm을 사용해서 prometheus-operator를 설치할 것이다. 그래서 prometheus-operator를 설치할때, etcd를 모니터링하도록 설정 파일을 변경해서 사용한다.

values.yaml 수정하기

kubeEtcd

kubeEtcd.serviceMonitor의 값들을 변경한다. scheme를 https로 변경하고, 인증서 정보를 등록한다.

## Component scraping etcd
##
kubeEtcd:
...
  serviceMonitor:
    scheme: https
    insecureSkipVerify: false
    serverName: localhost
    caFile: /etc/prometheus/secrets/etcd-client-cert/etcd-ca
    certFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client
    keyFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client-key
...

prometheus

프로메테우스를 기동할때 etcd-client-cert란 이름의 secret를 pod에 마운트하기 위해서, prometheus.secrets에 etcd-client-cert를 추가해 준다. 그리고, etcd 스크랩 설정 추가를 위해서, prometheus.additionalScrapeConfigs의 kube-etcd 부분을 활성화 해준다

## Deploy a Prometheus instance
##
prometheus:
...
    secrets:
      - "etcd-client-cert"

...

    additionalScrapeConfigs:
      - job_name: kube-etcd
        kubernetes_sd_configs:
          - role: node
        scheme: https
        tls_config:
          ca_file:   /etc/prometheus/secrets/etcd-client-cert/etcd-ca
          cert_file: /etc/prometheus/secrets/etcd-client-cert/etcd-client
          key_file:  /etc/prometheus/secrets/etcd-client-cert/etcd-client-key
        relabel_configs:
        - action: labelmap
          regex: __meta_kubernetes_node_label_(.+)
        - source_labels: [__address__]
          action: replace
          target_label: __address__
          regex: ([^:;]+):(\d+)
          replacement: ${1}:2379
        - source_labels: [__meta_kubernetes_node_name]
          action: keep
          regex: .*mst.*
        - source_labels: [__meta_kubernetes_node_name]
          action: replace
          target_label: node
          regex: (.*)
          replacement: ${1}
        metric_relabel_configs:
        - regex: (kubernetes_io_hostname|failure_domain_beta_kubernetes_io_region|beta_kubernetes_io_os|beta_kubernetes_io_arch|beta_kubernetes_io_instance_type|failure_domain_beta_kubernetes_io_zone)
          action: labeldrop
          
...

인증서 복사하기

etcd 인증서를, 프로메테스를 설치할 monitoring 네임스페이스에, etcd-client-cert란 이름의 secret로 복사한다.

POD_NAME=$(kubectl get pods -o=jsonpath='{.items[0].metadata.name}' -l component=kube-apiserver -n kube-system)

kubectl create secret generic etcd-client-cert -n monitoring \
  --from-literal=etcd-ca="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/ca.crt)" \
  --from-literal=etcd-client="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/healthcheck-client.crt)" \
  --from-literal=etcd-client-key="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/healthcheck-client.key)"

helm으로 prometheus-operator 설치하기

helm 설치 명령어로, proemtheus-operator를 설치한다. 수정한 설정값을 적용하기 위해서 --values values.yaml 옵션을 사용한다.

helm install stable/prometheus-operator --name mon --namespace monitoring --values values.yaml --tls

참고 사항

helm으로 prometheus-operator를 삭제할때, crd는 자동으록 삭제되지 않는다. 아래 명령어로 직접 삭제해야한다.

kubectl delete --ignore-not-found customresourcedefinitions \
  prometheuses.monitoring.coreos.com \
  servicemonitors.monitoring.coreos.com \
  podmonitors.monitoring.coreos.com \
  alertmanagers.monitoring.coreos.com \
  prometheusrules.monitoring.coreos.com

Prometheus Operator로 etcd 모니터링 하기

 1 분 소요

etcd 모니터링 하기

etcd는 /metrics 라는, 프로메테우스가 수집할 수 있는 매트릭 엔드 포인트를 제공한다. 하지만, Secure Etcd 클러스터인 경우에는 해당 엔드 포인트에 접근하기 위해서는 인증서가 필요하다.

(다른 방법으로는 /metrics 엔드 포인트를 다른 포트로 분리하여, 인증서 없이 접근할 수도 있다. --listen-metrics-urls 옵션을 참고 바란다.)

환경

helm을 사용해서 prometheus-operator를 설치할 것이다. 그래서 prometheus-operator를 설치할때, etcd를 모니터링하도록 설정 파일을 변경해서 사용한다.

values.yaml 수정하기

kubeEtcd

kubeEtcd.serviceMonitor의 값들을 변경한다. scheme를 https로 변경하고, 인증서 정보를 등록한다.

## Component scraping etcd
##
kubeEtcd:
...
  serviceMonitor:
    scheme: https
    insecureSkipVerify: false
    serverName: localhost
    caFile: /etc/prometheus/secrets/etcd-client-cert/etcd-ca
    certFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client
    keyFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client-key
...

prometheus

프로메테우스를 기동할때 etcd-client-cert란 이름의 secret를 pod에 마운트하기 위해서, prometheus.secrets에 etcd-client-cert를 추가해 준다. 그리고, etcd 스크랩 설정 추가를 위해서, prometheus.additionalScrapeConfigs의 kube-etcd 부분을 활성화 해준다.

## Deploy a Prometheus instance
##
prometheus:
...
    secrets:
      - "etcd-client-cert"

...

    additionalScrapeConfigs:
      - job_name: kube-etcd
        kubernetes_sd_configs:
          - role: node
        scheme: https
        tls_config:
          ca_file:   /etc/prometheus/secrets/etcd-client-cert/etcd-ca
          cert_file: /etc/prometheus/secrets/etcd-client-cert/etcd-client
          key_file:  /etc/prometheus/secrets/etcd-client-cert/etcd-client-key
        relabel_configs:
        - action: labelmap
          regex: __meta_kubernetes_node_label_(.+)
        - source_labels: [__address__]
          action: replace
          target_label: __address__
          regex: ([^:;]+):(\d+)
          replacement: ${1}:2379
        - source_labels: [__meta_kubernetes_node_name]
          action: keep
          regex: .*mst.*
        - source_labels: [__meta_kubernetes_node_name]
          action: replace
          target_label: node
          regex: (.*)
          replacement: ${1}
        metric_relabel_configs:
        - regex: (kubernetes_io_hostname|failure_domain_beta_kubernetes_io_region|beta_kubernetes_io_os|beta_kubernetes_io_arch|beta_kubernetes_io_instance_type|failure_domain_beta_kubernetes_io_zone)
          action: labeldrop
          
...

인증서 복사하기

etcd 인증서를, 프로메테스를 설치할 monitoring 네임스페이스에, etcd-client-cert란 이름의 secret로 복사한다.

POD_NAME=$(kubectl get pods -o=jsonpath='{.items[0].metadata.name}' -l component=kube-apiserver -n kube-system)

kubectl create secret generic etcd-client-cert -n monitoring \
  --from-literal=etcd-ca="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/ca.crt)" \
  --from-literal=etcd-client="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/healthcheck-client.crt)" \
  --from-literal=etcd-client-key="$(kubectl exec $POD_NAME -n kube-system -- cat /etc/kubernetes/pki/etcd/healthcheck-client.key)"

helm으로 prometheus-operator 설치하기

helm 설치 명령어로, proemtheus-operator를 설치한다. 수정한 설정값을 적용하기 위해서 --values values.yaml 옵션을 사용한다.

helm install stable/prometheus-operator --name mon --namespace monitoring --values values.yaml --tls

참고 사항

helm으로 prometheus-operator를 삭제할때, crd는 자동으록 삭제되지 않는다. 아래 명령어로 직접 삭제해야한다.

kubectl delete --ignore-not-found customresourcedefinitions \
  prometheuses.monitoring.coreos.com \
  servicemonitors.monitoring.coreos.com \
  podmonitors.monitoring.coreos.com \
  alertmanagers.monitoring.coreos.com \
  prometheusrules.monitoring.coreos.com

참고 링크

  • https://github.com/helm/charts/tree/master/stable/prometheus-operator
  • https://github.com/kubernetes-monitoring/kubernetes-mixin
  • https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/monitoring.md

Helm v2

Helm

Helm은 쿠버네티스 패키지 관리 툴이다. chart라고 부르는, 이미 만들어 놓은 패키지 명세서를 이용해서 손쉽게 애플리케이션을 배포하고 관리할 수 있다.

사용의 편의성을 제공하기는 하지만, v2까지는 권한 문제로 인해서 약간의 불편한 점이 있다.

멀티 테넌시 환경의 쿠버네티스 클러스터에서 사용할 경우, 각 사용자의 권한별로 리소스 접근을 제어하기가 힘들다. 네임스페이스별로 tiller를 설치하고, 인증서를 관리할 수 있지만, 상당히 불편하다. 이러한 문제의 근본적인 이유는 패키지 설치를 실행하는 사용자의 권한으로 리소스를 설치하는 것이 아니라, tiller가 가진 권한으로 리소스가 설치되기 때문이다. 즉, 나에게 권한이 없어도, tiller에 권한이 있다면, 내 권한 밖의 리소스를 제어할 수 있는것이다.

다행히도 새로 만들어진 v3 부터는 이러한 문제가 해결될 것으로 보인다.

이 글을 쓰는 시점에서는 아직 v3가 정식 릴리즈 되지 않았다. 그래서 어쩔 수 없이 v2를 사용하였고, v2을 멀티 테넌시 환경에서 사용하기 쉽도록 하기 위해서 kubeapps을 사용했다.

Kubeapps

Kubeapps는 쿠버네티스트 클러스터에 애플리케이션을 배포하고 관리할 수 있게 도와주는 웹 기반의 UI 애플리케이션이다. Kubeapps는 ‘helm chart’를 사용할 수 있을 뿐 아니라, 사용자 기반의 권한 제어 기능도 제공한다.

준비물

  • RBAC 기반의 쿠버네티스 클러스터
  • OIDC Provider + 쿠버네티스 연동

Helm 설치하기

Helm 설치

helm을 설치한다.

개발 환경이 mac이라서 brew를 사용해서 간단히 설치하였다. 환경이 다르다면, helm 문서를 참고하길 바란다.

$ brew install kubernetes-helm

Using SSL/TLS Between Helm and Tiller

helm v2를 사용려면, 쿠버네티스 클러스터에 Tiller가 설치되어 있어야한다. 기본값으로 Tiller를 설치할 경우 보안상의 문제가 있기때문에 TLS 인증서를 사용하는 형태로 설치한다.

CA 만들기

openssl 툴을 이용해서, CA를 생성한다.

CA용 개인키를 생성한다.

$ openssl genrsa -out ./ca.key.pem 4096
Generating RSA private key, 4096 bit long modulus
..........................++
.........................................++
e is 65537 (0x010001)

CA용 인증서를 생성한다.

$ openssl req -key ca.key.pem -new -x509 -days 7300 -sha256 -out ca.cert.pem -extensions v3_ca
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Gyeonggi-do
Locality Name (eg, city) []:Seongnam
Organization Name (eg, company) [Internet Widgits Pty Ltd]:tiller
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:tiller
Email Address []:tiller@example.com

이렇게 생성한 CA를 이용해서, Tiller와 Helm client을 인증서를 만들것이다.

Tiller 인증서 만들기

Tiller용 개인키를 생성한다.

$ openssl genrsa -out ./tiller.key.pem 4096
Generating RSA private key, 4096 bit long modulus
..........................................................++
.................................++
e is 65537 (0x010001)

Tiller용 인증서를 생성한다.

$ openssl req -key tiller.key.pem -new -sha256 -out tiller.csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Gyeonggi-do
Locality Name (eg, city) []:Seongnam
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Tiller Server
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:tiller-server
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Tiller용 인증서를 CA의 인증서로 서명한다.

$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in tiller.csr.pem -out tiller.cert.pem -days 365
Signature ok
subject=C = KR, ST = Gyeonggi-do, L = Seongnam, O = Tiller Server, CN = tiller-server
Getting CA Private Key

Helm client 인증서 만들기

Helm client용 개인키를 생성한다.

$ openssl genrsa -out ./helm.key.pem 4096
  Generating RSA private key, 4096 bit long modulus
  ..................................++
  ......................................++
  e is 65537 (0x010001)

Helm client용 인증서를 생성한다.

$ openssl req -key helm.key.pem -new -sha256 -out helm.csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Gyeonggi-do
Locality Name (eg, city) []:Seongnam
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Helm Client
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:helm-client
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Helm client용 인증서를 CA의 인증서로 서명한다.

openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in helm.csr.pem -out helm.cert.pem -days 365
Signature ok
subject=C = KR, ST = Gyeonggi-do, L = Seongnam, O = Helm Client, CN = helm-client
Getting CA Private Key

서비스 어카운트 만들기

tiller가 사용할 serviceaccount를 생성하고, cluster-admin 클러스터롤(ClusterRole)을 바인딩해준다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
EOF

Tiller 설치하기

생성한 인증서와 서비스어카운트를 지정하여, ‘Tiller’를 설치한다.

설치 명령어는 다음과 같다.

$ helm init --service-account tiller --tiller-tls --tiller-tls-cert ./tiller.cert.pem --tiller-tls-key ./tiller.key.pem --tiller-tls-verify --tls-ca-cert ca.cert.pem

설치가 완료되면, helm ls명령어를 실행해 본다. 다음과 같은 에러가 발생하면 정상적으로 설치한 것이다.

$ helm ls
Error: transport is closing

Helm client 설정하기

설치한 ‘Tiller’는 TLS 로 보호받고 있기 때문에, helm 클라이언트로 접근하려면 인증서를 지정해 줘야한다.

가장 간단한 방법은 인증서 정보를 모두 지정해 주는 것이다.

$ helm ls --tls --tls-ca-cert ca.cert.pem --tls-cert helm.cert.pem --tls-key helm.key.pem

매번 인증서를 지정해주는것은 불편하기 때문에, 인증서를 $HELM_HOME에 복사해 놓으면 좀 더 쉽게 사용할 수 있다.

$ export HELM_HOME=/Users/kangwoo/.helm
$ cp ca.cert.pem $HELM_HOME/ca.pem
$ cp helm.cert.pem $HELM_HOME/cert.pem
$ cp helm.key.pem $HELM_HOME/key.pem

인증서를 $HELM_HOME에 복사하였다면, helm을 실행할때 --tls만 붙여주면 된다.

$ helm ls --tls

Kubeapps 설치하기

helm을 사용해서 kubeapps를 설치할 것이다. tiller의 tls와 OIDC 인증을 위해서 values.yaml값을 수정해 준다.

  • App Version: v1.5.0
  • Chart Version: 2.1.2

Ingress 설정하기

ingress를 사용하기 위해서 설정해준다.

ingress.enabled를 true로 변경하고, ingress.hosts.name을 설정한다.

ingress:
  enabled: true
...
  hosts:
    - name: kubeapps.xxx.com
      path: /

tiller Proxy tls 설정하기

tillerProxy.tls.verify을 true로 변경하고, tillerProxy.tls.catillerProxy.tls.certtillerProxy.tls.key 값을 설정한다.

  • tillerProxy.tls.ca=”$(cat ca.cert.pem)”
  • tillerProxy.tls.cert=”$(cat helm.cert.pem)”
  • tillerProxy.tls.key=”$(cat helm.key.pem)”
...
tillerProxy:
  replicaCount: 2
  image:
    registry: docker.io
    repository: bitnami/kubeapps-tiller-proxy
    tag: 1.5.0-r0
  service:
    port: 8080
  host: tiller-deploy.kube-system:44134
  tls: 
    ca: |-
      -----BEGIN CERTIFICATE-----
      MIIF1zCCA7+gAwIBAgIJAPrXoUYpgyDEMA0GCSqGSIb3DQEBCwUAMIGBMQswCQYD
      ...
      -----END CERTIFICATE-----
    cert: |-
      -----BEGIN CERTIFICATE-----
      MIIFYDCCA0gCCQCnyMMmF4lKHzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC
      ...
      -----END CERTIFICATE-----
    key: |-
      -----BEGIN RSA PRIVATE KEY-----
      MIIJKgIBAAKCAgEA3Mb/4vvMqMVouSV2wLOX94R2okP0rcswLBUGR66asD1CLIa/
      ...
      -----END RSA PRIVATE KEY-----
    verify: true
...

OIDC 인증 활성화하기

쿠버네티스 클러스터에서 사용하는 OIDC Provider를 authProxy에 설정해준다. 그래야 kubeapps 웹 UI 화면에 접속할 때, 로그인을 할 수 있고 해당 토큰으로 kubeapps를 사용할 수 있다.

authProxy.enabled을 true로 변경하고, authProxy.discoveryURLauthProxy.clientIDauthProxy.clientSecret의 값을 설정한 후, authProxy.additionalFlags에 --secure-cookie=false--scopes=openid groups email을 추가해 준다.

...
authProxy:
  # Set to true to enable the OIDC proxy
  enabled: true
  # Image used for the proxy
  image:
    registry: docker.io
    repository: bitnami/keycloak-gatekeeper
    tag: 2.3.0-r1
  # Mandatory parametes
  discoveryURL: https://REPLACE_URL
  clientID: REPLACE_CLIENT_ID
  clientSecret: REPLACE_CLIENT_SECRET
  # Additional flags for Keycloak-Gatekeeper
  additionalFlags:
    - --secure-cookie=false
    - --scopes=openid groups email
$ helm install -f values.yaml bitnami/kubeapps \
  --namespace kubeapps --name kubeapps \
  --tls


NAME:   kubeapps
LAST DEPLOYED: Tue Sep 10 19:45:04 2019
NAMESPACE: kubeapps
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME                                DATA  AGE
kubeapps-frontend-config            1     1s
kubeapps-internal-dashboard-config  2     1s

==> v1/Pod(related)
NAME                                                        READY  STATUS             RESTARTS  AGE
kubeapps-86cd959cc8-knbwk                                   0/2    ContainerCreating  0         1s
kubeapps-86cd959cc8-mtgqc                                   0/2    Pending            0         1s
kubeapps-internal-apprepository-controller-77cc98bcc-8s5dv  0/1    ContainerCreating  0         1s
kubeapps-internal-chartsvc-7fc7bc4fc5-4ssdx                 0/1    ContainerCreating  0         1s
kubeapps-internal-chartsvc-7fc7bc4fc5-n4x65                 0/1    ContainerCreating  0         1s
kubeapps-internal-dashboard-5df4c549b9-dckw2                0/1    Pending            0         1s
kubeapps-internal-dashboard-5df4c549b9-qlvjl                0/1    ContainerCreating  0         1s
kubeapps-internal-tiller-proxy-68c5cb8998-fnfbg             0/1    Pending            0         1s
kubeapps-internal-tiller-proxy-68c5cb8998-rgc6x             0/1    ContainerCreating  0         1s
kubeapps-mongodb-85f58746ff-d6p5g                           0/1    ContainerCreating  0         1s

==> v1/Secret
NAME                            TYPE    DATA  AGE
kubeapps-internal-tiller-proxy  Opaque  3     1s

==> v1/Service
NAME                            TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)    AGE
kubeapps                        ClusterIP  172.30.67.115   <none>       80/TCP     1s
kubeapps-internal-chartsvc      ClusterIP  172.31.216.84   <none>       8080/TCP   1s
kubeapps-internal-dashboard     ClusterIP  172.30.205.251  <none>       8080/TCP   1s
kubeapps-internal-tiller-proxy  ClusterIP  172.31.13.240   <none>       8080/TCP   1s
kubeapps-mongodb                ClusterIP  172.30.33.149   <none>       27017/TCP  1s

==> v1/ServiceAccount
NAME                                        SECRETS  AGE
kubeapps-internal-apprepository-controller  1        1s
kubeapps-internal-tiller-proxy              1        1s

==> v1beta1/Deployment
NAME              READY  UP-TO-DATE  AVAILABLE  AGE
kubeapps-mongodb  0/1    1           0          1s

==> v1beta1/Ingress
NAME      HOSTS                                 ADDRESS  PORTS  AGE
kubeapps  kubeapps.xxx.com  80       1s

==> v1beta1/Role
NAME                                        AGE
kubeapps-internal-apprepository-controller  1s
kubeapps-internal-tiller-proxy              1s
kubeapps-repositories-read                  1s
kubeapps-repositories-write                 1s

==> v1beta1/RoleBinding
NAME                                        AGE
kubeapps-internal-apprepository-controller  1s
kubeapps-internal-tiller-proxy              1s

==> v1beta2/Deployment
NAME                                        READY  UP-TO-DATE  AVAILABLE  AGE
kubeapps                                    0/2    2           0          1s
kubeapps-internal-apprepository-controller  0/1    1           0          1s
kubeapps-internal-chartsvc                  0/2    2           0          1s
kubeapps-internal-dashboard                 0/2    2           0          1s
kubeapps-internal-tiller-proxy              0/2    2           0          1s


NOTES:
** Please be patient while the chart is being deployed **

Tip:

  Watch the deployment status using the command: kubectl get pods -w --namespace kubeapps

Kubeapps can be accessed via port 80 on the following DNS name from within your cluster:

   kubeapps.kubeapps.svc.cluster.local

To access Kubeapps from outside your K8s cluster, follow the steps below:

1. Get the Kubeapps URL and associate Kubeapps hostname to your cluster external IP:

   export CLUSTER_IP=$(minikube ip) # On Minikube. Use: `kubectl cluster-info` on others K8s clusters
   echo "Kubeapps URL: http://kubeapps.xxx.com/"
   echo "$CLUSTER_IP  kubeapps.xxx.com" | sudo tee -a /etc/hosts

2. Open a browser and access Kubeapps using the obtained URL.

Helm v2

 7 분 소요

Helm

Helm은 쿠버네티스 패키지 관리 툴이다. chart라고 부르는, 이미 만들어 놓은 패키지 명세서를 이용해서 손쉽게 애플리케이션을 배포하고 관리할 수 있다.

사용의 편의성을 제공하기는 하지만, v2까지는 권한 문제로 인해서 약간의 불편한 점이 있다.

멀티 테넌시 환경의 쿠버네티스 클러스터에서 사용할 경우, 각 사용자의 권한별로 리소스 접근을 제어하기가 힘들다. 네임스페이스별로 tiller를 설치하고, 인증서를 관리할 수 있지만, 상당히 불편하다. 이러한 문제의 근본적인 이유는 패키지 설치를 실행하는 사용자의 권한으로 리소스를 설치하는 것이 아니라, tiller가 가진 권한으로 리소스가 설치되기 때문이다. 즉, 나에게 권한이 없어도, tiller에 권한이 있다면, 내 권한 밖의 리소스를 제어할 수 있는것이다.

다행히도 새로 만들어진 v3 부터는 이러한 문제가 해결될 것으로 보인다.

이 글을 쓰는 시점에서는 아직 v3가 정식 릴리즈 되지 않았다. 그래서 어쩔 수 없이 v2를 사용하였고, v2을 멀티 테넌시 환경에서 사용하기 쉽도록 하기 위해서 kubeapps을 사용했다.

Kubeapps

Kubeapps는 쿠버네티스트 클러스터에 애플리케이션을 배포하고 관리할 수 있게 도와주는 웹 기반의 UI 애플리케이션이다. Kubeapps는 ‘helm chart’를 사용할 수 있을 뿐 아니라, 사용자 기반의 권한 제어 기능도 제공한다.

Applications
Catalog

준비물

  • RBAC 기반의 쿠버네티스 클러스터
  • OIDC Provider + 쿠버네티스 연동

Helm 설치하기

Helm 설치

‘helm’을 설치한다.

개발 환경이 mac이라서 brew를 사용해서 간단히 설치하였다. 환경이 다르다면, helm 문서를 참고하길 바란다.

$ brew install kubernetes-helm

Using SSL/TLS Between Helm and Tiller

‘helm’ v2를 사용려면, 쿠버네티스 클러스터에 Tiller가 설치되어 있어야한다. 기본값으로 Tiller를 설치할 경우 보안상의 문제가 있기때문에 TLS 인증서를 사용하는 형태로 설치한다.

CA 만들기

openssl 툴을 이용해서, CA를 생성한다.

  • CA용 개인키를 생성한다.$ openssl genrsa -out ./ca.key.pem 4096 Generating RSA private key, 4096 bit long modulus ..........................++ .........................................++ e is 65537 (0x010001)
  • CA용 인증서를 생성한다.$ openssl req -key ca.key.pem -new -x509 -days 7300 -sha256 -out ca.cert.pem -extensions v3_ca You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:KR State or Province Name (full name) [Some-State]:Gyeonggi-do Locality Name (eg, city) []:Seongnam Organization Name (eg, company) [Internet Widgits Pty Ltd]:tiller Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:tiller Email Address []:tiller@example.com 이렇게 생성한 CA를 이용해서, Tiller와 Helm client을 인증서를 만들것이다.

Tiller 인증서 만들기

  • Tiller용 개인키를 생성한다.
$ openssl genrsa -out ./tiller.key.pem 4096
Generating RSA private key, 4096 bit long modulus
..........................................................++
.................................++
e is 65537 (0x010001)
  • Tiller용 인증서를 생성한다.
$ openssl req -key tiller.key.pem -new -sha256 -out tiller.csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Gyeonggi-do
Locality Name (eg, city) []:Seongnam
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Tiller Server
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:tiller-server
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
  • Tiller용 인증서를 CA의 인증서로 서명한다.
$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in tiller.csr.pem -out tiller.cert.pem -days 365
Signature ok
subject=C = KR, ST = Gyeonggi-do, L = Seongnam, O = Tiller Server, CN = tiller-server
Getting CA Private Key

Helm client 인증서 만들기

  • Helm client용 개인키를 생성한다.
$ openssl genrsa -out ./helm.key.pem 4096
  Generating RSA private key, 4096 bit long modulus
  ..................................++
  ......................................++
  e is 65537 (0x010001)
  • Helm client용 인증서를 생성한다.
openssl req -key helm.key.pem -new -sha256 -out helm.csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Gyeonggi-do
Locality Name (eg, city) []:Seongnam
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Helm Client
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:helm-client
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
  • Helm client용 인증서를 CA의 인증서로 서명한다.
$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in helm.csr.pem -out helm.cert.pem -days 365
Signature ok
subject=C = KR, ST = Gyeonggi-do, L = Seongnam, O = Helm Client, CN = helm-client
Getting CA Private Key

서비스 어카운트 만들기

‘Tiller’가 사용할 serviceaccount를 생성하고, cluster-admin 클러스터롤(ClusterRole)을 바인딩해준다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
EOF

Tiller 설치하기

생성한 인증서와 서비스어카운트를 지정하여, ‘Tiller’를 설치한다.

설치 명령어는 다음과 같다.

$ helm init --service-account tiller --tiller-tls --tiller-tls-cert ./tiller.cert.pem --tiller-tls-key ./tiller.key.pem --tiller-tls-verify --tls-ca-cert ca.cert.pem

설치가 완료되면, helm ls명령어를 실행해 본다. 다음과 같은 에러가 발생하면 정상적으로 설치한 것이다.

$ helm ls
Error: transport is closing

Helm client 설정하기

설치한 ‘Tiller’는 TLS 로 보호받고 있기 때문에, helm 클라이언트로 접근하려면 인증서를 지정해 줘야한다.

가장 간단한 방법은 인증서 정보를 모두 지정해 주는 것이다.

$ helm ls --tls --tls-ca-cert ca.cert.pem --tls-cert helm.cert.pem --tls-key helm.key.pem

매번 인증서를 지정해주는것은 불편하기 때문에, 인증서를 $HELM_HOME에 복사해 놓으면 좀 더 쉽게 사용할 수 있다.

$ export HELM_HOME=/Users/kangwoo/.helm
$ cp ca.cert.pem $HELM_HOME/ca.pem
$ cp helm.cert.pem $HELM_HOME/cert.pem
$ cp helm.key.pem $HELM_HOME/key.pem

인증서를 $HELM_HOME에 복사하였다면, helm을 실행할때 --tls만 붙여주면 된다.

$ helm ls --tls

Kubeapps 설치하기

helm을 사용해서 kubeapps를 설치할 것이다. tiller의 tls와 OIDC 인증을 위해서 values.yaml값을 수정해 준다.

  • App Version: v1.5.0
  • Chart Version: 2.1.2

ingress 설절하기

ingress를 사용하기 위해서 설정해준다.

ingress.enabled를 true로 변경하고, ingress.hosts.name을 설정한다.

ingress:
  enabled: true
...
  hosts:
    - name: kubeapps.xxx.com
      path: /

tillerProxy tls 설정하기

tillerProxy.tls.verify을 true로 변경하고, tillerProxy.tls.catillerProxy.tls.certtillerProxy.tls.key 값을 설정한다.

  • tillerProxy.tls.ca=”$(cat ca.cert.pem)”
  • tillerProxy.tls.cert=”$(cat helm.cert.pem)”
  • tillerProxy.tls.key=”$(cat helm.key.pem)”
...
tillerProxy:
  replicaCount: 2
  image:
    registry: docker.io
    repository: bitnami/kubeapps-tiller-proxy
    tag: 1.5.0-r0
  service:
    port: 8080
  host: tiller-deploy.kube-system:44134
  tls: 
    ca: |-
      -----BEGIN CERTIFICATE-----
      MIIF1zCCA7+gAwIBAgIJAPrXoUYpgyDEMA0GCSqGSIb3DQEBCwUAMIGBMQswCQYD
      ...
      -----END CERTIFICATE-----
    cert: |-
      -----BEGIN CERTIFICATE-----
      MIIFYDCCA0gCCQCnyMMmF4lKHzANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC
      ...
      -----END CERTIFICATE-----
    key: |-
      -----BEGIN RSA PRIVATE KEY-----
      MIIJKgIBAAKCAgEA3Mb/4vvMqMVouSV2wLOX94R2okP0rcswLBUGR66asD1CLIa/
      ...
      -----END RSA PRIVATE KEY-----
    verify: true
...

OIDC 인증 활성화하기

쿠버네티스 클러스터에서 사용하는 OIDC Provider를 authProxy에 설정해준다. 그래야 kubeapps 웹 UI 화면에 접속할 때, 로그인을 할 수 있고 해당 토큰으로 kubeapps를 사용할 수 있다.

authProxy.enabled을 true로 변경하고, authProxy.discoveryURLauthProxy.clientIDauthProxy.clientSecret의 값을 설정한 후, authProxy.additionalFlags에 --secure-cookie=false--scopes=openid groups email을 추가해 준다.

...
authProxy:
  # Set to true to enable the OIDC proxy
  enabled: true
  # Image used for the proxy
  image:
    registry: docker.io
    repository: bitnami/keycloak-gatekeeper
    tag: 2.3.0-r1
  # Mandatory parametes
  discoveryURL: https://REPLACE_URL
  clientID: REPLACE_CLIENT_ID
  clientSecret: REPLACE_CLIENT_SECRET
  # Additional flags for Keycloak-Gatekeeper
  additionalFlags:
    - --secure-cookie=false
    - --scopes=openid groups email
$ helm install -f values.yaml bitnami/kubeapps \
  --namespace kubeapps --name kubeapps \
  --tls


NAME:   kubeapps
LAST DEPLOYED: Tue Sep 10 19:45:04 2019
NAMESPACE: kubeapps
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME                                DATA  AGE
kubeapps-frontend-config            1     1s
kubeapps-internal-dashboard-config  2     1s

==> v1/Pod(related)
NAME                                                        READY  STATUS             RESTARTS  AGE
kubeapps-86cd959cc8-knbwk                                   0/2    ContainerCreating  0         1s
kubeapps-86cd959cc8-mtgqc                                   0/2    Pending            0         1s
kubeapps-internal-apprepository-controller-77cc98bcc-8s5dv  0/1    ContainerCreating  0         1s
kubeapps-internal-chartsvc-7fc7bc4fc5-4ssdx                 0/1    ContainerCreating  0         1s
kubeapps-internal-chartsvc-7fc7bc4fc5-n4x65                 0/1    ContainerCreating  0         1s
kubeapps-internal-dashboard-5df4c549b9-dckw2                0/1    Pending            0         1s
kubeapps-internal-dashboard-5df4c549b9-qlvjl                0/1    ContainerCreating  0         1s
kubeapps-internal-tiller-proxy-68c5cb8998-fnfbg             0/1    Pending            0         1s
kubeapps-internal-tiller-proxy-68c5cb8998-rgc6x             0/1    ContainerCreating  0         1s
kubeapps-mongodb-85f58746ff-d6p5g                           0/1    ContainerCreating  0         1s

==> v1/Secret
NAME                            TYPE    DATA  AGE
kubeapps-internal-tiller-proxy  Opaque  3     1s

==> v1/Service
NAME                            TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)    AGE
kubeapps                        ClusterIP  172.30.67.115   <none>       80/TCP     1s
kubeapps-internal-chartsvc      ClusterIP  172.31.216.84   <none>       8080/TCP   1s
kubeapps-internal-dashboard     ClusterIP  172.30.205.251  <none>       8080/TCP   1s
kubeapps-internal-tiller-proxy  ClusterIP  172.31.13.240   <none>       8080/TCP   1s
kubeapps-mongodb                ClusterIP  172.30.33.149   <none>       27017/TCP  1s

==> v1/ServiceAccount
NAME                                        SECRETS  AGE
kubeapps-internal-apprepository-controller  1        1s
kubeapps-internal-tiller-proxy              1        1s

==> v1beta1/Deployment
NAME              READY  UP-TO-DATE  AVAILABLE  AGE
kubeapps-mongodb  0/1    1           0          1s

==> v1beta1/Ingress
NAME      HOSTS                                 ADDRESS  PORTS  AGE
kubeapps  kubeapps.xxx.com  80       1s

==> v1beta1/Role
NAME                                        AGE
kubeapps-internal-apprepository-controller  1s
kubeapps-internal-tiller-proxy              1s
kubeapps-repositories-read                  1s
kubeapps-repositories-write                 1s

==> v1beta1/RoleBinding
NAME                                        AGE
kubeapps-internal-apprepository-controller  1s
kubeapps-internal-tiller-proxy              1s

==> v1beta2/Deployment
NAME                                        READY  UP-TO-DATE  AVAILABLE  AGE
kubeapps                                    0/2    2           0          1s
kubeapps-internal-apprepository-controller  0/1    1           0          1s
kubeapps-internal-chartsvc                  0/2    2           0          1s
kubeapps-internal-dashboard                 0/2    2           0          1s
kubeapps-internal-tiller-proxy              0/2    2           0          1s


NOTES:
** Please be patient while the chart is being deployed **

Tip:

  Watch the deployment status using the command: kubectl get pods -w --namespace kubeapps

Kubeapps can be accessed via port 80 on the following DNS name from within your cluster:

   kubeapps.kubeapps.svc.cluster.local

To access Kubeapps from outside your K8s cluster, follow the steps below:

1. Get the Kubeapps URL and associate Kubeapps hostname to your cluster external IP:

   export CLUSTER_IP=$(minikube ip) # On Minikube. Use: `kubectl cluster-info` on others K8s clusters
   echo "Kubeapps URL: http://kubeapps.xxx.com/"
   echo "$CLUSTER_IP  kubeapps.xxx.com" | sudo tee -a /etc/hosts

2. Open a browser and access Kubeapps using the obtained URL.

ingress에 설정한 주소로 접속하면 kubeapps를 사용할 수 있다.

참고 문서

쿠버네티스 네임스페이스가 삭제되지 않을 때 강제 삭제하기

문제

가끔식 문제가 발생하여, 네임스페이스(namespace)를 삭제할때, 상태만 Terminating으로 변하고, 계속 기다려도 삭제가 되지 않는 경우가 있다.

이럴 경우에는 네임스페이스의 finalizers를 제거해 주면 된다. (하지만 정상작으로 삭제될때까지 기다리는게 가장 좋다)

해결 방법

foo라는 네임스페이스가 있다고 가정한다.

다음과 같은 명령어로 네임스페이스 정의 내역을 json 파일로 저장한다.

$ kubectl get namespace foo -o json > foo.json

foo.json 파일을 영어서 finalizers 부분에 있는 kubernetes 값을 삭제하고, 저장한다.

그런 다음 쿠베 프락시를 실행한다. 쿠버네티스 api를 호출할 예정인데, 인증 토큰이 필요하다. kubectl proxy를 이용하면, 저장되어 있는 인증토큰을 자동으로 이용한다.

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

다른 터미널을 열어서 쿠버네티스 api를 호출한다. 다음과 같이 api를 호출하면 변경된 finalizers 부분이 쿠버네티스에 반영된다.

curl -k -H "Content-Type: application/json" -X PUT --data-binary @foo.json http://127.0.0.1:8001/api/v1/namespaces/foo/finalize

Prometheus를 사용해서 NVIDIA GPU 모니터링 하기

Node의 GPU 모니터링 하기

prometheus를 사용해서 노드들의 매트틱을 수집하고 있다면, 아마 node-exporter를 사용하고 있을 것이다. NVIDIA에서는 dcgm-exporter라는 GPU 매트릭 출력용 이미지를 제공하고 있다. 이 dcgm-exporter과 node-exporter를 결합하여 사용하면, GPU 매트릭을 수집할 수 있다.

dcgm-exporter

dcgm(Data Center GPU Manager) exporter는 nv-hostenging을 시작해서, 매초마다 GPU 매트릭을 읽어서 prometheus 형식으로 출력해주는 간단한 쉘 스크립트이다.

Node 설정하기

우선 일반 노드와 GPU 노드를 분리하기 위해서 taint와 label을 설정해주었다. 대부분 node-exporter를 실행하기 위해서 DaemonSet을 사용했을 것이다.

일반 노드에서는 node-exporter만을 실행하기 위해서 taint nvidia.com/gpu=:NoSchedule를 사용하였고, GPU 노드에서는 node-exporter + dcgm-exporter를 실행하기 위해서 label hardware-type=NVIDIAGPU를 사용하였다.

nvidia.com/brand는 현재로는 별의미가 없지만 붙여주었다.

kubectl taint nodes ${node} nvidia.com/gpu=:NoSchedule

kubectl label nodes ${node} "nvidia.com/brand=${label}"
kubectl label nodes ${node} hardware-type=NVIDIAGPU

기존 node-exporter에 dcgm-exporter 추가하기

dcgm-exporter가 GPU 매트릭을 파일로 남기고, prometheus는 그 파일을 읽어서 GPU 매트릭을 같이 출력한다.

GPU 노드용

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app.kubernetes.io/name: node-exporter
    app.kubernetes.io/instance: gpu-node-exporter
    app.kubernetes.io/part-of: prometheus
    app.kubernetes.io/managed-by: argo-system
  name: prometheus-gpu-node-exporter
  namespace: argo-system
spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app.kubernetes.io/name: node-exporter
      app.kubernetes.io/instance: gpu-node-exporter
      app.kubernetes.io/part-of: prometheus
      app.kubernetes.io/managed-by: argo-system
  template:
    metadata:
      labels:
        app.kubernetes.io/name: node-exporter
        app.kubernetes.io/instance: gpu-node-exporter
        app.kubernetes.io/part-of: prometheus
        app.kubernetes.io/managed-by: argo-system
    spec:
      nodeSelector:
        hardware-type: NVIDIAGPU
      containers:
      - args:
        - --path.procfs=/host/proc
        - --path.sysfs=/host/sys
        - "--collector.textfile.directory=/run/prometheus"
        image: prom/node-exporter:v0.18.1
        imagePullPolicy: IfNotPresent
        name: prometheus-node-exporter
        ports:
        - containerPort: 9100
          hostPort: 9100
          name: metrics
          protocol: TCP
        resources:
          limits:
            cpu: 500m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /host/proc
          name: proc
          readOnly: true
        - mountPath: /host/sys
          name: sys
          readOnly: true
        - name: collector-textfiles
          readOnly: true
          mountPath: /run/prometheus
      - image: nvidia/dcgm-exporter:1.4.6
        name: nvidia-dcgm-exporter
        securityContext:
          runAsNonRoot: false
          runAsUser: 0
        volumeMounts:
          - name: collector-textfiles
            mountPath: /run/prometheus
      dnsPolicy: ClusterFirst
      hostNetwork: true
      hostPID: true
      restartPolicy: Always
      serviceAccount: prometheus-node-exporter
      serviceAccountName: prometheus-node-exporter
      terminationGracePeriodSeconds: 30
      tolerations:
      - effect: NoSchedule
        key: node-role.kubernetes.io/master
      - effect: NoSchedule
        key: node-role.kubernetes.io/ingress
        operator: Exists
      - effect: NoSchedule
        key: nvidia.com/gpu
        operator: Exists
      volumes:
      - hostPath:
          path: /proc
          type: ""
        name: proc
      - hostPath:
          path: /sys
          type: ""
        name: sys
      - name: collector-textfiles
        emptyDir:
          medium: Memory
      - name: pod-gpu-resources
        hostPath:
          path: /var/lib/kubelet/pod-resources
  updateStrategy:
    type: OnDelete

일반 노드용

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app.kubernetes.io/name: node-exporter
    app.kubernetes.io/instance: node-exporter
    app.kubernetes.io/part-of: prometheus
    app.kubernetes.io/managed-by: argo-system
  name: prometheus-node-exporter
  namespace: argo-system
spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app.kubernetes.io/name: node-exporter
      app.kubernetes.io/instance: node-exporter
      app.kubernetes.io/part-of: prometheus
      app.kubernetes.io/managed-by: argo-system
  template:
    metadata:
      labels:
        app.kubernetes.io/name: node-exporter
        app.kubernetes.io/instance: node-exporter
        app.kubernetes.io/part-of: prometheus
        app.kubernetes.io/managed-by: argo-system
    spec:
      containers:
      - args:
        - --path.procfs=/host/proc
        - --path.sysfs=/host/sys
        image: prom/node-exporter:v0.18.1
        imagePullPolicy: IfNotPresent
        name: prometheus-node-exporter
        ports:
        - containerPort: 9100
          hostPort: 9100
          name: metrics
          protocol: TCP
        resources:
          limits:
            cpu: 500m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /host/proc
          name: proc
          readOnly: true
        - mountPath: /host/sys
          name: sys
          readOnly: true
      dnsPolicy: ClusterFirst
      hostNetwork: true
      hostPID: true
      restartPolicy: Always
      serviceAccount: prometheus-node-exporter
      serviceAccountName: prometheus-node-exporter
      terminationGracePeriodSeconds: 30
      tolerations:
      - effect: NoSchedule
        key: node-role.kubernetes.io/master
      - effect: NoSchedule
        key: node-role.kubernetes.io/ingress
        operator: Exists
      volumes:
      - hostPath:
          path: /proc
          type: ""
        name: proc
      - hostPath:
          path: /sys
          type: ""
        name: sys
  updateStrategy:
    type: OnDelete

kubebuilder

Install

Install kubebuilder

os=$(go env GOOS)
arch=$(go env GOARCH)

# download kubebuilder and extract it to tmp
curl -sL https://go.kubebuilder.io/dl/2.0.0-beta.0/${os}/${arch} | tar -xz -C /tmp/

# move to a long-term location and put it on your path
# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else)
sudo mv /tmp/kubebuilder_2.0.0-beta.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

Create a Project

mkdir namespace-operator
cd namespace-operator

go mod init kangwoo.github.io/namespace-operator

kubebuilder init --domain kangwoo.github.io

Adding a new API

kubebuilder create api --group tenant --version v1 --kind NamespaceRequest --namespaced false

Adding a new Webhook

kubebuilder create webhook --group tenant --version v1 --kind NamespaceRequest --defaulting --programmatic-validation

Kubernets ServiceAccount로 kuebconfig 파일 생성하기

서비스계정(ServiceAccount) 생성

kubectl create serviceaccount super-man

ClusterRole 또는 Role Binding

kubectlcreate clusterrolebinding cluster-admin:super-man --clusterrole=cluster-admin --serviceaccount=default:super-man

Kubernets ServiceAccount로 kuebconfig 파일 생성하기

# your server name goes here
server=https://localhost:6443
# the name of the service account
name=SERVICE_ACCOUNT_NAME
# the name of the namespace
namespace=default

token_name=$(kubectl -n $namespace get serviceaccount $name -o jsonpath='{.secrets[].name}')
ca=$(kubectl -n $namespace get secret/$token_name -o jsonpath='{.data.ca\.crt}')
token=$(kubectl -n $namespace get secret/$token_name -o jsonpath='{.data.token}' | base64 --decode)
namespace=$(kubectl -n $namespace get secret/$token_name -o jsonpath='{.data.namespace}' | base64 --decode)

echo "
apiVersion: v1
kind: Config
clusters:
- name: default-cluster
  cluster:
    certificate-authority-data: ${ca}
    server: ${server}
contexts:
- name: default-context
  context:
    cluster: default-cluster
    namespace: ${namespace}
    user: ${name}
current-context: default-context
users:
- name: ${name}
  user:
    token: ${token}
" > kubeconfig

Istio client go

시작하기전에..

애플리케이션에서 Istio CR(Custom Resources)을 생성해야 하는데, 공식적으로 제공하는 라이브러리가 없다.

구글에 검색해 본 결과 istio-client-go가 존재한다. 하지만, 필요한 리소그 몇개가 빠져 있어어서 재미삼아 만들어봤다.

주소 : https://github.com/kangwoo/istio-client-go

준비물

처음에는 kubebuilder를 사용하려 했으나, 현재 버전(2.0.0-beta.0)에서는 복수개의 그룹을 지원하지 않는다. (Multiple groups are not supported yet) 그래서 operator-sdk를 사용한다.

  • golang
  • operator-sdk

프로젝트 생성

operator-sdk의 new 명령어를 사용해서 프로젝트 생성한다.

$ operator-sdk new istio-client-go --repo github.com/kangwoo/istio-client-go
$ cd istio-client-go

istio 추가

$ go get istio.io/api

필요한 리소스 추가

$ operator-sdk add api --api-version=authentication.istio.io/v1alpha1 --kind=Policy
$ operator-sdk add api --api-version=networking.istio.io/v1alpha3 --kind=Gateway
$ operator-sdk add api --api-version=rbac.istio.io/v1alpha1 --kind=ServiceRole
$ operator-sdk add api --api-version=rbac.istio.io/v1alpha1 --kind=ServiceRoleBinding

불필요한 파일 삭제 및 코드 수정

  • PolicySpec 타입 위의 // +k8s:openapi-gen=true 제거
  • PolicyStatus 타입 제거 및 Policy 타입에 Status 제거
  • json 변환 및 deepcopy 구현
import (
      "bufio"
      "bytes"
      "log"
    
      "github.com/gogo/protobuf/jsonpb"
      metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
      istiov1alpha1 "istio.io/api/authentication/v1alpha1"
  )
    
  ...
    
        
  func (p *PolicySpec) MarshalJSON() ([]byte, error) {
      buffer := bytes.Buffer{}
      writer := bufio.NewWriter(&buffer)
      marshaler := jsonpb.Marshaler{}
      err := marshaler.Marshal(writer, &p.Policy)
      if err != nil {
          log.Printf("Could not marshal PolicySpec. Error: %v", err)
          return nil, err
      }
    
      writer.Flush()
      return buffer.Bytes(), nil
  }
    
  func (p *PolicySpec) UnmarshalJSON(b []byte) error {
      reader := bytes.NewReader(b)
      unmarshaler := jsonpb.Unmarshaler{}
      err := unmarshaler.Unmarshal(reader, &p.Policy)
      if err != nil {
          log.Printf("Could not unmarshal PolicySpec. Error: %v", err)
          return err
      }
      return nil
  }
    
  // DeepCopyInto is a deepcopy function, copying the receiver, writing into out. in must be non-nil.
  // Based of https://github.com/istio/istio/blob/release-0.8/pilot/pkg/config/kube/crd/types.go#L450
  func (in *PolicySpec) DeepCopyInto(out *PolicySpec) {
      *out = *in
  }

참고 코드

원본 파일

package v1alpha1
    
  import (
      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 be serialized.
    
  // PolicySpec defines the desired state of Policy
  // +k8s:openapi-gen=true
  type PolicySpec struct {
      // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
      // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
      // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
  }
    
  // PolicyStatus defines the observed state of Policy
  // +k8s:openapi-gen=true
  type PolicyStatus struct {
      // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
      // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
      // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
  }
    
  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    
  // Policy is the Schema for the policies API
  // +k8s:openapi-gen=true
  // +kubebuilder:subresource:status
  type Policy struct {
      metav1.TypeMeta   `json:",inline"`
      metav1.ObjectMeta `json:"metadata,omitempty"`
    
      Spec   PolicySpec   `json:"spec,omitempty"`
      Status PolicyStatus `json:"status,omitempty"`
  }
    
  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    
  // PolicyList contains a list of Policy
  type PolicyList struct {
      metav1.TypeMeta `json:",inline"`
      metav1.ListMeta `json:"metadata,omitempty"`
      Items           []Policy `json:"items"`
  }
    
  func init() {
      SchemeBuilder.Register(&Policy{}, &PolicyList{})
  }

수정 후 파일

package v1alpha1
    
  import (
      "bufio"
      "bytes"
      "log"
    
      "github.com/gogo/protobuf/jsonpb"
      metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
      istiov1alpha1 "istio.io/api/authentication/v1alpha1"
  )
    
  // 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.
    
  // PolicySpec defines the desired state of Policy
  type PolicySpec struct {
      // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
      // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
      // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
      istiov1alpha1.Policy
  }
    
    
  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    
  // Policy is the Schema for the policies API
  // +k8s:openapi-gen=true
  // +kubebuilder:subresource:status
  type Policy struct {
      metav1.TypeMeta   `json:",inline"`
      metav1.ObjectMeta `json:"metadata,omitempty"`
    
      Spec   PolicySpec   `json:"spec,omitempty"`
  }
    
  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    
  // PolicyList contains a list of Policy
  type PolicyList struct {
      metav1.TypeMeta `json:",inline"`
      metav1.ListMeta `json:"metadata,omitempty"`
      Items           []Policy `json:"items"`
  }
    
  func init() {
      SchemeBuilder.Register(&Policy{}, &PolicyList{})
  }
    
  func (p *PolicySpec) MarshalJSON() ([]byte, error) {
      buffer := bytes.Buffer{}
      writer := bufio.NewWriter(&buffer)
      marshaler := jsonpb.Marshaler{}
      err := marshaler.Marshal(writer, &p.Policy)
      if err != nil {
          log.Printf("Could not marshal PolicySpec. Error: %v", err)
          return nil, err
      }
    
      writer.Flush()
      return buffer.Bytes(), nil
  }
    
  func (p *PolicySpec) UnmarshalJSON(b []byte) error {
      reader := bytes.NewReader(b)
      unmarshaler := jsonpb.Unmarshaler{}
      err := unmarshaler.Unmarshal(reader, &p.Policy)
      if err != nil {
          log.Printf("Could not unmarshal PolicySpec. Error: %v", err)
          return err
      }
      return nil
  }
    
  // DeepCopyInto is a deepcopy function, copying the receiver, writing into out. in must be non-nil.
  // Based of https://github.com/istio/istio/blob/release-0.8/pilot/pkg/config/kube/crd/types.go#L450
  func (in *PolicySpec) DeepCopyInto(out *PolicySpec) {
      *out = *in
  }

코드 생성

$ operator-sdk generate k8s

Kubernetes operator-sdk를 이용한 Go Operator 만들기

Install the Operator SDK CLI

Install from Homebrew

$ brew install operator-sdk

Create an operator

작업할 디렉토리를 생성하고, operator-sdk를 사용해서 operator를 생성한다. go 모듈을 사용하기 위해서 GO111MODULE=on을 설정하거나, GOPATH가 아닌 경로에 디렉토리를 생성해야한다.

$ mkdir -p ~/workspace
$ cd ~/workspace
$ export GO111MODULE=on
$ operator-sdk new jupyter-operator --repo github.com/kangwoo/jupyter-operator
INFO[0000] Creating new Go operator 'jupyter-operator'.
INFO[0000] Created go.mod
INFO[0000] Created tools.go
INFO[0000] Created cmd/manager/main.go
INFO[0000] Created build/Dockerfile
INFO[0000] Created build/bin/entrypoint
INFO[0000] Created build/bin/user_setup
INFO[0000] Created deploy/service_account.yaml
INFO[0000] Created deploy/role.yaml
INFO[0000] Created deploy/role_binding.yaml
INFO[0000] Created deploy/operator.yaml
INFO[0000] Created pkg/apis/apis.go
INFO[0000] Created pkg/controller/controller.go
INFO[0000] Created version/version.go
INFO[0000] Created .gitignore
INFO[0000] Validating project
go: finding github.com/operator-framework/operator-sdk master
INFO[0108] Project validation successful.
INFO[0108] Project creation complete.

Mercurial 설치

만일 operator를 생성하는 중 다음과 같이 hg 실행 파일을 찾을 수 없다는 에러가 발생한다면, hg를 별도로 설치해야한다.

$ operator-sdk new jupyter-operator --repo github.com/kangwoo/jupyter-operator
...
go: finding github.com/operator-framework/operator-sdk master
go: bitbucket.org/ww/goautoneg@v0.0.0-20120707110453-75cd24fc2f2c: hg clone -U https://bitbucket.org/ww/goautoneg . in /Users/lineplus/go/pkg/mod/cache/vcs/59c2185b80ea440a7c3b8c5eff3d8abb68c53dea1f20f615370c924c4150b27f: exec: "hg": executable file not found in $PATH
go: error loading module requirements
Error: failed to exec []string{"go", "build", "./..."}: exit status 1
...

hg도 ‘brew’를 사용해서 설치할 수 있다. 참고로 ‘hg’는 [Mercurial]https://www.mercurial-scm.org/ 이라는 크로스 플랫폼 분산 버전 관리 도구의 명령툴이다.

$ brew install hg

생성한 operator 디렉토리 이동

생성한 operator 디렉토리로 이동한다.

$ cd jupyter-operator

CR(Custom Resource) 생성

operator-sdk add api 명령어를 이용해서, API를 생성한다.

$ operator-sdk add api --api-version=kangwoo.github.io/v1alpha1 --kind=Jupyter
INFO[0000] Generating api version kangwoo.github.io/v1alpha1 for kind Jupyter.
INFO[0000] Created pkg/apis/kangwoo/group.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/jupyter_types.go
INFO[0003] Created pkg/apis/addtoscheme_kangwoo_v1alpha1.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/register.go
INFO[0003] Created pkg/apis/kangwoo/v1alpha1/doc.go
INFO[0003] Created deploy/crds/kangwoo_v1alpha1_jupyter_cr.yaml
INFO[0011] Created deploy/crds/kangwoo_v1alpha1_jupyter_crd.yaml
INFO[0011] Running deepcopy code-generation for Custom Resource group versions: [kangwoo:[v1alpha1], ]
INFO[0019] Code-generation complete.
INFO[0019] Running OpenAPI code-generation for Custom Resource group versions: [kangwoo:[v1alpha1], ]
INFO[0036] Created deploy/crds/kangwoo_v1alpha1_jupyter_crd.yaml
INFO[0036] Code-generation complete.
INFO[0036] API generation complete.

Controller 생성

operator-sdk add controller 명령어를 이용해서, Controller를 생성한다.

$ operator-sdk add controller --api-version=kangwoo.github.io/v1alpha1 --kind=Jupyter
INFO[0000] Generating controller version kangwoo.github.io/v1alpha1 for kind Jupyter.
INFO[0000] Created pkg/controller/jupyter/jupyter_controller.go
INFO[0000] Created pkg/controller/add_jupyter.go
INFO[0000] Controller generation complete.

빌드(Build) 하기

$ operator-sdk build kangwoo/jupyter-operator:latest

코드 생성 하기

리소스의 Spec이 변경되었을 경우, 코드를 다시 생성해줘야한다.

$ operator-sdk generate k8s
$ operator-sdk generate openapi

참고 자료

  • https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md
  • https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md
  • https://github.com/operator-framework/operator-sdk/blob/master/doc/operator-scope.md

쿠버네티스 메트릭 서버 인증 실패

쿠버네티스 v1.11.x에서 metrics-server를 설치하였으나, 한개의 마스터 서버에서만 정상적으로 작동하는 문제가 발생하였습니다. (3개의 마스터 서버로 HA 구성 상태)

kubectl top node 명령어를 사용하면, 약 1/3 확률로 정상 응답을 하고, 나머지는 아래와 같이 권한이 없다는 메시지가 나옵니다.

F0531 10:41:33.286003 52081 helpers.go:119] error: You must be logged in to the server (Unauthorized)

그래서 kube-apiserver를 로드 밸런서 없이, 마스터 서버 아이피로 직접 연결해서 테스트해보았는데, 단 1개의 마스터 서버만 정상 응답하고, 나머지는 권한이 없다는 메시지가 나왔습니다.

metrics-server는 어그리게이션 레이어를 사용하는데, kube-apiserver와 metrics-server 간에 인증서로 상호 연동을 합니다. 문제는 해당 쿠버네티스 클러스터를 설치하는 과정에서, 이 인증서를 마스터 서버마다 따로 생성을 해버려서, 한군데만 정상적으로 작동한다는 것이였습니다. (kubeadm을 이용해서 설치하였는데, 해당 인증서를 복사해서 사용하지 않고, 서버 마다 직접 설치하여서 자동으로 생성된 경우입니다.) 그래서, 인증서를 다시 생성한 후, 각 마스터 서버에 복사하고, kube-api-server를 재시작 하였고, metrics-server를 재시작 해서 문제를 해결하였습니다.

쿠버네티스 버전이 1.11이기 때문에 kubeadm.k8s.io/v1alpha1 형식으로 파일을 만들어야 했습니다. 로드 밸런서 도메인 이름과, IP 주소, 각 마스터 서버의 IP 주소를 apiServerCertSANs 에 추가하여 kube-config.yaml 파일을 생성하였습니다

apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  advertiseAddress: xx.xx.xx.xx
...
apiServerCertSANs:
  - lb.xx.xx.xx
  - lb.xx.xx.xx
  - ma.xx.xx.xx
  - ma.xx.xx.xx
  - ma.xx.xx.xx

그리고 kubeadm을 실행해서 front-proxy 인증서를 생성하였습니다.

kubeadm alpha phase certs front-proxy-ca --config kube-config.yaml
kubeadm alpha phase certs front-proxy-client --config kube-config.yaml

위의 명령을 실행하면 해당 파일들이 생성됩니다.

  • front-proxy-ca.crt
  • front-proxy-ca.key
  • front-proxy-client.crt
  • front-proxy-client.key

생성한 파일들을, 나머지 마스터 서버에 복사하고, kube-api-server를 재시작합니다.

모든 마스터 서버의 작업이 끝났으면, metrics-server를 재시작합니다.

flannel : failed to find IPv4 address

flannel을 설치하였으나, 아이피를 찾을 수 없다는 에러가 발생하고 작동하지 않는 문제가 발생하였습니다.

$ kubectl -n kube-system get pod -lapp=flannel
NAME                    READY     STATUS             RESTARTS   AGE
kube-flannel-ds-2vnhj   1/1       CrashLoopBackOff   5          1m12s
kube-flannel-ds-b6mqq   1/1       CrashLoopBackOff   5          1m12s
kube-flannel-ds-f2bpz   1/1       CrashLoopBackOff   5          1m12s
$ kubectl -n kube-system logs kube-flannel-ds-2vnhj
I0601 19:31:31.628591       1 main.go:475] Determining IP address of default interface
E0601 19:31:31.630621       1 main.go:193] Failed to find any valid interface to use: failed to find IPv4 address for interface eth0.100

flannel은 기본적으로 eth0 인터페이스에서 아이피를 찾게 되는데, 해당 서버는 bond0에 IP가 할당 되어 있었습니다. 이 문제를 해결하기 위해서는 실행 플래그에 --iface=bond0을 추가해 주면 됩니다.

$ kubectl -n kube-system edit daemonset kube-flannel-ds
 
...
    spec:
      containers:
      - args:
        - --ip-masq
        - --kube-subnet-mgr
        - --iface=bond0
        - --iface=eth0
        command:
        - /opt/bin/flanneld
        env:
        - name: POD_NAME
...