BREAKING NEWS

2019/08/06

Raspberry PiでKubernetesクラスターを構築

この記事のまとめ:
  • Raspberry Pi 3B+を使ってKubernetesクラスターをAnsibleで構築する方法をまとめています。
背景

ずっとKubernetesの勉強をしようと思っていて、せっかくだからイチから作りたいよねってことでRaspberry PiでKubernetesクラスターを構築したいと思います。

何番煎じかわかりませんが、、、なんていうとネガティブに聞こえますが、先人が沢山いることは大変ありがたいです。

はじめに

今回行うKubernetesクラスターの構築はすべてAnsibleプレイブックとして私のGitHubリポジトリーに置いておきました。

これ以降、Ansibleタスクのコードを記載しておりますが、GitHubリポジトリーが最新版ですので多少差分があるかもしれませんのがご了承ください。

なお、Ansibleを使わないでKubernetesをインストールする方法は本記事ではまとめておりません。実行コマンド等は基本的に下記の記事を参考にさせていただきましたので、Ansibleを使わずにインストールする場合にはこちらをご参考にされたほうがよいかと思います。

材料調達

下記がおおよそ必須材料です。

部材 数量 価格
Raspberry Pi 3B+ 4 19,440円
積層式ケース 1 1,889円
GbE 5ポートスイッチングハブ 1 2,651円
LANケーブル 30cm 4 1,072円
5ポート USB充電器 1 3,299円
Micro USBケーブル 30cm 3本セット 2 1,598円
Micro SDカード 32GB 4 2,396円
32,345円

沢山税金を納めている方に朗報!埼玉県狭山市のふるさと納税で2万円の寄付でRaspberry Pi 3B+を1台返礼してくれます。他に寄付する予定がなければこれで手に入れるのもいいんじゃないでしょうか。

その他、他の方は小型無線LAN APを包含させてしまってどこでも無線でつながるようにされていたりしますが、今回はシンプルに有線ハブで接続するだけです。

下記はなんとなく基板を垂直に設置したいと思い立って購入したものです。

部材 数量 価格
USB - DC5V電源プラグコネクタ 1 660円
ボルト M3x6mm 1 326円
垂直取付金具 10mm角 10 1,760円 (送料780円込み)
アクリル板+加工 3 2,714円 (送料800円込み)
5,954円

アクリル板の加工は下記の通りにしてもらっています。

  • カットサイズ 縦:120mm 横:150mm 板厚:3.0mm (横は140mmでも十分そう)
  • 切断処理:カンナ仕上げ
  • 角丸め加工 ABCD端 半径:8mm
  • 穴あけ加工 ABCD端 穴直径:3mm 距離X:8mm 距離Y:8mm

なお、天板には基板を止めるための穴あけ加工と積層式ケースの削りを自前で行っています。

裏から見たらこんな感じ。 下段にはスイッチングハブとUSB充電器が入っています。

上から見たらこんな感じ。 25mm間隔で並べてると5枚分くらい入りそう。

下準備
OSのインストールとSSHのセットアップ

OSにはRasbian (2019-04-08-raspbian-stretch-lite)を使用します。 なお、執筆時点での最新版のRaspbian BusterはDebian 10ベースなのですが、Dockerが最新版に対応していないため、Debian 9ベースのRaspbian Stretchを使います。

OSのセットアップとSSHでログインできるまで準備しておきます。 下記でそれらのセットアップ方法を紹介していますので必要であればご覧ください。

Ansible関連ファイル構成

クラスターを構築するとなると必然的に数が増えてきますので、ここからの作業はすべてAnsibleで設定を行います。

全ファイル構成は下記の通りです。

$ tree
.
├── ansible
│   ├── ansible.cfg
│   ├── inventory
│   │   └── inventory.ini
│   ├── k8s_provisioning.yml
│   ├── raspbian_provisioning.yml
│   └── roles
│       ├── common
│       │   ├── geerlingguy.docker_arm
│       │   │   │
│       │   │   :
│       │   │   └── xxx
│       │   ├── geerlingguy.ntp
│       │   │   │
│       │   │   :
│       │   │   └── xxx
│       │   └── ypsman.hostname
│       │       │
│       │       :
│       │       └── xxx
│       └── k8s
│           ├── provisioning
│           │   ├── defaults
│           │   │   └── main.yml
│           │   └── tasks
│           │       ├── common_setup.yml
│           │       ├── install_k8s_packages.yml
│           │       ├── main.yml
│           │       ├── provision_master.yml
│           │       ├── provision_workers.yml
│           │       └── store_info_to_join.yml
│           └── roles
│               └── tasks
│                   └── main.yml
├── docker-compose.yml
├── Dockerfile
├── k8s_provisioning.sh
└── raspbian_provisioning.sh

