AKSをTerraformでセットアップしてプライベートレジストリからコンテナをPullするまで

AzureのAKSにコンテナをDocker HubなどからPullするのは、特に設定などしなくてもできますが、もし社内のプライベートレジストリなどにVPN経由で行うと、おそらく初回は失敗する思います。ごく簡単な回避方法をメモしてみました。できるだけ、手作業を省いて、再構築の可能性なども考慮してCLIだけですべてを完成できるよう行ってみました。AzureにはAzure Container Registryのサービスが提供されていますが、今回はすでに会社内でプライベートレジストリがある場合を想定しました。

# TerraformでAKSにクラスターを設定
AKSのクラスターはTerraformを使って設定します。詳しくは、以下をご参考にしてください。

* https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster
* https://docs.microsoft.com/ja-jp/azure/developer/terraform/create-k8s-cluster-with-tf-and-aks


```terraform:aks.tf
resource "azurerm_kubernetes_cluster" "app_aks" {
    name                = "${var.prefix}-${local.cluster_name}"
    resource_group_name = data.azurerm_resource_group.aps_cluster.name
    location            = data.azurerm_resource_group.aps_cluster.location
    dns_prefix          = local.cluster_name
    kubernetes_version  = local.k8s_version
    api_server_authorized_ip_ranges = ["101.xxx.xxx.0/22","124.xx.xxx.xxx/28"]

    node_resource_group = "${var.prefix}-${var.environment}-nodes"

    network_profile {
        network_plugin      = "kubenet"
        pod_cidr            = "171.30.0.0/16"
        service_cidr        = "171.31.0.0/16"
        dns_service_ip      = "171.31.0.10"
        docker_bridge_cidr  = "171.31.0.1/16"
        load_balancer_sku   = "Standard"
        outbound_type       = "userDefinedRouting"
    }

    default_node_pool {
        name                    = local.cluster_name
        vm_size                 = local.vm_size
        type                    = "VirtualMachineScaleSets"
        availability_zones      = [1,2,3]
        node_count              = local.node_count
        os_disk_size_gb         = 50
        vnet_subnet_id          = data.azurerm_subnet.aps_cluster.id
        max_pods                = local.max_pods
        enable_auto_scaling     = false
        enable_node_public_ip   = false
    }

    service_principal {
        client_id     = var.client_id
        client_secret = var.client_secret
    }
    addon_profile {
        oms_agent {
            enabled = false
        }
        
        kube_dashboard {
            enabled = false 
        }
    }
    role_based_access_control {
        enabled = true
        azure_active_directory {
            managed = true
            admin_group_object_ids = [var.mycompany_ad_group]
        }
    }
    tags = local.tags
    depends_on = [
        data.azurerm_route_table.aps_cluster
    ]
}

```

これを設定することによって、ルーティングを設定して自社のVPNとかにつなげたりします。

```
outbound_type       = "userDefinedRouting"
```

この辺りは今回の本題ではないので、詳しいところは省きます。

# コンテナPullのテスト
テスト用のYamlファイルを作成。
Namespaceを作ってそこに`busybox`をデプロイ。これは特に問題がなく立ち上がると思います。

```yaml:aks-test.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: myspace

---
kind: Pod
apiVersion: v1
metadata:
  name: app-test
  namespace: myspace
spec:
  containers:
   - name: app-test-pod
     image: busybox
     command: ["/bin/sh", "-c"]
     args:
      - tail -f /dev/null
---
kind: Pod
apiVersion: v1
metadata:
  name: app-test1
  namespace: myspace
spec:
  containers:
   - name: app-test-pod
     image: containers.name.company/alpine:latest
     command: ["/bin/sh", "-c"]
     args:
      - tail -f /dev/null
```
問題は二つ目のPodです。これは会社のプライベートレジストリで、当然アドレスも会社内で使っているものです。

```
image: containers.name.company/alpine:latest
```
ネットワークなどの設定はすべて正常に行われると仮定しても、AKSでクラスターを作っていきなりこのコンテナをPullしようとしても、おそらく`ImagePullBackOff`のようなエラーが出ると思います。
エラーメッセージとかもあまり親切ではなく、何が問題なのか気付くまでちょっと時間がかかってしまいそうな感じです。

# トラブルシューティング
まず最初にしたのは通信が正常に行えているか。先ほど`busybox`をデプロイしているのでそれを使って

```
telnet containers.name.company 443
```
とかすると、レジストリ向けに443のポートがつながっているか確認できます。

次に`curl`をつかって実際レジストリにアクセスしてみます。`busybox`の`wget`ではTLS認証がテストできないので、新たにテスト用のPodをひとつ作ります。

```yaml:curl.yaml
kind: Pod
apiVersion: v1
metadata:
  name: app-test
  namespace: myspace
spec:
  containers:
   - name: app-test-pod
     image: alpine
     command: ["/bin/sh", "-c"]
     args:
      - tail -f /dev/null
```
先ず`curl`をインストール

