ClusterHATを使って世界最小Kubernetesクラスタを構築する - TLS証明書編

#はじめに
前回の[ClusterHATを使って世界最小Kubernetesクラスタを構築する - サーバー構築編](https://qiita.com/ShinHashitani/items/24fe88a012e01dfc28d0)では、Raspberry Pi3 BにClusterHATというクラスタ拡張基盤を足し、Pi Zero3枚を接続した4台構成の豆腐クラスタの構築まで行った。今回からはいよいよ本題のKubernetesをこの環境上に構築する。

Kubernetesは有名なKelsey Hightowerさんの[Kubernetes the Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way)をなぞってマニュアルで構築していく。このチュートリアルはKubernetesの運用マニュアルではなく、一から構築することによりKubernetesクラスタを構築する上で必要な構成要素を理解する目的で書かれたもの。私の目的とも完全に合致するので、あまり色気を出さずに素直にチュートリアル通りに進めていこうと思う。いや、ほんのちょっと色気出して1.10.xを入れてみる。

前編では第6章、TLS証明書を発行するところまでをカバーする。

尚、本家はGCPに6つのインスタンスを用意して構築するものだが、今回はおうちクラスタ環境に合わせて、というより先日作った豆腐クラスタ通り4つの物理サーバーでの構築となる。チュートリアルを進めて行くにあたって、本家とギャップが発生する箇所を特に重点的に記載したい。チュートリアルは全14章構成になっているが、GCP特有の章も含まれているのでこれらは割愛する。

#環境
サーバー：Raspberry Pi3 B + ClusterHAT + Pi Zero x 3
OS：Raspbian Stretch（2018-03-13 カーネル：4.14.34-v7+）（ClusterHAT提供イメージ）
Kubernetes: 1.10.2
etcd: 3.3.4
cfssl: 1.2.0
ネットワーク：

|     host name    | Internal IP    |  DNS NAME          |     Description    |
|:-----------------|---------------:|:-------------------|:------------------:|
|    controller    |192.168.1.100   |  controller.local  |母艦Pi3 B           |
|       p1         |192.168.1.101   |     p1.local       |Pi Zero 1枚目       |
|       p2         |192.168.1.102   |     p2.local       |Pi Zero 2枚目       |
|       p3         |192.168.1.103   |     p3.local       |Pi Zero 3枚目       |
※DNS Nameは上記4サーバー間でのみ有効 
# 1. [Prerequisites](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/01-prerequisites.md)
GCPのゾーン設定等なので割愛
# 2. [Installing the Client Tools](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/02-client-tools.md)
作業端末に必要なのはkubectlとCloudflareのKPI/TSLツールの2つ。どちらもバイナリを取得し実行ビットを立てて/user/local/bin配下に移動しているのみ。今回kubectlのバージョン違いを取ってくるので下記のみ変更。

```bash:WSL
## kubectlをインストール
$ wget https://storage.googleapis.com/kubernetes-release/release/v1.10.2/bin/linux/amd64/kubectl
$ chmod +x kubectl
$ sudo mv kubectl /usr/local/bin/
$ kubectl version --client
Client Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.2", GitCommit:"81753b10df112992bf51bbc2c2f85208aad78335", GitTreeState:"clean", BuildDate:"2018-04-27T09:22:21Z", GoVersion:"go1.9.3", Compiler:"gc", Platform:"linux/amd64"}
```

#3. [Provisioning Compute Resources](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/03-compute-resources.md)
既に豆腐クラスタが準備済みなので割愛。
#4. [Provisioning a CA and Generating TLS Certificates](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/04-certificate-authority.md)
CAの設定と各種TLS証明書を準備する章。このあたりあまりに理解があやふやなとこなので踏み込む。

###TLS証明書とキーの生成

PKI（Public Key Infrastructure）はグループのサーバー内で各サーバーの公開鍵を管理する仕組みで、PKIを管轄するCA（認証局:Certificate Authority）が自分のプライベートキーで各サーバーの公開鍵を発行する。外の世界では信頼できる第三者CAに証明書を発行してもらうんだが、KubernetesクラスタではControllerのAPI Serverがその役を担う（Self-Signed Certificate）。証明書をネットワーク経由で送るのはセキュリティ上よろしくないのでcfssl serveを使ってコントローラー上で証明書を発行するのが正しい気もする（そして他にも方法は色々ありそうな気がする）が、今回はチュートリアル通りローカルで証明書とキーを纏めて生成してから各サーバーに転送することにする。

チュートリアルではGCPではKubeletとAPI Server向けの証明書を発行する際にサーバーのパブリックIPを指定しているんだが、如何せんおうちクラスタではパブリックIPが無い。分からないながら色々調べてみたが、どうも別に外からアクセスする必要は無くって、GCPだから記載があるのでは？と思う。証明書にはhostnameという名前でホストネームやIPを複数紐づけれるので、とりあえずはコントローラーのDNS名（avahi）とIP、ならびに作業端末のインターナルIPをセットすることにした。（後に変更する可能性もある。）

まずはCA（認証局）の設定ファイルと証明書要求ファイルを生成する。

```bash:CA
## CA用証明書とキーの生成 ##
#CA設定ファイルの生成（チュートリアルと同じ）
cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

#CSR設定ファイルの作成
cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Tokyo"
    }
  ]
}
EOF

#証明書と鍵の生成（チュートリアルと同じ）
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
```

次に管理者権限（コントローラー）の証明書要求と証明書、キーを発行する。

```bash:Admin
## Adminアカウント用証明書とキーの生成 ##
#CSR用設定ファイルの作成
$ cat > admin-csr.json <<EOF
{
  "CN": "admin",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "Kubernetes",
      "OU": "admin",
      "ST": "Tokyo"
    }
  ]
}
EOF

#証明書と鍵の生成（チュートリアルと同じ）
$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  admin-csr.json | cfssljson -bare admin
...
$ [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
websites. For more information see the Baseline Requirements for the Issuance and Management
of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
specifically, section 10.2.3 ("Information Requirements").
```

Kubelet用の証明書要求、証明書とキーを生成。チュートリアルではここで各ノードのPublic IPとPrivate IPを取得しhostnameに列挙しているが、インターナルのIPのみ登録している。

```bash:Kubelet
## Kubeletアカウント用証明書とキーの生成 ##
# ホスト名とIPのHash
$ declare -A NODE_LIST
$ NODE_LIST=(
  ["p1"]="192.168.1.101"
  ["p2"]="192.168.1.102"
  ["p3"]="192.168.1.103"
)

#CSR用設定ファイルの作成
$ for instance in ${!NODE_LIST[@]}; do
cat > ${instance}-csr.json <<EOF
{
  "CN": "system:node:${instance}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "system:nodes",
      "OU": "kubelet",
      "ST": "Tokyo"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${instance}.local,${NODE_LIST[$instance]} \
  -profile=kubernetes \
  ${instance}-csr.json | cfssljson -bare ${instance}
done
```

次にKube-Proxy用の証明書要求と証明書、ならびにキーを発行。チュートリアルではKubeletの場合にはhostnameの指定が無い。Kubelet同様API-Serverとリモートでやりとりするので必要だとは思うのだが、どうも理解が足りていないよう。ここは後に理由を理解しないといけない。とりあえずチュートリアル通りに作成。

```bash:Kube-Proxy
## Kube-Proxyアカウント用証明書とキーの生成 ##
#CSR用設定ファイルの作成
$ cat > kube-proxy-csr.json <<EOF
{
  "CN": "system:kube-proxy",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "system:node-proxier",
      "OU": "kubelet",
      "ST": "Tokyo"
    }
  ]
}
EOF

#証明書と鍵の生成（チュートリアルと同じ）
$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-proxy-csr.json | cfssljson -bare kube-proxy
...
$ [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
websites. For more information see the Baseline Requirements for the Issuance and Management
of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
specifically, section 10.2.3 ("Information Requirements").
```

最後にAPI-Serverの証明書要求、証明書とキーを発行する。チュートリアルではクラスタIPを指定することによってリモートからの要求に対応できるとあるので、代わりに個別でIPとDNSを指定することとした。また、合わせて作業端末の固定IP（192.168.1.10）も登録しておく。

localhostをhostnameに含んでいる理由までは分からない。スケジューラーとかはローカルアクセスだがTLS認証しない接続だったように思えるので何故だろう？

```bash:API-Server
## API-Serverアカウント用証明書とキーの生成 ##
#CSR用設定ファイルの作成
$ cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "Kubernetes",
      "OU": "API-Server",
      "ST": "Tokyo"
    }
  ]
}
EOF

#証明書と鍵の生成
$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=192.168.1.10,192.168.1.100,controller.local,192.168.1.101,192.168.1.102,192.168.1.103,p1.local,p2.local,p3.local,127.0.0.1,kubernetes.default \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes
```

### 証明書とキーの配布
ここで証明書とキーを各サーバーに配布する手順となるんだが、チュートリアルを見ている限りユーザーを指定せずホームディレクトリに転送している。これはルートに送ってるんだろうか？あまり色を付けるのも嫌なんだが、一応Kubernetes実行用のユーザーとグループを作っておき、そのアカウントのホームディレクトリに転送するようにしておく。

```bash:
$ sudo groupadd kubernetes
$ sudo useradd kubernetes -g kubernetes -m
$ sudo passwd kubernetes
```

あとはコントローラー、ノードそれぞれに該当の証明書なりキーなりを転送する。

```bash:Controller
$ scp ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem kubernetes@192.168.1.100:~/
```

```bash:Node
$ for instance in ${!NODE_LIST[@]}; do
    scp ca.pem ${instance}-key.pem ${instance}.pem kubernetes@${NODE_LIST[$instance]}:~/
done
```
#5. [Generating Kubernetes Configuration Files for Authentication](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/05-kubernetes-configuration-files.md)

ここではノード側からAPI Serverに接続するための設定ファイル（kubeconfig）を生成している。kubectl config set-clusterではクラスタ名を指定する必要があるが、tofu-clusterとしている。まずはKubelet用から生成する。

```bash:Kubelet
$ for instance in p1 p2 p3; do
  kubectl config set-cluster tofu-cluster \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://192.168.1.100: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=tofu-cluster \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

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

同様にKube-Proxy用にも生成する。

```bash:Kube-Proxy
$ kubectl config set-cluster tofu-cluster \
  --certificate-authority=ca.pem \
  --embed-certs=true \
  --server=https://192.168.1.100:6443 \
  --kubeconfig=kube-proxy.kubeconfig

$ kubectl config set-credentials kube-proxy \
  --client-certificate=kube-proxy.pem \
  --client-key=kube-proxy-key.pem \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig

$ kubectl config set-context default \
  --cluster=tofu-cluster \
  --user=kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig
```

いずれも3つのコマンドを続けて実行しているが、これでtofu-clusterという名前のクラスタを、各クライアントごとの証明書を紐づけて、最後にデフォルトのネームスペースに登録している。

最後に生成したKubelet、Kube-Proxy用のkubeconfigをそれぞれのノードに転送する。

```bash:Node
$ for instance in ${!NODE_LIST[@]}; do
    scp ${instance}.kubeconfig kube-proxy.kubeconfig kubernetes@${NODE_LIST[$instance]}:~/
done
```

#6. [Generating the Data Encryption Config and Key](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/06-data-encryption-keys.md)
最後に、TLSとは関係ないがKubernetesで試験的に導入されている暗号化機能用の設定ファイル（encryption-config.yaml）を生成する。このファイルは後にAPI Serverの設定時に参照される。

```bash:Controller
#魔法の言葉
$ ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
#設定ファイルの生成
$ cat > encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
EOF
```

このファイルをコントローラーに転送して終了。

```bash
$ scp encryption-config.yaml kubernetes@192.168.1.100:~/
```
#おわりに
今回はetcdやAPI-Serverの実際のインストール前に必要な前準備を行った。結果として私個人の理解が及んでいないTLS証明書あたりの理解に時間を費やしたが、なるほど通信用の証明書を一通り準備したりローテーションするのは結構大変な作業なのは感じた。また閉じらた環境（というよりRaspberry Piのクラスタ）である為iptablesもいじっていないが、せっかくクラスタがあるのでここも後々色々試してみようと思う。

このあたりサーバーの設定まわりに興味が無い方には退屈だったとは思うがご容赦頂きたい。そして、将来的にはこのあたりがずっとシンプルに出来るようになる気もするが、今Kubernetesを触っているのだから折角と思って律義にチュートリアルをなぞってみた。

次回はようやく[サーバーに色々インストールしたりする](https://qiita.com/ShinHashitani/items/c59348bda7365be644e0)楽しみな回。今回の手戻りがあまり無いよう祈りながら進めたい。