🏊‍♂️

「kubernetes-the-hard-way」実施記録

2021/09/21に公開

はじめに

本記事は、以下inductor様による、kubernetes-the-hard-wayの和訳に基づいて実施した環境構築の手順に関する、2021-09-19時点における自分なりの理解をメモしたものです。
インフラ初学者はこんな誤解すんのか~とか思いながら、明日から後輩・新人に優しくしてあげるきっかけとして読んでいただければ。

本記事の正確性等は一切保証しませんが、理解の誤りや、誤植・手順漏れ等、コメント・ご指摘に関しては何でも頂ければ幸いです。マサカリは優しく投げてください。

https://github.com/inductor/kubernetes-the-hard-way/blob/fork/docs/01-prerequisites.md#tmuxを使った並列なコマンド実行

背景

kubernetesを実運用するにあたって、クラスタを構成する各コンポーネントとコンポーネント間通信(特に証明書周りとネットワーク周り)に関する理解を深めたくなったので実施した。

ついでにCKAの勉強にもなればより嬉しい。

本文

GCP環境構築

GCPの利用自体、かなり歴が浅いのでSDK(gcloud)を利用した環境構築から理解を得た。

gcloud init
gcloud auth logine

Go to the following link in your browser:
    # ここにアクセスすると見慣れたGoogleログイン画面に飛ぶ
    https://accounts.google.com/o/oauth2/auth?xxxxxxxxxxxxxxxxxxxxxxxxx
    
# Googleロvグイン画面から取得したコードを貼り付ける
Enter verification code:

Googleアカウント自体は普段からChrome認証等で使っていて、GCPと紐ついて認証基盤を管理できるのは、楽で良さそう。

tmux

手順上必須ではないとは記載があるものの、今回構築予定のクラスターはController:Worker=3:3の構成で、etcdの設定等の段階で同じ手順を全Controller Nodeに繰り返す、といったことをすると気が狂ってしまうのでほぼ必須。

自分は普段Windowsで使うSSH TerminalとしてはRloginを使っていたが、似たような機能をCLI上で実行できるのは感動的な体験。チートシートを見ながらではあるが、慣れてくるとvimみたいな感じで使いやすい。

02-クライアントツールのインストール

cfssl, cfssljsonkubectlのインストールを行う手順。
オレオレ証明書の生成といえばopensslだと思っていたが、どういう基準で使い分けているんだろう。[要調査]

03-計算資源のプロビジョニング

Network

主にGCP上でのVM/VPCその他のプロビジョニング。
GCPの場合はAWSでいうSecurity Groupに該当する概念がFirewall Rulesみたいなので、Firewall Rulesに各コンポーネントが通信可能と成るようにIngress/Egressを設定する。

gcloud compute firewall-rules create kubernetes-the-hard-way-allow-external
--allow tcp:22,tcp:6443,icmp
--network kubernetes-the-hard-way
--source-ranges 0.0.0.0/0

source rangeが0.0.0.0からのSSHおよびkube-apiserver向け通信を許可する設定を入れている。
外部NW全域からのSSH許可は何となく気持ちが悪いので、MyIP Onlyに設定した。
この先の手順で動かなかったらここも疑う。

Kubernetesの公開IPアドレス

KubernetesのAPIサーバの前面に置かれる外部ロードバランサにアタッチする静的IPアドレスを割り当てます:

とのこと。
Controller:Worker=3:3構成なので、前段にController向けのAPIリクエストを一手に引き受けるロードバランサーを置くっぽい。まだ全体構成が見えてない。

インスタンス

Controller:Worker=3:3構成の合計6台を先程のNW上にSDKを使って立ち上げる。

本実習のインスタンスでは、コンテナランタイムのcontainerdにて推奨されるUbuntu Server 20.04を使用します。

とのことでOSはUbuntu 20.04を使う。
自宅でkubeadmを使ってクラスターを組んだときはUbuntu20.04+Docker(as CRI)として組んだので、推奨がcontainerdだとすると組み直したほうが良いかも。[要調査]

SSHアクセスの設定

GCP上のインスタンスにSSH接続するときは、gcloud compute sshコマンドを用いる。
AWSでは、Session managerを使って、閉域に置かれたサーバー(上のエージェント)とHTTPS経由での疑似SSHを行っていた[要出典]ので似たようなものかと思ったが、わざわざインスタンスにpublic IPを持たせているので、仕組みは違うのかもしれない。

初めてインスタンスに接続するとSSHキーが生成され、プロジェクトまたはインスタンスのメタデータに格納されます (インスタンスへの接続のマニュアルを参照してください)。

実際以下のコマンドを実行すると、アカウントに紐ついたインスタンス名を解決していい感じにSSHキーの発行と公開鍵のアップロードを行ってくれた。

GCPのプロジェクトという単位をもう少しまだ理解できていないところがあるが、今回の手順ではSSHキーは全インスタンスで暗黙的に共有されているみたいで、一台にSSH接続したあとはキーの発行の手順はスキップされた。

gcloud compute ssh controller-0

04-CA証明書のプロビジョニングとTLS証明書の生成

このあたり本当によく理解ってない。多分SSL/TLSの仕組みが怪しい。
ちゃんとそのあたりからまとめて復習すること。

etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxyの各コンポーネントのTLS証明書を生成します。

はなんとなく、Certificate Authority章で作成したローカルの認証局(CA構成ファイル、証明書、および秘密鍵)を使って、各コンポーネント(etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy)と通信する際、通信先が本当に意図したコンポーネントであることを証明するためのクライアント証明書を作るための手順と理解した。

よくわからないのは以下。

> admin用クライアント証明書

急に出てきたadmin概念。おそらくkubernetesクラスターを色々いじれる権限を持ったユーザー。
後ほどadminユーザーを使って諸々やるんだろうので、そのときに理解する。

KubernetesのAPIサーバー用証明書

静的IPアドレスkubernetes-the-hard-wayはKubernetesのAPIサーバー用証明書のSubject Alternative Name(SAN)リストに含まれます。これによって証明書をリモートクライアントで検証できるようになります。

さっき作ったロードバランサー用IPアドレス用の証明書を作るらしいが、

  • Subject Alternative Name(SAN)リスト
  • 証明書をリモートクライアントで検証
    のあたりがよくわからない。
    自分(主体たるNode)以外のhostnameを名乗れるみたいな仕組み?

サービスアカウントのキーペア

Kubernetesのコントローラーマネージャーは、サービスアカウントの管理に関するドキュメントで説明されているように、キーペアを使用してサービスアカウントトークンを生成して署名します。

らしい。サービスアカウントって何?
本実習の中で出てきたらここに戻って追記する。

05-認証用Kubernetes設定ファイルの生成

本実習ではKubernetesのコンフィグファイル(kubeconfigとも呼ばれます)を生成します。これにより、KubernetesクライアントがKubernetesのAPIサーバーを特定して認証できるようになります。

kubeconfigファイルは、以下公式の記述を自分なりに解釈した感じだと、コンポーネントが動作するにあたって、どのクライアント証明書を使うか、どのユーザー(+クラスター=コンテキスト)で動作するか、Controller NodeのIPアドレスはどこか、といった情報が記載されたコンフィグファイルになると理解した。

kubeconfigを使用すると、クラスターに、ユーザー、名前空間、認証の仕組みに関する情報を組織できます。kubectlコマンドラインツールはkubeconfigファイルを使用してクラスターを選択するために必要な情報を見つけ、クラスターのAPIサーバーと通信します。

さっき作ったetcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxyあたりのクライアント証明書を、各Controller/Worker Nodeにアップロードしなかったのは、ここでそれら証明書を内包するkubeconfigファイルを作ってアップロードするからだろう。

実際、kubeconfigファイルの生成にあたっては、以下に示すように認証局とかHTTPS通信(kubectl APIリクエスト)先の情報、ユーザー名やdefault contextの作成とdefault contextを使う指示が記載されてたりする。

kubeletは、Node Authorizerとかいう仕組みによって認可を受けるために、ユーザーはsystem:node:${instance}という名前になっていないといけないんだとか。

for instance in worker-{0..2}; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-credentials system:node:${instance} \
    --client-certificate=${instance}.pem \
    --client-key=${instance}-key.pem \
    --embed-certs=true \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

  kubectl config use-context default --kubeconfig=${instance}.kubeconfig
done

06-データ暗号化の設定とキーの生成

Kubernetesはクラスタの状態、アプリケーションの構成、機密情報など、さまざまなデータを保存します。Kubernetesはクラスタデータを暗号化する機能をサポートします。

はい。

クラスタ全体の(コンテキスト全体の?)暗号化の設定は、EncryptionConfigを適用することで実施できるらしい。
ここでは作っただけでまだ適用はしてない。

07-etcdクラスターのブートストラップ

クラスターの状態を格納するetcdを、controller node上にHA構成で配置する。
ここの手順は、3台のController Nodeに対して同様に行う操作なので、tmuxを使う。

tmuxのチートシートは以下を参照した。
set-window-option synchronize-panes on/offの話が欲しかったので非常に助かりました。

https://wiki.rookie-inc.com/serverapps/tmux

以下のコマンド実行はおそらく誤植で、後段の処理とVersionの整合性がないので注意。(★メモ:重箱の隅をつつくようで恐縮ではあるが、プルリクは出すように)

wget -q --show-progress --https-only --timestamping \ "https://github.com/etcd-io/etcd/releases/download/v3.4.10/etcd-v3.4.10-linux-amd64.tar.gz"

GCP上のVMは、Instanceのmetadataに内部IPアドレスを持つので、以下手順を踏む。

クライアントリクエストの処理とetcdクラスター・ピアとの通信にはインスタンスの内部IPアドレスが使用されます。以下のコマンドで現在作業中のインスタンスが持つ内部IPアドレスを取得します:

