エンジニアHubproduced by エン

若手Webエンジニアのための情報メディア

Kubernetesのモダンな活用法 - 設計メソッドと、Virtual Kubeletで実現するサーバーレス化を学ぼう

Kubernetesはここ数年で一気にユーザーを増やしたコンテナオーケストレーターですが、一般化にともない、その活用法も洗練されてきました。本稿では「The Twelve-Factor Appを援用したKubernetes設計」と「Virtual Kubeletを活用したKubernetesのサーバーレス化」という、比較的新しい2つの活用法を武井宜行さんが解説します。

f:id:blog-media:20200806184924j:plain

こんにちは。サイオステクノロジー株式会社でエンジニアをしております武井宜行(タケイ・ノリユキ/ @noriyukitakeiと申します。本稿では、比較的新しいKubernetesの活用法とその実践を紹介します。

Kubernetesは、ご存じの通り、ここ数年で一気に認知度が向上したコンテナオーケストレーターであり、コンテナをプロダクション環境で動作させるための様々な機能(Rolling Updateや負荷分散など)を持ったオープンソースソフトウェアです。

Kubernetesはかなり進化のスピードが速く、取り巻く環境も常々変化しています。新しい活用法、といってもさまざまなトピックがあり迷うところですが、本稿では「The Twelve-Factor Appを援用したKubernetes設計」「Kubernetesのサーバーレス化」の2点を特筆すべきポイントとしてピックアップしたいと思います。

「The Twelve-Factor App」に基づく、Kubernetesシステム設計のポイント

まずはKubernetesによるシステム設計のポイントをお伝えします。最近では「The Twelve-Factor App」に従い、Kubernetesの特性を生かした設計が見られるようになってきました。

「The Twelve-Factor App」とは、Herokuのエンジニアによって提唱された、モダンなアプリケーションを開発するための12のベストプラクティスをまとめたものです。もともとKubernetesのために定められた指針ではありませんが、12のうちのいくつかは、Kubernetesの構築に欠かせない重要な要素が含まれており、多くのエンジニアに参照されています。本家サイトより、12の要素を以下に記載します。

  1. コードベース:バージョン管理されている1つのコードベースと複数のデプロイ
  2. 依存関係:依存関係を明示的に宣言し分離する
  3. 設定:設定を環境変数に格納する
  4. バックエンドサービス:バックエンドサービスをアタッチされたリソースとして扱う
  5. ビルド、リリース、実行:ビルド、リリース、実行の3つのステージを厳密に分離する
  6. プロセス:アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する
  7. ポートバインディング:ポートバインディングを通してサービスを公開する
  8. 並行性:プロセスモデルによってスケールアウトする
  9. 廃棄容易性:高速な起動とグレースフルシャットダウンで堅牢性を最大化する
  10. 開発 / 本番一致:開発、ステージング、本番環境をできるだけ一致させた状態を保つ
  11. ログ:ログをイベントストリームとして扱う
  12. 管理プロセス:管理タスクを1回限りのプロセスとして実行する

上記12の中でも、Kubernetesの設計に特に欠かせないのは「設定」「プロセス」「廃棄容易性」「ログ」です。これら4項目について、具体的な事例を交えながら設計の勘どころを紹介します。

【設定】ビルドの手間を減らす、設定ファイルの配置

データベースの接続情報などの設定ファイルは、ソースコードを格納するリポジトリの中に保存するのではなく、環境変数で外部から与えることが望ましいです。もしソースコードに含めてしまうと環境ごとにビルドが必要になってしまうからです。例えば、ステージング用、プロダクション用にビルドが必要となり、これにデモ用など新しい環境が加わると、さらに新しいビルドが必要になってきます。さらに設定ファイルを修正するだけでもビルドが必要となるという憂き目にあってしまいます。
 
実際の運用では、以下の例のようにConfigMapやSecretなどに設定情報を保存し、マニフェストにて環境変数として渡します。

    spec:
      terminationGracePeriodSeconds: 40
      containers:
      - name: wordpress
        image: wordpress:php7.4
        ports:
        - containerPort: 80
        env:
          - name: WORDPRESS_DB_USER
            valueFrom:
              configMapKeyRef:
                name: wp-user
                key: WORDPRESS_DB_USER
          - name: WORDPRESS_DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: wp-secret
                key: WORDPRESS_DB_PASSWORD

【プロセス】Podをステートレスにし、ユーザビリティを確保する

KubernetesのPodは、スケールアウトやスケールイン、ハード障害やPodそのものの障害などによる自律復旧のため、生成や破棄を繰り返すことが前提となっています。このため、セッション情報やデータベースに格納するデータは、Podの外に永続化し、Pod自体はデータを持たない、いわゆるステートレスにするべきとされています。
 
仮にPod内部にセッション情報を持たせてしまうと、負荷の増減の影響でスケールアウト / スケールインし、ユーザーのリクエストが別のPodに振られると、認証のセッションが切れて突然ログイン画面が表示されるという事態になります。
 
実際の運用では、MySQLやRedisなどに永続化します。Azure Kubernetes Serviceの場合は、MySQLのマネージドサービスであるAzure Database for MySQL、RedisのマネージドサービスであるAzure Cache for Redisなどを使うことがあります。

KubernetesのPodステートレス化イメージ

【廃棄容易性】Podを安全に停止する

繰り返しになりますが、Podは生成や破棄を繰り返すことが前提となっています。よって破棄された後は即座に起動する必要があり、またグレースフルシャットダウンにより、サービスを止めることなくPodを起動・破棄することが要求されます。
 
運用での対策として、Podが高速に起動するために、Dockerリポジトリのイメージをできるだけ小さくするという方法が挙げられます。以下は、レイヤーの数を減らすために、できるだけRUNで実施するコマンドを結合したり、aptのキャッシュを削除するなどしてイメージの容量を削減している例です。

RUN apt-get update && apt-get install -y \
    php \
    apache2 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

また、Podを安全に停止する(グレースフルシャットダウン)ために、preStopコマンドを活用します。

KubernetesのPodを終了するシーケンスは以下のようになります。

  1. kubetctlが、Podを終了するためのリクエストをAPI Serverに送信する。
  2. kubeletが、Pod終了のリクエストをAPI Server経由で受け取り、Podの終了処理を開始する。
  3. 「サービスからPodを除外する処理」と「preStop+SIGTERMをPodに送信する」という2つの処理を同時に開始する。これらの処理は完全に非同期で行われる。
  4. 事前に定義したterminationGracePeriodSeconds秒以内に3の処理が終わらなかった場合、PodにSIGKILLが送信されて、強制的に終了される。

より具体的な事例として、Apacheをgracefulに停止する方法(ユーザーのリクエストを途中で切断することなく安全に停止する)を考えてみます。

先述のとおり最終的にはSIGTERMが送信されますが、ApacheはSIGTERMを受け取ると、ユーザーがリクエストを送受信している途中でも強制的にプロセスを終了し切断してしまいます。

これを防ぐために、preStopを使います。preStopを使うと、Podを終了する際の処理を定義でき、Apacheをgracefulにシャットダウンできるのです。

その一連の処理を図解すると以下の通りとなります。

preStopの使用イメージ

最初に説明したように、Podは停止のリクエストを受け取ると、まずサービスから除外する処理を行い、該当のPodにリクエストが届かないようになります。

ただし、除外処理が終わる前にApacheのgraceful shutdownが始まってしまうと、ユーザーのリクエストが処理中に切断されてしまう恐れがあるので、除外処理が終わるであろう時間(約3秒程度)sleepします。

そして、サービス除外の処理によって、停止対象のPodにリクエストが振られなくなると、apachectl –k graceful-stopによって、Apacheはgracefulにシャットダウンされます。

そのあと30秒ほど待ちます。これは、Apacheのリクエストタイムアウトの設定が30秒だったと仮定した場合ですが、Apacheのgraceful shutdownは非同期で行われるので、ここで30秒待機しないと、ユーザーのリクエストが終わる前にSIGTERMが送信され、処理中に強制的に切断されてしまいます。

terminationGracePeriodSecondsについては、Podの終了処理がこの値で定義した秒数以内に終わらないとSIGKILLが送信され強制的に切断されるので、サービス除外の処理(約3秒)+ユーザーのリクエストタイムアウト(30秒)=33秒にプラスアルファして40秒としています。

この設定であればPodを安全に停止でき、廃棄容易性を実現できるのです。

これらの処理を実現するpreStopは以下のように定義します。

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 3; apachectl -k graceful-stop; sleep 30"]

【ログ】Pod破棄に対応したログ出力

前項のとおり、Podは生成と破棄を繰り返すことを前提として設計するべきです。従来のように、ログを個別のファイルに書き込んで、ローテーション……といった処理では、Podが破棄されると同時にそのログも消失してしまいます。
 
よってPodからは標準出力にログを出力し、そのログはflunetdなどのログコレクターに収集、ストレージに出力したり、ログのビジュアライザーなどで閲覧・分析するといった運用がベストです。
 
具体例を説明します。Pod内のアプリケーションのログを標準出力に出力すると、ワーカーノードの/var/log/containers以下にログがファイルとして出力されます。そのログをDaemonSetのPodとして構築したfluentd(ログコレクター)で収集し、Azureのログ管理サービスであるApplication Insightsに送信するといった運用です。

Pod破棄に対応したログ出力のイメージ

Kubernetesの“真の”サーバーレス化ソリューション

現在ではマスターノードやワーカーノードの構築や運用管理が不要な、いわゆるマネージドなKubernetesがいくつかあります。AWSのElastic Kubernetes Service、GCPのGoogle Kubernetes Engine、そしてAzure Kubernetes Serviceもそのひとつです。Azure Kubernetes Service(以下、AKS)を一例に挙げますが、AKSではマスターノードは確かにフルマネージドですが、ワーカーノードの実態は仮想マシンです。そしてAKSの運用では、仮想マシンであることを意識することが必要なケースがあります。

例えば、クラスタ新規構築時に仮想マシンのサイズを指定したり、ワーカーノード(稼働マシン)をスケールさせる際も同様に、スケール上限数などを設定する必要があります。ただし、実際の運用において、未来のワークロードを正確に予測して、仮想マシンの適正サイズや数を事前に設定するというのは、なかなかに困難です。

この問題を解決するために、ワーカーノードさえも意識しなくてよい、本当の意味でのサーバーレスKubernetesを実現するソリューションがいくつか登場しました。その中から今回はVirtual Kubeletを紹介します。

ユーザーを仮想マシン運用から開放するサーバーレスコンテナプラットフォーム

Virtual Kubeletをご説明する前に、その構成の中核をなす「サーバーレスコンテナプラットフォーム」について、説明します。

まずクラウド上でコンテナを稼働させたい場合にはどのようにするでしょうか。すぐ思いつくのは、仮想マシンを立ち上げ、そこにDockerをインストールし、その上でコンテナを立てる、といった方法でしょう。しかし、この方法では、「仮想マシンの運用」が必要になってきます。サーバーレスが隆盛を極める昨今、やはりこの手間は省きたいものです。

「サーバーレスコンテナプラットフォーム」はこうした課題を解決するためのテクノロジーです。利用者はコンテナを稼働させるためのインフラを意識することなく、設定を定義するだけで必要なだけコンテナを稼働させることができます。コンテナを実行する基盤はすべてクラウド側で面倒を見てくれます。

また、サーバーレスコンテナプラットフォームは、コンテナが実行された時間だけ秒単位で課金されます。例えば1日1時間しか動かさないバッチのために、仮想マシンを1日中動作させているのはコストのムダですが、サーバーレスコンテナプラットフォームでは、この場合1日1時間分しか課金がされず、限られたリソースの有効活用が可能です。

サーバーレスコンテナプラットフォームは各社から出揃っており、AWSではAWS Fargate、GCPではServerless containers、そしてAzureではAzure Container Instancesが提供されています。

サーバーレスコンテナプラットフォームを実現するVirtual Kubelet

こうしたサーバーレスコンテナプラットフォームに、Kubeletが実際に行う処理を委ねる仕組みが、KubernetesにおけるKubeletの実装の一つであるVirtual Kubeletです。Kubernetesからはノードのひとつのように見えますが、従来の仮想マシンで構成されていたノードのような実態はありません。

仮想的なノード内にあるVirtual Kubeletが従来のKubeletのように振る舞い、先に紹介したAzure Container InstancesやAWS FargateなどのサーバーレスコンテナプラットフォームにAPIを発行し、Pod(コンテナを格納する最小単位)を作成します。その構成は、下図のようなイメージになります。

仮想的なノード内にあるVirtual Kubeletの振る舞いイメージ

 

例えば、Podを100個増やすとします。仮想マシンで構成されるワーカーノードの場合、Podを100個動かすためには、ワーカーノードをスケールアップしたりスケールアウトしたりと、仮想マシンのサイズや数を意識した運用が必須になります。しかし、Virtual Kubeletの場合は、サーバーレスコンテナプラットフォームが管理するコンピューティングリソースの許す限り、Podを増やせます。必要な作業は下記のコマンド一発です。

kubectl scale --replicas=100 rs/hoge

これだけでPodを100個生成できます。仮想マシンを全く意識せずにPodを作成できるーーこれがサーバーレスと表現される由縁です。

Virtul Kubeletのプロバイダー(Virtual KubeletがAPIを発行する先のサービス)は、上図に記載のAWS FargateやAzure Container Instancesの他に、Alibaba CloudElastic Container Instanceのなど多数のものが利用できます。

Azure Kubernetes Serviceによるサーバーレスの実践

では、サーバーレスをいかにして実現するか。本章では実践編としてAKSを用いて、Kubernetesクラスターを構築するとともに、先程ご紹介した「irtual Kubeletによって、サーバーレスKubernetesを実現する手順を紹介します。

AKSによるKubernetesクラスターの構築

まずはAKSの構築から始めましょう。本実践はAzure CLIを用いて行いますので、以下のリンクを参考に事前にAzure CLIの環境を用意してください。

Azure CLI のインストール | Microsoft Docs

まず、AKSクラスターを格納するリソースグループを作成します。

$ az group create --name myAKSrg --location japaneast

続いて、AKSクラスターが配置される仮想ネットワークを作成します。

$ az network vnet create \
    --resource-group myAKSrg \
    --name myVnet \
    --address-prefixes 10.0.0.0/8 \
    --subnet-name myAKSSubnet \
    --subnet-prefix 10.240.0.0/16

AKSクラスターが仮想ネットワークなどの各種リソースにアクセスできるよう、サービスプリンシパル(サービス専用アカウント)を作成します。

$ az ad sp create-for-rbac --skip-assignment
{
  "appId": "cef76eb3-f743-4a97-8534-03e9388811fc",
  "displayName": "azure-cli-2020-05-30-18-42-00",
  "name": "http://azure-cli-2020-05-30-18-42-00",
  "password": "1d257915-8714-4ce7-a7fb-0e5a5411df7f",
  "tenant": "73f988dd-86f2-41vf-91ad-2e7cd011eb48"
}

また、AKSクラスターが仮想ネットワークにアクセスできるように、先程作成したサービスプリンシパルに適切な権限を付与します。まずは、先程作成した仮想ネットワークのリソースIDを取得します。

$ az network vnet show --resource-group myAKSrg --name myVnet --query id -o tsv
/subscriptions/a2b379a4-5530-4d43-9360-b705cf560d75/resourceGroups/myAKSrg/providers/Microsoft.Network/virtualNetworks/myVnet

以下のコマンドで、サービスプリンシパルに権限を付与します。

$ az role assignment create --assignee <先程作成したサービスプリンシパルのappId> --scope <先程取得した仮想ネットワークのリソースID> --role Contributor

先の手順で作成したAKS用のサブネットに、AKSクラスターをデプロイします。そのために、このサブネットのIDを取得します。

$ az network vnet subnet show --resource-group myAKSrg --vnet-name myVnet --name myAKSSubnet --query id -o tsv

以下のコマンドでAKSクラスターを作成します。

az aks create \
    --resource-group myAKSrg \
    --name myAKSCluster \
    --network-plugin azure \
    --service-cidr 10.0.0.0/16 \
    --dns-service-ip 10.0.0.10 \
    --docker-bridge-address 172.17.0.1/16 \
    --vnet-subnet-id <先程取得したサブネットのID> \
    --service-principal <先程作成したサービスプリンシパルのappId> \
    --client-secret <先程作成したサービスプリンシパルのpassword> \
    -s Standard_B2s

さらに、AKSクラスターに接続するための資格情報を取得します。

$ az aks get-credentials --resource-group myAKSrg --name myAKSCluster

AKSクラスターの作成が完了していることを確認します。以下のようにワーカーノードが3つ表示されるはずです。

$ kubectl get nodes
NAME                                STATUS   ROLES   AGE   VERSION
aks-nodepool1-38191134-vmss000000   Ready    agent   16m   v1.15.11
aks-nodepool1-38191134-vmss000001   Ready    agent   15m   v1.15.11
aks-nodepool1-38191134-vmss000002   Ready    agent   16m   v1.15.11

続いて、このAKSクラスターにアプリケーションをデプロイしてみます。ReplicaSetのリソースを使い、ApacheのDockerイメージを元にしたPodを3つ作成します。さらに、Load Balancer Serviceを作成し、外部からアクセス可能にします。Load Balancer Serviceを作成すると、内部的にはAzure Load Balancerが作成され、Podへのリクエストが外部から到達可能になります。

まず、上記の内容を実現するためのマニフェストを作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aks-sample
spec:
  replicas: 3
  selector:
    matchLabels:
      app: aks-sample
  template:
    metadata:
      labels:
        app: aks-sample
    spec:
      containers:
      - name: aks-sample
        image: httpd:2.4.43
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: aks-sample
spec:
  type: LoadBalancer
  selector:
    app: aks-sample
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

このマニフェストをAKSクラスターに適用します。

$ kubectl apply -f aks-sample.yml

以下のコマンドを実施して、ロードバランサーのIPアドレス(aks-sampleリソースのEXTERNAL-IP)を取得します。

$ kubectl get service
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
aks-sample   LoadBalancer   10.0.164.152   52.140.234.246   80:30863/TCP   46s
kubernetes   ClusterIP      10.0.0.1       <none>           443/TCP        41m

http://<先程取得したロードバランサーのIPアドレス>にアクセスすると、Apacheデフォルトのindex.html(It works!)が表示されます。これでAKSクラスターの構築は完了です。

Virtual KubeletによるKubernetesのサーバーレス化

では、先程作成したAKSクラスターをサーバーレス化してみます。まずはVirtual Kubeletによって仮想ノードを作成します。

過去、Azure Container Instancesを一度も使用していない場合、Azure Container Instancesのサービスをプロバイダー登録する必要があります。登録済みかどうかは以下のコマンドで確認できます。

$ az provider list --query "[?contains(namespace,'Microsoft.ContainerInstance')]" -o table

コマンド実施後、以下のように表示されれば登録済みです。

Namespace                    RegistrationState    RegistrationPolicy
---------------------------  -------------------  --------------------
Microsoft.ContainerInstance  Registered           RegistrationRequired

もし未登録の場合には、以下のコマンドを実行して、プロバイダー登録します。

$ az provider register --namespace Microsoft.ContainerInstance

プロバイダーの登録が完了したら、次に仮想ノード用のサブネットを作成します。

$ az network vnet subnet create \
    --resource-group myAKSrg \
    --vnet-name myVnet \
    --name myVirtualNodeSubnet \
    --address-prefixes 10.241.0.0/16

仮想ノードを有効にするために、以下のコマンドを実施します。先ほど作成した myVirtualNodeSubnetに仮想ノードを作成します。

$ az aks enable-addons \
    --resource-group myAKSrg \
    --name myAKSCluster \
    --addons virtual-node \
    --subnet-name myVirtualNodeSubnet

これで仮想ノードの作成は完了です。実際に仮想ノードが作成されていることを確認してみます。以下のvirtual-node-aci-linuxが作成した仮想ノードです。

$ kubectl get nodes
NAME                                STATUS   ROLES   AGE     VERSION
aks-nodepool1-38191134-vmss000000   Ready    agent   84m     v1.15.11
aks-nodepool1-38191134-vmss000001   Ready    agent   83m     v1.15.11
aks-nodepool1-38191134-vmss000002   Ready    agent   84m     v1.15.11
virtual-node-aci-linux              Ready    agent   5m55s   v1.14.3-vk-azure-aci-v1.2.1.1

この仮想ノードにPodをデプロイしてみましょう。動作確認を簡単にするため、先程AKSクラスター構築の際にデプロイしたPodは以下のコマンドで削除します。

$ kubectl delete deployment.apps/aks-sample

Podをデプロイするために以下のマニフェストを作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aks-sample
spec:
  replicas: 3
  selector:
    matchLabels:
      app: aks-sample
  template:
    metadata:
      labels:
        app: aks-sample
    spec:
      containers:
      - name: aks-sample
        image: httpd:2.4.43
        ports:
        - containerPort: 80
      nodeSelector:
        kubernetes.io/role: agent
        beta.kubernetes.io/os: linux
        type: virtual-kubelet
      tolerations:
      - key: virtual-kubelet.io/provider
        operator: Exists
      - key: azure.com/aci
        effect: NoSchedule

AKSを構築したときのマニフェストとは異なるポイントが2つあります。

ひとつは、nodeSelectorを指定することで、作成された仮想ノードにPodがデプロイされるようにする必要がある点です。

もうひとつは、Kube-proxyなどの重要なPodが仮想ノードにデプロイされないよう、仮想ノードにはTaintsが付与されている点です。よって、仮想ノードにPodをデプロイする場合は、Tolerationsを指定し強制的に仮想ノードにPodをデプロイさせる必要があります。

では、以下のコマンドで先程のマニフェストをデプロイしてみましょう。

$ kubectl apply -f aks-sample-virtual-nodes.yml

ここでPodの状態を取得してみると、3個のPodが仮想ノードにデプロイされているのがわかります。

$ kubectl describe nodes virtual-node-aci-linux
・・・ 略 ・・・
Non-terminated Pods:         (3 in total)
  Namespace                  Name                           CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                  ----                           ------------  ----------  ---------------  -------------  ---
  default                    aks-sample-6d586c44b8-4vrf9    0 (0%)        0 (0%)      0 (0%)           0 (0%)         21m
  default                    aks-sample-6d586c44b8-7n98h    0 (0%)        0 (0%)      0 (0%)           0 (0%)         21m
  default                    aks-sample-6d586c44b8-rwkk4    0 (0%)        0 (0%)      0 (0%)           0 (0%)         25m
・・・ 略 ・・・

http://<先程取得したロードバランサーのIPアドレス>にアクセスすると、Apacheデフォルトのindex.html(It works!)が表示されます。

ここでAzureポータルで、AKSクラスターのリソースグループを見てみます。

AKSクラスターのリソースグループ

マニフェストにおいてReplicaSetsで指定した3つのAzure Container Instancesが作成されています。Virtual KubeletがサーバーレスコンテナプラットフォームにAPIを発行して、Podを作成しているのがわかるかと思います。

まとめ

駆け足でしたが、設計、サーバーレス化の実践までKubernetesの比較的新しい活用ノウハウを紹介してきました。繰り返しになりますがKubernetesは非常に速いスピードで進化しており、その活用法も拡大し続けています。例えば、Iot分野におけるエッジ(IoTセンサー / デバイスと、クラウドサービスの中間に位置し、集計やフィルタリングを実施手からクラウドにデータを送付する機能を持つサーバー)へのKubernetesの導入など、新たなトピックも豊富です。本稿だけでなく、ぜひ、さまざまなKubernetesの最新事例にアンテナを張り、みなさんの開発に役立ててみてください。

武井宜行(たけい・のりゆき) @noriyukitakei

武井宜行
サイオステクノロジー株式会社シニアアーキテクト。Java、PHP、サーバーレスアプリケーション、コンテナ、Azure(Functions、Azure Kubernetes Serviceなど)を得意とする。多数の登壇、ブログ『SIOS Tech.Lab』での記事執筆は年間で100本を超えるなど、発信活動にも注力している。Microsoft MVP for Azure。

関連記事

編集:馮富久(株式会社技術評論社