```
apk update
apk add curl
```

次に`curl`を使っていTLS認証のテストをします。

```
curl https://containers.name.company

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
```
こんな感じのエラーがきっと出ると思います。書いていることは公開鍵を発行しているlocalの発行元を確認できませんってことです。当然会社のプライベートのアドレスなので、デフォルトでalpineに会社のルート証明書があるはずはありません。

自分だったら、おそらくこのような手順で何が問題か探っていくと思われます。ただ、お気づきの方もいると思いますが、最後の`curl`を使ったテストはあまり問題解決とは直接関係ないことが分かります。コンテナ上から`curl`でアクセスできた場合はポートが開いていて通信ができるということと、レジストリが反応しているということが分かりますが、なぜコンテナをPullできないかまではたどり着けません。

ただ、このテストで出たTLSの認証の失敗が、コンテナをPullしているときにも起こっているのではと言うヒントにはなります。コンテナをPullするのはK8Sのノードであって、その上にあるコンテナではありません。このことからK8Sのノード上に、プライベートレジストリの公開鍵を発行した会社のルート証明書がないのが原因ではと想像できます。

# kubectlを使ってルート証明書をK8Sのシークレットにデプロイ
次にAKSのクラスターにいくつかリソースをデプロイします。本当は`cloud-init`をつかって以下のことができればいいのですが、やり方が見つかりませんでした。ご存知の方がいたら、コメント欄で教えていただければありがたいです。

```
kubectl -n myspace create secret generic company-ca --from-file=CompanyCA.crt
kubectl -n myspace create secret generic company-global-ca --from-file=CompanyGlobalRootCA.crt
```
2つのルート証明書をK8Sのシークレットにデプロイします。これは`containers.name.company`の公開鍵が`CompanyCA`で発行され、`CompanyCA`のルート証明書が`CompanyGlobalRootCA`であるためです。
このルート証明書がK8SのノードにないためにPullをする時に、`SSL certificate problem`のようなエラーがノード側で発生して正常に動作していないからと考えられるからです。レジストリ側で中間CA`CompanyCA`を正しく設定されていれば、本来なら`CompanyCA`をデプロイする必要はありませんが、これがあると他のシステムとかで中間CAの設定がされていないような場合でも対処できたりします。


# ルート証明書をK8Sのノードにインストール
最後に、K8Sのシークレットにデプロイしたルート証明書をK8Sのノードにインストールします。最初のTerraformで`linux_profile`を使ってユーザを設定していませんので、基本的にノードに直接sshなどでアクセスすることはないというのが前提です。これはセキュリティ対策のポリシー次第ですが、必要のないアクセスは設定しない方がセキュリティ的には良いかと思います。

```yaml:root-ca.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: root-ca
  namespace: myspace 
  labels:
    k8s-app: root-ca
spec:
  selector:
    matchLabels:
      name: root-ca
  template:
    metadata:
      labels:
        name: root-ca
    spec:
      containers:
      - name: root-ca
        image: busybox
        command: ["/bin/sh", "-c"]
        args:
         - /bin/cp /home/ca1/companyCA_G2.crt /etc/docker/certs.d/containers.name.company/company-ca.crt;
           /bin/cp /home/ca2/CompanyGlobalRootCA.crt /etc/docker/certs.d/containers.name.company/company-global-ca.crt;
           find /etc/docker;
           tail -f /dev/null
        volumeMounts:
        - name: etc-docker
          mountPath: /etc/docker/certs.d/containers.name.company
        - name: company-ca
          mountPath: /home/ca1
        - name: company-global-ca
          mountPath: /home/ca2
      volumes:
      - name: etc-docker
        hostPath:
          path: /etc/docker/certs.d/containers.name.company
      - name: company-ca
        secret:
          secretName: company-ca
      - name: company-global-ca
        secret:
          secretName: company-global-ca   
```

Daemonsetを使うことによってすべてのノードにPodが配置され、コマンドで設定されている`/etc/docker/certs.d/containers.name.company`にルート証明書がコピーされます。

Podが正常に起動したら、ログにコピーされたルート証明書のリストが表示されるので、それですべてのノードにインストールされたかどうか確認できます。

ここまで終えると、先ほどPullに失敗していたコンテナも正常に起動すはずです。

```
image: containers.name.company/alpine:latest
```

# 最後に
このDaemonsetは、この後は必要はないので消去します。

```
kubectl -n myspace delete ds root-ca
```
AKSで使われるノードにプライベートイメージを使うことができれば、プライベートイメージに最初からルート証明書をインストールしておけば手間は省けるのですが、みたところプライベートイメージをAKSで使うことはサポートされていないようです。

# 参考文献
* https://github.com/coreos/tectonic-docs/blob/master/Documentation/admin/add-registry-cert.md
* https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster
* https://docs.microsoft.com/ja-jp/azure/developer/terraform/create-k8s-cluster-with-tf-and-aks