INTERNAL_IP=$(curl -s -H "Metadata-Flavor: Google" \
  ehttp://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip)

systemdユニットファイルetcd.serviceを作成

systemdユニットファイルと言う存在を知らなかった。そもそもsystemdのことがよくわかってない、なんかApacheとかDockerのサービスを上げるときにsystemctlを打つけど、その仲間か?[要調査]

cert-file=/etc/etcd/kubernetes.pemのように、etcdが用いる証明書ファイルとしては前段で作った『KubernetesのAPIサーバー用証明書』を使うらしい。このkubernetes.pemが他に共通して出てくるコンポーネントが後々わかれば、たくさん作った証明書の用途も整理できるかも。

etcdの証明書とかセキュアなファイルや変更されるべきではない設定ファイル関連は/etc/etcd配下に置いてるらしいので、それに従う。

cat <<EOF | sudo tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd
Documentation=https://github.com/coreos

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\
  --cert-file=/etc/etcd/kubernetes.pem \\
  --key-file=/etc/etcd/kubernetes-key.pem \\
  --peer-cert-file=/etc/etcd/kubernetes.pem \\
  --peer-key-file=/etc/etcd/kubernetes-key.pem \\
  --trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-client-cert-auth \\
  --client-cert-auth \\
  --initial-advertise-peer-urls https://${INTERNAL_IP}:2380 \\
  --listen-peer-urls https://${INTERNAL_IP}:2380 \\
  --listen-client-urls https://${INTERNAL_IP}:2379,https://127.0.0.1:2379 \\
  --advertise-client-urls https://${INTERNAL_IP}:2379 \\
  --initial-cluster-token etcd-cluster-0 \\
  --initial-cluster controller-0=https://10.240.0.10:2380,controller-1=https://10.240.0.11:2380,controller-2=https://10.240.0.12:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

長いが、エラーメッセージが結構親切なので本来必要なoptionが欠けてるとわかりやすい。

systemd等のkubernetesの外側の概念に関して理解できないところに目をつぶれば(つぶってはいけないが)、etcdのクラスターを構成できた。
上記で--initial-cluster controller-0=https://10.240.0.10:2380,controller-1=https://10.240.0.11:2380,controller-2=https://10.240.0.12:2380を打ったことで各Controller Node上のetcdがよしなにしてくれたんだろう。かなり便利。

sudo ETCDCTL_API=3 etcdctl member list \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/kubernetes.pem \
  --key=/etc/etcd/kubernetes-key.pem
  
  
3a57933972cb5131, started, controller-2, https://10.240.0.12:2380, https://10.240.0.12:2379, false
f98dc20bce6225a0, started, controller-0, https://10.240.0.10:2380, https://10.240.0.10:2379, false
ffed16798470cab5, started, controller-1, https://10.240.0.11:2380, https://10.240.0.11:2379, false

ちなみに実習の教本ドキュメントと、実行結果が全く一致(etcdのクラスタメンバーIDが一致)したので、メンバーIDはコンフィグや実行環境から片方向的かつ一意に生成される値っぽい。

etcdのバックアップ・リストアは以下の手順でできた。
etcdctlのコマンドのいずれにおいても、各etcd向けの通信がすべてSSL/TLS暗号化されるため、証明書をくっつけて送るためにcert/key/cacert/endpointsの指定が必要らしい。めんどい。

sudo ETCDCTL_API=3 etcdctl snapshot save etcd-snapshot-0919 \
--cert=/etc/etcd/kubernetes.pem \
--key=/etc/etcd/kubernetes-key.pem \
--cacert=/etc/etcd/ca.pem \
--endpoints=https://10.240.0.10:2379
sudo ETCDCTL_API=3 etcdctl snapshot restore etcd-snapshot-0919 \
--cert=/etc/etcd/kubernetes.pem \
--key=/etc/etcd/kubernetes-key.pem \
--cacert=/etc/etcd/ca.pem \
--endpoints=https://10.240.0.10:2379

08-Kubernetesコントロールプレーンのブートストラップ

本実習では、3つのインスタンスでコントロールプレーンをブートストラップして可用性の高い構成を実現します。また、KubernetesのAPIサーバーをリモートクライアントに公開する外部ロードバランサも作成します。各ノードには、Kubernetes API Server、Scheduler、Controller Managerの各コンポーネントがインストールされます。

本実習ではController NodeにKubeletやKubeProxyを置かないので、Controller Node上にworker podsを配置することはできない?[要確認]

引き続きcontroller node全部における作業なのでtmuxを使う。

Kubernetes APIサーバーの設定

出た。
ca.pem ca-key.pem kubernetes-key.pem kubernetes.pemはすべて/var/lib/kubernetes下に移動した。

/var/lib/kubernetesは各種鍵ファイル、ではさっき作った/etc/kubernetes/configにはkubeconfigとかを置くのだろうか。

ca.pem kubernetes-key.pem kubernetes.pemは、etcdも使うファイル(etcd用には/etc/etcd下に置いたが)で、共通の証明書を使う。

色んな所にファイルが置かれるので、証明書をローテーションするなんて作業をやるときは大変そう。

{
  sudo mkdir -p /var/lib/kubernetes/

  sudo mv ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
    service-account-key.pem service-account.pem \
    encryption-config.yaml /var/lib/kubernetes/
}

Kubernetesコントロールプレーンのプロビジョニング

REGIONをインスタンスの(プロジェクトの)メタデータから取得するのに失敗してしまう。
01-前提条件における下記手順の設定漏れで、Projectの設定が正しく行われていないと思われる。

次に、デフォルトのリージョンを設定します:

gcloud config set compute/region us-west1

ダサいかもしれないが、GCPのコンソール画面から設定して事なきを得る。

https://console.cloud.google.com/compute/settings

REGION=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/project/attributes/google-compute-default-region)

