概要
kubernetes-the-hard-way を実践しようと思い、自宅環境に手軽に構築したいためにdockerコンテナ上に構築することを考えました。クラスタ構築の勉強方法の一つと捉えていただければ。
事実誤認や別の効率的なやり方等存じていたら教えていただければ幸いです。
TL:DR
本稿は、k8sクラスタを手で構築しようとした筆者が詰まったポイントの備忘、もしくは同じくクラスタを構築したいという人がもしかしたら悩むかもしれないポイントの共有を目的としています。
そのため主に詰まったポイントと対処方法、リファレンスについて下記にまとめておきます。
詳細な内容については各章で触れていきます。
Issue | Proposal | Reference |
---|---|---|
各コンポーネントバイナリの実行時に”Operation not permitted”のエラー | コンテナ実行時にcap_addする | DockerCapability LinuxCapability |
SSL証明書関連で実行権がないと怒られる | 署名時に該当のhostnameを追加し直す | 本稿参照 |
/procや/sysへの書き込み権限がないと怒られる | write権限を付与して該当のFSをマウント、もしくは再マウント | 本稿参照 |
containerdバイナリ実行時にcontained Not Found
|
依存パッケージ(libseccomp-dev libc6-compat )のインストール |
ref1, ref2 |
コンテナイメージをPullする際にx509: certificate signed by unknown authority
|
ca-certificates をインストール |
project page |
servicesリソースへのアクセスができない | ルーティングを追加 | 本稿参照 |
kube-proxyがwrite /sys/module/nf_conntrack/parameters/hashsize: operation not supported
|
--conntrack-max=0 を付加 |
ref |
kubelet起動時の権限不足 | tmpfsをマウント | 本稿参照 |
はじめに
基本的にgcpの機能は使えないということと、dockerの機能で抑制されている部分を変更する必要がありました。
以下、k8s-the-hard-wayのチュートリアル(以降:本家)に沿ってそれぞれ解説していきます。
構築環境は、VirtualBOX上にCPU:1, RAM:4Gi, Disk:32GiのCentos8をminimalインストールした状態です。centos7でも同様の手順で問題なく動くこと実験済みです。
$ cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
$ grep cpu.cores /proc/cpuinfo
cpu cores : 1
$ lsblk | grep disk
sda 8:0 0 32G 0 disk
$ grep MemTotal /proc/meminfo
MemTotal: 3871792 kB
$ docker --version
Docker version 19.03.8, build afacb8b
01-prerequisites
GoogleクラウドのCLI導入およびアカウント登録。
本稿では不要なので割愛。
02-client-tools
SSL証明書系の作成ツール(cfssl, cfssljson)およびkubernetes操作用のCLIツール(kubectl)の導入。
証明書一連の作業はその他のコマンドでも可能ですが、cfsslは公開鍵通信用の鍵作成から署名要求(CSR: Certificate Signing Request)、CAによる署名までを一気に行うことが可能なので、導入しておくことが好ましいです、素直にインストラクションに従います。
ちなみにcfsslは鍵のjson形式での出力、cfssljsonで鍵ファイルの保存を行います。
$ {
curl -O https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/linux/cfssl
curl -O https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/linux/cfssljson
chmod +x cfssl cfssljson
mv cfssl cfssljson /usr/local/bin/
}
# 1.3.4 >= であることを確認
$ cfssl version
Version: 1.3.4
Revision: dev
Runtime: go1.13
$ cfssljson --version
Version: 1.3.4
Revision: dev
Runtime: go1.13
次にkubectlを同様にしてインストール。
こちらはkubernetesのいわば神経系とも言うべきkube-apiサーバに対しリクエストをなげるためのバイナリです。
k8sを扱う際、基本全てkube-api経由で管理することになりますが、例えばクラスタ内のワーカーノードの情報を取得したいといった場合に
curl -X GET --cacert ../ca/ca.pem --cert admin.pem --key admin-key.pem https://balancer:443/api/v1/nodes
とGETリクエストを出し、帰ってきたJSON形式の文字列を処理して・・・といったような面倒くさいことになります。しかしこちらのバイナリを使えば
kubectl get no
と、3単語入れるだけで情報の取得から人間に読みやすい形への整形・出力を勝手にしてくれます。
素のAPI経由での操作が気になればReferenceを読むといいと思います(丸投げ
とてもいい感じにAPIサーバとのやりとりが書かれている記事もありました
→転職したらKubernetesだった件
$ {
curl -O https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# 1.15.3 >=
kubectl version --client
}
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.2", GitCommit:"52c56ce7a8272c798dbc29846288d7cd9fbae032", GitTreeState:"clean", BuildDate:"2020-04-16T11:56:40Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
03-compute-resources
本家ではGCP上にVPCの作成とインスタンスの作成を行なっているが、本稿ではここをDockerコンテナに置き換えます。
なお、ここで作成されるネットワークは以下のような構成になっています。
+---------------------------------------------------+ host machine +----------------------------------------------+
| |
| |
| docker network(k8s) |
| +-------------------------------------------+ 172.16.10.0/24 +------------------------------------------+ |
| | | |
| | | |
| | balancer | |
| | +-+ 172.16.10.10 +-+ | |
| | | | | |
| | +------------------+ | |
| | | |
| | ctl-0 ctl-1 ctl-2 | |
| | +-+ 172.16.10.20 +-+ +-+ 172.16.10.21 +-+ +-+ 172.16.10.22 +-+ | |
| | | | | | | | | |
| | +------------------+ +------------------+ +------------------+ | |
| | | |
| | worker-0 worker-1 worker-2 | |
| | +--+ 172.16.10.30 +--+ +--+ 172.16.10.31 +--+ +--+ 172.16.10.32 +--+ | |
| | | | | | | | | |
| | | podCIDR | | podCIDR | | podCIDR | | |
| | | + 10.200.0.0/24 + | | + 10.200.1.0/24 + | | + 10.200.2.0/24 + | | |
| | | | | | | | | | | | | | | |
| | | +---------------+ | | +---------------+ | | +---------------+ | | |
| | | | | | | | | |
| | | | | | | | | |
| | | | | serviceClusterCIDR | | | | |
| | +-------------------------------+ 10.100.0.0/16 +----------------------------------+ | |
| | | | | |
| | +----------------------------------------------------------------------------------+ | |
| | | | | | | | | |
| | +--------------------+ +--------------------+ +--------------------+ | |
| | | |
| +-------------------------------------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------------------------------------+
Networking
最初にNetworkingについて説明があるが、ひとまず本チュートリアルの操作に関係のある部分は存在しないので読み流す。
内容的にはkubernetesのネットワーキングの思想は「クラスタ内のpodは一意のIPを持ち、それを通じてノードを問わず互いに認識できる」ことである、というようなことが書いてある。
なお、これもチュートリアルの粋を超えている内容ではあるが、クラスタ内でアクセスコントロールする際はNetworkPolicyリソースを用いる(対応しているPluginをkubelet起動時に指定している必要がある。有名どころはCalicoとかでしょうか)
Firewall Rules
今回CentOS8で構築したため、OSインストールした初期状態ではコンテナ間の通信を許可するために下記の設定がホストマシンで必要です。
CentOS7では特に設定は必要ありませんでした。
# コンテナからWANへの通信を許可(apk利用のため)
$ {
nft add rule inet firewalld filter_FWDI_public_allow udp dport 53 accept
nft add rule inet firewalld filter_FWDI_public_allow tcp dport { 80,443 } accept
# コンテナ内部での通信を許可
nft add rule inet firewalld filter_FWDI_public_allow ip saddr 172.16.10.0/24 ip daddr 172.16.10.0/24 accept
}
これに関連して格闘した形跡をこちらに記しているので、気が向いたら読んでいただければと思います。
Kubernetes Public IP Address
Load Balancerに紐づけるIPの取得。
本稿ではコンテナでまとめて作るので省略
Compute Instances
いよいよ一つの山場。
若干長くなるのと、本家のチュートリアルから外れる内容ではあるので面倒な方はこちらをcloneしていただければそのまま次の04-certificate-authorityに進めます。
ここではLoadBalancer、ControlPlane、WorkerNodeの3種類のイメージを作成し、Dockerサービスを作成していきます。
まずDockerイメージをビルドする際無駄にデーモンが重くならないようにディレクトリを分けます。
テスト目的なのでほとんどの実行バイナリをホスト上から共有するようにしています。
$ mkdir -p build \
bin/ctl/bin bin/ctl/exec bin/worker/bin bin/worker/exec bin/worker/opt \
lib/balancer lib/ctl lib/worker/cni lib/worker/containerd lib/worker/kubelet lib/worker/kube-proxy \
lib/certs/ca lib/certs/ca_sec lib/certs/ctl lib/certs/worker \
env/common \
cert_configs
# こんな感じのディレクトリ構成になります
# .
# |-- bin
# | |-- ctl
# | | |-- bin
# | | `-- exec
# | `-- worker
# | |-- bin
# | |-- exec
# | `-- opt
# |-- build
# |-- env
# | `-- common
# |-- lib
# | |-- balancer
# | |-- certs
# | | |-- ca
# | | |-- ca_sec
# | | |-- ctl
# | | `-- worker
# | | |-- kubelet
# | | `-- kube-proxy
# | |-- ctl
# | `-- worker
# | |-- etccontainerd
# | |-- kubelet
# | |-- kube-proxy
# | `-- varcni
# `-- cert_utils
以下の内容でDockerfile、entrypoint.sh(サービス起動用のシェルスクリプト)、docker-composeファイルを作成。インスタンス数は本家と同じくLoadBalancer=1, ControlPlane=3, WorkerNode=3で構成しています。
DockerFile、docker-compose.ymlの各Instructionは本家のページを参考ください。
FROM alpine:3.11
RUN apk --update --no-cache --no-progress add \
bash \
&& rm -rf /var/cache/apk/*
#for ETCD runtime
RUN mkdir -p /etc/etcd /var/lib/etcd
#for kubernetes control
#such as api-server, conroller-manager, scheduler
RUN mkdir -p /var/lib/kubernetes/
#
COPY entrypoint.sh /
CMD [ "/bin/bash", "/entrypoint.sh" ]
FROM alpine:3.11
RUN apk --update --no-cache --no-progress add \
bash \
socat ipset conntrack-tools \
libseccomp-dev libc6-compat \
iptables \
ca-certificates \
&& rm -rf /var/cache/apk/*
# for provisioning worker node (kubelet, proxy)
RUN mkdir -p /etc/cni/net.d \
/opt/cni/bin \
/var/lib/kubelet \
/var/lib/kube-proxy \
/var/lib/kubernetes \
/var/run/kubernetes
#
# ENV PATH ${C_HOME}/bin
COPY entrypoint.sh /
CMD [ "/bin/bash", "/entrypoint.sh" ]
FROM nginx:alpine
RUN printf 'stream {\n\tinclude /etc/nginx/conf.d/*.conf.stream;\n}\n' >> /etc/nginx/nginx.conf
見ての通り全てalpineベースで構成している。
重点だけ記しておくと
-
socat ipset conntrack-tools
はチュートリアル本編に沿って入れる、ネットワーク関連 -
iptables
はkube-proxy動作のために必要だがalpineにデフォルトで入っていないので改めてインストール -
ca-certificates
は証明書インストールで、これがないとcontainerdがイメージをpullする際にx509: certificate signed by unknown authority
となってpullできない - LoadBalancerはnginxのTCPロードバランサー機能を利用します。そのためnginx.confに、別の設定ファイルをhttpブロック外でincludeする行を記載
続けてentrypoint.sh。
単純にkubernetes関連のサービス起動用に作成するシェルスクリプトをスリープを含んでキックしていくだけの代物です。
いろいろ試すためにスクリプトを修正してすぐに反映して、という進め方を想定しているので、openrcなどsysinit系は特に仕込みませんでした(本家からずれてるけど、、)
#!/bin/bash
while read svc sleepSec; do
[ -f ${svc} ] && {
echo " service '$(basename ${svc%.sh})' starting..."
/bin/bash ${svc} &
sleep ${sleepSec:-5}
echo "done."
}
done<<EOF
/exec/ETCD_SERVICE.sh 10
/exec/KUBE-API_SERVICE.sh 20
/exec/KUBE-CONTROLLER-MANAGER_SERVICE.sh
/exec/KUBE-SCHEDULER_SERVICE.sh
/exec/RBAC_AUTH.sh
/exec/CONTAINERD_SERVICE.sh 30
/exec/KUBELET_SERVICE.sh 20
/exec/KUBE-PROXY_SERVICE.sh
EOF
echo "container Ready"
tail -f /dev/null
最後にdocker-composeファイル。LinuxCapability について勉強する機会となったので最小限のCapabilityとなるように気をつけましたが、ワーカーノードは結局--privilrged
をつけないと起動できなかった、、
version: "3.7"
x-services:
&default-ctl
build:
context: ./build
dockerfile: Dockerfile_ctl
image: k8snodes-ctl:0.1
init: true
networks:
k8s:
volumes:
- "/sys/fs/cgroup:/sys/fs/cgroup:rw"
- "./bin/ctl/exec:/exec:ro"
- "./bin/ctl/bin:/usr/local/bin:ro"
- "./lib/certs/ctl:/var/lib/kubernetes:ro"
- "./lib/certs/ca:/var/lib/ca:ro"
- "./lib/certs/ca_sec:/var/lib/ca_sec:ro"
cap_drop:
- ALL
cap_add:
- SYS_CHROOT
- SETGID
- SETUID
- CHOWN
- NET_RAW
- NET_ADMIN
- DAC_OVERRIDE
dns_search:
- k8s
environment:
- KUBECONFIG=/var/lib/kubernetes/admin.kubeconfig
x-services:
&default-worker
build:
context: ./build
dockerfile: Dockerfile_worker
image: k8snodes-worker:0.1
init: true
networks:
k8s:
volumes:
- "/lib/modules:/lib/modules:ro"
- "./bin/worker/exec:/exec:ro"
- "./bin/worker/bin:/usr/local/bin:ro"
- "./bin/worker/opt:/opt/cni/bin:ro"
- "./lib/worker/etccontainerd:/etc/containerd:ro"
- "./lib/worker/varcni:/var/cni:ro"
- "./lib/worker/kubelet:/var/lib/kubelet_config:ro"
- "./lib/worker/kube-proxy:/var/lib/kube-proxy_config:ro"
- "./lib/certs/worker/kubelet:/var/lib/kubelet_certs:ro"
- "./lib/certs/worker/kube-proxy:/var/lib/kube-proxy:ro"
- "./lib/certs/ca:/var/lib/ca:ro"
- type: tmpfs
target: /var/lib/containerd
tmpfs:
size: 500M
privileged: true
dns_search:
- k8s
x-services:
&default-balancer
build:
context: ./build
dockerfile: Dockerfile_lb
image: nginx_lb:alpine
ports:
- "10443:443"
volumes:
- "./lib/balancer:/etc/nginx/conf.d:ro"
networks:
k8s:
cap_drop:
- ALL
cap_add:
- NET_RAW
- NET_BIND_SERVICE
- CHOWN
- SETGID
- SETUID
services:
balancer:
<< : *default-balancer
container_name: balancer
hostname: balancer
networks:
k8s:
ipv4_address: 172.16.10.10
ctl-0:
<< : *default-ctl
container_name: ctl-0
hostname: ctl-0
networks:
k8s:
ipv4_address: 172.16.10.20
ctl-1:
<< : *default-ctl
container_name: ctl-1
hostname: ctl-1
networks:
k8s:
ipv4_address: 172.16.10.21
ctl-2:
<< : *default-ctl
container_name: ctl-2
hostname: ctl-2
networks:
k8s:
ipv4_address: 172.16.10.22
worker-0:
<< : *default-worker
container_name: worker-0
hostname: worker-0
networks:
k8s:
ipv4_address: 172.16.10.30
worker-1:
<< : *default-worker
container_name: worker-1
hostname: worker-1
networks:
k8s:
ipv4_address: 172.16.10.31
worker-2:
<< : *default-worker
container_name: worker-2
hostname: worker-2
networks:
k8s:
ipv4_address: 172.16.10.32
networks:
k8s:
name: k8s
ipam:
config:
- subnet: 172.16.10.0/24
driver_opts:
com.docker.network.bridge.name: k8s_br
これも要点だけまとめると
-
x-
接頭辞はExtensionフィールドであり、記述はできるが解釈されなくなるので、yamlのアンカー・エイリアス構文を用いることができるようになる -
/sys/fs/cgroup:/sys/fs/cgroup:rw
の権限でのマウントは必須だが、workerではprivilegedオプションをつけているので省略可 - workerノードの
/lib/modules:/lib/modules:ro
はkube-proxyに求められるが、無視しても問題ないとは言われるので必須ではない - workerノードのcontainerdのルートディレクトリに
tmpfs
をマウントは、デフォルトのoverlayfsではエラーとなるため必須 - dns-searchは互いのノードIPを調べる際に必要となる。<dokcer networkの名前>というドメインに設定されるので、そこに合わせるようにする。
- docker-composeのバージョン3系を用いる場合、
init: true
の部分に関しては3.7>=
となっている。ただしこのオプションを付けなくても特に問題はない。ただしExtensionフィールドは3.4>=
となっているのでそこだけ注意
以上です。少々長くなりましたが、これでkubernetesの各コンポーネントを動かす準備ができました。
一応本家ではsshで各インスタンスにアクセスするようになっていますが、ここではdockerなのでプロセスのアタッチで各ノードに入ります。
$ docker exec -ti <container name> sh
04-certificate-authority
kubernetesの各コンポーネントはクラウド基板上で稼働することが前提であり、互いの接続をTLS認証で行います。
このセクションでは自分用PKIを作成して自己署名した証明書を作ります。
まずはCAを作ります。
$ cd cert_util
$ {
cat > ca-config.json <<EOF
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"kubernetes": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "8760h"
}
}
}
}
EOF
cat > ca-csr.json <<EOF
{
"CN": "Kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "JP",
"L": "Tokyo",
"O": "Kubernetes",
"OU": "CA",
"ST": "Asia"
}
]
}
EOF
# C : Country
# L : Locality or City
# O : Organization
# OU : Organization Unit
# ST : State
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
}
CAの公開鍵認証用のペアができます。
ca-key.pem
ca.pem
ここからはkubernetesクラスタのサーバの各コンポーネントと、kubectlを扱うクライアントのための証明書を作成し、このCAによって署名していきます。
各証明書の役割は以下の通りです
- adminユーザ用 (controlプレーン&クライアント)
- kubelet用 (workerノード)
- kube-proxy用 (workerノード)
- Controller-Manager用 (controlプレーン)
- Scheduler用 (controlプレーン)
- Service-Account-Key用 (controlプレーン)
- kubernetes-API-server(兼etcd)用 (controlプレーン)
Service Account Keyに関してはmanaging service accountsに記載ありますが、Contoroller Managerとkube-api-serverコンポーネントで用いられ、各podのプロセスがデフォルトの状態でkube-api-serverにアクセスする権限を持たせるために使われる。ということらしいです。
まずはadminユーザ用。これはcontrolプレーン、及び自分がクラスタと更新する際にkubectlから呼び出して使う用の証明書になります。
# in ./cert_util
$ sed '
/"CN"/s/Kubernetes/admin/
/"O"/s/Kubernetes/system:masters/
/"OU"/s/CA/Kubernetes The Hard Way/
' ca-csr.json | tee admin-csr.json
$ cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
admin-csr.json | cfssljson -bare admin
上と同じく、adminと頭についた鍵ペアができます。
*.csr
のファイルはcaで署名される前の署名要求なので、基本鍵ペア以外は不要です。
次にworkerノードの各コンポーネント用の証明書ペアを作成していきます。
kubeletではNode Authorizerという認証モードを用いてkubeletからのAPIリクエストを認証するようです。これを用いるためには 「system:nodes
グループ」に属し、「system:node:<nodeName>
というユーザ名」を持った証明書をkubeletが用いる必要があるようです。公式のドキュメントにもしっかりと記載がありました。
x509公開鍵認証ではCommonName(CN)フィールドの値が「ユーザ名」、Organization(O)フィールドの値が「グループ」となるようなのでここの整合性がちゃんと取れるように気をつけます。ちなみにnodeNameはworkerノード内でhostname
で帰ってくる値と一致している必要がありました。
# in ./cert_util
$ for instance in worker-0 worker-1 worker-2
do sed '
/"CN"/s/admin/system:node:'${instance}'/
/"O"/s/system:masters/system:nodes/
' admin-csr.json | tee ${instance}-csr.json
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-hostname=${instance} \
-profile=kubernetes \
${instance}-csr.json | cfssljson -bare ${instance}
done
-hostname
で証明書のSAN (Subject Altenative Name)を設定できます。この証明書を使って接続するリモート側のDNS名、もしくはアドレスが、CNもしくはSANに設定されている値に一致しないと証明書エラーとなってしまいます。
実際に試したところkubectlを用いてpodにアタッチする際、kubeletは自身に対してAPIリクエストを出しており、ここで-hostname
を指定しなければkubectl exec
やkubectl logs
を実行しようとする際に証明書エラーとなりました。
本家ではworkerノード自体のInternal・External IPを指定していましたが、特に指定しなくでも(今のところは)動かせたので確認できたケースではDNS名しか要求していない模様。
問題生じたらアップデートしようかと思います。
長くなりましたが次にkube-proxy用の証明書。
ここからは特に意識することはなく本家通り粛々と作成していきます。
# in ./cert_util
$ sed '
/"CN"/s/admin/system:kube-proxy/
/"O"/s/system:masters/system:node-proxier/
' admin-csr.json | tee kube-proxy-csr.json
$ cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-proxy-csr.json | cfssljson -bare kube-proxy
次にkube-controller-manager用の証明書。一応、ここからはcontrolプレーンの各コンポーネントになります。
# in ./cert_util
$ sed '
/"CN"/s/admin/system:kube-controller-manager/
/"O"/s/system:masters/system:kube-controller-manager/
' admin-csr.json | tee kube-controller-manager-csr.json
$ cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager
kube-scheduler用
# in ./cert_util
$ sed '
/"CN"/s/admin/system:kube-scheduler/
/"O"/s/system:masters/system:kube-scheduler/
' admin-csr.json | tee kube-scheduler-csr.json
$ cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
kube-scheduler-csr.json | cfssljson -bare kube-scheduler
service-account用
# in ./cert_util
$ sed '
/"CN"/s/admin/service-accounts/
/"O"/s/system:masters/Kubernetes/
' admin-csr.json | tee service-account-csr.json
$ cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=kubernetes \
service-account-csr.json | cfssljson -bare service-account
最後にkubernetes-api-server用なのですが、こいつは最低限etcd達とLoad Balancer、あとkubernetesが自動的に作成するサービスリソースのClusterIP(後ほど割り当てるサービスCIDRの、一番最初の序列のIP。CIDRが10.100.0.0/16なら10.100.0.1となる)につながれば動作できたので、そこにSANを限定していきます(本家ではデフォルトで作成されるサービスリソース(kubernetes)のいろいろな形でのドメイン含めた名前が加えられており、そこから外れているが最低限の設定の方が不具合発生時した際に勉強になる部分が多いと感じるので、、)
# in ./cert_util
$ sed '
/"CN"/s/admin/kubernetes/
/"O"/s/system:masters/Kubernetes/
' admin-csr.json | tee kubernetes-csr.json
$ {
LOs='localhost,127.0.0.1'
# Control Plane
CTLs='172.16.10.20,172.16.10.21,172.16.10.22'
# kubernetes services
SVC='10.100.0.1,kubernetes,balancer'
# 外部のクライアントからアクセスする用の、dockerホスト
EXTERNAL='172.16.10.1'
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-hostname=${CTLs},${LOs},${SVC},${EXTERNAL} \
-profile=kubernetes \
kubernetes-csr.json | cfssljson -bare kubernetes
}
最後にしかるべき場所にこれらの証明書を配置します。
kube-proxy
,kube-controller-manager
,kube-scheduler
, andkubelet
は
次の段階でkubeconfigファイルに含めて用いるので(あと一応、admin
も)、これらは動かしません。
# in ./cert_util
$ {
cp -p ca.pem ../lib/certs/ca
mv ca-key.pem ../lib/certs/ca_sec
mv service-account*.pem ../lib/certs/ctl
mv kubernetes*.pem ../lib/certs/ctl
}
05-kubernetes-configuration-files
ここではkubernetes APIのクライアントとなる
controller manager
,kubelet
,kube-proxy
, andscheduler
clients and theadmin
user.
のための、接続のための認証情報と接続先の情報をkubeconfigファイルという物の中に埋め込んでいきます。
クライアント達の接続先はローカルホスト、及びLoad Balancerとなるので、Load BalancerのIPを本家では取得しています。本稿ではdocker-compose.ymlの中で定めた172.16.10.10
です。
$ KUBERNETES_PUBLIC_ADDRESS='balancer'
それでは作成していきます。
# in ./cert_util
$ {
for instance in worker-0 worker-1 worker-2; do
kubectl config set-cluster kubernetes-the-hard-way \
--certificate-authority=ca.pem \
--embed-certs=true \
--server=https://${KUBERNETES_PUBLIC_ADDRESS}:443 \
--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
}
# in ./cert_util
$ {
kubectl config set-cluster kubernetes-the-hard-way \
--certificate-authority=ca.pem \
--embed-certs=true \
--server=https://${KUBERNETES_PUBLIC_ADDRESS}:443 \
--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
}
# in ./cert_util
$ {
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
}
# in ./cert_util
$ {
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
}
# in ./cert_util
$ {
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
}
最後にこれらもしかるべき場所に配置します。一応、kubelet用のworker-?.kubeconfig
は本来それぞれのノードにあるべきですが特にそこまで拘らなくてもいいかと思いひとところにまとめています。
# in ./cert_util
$ {
mv admin.kubeconfig kube-scheduler.kubeconfig kube-controller-manager.kubeconfig ../lib/certs/ctl/
mv kube-proxy.kubeconfig ../lib/certs/worker/kube-proxy/
mv worker-?.kubeconfig ../lib/certs/worker/kubelet/
mv worker-*.pem ../lib/certs/worker/kubelet/
}
06-data-encryption-keys
ここではKubernetes上で扱われるデータ(APIリソース)の暗号化を行う設定ファイルを作成します。
ここで作成するファイルに関して言えば、APIリソースの中でもsecretsリソースに対して暗号化するようにしています。13-smoke-test
で動作確認します。
$ cd ..
# in .
$ cat | tee lib/certs/ctl/encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: $(head -c 32 /dev/urandom | base64)
- identity: {}
EOF
07-bootstrapping-etcd
ここではconrolプレーンのデータストアであるetcdを起動させます。
各controlプレーン内で起動させ、3台でクラスタ構成を組みます。
まず、etcd
とetcdctl
のバイナリを入手します
# in .
$ {
curl -LO "https://github.com/etcd-io/etcd/releases/download/v3.4.0/etcd-v3.4.0-linux-amd64.tar.gz"
# 最新版は https://github.com/etcd-io/etcd/releases をチェック
tar -xf etcd-v3.4.0-linux-amd64.tar.gz
mv etcd-v3.4.0-linux-amd64/etcd* bin/ctl/bin/
# controlプレーンで/usr/local/binにマウントする領域。移動したもの以外は削除してよし
rm -fr etcd-v3.4.0-linux-amd64*
ls -lh bin/ctl/bin/
}
etcd起動用のシェルを書きます。
それぞれのホスト名とdockerから割り当てられているIPを動的に入手するようにしています。
それと、証明書類について前章で作成したkubernetes*.pem
が必要になりますが、本編での設定と合わせるためシンボリックリンク貼るようにしています。
#!/bin/bash
ETCD_NAME=$(hostname -s)
INTERNAL_IP=$(awk '/'${ETCD_NAME}'/ {print $1}' /etc/hosts)
CTLs=$(IFS=',';for i in {0..2};do CTLs[$i]=ctl-${i}=https://172.16.10.2${i}:2380;done;echo "${CTLs[*]}"; )
for link_target in /var/lib/kubernetes/kubernetes.pem /var/lib/kubernetes/kubernetes-key.pem /var/lib/ca/ca.pem; do
dest="/etc/etcd/$(basename ${link_target})"
[ -L ${dest} ] || ln -s ${link_target} ${dest}
done
/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 ${CTLs} \
--initial-cluster-state new \
--log-outputs '/etc/etcd/etcd.log' \
--logger=zap \
--data-dir=/var/lib/etcd
オプション名でなんとなく判別できますが、一応要点だけまとめると
- etcdはデフォルトでクラスタ間用の通信にポート番号
2380
を、クライアント用に2379
を用いる(もちろん変更可) - peerでクラスタ間の通信について定義
- clientで外部からのリクエストを受け付ける通信について定義。ローカルを入れてるのはデバッグ時にetcdctlを用いるが、そのデフォルトの向き先がそうなっているため
- clientからの通信の際は
--key
と--cert
で指定した証明書類と適合するものが必要になる -
--log-outputs
は本家では指定されていない。おそらくsysinitで起動するためと思われるが、ここでは明示的に指定して出力させておく
次に動作確認です。
これだけ作成した状態でいったんサービスを起動させます。
# in .
$ docker-compose up -d ctl-{0..2}
Creating network "k8s" with the default driver
Creating ctl-2 ... done
Creating ctl-0 ... done
Creating ctl-1 ... done
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ed27920de960 k8snodes-ctl:0.1 "/bin/bash /entrypoi…" 47 seconds ago Up 44 seconds ctl-1
3e39308fc506 k8snodes-ctl:0.1 "/bin/bash /entrypoi…" 47 seconds ago Up 44 seconds ctl-0
5ca113b3596e k8snodes-ctl:0.1 "/bin/bash /entrypoi…" 47 seconds ago Up 44 seconds ctl-2
$ docker exec -ti ctl-0 /bin/sh -c "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 -w table"
+------------------+---------+-------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+-------+---------------------------+---------------------------+------------+
| 61c4f7534361ad48 | started | ctl-1 | https://172.16.10.21:2380 | https://172.16.10.21:2379 | false |
| b167b173b818a05c | started | ctl-0 | https://172.16.10.20:2380 | https://172.16.10.20:2379 | false |
| f6d332cd022ef5f8 | started | ctl-2 | https://172.16.10.22:2380 | https://172.16.10.22:2379 | false |
+------------------+---------+-------+---------------------------+---------------------------+------------+
ちゃんと表示されば成功です。
失敗したら、大抵の場合はdockerホスト側のNetfilterの設定がうまくできていない可能性が高いです。
ここまで確認できたらいったん止めます。
$ docker-compose stop
08-bootstrapping-kubernetes-controllers
次にcontrolプレーンの他のコンポーネントを起動していきます。
まずコンポーネント用のバイナリを入手し、共有領域に置いてあげます。
# in .
$ {
curl -L \
-O "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-apiserver" \
-O "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-controller-manager" \
-O "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-scheduler" \
-O "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl"
chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl
mv kube-apiserver kube-controller-manager kube-scheduler kubectl bin/ctl/bin
}
続けてそれぞれのサービスを起動するためのスクリプトを書いていきます。
APIサーバ
#!/bin/bash
INTERNAL_IP=$(awk '/'$(hostname -s)'/ {print $1}' /etc/hosts)
CTLs=$(IFS=',';for i in {0..2};do CTLs[$i]=https://172.16.10.2${i}:2380;done;echo "${CTLs[*]}"; )
# add route to other node's pods
# this config required in "11-pod-network-routes" section
for ((i=0;i<3;i++));do
ip r add to 10.200.$i.0/24 via 172.16.10.3$i
done
LOGFILE='/var/log/kube-api.log'
/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/ca/ca.pem \
--enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
--etcd-cafile=/var/lib/ca/ca.pem \
--etcd-certfile=/var/lib/kubernetes/kubernetes.pem \
--etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \
--etcd-servers=${CTLs} \
--event-ttl=1h \
--encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \
--kubelet-certificate-authority=/var/lib/ca/ca.pem \
--kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \
--kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \
--kubelet-https=true \
--runtime-config=api/all=true \
--service-account-key-file=/var/lib/kubernetes/service-account.pem \
--service-cluster-ip-range=10.100.0.0/16 \
--service-node-port-range=30000-32767 \
--tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
--tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
--log-file=${LOGFILE} \
--logtostderr=false \
--v=2
- ここでもログファイルを明示的に指定する部分を追加しています。
- APIリソースの中のserviceリソースが割り当てられるCIDRもここで指定しています。Controller-Managerの起動オプションの中でも指定できますが、こちらで設定した内容の方が優先されました
Controller-Manager
#!/bin/bash
LOGFILE="/var/log/kube-controller-manager.log"
/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/ca/ca.pem \
--cluster-signing-key-file=/var/lib/ca_sec/ca-key.pem \
--kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
--leader-elect=true \
--root-ca-file=/var/lib/ca/ca.pem \
--service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \
--use-service-account-credentials=true \
--log-file=${LOGFILE} \
--logtostderr=false \
--v=2
kube-Scheduler
ここだけConfigファイルでパラメータを渡します。
基本k8sではConfigファイルを経由してオプション類を渡すことが推奨されています。
APIとController-ManagerだけなぜかConfigファイルを渡すためのオプション等がなくベタ書きという感じでした。コアモジュールだからかわかりませんが、そのうち設定できるようになるのかもしれません。
# in .
$ cat <<EOF | sudo tee lib/certs/ctl/kube-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
leaderElect: true
EOF
#!/bin/bash
CONFIGFILE='/var/lib/kubernetes/kube-scheduler.yaml'
LOGFILE='/var/log/kube-scheduler.log'
/usr/local/bin/kube-scheduler \
--config=${CONFIGFILE} \
--log-file=${LOGFILE} \
--logtostderr=false \
--v=2
本家ではここで外部のロードバランサーが行うヘルスチェックのため、contarolプレーンの80番ポートに対するリクエストを、kube-api-serverが公開するhttps://127.0.0.1:6443/healthz
へと中継させるようにnginxのproxy機能を活用する設定をしています。
が、本稿で用いているロードバランサーはフリー版のnginxで代用しており、製品バージョンでしかActiveヘルスチェック機能は提供されないようでしたので、やるとしたらPassiveですがここまでマネなくてもいいかと思い割愛しました。
最後にkube-api-serverから各workerノードのkubeletにRBACでアクセスするための設定をします。
これはpodのログを取得する際やコマンドを実行する際に用いられます。
後ほどpodは起動するけど上記のような操作ができないといった場合にはこちらのClusterRoleリソースがちゃんと作られているかどうか確認するといいと思います。
こちらはAPIサーバで管理されるリソース作りなので、一度作成すればクラスタ全体で有効になりますが、特に何度適用しても問題はないのでサービス起動時のスクリプトに含めています。
ちなみにこの際にadminユーザのkubeconfigが用いられるようになっています。
#!/bin/bash
CONFIGFILE='/var/lib/kubernetes/admin.kubeconfig'
cat <<EOF | kubectl apply --kubeconfig ${CONFIGFILE} -f -
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdata: "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:
- "*"
---
apiVersion: rbac.authorization.k8s.io/v1beta1
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
- kube-api-serverは
--kubelet-client-certificate
で指定した証明書で持って、kubernetes
ユーザとしてkubeletにアクセスしにいく - そのため作成したClusterRoleリソースを
kubernetes
ユーザに紐づけるClusterRoleBindingリソースを作成している
API起動時に上記のオプションで指定した証明書(kubernetes.pem)のCNはkubernetesでした。
それをユーザ名としてリクエストが正当かどうか(resourceに対してverbを行っても良いか)Authorizeするための設定ということですね。
ここまできたところで、Controlプレーンのコンポーネントは全て起動するようになっているはずなので、いったんサービスを起動しヘルスチェックを行います。
$ docker-compose up --force-recreate -d ctl-{0..2}
$ docker-compose logs -f
# 'container Ready'が表示されるまで待ちます。<C-c>
$ docker exec -ti ctl-0 sh
# in ctl-0 container
$ kubectl get cs
NAME STATUS MESSAGE ERROR
controller-manager Healthy ok
scheduler Healthy ok
etcd-0 Healthy {"health":"true"}
etcd-2 Healthy {"health":"true"}
etcd-1 Healthy {"health":"true"}
$ kubectl get clusterRole | grep apiserver-to-
system:kube-apiserver-to-kubelet 2020-06-14T05:51:06Z
$ kubectl get clusterRoleBinding -o wide| grep apiserver-to-
system:kube-apiserver 10m ClusterRole/system:kube-apiserver-to-kubelet kubernetes
# 上記のような出力があれば正常に動作している
# 'get cs'の結果は帰ってくるけどclusterRole関連が帰ってこない場合はリソースの関係で起動が遅れているので、下記のコマンドで改めて登録し直す
$ bash /exec/RBAC_AUTH.sh
# 確認できたらホストマシンに戻ります。
$ exit
このセクションの最後にロードバランサーにルールを追加し、正常にバランシングが行われているか確かめています。
本稿ではここをnginx用の設定に置き換えています。
# in .
$ cat <<EOF | tee lib/balancer/lb.conf.stream
upstream k8s_ctl {
server ctl-0:6443;
server ctl-1:6443;
server ctl-2:6443;
}
server {
listen 443;
proxy_pass k8s_ctl;
}
EOF
$ docker-compose up -d balancer
Creating balancer ... done
# dockerのホストマシンの10443番ポートに紐づけられていることを確認
$ docker port balancer
# 疎通確認
$ curl --cacert lib/certs/ca/ca.pem https://127.0.0.1:10443/version
{
"major": "1",
"minor": "18",
"gitVersion": "v1.18.1",
"gitCommit": "7879fc12a63337efff607952a323df90cdc7a335",
"gitTreeState": "clean",
"buildDate": "2020-04-08T17:30:47Z",
"goVersion": "go1.13.9",
"compiler": "gc",
"platform": "linux/amd64"
}
証明書類を指定しないアクセスはsystem:anonymous
ユーザと解釈されAuthorizationで弾かれますが、/versionは取得できるようです。
上記のような出力が帰ってくれば正常にバランシングできていることになります。
長かったですがこれでやっとcontrolプレーンが動くようになりました。
次のセクションでworkerノードを動かせばひとまずクラスタとしては動くようになりますのでもうひと頑張りです。
09-bootstrapping-kubernetes-workers
ここではWorkerノードを作成していきます。
最初に依存パッケージを入れていますが、これはDockerfileで入れているため特に操作は不要です。
次にswappingをオフにすることがデフォルトで求められます。
Dockerの場合はホストマシンのシステムファイルを共有するため、ホストマシン(か、--priviledged
しているのでworkerからでもいいですが)にて swapoff
しないとデフォルト設定のkubeletの起動に失敗します。
が、本稿ではそれほど本格的に運用することを目的としているわけではなく、もしswappingがが起きてもそれほど深刻なことにはならないので、swapoff
にせずにkubeletの設定の方を変えます。
まず、Workerノード用のバイナリをダウンロードし、展開します。
kubectlも再度ダウンロードしているのですが、workerノードからの使いどころがよくわからなかったので省略しています。
# in .
$ curl -L \
-O https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.18.0/crictl-v1.18.0-linux-amd64.tar.gz \
-O https://github.com/opencontainers/runc/releases/download/v1.0.0-rc8/runc.amd64 \
-O https://github.com/containernetworking/plugins/releases/download/v0.8.2/cni-plugins-linux-amd64-v0.8.2.tgz \
-O https://github.com/containerd/containerd/releases/download/v1.2.13/containerd-1.2.13.linux-amd64.tar.gz \
-O https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-proxy \
-O https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubelet
$ {
tar -xf containerd-1.2.13.linux-amd64.tar.gz -C bin/worker/
tar -xf cni-plugins-linux-amd64-v0.8.2.tgz -C bin/worker/opt/
tar -xf crictl-v1.18.0-linux-amd64.tar.gz -C bin/worker/bin/
mv runc.amd64 runc
chmod +x kube-proxy kubelet runc
mv kube-proxy kubelet runc bin/worker/bin
rm -f cni-plugins-linux-amd64-v0.8.2.tgz crictl-v1.18.0-linux-amd64.tar.gz containerd-1.2.13.linux-amd64.tar.gz
}
次にcniの設定を行います。
このチュートリアルではコンテナランタイムとしてcontainerdを直接用いていますが、こいつがpodを作る際にこちらのcniを用いてネットワークデバイスを割り当てます。
kubeletではなく自分の手でcniを使ってネットワークデバイスを作成するといった試みはネットワーク上探すと結構出てきますが、大元の使い方はこちらにありますので、やってみると余計kubeletのありがたみを感じるかもしれません(知らんけど)
設定にあたり、ipam
のhost-local
では、ここで定義したレンジからpodにアドレスを割り当てます。あくまでkubeletが管理しており、kube-api-serverは「こういうpod作ったよ」情報を保持するのみです。
つまり、ここで渡すCIDRが被ってしまうと同じIPを持つpodがたくさんできてしまいアクセスできなくなるので、workerノードごとにCIDRレンジを変更する必要があります。
そのように動的に設定するため、まずは各workerノードコンテナ内の/var/cniに一時的に設定ファイルを起き、containerd起動のスクリプト内で置換して本来の場所に移すように作ります。
# in .
$ cat <<EOF | sudo tee lib/worker/varcni/10-bridge.conf
{
"cniVersion": "0.3.1",
"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 lib/worker/varcni/99-loopback.conf
{
"cniVersion": "0.3.1",
"name": "lo",
"type": "loopback"
}
EOF
次にcontainerdを起動するスクリプトを書いていきます。
containerdは外部から設定ファイルを読み込みます。
ファイルにて設定できる項目はcontainerd config default
コマンドで確認できます。
$ cat << EOF | tee lib/worker/etccontainerd/config.toml
[plugins]
[plugins.cri.containerd]
snapshotter = "overlayfs"
[plugins.cri.containerd.default_runtime]
runtime_type = "io.containerd.runtime.v1.linux"
runtime_engine = "/usr/local/bin/runc"
runtime_root = ""
EOF
#!/bin/bash
LOGFILE="/var/log/containerd.log"
cp -p /var/cni/* /etc/cni/net.d/
POD_CIDR="10.200.${HOSTNAME#worker-}.0/24"
CNICONF='/etc/cni/net.d/10-bridge.conf'
sed -i 's|${POD_CIDR}|'${POD_CIDR}'|' ${CNICONF}
/usr/local/bin/containerd \
--config /etc/containerd/config.toml >>${LOGFILE} 2>&1
次にkubeletの起動スクリプト
workerノード内のバイナリは基本設定ファイルを読み込ませる形で起動します。
まずは設定ファイルの中身です。ここでもノードごとに動的に変えたい部分が存在しますので、kubelet起動時に置き換えるようにしています。
本家ではここでもPODCIDRを指定していますが、これはstandaloneモードでのみ用いられるもののようです。今回はクラスタモードで起動するので特に指定する必要はありません。さらに言えばhost-localなcniを用いるのでMasterノードで行うべき設定すらも関係ありません。
それと、各ノードで実行されるpodへの通信を確保するためにルーティングテーブルを作成する必要があります。そのルーティングも動的に実行するように設定しています。
まずは設定ファイルです。
# in .
$ cat <<EOF | tee lib/worker/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: "/var/lib/ca/ca.pem"
authorization:
mode: Webhook
clusterDomain: "cluster.local"
clusterDNS:
- "10.100.0.10"
# resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet_certs/\${HOSTNAME}.pem"
tlsPrivateKeyFile: "/var/lib/kubelet_certs/\${HOSTNAME}-key.pem"
failSwapOn: false
EOF
起動スクリプト
#!/bin/bash
CONFIGFILE="/var/lib/kubelet-config.yaml"
LOGFILE="/var/log/kubelet.log"
NodeNum=${HOSTNAME#worker-}
sed -e '
s|${HOSTNAME}|'${HOSTNAME}'|g
' /var/lib/kubelet_config/kubelet-config.yaml > ${CONFIGFILE}
# add route to other node's pods
# this config required in "11-pod-network-routes" section
for ((i=0;i<3;i++));do
[ $i -ne ${NodeNum} ] && ip r add to 10.200.$i.0/24 via 172.16.10.3$i
done
/usr/local/bin/kubelet \
--config=${CONFIGFILE} \
--container-runtime=remote \
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
--image-pull-progress-deadline=2m \
--kubeconfig=/var/lib/kubelet_certs/${HOSTNAME}.kubeconfig \
--network-plugin=cni \
--register-node=true \
--logtostderr=false \
--log-file=${LOGFILE} \
--v=2
最後にkube-proxyです。
こいつはノードごとのネットワーク設定を、主にiptables(可変だけども)を用いて操作する担当です。
なのでServiceリソースを作った時なんかに各ノードにてiptablesでみてみると、確率でトラフィックを振り分けているServiceの実態が見えたりします。
それと、dockerコンテナの方に--priviledged
をつけているにもかかわらずデフォルトで/proc/sys以下をReadOnlyモードでマウントするらしく、ここをReadWriteでリマウントしてあげないとこれもまたエラーになります。Workerノードは何もかも捧げているといった感じですね。
ここでもCIDRを指定していますが、ここでの意味合いは指定したCIDRレンジ以外へのトラフィックをマスカレードするという設定になるらしいです。
# in .
cat <<EOF | sudo tee lib/worker/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
kubeconfig: "/var/lib/kube-proxy/kube-proxy.kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"
conntrack:
maxPerCore: 0
EOF
CONFIGFILE="/var/lib/kube-proxy_config/kube-proxy-config.yaml"
LOGFILE="/var/log/kube-proxy.log"
#re-mount the Required File System if that was as ReadOnly mode
RFS='/proc/sys'
[[ $(awk -v RFS=${RFS} '/^proc/ {if($2==RFS){split($4, mode, ",")}}END{print mode[1]}' /proc/mounts) == 'ro' ]] && \
mount -o remount,rw,bind ${RFS} ${RFS}
/usr/local/bin/kube-proxy \
--config=${CONFIGFILE} \
--logtostderr=false \
--log-file=${LOGFILE} \
--v=2
以上で k8sクラスタコンポーネントの起動設定は完了です。
Workerも起動し、無事ノードとして認識されるか確かめて次のセクションに移動します。
# in .
$ docker-compose up -d worker-{0..2}
# 'container Ready'が表示されるくらいまで待ちます
$ docker exec -it ctl-0 kubectl get no
NAME STATUS ROLES AGE VERSION
worker-0 NotReady <none> 7s v1.18.1
worker-1 NotReady <none> 7s v1.18.1
worker-2 NotReady <none> 7s v1.18.1
10-configuring-kubectl
このセクションでは、自分作成したクラスタに対して任意の場所からアクセスできるようにします。
そのためにはkubeconfigという設定ファイルに接続先と認証情報を書き込み、kubectlを打つときにこの設定ファイルを参照するようにしてあげます。
まず接続先はbalancer(つまりdockerホストのネットワークデバイス(172.16.10.1)の10443ポート)になります。
# in .
$ (
cd cert_util/
kubectl config set-cluster kubernetes-the-hard-way \
--certificate-authority=ca.pem \
--embed-certs=true \
--server=https://172.16.10.1:10443
kubectl config set-credentials admin \
--client-certificate=admin.pem \
--client-key=admin-key.pem \
--embed-certs=true
kubectl config set-context kubernetes-the-hard-way \
--cluster=kubernetes-the-hard-way \
--user=admin
kubectl config use-context kubernetes-the-hard-way
)
kubectlが用いるconfigファイルは
-
--kubeconfig
で指定した値 -
$KUBECONFIG
の値 ${HOME}/.kube/config
の順で参照されますので、特に何もいじっていないこの場合は${HOME}/.kube/config
にここで設定した値が保存されています。
最後に疎通確認です。
# from docker host machine
$ kubectl get cs
NAME STATUS MESSAGE ERROR
scheduler Healthy ok
controller-manager Healthy ok
etcd-2 Healthy {"health":"true"}
etcd-0 Healthy {"health":"true"}
etcd-1 Healthy {"health":"true"}
11-pod-network-routes
ここではあるpodから別のworkerノードで動いているpodに対しても疎通ができるようにルーティング設定を行います。具体的にはpodからのトラフィックを、宛先podが動いているworkerノードのethデバイスにルーティングするようにします。
本来GWであるdockerホストにおいて、これように作成されるデバイス用に下記のように設定を行うべきです、が、実は本チュートリアルにおいて、この設定自体は08-bootstrapping-kubernetes-controllers
及び09-bootstrapping-kubernetes-workers
内のkubelet起動スクリプトの中に組み込んでいます ので、pod宛の通信は各ノードが直接ルーティングするようになっています。
これによりdockerコンテナの方をdownさせると自動的に設定自体も削除されるので、個人的にはよりクリーンかなと。
ということでこのセクションも割愛します。
一応解説だけしておくと、キモの部分は自分以外のpodへのトラフィックのみルーティングを行うという部分です。
今回workerノードとなっているdockerコンテナは、基本vethデバイスをdokcerホストマシン内に作成されるbridgeデバイスに差し込んで各種通信を行います。そのため各種通信は必ずホストマシンのbridgeデバイスを介して行われます。詳しくは別で記事にしています。
同様のことがworkerとpodの間でも行われます。つまりpodからの通信はworker内に自動で生成されるbridgeデバイスを介して行われるので、もしここで自分の中で動いているpodへのトラフィックを自分のethデバイスにむけてルーティングするとそこでループとなりトラフィックが消失します(最初手探りで構築していたので、途中でトラフィックが消えていたことをコンテナのバグかと思い込みすごくハマりました、、、)。
12-dns-addon
ここではpod及びserviceリソースに対する名前解決を行えるようにするaddonをデプロイします。
kubernetesで使用されるDNSは、現在ではkube-dnsというのがレガシーでcorednsがモダンのようです。
本家でも corednsを用いているので素直にその通りにします。
ただ当チュートリアルでは手作り感を出すためにserviceリソース用のCIDRを変更しており、本家で行っているデプロイ用のYAMLをそのまま用いることはできないので少しだけ変更を加えます。
まず、元となるYAMLをダウンロードします。
# in .
$ curl -O https://storage.googleapis.com/kubernetes-the-hard-way/coredns.yaml
# お好みのテキストエディタか下記コマンドで該当箇所修正します。
$ sed -i '/clusterIP/s/32/100/' coredns.yaml
$ kubectl apply -f coredns.yaml
serviceaccount/coredns created
clusterrole.rbac.authorization.k8s.io/system:coredns created
clusterrolebinding.rbac.authorization.k8s.io/system:coredns created
configmap/coredns created
deployment.apps/coredns created
service/kube-dns created
# ちゃんと作成され、DNSサービスが機能するか確かめます
$ kubectl get pods -l k8s-app=kube-dns -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-589fff4ffc-d8745 1/1 Running 0 21s
coredns-589fff4ffc-k6zwl 1/1 Running 0 22s
$ kubectl run busy --image=busybox --rm -ti -- sh
# もしここでPermissionDeniedになったらkubeapi-to-kubeletのrbacが正常に作られているかcontrolプレーンの中で確認してみてください。
# もしくはとりあえず下記コマンド打ってみてください
# docker exec ctl-0 bash /exec/RBAC_AUTH.sh
#
# APIサーバを起動したときにデフォルトで作成されている'kubernetes'という名前のサービスリソースを名前解決します。
# in pod
$ nslookup kubernetes
Server: 10.100.0.10
Address: 10.100.0.10:53
Name: kubernetes.default.svc.cluster.local
Address: 10.100.0.1
$ exit
13-smoke-test
ここではkubernetesリソース関連の各種操作を行なっていきます。
ここに記載の範囲の操作は可能なことは確認できています。
secrets
ここでは06-data-encryption-keys
で実施したsecretsリソースがちゃんと暗号化されていることを確認します。
# ctl-0にattach
$ docker exec -ti ctl-0 sh
$ kubectl create secret generic kubernetes-the-hard-way --from-literal="mykey=mydata"
$ etcdctl get \
--cacert=/etc/etcd/ca.pem \
--key=/etc/etcd/kubernetes-key.pem \
--cert=/etc/etcd/kubernetes.pem \
/registry/secrets/default/kubernetes-the-hard-way | hexdump -C
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 64 65 66 61 75 6c 74 2f 6b 75 62 65 72 6e |s/default/kubern|
00000020 65 74 65 73 2d 74 68 65 2d 68 61 72 64 2d 77 61 |etes-the-hard-wa|
00000030 79 0a 6b 38 73 3a 65 6e 63 3a 61 65 73 63 62 63 |y.k8s:enc:aescbc|
00000040 3a 76 31 3a 6b 65 79 31 3a 84 82 28 36 9a 28 ef |:v1:key1:..(6.(.|
#---<省略>---
aescbcという文字列が、このアルゴリズムがそれ以降文字列の暗号化に使われたということを示すようです。
試しにそれ以外のリソースについて暗号化されていないのか確かめてみます。
# still in ctl-0
# 'default' namespaceに関連して保存されているリソースの表示
$ etcdctl get \
--cacert=/etc/etcd/ca.pem \
--key=/etc/etcd/kubernetes-key.pem \
--cert=/etc/etcd/kubernetes.pem "" --prefix --keys-only | grep default
# kubernetesサービスリソースの情報確認
$ etcdctl get \
--cacert=/etc/etcd/ca.pem \
--key=/etc/etcd/kubernetes-key.pem \
--cert=/etc/etcd/kubernetes.pem /registry/services/specs/default/kubernetes | hexdump -C
outputs
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 72 76 69 63 |/registry/servic|
00000010 65 73 2f 73 70 65 63 73 2f 64 65 66 61 75 6c 74 |es/specs/default|
00000020 2f 6b 75 62 65 72 6e 65 74 65 73 0a 6b 38 73 00 |/kubernetes.k8s.|
00000030 0a 0d 0a 02 76 31 12 07 53 65 72 76 69 63 65 12 |....v1..Service.|
00000040 87 04 0a bb 03 0a 0a 6b 75 62 65 72 6e 65 74 65 |.......kubernete|
00000050 73 12 00 1a 07 64 65 66 61 75 6c 74 22 00 2a 24 |s....default".*$|
00000060 35 39 66 35 32 61 62 62 2d 34 61 39 61 2d 34 63 |59f52abb-4a9a-4c|
00000070 30 34 2d 39 64 35 61 2d 35 33 36 30 32 34 32 34 |04-9d5a-53602424|
00000080 36 63 63 38 32 00 38 00 42 08 08 82 e7 9f f7 05 |6cc82.8.B.......|
00000090 10 00 5a 16 0a 09 63 6f 6d 70 6f 6e 65 6e 74 12 |..Z...component.|
000000a0 09 61 70 69 73 65 72 76 65 72 5a 16 0a 08 70 72 |.apiserverZ...pr|
000000b0 6f 76 69 64 65 72 12 0a 6b 75 62 65 72 6e 65 74 |ovider..kubernet|
000000c0 65 73 7a 00 8a 01 b8 02 0a 0e 6b 75 62 65 2d 61 |esz.......kube-a|
000000d0 70 69 73 65 72 76 65 72 12 06 55 70 64 61 74 65 |piserver..Update|
000000e0 1a 02 76 31 22 08 08 82 e7 9f f7 05 10 00 32 08 |..v1".........2.|
000000f0 46 69 65 6c 64 73 56 31 3a 85 02 0a 82 02 7b 22 |FieldsV1:.....{"|
00000100 66 3a 6d 65 74 61 64 61 74 61 22 3a 7b 22 66 3a |f:metadata":{"f:|
00000110 6c 61 62 65 6c 73 22 3a 7b 22 2e 22 3a 7b 7d 2c |labels":{".":{},|
00000120 22 66 3a 63 6f 6d 70 6f 6e 65 6e 74 22 3a 7b 7d |"f:component":{}|
00000130 2c 22 66 3a 70 72 6f 76 69 64 65 72 22 3a 7b 7d |,"f:provider":{}|
00000140 7d 7d 2c 22 66 3a 73 70 65 63 22 3a 7b 22 66 3a |}},"f:spec":{"f:|
00000150 63 6c 75 73 74 65 72 49 50 22 3a 7b 7d 2c 22 66 |clusterIP":{},"f|
00000160 3a 70 6f 72 74 73 22 3a 7b 22 2e 22 3a 7b 7d 2c |:ports":{".":{},|
00000170 22 6b 3a 7b 5c 22 70 6f 72 74 5c 22 3a 34 34 33 |"k:{\"port\":443|
00000180 2c 5c 22 70 72 6f 74 6f 63 6f 6c 5c 22 3a 5c 22 |,\"protocol\":\"|
00000190 54 43 50 5c 22 7d 22 3a 7b 22 2e 22 3a 7b 7d 2c |TCP\"}":{".":{},|
000001a0 22 66 3a 6e 61 6d 65 22 3a 7b 7d 2c 22 66 3a 70 |"f:name":{},"f:p|
000001b0 6f 72 74 22 3a 7b 7d 2c 22 66 3a 70 72 6f 74 6f |ort":{},"f:proto|
000001c0 63 6f 6c 22 3a 7b 7d 2c 22 66 3a 74 61 72 67 65 |col":{},"f:targe|
000001d0 74 50 6f 72 74 22 3a 7b 7d 7d 7d 2c 22 66 3a 73 |tPort":{}}},"f:s|
000001e0 65 73 73 69 6f 6e 41 66 66 69 6e 69 74 79 22 3a |essionAffinity":|
000001f0 7b 7d 2c 22 66 3a 74 79 70 65 22 3a 7b 7d 7d 7d |{},"f:type":{}}}|
00000200 12 43 0a 1a 0a 05 68 74 74 70 73 12 03 54 43 50 |.C....https..TCP|
00000210 18 bb 03 22 07 08 00 10 ab 32 1a 00 28 00 1a 0a |...".....2..(...|
00000220 31 30 2e 31 30 30 2e 30 2e 31 22 09 43 6c 75 73 |10.100.0.1".Clus|
00000230 74 65 72 49 50 3a 04 4e 6f 6e 65 42 00 52 00 5a |terIP:.NoneB.R.Z|
00000240 00 60 00 68 00 1a 02 0a 00 1a 00 22 00 0a |.`.h......."..|
0000024e
"
情報が平文で保存されていることが確認できます。
deployment
ここではアプリケーションを運用していく上で主に使われることになるdeploymentリソースの作成です。
$ kubectl create deployment nginx --image=nginx
$ kubectl get po -w
# コンテナがRunningになるまで待ちます。確認できたらCtl-Cで抜けます。
Port Forwarding
ここではkubectlを用いるホスト(ここではdockerホスト)に、podのポートへとforwardingを行う操作の確認を行います。
主にデバッグ目的で行われる操作になります。
# 上のdeploymentで表示したpodの名前をコピーします。
$ POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
$ kubectl port-forward $POD_NAME 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
# in new terminal of docker host
$ curl --head 127.0.0.1:8080
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Mon, 15 Jun 2020 10:11:53 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 May 2020 15:00:20 GMT
Connection: keep-alive
ETag: "5ecd2f04-264"
Accept-Ranges: bytes
Logs
podのログを表示します。デバッグの基本となる、個人的には重要なコマンドです。
先ほどのアクセスログが表示されるはずです。
$ kubectl logs $POD_NAME
127.0.0.1 - - [15/Jun/2020:10:11:53 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"
Exec
podの中でコマンドを実行します。
上記のログ同様、デバッグを行う上で結構用います。
kubectl exec -ti $POD_NAME -- nginx -v
Services
ここでは先ほど作成したpodに、その付加されたタグに該当すればトラフィックをフォワードするというサービスリソースを作成します。
kube-proxyを作成する段でも少し触れましたが、iptabesなどNetfilter操作系のコマンドで実装されているようです。
どのタグがどのpodについて、、とかを考えて作ると割と面倒なのですが、kubectlには一括でpodやdeploymentなどのリソースとサービスを紐づけるためのコマンドがあります。それがexpose
になります。
ここでは完全に外部からアクセスするため、NodePort型のサービスリソースを作成しています。
ClusterIPではあくまでpodへの通信をforwardするため、サービスクラスタのネットワークが見える環境でなければいけませんが(つまり今回の環境においてはホスト上で10.100.0.0/16への通信をどこかのノードへルーティングする必要があります)、NodePortはpodがデプロイされるworkerノードのポートからpodにフォワードするため、何もしなくでもdockerホストからpodへと疎通できるようになります。
$ kubectl expose deployment nginx --port 80 --type NodePort
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 162m
nginx NodePort 10.100.211.41 <none> 80:31867/TCP 7s
$ NODE_PORT=$(kubectl get svc nginx \
--output=jsonpath='{range .spec.ports[0]}{.nodePort}')
# 上で表示したportの値となる
# どこか適当なworkerノードのポートに疎通確認
$ curl -I 172.16.10.30:${NODE_PORT}
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Mon, 15 Jun 2020 10:47:25 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 May 2020 15:00:20 GMT
Connection: keep-alive
ETag: "5ecd2f04-264"
Accept-Ranges: bytes
14-cleanup
ここまでに構築した環境を一掃します。
特にカスタマイズしてなければ下記コマンド一発です。
# in .
$ docker-compose down
ちなみにまたサービスをupすれば再度クリーンなkubernetesクラスタの実行環境が立ち上がります。
$ docker-compose up
おわりに
以上、docker環境上でhard-wayするための方法と、つまづいたところの共有になります。
実際にやってみようとなっても、ここで行うテストの範囲ならかなり低リソース(再喝しますがdockerホストはCPU: 1コア、RAM: 4GB、ディスク: 32GBです)でも十分動きました。全てフリーの範囲なのでお金の心配もありません。
意外とネットワーク周りの理解に手間取り結構時間かかってしまいましたが、コンテナ技術を理解するための入り口としてとてもいいチュートリアルでした。
hard-wayしたいけどパブリッククラウドに手を出すと後片付け忘れそうで不安、サインアップが面倒くさい、自前の環境で済ませたい、コンテナの制限を知りたいなど、自分と似た境遇の方の何かの助けになれば幸いです。