Kubernetes ハンズオン

この記事は前回の記事と紐づいています。適宜参照してください。

Kubernetes基礎とアーキテクチャ

kubectlコマンド

まずはPodを作成しましょう。

# basic-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx:stable-alpine
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-no-label
spec:
  containers:
    - name: nginx
      image: nginx:stable-alpine


マニフェストファイルに記載されたリソースを作成する時は、kubectl applyコマンドを使用します。

$ kubectl apply -f basic-pod.yaml
pod/nginx created
pod/nginx-no-label created


起動中のリソースを一覧表示するときはkubectl getコマンドを使います。Podを一覧するときはkubectl get podsです。

$ kubectl get pods 
NAME             READY   STATUS    RESTARTS   AGE
nginx            1/1     Running   0          2m44s
nginx-no-label   1/1     Running   0          2m17s


より詳しい情報を見るときは-o wideを付けます。

$ kubectl get pods -o wide
NAME             READY   STATUS              RESTARTS   AGE   IP       NODE                                          NOMINATED NODE   READINESS GATES
nginx            0/1     ContainerCreating   0          3s    <none>   gke-kubernetes-1-default-pool-2c12379a-jkjk   <none>           <none>
nginx-no-label   0/1     ContainerCreating   0          3s    <none>   gke-kubernetes-1-default-pool-2c12379a-jkjk   <none>           <none>


Podにはラベルを付けることができます。特定のラベルがついているPodの一覧は-lオプションを使用することでラベルを指定することができます。

$ kubectl get pods -l app=web
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          22m


起動中のPodの中にあるコンテナにログインすることは非常に多いので覚えましょう。kubectl execコマンドでログインができます。

$ kubectl exec -it nginx ash
/ #
/ # cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.16.2.2       nginx
/ #


Nginxサーバのコンテナを起動するPodに直接トラフィックを送ってみましょう。kubectl port-fowardでPodへのポートフォワードを行うことができ、kubectl port-foward <Pod名> ローカルポート:Podのポート番号のように使います。

$ kubectl port-forward nginx 8081:80
Forwarding from 127.0.0.1:8081 -> 80


起動中のPodの定義を少しだけ変更したい場合、わざわざマニフェストファイルを書き出してkubectl applyをするのは面倒です。そんな時はkubectl edit <Pod名>で定義を直接変更することができます。

$ kubectl edit pods nginx
※vimが起動して編集することができます


k8sのリソースを一覧表示するコマンドは以下です。やってみてください。

$ kubectl api-resources
(省略)


それぞれのリソースにどんな定義があるかはkubectl explainコマンドで調べられます。

$ kubectl explain Pod.spec
KIND:     Pod
VERSION:  v1

RESOURCE: spec <Object>

DESCRIPTION:
     Specification of the desired behavior of the pod. More info:
     https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status

     PodSpec is a description of a pod.

FIELDS:
...

Podをネットワークに公開するService


ClusterIP
以下のマニフェストファイルには先ほど作ったapp=webのラベルを持つnginxのPodに対応したselectorを持つClusterIP Serviceを起動する定義が書いてあります。kubectlコマンドを利用してデプロイしてください。

# nginx-clusterip.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx-clusterip
spec:
  selector:
    app: web
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80


ClusterIPはクラスタの内部に公開するServiceなので、このままローカル環境にいてはトラフィックを送れません。そのため、alpineコンテナを起動するPodをデバック用のPodとして用意します。このようなケースでわざわざマニフェストファイルを作るのは面倒なので、kubectl runコマンドで直接Podを作成しましょう。

$ kubectl run -it --generator=run-pod/v1 alpine --image=alpine ash
If you don't see a command prompt, try pressing enter.
/ #
/ #


プロンプトが表示されたら、apkパッケージのアップデート、curlコマンドのインストールを行いましょう。インストールが終わったら、nginx-clusteripに対してリクエストを送ってみましょう。

