がべーじこれくしょん

技術系とかいろいろ

Kubernetes The Hard Way with Time4VPS

tl;dr Kubernetes The Hard Wayを格安VPSでお馴染みTime4VPSでやったときのメモ。

もくじ

注意事項

(絶対にやる人はいないと思うんですが)あくまでk8sの勉強用としてやったものなので、このチュートリアルを通して作成したクラスタを実運用するのは避けてください

Kubernetes The Hard Wayとは?

k8sを構成する関連コンポーネントを一つずつ手動でデプロイしていってk8sに対する理解を深めようぜ、というマゾ向けのチュートリアルです。

github.com

単純にk8sを使いたいだけならオススメしません。kubeadmやkubespray等を使ってください。

さらに、上記チュートリアルGCPを想定しています。基本的に使用するのは通常のComputing Engineのインスタンスであるため、自前で用意したオンプレサーバーや通常のVPSとさほど変わりません。

素直にGCP上でやる方が楽なので基本的にはGCPを使用することをおすすめします。GCPでやる場合、Free tierは余裕で超えるので300ドルの無料クレジットを使う形になります。

ただ、遊ばせている格安VPSインスタンスがもったいないので、せっかくなら有効活用していこう、というわけです。

下ごしらえ

インスタンスを立てるまで

GCPでは色々と準備が必要ですが今回使用するのはTime4VPSなので、インスタンスを起動してSSHできるようにしておけば問題ありません。

立て方について特に特筆すべき注意事項はないので、公式サイトの指示に従って立ててください。

ちなみに以下リンクからTime4VPSのインスタンスを立ててもらうと僕に多少の実験資金が降ってきます。ご協力お願いします(?)

billing.time4vps.eu

チュートリアルでは、Master NodeとWorker Nodeそれぞれ3台ずつ、合計6台で組みます。インスタンスタイプは全てn1-standard-1 (vCPUx1, RAM3.75GB)で揃えてあるようです。

今回は、Linux16が1台余っていた他は、全て最安のLinux2インスタンス (vCPUx1, RAM2GB)で揃えました。本来であれば全て同じスペックのほうがいいのですがまあ仕方ない。

必要なツールのインストール

手元のPCに必要なソフトをインストールします。

具体的には、TLS証明書を入手するためのcfsslとcfssljson、そしてk8s APIを叩くためのkubectlを手に入れます。

github.com

TLS証明書の発行

k8sを構成する各コンポーネント(etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kube-proxy)用のTLS証明書を発行します。

github.com

GCPとちがって面倒なのは、各インスタンスのExternal IPとInternal(Local) IPを、Time4VPSのClientAreaを見ながら入力しなければならない点です。*1

さらに、GCPでやる場合はLBが生えて、そこにPublic IPが割り当てられまが、今回は普通のVPSなので基本的に全部Public IPが割り当てられてます。

dotenvやdirenv等を用いてあらかじめ環境変数にセットしておくと楽です。

github.com

非常に冗長ですが以下のような.envを用意し、環境変数として読み込んでいることを想定します。*2

MASTER_1_EXTERNAL_IP=
MASTER_1_INTERNAL_IP=
MASTER_1_HOSTNAME=
MASTER_2_EXTERNAL_IP=
MASTER_2_INTERNAL_IP=
MASTER_2_HOSTNAME=
MASTER_3_EXTERNAL_IP=
MASTER_3_INTERNAL_IP=
MASTER_3_HOSTNAME=
WORKER_1_EXTERNAL_IP=
WORKER_1_INTERNAL_IP=
WORKER_1_HOSTNAME=
WORKER_2_EXTERNAL_IP=
WORKER_2_INTERNAL_IP=
WORKER_2_HOSTNAME=
WORKER_3_EXTERNAL_IP=
WORKER_3_INTERNAL_IP=
WORKER_3_HOSTNAME=

kube-admin, kube-controller-manager, kube-proxy, kube-scheduler

チュートリアルどおりにそれぞれ実行して発行します。

service-account用のキーペア

これもチュートリアル通りに作成します。

kubelet