以降でそれぞれのファイルの概要を説明していきます。

基本設定

Raspbianに下記の設定をしていきます。

  • Dockerのインストール
  • NTPの設定(必須じゃないです)
  • ホスト名の変更(趣味です)

Ansibleの実行コンテナの作成

Ansibleは個人的にバージョン依存性が多々あると感じており、安定動作のためにAnsibleを実行するコンテナを作成してます。執筆時点の最新版であるv2.7.16を使用します。 Dockerfileは下記の通りです。

FROM python:2.7
 
RUN echo "deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main" >> /etc/apt/sources.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367 && \
    apt-get update && \
    apt-get install -y ansible=2.7.16
 
WORKDIR /ansible
CMD bash

実行していることはAnsibleの公式のインストール方法のままです。

ビルドしてイメージを作っておきます。

$ docker build -t hassiweb/ansible:2.7.16 .

タグ名は適宜変更してください。

ansible.cfgの設定

コンテナの中からAnsibleを実行する場合には、ターゲットノードに必ず初めてSSHで接続することになるため、OpenSSHのチェック機能によってAnsibleを実行すると次のようなエラーが出ます。

TASK [Gathering Facts] ****************************************************************************************************************************************************fatal: [192.168.0.43]: FAILED! => {"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this.  Please add this host's fingerprint to your known_hosts file to manage this host."}

これを回避するために ansible/ansible.cfg に下記のように ssh_args の設定を追加します。

[defaults]
forks = 4
log_path = ./ansible.log
host_key_cheking = False
gathering = smart
gather_subset = all
transport = smart
 
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

なお、下記の記事を参考にさせていただきました。

Ansibleタスク

Ansible Galaxyから geerlingguy.docker_armgeerlingguy.ntpypsman.hostname をインストールしてきます。

  • geerlingguy.docker_armの修正点

デフォルトではdocker-composeをインストールしようとしますがこれをfalseにします。具体的には ansible/roles/common/geerlingguy.docker_arm/defaults/main.yml を下記のように編集します。

# Whether to install Docker Compose via Pip.
docker_install_compose: false

このタスクではPipでdocker-composeのインストールをしようとしますが、Pipをインストールしていないためです。なお、PipをインストールしてもPyPIで管理されているdocker-composeはPython 2.7ではarmプロセッサ対応していないようです。

  • geerlingguy.ntpの修正点

タイムゾーンやNTPエリアを修正します。ansible/roles/common/geerlingguy.ntp/defaults/main.yml を下記のように編集します。

---
ntp_enabled: true
ntp_timezone: Asia/Tokyo
 
ntp_manage_config: True
 
# NTP server area configuration (leave empty for 'Worldwide').
# See: http://support.ntp.org/bin/view/Servers/NTPPoolServers
ntp_area: 'asia'
ntp_servers:
  - "0{{ '.' + ntp_area if ntp_area else '' }}.pool.ntp.org iburst"
  - "1{{ '.' + ntp_area if ntp_area else '' }}.pool.ntp.org iburst"
  - "2{{ '.' + ntp_area if ntp_area else '' }}.pool.ntp.org iburst"
  - "3{{ '.' + ntp_area if ntp_area else '' }}.pool.ntp.org iburst"
 
ntp_restrict:
  - "127.0.0.1"
  - "::1"
  • ypsman.hostnameの修正点

このタスクでは変更したいホスト名はインベントリーに記載します。インベントリーファイル ansible/inventory/inventory.ini に次のように host_name の後に変更したいホスト名を記載します(この例ではrpi00)。

[all:children]
master
workers
 
[master]
192.168.0.40 ansible_user=pi ansible_password=raspberry host_name=rpi00
 
[workers]
192.168.0.41 ansible_user=pi ansible_password=raspberry host_name=rpi01
192.168.0.42 ansible_user=pi ansible_password=raspberry host_name=rpi02
192.168.0.43 ansible_user=pi ansible_password=raspberry host_name=rpi03

Ansible Playbookの実行

先ほどインストールしたタスクを実行するplaybookを次のように書きます(ファイル名:ansible/raspbian_provisioning.yml)。

---
- name: Provisioning Raspberri Pi
  hosts: raspberry_pi
  become: true
  roles:
    - name: common/geerlingguy.ntp
      tags: ntp
    - name: common/geerlingguy.docker_arm
      tags: docker
    - name: common/ypsman.hostname
      tags: hostname

Ansible Playbookの実行はコンテナから行います。そのコマンドは下記の通りです。

$ docker run -it --rm -v $PWD/ansible:/ansible hassiweb/ansible:2.7.16 ansible-playbook raspbian_provisioning.yml -i inventory/inventory.ini

さて、ようやく下準備が完了しました。

Kubernetesのインストール

