1 - Deploymentを使用してステートレスアプリケーションを実行する

このページでは、Kubernetes Deploymentオブジェクトを使用してアプリケーションを実行する方法を説明します。

目標

  • nginx deploymentを作成します。
  • kubectlを使ってdeploymentに関する情報を一覧表示します。
  • deploymentを更新します。

始める前に

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

作業するKubernetesサーバーは次のバージョン以降のものである必要があります: v1.9. バージョンを確認するには次のコマンドを実行してください: kubectl version.

nginx deploymentの作成と探検

Kubernetes Deploymentオブジェクトを作成することでアプリケーションを実行できます。また、YAMLファイルでDeploymentを記述できます。例えば、このYAMLファイルはnginx:1.14.2 Dockerイメージを実行するデプロイメントを記述しています:

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
  1. YAMLファイルに基づいてDeploymentを作成します:

     kubectl apply -f https://k8s.io/examples/application/deployment.yaml
    
  2. Deploymentに関する情報を表示します:

     kubectl describe deployment nginx-deployment
    

    出力はこのようになります:

     Name:     nginx-deployment
     Namespace:    default
     CreationTimestamp:  Tue, 30 Aug 2016 18:11:37 -0700
     Labels:     app=nginx
     Annotations:    deployment.kubernetes.io/revision=1
     Selector:   app=nginx
     Replicas:   2 desired | 2 updated | 2 total | 2 available | 0 unavailable
     StrategyType:   RollingUpdate
     MinReadySeconds:  0
     RollingUpdateStrategy:  1 max unavailable, 1 max surge
     Pod Template:
       Labels:       app=nginx
       Containers:
        nginx:
         Image:              nginx:1.14.2
         Port:               80/TCP
         Environment:        <none>
         Mounts:             <none>
       Volumes:              <none>
     Conditions:
       Type          Status  Reason
       ----          ------  ------
       Available     True    MinimumReplicasAvailable
       Progressing   True    NewReplicaSetAvailable
     OldReplicaSets:   <none>
     NewReplicaSet:    nginx-deployment-1771418926 (2/2 replicas created)
     No events.
    
  3. Deploymentによって作成されたPodを一覧表示します:

     kubectl get pods -l app=nginx
    

    出力はこのようになります:

     NAME                                READY     STATUS    RESTARTS   AGE
     nginx-deployment-1771418926-7o5ns   1/1       Running   0          16h
     nginx-deployment-1771418926-r18az   1/1       Running   0          16h
    
  4. Podに関する情報を表示します:

     kubectl describe pod <pod-name>
    

    ここで<pod-name>はPodの1つの名前を指定します。

Deploymentの更新

新しいYAMLファイルを適用してDeploymentを更新できます。このYAMLファイルは、Deploymentを更新してnginx 1.16.1を使用するように指定しています。

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.16.1 # Update the version of nginx from 1.14.2 to 1.16.1
        ports:
        - containerPort: 80
  1. 新しいYAMLファイルを適用します:

      kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
    
  2. Deploymentが新しい名前でPodを作成し、古いPodを削除するのを監視します:

      kubectl get pods -l app=nginx
    

レプリカ数を増やすことによるアプリケーションのスケール

新しいYAMLファイルを適用することで、Deployment内のPodの数を増やすことができます。このYAMLファイルはreplicasを4に設定します。これはDeploymentが4つのPodを持つべきであることを指定します:

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 4 # Update the replicas from 2 to 4
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
  1. 新しいYAMLファイルを適用します:

     kubectl apply -f https://k8s.io/examples/application/deployment-scale.yaml
    
  2. Deploymentに4つのPodがあることを確認します:

     kubectl get pods -l app=nginx
    

    出力はこのようになります:

     NAME                               READY     STATUS    RESTARTS   AGE
     nginx-deployment-148880595-4zdqq   1/1       Running   0          25s
     nginx-deployment-148880595-6zgi1   1/1       Running   0          25s
     nginx-deployment-148880595-fxcez   1/1       Running   0          2m
     nginx-deployment-148880595-rwovn   1/1       Running   0          2m
    

Deploymentの削除

Deploymentを名前を指定して削除します:

kubectl delete deployment nginx-deployment

ReplicationControllers -- 昔のやり方

複製アプリケーションを作成するための好ましい方法はDeploymentを使用することです。そして、DeploymentはReplicaSetを使用します。 DeploymentとReplicaSetがKubernetesに追加される前は、ReplicationControllerを使用して複製アプリケーションを構成していました。

次の項目

2 - 単一レプリカのステートフルアプリケーションを実行する

このページでは、PersistentVolumeとDeploymentを使用して、Kubernetesで単一レプリカのステートフルアプリケーションを実行する方法を説明します。アプリケーションはMySQLです。

目標

  • 自身の環境のディスクを参照するPersistentVolumeを作成します。
  • MySQLのDeploymentを作成します。
  • MySQLをDNS名でクラスター内の他のPodに公開します。

始める前に

  • Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

    バージョンを確認するには次のコマンドを実行してください: kubectl version.

  • ここで使用されているPersistentVolumeClaimsの要件を満たすには、デフォルトのStorageClassを使用して動的PersistentVolumeプロビジョナーを作成するか、PersistentVolumesを静的にプロビジョニングする必要があります。

MySQLをデプロイする

Kubernetes Deploymentを作成し、PersistentVolumeClaimを使用して既存のPersistentVolumeに接続することで、ステートフルアプリケーションを実行できます。 たとえば、以下のYAMLファイルはMySQLを実行し、PersistentVolumeClaimを参照するDeploymentを記述しています。 このファイルは/var/lib/mysqlのボリュームマウントを定義してから、20Gのボリュームを要求するPersistentVolumeClaimを作成します。 この要求は、要件を満たす既存のボリューム、または動的プロビジョナーによって満たされます。

注:パスワードはYAMLファイル内に定義されており、これは安全ではありません。安全な解決策についてはKubernetes Secretを参照してください 。

apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
          # Use secret in real usage
        - name: MYSQL_ROOT_PASSWORD
          value: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mysql-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

  1. YAMLファイルに記述されたPVとPVCをデプロイします。

     kubectl create -f https://k8s.io/examples/application/mysql/mysql-pv.yaml
    
  2. YAMLファイルの内容をデプロイします。

     kubectl create -f https://k8s.io/examples/application/mysql/mysql-deployment.yaml
    
  3. 作成したDeploymentの情報を表示します。

     kubectl describe deployment mysql
    
     Name:                 mysql
     Namespace:            default
     CreationTimestamp:    Tue, 01 Nov 2016 11:18:45 -0700
     Labels:               app=mysql
     Annotations:          deployment.kubernetes.io/revision=1
     Selector:             app=mysql
     Replicas:             1 desired | 1 updated | 1 total | 0 available | 1 unavailable
     StrategyType:         Recreate
     MinReadySeconds:      0
     Pod Template:
       Labels:       app=mysql
       Containers:
        mysql:
         Image:      mysql:5.6
         Port:       3306/TCP
         Environment:
           MYSQL_ROOT_PASSWORD:      password
         Mounts:
           /var/lib/mysql from mysql-persistent-storage (rw)
       Volumes:
        mysql-persistent-storage:
         Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
         ClaimName:  mysql-pv-claim
         ReadOnly:   false
     Conditions:
       Type          Status  Reason
       ----          ------  ------
       Available     False   MinimumReplicasUnavailable
       Progressing   True    ReplicaSetUpdated
     OldReplicaSets:       <none>
     NewReplicaSet:        mysql-63082529 (1/1 replicas created)
     Events:
       FirstSeen    LastSeen    Count    From                SubobjectPath    Type        Reason            Message
       ---------    --------    -----    ----                -------------    --------    ------            -------
       33s          33s         1        {deployment-controller }             Normal      ScalingReplicaSet Scaled up replica set mysql-63082529 to 1
    
  4. Deploymentによって作成されたPodを一覧表示します。

     kubectl get pods -l app=mysql
    
     NAME                   READY     STATUS    RESTARTS   AGE
     mysql-63082529-2z3ki   1/1       Running   0          3m
    
  5. PersistentVolumeClaimを確認します。

     kubectl describe pvc mysql-pv-claim
    
     Name:         mysql-pv-claim
     Namespace:    default
     StorageClass:
     Status:       Bound
     Volume:       mysql-pv-volume
     Labels:       <none>
     Annotations:    pv.kubernetes.io/bind-completed=yes
                     pv.kubernetes.io/bound-by-controller=yes
     Capacity:     20Gi
     Access Modes: RWO
     Events:       <none>
    

MySQLインスタンスにアクセスする

前述のYAMLファイルは、クラスター内の他のPodがデータベースにアクセスできるようにするServiceを作成します。 ServiceのオプションでclusterIP: Noneを指定すると、ServiceのDNS名がPodのIPアドレスに直接解決されます。 このオプションは、ServiceのバックエンドのPodが1つのみであり、Podの数を増やす予定がない場合に適しています。

MySQLクライアントを実行してサーバーに接続します。

kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -ppassword

このコマンドは、クラスター内にMySQLクライアントを実行する新しいPodを作成し、Serviceを通じてMySQLサーバーに接続します。 接続できれば、ステートフルなMySQLデータベースが稼働していることが確認できます。

Waiting for pod default/mysql-client-274442439-zyp6i to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

mysql>

アップデート

イメージまたはDeploymentの他の部分は、kubectl applyコマンドを使用して通常どおりに更新できます。 ステートフルアプリケーションに固有のいくつかの注意事項を以下に記載します。

  • アプリケーションをスケールしないでください。このセットアップは単一レプリカのアプリケーション専用です。 下層にあるPersistentVolumeは1つのPodにしかマウントできません。 クラスター化されたステートフルアプリケーションについては、StatefulSetのドキュメントを参照してください。
  • Deploymentを定義するYAMLファイルではstrategy: type: Recreateを使用して下さい。 この設定はKubernetesにローリングアップデートを使用 しない ように指示します。 同時に複数のPodを実行することはできないため、ローリングアップデートは使用できません。 Recreate戦略は、更新された設定で新しいPodを作成する前に、最初のPodを停止します。

Deploymentの削除

名前を指定してデプロイしたオブジェクトを削除します。

kubectl delete deployment,svc mysql
kubectl delete pvc mysql-pv-claim
kubectl delete pv mysql-pv-volume

PersistentVolumeを手動でプロビジョニングした場合は、PersistentVolumeを手動で削除し、また、下層にあるリソースも解放する必要があります。 動的プロビジョニング機能を使用した場合は、PersistentVolumeClaimを削除すれば、自動的にPersistentVolumeも削除されます。 一部の動的プロビジョナー(EBSやPDなど)は、PersistentVolumeを削除すると同時に下層にあるリソースも解放します。

次の項目

3 - レプリカを持つステートフルアプリケーションを実行する

このページでは、StatefulSet コントローラーを使用して、レプリカを持つステートフルアプリケーションを実行する方法を説明します。 ここでの例は、非同期レプリケーションを行う複数のスレーブを持つ、単一マスターのMySQLです。

この例は本番環境向けの構成ではないことに注意してください。 具体的には、MySQLの設定が安全ではないデフォルトのままとなっています。 これはKubernetesでステートフルアプリケーションを実行するための一般的なパターンに焦点を当てるためです。

始める前に

  • Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

    バージョンを確認するには次のコマンドを実行してください: kubectl version.
  • ここで使用されているPersistentVolumeClaimsの要件を満たすには、デフォルトのStorageClassを使用して動的PersistentVolumeプロビジョナーを作成するか、PersistentVolumesを静的にプロビジョニングする必要があります。

  • このチュートリアルは、あなたがPersistentVolumeStatefulSet、 さらにはPodServiceConfigMapなどの 他のコアな概念に精通していることを前提としています。
  • MySQLに関する知識は記事の理解に役立ちますが、 このチュートリアルは他のシステムにも役立つ一般的なパターンを提示することを目的としています。

目標

  • StatefulSetコントローラーを使用して、レプリカを持つMySQLトポロジーをデプロイします。
  • MySQLクライアントトラフィックを送信します。
  • ダウンタイムに対する耐性を観察します。
  • StatefulSetをスケールアップおよびスケールダウンします。

MySQLをデプロイする

このMySQLのデプロイの例は、1つのConfigMap、2つのService、および1つのStatefulSetから構成されます。

ConfigMap

次のYAML設定ファイルからConfigMapを作成します。

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
data:
  primary.cnf: |
    # Apply this config only on the primary.
    [mysqld]
    log-bin    
  replica.cnf: |
    # Apply this config only on replicas.
    [mysqld]
    super-read-only    

このConfigMapは、MySQLマスターとスレーブの設定を独立して制御するために、 それぞれのmy.cnfを上書きする内容を提供します。 この場合、マスターはスレーブにレプリケーションログを提供するようにし、 スレーブはレプリケーション以外の書き込みを拒否するようにします。

ConfigMap自体に特別なことはありませんが、ConfigMapの各部分は異なるPodに適用されます。 各Podは、StatefulSetコントローラーから提供される情報に基づいて、 初期化時にConfigMapのどの部分を見るかを決定します。

Services

以下のYAML設定ファイルからServiceを作成します。

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml
# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mysql
---
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the master: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  labels:
    app: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql

ヘッドレスサービスは、StatefulSetコントローラーが StatefulSetの一部であるPodごとに作成するDNSエントリーのベースエントリーを提供します。 この例ではヘッドレスサービスの名前はmysqlなので、同じKubernetesクラスタの 同じ名前空間内の他のPodは、<pod-name>.mysqlを名前解決することでPodにアクセスできます。

mysql-readと呼ばれるクライアントサービスは、独自のクラスタIPを持つ通常のServiceであり、 Ready状態のすべてのMySQL Podに接続を分散します。 Serviceのエンドポイントには、MySQLマスターとすべてのスレーブが含まれる可能性があります。