ここだけ若干コマンドが違います。チュートリアルではGoogle Cloud CLI経由でIPアドレスを取得していますが、今回は環境変数に押し込んだ情報をもとにコマンドを叩きます。

各Worker用にそれぞれ作成します。

for number in $(seq 1 3); do
instance="WORKER_${number}"
cat > ${instance}-csr.json <<EOF
{
  "CN": "system:node:${instance}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:nodes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

HOSTNAME=$(printenv ${instance}_HOSTNAME)

EXTERNAL_IP=$(printenv ${instance}_EXTERNAL_IP)

INTERNAL_IP=$(printenv ${instance}_INTERNAL_IP)

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${HOSTNAME},${EXTERNAL_IP},${INTERNAL_IP} \
  -profile=kubernetes \
  ${instance}-csr.json | cfssljson -bare ${instance}
done

kube-apiserver

VPSなので、各インスタンスにそれぞれPublic IPが割り当てられています。そのため、チュートリアルのコマンドを若干書き換えます。

{

for number in $(seq 1 3); do
instance="MASTER_${number}"
KUBERNETES_PUBLIC_ADDRESS=${KUBERNETES_PUBLIC_ADDRESS},$(printenv ${instance}_EXTERNAL_IP)
KUBERNETES_PRIVATE_ADDRESS=${KUBERNETES_PRIVATE_ADDRESS},$(printenv ${instance}_INTERNAL_IP)
done

KUBERNETES_HOSTNAMES=kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.svc.cluster.local

cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${KUBERNETES_PRIVATE_ADDRESS},${KUBERNETES_PUBLIC_ADDRESS},127.0.0.1,${KUBERNETES_HOSTNAMES} \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes

}

証明書を各ノードへコピーする

.ssh/configにあらかじめ色々書いておくと楽です(今更)

Host MASTER_1
...

Host MASTER_2
...

Host MASTER_3
...

Host WORKER_1
...

Host WORKER_2
...

Host WORKER_3
...

configを設定してから、各種証明書が置いてある場所で以下を実行します。

まずはWorker Nodeへ必要な証明書を配送します。

for number in $(seq 1 3); do
  instance=WORKER_${number}
  scp ca.pem ${instance}-key.pem ${instance}.pem ${instance}:~/
done

次にMaster Nodeへ必要な証明書を配送します。

for number in $(seq 1 3); do
  instance=MASTER_${number}
  scp ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem service-account-key.pem service-account.pem ${instance}:~/
done

k8s構成ファイルの生成

kubelet

for master_number in $(seq 1 3); do
  master_instance="MASTER_${master_number}"
  KUBERNETES_PUBLIC_ADDRESS=$(printenv ${master_instance}_EXTERNAL_IP)
  for worker_number in $(seq 1 3); do
    worker_instance="WORKER_${worker_number}"
    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:${worker_instance} \
      --client-certificate=${worker_instance}.pem \
      --client-key=${worker_instance}-key.pem \
      --embed-certs=true \
      --kubeconfig=${worker_instance}.kubeconfig

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

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

kube-proxy

for master_number in $(seq 1 3); do
  master_instance="MASTER_${master_number}"
  KUBERNETES_PUBLIC_ADDRESS=$(printenv ${master_instance}_EXTERNAL_IP)
  kubectl config set-cluster kubernetes-the-hard-way \
     --certificate-authority=ca.pem \
     --embed-certs=true \
     --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \
     --kubeconfig=kube-proxy.kubeconfig

    kubectl config set-credentials system: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=kubernetes-the-hard-way \
      --user=system:kube-proxy \
      --kubeconfig=kube-proxy.kubeconfig

    kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig
done

kube-controller-manager

for master_number in $(seq 1 3); do
  master_instance="MASTER_${master_number}"
  KUBERNETES_PUBLIC_ADDRESS=$(printenv ${master_instance}_EXTERNAL_IP)
  kubectl config set-cluster kubernetes-the-hard-way \
     --certificate-authority=ca.pem \
     --embed-certs=true \
     --server=https://127.0.0.1:6443 \
     --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-credentials system:kube-controller-manager \
     --client-certificate=kube-controller-manager.pem \
     --client-key=kube-controller-manager-key.pem \
     --embed-certs=true \
     --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-context default \
     --cluster=kubernetes-the-hard-way \
     --user=system:kube-controller-manager \
     --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig
done

kube-scheduler

for master_number in $(seq 1 3); do
  master_instance="MASTER_${master_number}"
  KUBERNETES_PUBLIC_ADDRESS=$(printenv ${master_instance}_EXTERNAL_IP)
  kubectl config set-cluster kubernetes-the-hard-way \
     --certificate-authority=ca.pem \
     --embed-certs=true \
     --server=https://127.0.0.1:6443 \
     --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-credentials system:kube-scheduler \
     --client-certificate=kube-scheduler.pem \
     --client-key=kube-scheduler-key.pem \
     --embed-certs=true \
     --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-context default \
     --cluster=kubernetes-the-hard-way \
     --user=system:kube-scheduler \
     --kubeconfig=kube-scheduler.kubeconfig

  kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig
done

kube-admin

for master_number in $(seq 1 3); do
  master_instance="MASTER_${master_number}"
  KUBERNETES_PUBLIC_ADDRESS=$(printenv ${master_instance}_EXTERNAL_IP)
  kubectl config set-cluster kubernetes-the-hard-way \
     --certificate-authority=ca.pem \
     --embed-certs=true \
     --server=https://127.0.0.1:6443 \
     --kubeconfig=admin.kubeconfig

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

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

  kubectl config use-context default --kubeconfig=admin.kubeconfig
done

各ノードへ配送する

for number in $(seq 1 3); do
  instance=WORKER_${number}
  scp ${instance}.kubeconfig kube-proxy.kubeconfig ${instance}:~/
done
for number in $(seq 1 3); do
  instance=MASTER_${number}
  scp admin.kubeconfig kube-controller-manager.kubeconfig kube-scheduler.kubeconfig ${instance}:~/
done

データ暗号化に関するConfig・鍵生成

鍵生成

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

作成した構成ファイルを各Nodeに転送します。

for number in $(seq 1 3); do
  instance=MASTER_${number}
  scp encryption-config.yaml ${instance}:~/
done

etcdクラスターの構築

k8sコンポーネントは基本的にステートレスなので、クラスタの状態に関するデータは全てetcdに保存されています。このチュートリアルでは3つのMaster Nodeそれぞれにetcdを入れてクラスタを組みます。

以下のコマンドは、各Master Node上で実行されることを想定されています。

wget -q --show-progress --https-only --timestamping \
  "https://github.com/etcd-io/etcd/releases/download/v3.4.0/etcd-v3.4.0-linux-amd64.tar.gz"
{
  tar -xvf etcd-v3.4.0-linux-amd64.tar.gz
  sudo mv etcd-v3.4.0-linux-amd64/etcd* /usr/local/bin/
}
{
  sudo mkdir -p /etc/etcd /var/lib/etcd
  sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/
}
INTERNAL_IP=<master_node_internal_ip>
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 etcd1 \\
  --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 etcd1=https://${MASTER_1_INTERNAL_IP}:2380,etcd2=https://${MASTER_2_INTERNAL_IP}:2380,etcd3=https://${MASTER_3_INTERNAL_IP}:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

nameの部分は各ノードごとに変えてください。

各ノードで実行が済んだら以下コマンドを実行してetcdを有効にしてください。

{
  sudo systemctl daemon-reload
  sudo systemctl enable etcd
  sudo systemctl start etcd
}

動作確認

etcdのクラスターがちゃんと機能しているか確認しましょう。

MASTER_1のノード上で以下を実行すると、各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

コントロールプレーンの設定 (TBD)

ワーカーの設定 (TBD)

kubectlの設定 (TBD)

Pod間のルーティング設定 (TBD)

DNS add-onの導入 (TBD)

クラスタ全体の動作確認 (TBD)

*1:ただのVPSなのでもしかしなくてもローカルでの相互通信にiptablesの設定が必要かも

*2:書いてて気づいたのですがチュートリアルは0-indexedなのに1-indexedにしてしまった…