/ # apk update
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
v3.11.5-40-ge30587e6d0 [http://dl-cdn.alpinelinux.org/alpine/v3.11/main]
v3.11.5-39-g0b5b5381b3 [http://dl-cdn.alpinelinux.org/alpine/v3.11/community]
OK: 11268 distinct packages available

/ # apk add curl
(1/4) Installing ca-certificates (20191127-r1)
(2/4) Installing nghttp2-libs (1.40.0-r0)
(3/4) Installing libcurl (7.67.0-r0)
(4/4) Installing curl (7.67.0-r0)
Executing busybox-1.31.1-r9.trigger
Executing ca-certificates-20191127-r1.trigger
OK: 7 MiB in 18 packages

/ # curl -i nginx-clusterip
HTTP/1.1 200 OK
Server: nginx/1.16.1
...
<h1>Welcome to nginx!</h1>
...



NodePort
まずはtype: NodePortのServiceを以下のマニュフェストファイルから作成しましょう。

# nginx-nodeport.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx-nodeport
spec:
  selector:
    app: web
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30001


NodePortにトラフィックを送信するためにはもちろんNodeのIPアドレスが必要です。k8sクラスタを構成するNodeのIPアドレスkubectl get nodesで調べられます。

$ kubectl get nodes -o wide
NAME                                          STATUS   ROLES    AGE    VERSION           INTERNAL-IP   EXTERNAL-IP      OS-IMAGE                             KERNEL-VERSION   CONTAINER-RUNTIME
gke-kubernetes-1-default-pool-0c238d8b-p9z9   Ready    <none>   28h    v1.14.10-gke.27   10.146.0.52   34.85.11.21      Container-Optimized OS from Google   4.14.138+        docker://18.9.7
gke-kubernetes-1-default-pool-2c12379a-jkjk   Ready    <none>   73m    v1.14.10-gke.27   10.146.0.54   35.221.123.134   Container-Optimized OS from Google   4.14.138+        docker://18.9.7
gke-kubernetes-1-default-pool-c724d8c0-l09b   Ready    <none>   7d8h   v1.14.10-gke.27   10.146.0.53   104.198.92.230   Container-Optimized OS from Google   4.14.138+        docker://18.9.7


この出力の例では、34.85.11.2135.221.123.134または104.198.92.230です。早速、34.85.11.21の30001番にトラフィックを送ってみたいところですが、GCPではファイアウォールルールが許可されていないため、トラフィックを送信することができません。まずはこのGKEクラスタが作成されているdefaultネットワークに対して、NodePortの範囲である30000~32767番のポートへのTCPを許可するルールを作ります。

$ gcloud compute firewall-rules create allow-nodeport \
    --allow=tcp:30000-32767    \
    --source-ranges=0.0.0.0/0  \
    --network=default
Creating firewall...⠧Created [https://www.googleapis.com/compute/v1/projects/ca-container-book/global/firewalls/allow-nodeport].
Creating firewall...done.
NAME            NETWORK  DIRECTION  PRIORITY  ALLOW            DENY  DISABLED
allow-nodeport  default  INGRESS    1000      tcp:30000-32767        False


こうすることでNodePortへのトラフィックが許可されます。

$ curl -i 34.85.11.21:30001
HTTP/1.1 200 OK
Server: nginx/1.16.1
...



LoadBalancer
実際にコンテナを外部に公開する際には、LoadBalancerを使います。以下のマニフェストファイルをkubectlでデプロイして、LoadBalancerを作成してください。

#nginx-loadbalancer.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx-lb
spec:
  selector:
    app: web
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80



ここで、EXTERNAL-IPが”pending”となっているはずです。これは、GCPのロードバランサが外部IPアドレスを確保している最中であることを意味しています。ロードバランサが外部IPアドレスの確保に成功すると、"pending"が次のように置き換わります。

$ kubectl get svc nginx-lb -w
NAME       TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
nginx-lb   LoadBalancer   10.0.12.136   <pending>     80:30097/TCP   5s
nginx-lb   LoadBalancer   10.0.12.136   35.243.113.150   80:30097/TCP   73s


kubectl getコマンドの最後につけている-wは、一覧表示中のリソースに変更があった場合に自動的に新しい情報を出力してくれるオプションです。この場合は、curlを使って次のようにロードバランサにトラフィックを送信できます。

$ curl -i 35.243.113.150
HTTP/1.1 200 OK
Server: nginx/1.16.1

試しに上記のIPに別のWindowからアクセスしてみましょう。
"http://35.243.113.150"
どうですか?Welcome to nginxと出てきましたか?

コンテナの監視

本番環境でコンテナを起動し続けるためには、readinessProbeとlivenessProbeの設定が不可欠です。ここでは2つのProbeの動作を確認するためのコンテナを作ってみます。 まずはそのコンテナをビルドします。以下のDockerfile, main.goを使ってください。 そのあと、GCR(Google Containar Registry)にpushします。(DockerhubではなくGoogleの自分のローカルGCP上に安全に保存することができます)
YOUR_PROJECT_IDの部分は、自分のGCPプロジェクトIDに変更して実行してください。(私はここをプロジェクト名にしててpushできなくて1週間悩みました。無知)

# Dockerfile

FROM golang:latest as builder
WORKDIR /app
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates curl
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080 80
CMD ["./main"]
// main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

var isHealth bool
var hostname = os.Getenv("HOSTNAME")

func main() {
    if os.Getenv("HEALTHY") == "FALSE" || os.Getenv("HEALTHY") == "false" {
        isHealth = false
    } else {
        isHealth = true
    }

    http.HandleFunc("/health", healthyHandler)
    http.HandleFunc("/unhealth", unhealthyHandler)
    http.HandleFunc("/ping", pingHandler)
    http.HandleFunc("/", indexHandler)
    port := os.Getenv("PORT")
    if port == "" {
        port = "80"
        log.Printf("HostName is %s", hostname)
    }
    log.Printf("Listening on port %s", port)
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

func healthyHandler(w http.ResponseWriter, r *http.Request) {
    isHealth = true
    _, err := fmt.Fprintf(w, "%s: change response code to 200", hostname)
    if err != nil {
        w.WriteHeader(http.StatusOK)
    }
}

func unhealthyHandler(w http.ResponseWriter, r *http.Request) {
    isHealth = false
    _, err := fmt.Fprintf(w, "%s: change response code to 503", hostname)
    if err != nil {
        w.WriteHeader(http.StatusOK)
    }
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    if isHealth == true {
        _, err := fmt.Fprintf(w, "HostName is %s", hostname)
        if err != nil {
            w.WriteHeader(http.StatusOK)
        }
        return
    }

    w.WriteHeader(http.StatusServiceUnavailable)
    return
}

func pingHandler(w http.ResponseWriter, r *http.Request) {
    _, err := fmt.Fprintf(w, "%s: pong", hostname)
    if err != nil {
        w.WriteHeader(http.StatusOK)
    }
}
$ docker build -t gcr.io/YOUR_PROJECT_ID/probe .


GCRにコンテナをpushするために、dockerコマンドに対してGCPに認証させる必要があります。

$ gcloud auth configure-docker


ビルドしたイメージをpushします。

$ docker push gcr.io/YOUR_PRIOJECT_ID/probe



GCP GUIにContainar Registryにコンテナがpushされているのを確認できると思います。
コンテナイメージがpushできたら、さっそくk8sにデプロイします。以下のマニフェストファイルをkubectlコマンドでデプロイしてください。

apiVersion: v1
kind: Pod
metadata:
  name: probe
  labels:
    app: probe
spec:
  containers:
    - name: probe
      image: gcr.io/k8s-seminar-272301/probe
      readinessProbe:
        initialDelaySeconds: 10
        periodSeconds: 30
        failureThreshold: 2
        successThreshold: 1
        timeoutSeconds: 5
        httpGet:
          port: 80
          path: /
      livenessProbe:
        httpGet:
          port: 80
          path: /
---
apiVersion: v1
kind: Service
metadata:
  name: probe
spec:
  selector:
    app: probe
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30003


readinessProbeに成功している場合、kubectl get podsのREADYステータスが1/1となります。(この数字は正常に動いているコンテナ数/内部コンテナ数) このコンテナはルートパス/へのリクエストの結果が/healthへのリクエストで成功するようになり、/unhealthへのリクエストで失敗するようになります。つまり、この状態で/unhealthへリクエストを送ると、readinessProbeに失敗するようになります。

# 最初はリクエストに成功する
$ curl -i 34.84.230.195:30003
HTTP/1.1 200 OK

#/unhealthにリクエストを送る
$ curl -i 34.84.230.195:30003/unhealth
HTTP/1.1 200 OK

#リクエストに失敗するようになる
$ curl -i 34.84.230.195:30003
HTTP/1.1 503 Service Unavailable


この状態でREADYステータスを確認します。

$ kubectl get po probe -w
NAME    READY   STATUS    RESTARTS   AGE
probe   0/1     Running   3          42m
probe   1/1     Running   3          42m


readinessProbeはREADYではないPodにトラフィックを送信しないように振る舞います。
livenessProbeはREADYではないPodを再起動します。
いまはlivenessProbeがいるのですぐに再起動してしまいますが、livenessProbeをコメントアウトしてPodを作り直すと、503のあとにしばらくしてリクエストを送ると何も帰ってこないというreadinessProbeの振る舞いを確認することができます。

ConfigMap,Secret

ConfigMap
以下のマニフェストファイルはspec.envFromでConfigMapを指定し、指定しているConfigMapを作成するマニフェストファイルです。 では、実際にkubectlコマンドでデプロイしてみましょう。

# pod-with-configmap.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-config
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx:stable-alpine
      envFrom:
        - configMapRef:
            name: env-value
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-value
data:
  ENV: "Hello World"


作成したPodにログインして環境変数を確認してみましょう。

$ kubectl exec -it nginx-with-config ash
/ #
/ # echo $ENV
Hello World
/ #


実際に環境変数が入っていることが確認できました。

Secret
Secretはまず自分で入れたい環境変数base64エンコードします。

$ echo -n 'Hello World!' | base64    
SGVsbG8gV29ybGQh


それをSecretに入れます。

# secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: secret
data:
  key: SGVsbG8gV29ybGQh


これで上のConfigMapと同じ要領でPodをデプロイすれば環境変数keyHello World!が設定されます。

Kubernetesにおけるコンテナのスケール

すいません。この項目については私自身きちんと理解しきれていないので今回は控えさせていただきます。



今回はここまでです。
一応直近3つの記事で自分が理解したことは書けたかなと思います。
それでは