読み込みクエリーのみが、負荷分散されるクライアントサービスを使用できることに注意してください。 MySQLマスターは1つしかいないため、クライアントが書き込みを実行するためには、 (ヘッドレスサービス内のDNSエントリーを介して)MySQLのマスターPodに直接接続する必要があります。

StatefulSet

最後に、次のYAML設定ファイルからStatefulSetを作成します。

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - |
          set -ex
          # Generate mysql server-id from pod ordinal index.
          [[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          # Add an offset to avoid reserved server-id=0 value.
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          # Copy appropriate conf.d files from config-map to emptyDir.
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/master.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/slave.cnf /mnt/conf.d/
          fi          
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      - name: clone-mysql
        image: gcr.io/google-samples/xtrabackup:1.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          # Skip the clone if data already exists.
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          # Skip the clone on master (ordinal index 0).
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          [[ $ordinal -eq 0 ]] && exit 0
          # Clone data from previous peer.
          ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
          # Prepare the backup.
          xtrabackup --prepare --target-dir=/var/lib/mysql          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ALLOW_EMPTY_PASSWORD
          value: "1"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            # Check we can execute queries over TCP (skip-networking is off).
            command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
      - name: xtrabackup
        image: gcr.io/google-samples/xtrabackup:1.0
        ports:
        - name: xtrabackup
          containerPort: 3307
        command:
        - bash
        - "-c"
        - |
          set -ex
          cd /var/lib/mysql

          # Determine binlog position of cloned data, if any.
          if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
            # XtraBackup already generated a partial "CHANGE MASTER TO" query
            # because we're cloning from an existing slave. (Need to remove the tailing semicolon!)
            cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
            # Ignore xtrabackup_binlog_info in this case (it's useless).
            rm -f xtrabackup_slave_info xtrabackup_binlog_info
          elif [[ -f xtrabackup_binlog_info ]]; then
            # We're cloning directly from master. Parse binlog position.
            [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
            rm -f xtrabackup_binlog_info xtrabackup_slave_info
            echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
                  MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
          fi

          # Check if we need to complete a clone by starting replication.
          if [[ -f change_master_to.sql.in ]]; then
            echo "Waiting for mysqld to be ready (accepting connections)"
            until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done

            echo "Initializing replication from clone position"
            mysql -h 127.0.0.1 \
                  -e "$(<change_master_to.sql.in), \
                          MASTER_HOST='mysql-0.mysql', \
                          MASTER_USER='root', \
                          MASTER_PASSWORD='', \
                          MASTER_CONNECT_RETRY=10; \
                        START SLAVE;" || exit 1
            # In case of container restart, attempt this at-most-once.
            mv change_master_to.sql.in change_master_to.sql.orig
          fi

          # Start a server to send backups when requested by peers.
          exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
            "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

次のコマンドを実行して起動の進行状況を確認できます。

kubectl get pods -l app=mysql --watch

しばらくすると、3つのPodすべてがRunning状態になるはずです。

NAME      READY     STATUS    RESTARTS   AGE
mysql-0   2/2       Running   0          2m
mysql-1   2/2       Running   0          1m
mysql-2   2/2       Running   0          1m

Ctrl+Cを押してウォッチをキャンセルします。 起動が進行しない場合は、始める前にで説明されているように、 PersistentVolumeの動的プロビジョニング機能が有効になっていることを確認してください。

このマニフェストでは、StatefulSetの一部としてステートフルなPodを管理するためにさまざまな手法を使用しています。 次のセクションでは、これらの手法のいくつかに焦点を当て、StatefulSetがPodを作成するときに何が起こるかを説明します。

ステートフルなPodの初期化を理解する

StatefulSetコントローラーは、序数インデックスの順にPodを一度に1つずつ起動します。 各PodがReady状態を報告するまで待機してから、その次のPodの起動が開始されます。

さらに、コントローラーは各Podに <statefulset-name>-<ordinal-index>という形式の一意で不変の名前を割り当てます。 この例の場合、Podの名前はmysql-0mysql-1、そしてmysql-2となります。

上記のStatefulSetマニフェスト内のPodテンプレートは、これらのプロパティーを利用して、 MySQLレプリケーションの起動を順序正しく実行します。

構成を生成する

Podスペック内のコンテナを起動する前に、Podは最初に 初期化コンテナを定義された順序で実行します。

最初の初期化コンテナはinit-mysqlという名前で、序数インデックスに基づいて特別なMySQL設定ファイルを生成します。

スクリプトは、hostnameコマンドによって返されるPod名の末尾から抽出することによって、自身の序数インデックスを特定します。 それから、序数を(予約された値を避けるために数値オフセット付きで)MySQLのconf.dディレクトリーのserver-id.cnfというファイルに保存します。 これは、StatefulSetコントローラーによって提供される一意で不変のIDを、同じ特性を必要とするMySQLサーバーIDの領域に変換します。

さらに、init-mysqlコンテナ内のスクリプトは、master.cnfまたはslave.cnfのいずれかを、 ConfigMapから内容をconf.dにコピーすることによって適用します。 このトポロジー例は単一のMySQLマスターと任意の数のスレーブで構成されているため、 スクリプトは単に序数の0がマスターになるように、それ以外のすべてがスレーブになるように割り当てます。 StatefulSetコントローラーによる デプロイ順序の保証と組み合わせると、 スレーブが作成される前にMySQLマスターがReady状態になるため、スレーブはレプリケーションを開始できます。

既存データをクローンする

一般に、新しいPodがセットにスレーブとして参加するときは、 MySQLマスターにはすでにデータがあるかもしれないと想定する必要があります。 また、レプリケーションログが期間の先頭まで全て揃っていない場合も想定する必要があります。 これらの控えめな仮定は、実行中のStatefulSetのサイズを初期サイズに固定するのではなく、 時間の経過とともにスケールアップまたはスケールダウンできるようにするために重要です。

2番目の初期化コンテナはclone-mysqlという名前で、スレーブPodが空のPersistentVolumeで最初に起動したときに、 クローン操作を実行します。 つまり、実行中の別のPodから既存のデータをすべてコピーするので、 そのローカル状態はマスターからレプリケーションを開始するのに十分な一貫性があります。

MySQL自体はこれを行うためのメカニズムを提供していないため、この例ではPercona XtraBackupという人気のあるオープンソースツールを使用しています。 クローンの実行中は、ソースとなるMySQLサーバーのパフォーマンスが低下する可能性があります。 MySQLマスターへの影響を最小限に抑えるために、スクリプトは各Podに序数インデックスが自分より1低いPodからクローンするように指示します。 StatefulSetコントローラーは、N+1のPodを開始する前には必ずNのPodがReady状態であることを保証するので、この方法が機能します。

レプリケーションを開始する

初期化コンテナが正常に完了すると、通常のコンテナが実行されます。 MySQLのPodは実際にmysqldサーバーを実行するmysqlコンテナと、 サイドカー として機能するxtrabackupコンテナから成ります。

xtrabackupサイドカーはクローンされたデータファイルを見て、 スレーブ上でMySQLレプリケーションを初期化する必要があるかどうかを決定します。 もし必要がある場合、mysqldが準備できるのを待ってから、 XtraBackupクローンファイルから抽出されたレプリケーションパラメーターでCHANGE MASTER TOSTART SLAVEコマンドを実行します。

スレーブがレプリケーションを開始すると、スレーブはMySQLマスターを記憶し、 サーバーが再起動した場合または接続が停止した場合に、自動的に再接続します。 また、スレーブはその不変のDNS名(mysql-0.mysql)でマスターを探すため、 再スケジュールされたために新しいPod IPを取得したとしても、自動的にマスターを見つけます。

最後に、レプリケーションを開始した後、xtrabackupコンテナはデータのクローンを要求する他のPodからの接続を待ち受けます。 StatefulSetがスケールアップした場合や、次のPodがPersistentVolumeClaimを失ってクローンをやり直す必要がある場合に備えて、 このサーバーは無期限に起動したままになります。

クライアントトラフィックを送信する

テストクエリーをMySQLマスター(ホスト名 mysql-0.mysql)に送信するには、 mysql:5.7イメージを使って一時的なコンテナを実行し、mysqlクライアントバイナリーを実行します。

kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
  mysql -h mysql-0.mysql <<EOF
CREATE DATABASE test;
CREATE TABLE test.messages (message VARCHAR(250));
INSERT INTO test.messages VALUES ('hello');
EOF

Ready状態を報告したいずれかのサーバーにテストクエリーを送信するには、ホスト名mysql-readを使用します。

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-read -e "SELECT * FROM test.messages"

次のような出力が得られるはずです。

Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

mysql-readサービスがサーバー間で接続を分散させることを実証するために、 ループでSELECT @@server_idを実行することができます。

kubectl run mysql-client-loop --image=mysql:5.7 -i -t --rm --restart=Never --\
  bash -ic "while sleep 1; do mysql -h mysql-read -e 'SELECT @@server_id,NOW()'; done"

接続の試行ごとに異なるエンドポイントが選択される可能性があるため、 報告される@@server_idはランダムに変更されるはずです。

+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2006-01-02 15:04:05 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         102 | 2006-01-02 15:04:06 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         101 | 2006-01-02 15:04:07 |
+-------------+---------------------+

ループを止めたいときはCtrl+Cを押すことができますが、別のウィンドウで実行したままにしておくことで、 次の手順の効果を確認できます。

PodとNodeのダウンタイムをシミュレーションする

単一のサーバーではなくスレーブのプールから読み取りを行うことによって可用性が高まっていることを実証するため、 Podを強制的にReadyではない状態にする間、上記のSELECT @@server_idループを実行したままにしてください。

Readiness Probeを壊す

mysqlコンテナに対する readiness probe は、mysql -h 127.0.0.1 -e 'SELECT 1'コマンドを実行することで、サーバーが起動していてクエリーが実行できることを確認します。

このreadiness probeを失敗させる1つの方法は、そのコマンドを壊すことです。

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

ここでは、mysql-2 Podの実際のコンテナのファイルシステムにアクセスし、 mysqlコマンドの名前を変更してreadiness probeがコマンドを見つけられないようにしています。 数秒後、Podはそのコンテナの1つがReadyではないと報告するはずです。以下を実行して確認できます。

kubectl get pod mysql-2

READY列の1/2を見てください。

NAME      READY     STATUS    RESTARTS   AGE
mysql-2   1/2       Running   0          3m

この時点で、SELECT @@server_idループは実行され続け、しかしもう102が報告されないことが確認できるはずです。 init-mysqlスクリプトがserver-id100+$ordinalとして定義したことを思い出して下さい。 そのため、サーバーID102はPodのmysql-2に対応します。

それではPodを修復しましょう。すると数秒後にループ出力に再び現れるはずです。

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql.off /usr/bin/mysql

Podを削除する

StatefulSetは、Podが削除された場合にPodを再作成します。 これはReplicaSetがステートレスなPodに対して行うのと同様です。

kubectl delete pod mysql-2

StatefulSetコントローラーはmysql-2 Podがもう存在しないことに気付き、 同じ名前で同じPersistentVolumeClaimにリンクされた新しいPodを作成します。 サーバーID102がしばらくの間ループ出力から消えて、また元に戻るのが確認できるはずです。

ノードをdrainする

Kubernetesクラスタに複数のノードがある場合は、 drainを発行して ノードのダウンタイム(例えばノードのアップグレード時など)をシミュレートできます。

まず、あるMySQL Podがどのノード上にいるかを確認します。

kubectl get pod mysql-2 -o wide

ノード名が最後の列に表示されるはずです。

NAME      READY     STATUS    RESTARTS   AGE       IP            NODE
mysql-2   2/2       Running   0          15m       10.244.5.27   kubernetes-minion-group-9l2t

その後、次のコマンドを実行してノードをdrainします。 これにより、新しいPodがそのノードにスケジュールされないようにcordonされ、そして既存のPodは強制退去されます。 <node-name>は前のステップで確認したノードの名前に置き換えてください。

この操作はノード上の他のアプリケーションに影響を与える可能性があるため、 テストクラスタでのみこの操作を実行するのが最善です。

kubectl drain <node-name> --force --delete-local-data --ignore-daemonsets

Podが別のノードに再スケジュールされる様子を確認しましょう。

kubectl get pod mysql-2 -o wide --watch

次のような出力が見られるはずです。

NAME      READY   STATUS          RESTARTS   AGE       IP            NODE
mysql-2   2/2     Terminating     0          15m       10.244.1.56   kubernetes-minion-group-9l2t
[...]
mysql-2   0/2     Pending         0          0s        <none>        kubernetes-minion-group-fjlm
mysql-2   0/2     Init:0/2        0          0s        <none>        kubernetes-minion-group-fjlm
mysql-2   0/2     Init:1/2        0          20s       10.244.5.32   kubernetes-minion-group-fjlm
mysql-2   0/2     PodInitializing 0          21s       10.244.5.32   kubernetes-minion-group-fjlm
mysql-2   1/2     Running         0          22s       10.244.5.32   kubernetes-minion-group-fjlm
mysql-2   2/2     Running         0          30s       10.244.5.32   kubernetes-minion-group-fjlm

また、サーバーID102SELECT @@server_idループの出力からしばらくの消えて、 そして戻ることが確認できるはずです。

それでは、ノードをuncordonして正常な状態に戻しましょう。

kubectl uncordon <node-name>

スレーブの数をスケーリングする

MySQLレプリケーションでは、スレーブを追加することで読み取りクエリーのキャパシティーをスケールできます。 StatefulSetを使用している場合、単一のコマンドでこれを実行できます。

kubectl scale statefulset mysql  --replicas=5

次のコマンドを実行して、新しいPodが起動してくるのを確認します。

kubectl get pods -l app=mysql --watch

新しいPodが起動すると、サーバーID103104SELECT @@server_idループの出力に現れます。

また、これらの新しいサーバーが、これらのサーバーが存在する前に追加したデータを持っていることを確認することもできます。

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-3.mysql -e "SELECT * FROM test.messages"
Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

元の状態へのスケールダウンもシームレスに可能です。

kubectl scale statefulset mysql --replicas=3

ただし、スケールアップすると新しいPersistentVolumeClaimが自動的に作成されますが、 スケールダウンしてもこれらのPVCは自動的には削除されないことに注意して下さい。 このため、初期化されたPVCをそのまま置いておいくことで再スケールアップを速くしたり、 PVを削除する前にデータを抽出するといった選択が可能になります。

次のコマンドを実行してこのことを確認できます。

kubectl get pvc -l app=mysql

StatefulSetを3にスケールダウンしたにもかかわらず、5つのPVCすべてがまだ存在しています。

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
data-mysql-0   Bound     pvc-8acbf5dc-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-1   Bound     pvc-8ad39820-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-2   Bound     pvc-8ad69a6d-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-3   Bound     pvc-50043c45-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m
data-mysql-4   Bound     pvc-500a9957-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m

余分なPVCを再利用するつもりがないのであれば、削除することができます。

kubectl delete pvc data-mysql-3
kubectl delete pvc data-mysql-4

クリーンアップ

  1. SELECT @@server_idループを実行している端末でCtrl+Cを押すか、 別の端末から次のコマンドを実行して、ループをキャンセルします。

    kubectl delete pod mysql-client-loop --now
    
  2. StatefulSetを削除します。これによってPodの終了も開始されます。

    kubectl delete statefulset mysql
    
  3. Podが消えたことを確認します。 Podが終了処理が完了するのには少し時間がかかるかもしれません。

    kubectl get pods -l app=mysql
    

    上記のコマンドから以下の出力が戻れば、Podが終了したことがわかります。

    No resources found.
    
  4. ConfigMap、Services、およびPersistentVolumeClaimを削除します。

    kubectl delete configmap,service,pvc -l app=mysql
    
  5. PersistentVolumeを手動でプロビジョニングした場合は、それらを手動で削除し、 また、下層にあるリソースも解放する必要があります。 動的プロビジョニング機能を使用した場合は、PersistentVolumeClaimを削除すれば、自動的にPersistentVolumeも削除されます。 一部の動的プロビジョナー(EBSやPDなど)は、PersistentVolumeを削除すると同時に下層にあるリソースも解放します。

次の項目

  • その他のステートフルアプリケーションの例は、Helm Charts repositoryを見てください。

4 - StatefulSetのスケール

このタスクは、StatefulSetをスケールする方法を示します。StatefulSetをスケーリングするとは、レプリカの数を増減することです。

始める前に

  • StatefulSetはKubernetesバージョン1.5以降でのみ利用可能です。 Kubernetesのバージョンを確認するには、kubectl versionを実行してください。

  • すべてのステートフルアプリケーションがうまくスケールできるわけではありません。StatefulSetがスケールするかどうかわからない場合は、StatefulSetの概念またはStatefulSetのチュートリアルを参照してください。

  • ステートフルアプリケーションクラスターが完全に健全であると確信できる場合にのみ、スケーリングを実行してください。

StatefulSetのスケール

kubectlを使用したStatefulSetのスケール

まず、スケールしたいStatefulSetを見つけます。

kubectl get statefulsets <stateful-set-name>

StatefulSetのレプリカ数を変更します:

kubectl scale statefulsets <stateful-set-name> --replicas=<new-replicas>

StatefulSetのインプレースアップデート

コマンドライン上でレプリカ数を変更する代わりに、StatefulSetにインプレースアップデートが可能です。

StatefulSetが最初に kubectl applyで作成されたのなら、StatefulSetマニフェストの.spec.replicasを更新してから、kubectl applyを実行します:

kubectl apply -f <stateful-set-file-updated>

そうでなければ、kubectl editでそのフィールドを編集してください:

kubectl edit statefulsets <stateful-set-name>

あるいはkubectl patchを使ってください:

kubectl patch statefulsets <stateful-set-name> -p '{"spec":{"replicas":<new-replicas>}}'

トラブルシューティング

スケールダウンがうまくいかない

管理するステートフルPodのいずれかが異常である場合、StatefulSetをスケールダウンすることはできません。それらのステートフルPodが実行され準備ができた後にのみ、スケールダウンが行われます。

spec.replicas > 1の場合、Kubernetesは不健康なPodの理由を判断できません。それは、永続的な障害または一時的な障害の結果である可能性があります。一時的な障害は、アップグレードまたはメンテナンスに必要な再起動によって発生する可能性があります。

永続的な障害が原因でPodが正常でない場合、障害を修正せずにスケーリングすると、StatefulSetメンバーシップが正しく機能するために必要な特定の最小レプリカ数を下回る状態になる可能性があります。これにより、StatefulSetが利用できなくなる可能性があります。

一時的な障害によってPodが正常でなくなり、Podが再び使用可能になる可能性がある場合は、一時的なエラーがスケールアップまたはスケールダウン操作の妨げになる可能性があります。一部の分散データベースでは、ノードが同時に参加および脱退するときに問題があります。このような場合は、アプリケーションレベルでスケーリング操作を考えることをお勧めします。また、ステートフルアプリケーションクラスタが完全に健全であることが確実な場合にのみスケーリングを実行してください。

次の項目

5 - StatefulSetの削除

このタスクでは、StatefulSetを削除する方法を説明します。

始める前に

  • このタスクは、クラスター上で、StatefulSetで表現されるアプリケーションが実行されていることを前提としています。

StatefulSetの削除

Kubernetesで他のリソースを削除するのと同じ方法でStatefulSetを削除することができます。つまり、kubectl deleteコマンドを使い、StatefulSetをファイルまたは名前で指定します。

kubectl delete -f <file.yaml>
kubectl delete statefulsets <statefulset-name>

StatefulSet自体が削除された後で、関連するヘッドレスサービスを個別に削除する必要があるかもしれません。

kubectl delete service <service-name>

kubectlを使ってStatefulSetを削除すると0にスケールダウンされ、すべてのPodが削除されます。PodではなくStatefulSetだけを削除したい場合は、--cascade=orphanを使用してください。

kubectl delete -f <file.yaml> --cascade=orphan

--cascade=orphankubectl deleteに渡すことで、StatefulSetオブジェクト自身が削除された後でも、StatefulSetによって管理されていたPodは残ります。Podにapp=myappというラベルが付いている場合は、次のようにして削除できます:

kubectl delete pods -l app=myapp

永続ボリューム

StatefulSet内のPodを削除しても、関連付けられているボリュームは削除されません。これは、削除する前にボリュームからデータをコピーする機会があることを保証するためです。Podが終了した後にPVCを削除すると、ストレージクラスと再利用ポリシーによっては、背後にある永続ボリュームの削除がトリガーされることがあります。決してクレーム削除後にボリュームにアクセスできると想定しないでください。

StatefulSetの完全削除

関連付けられたPodを含むStatefulSet内のすべてのものを単純に削除するには、次のような一連のコマンドを実行します:

grace=$(kubectl get pods <stateful-set-pod> --template '{{.spec.terminationGracePeriodSeconds}}')
kubectl delete statefulset -l app=myapp
sleep $grace
kubectl delete pvc -l app=myapp

上の例では、Podはapp=myappというラベルを持っています。必要に応じてご利用のラベルに置き換えてください。

StatefulSet Podの強制削除

StatefulSet内の一部のPodが長期間TerminatingまたはUnknown状態のままになっていることが判明した場合は、手動でapiserverからPodを強制的に削除する必要があります。これは潜在的に危険な作業です。詳細はStatefulSet Podの強制削除を参照してください。

次の項目

StatefulSet Podの強制削除の詳細

6 - StatefulSet Podの強制削除

このページでは、StatefulSetの一部であるPodを削除する方法と、削除する際に考慮すべき事項について説明します。

始める前に

  • これはかなり高度なタスクであり、StatefulSetに固有のいくつかの特性に反する可能性があります。
  • 先に進む前に、以下に列挙されている考慮事項をよく理解してください。

StatefulSetに関する考慮事項

StatefulSetの通常の操作では、StatefulSet Podを強制的に削除する必要はまったくありません。StatefulSetコントローラーは、StatefulSetのメンバーの作成、スケール、削除を行います。それは序数0からN-1までの指定された数のPodが生きていて準備ができていることを保証しようとします。StatefulSetは、クラスター内で実行されている特定のIDを持つ最大1つのPodがいつでも存在することを保証します。これは、StatefulSetによって提供される最大1つのセマンティクスと呼ばれます。

手動による強制削除は、StatefulSetに固有の最大1つのセマンティクスに違反する可能性があるため、慎重に行う必要があります。StatefulSetを使用して、安定したネットワークIDと安定した記憶域を必要とする分散型およびクラスター型アプリケーションを実行できます。これらのアプリケーションは、固定IDを持つ固定数のメンバーのアンサンブルに依存する構成を持つことがよくあります。同じIDを持つ複数のメンバーを持つことは悲惨なことになり、データの損失につながる可能性があります(例:定足数ベースのシステムでのスプリットブレインシナリオ)。

Podの削除

次のコマンドで正常なPod削除を実行できます:

kubectl delete pods <pod>

上記がグレースフルターミネーションにつながるためには、pod.Spec.TerminationGracePeriodSecondsに0を指定してはいけませんpod.Spec.TerminationGracePeriodSecondsを0秒に設定することは安全ではなく、StatefulSet Podには強くお勧めできません。グレースフル削除は安全で、kubeletがapiserverから名前を削除する前にPodが適切にシャットダウンすることを保証します。

Kubernetes(バージョン1.5以降)は、Nodeにアクセスできないという理由だけでPodを削除しません。到達不能なNodeで実行されているPodは、タイムアウトの後にTerminatingまたはUnknown状態になります。到達不能なNode上のPodをユーザーが適切に削除しようとすると、Podはこれらの状態に入ることもあります。そのような状態のPodをapiserverから削除することができる唯一の方法は以下の通りです:

  • (ユーザーまたはNode Controllerによって)Nodeオブジェクトが削除されます。
  • 応答していないNodeのkubeletが応答を開始し、Podを終了してapiserverからエントリーを削除します。
  • ユーザーによりPodを強制削除します。

推奨されるベストプラクティスは、1番目または2番目のアプローチを使用することです。Nodeが死んでいることが確認された(例えば、ネットワークから恒久的に切断された、電源が切られたなど)場合、Nodeオブジェクトを削除します。Nodeがネットワークパーティションに苦しんでいる場合は、これを解決するか、解決するのを待ちます。パーティションが回復すると、kubeletはPodの削除を完了し、apiserverでその名前を解放します。

通常、PodがNode上で実行されなくなるか、管理者によってそのNodeが削除されると、システムは削除を完了します。あなたはPodを強制的に削除することでこれを無効にすることができます。

強制削除

強制削除はPodが終了したことをkubeletから確認するまで待ちません。強制削除がPodの削除に成功したかどうかに関係なく、apiserverから名前をすぐに解放します。これにより、StatefulSetコントローラーは、同じIDを持つ交換Podを作成できます。これは、まだ実行中のPodの複製につながる可能性があり、そのPodがまだStatefulSetの他のメンバーと通信できる場合、StatefulSetが保証するように設計されている最大1つのセマンティクスに違反します。

StatefulSetのPodを強制的に削除するということは、問題のPodがStatefulSet内の他のPodと再び接触することはなく、代わりのPodを作成するために名前が安全に解放されることを意味します。

バージョン1.5以上のkubectlを使用してPodを強制的に削除する場合は、次の手順を実行します:

kubectl delete pods <pod> --grace-period=0 --force

バージョン1.4以下のkubectlを使用している場合、--forceオプションを省略する必要があります:

kubectl delete pods <pod> --grace-period=0

これらのコマンドを実行した後でもPodがUnknown状態のままになっている場合は、次のコマンドを使用してPodをクラスターから削除します:

kubectl patch pod <pod> -p '{"metadata":{"finalizers":null}}'

StatefulSet Podの強制削除は、常に慎重に、関連するリスクを完全に把握して実行してください。

次の項目

StatefulSetのデバッグの詳細

7 - Horizontal Pod Autoscalerウォークスルー

Horizontal Pod Autoscalerは、Deployment、ReplicaSetまたはStatefulSetといったレプリケーションコントローラー内のPodの数を、観測されたCPU使用率(もしくはベータサポートの、アプリケーションによって提供されるその他のメトリクス)に基づいて自動的にスケールさせます。

このドキュメントはphp-apacheサーバーに対しHorizontal Pod Autoscalerを有効化するという例に沿ってウォークスルーで説明していきます。Horizontal Pod Autoscalerの動作についてのより詳細な情報を知りたい場合は、Horizontal Pod Autoscalerユーザーガイドをご覧ください。

始める前に

この例ではバージョン1.2以上の動作するKubernetesクラスターおよびkubectlが必要です。 Metrics APIを介してメトリクスを提供するために、Metrics serverによるモニタリングがクラスター内にデプロイされている必要があります。 Horizontal Pod Autoscalerはメトリクスを収集するためにこのAPIを利用します。metrics-serverをデプロイする方法を知りたい場合はmetrics-server ドキュメントをご覧ください。

Horizontal Pod Autoscalerで複数のリソースメトリクスを利用するためには、バージョン1.6以上のKubernetesクラスターおよびkubectlが必要です。カスタムメトリクスを使えるようにするためには、あなたのクラスターがカスタムメトリクスAPIを提供するAPIサーバーと通信できる必要があります。 最後に、Kubernetesオブジェクトと関係のないメトリクスを使うにはバージョン1.10以上のKubernetesクラスターおよびkubectlが必要で、さらにあなたのクラスターが外部メトリクスAPIを提供するAPIサーバーと通信できる必要があります。 詳細についてはHorizontal Pod Autoscaler user guideをご覧ください。

php-apacheの起動と公開

Horizontal Pod Autoscalerのデモンストレーションのために、php-apacheイメージをもとにしたカスタムのDockerイメージを使います。 このDockerfileは下記のようになっています。

FROM php:5-apache
COPY index.php /var/www/html/index.php
RUN chmod a+rx index.php

これはCPU負荷の高い演算を行うindex.phpを定義しています。

<?php
  $x = 0.0001;
  for ($i = 0; $i <= 1000000; $i++) {
    $x += sqrt($x);
  }
  echo "OK!";
?>

まず最初に、イメージを動かすDeploymentを起動し、Serviceとして公開しましょう。 下記の設定を使います。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      run: php-apache
  replicas: 1
  template:
    metadata:
      labels:
        run: php-apache
    spec:
      containers:
      - name: php-apache
        image: k8s.gcr.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
  labels:
    run: php-apache
spec:
  ports:
  - port: 80
  selector:
    run: php-apache

以下のコマンドを実行してください。

kubectl apply -f https://k8s.io/examples/application/php-apache.yaml
deployment.apps/php-apache created
service/php-apache created

Horizontal Pod Autoscalerを作成する

サーバーが起動したら、kubectl autoscaleを使ってautoscalerを作成しましょう。以下のコマンドで、最初のステップで作成したphp-apache deploymentによって制御されるPodレプリカ数を1から10の間に維持するHorizontal Pod Autoscalerを作成します。 簡単に言うと、HPAは(Deploymentを通じて)レプリカ数を増減させ、すべてのPodにおける平均CPU使用率を50%(それぞれのPodはkubectl runで200 milli-coresを要求しているため、平均CPU使用率100 milli-coresを意味します)に保とうとします。 このアルゴリズムについての詳細はこちらをご覧ください。

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
horizontalpodautoscaler.autoscaling/php-apache autoscaled

以下を実行して現在のAutoscalerの状況を確認できます。

kubectl get hpa
NAME         REFERENCE                     TARGET    MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   0% / 50%  1         10        1          18s

現在はサーバーにリクエストを送っていないため、CPU使用率が0%になっていることに注意してください(TARGETカラムは対応するDeploymentによって制御される全てのPodの平均値を示しています。)。

負荷の増加

Autoscalerがどのように負荷の増加に反応するか見てみましょう。 コンテナを作成し、クエリの無限ループをphp-apacheサーバーに送ってみます(これは別のターミナルで実行してください)。

kubectl run -i --tty load-generator --rm --image=busybox --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

数分以内に、下記を実行することでCPU負荷が高まっていることを確認できます。

kubectl get hpa
NAME         REFERENCE                     TARGET      MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   305% / 50%  1         10        1          3m

ここでは、CPU使用率はrequestの305%にまで高まっています。 結果として、Deploymentはレプリカ数7にリサイズされました。

kubectl get deployment php-apache
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
php-apache   7/7      7           7           19m

負荷の停止

ユーザー負荷を止めてこの例を終わらせましょう。

私たちがbusyboxイメージを使って作成したコンテナ内のターミナルで、<Ctrl> + Cを入力して負荷生成を終了させます。

そして結果の状態を確認します(数分後)。

kubectl get hpa
NAME         REFERENCE                     TARGET       MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   0% / 50%     1         10        1          11m
kubectl get deployment php-apache
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
php-apache   1/1     1            1           27m

ここでCPU使用率は0に下がり、HPAによってオートスケールされたレプリカ数は1に戻ります。

複数のメトリクスやカスタムメトリクスを基にオートスケーリングする

autoscaling/v2beta2 APIバージョンと使うと、php-apache Deploymentをオートスケーリングする際に使う追加のメトリクスを導入することが出来ます。

まず、autoscaling/v2beta2内のHorizontalPodAutoscalerのYAMLファイルを入手します。

kubectl get hpa.v2beta2.autoscaling -o yaml > /tmp/hpa-v2.yaml

/tmp/hpa-v2.yamlファイルをエディタで開くと、以下のようなYAMLファイルが見えるはずです。

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
status:
  observedGeneration: 1
  lastScaleTime: <some-time>
  currentReplicas: 1
  desiredReplicas: 1
  currentMetrics:
  - type: Resource
    resource:
      name: cpu
      current:
        averageUtilization: 0
        averageValue: 0

targetCPUUtilizationPercentageフィールドはmetricsと呼ばれる配列に置換されています。 CPU使用率メトリクスは、Podコンテナで定められたリソースの割合として表されるため、リソースメトリクスです。CPU以外のリソースメトリクスを指定することもできます。デフォルトでは、他にメモリだけがリソースメトリクスとしてサポートされています。これらのリソースはクラスター間で名前が変わることはなく、そしてmetrics.k8s.io APIが利用可能である限り常に利用可能です。

さらにtarget.typeにおいてUtilizationの代わりにAverageValueを使い、target.averageUtilizationフィールドの代わりに対応するtarget.averageValueフィールドを設定することで、リソースメトリクスをrequest値に対する割合に代わり、直接的な値に設定することも可能です。

PodメトリクスとObjectメトリクスという2つの異なる種類のメトリクスが存在し、どちらもカスタムメトリクスとみなされます。これらのメトリクスはクラスター特有の名前を持ち、利用するにはより発展的なクラスター監視設定が必要となります。

これらの代替メトリクスタイプのうち、最初のものがPodメトリクスです。これらのメトリクスはPodを説明し、Podを渡って平均され、レプリカ数を決定するためにターゲット値と比較されます。 これらはほとんどリソースメトリクス同様に機能しますが、targetの種類としてはAverageValueのみをサポートしている点が異なります。

Podメトリクスはmetricブロックを使って以下のように指定されます。

type: Pods
pods:
  metric:
    name: packets-per-second
  target:
    type: AverageValue
    averageValue: 1k

2つ目のメトリクスタイプはObjectメトリクスです。これらのメトリクスはPodを説明するかわりに、同一Namespace内の異なったオブジェクトを説明します。このメトリクスはオブジェクトから取得される必要はありません。単に説明するだけです。Objectメトリクスはtargetの種類としてValueAverageValueをサポートします。Valueでは、ターゲットはAPIから返ってきたメトリクスと直接比較されます。AverageValueでは、カスタムメトリクスAPIから返ってきた値はターゲットと比較される前にPodの数で除算されます。以下の例はrequests-per-secondメトリクスのYAML表現です。

type: Object
object:
  metric:
    name: requests-per-second
  describedObject:
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    name: main-route
  target:
    type: Value
    value: 2k

もしこのようなmetricブロックを複数提供した場合、HorizontalPodAutoscalerはこれらのメトリクスを順番に処理します。 HorizontalPodAutoscalerはそれぞれのメトリクスについて推奨レプリカ数を算出し、その中で最も多いレプリカ数を採用します。

例えば、もしあなたがネットワークトラフィックについてのメトリクスを収集する監視システムを持っているなら、kubectl editを使って指定を次のように更新することができます。

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  - type: Pods
    pods:
      metric:
        name: packets-per-second
      target:
        type: AverageValue
        averageValue: 1k
  - type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1beta1
        kind: Ingress
        name: main-route
      target:
        type: Value
        value: 10k
status:
  observedGeneration: 1
  lastScaleTime: <some-time>
  currentReplicas: 1
  desiredReplicas: 1
  currentMetrics:
  - type: Resource
    resource:
      name: cpu
    current:
      averageUtilization: 0
      averageValue: 0
  - type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1beta1
        kind: Ingress
        name: main-route
      current:
        value: 10k

この時、HorizontalPodAutoscalerはそれぞれのPodがCPU requestの50%を使い、1秒当たり1000パケットを送信し、そしてmain-route Ingressの裏にあるすべてのPodが合計で1秒当たり10000パケットを送信する状態を保持しようとします。

より詳細なメトリクスをもとにオートスケーリングする

多くのメトリクスパイプラインは、名前もしくは labels と呼ばれる追加の記述子の組み合わせによって説明することができます。全てのリソースメトリクス以外のメトリクスタイプ(Pod、Object、そして下で説明されている外部メトリクス)において、メトリクスパイプラインに渡す追加のラベルセレクターを指定することができます。例えば、もしあなたがhttp_requestsメトリクスをverbラベルとともに収集しているなら、下記のmetricブロックを指定してGETリクエストにのみ基づいてスケールさせることができます。

type: Object
object:
  metric:
    name: http_requests
    selector: {matchLabels: {verb: GET}}

このセレクターは完全なKubernetesラベルセレクターと同じ文法を利用します。もし名前とセレクターが複数の系列に一致した場合、この監視パイプラインはどのようにして複数の系列を一つの値にまとめるかを決定します。このセレクターは付加的なもので、ターゲットオブジェクト(Podsタイプの場合は対象Pod、Objectタイプの場合は説明されるオブジェクト)ではないオブジェクトを説明するメトリクスを選択することは出来ません。

Kubernetesオブジェクトと関係ないメトリクスに基づいたオートスケーリング

Kubernetes上で動いているアプリケーションを、Kubernetes Namespaceと直接的な関係がないサービスを説明するメトリクスのような、Kubernetesクラスター内のオブジェクトと明確な関係が無いメトリクスを基にオートスケールする必要があるかもしれません。Kubernetes 1.10以降では、このようなユースケースを外部メトリクスによって解決できます。

外部メトリクスを使うにはあなたの監視システムについての知識が必要となります。この設定はカスタムメトリクスを使うときのものに似ています。外部メトリクスを使うとあなたの監視システムのあらゆる利用可能なメトリクスに基づいてクラスターをオートスケールできるようになります。上記のようにmetricブロックでnameselectorを設定し、ObjectのかわりにExternalメトリクスタイプを使います。 もし複数の時系列がmetricSelectorにより一致した場合は、それらの値の合計がHorizontalPodAutoscalerに使われます。 外部メトリクスはValueAverageValueの両方のターゲットタイプをサポートしています。これらの機能はObjectタイプを利用するときとまったく同じです。

例えばもしあなたのアプリケーションがホストされたキューサービスからのタスクを処理している場合、あなたは下記のセクションをHorizontalPodAutoscalerマニフェストに追記し、未処理のタスク30個あたり1つのワーカーを必要とすることを指定します。

- type: External
  external:
    metric:
      name: queue_messages_ready
      selector: "queue=worker_tasks"
    target:
      type: AverageValue
      averageValue: 30

可能なら、クラスター管理者がカスタムメトリクスAPIを保護することを簡単にするため、外部メトリクスのかわりにカスタムメトリクスを用いることが望ましいです。外部メトリクスAPIは潜在的に全てのメトリクスへのアクセスを許可するため、クラスター管理者はこれを公開する際には注意が必要です。

付録: Horizontal Pod Autoscaler status conditions

autoscaling/v2beta2形式のHorizontalPodAutoscalerを使っている場合は、KubernetesによるHorizontalPodAutoscaler上のstatus conditionsセットを見ることができます。status conditionsはHorizontalPodAutoscalerがスケール可能かどうか、そして現時点でそれが何らかの方法で制限されているかどうかを示しています。

このconditionsはstatus.conditionsフィールドに現れます。HorizontalPodAutoscalerに影響しているconditionsを確認するために、kubectl describe hpaを利用できます。

kubectl describe hpa cm-test
Name:                           cm-test
Namespace:                      prom
Labels:                         <none>
Annotations:                    <none>
CreationTimestamp:              Fri, 16 Jun 2017 18:09:22 +0000
Reference:                      ReplicationController/cm-test
Metrics:                        ( current / target )
  "http_requests" on pods:      66m / 500m
Min replicas:                   1
Max replicas:                   4
ReplicationController pods:     1 current / 1 desired
Conditions:
  Type                  Status  Reason                  Message
  ----                  ------  ------                  -------
  AbleToScale           True    ReadyForNewScale        the last scale time was sufficiently old as to warrant a new scale
  ScalingActive         True    ValidMetricFound        the HPA was able to successfully calculate a replica count from pods metric http_requests
  ScalingLimited        False   DesiredWithinRange      the desired replica count is within the acceptable range
Events:

このHorizontalPodAutoscalerにおいて、いくつかの正常な状態のconditionsを見ることができます。まず最初に、AbleToScaleは、HPAがスケール状況を取得し、更新させることが出来るかどうかだけでなく、何らかのbackoffに関連した状況がスケーリングを妨げていないかを示しています。2番目に、ScalingActiveは、HPAが有効化されているかどうか(例えば、レプリカ数のターゲットがゼロでないこと)や、望ましいスケールを算出できるかどうかを示します。もしこれがFalseの場合、大体はメトリクスの取得において問題があることを示しています。最後に、一番最後の状況であるScalingLimitedは、HorizontalPodAutoscalerの最大値や最小値によって望ましいスケールがキャップされていることを示しています。この指標を見てHorizontalPodAutoscaler上の最大・最小レプリカ数制限を増やす、もしくは減らす検討ができます。

付録: 数量

全てのHorizontalPodAutoscalerおよびメトリクスAPIにおけるメトリクスはquantityとして知られる特殊な整数表記によって指定されます。例えば、10500mという数量は10進数表記で10.5と書くことができます。メトリクスAPIは可能であれば接尾辞を用いない整数を返し、そうでない場合は基本的にミリ単位での数量を返します。これはメトリクス値が11500mの間で、もしくは10進法表記で書かれた場合は11.5の間で変動するということを意味します。

付録: その他の起きうるシナリオ

Autoscalerを宣言的に作成する

kubectl autoscaleコマンドを使って命令的にHorizontalPodAutoscalerを作るかわりに、下記のファイルを使って宣言的に作成することができます。

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50

下記のコマンドを実行してAutoscalerを作成します。

kubectl create -f https://k8s.io/examples/application/hpa/php-apache.yaml
horizontalpodautoscaler.autoscaling/php-apache created