Calico the hard wayの作業ログ

2019-11-12に作成

KubernetesのCNIプラグインにCalicoというものがあって、いま自分が運用に関わっているシステムはそれを使っているんだけど、今までちゃんとCalicoについて勉強したことがなかったので、ちょっとチュートリアルをやってみようと思う。今回はCalico the hard wayというやつ。
これはCalicoのチュートリアルではあるんだけど、いわゆるハローワールドみたいなものとは違って、Calicoの機能やCalicoを支えるコンポーネントも網羅してハンズオンするみたいな感じ。というわけで、Calicoについて勉強するには適切な題材となっている。
ただ、hard wayとあるように、ややボリュームが多いので、数日にわたって行うことになる。というわけで、せっかくだし、Crieitのボード機能を使って作業ログを残してみようと思う。

所有者限定モードのためこのボードには投稿できません ボードとは?

Day4: Follow up

だいぶ間が空いてしまったけど、前回言っていたように、Day4の内容の解説を行う。

まず前提としてCalicoはKubernetesのNetwork Pluginなので、Kubernetesと通信する必要がある。そして、KubernetesのAPIとの通信には認証が必要である。ということで、最初にやることは、認証用の鍵を作成して署名すること。

以下のコマンドで認証用の鍵を作成している。

$ openssl req -newkey rsa:4096 \  # 鍵を4096bitのRSA暗号で作成する
           -keyout cni.key \      # 鍵の出力先ファイル名
           -nodes \               # 鍵の中身を暗号化しない
           -out cni.csr \         #  CSRの出力先ファイル名
           -subj "/CN=calico-cni" # 証明書のサブジェクトのdistinguish nameを設定する

これをKubernetesのCA(認証局)に署名してもらう。

$ sudo openssl x509 -req -in cni.csr \  # CSRが入力であり(--req)、CSRの場所は cni.csrである(--in)
                  -CA /etc/kubernetes/pki/ca.crt \  # KubernetesのCA証明書の場所
                  -CAkey /etc/kubernetes/pki/ca.key \  # KubernetesのCA鍵の場所
                  -CAcreateserial \  # シリアル番号がなければ番号を生成する
                  -out cni.crt \     # 署名された証明書の出力先ファイル名
                  -days 365         # 署名された証明書の有効期限
sudo chown ubuntu:ubuntu cni.crt

これでKubernetesにアクセスするための証明書が作成されたことになる。次に、ここで作成した証明書を使うように設定する。つまり、(Kubernetesをサーバーだと見立てた場合に)クライアント側の設定を行う。

# KubernetesのAPIサーバーのIPアドレスを取得する
$ APISERVER=$(kubectl config view -o jsonpath='{.clusters[0].cluster.server}')

$ kubectl config set-cluster kubernetes \  # kubeconfigに"kubernetes"という名前でクラスターエントリを設定する
    --certificate-authority=/etc/kubernetes/pki/ca.crt \  # CA証明書の位置。クライアント証明書を検証するために必要
    --embed-certs=true \  # kubeconfigの中に証明書を埋め込むかどうか
    --server=$APISERVER \  # KubernetesのAPIサーバーのIPアドレスの指定
    --kubeconfig=cni.kubeconfig  # 設定を保存するkubeconfigのファイルの場所

$ kubectl config set-credentials calico-cni \  # kubeconfigに"calico-cni"という名前でユーザーエントリを設定する
    --client-certificate=cni.crt \  # CAに署名してもらった証明書の場所
    --client-key=cni.key \  # 証明書に対応する鍵
    --embed-certs=true \  # kubeconfigの中に証明書を埋め込むかどうか
    --kubeconfig=cni.kubeconfig  # 設定を保存するkubeconfigのファイルの場所

$ kubectl config set-context default \  # kubeconfigに"default"というコンテキストエントリを設定する
    --cluster=kubernetes \  # "default"というコンテキストに対応するクラスターの名前
    --user=calico-cni \  # "default"というコンテキストに対応するユーザーの名前
    --kubeconfig=cni.kubeconfig  # 設定を保存するkubeconfigのファイルの場所

# 設定した"default"コンテキストを指定する。読み込む設定ファイルはcni.kubeconfigとする
$ kubectl config use-context default --kubeconfig=cni.kubeconfig

これはCalicoが使う設定ファイルなので、Calicoノードに配布しておく必要がある。