各種コンポーネントのsystemdユニットファイル作成

etcd同様に、systemctlで管理するkube-apiserverを起動するための設定ファイルを作る。(kube-apiserver, kube-controller-manager, kube-schedulerで同様の手順を行う)

kube-apiserver

cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\
  --allow-privileged=true \\
  --apiserver-count=3 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-servers=https://10.240.0.10:2379,https://10.240.0.11:2379,https://10.240.0.12:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-account-issuer=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

これはネタバレだが、--etcd-servers=https://10.240.0.10:2379,https://10.240.0.11:2379,https://10.240.0.12:2379等の、etcd関連のOptionはKubernetes API Server以外には与えてないので、etcdと直接通信するのはKubernetes API Serverのみだと思う。

その他はSSL/TLS関連のオプションがほとんど。

kube-controller-manager

cat <<EOF | sudo tee /etc/systemd/system/kube-controller-manager.service
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-controller-manager \\
  --bind-address=0.0.0.0 \\
  --cluster-cidr=10.200.0.0/16 \\
  --cluster-name=kubernetes \\
  --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem \\
  --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \\
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --leader-elect=true \\
  --root-ca-file=/var/lib/kubernetes/ca.pem \\
  --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --use-service-account-credentials=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Optionの中に--kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfigとあるので、kube-controller-manager.kubeconfig/var/lib/kubernetes配下に置く。

言われてみれば確かにkubeconfigは鍵ファイルを内包してるので、他の鍵ファイル(kubernetes API Server証明書とか)と同じ扱いとして/var/lib配下に置くのが正しそう。

だから以下の予想ははずれ。

/var/lib/kubernetesは各種鍵ファイル、ではさっき作った/etc/kubernetes/configにはkubeconfigとかを置くのだろうか。

知らんけど大事なmanifestとか置くんだよ多分。

kube-scheduler

他同様にsystemdユニットファイルを作る。

cat <<EOF | sudo tee /etc/systemd/system/kube-scheduler.service
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-scheduler \\
  --config=/etc/kubernetes/config/kube-scheduler.yaml \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

オプションに置いた--config=/etc/kubernetes/config/kube-scheduler.yamlを作る。

/etc/kubernetes/config以下には、kubernetesで管理しない(systemdが管理する)Podの設定Manifestが置かれるのかな。

apiVersion: kubescheduler.config.k8s.io/v1beta1になってるのが気になる。
kubeschedulerは中核をなすコンポーネントなのに、その設定を行うためのAPIがまだ(もう?)betaなのはなんでだろう。

そもそもkube-controller-managerkube-apiserverだけYAMLのConfigを持たないのもよくわからない(もしかしたら設定できるのかも)。このあたりのAPIが統一されていないのはなにか背景があるのかもしれない[要調査]

cat <<EOF | sudo tee /etc/kubernetes/config/kube-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true
EOF

HTTPのヘルスチェックを有効化

散々出てきた$KUBERNETES_PUBLIC_ADDRESSの伏線回収パート。

Google Network Load Balancerを使用して3つのAPIサーバーにトラフィックを分散し、各APIサーバーがTLS接続を終端してクライアント証明書を検証できるようにします。

GCPにも、AWS同様にレイヤ7負荷分散とレイヤ4負荷分散があるらしい。
ここでは、ネットワークロードバランサー(レイヤ4負荷分散)を用いる。
レイヤ7負荷分散だと、せっかく取得して、各コンポーネントに教えたLBエンドポイントのIPアドレスを固定できないとかが原因か?[要調査]

ネットワークロードバランサーは、HTTPヘルスチェックのみをサポートします。つまり、APIサーバーによって公開されたHTTPSエンドポイントは使用できません。回避策として、nginxを使用してHTTPのヘルスチェックをプロキシすることができます。本セクションではnginxをインストールし、ポート80でHTTPヘルスチェックを受け入れ、https://127.0.0.1:6443/healthz 上のAPIサーバへの接続をプロキシするように設定します。

ネットワークロードバランサーなので、クライアント証明書を持ってヘルスチェックみたいな気の利いたことはできないためか、回避策が書かれてる。

恥ずかしながらnginxを使ってWebサーバーを立てたことがないのでここからは完全初見。
いつもは各種プログラミング言語が具備しているライブラリ実装を使ってWebサーバーを立てているゆとり世代であるため、実はApacheもnginxも、k8sの動作確認くらいでしかほぼ触ったことない。

server {
  listen      80;
  server_name kubernetes.default.svc.cluster.local;

  location /healthz {
     proxy_pass                    https://127.0.0.1:6443/healthz;
     proxy_ssl_trusted_certificate /var/lib/kubernetes/ca.pem;
  }
}

このファイルを/etc/nginx/sites-enabled/以下に置いてnginxを起動すると、Host=kubernetes.default.svc.cluster.local, Port=80向けの通信をhttps://127.0.0.1:6443/healthz、即ちkube-apiserverにフォワーディング(プロキシ?)してくれるんだと思う。こんなイメージ。

[GCP NLB] 
--> [kubernetes.default.svc.cluster.local/healthz:80] 
--> [nginx]
--> [<Node>/healthz:6443 = kube-apiserver]

ローカルからリクエストを送ってプロキシ動作確認。