kubeadmを使ったKubernetesクラスターの構築を行いますが、引き続きAnsibleでタスクを記述していきます。Ansibleプレイブックとしての実行は最後に記載しますが、Kubernetesの構築手順に沿ってどのような処理をするのかとそのAnsibleタスクの記述を説明していきます。

なお、最新版のKubernetes v1.15.1でも基本的にはクラスターに属するすべてのラズパイで、swapをオフにすること、cgroup memoryをenableにすることを忘れなければそこまでハマらなさそうです。

#むしろ、Ansibleのモジュールとかの使い方を知らなさ過ぎてハマりました…。

クラスター構成

今回は4台のラズパイがありますので、1台をmaster、残り3台をworkerとしてセットアップします。

共通設定

Kubernetesパッケージのインストール まずはクラスターを構成するすべてのノードでkubectl、kubelet、kubeadmのインストールします(ファイル名:ansible/roles/k8s/provisioning/tasks/install_k8s_packages.yml)。

---
- name: Ensure dependencies are installed.
  apt:
    name:
      - apt-transport-https=1.4.9
      - ca-certificates=20161130+nmu1+deb9u1
    state: present
 
- name: Add Kubernetes apt key.
  apt_key:
    url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
    state: present
 
- name: Add Kubernetes repository.
  apt_repository:
    repo: "deb https://apt.kubernetes.io/ kubernetes-xenial main"
    state: present
    update_cache: true
 
- name: Add Kubernetes apt preferences file to pin a version.
  template:
    src: apt-preferences-kubernetes.j2
    dest: /etc/apt/preferences.d/kubernetes
 
- name: Update cache.
  apt: update_cache=yes cache_valid_time=3600
 
- name: Install Kubernetes packages.
  apt:
    name:
      - kubelet
      - kubeadm
      - kubectl

メモリー関連設定

ラズパイはメモリー管理が特殊な設定になっているようで、kubeadmを実行する前に、仮想メモリーであるswapの設定をオフにすることと、メモリー管理をcgroupで行うための設定を行います(ファイル名:ansible/roles/k8s/provisioning/tasks/common_setup.yml)。

---
# Disable swap
- name: Disable swap.
  command: "{{ item }}"
  with_items:
    - dphys-swapfile swapoff
    - dphys-swapfile uninstall
    - update-rc.d dphys-swapfile remove
 
# Enable cgroup memory
- name: Check if cgroup memory is enabled.
  shell: "cat /proc/cgroups | grep memory | awk '{print $4}'"
  register: cgroup_memory_status
 
- name: Enable cgroup memory if desabled.
  lineinfile:
    path: /boot/cmdline.txt
    backrefs: true
    state: present
    regexp: '(.*)$'
    line: '\1 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory'
  when: cgroup_memory_status.stdout|int == 0
 
- name: Restart machine.
  shell: shutdown -r now
  async: 1
  poll: 0
  ignore_errors: true
  when: cgroup_memory_status.stdout|int == 0
 
- name: Wait for reboot.
  wait_for_connection:
    delay: 30
    timeout: 300
  when: cgroup_memory_status.stdout|int == 0
Masterの設定

kubeadmを使ってmasterのセットアップをし、その後ネットワークプラグインとしてFlannelの設定をします(ファイル名:ansible/roles/k8s/provisioning/tasks/provision_master.yml)。

----
- name: Initialize Kubernetes master with kubeadm init.
  command: >
    kubeadm init
    --pod-network-cidr='10.244.0.0/16'
    --kubernetes-version 'stable-1.15'
  register: kubeadmin_init
 
- name: Print the init output to screen.
  debug:
    var: kubeadmin_init.stdout
 
- name: Ensure .kube directory exists.
  file:
    path: ~/.kube
    state: directory
 
- name: Symlink the kubectl admin.conf to ~/.kube/conf.
  file:
    src: /etc/kubernetes/admin.conf
    dest: ~/.kube/config
    state: link
 
- name: Configure Flannel networking.
  command: "{{ item }}"
  with_items:
    - kubectl apply -f 'https://raw.githubusercontent.com/coreos/flannel/62e44c867a2846fefb68bd5f178daf4da3095ccb/Documentation/kube-flannel.yml'
  register: flannel_result
  changed_when: "'created' in flannel_result.stdout"

ここまででmasterの設定は完了です。

Workerの設定

Workerの設定には、kubeadm init コマンドでmasterを設定した際に、別のノードをworkerとしてクラスターにjoinするための情報が次のように出力されます。

Then you can join any number of worker nodes by running the following on each as root:
 
kubeadm join 192.168.0.40:6443 --token 9ypxep.s85dtgydsxq3yv0t \
    --discovery-token-ca-cert-hash sha256:d4b3748eb02991613e885f247cc7dac9b27564345f16e75cf52eeb642db12f41