ここまで見たように、1つ1つのステップがCalicoがKubernetesのAPIサーバーと通信するために必要な準備作業であることがわかる。

では、ここまで完了したら後はCalicoがKubernetesと協調的に動くようになるかと言うとそうでもなくて、Calicoに限らず、Kubernetesのリソース(DeploymentとかPodとか)を扱うときにはRoleを割り当てなければならない。

というわけで、作成するRoleが以下のようになる

$ kubectl apply -f - <<EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: calico-cni
rules:
  # The CNI plugin needs to get pods, nodes, and namespaces.
  - apiGroups: [""]
    resources:
      - pods
      - nodes
      - namespaces
    verbs:
      - get
  # The CNI plugin patches pods/status.
  - apiGroups: [""]
    resources:
      - pods/status
    verbs:
      - patch
 # These permissions are required for Calico CNI to perform IPAM allocations.
  - apiGroups: ["crd.projectcalico.org"]
    resources:
      - blockaffinities
      - ipamblocks
      - ipamhandles
    verbs:
      - get
      - list
      - create
      - update
      - delete
  - apiGroups: ["crd.projectcalico.org"]
    resources:
      - ipamconfigs
      - clusterinformations
      - ippools
    verbs:
      - get
      - list
EOF

上について細かい説明は省略するというか、追々わかってくると思うのでここでは一旦"オマジナイ"的なものとしてコピペしておくにとどめておいて良いと思う。強いて言うなら、verbsはHTTPのメソッド的なやつだというぐらいの認識で良いと思う。

あとはCalicoのバイナリをダウンロードして所定の位置に配置するだけなので割愛。Day4のフォローアップはここまでにして、次はDay5をやる。

Day4: Install CNI plugin

Day3ではIP poolsを設定して、 Calicoのノード(というかPod)に割り当てるIPの設定を行った。今回はようやくCalicoをCNIとしてインストールする。

まず最初に、master node上でCNIが利用するSSL通信のための証明書と鍵を作成する。

$ openssl req -newkey rsa:4096 \
           -keyout cni.key \
           -nodes \
           -out cni.csr \
           -subj "/CN=calico-cni"

次に、Kubernetesが持っているCA(Certification Authority/認証局)を利用して、作成した証明書に署名する。

$ sudo openssl x509 -req -in cni.csr \
                  -CA /etc/kubernetes/pki/ca.crt \
                  -CAkey /etc/kubernetes/pki/ca.key \
                  -CAcreateserial \
                  -out cni.crt \
                  -days 365
$ sudo chown user:user cni.crt

次に、CNI Plugin(つまりCalico)がKubernetesにアクセスするために使うkubeconfigファイルを作成する。

$ APISERVER=$(kubectl config view -o jsonpath='{.clusters[0].cluster.server}')
kubectl config set-cluster kubernetes \
    --certificate-authority=/etc/kubernetes/pki/ca.crt \
    --embed-certs=true \
    --server=$APISERVER \
    --kubeconfig=cni.kubeconfig

kubectl config set-credentials calico-cni \
    --client-certificate=cni.crt \
    --client-key=cni.key \
    --embed-certs=true \
    --kubeconfig=cni.kubeconfig

kubectl config set-context default \
    --cluster=kubernetes \
    --user=calico-cni \
    --kubeconfig=cni.kubeconfig

kubectl config use-context default --kubeconfig=cni.kubeconfig

ドキュメントによると、ここで作ったcni.kubeconfigは各ノードにコピーしておいたほうが良いらしい。とはいえ、どこにコピーすれば良いのかわからなかったので、とりあえず各ノードの同じユーザーのホームディレクトリに置いといた。

次にCalicoのノードがKubernetesのリソースにアクセスするための権限を持ったRoleを作る。

kubectl apply -f - <<EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: calico-cni
rules:
  # The CNI plugin needs to get pods, nodes, and namespaces.
  - apiGroups: [""]
    resources:
      - pods
      - nodes
      - namespaces
    verbs:
      - get
  # The CNI plugin patches pods/status.
  - apiGroups: [""]
    resources:
      - pods/status
    verbs:
      - patch
 # These permissions are required for Calico CNI to perform IPAM allocations.
  - apiGroups: ["crd.projectcalico.org"]
    resources:
      - blockaffinities
      - ipamblocks
      - ipamhandles
    verbs:
      - get
      - list
      - create
      - update
      - delete
  - apiGroups: ["crd.projectcalico.org"]
    resources:
      - ipamconfigs
      - clusterinformations
      - ippools
    verbs:
      - get
      - list