curl -H "Host: kubernetes.default.svc.cluster.local" -i http://127.0.0.1/healthz

RBACを使ったKubeletの認可

本セクションでは、RBACのアクセス権を設定して、KubernetesのAPIサーバーが各ワーカーノード上のKubeletにアクセスできるようにします。メトリクスやログを取得したり、Pod内でコマンドを実行するためにはKubelet APIへのアクセスが必要です。

本チュートリアルでは、Kubeletの--authorization-modeフラグをWebhookに設定します。Webhookモードでは、SubjectAccessReview APIを使用して認可を判定します。

言葉の意味が一切わからんので、調査したところ以下のように解釈した。
Webhookはkubelet自身では認可を判断せず、認可の責任を別のコンポーネントに移譲するための仕組みで、kubeletにリクエストを送ったユーザーのロールを元にkubelet(から判断を任されたコンポーネント)がそのリクエストを許可するかしないか、を決定するみたいな話。

ということでまずはユーザーロールを先に作る。

system:kube-apiserver-to-kubeletという名前のClusterRoleを作成し、Kubelet APIにアクセスしたり、Podの管理に関連する一般的なタスクを実行したりするための権限を付与します:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-apiserver-to-kubelet
rules:
  - apiGroups:
      - ""
    resources:
      - nodes/proxy
      - nodes/stats
      - nodes/log
      - nodes/spec
      - nodes/metrics
    verbs:
      - "*"

で、そのあとにkubernetesユーザーにアタッチ。
--kubeconfig admin.kubeconfig admin.kubeconfigとするのは、admin.kubeconfigの中に書かれたクライアント証明書を使うため。

cat <<EOF | kubectl apply --kubeconfig admin.kubeconfig -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system:kube-apiserver
  namespace: ""
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-apiserver-to-kubelet
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: kubernetes
EOF

ネットワークロードバランサーのプロビジョニング

One Commandで出来ちゃうので味気ないが、すごい。

{
  KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
    --region $(gcloud config get-value compute/region) \
    --format 'value(address)')

  gcloud compute http-health-checks create kubernetes \
    --description "Kubernetes Health Check" \
    --host "kubernetes.default.svc.cluster.local" \
    --request-path "/healthz"

  gcloud compute firewall-rules create kubernetes-the-hard-way-allow-health-check \
    --network kubernetes-the-hard-way \
    --source-ranges 209.85.152.0/22,209.85.204.0/22,35.191.0.0/16 \
    --allow tcp

  gcloud compute target-pools create kubernetes-target-pool \
    --http-health-check kubernetes

  gcloud compute target-pools add-instances kubernetes-target-pool \
   --instances controller-0,controller-1,controller-2

  gcloud compute forwarding-rules create kubernetes-forwarding-rule \
    --address ${KUBERNETES_PUBLIC_ADDRESS} \
    --ports 6443 \
    --region $(gcloud config get-value compute/region) \
    --target-pool kubernetes-target-pool
}

$KUBERNETES_PUBLIC_ADDRESSはお馴染みの外部IPアドレス。
--host "kubernetes.default.svc.cluster.local"nginxでプロキシ設定を行ったリスナーホスト名。

  gcloud compute firewall-rules create kubernetes-the-hard-way-allow-health-check \
    --network kubernetes-the-hard-way \
    --source-ranges 209.85.152.0/22,209.85.204.0/22,35.191.0.0/16 \
    --allow tcp

はNLBヘルスチェック用の許可設定。
NLBのヘルスチェックは209.85.152.0/22,209.85.204.0/22,35.191.0.0/16から飛んでくるのか、Private/Isolated Subnetの場合どうするんだろう?[要調査]

  gcloud compute forwarding-rules create kubernetes-forwarding-rule \
    --address ${KUBERNETES_PUBLIC_ADDRESS} \
    --ports 6443 \
    --region $(gcloud config get-value compute/region) \
    --target-pool kubernetes-target-pool

はNLBの転送設定。
--target-poolは予め--instances controller-0,controller-1,controller-2として作成済み。

かなり長かったが、ここまででようやくController Node内のコンポーネントは揃った。まだcontroller nodeからkubectl get nodeとか打つとエラーが出るので、今後解消されるタイミングがあると信じたい。

sandship@controller-0:~$ kubectl get node
The connection to the server localhost:8080 was refused - did you specify the right host or port?

09-Kubenretesワーカーノードのブートストラップ

本実習では、3つのKubernetesワーカーノードをブートストラップします。各ノードにはrunc、CNI, containerd、kubeletおよびkube-proxyがインストールされます。

ということでtmuxを使って進めていく。
以下をインストールするわけだが、恥ずかしながら一つも知らないライブラリたちなので調べてみた。