基本的にはこれらの情報を使うのですが、このコマンドを実行後に改めてこれらの情報を取得する方法をたまたま紹介してくれている方がおられたのでそのコードをそのまま拝借して、workerの設定を行います。

まずは、これらの情報を別のタスクから呼び出せるようにするために、hostvars に格納します(ファイル名:ansible/roles/k8s/provisioning/tasks/store_info_to_join.yml)。

---
- name: "Cluster token"
  shell: kubeadm token list | cut -d ' ' -f1 | sed -n '2p'
  register: K8S_TOKEN
 
- name: "CA Hash"
  shell: openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
  register: K8S_MASTER_CA_HASH
 
- name: "Add K8S Token and Hash to dummy host"
  add_host:
    name:   "K8S_TOKEN_HOLDER"
    token:  "{{ K8S_TOKEN.stdout }}"
    hash:   "{{ K8S_MASTER_CA_HASH.stdout }}"
 
- name:
  debug:
    msg: "\[Master] K8S_TOKEN_HOLDER K8S token is {{ hostvars['K8S_TOKEN_HOLDER'\]['token'] }}"
 
- name:
  debug:
    msg: "\[Master] K8S_TOKEN_HOLDER K8S Hash is  {{ hostvars['K8S_TOKEN_HOLDER'\]['hash'] }}"

このタスクを利用して、workerをクラスターにjoinさせます(ファイル名:ansible/roles/k8s/provisioning/tasks/provision_workers.yml)。

---
- name: Provision workers
  shell: >
    kubeadm join --token={{ hostvars\['K8S_TOKEN_HOLDER'\]['token'] }}
    --discovery-token-ca-cert-hash sha256:{{ hostvars\['K8S_TOKEN_HOLDER'\]['hash'] }}
    {{ item }}:{{ kubernetes_apiserver_port }}
  with_items: "{{ groups['master'] }}"
  when: "'workers' in group_names"

ここまでで一連の流れは終わりです。上記の初期設定はすべて ansible/roles/k8s/provisioning/tasks/main.yml から呼び出されて実行されます。

---
# Common
- name: Common setup.
  include: common_setup.yml
 
# Provision Master
- name: Check if Kubernetes has already been initialized.
  stat:
    path: /etc/kubernetes/admin.conf
  register: kubernetes_init_stat
 
- name: Provision master if not installed yet
  include: provision_master.yml
  when: "'master' in group_names and not kubernetes_init_stat.stat.exists"
 
# Provision Workers
- name: Sotre k8s token and hash to dummy host
  include: store_info_to_join.yml
  when: "'master' in group_names"
 
- name: Provision workers
  include: provision_workers.yml
  when: "'workers' in group_names"

まだAnsibleプレイブックの実行は行っていませんが、ここまでの手順でKubernetesクラスターの構築は完了です。

ただし、workerをjoinさせただけだと表示上、workerのroleが設定されていないのでroleとして”worker”と表示させる設定を行います(ファイル名:ansible/roles/k8s/roles/tasks/main.yml)。

---
- name: Change roles of workers
  shell: >
    kubectl label node {{ hostvars\[item\]['host_name'] }} node-role.kubernetes.io/worker=worker
  with_items: "{{ groups['workers'] }}"

最後にこれまでのすべてのKubernetesクラスターの構築手順を行うプレイブックを書きます(ファイル名:ansible/k8s_provisioning.yml)。

---
- name: Provisioning Kubernetes master
  hosts: master
  become: true
  roles:
    - name: k8s/provisioning
 
- name: Provisioning Kubernetes workers
  hosts: workers
  become: true
  roles:
    - name: k8s/provisioning
 
- name: Change roles of workers
  hosts: master
  become: true
  roles:
    - name: k8s/roles

上記のプレイブックをDockerコンテナーから実行します(ファイル名:k8s_provisioning.sh)。 ディレクトリーについては適宜修正してください。

#!/bin/sh
docker run -it --rm -v $PWD/ansible/k8s_on_rpi/ansible:/ansible hassiweb/ansible:rpi ansible-playbook k8s_provisioning.yml -i inventory/inventory.ini

上記のAnsibleプレイブックの実行が無事完了していれば、masterにログインして下記を実行すると次のようにクラスターを構築するノードを表示できるはずです。

$ sudo kubectl get nodes
NAME    STATUS   ROLES    AGE    VERSION
rpi00   Ready    master   2d1h   v1.15.1
rpi01   Ready    worker   45h    v1.15.1
rpi02   Ready    worker   45h    v1.15.1
rpi03   Ready    worker   45h    v1.15.1

ここまで表示できれば完了です。お疲れさまでした!


今回は以上です。 最後まで読んでいただき、ありがとうございます。
関連記事



Share this:

 
Copyright © 2014 hassiweb. Designed by OddThemes