EOF

で、作ったRoleをCalicoのアカウントcalico-cniにバインドする。

$ kubectl create clusterrolebinding calico-cni --clusterrole=calico-cni --user=calico-cni

ここまで来てようやくCalicoがインストールできる準備が整った。以下のコマンドを各ノードで、かつrootユーザーで行う必要がある。

...なんか特にこれといった説明もなくザザッと設定ファイルを作ってコマンドを叩いただけなので、次回はここでやっていることを1つずつ解説してみたいと思う。Calicoとは直接関係ないけど、KubernetesとCalicoを理解する上では役に立つと思うので。

今日はここまで。

Day3: Configure IP pools

3日目はConfigure IP poolsから。前回はcalicoctlをインストールして簡単な動作確認をしただけ。

Calicoにはworkloadというリソースがあって、VMだったりコンテナだったり、Kubernetesの世界でいうとPodの形で配置される。それが何をするのかというと、仮想ネットワークのための仕事をする。Workloadにはworkload endpointというものがあって、それがCalicoのネットワークの世界とCalicoの外側のネットワークをつなぐためのインターフェースとなる。

で、ここでタイトルにある"IP pools"の話になるんだけど、"IP pools"というのは、Calicoがworkload endpointsのために利用するIPアドレスの範囲のこと。

じゃあそのIPアドレスの範囲はどうやって決めるのかと言うと、Kubernetesクラスタを立ち上げるときに--pod-netwrok-cidrを指定したと思うんだけど、その指定した範囲のサブセットを用いる。

というわけで、以上を踏まえてIP poolsの設定をしてみる。以下のようにする。

$ cat > pool1.yaml <<EOF
> apiVersion: projectcalico.org/v3
> kind: IPPool
> metadata:
>   name: pool1
> spec:
>   cidr: 192.168.0.0/18
>   ipipMode: Never
>   natOutgoing: true
>   disabled: false
>   nodeSelector: all()
> EOF

ドキュメントでは触れてないけど、設定ファイルの内容がKubernetesっぽいのはDatastoreがKubernetesだからかな?etcdだとまた違う(とはいえ、これをjson形式にするとかそんな感じ?)なんだろうと思う。

では内容の解説。apiVersionkindはKubernetesでさんざん見ていると思うので割愛。spec以下だけに絞って説明する。

cidr: 192.168.0.0/18

これは見ての通り、CalicoのIP poolが利用する範囲をCIDRで指定している。KubernetesのPodには192.168.0.0/16を指定したので、

# https://devops.stackexchange.com/a/8486より
kubectl cluster-info dump | grep -m 1 cluster-cidr

192.168.0.0/16がどういう意味かというと、ネットワーク部(fixed prefix)が192.168.0.0で、ネットマスクが16bitですよっていう意味。めっちゃ雑にいうと、32bitのうち、16bitはマスク部なので、残りの16bitを利用できますよっていうことになる。なので、ここでは、2^16-2(networkアドレスとbrodcastアドレスを除く) = 65536 - 2 = 65534個のIPアドレスを割り当てますよっていう設定をしている。

で、Calicoはこの割り当てられている範囲でIPアドレスを利用するので、サブセットとして/16より狭い範囲でCIDRを指定しなければならない。ということで、ここでは192.168.0.0/18として、これまた雑にいうと32bitのうち18bitがマスク部なので、残りの14bit、つまり2^14-2 = 16384 - 2 = 16382個のIPアドレス を使いますよっていう宣言をしていることになる。

次はこれ↓

ipipMode: Never