{
  sudo apt-get update
  sudo apt-get -y install socat conntrack ipset
}
  • socat
    • Socat is a command line based utility that establishes two bidirectional byte streams and transfers data between them. Because the streams can be constructed from a large set of different types of data sinks and sources (see address types), and because lots of address options may be applied to the streams, socat can be used for many different purposes.

    • 双方向のByte Streamを2つ作って、それぞれのStream同士をPipeしてくれるもの
    • リバプロみたいな用途で使う?
  • conntrack
    • conntrack provides a full featured userspace interface to the netfilter connection tracking system that is intended to replace the old /proc/net/ip_conntrack interface. This tool can be used to search, list, inspect and maintain the connection tracking subsystem of the Linux kernel. Using conntrack , you can dump a list of all (or a filtered selection of) currently tracked connections, delete connections from the state table, and even add new ones.
      In addition, you can also monitor connection tracking events, e.g. show an event message (one line) per newly established connection.

    • Linux Kernelが行う通信を覗き見るための機能がたくさんついたライブラリ
    • wiresharkみたいな感じか?
  • ipset
    • ipset is used to set up, maintain and inspect so called IP sets in the Linux kernel. Depending on the type of the set, an IP set may store IP(v4/v6) addresses, (TCP/UDP) port numbers, IP and MAC address pairs, IP address and port number pairs, etc. See the set type definitions below.
      Iptables matches and targets referring to sets create references, which protect the given sets in the kernel. A set cannot be destroyed while there is a single reference pointing to it.

    • IPアドレスとかPortとかNICのMACアドレスをペア(セット?)という単位として取り扱うことができるようになり、Iptablesコマンドがそのセットを使っていい感じに管理できるようになる?
    • kubernetesでよく出てくるiptablesコマンドの管理をもっとかんたんにするためのツールチェインの一つらしい

3つともNW関連で、ソケットの監視、接続の監視、管理を行う感じだと思う。
kubernetes箱のあたりのライブラリに依存しているらしいのでインストールした。

swapはoffにしないとkubeletは起動失敗する。
ラズパイとかだとswapが有効になっていたので、一度詰まった。

sudo swapon --show
sudo swapoff -a

ワーカーでブートしたい各種バイナリをダウンロード&インストールする。
ここでは、上記したrunc, CNI, containerd, kubelet, kube-proxykubectlを取ってくる。

事前に、runc, CNI, containerdについては理解しておきたいので調べた。
それぞれ以下のイメージで理解した。

  • runc
    *

  • CNI

    • CNI (Container Network Interface), a Cloud Native Computing Foundation project, consists of a specification and libraries for writing plugins to configure network interfaces in Linux containers, along with a number of supported plugins. CNI concerns itself only with network connectivity of containers and removing allocated resources when the container is deleted. Because of this focus, CNI has a wide range of support and the specification is simple to implement.

    • この文章からだとむつかしくてよくわからなかったが、ContainerのNW Interfaceのライフサイクル管理を行うための各種CNI Pluginsの一種。
    • 聞きかじったことのあるcalicoとかflannelとかの仲間で、Pluginバイナリを/opt/cni/binに配置、ネットワーク構成ファイルを/etc/cni/net.d/xxx.confに置けばいい感じにしてくれる統一の規格のようなものが定められているとか。
    • 詳しい解説は[こちら]http://netstars.co.jp/kubestarblog/kubestarblog3/k8s/がわかりやすくてよかった。
  • containerd

    • Container Runtime Interfaceの一種。Dockerみたいにコンテナを動かす主体で、Kubernetesと通信を行って必要なときにContainerをPullして立ち上げる。(PullはCRIのしごとではないかも)
    • 今回の実習ではkubernetesのCRIとしてdockerではなくcontaienrdを用いる。
    • KubernetesのデフォルトCRIがdockerからcontainerdになるみたいな話を以前聞いた。(誤解してるかも)
wget -q --show-progress --https-only --timestamping \
  https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.21.0/crictl-v1.21.0-linux-amd64.tar.gz \
  https://github.com/opencontainers/runc/releases/download/v1.0.0-rc93/runc.amd64 \
  https://github.com/containernetworking/plugins/releases/download/v0.9.1/cni-plugins-linux-amd64-v0.9.1.tgz \
  https://github.com/containerd/containerd/releases/download/v1.4.4/containerd-1.4.4-linux-amd64.tar.gz \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubectl \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kube-proxy \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubelet

CNIのネットワーク設定

以下手順で/etc/cni/net.d/配下にネットワーク構成ファイル(10-bridge.conf, 99-loopback.conf)を作成する。
後々に、Node間Podネットワークの設定を行う章が別であるので、多分ここでは各コンポーネント間のネットワーク設定を行っている?

構成ファイルに関しては、[公式リポジトリ]https://github.com/containernetworking/cni#how-do-i-use-cniにフォーマットが記載されている。

podネットワークのrouteを規定しており、${POD_CIDR}向けの通信を0.0.0.0/0にルーティングする設定になっている?

cat <<EOF | sudo tee /etc/cni/net.d/10-bridge.conf
{
    "cniVersion": "0.4.0",
    "name": "bridge",
    "type": "bridge",
    "bridge": "cnio0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "${POD_CIDR}"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
    }
}
EOF

cat <<EOF | sudo tee /etc/cni/net.d/99-loopback.conf
{
    "cniVersion": "0.4.0",
    "name": "lo",
    "type": "loopback"
}
EOF

kubeletの設定

controller Node内のkubernetesコンポーネントの設定同様、/var/lib配下に証明書を包含するconfig yamlを作って、systemctlで起動するためのユニットファイルを作成する。

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: "/var/lib/kubernetes/ca.pem"
authorization:
  mode: Webhook
