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
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
をデプロイ。これは特に問題がなく立ち上がると思います。
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をひとつ作ります。
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
のようなエラーがノード側で発生して正常に動作していないからと考えられるからです。レジストリ側で中間CACompanyCA
を正しく設定されていれば、本来ならCompanyCA
をデプロイする必要はありませんが、これがあると他のシステムとかで中間CAの設定がされていないような場合でも対処できたりします。
ルート証明書をK8Sのノードにインストール
最後に、K8Sのシークレットにデプロイしたルート証明書をK8Sのノードにインストールします。最初のTerraformでlinux_profile
を使ってユーザを設定していませんので、基本的にノードに直接sshなどでアクセスすることはないというのが前提です。これはセキュリティ対策のポリシー次第ですが、必要のないアクセスは設定しない方がセキュリティ的には良いかと思います。
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で使うことはサポートされていないようです。