ipipModeというのは(IP in IP)[https://en.wikipedia.org/wiki/IP_in_IP]を利用するかどうかを指定する項目。IP in IPに関してはリンク先を参照してほしいのだけど、触りだけをいうと、IPパケットを別のIPパケットでカプセル化する。そうすることでVPN通信なんかを可能にしたりする。今回はそういったことは必要ないのでNeverを指定する。

natOutgoing: true

これを有効(true)にすると、Calicoのコンテナから通信先にパケットが送られるときに、パケットがインターネット側から隠蔽されるようになる。つまりCalicoがNATしているように(ようにっていうか、まさにそうなんだろうけど)振る舞うので、本来の通信元のポート番号などがインターネット側に対して隠されるようになる。

disabled: false

これがtrueだと、CalickのIPAM(IP Address management)が割り当てられたIP poolsからIPアドレスを割り当てなくなる。つまり、指定したCIDRの範囲のIP poolsのIPアドレスをもつpodが作られなくなる。podはつくられなくなるんだけど、引き続き指定範囲のCIDRはCalicoのネットワークの一部として認識されている。

nodeSelector: all()

CalicoのIPAMが指定されたIP poolsからアドレスを割り当てるときにノードを選択するときの振る舞いを指定する。all()だとIP poolで指定した範囲内のノードにすべて割り当てる。
ちなみにここまで説明は全てドキュメントに記載されているので詳細はそちらで。

つぎに2つ目のIP poolsを設定する。

$ cat > pool2.yaml <<EOF
> apiVersion: projectcalico.org/v3
> kind: IPPool
> metadata:
>   name: pool2
> spec:
>   cidr: 192.168.192.0/19
>   ipipMode: Never
>   natOutgoing: true
>   disabled: true
>   nodeSelector: all()
> EOF`

で、最後にこれらを適用する。

$ calicoctl create -f pool1.yaml
Successfully created 1 'IPPool' resource(s)
$ calicoctl create -f pool2.yaml
Successfully created 1 'IPPool' resource(s)

$ calicoctl get ippools -o wide
NAME    CIDR               NAT    IPIPMODE   VXLANMODE   DISABLED   SELECTOR
pool1   192.168.0.0/18     true   Never      Never       false      all()
pool2   192.168.192.0/19   true   Never      Never       true       all()

こんな感じ。今日はここまで。

Days2: The Calico datastore

今日はThe Calico datastoreから。前回はKubernetesのクラスタを作った。

これからCalicoをインストールする作業を進めていくわけだけど、そのためにはCalicoが保持するデータの場所を設定する必要がある。設定場所の選択肢はKubernetesとetcdの2つ。どちらにも長所と短所がある。ざっくり言うとこんな感じ。

  • Kubernetesをデータストアとして使う: すでにあるKubernetesクラスタを利用するので、追加でなにかを用意する必要がない。また、Kubernetesがもっている様々な機構を利用することが出来る。
  • etcdををデータストアとして使う: Kubernetesに依存しない環境(例えばOpenStackなど)でCalicoを利用することができる。また、Kubernetesを意識する必要がないので、独自にスケールさせることが出来る。

Calico the hard wayではKubernetesをデータストアとして利用する。で、じゃあKubernetesをデータストアとして利用するならどうなるかというと、KubernetesのCustom resourceとしてCalicoのリソースが管理される。全部がCustome resourceというわけではない。例えば、workload endpointsなんかはPodとして管理される。兎にも角にも、Custome resourceとして管理するためにはリソース定義が必要。ということで、以下の手順でリソース定義を適用する。

$ wget 'https://docs.projectcalico.org/v3.10/manifests/crds.yaml'
...
2019-11-14 01:01:21 (3.92 MB/s) - ‘crds.yaml’ saved [4077/4077]

$ kubectl apply -f ./crds.yaml
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created

次にCalicoの操作を用意にするためにcalicoctlをインストールする。

$ wget https://github.com/projectcalico/calicoctl/releases/download/v3.8.0/calicoctl
$ sudo mv calicoctl /usr/local/bin/
$ chmod +x calicoctl
$ echo 'export KUBECONFIG=${HOME}/.kube/config' >> ~/.bashrc
$ echo 'export DATASTORE_TYPE=kubernetes' >> ~/.bashrc

軽く動作確認をする。

$ calicoctl get nodes
NAME
calicothehardway-001
calicothehardway-002
calicothehardway-003
calicothehardway-004
calicothehardway-005

# ↑と同じような出力になっているかを確認 -> OK
$ kubectl get nodes
NAME                   STATUS     ROLES    AGE   VERSION
calicothehardway-001   NotReady   master   24h   v1.16.2
calicothehardway-002   NotReady   <none>   24h   v1.16.2
calicothehardway-003   NotReady   <none>   23h   v1.16.2
calicothehardway-004   NotReady   <none>   23h   v1.16.2
calicothehardway-005   NotReady   <none>   23h   v1.16.2

# これはからの出力になるのが期待通り。
$ calicoctl get ippools
NAME   CIDR   SELECTOR

今日はここまで。