clusterDomain: "cluster.local"
clusterDNS:
  - "10.32.0.10"
podCIDR: "10.200.0.0/24"
resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet/worker-0.pem"
tlsPrivateKeyFile: "/var/lib/kubelet/worker-0-key.pem"

/var/lib/kubelet/kubelet-config.yamlは、kubeletの認証モード・証明書の設定と、クラスターの名前解決に関する設定が入っている。
kubeletへのアクセスはwebhookによって認証・認可されるため、そのように記載。
clusterDomainは, コンテナがこのクラスターにアクセスする際に利用可能なドメイン名を定義し、clusterDNSはクラスター内のDNSのIPアドレスのリストを記述する。

clusterDNSに関しては、"10.32.0.10"と見知らぬIPアドレスが設定されているが、後々にkube-dnsを建てる上で指定するIPアドレスを先に指定しているっぽい。

[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=containerd.service
Requires=containerd.service

[Service]
ExecStart=/usr/local/bin/kubelet \\
  --config=/var/lib/kubelet/kubelet-config.yaml \\
  --container-runtime=remote \\
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\
  --image-pull-progress-deadline=2m \\
  --kubeconfig=/var/lib/kubelet/kubeconfig \\
  --network-plugin=cni \\
  --register-node=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

/etc/systemd/system/kubelet.serviceに関しては、これまで同様kubelet起動時のオプションを定義する。
これまでの設定を踏まえて、container-runtime-endpointにはcontainerdのソケットを指定し、network-pluginにはcniを指定する。

kube-proxyの設定

kubelet同様、/var/lib配下に証明書を包含するconfig yamlを作って、systemctlで起動するためのユニットファイルを作成する。

kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"

modeに関しては、公式に以下の記述があった。
userspace, iptables, ipvsの3種類のモードがあり、ipvsが最も最新のモードであり、パフォーマンス・スケーラビリティに優れるが、デフォルトではiptablesが用いられるらしい。

Currently, three modes of proxy are available in Linux platform: 'userspace' (older, going to be EOL), 'iptables' (newer, faster), 'ipvs'(newest, better in performance and scalability).

In Linux platform, if proxy mode is blank, use the best-available proxy (currently iptables, but may change in the future). If the iptables proxy is selected, regardless of how, but the system's kernel or iptables versions are insufficient, this always falls back to the userspace proxy. IPVS mode will be enabled when proxy mode is set to 'ipvs', and the fall back path is firstly iptables and then userspace.

[Unit]
Description=Kubernetes Kube Proxy
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-proxy \\
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

kube-proxy.serviceに関しては特筆する設定はないので割愛。
いつもどおり--configオプションに、/var/lib配下のYAMLを渡してあげる。

確認

最終的にコントローラーノードからワーカークラスタに属するノード一覧を確認して終わり。
とはならず。Worker Nodeが一つも見つからない。

gcloud compute ssh controller-0 \
  --command "kubectl get nodes --kubeconfig admin.kubeconfig"

No resources found

Controller Node上のKubernetes API Serverが、Worker Node上のkube-proxyまたはkubeletを認識できていない。
各Node間のKubernetes Management通信はポート:6443を介して行われるはずで、その疎通に失敗していると考えられるとか整理していると、よくよく考えればだいぶ前に悪いことをしていた。

source rangeが0.0.0.0からのSSHおよびkube-apiserver向け通信を許可する設定を入れている。
外部NW全域からのSSH許可は何となく気持ちが悪いので、MyIP Onlyに設定した。
この先の手順で動かなかったらここも疑う。

ここがダメでした。
Controller NodeおよびWorker NodeのNICのIngressルールをfrom MyIP Onlyにしてしまっていたため、Controller Node上のKubernetes API Serverが、Worker Node上のkube-proxyまたはkubeletとの通信に失敗していた。

コンソールから修正して再度実行したら以下のように正常に認識できた。(各Worker上のコンポーネントの再起動もいらなかった。)

gcloud compute ssh controller-0 \
  --command "kubectl get nodes --kubeconfig admin.kubeconfig"
Enter passphrase for key '/home/sandship/.ssh/google_compute_engine':

NAME       STATUS   ROLES    AGE   VERSION
worker-0   Ready    <none>   22s   v1.21.0
worker-1   Ready    <none>   22s   v1.21.0
worker-2   Ready    <none>   22s   v1.21.0

10-リモートアクセス用のkubectl設定

本実習では、adminユーザーの認証情報に基づいたkubectlコマンド用のkubeconfigファイルを生成します。

本実習で使用するコマンドは、管理クライアント証明書の生成に使用したディレクトリと同じディレクトリから実行してください。

ここではリモートアクセス用のkubectlクライアントアカウントadminの設定を行う。
kubectl config set-cluster kubernetes-the-hard-wayで、kubernetes-the-hard-wayという名前で先の手順にて作成したクラスターを参照する設定を追加。
kubectl config set-credentials adminで、adminのクライアント証明書および証明書キーを設定。
kubectl config set-context kubernetes-the-hard-wayで、kubernetes-the-hard-wayクラスターとadminユーザーを紐つけたコンテキストkubernetes-the-hard-wayを作成。
kubectl config use-context kubernetes-the-hard-wayで、kubernetes-the-hard-wayコンテキストを使用するよう設定。

{
  KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
    --region $(gcloud config get-value compute/region) \
    --format 'value(address)')

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443

  kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem

  kubectl config set-context kubernetes-the-hard-way \
    --cluster=kubernetes-the-hard-way \
    --user=admin

  kubectl config use-context kubernetes-the-hard-way
}

