Azure Kubernetes Service(AKS)でACIによる仮想ノードを構築する

# はじめに

この記事ではAzure Kubernetes Service(以下、AKS)で、Azure Container Instance(以下、ACI)を利用して仮想ノードを構築する方法を解説します。

# 想定読者
- Kubernetesを利用したことがある方
- Azureが好きな方

# AKSでのワーカーノード

Kubernetesにおけるワーカーノードとは、クラスタにデプロイしたアプリケーション(Podなど)を実行する場所です。

AKSでのワーカーノードを構成する方法として、Virtual Machine Scale Set(以下、VMSS)を利用する方法、ACIを利用する方法が挙げられます。
VMSSによってワーカーノードを構成する場合、展開される各Virtual Machine(以下、VM)に`kubelet`やコンテナランタイム(`containerd`)が搭載されます。
このワーカーノードはVMと同様にあらかじめVMのサイズを決定し、それに沿ったキャパシティのもとアプリケーションを実行できます。
ACIを利用してワーカーノードを構成する場合、`kubelet`の代わりにOSSの[Virtual Kubelet](https://github.com/virtual-kubelet/virtual-kubelet)を利用してACIを仮想的なノードとして構成します。物理的なサーバを持たないことから、これを仮想ノードと呼びます。
VMSSによるワーカーノードと比較すると、以下の利点があります。

- ノードの空きリソースに関係なく、リクエストのバースト時に素早くアプリケーションをスケールアウトできる

  VMSSによるワーカーノードの場合、VMの空きリソースがある場合は素早くスケールアウトできますが、空きリソースがない場合にはノード自体をスケールアウト(VMを新しく起動)する必要があります。
  ACIによるワーカーノードの場合は、ノードの空きリソースに関係なく、VMノードを起動するよりも素早くスケールアウトできます。
  
- OS層のメンテナンスが必要ない

  OS層がマネージドなACIを利用することで管理負荷を下げることができます。
  ただし、仮想ノードのアドオン自体のバージョンアップは必要です。

# ACIを使った仮想ノード

前述の通り、ACIは[Virtual Kubelet](https://github.com/virtual-kubelet/virtual-kubelet)をもとに実装されています。ここではそのアーキテクチャと、仮想ノードを利用するうえでの注意点について説明します。

## アーキテクチャ

Virtual Kubeletを説明する前に、Kubernetesにおける[Kubelet](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/)について簡単に説明します。
Kubeletの役割はノード上で実行されるPodを管理・操作することです。Kubernetesクラスタを構成する各ノード上で実行されるエージェントとして動作します。Kubeletが動作するノードは物理サーバ・仮想サーバを問いません。Kubernetesのapiserverからの入力を受け取り、受け取ったマニフェストの`PodSpec`をもとにノード上のPodを監視し、`PodSpec`通りの動作を続けるよう管理します。

Kubeletを実装したものの１つがVirtual Kubeletで、Kubernetes外のAPIと接続することを目的につくられています。

このVirtual Kubeletと接続するAPIとして、ACIなどのコンテナプラットフォームを選択することで、Kubernetesのノードをサーバレスに構成することができます。

画像は[こちら](https://virtual-kubelet.io/docs/architecture/)から引用

![virtual-kubelet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155066/0e8820f8-35b5-21ec-a5e3-aa1dde7f55d7.png)


## 仮想ノードを利用するうえでの注意点

仮想ノードを利用するうえでの注意点(制約)もあるので、ここで代表的なものを紹介します。

- [少なくともAKSクラスターには1つ以上のVMノードが必要](https://docs.microsoft.com/ja-jp/azure/aks/use-system-pools#system-and-user-node-pools)

AKSでは`CoreDNS`や`metrics-server`のPodを常に起動させておく[システムノードプール](https://docs.microsoft.com/ja-jp/azure/aks/use-system-pools)が必要になります。
システムノードプールはノード数を0にできないため、AKSを構成するワーカーノードをすべて仮想ノードにすることはできません。

- [ACIのVNET上での制約事項](https://docs.microsoft.com/ja-jp/azure/container-instances/container-instances-virtual-network-concepts#other-limitations)

仮想ノードを利用すると、実際にはACIをVNETに配置することになるため、ACIのVNET上での制約事項がそのまま適用されます。

- Azure Container Registry(以下、ACR)からのサービスプリンシパルを利用したイメージのプルができない

仮想ノード上ではサービスプリンシパル(マネージドID含む)を利用してACRに対して認証することができません。仮想ノードでACRにホストされたイメージを利用したい場合、[Kubernetes Secret](https://github.com/virtual-kubelet/azure-aci/blob/master/README.md#private-registry)を利用する必要があります。

- Init Containerが利用できない

Init Containerを利用するワークロードを仮想ノード上で実現できません。VMノードで実行するか、Init Containerを必要としないアプリケーションアーキテクチャを採用する必要があります。

- Kubenetを利用できない

Azure CNIネットワークを利用したAKSクラスタを構築する必要があります。

- DaemonSetのPodは仮想ノードにデプロイされない

DaemonSetのPodがデプロイされるのはVMノードのみです。

その他制約事項は[こちら](https://docs.microsoft.com/ja-jp/azure/aks/virtual-nodes#known-limitations)を参照ください。

# ACIを利用して仮想ノードを構築する

ここからは[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/aks/virtual-nodes-cli)にならって、AKSに仮想ノードを構築してアプリケーションをホストする手順を解説していきます。

## Azure Cloud Shell

この記事では、Azure Cloud Shell(Bash)からAzure CLIを利用します。
Azure CLIからACIを利用するため、以下のコマンドを実行します。

```sh
az provider register --namespace Microsoft.ContainerInstance
```

次のコマンドによりACIのリソースプロバイダが登録されていることを確認します。

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

`RegistrationState`が`Registered`になっていることが確認できればOKです。

## AKSの構築
AKSをリンクを参考に構築します。

- [AKS](https://qiita.com/kk-ishii/items/e0f61e6411d9983a30b3)

  ネットワークの種類はAzure CNIを選択してください。


## AKSへの接続情報取得

以下コマンドを実行し、AKSへの接続情報を取得します。

```sh
az aks get-credentials --resource-group <リソースグループ名> --name <AKSクラスタ名>
```

`kubectl config get-contexts`を実行したときに作成したAKSクラスターが選択されていればOKです。


## マネージドIDの有効化とロールの割り当て

AKSクラスターでマネージドIDを有効化します。

```sh
az aks update -g <リソースグループ名> -n <AKSクラスタ名> --enable-managed-identity
```

`az aks show -g <リソースグループ名> -n <AKSクラスタ名> --query "servicePrincipalProfile"`を実行したとき、`"clientId": "msi"`が確認できればOKです。

次に有効化したマネージドIDに対して、ロールを割り当てます。
仮想ノードでは、AKSクラスターが利用しているVNETに対してACIをデプロイする必要があるため、そのデプロイに必要なロールを割り当てます。
なお、AKSのVNET名はAzureポータルから確認してください。

```sh
# マネージドIDのオブジェクトID取得
principalId=$(az aks show  -g <リソースグループ名> -n <AKSクラスタ名> --query "identity.principalId" -o tsv)
# ノードのリソースグループ取得
nodeRg=$(az aks show  -g <リソースグループ名> -n <AKSクラスタ名> --query "nodeResourceGroup" -o tsv)
# VNETのオブジェクトIDの取得
vnetId=$(az network vnet show --resource-group $nodeRg --name <AKSのVNET名> --query id -o tsv)
# マネージドIDへVNETへのアクセス許可を割り当て
az role assignment create --assignee $principalId --scope $vnetId --role Contributor
```

## 仮想ノード用のサブネットの作成

仮想ノードで利用するACIをデプロイするためのサブネットを作成します。
サブネットのCIDRは、作成されたVNETに合わせて調整してください。

```sh
az network vnet subnet create \
    --resource-group $nodeRg \
    --vnet-name <AKSのVNET名> \
    --name <仮想ノード用サブネット名> \
    --address-prefixes 10.241.0.0/16
```

## 仮想ノードアドオンを有効化

いよいよ仮想ノード用のアドオンを有効化します。
以下コマンドを実行してください。

```sh
az aks enable-addons \
    --resource-group <リソースグループ名> \
    --name <AKSクラスタ名> \
    --addons virtual-node \
    --subnet-name <仮想ノード用のサブネット名>
```

有効化後、`kubectl get pods --namespace kube-system`を実行したときに`aci-connector-linux`で始まるPodがデプロイされていればOKです。

## 仮想ノードにアプリケーションをデプロイ

仮想ノードにアプリケーションをデプロイするため、以下のマニフェスト(`virtual-node.yaml`)を作成します。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aci-helloworld
spec:
  replicas: 1
  selector:
    matchLabels:
      app: aci-helloworld
  template:
    metadata:
      labels:
        app: aci-helloworld
    spec:
      containers:
      - name: aci-helloworld
        image: mcr.microsoft.com/azuredocs/aci-helloworld
        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
```

ポイントは`nodeSelector`と`tolerations`です。

`nodeSelector`はPodが利用するノードを特定のノードに限定するためのもので、ここでは`virtual-kubelet`が指定されていることがわかります。VMノードにはPodを配置せず、仮想ノードにのみ配置するよう定義されています。

次に`tolerations`を説明する前に、`Taint`について説明します。
`Taint`とはノード上で実行するPodを制限するためのものです。仮想ノードで重要なPod(`kube-proxy`など)が配置されるのを避けるため、`Taint`が設定されています。
`tolerations`はこの`Taint`による制約の影響を受けなくすることができるものです。上記マニフェストでは仮想ノードに設定された`Taint`の影響を避けることができます。

`kubectl apply -f virtual-node.yaml`でデプロイします。

## 動作確認

`kubectl get pods -o wide`を実行すると、`aci-helloworld`のPodが`virtual-node-aci-linux`のノードで実行されていることが確認できます。これは仮想ノードであり、コンテナランタイムはACIになります。

実際にAzureポータルからACIのメニューを開くと、マニフェストで定義したコンテナが実行されていることが確認できます。

![aci.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2155066/a4e457e2-f562-5cd5-0480-90238fc8d9d7.png)

次に実際にアプリケーションが動作しているか確認します。
AKSクラスターに次のコマンドを実行して、仮想ノード上のアプリケーションに対して`curl`でアクセスしてみます。
まずはubuntuベースのコンテナをクラスタ内に展開します。

```sh
kubectl run -it --rm testvk --image=mcr.microsoft.com/aks/fundamental/base-ubuntu:v0.0.11
```

コンテナ内で`curl`をインストールします。

```sh
apt-get update && apt-get install -y curl
```

実際に`curl`により、仮想ノード上のアプリケーションにアクセスします。
IPアドレスは、`kubectl get pods`により得られるアドレスを指定してください。

```
curl -L http://<仮想ノード上のPodのIPアドレス>

<html>
<head>
  <title>Welcome to Azure Container Instances!</title>
</head>
...
```

htmlの出力が得られているため、正常にアプリケーションが実行されていることが確認できました。


# おわりに

この記事ではACIを利用して、AKSで仮想ノードを構成する方法を解説しました。
まだまだ制約事項が多く利用場所を選ぶ技術なので、実際のワークロードにそって導入を検討してください。