本手順によって、ローカル環境(クライアント)から、リモートのController Node上Kubernetes API Server向けにkubectlコマンドの実行が許可されるようになった。

試しに以下コマンドを実行すると、リモート・クライアント側のkubectlのバージョンを確認できる。

kubectl version

Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:31:21Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:25:06Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"}

11-Podが使うネットワーク経路のプロビジョニング

ノードにスケジュールされたPodは、ノードが持つPod CIDR範囲からIPアドレスを受け取ります。この時点ではネットワーク経路が欠落しているため、Podは異なるノード上で動作している他のPodと通信できません。
本実習では、ノードのPod CIDR範囲をノードの内部IPアドレスにマップするための経路を各ワーカーノード上に作成します。

ここまでの手順におけるネットワーク関連の設定は、Controller側のコンポーネント(主にkube-apiserver)とWorker側のコンポーネント(主にkube-proxyとkubelet)間の通信を行うための設定であったため、Podのネットワーク経路は別途設定する必要があるらしい。
本実習では、寡聞な身ではあるものの過去に少し聞きかじったことのあるようなcalicoとかflannelは本実習では使わず、GCP側のルートテーブルそのものに設定を加えてノード間通信を実現する。

具体的にはコンテナネットワークのサブネットが、ノードごとに完全に分離しているので、以下のように他ノード内コンテナネットワークのサブネットが指定された場合のネクストホップを、別ノードのNICのIPアドレスに指定する。

ノードごとにサブネットが完全に分離してない場合(複数ノードで一つのサブネットセグメントを共有したい場合など)は、この設定では複雑になりすぎるのでもう少し上手い管理が必要。

for instance in worker-{0..2}; do
  gcloud compute instances describe ${instance} \
    --format 'value[separator=" "](networkInterfaces[0].networkIP,metadata.items[0].value)'
done

10.240.0.20 10.200.0.0/24
10.240.0.21 10.200.1.0/24
10.240.0.22 10.200.2.0/24
NAME                            NETWORK                  DEST_RANGE     NEXT_HOP                  PRIORITY
default-route-36119a03cfa6823e  kubernetes-the-hard-way  10.240.0.0/24  kubernetes-the-hard-way   0
default-route-5380f59988ea070e  kubernetes-the-hard-way  0.0.0.0/0      default-internet-gateway  1000
kubernetes-route-10-200-0-0-24  kubernetes-the-hard-way  10.200.0.0/24  10.240.0.20               1000
kubernetes-route-10-200-1-0-24  kubernetes-the-hard-way  10.200.1.0/24  10.240.0.21               1000
kubernetes-route-10-200-2-0-24  kubernetes-the-hard-way  10.200.2.0/24  10.240.0.22               1000

ネットワーク設定に限らない話だが、プロダクトスタンダードだとどのようにkubernetes環境を構築するのかを知っておく必要はあるので、それはまた別途勉強します。

12-DNSクラスターアドオンのデプロイ

本実習では、CoreDNSによってサポートされるDNSベースのサービスディスカバリを提供するアドオンを、Kubernetesクラスター内で稼働するアプリケーションに導入します。

本実習では、もうすでに構築済みのcorednsのManifestファイルをapplyして完了する。
coredns.yamlの中では、サービスアカウントcorednsと、そのアカウントのロールsystem:coredns、あとConfigMapがnamespace=kube-system内に配備されている。

また、先述したようにWorker Nodeのkubeletのkubeconfigに記載した、clusterDNSオプションに設定したIPアドレスは、すなわちこのCoreDNSのIPアドレスであり、同一の値(10.32.0.10)を設定する。

# ...前略...

apiVersion: v1
kind: Service
metadata:
  name: kube-dns
  namespace: kube-system
  annotations:
    prometheus.io/port: "9153"
    prometheus.io/scrape: "true"
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "CoreDNS"
spec:
  selector:
    k8s-app: kube-dns
  clusterIP: 10.32.0.10
  ports:
  - name: dns
    port: 53
    protocol: UDP
  - name: dns-tcp
    port: 53
    protocol: TCP
  - name: metrics
    port: 9153
    protocol: TCP

CoreDNSを起動したので、実際にPodからkubernetes API Serverの名前解決を打ち、clusterDNSが正常に動作しているかを確認する。

kubectl exec -it $(kubectl get pods -l run=busybox -o jsonpath="{.items[0].metadata.name}") -- nslookup kubernetes

Server:    10.32.0.10
Address 1: 10.32.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kubernetes
Address 1: 10.32.0.1 kubernetes.default.svc.cluster.local

clusterDNSが10.32.0.10kubernetes API Serverが無事見つかったのでOK。

13-スモークテスト

省略。

14-お掃除

諸行無常。
本記事の中では、上記で作ったクラスターを元に色々操作の実習を行うので、お掃除はしない。

手順はインスタンス・ロードバランサー・ファイアウォールルール・VPC・External IPの削除を行って終わりになる。

Discussion