AnsibleでIaaSにKubernetesを自動デプロイする

#はじめに
構成管理ツールとAPIを使ってIaaS環境にKubernetes環境一式を自動デプロイし、プライベート・クラウド環境を構築します。構成管理ツールは、[Ansible](https://www.ansible.com/)を使います。デプロイ先となるIaaSは、[IBM Cloud IaaS](https://www.ibm.com/cloud-computing/bluemix/ja/cloud-servers)とします。IBM Cloud IaaSは、APIが公開されていますので、[SL API (SoftLayer API Python Client)](http://softlayer-python.readthedocs.io/en/latest/cli.html)を使ってIaaS部分の管理を自動化します。Kubernetesは、IBM版Kubernetesである、[IBM Cloud Private](https://www.ibm.com/support/knowledgecenter/SSBS6K_2.1.0.3/kc_welcome_containers.html)（以下、ICP）の[Community Edition](https://www.ibm.com/support/knowledgecenter/SSBS6K_2.1.0.3/installing/install_containers_CE.html)（無料）を用います。ソースコードは、Githubの[Deploy IBM Cloud Private]
(https://github.com/IBM/deploy-ibm-cloud-private)をご確認ください。AnsibleだけでなくVagrant、Terrraformといったバージョンもあります。

#IaaS構成
システム構成は下図のとおりです。MacbookをICPのBoot Node（Ansibleベースのインストーラー）として、IaaS側の4台の仮想サーバーにMaster node (icp-master01)、Proxy node (icp-proxy01)、Worker node (icp-worker01, icp-worker02)をオーダーした上でICPを自動デプロイしています。インバウンド／アウトバウンド通信については、[セキュリティ・グループ](https://console.bluemix.net/docs/infrastructure/security-groups/sg_index.html#getting-started-with-security-groups-beta-)の機能で必要なIPアドレスとポート番号を許可しておきます。

<img width="787" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/63451/ec40b74c-eef6-59fa-281b-ef5b0fc0ba4c.png">

#事前準備

MacbookのターミナルにIBM Cloud Privateの[リポジトリー](https://github.com/IBM/deploy-ibm-cloud-private.git)をクローンします。

```
$ git clone https://github.com/IBM/deploy-ibm-cloud-private.git
```

クローンしたリポジトリーに移動します。

```
$ cd deploy-ibm-cloud-private
$ ls
Cachefile		ansible.cfg		extensions		requirements.txt
LICENSE			camQuick.sh		hosts			terraform
README.md		cluster			images
Vagrantfile		docs			playbooks
```

pipコマンドでSL CLIとAnsibleをセットアップします。

```
$ sudo pip install -r requirements.txt
$ cat requirements.txt
softlayer
ansible
```

SL CLIのバージョンは以下のとおりです。

```
$ slcli --version
slcli (SoftLayer Command-line), version 5.3.1
```

Ansibleのバージョンは以下のとおりです。

```
$ ansible --version
ansible 2.5.3
...
```

SL CLIに資格情報をセットアップします。対話形式で必要な項目を入力していきます。

```
$ slcli config setup
Username [XXXXX@XX.ibm.com]:
API Key or Password [XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]:
Endpoint (public|private|custom) [public]:
Timeout [0]:
:..............:..................................................................:
:         name : value                                                            :
:..............:..................................................................:
:     Username : XXXXX@jp.ibm.com                                              :
:      API Key : XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX :
: Endpoint URL : https://api.softlayer.com/xmlrpc/v3.1/                           :
:      Timeout : not set                                                          :
:..............:..................................................................:
Are you sure you want to write settings to "/Users/asasaki/.softlayer"? [Y/n]:
```

仮想サーバーにパスワードなしでログインするためにSSH鍵を作成します。

```
$ ssh-keygen -f cluster/ssh_key -P ""
Generating public/private rsa key pair.
Overwrite (y/n)? y
Your identification has been saved in cluster/ssh_key
Your public key has been saved in cluster/ssh_key.pub
```

作成したSSH鍵をSoftLayerに追加します。

```
$ slcli sshkey add -f cluster/ssh_key.pub icp-key
```

``icp-key``という名前のSSH鍵がIBM Cloud IaaSに登録されていることを確認します。

```
$ slcli sshkey list               
:........:....................:.................................................:.......:
:   id   :       label        :                   fingerprint                   : notes :
:........:....................:.................................................:.......:
: XXXXXXX :    icp-key     : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX :   -   :
:........:....................:.................................................:.......:
```

デプロイ先となるVLANのIDを確認します。

```
$ slcli vlan list
:.........:........:......................:..........:............:..........:.................:............:
:    id   : number :         name         : firewall : datacenter : hardware : virtual_servers : public_ips :
:.........:........:......................:..........:............:..........:.................:............:
: XXXXXXX :  XXXX  :                      :    No    :   tok02    :    0     :        0        :     0      :
:.........:........:......................:..........:............:..........:.................:............:
```

# Ansibleによる仮想サーバーのデプロイ
Ansibleでは、Playbookと呼ばれるYAML形式のテキスト・ファイルに実行したいタスクを記述します。それをAnsibleで実行させることにより、インフラの構成管理を自動化することができます。ここでは、AnsibleのPlaybookを使用して、ICPのデプロイ先となるIBM Cloud IaaSの仮想サーバーをデプロイします。

デプロイ先のデータセンター名（ここではtok02）とさきほど確認したVLANのIDを使用して、Ansibleで利用するYAMLファイル``cluster/config.yaml``に``sl_datacenter``と``sl_vlan``の2つの変数を構成ファイルにセットします。

```cluster/config.yaml
$ cat cluster/config.yaml
network_type: calico
network_cidr: 192.168.0.0/16
service_cluster_ip_range: 192.168.0.0/24
ingress_enabled: true
ansible_user: root

mesos_enabled: false
install_docker_py: true

sl_datacenter: tok02
sl_ssh_key: XXXXXXX
sl_vlan: XXXXXXX

sl_icp_masters:
  - icp-master01

sl_icp_workers:
  - icp-worker01
  - icp-worker02

sl_icp_proxies:
  - icp-proxy01
```

次に、実際に仮想サーバーを作成するPlaybook``create_sl_vms.yml``の内容を確認します。内部的にさきほど設定した``cluster/config.yaml``が呼び出されていることが分かります。``cpus: 2`` ``memory: 8192`` ``disks: [100]``といった変数で、ICPを構成する各ノードのスペックを指定することができます。

```create_sl_vms.yml
$ cat playbooks/create_sl_vms.yml
---
- name: create servers
  hosts: localhost
  gather_facts: False

  tasks:
    - name: Include cluster vars
      include_vars:
        file: ../cluster/config.yaml
    - name: create master
      sl_vm:
        hostname: "{{ item }}"
        domain: example.com
        datacenter: "{{ sl_datacenter|default('dal09') }}"
        tags: "{{ item }},icp,master"
        hourly: True
        private: False
        dedicated: False
        local_disk: True
        cpus: 2
        memory: 8192
        disks: [100]
        os_code: UBUNTU_LATEST
        private_vlan: "{{ sl_vlan|default(omit) }}"
        wait: no
        ssh_keys: "{{ sl_ssh_key|default(omit) }}"
      with_items: " {{ sl_icp_masters }}"

    - name: create workers
      sl_vm:
        hostname: "{{ item }}"
        domain: example.com
        datacenter: "{{ sl_datacenter|default('dal09') }}"
        tags: "{{ item }},icp,worker"
        hourly: True
        private: False
        dedicated: False
        local_disk: True
        cpus: 2
        memory: 4096
        disks: [100]
        os_code: UBUNTU_LATEST
        private_vlan: "{{ sl_vlan|default(omit) }}"
        wait: no
        ssh_keys: "{{ sl_ssh_key|default(omit) }}"
      with_items: " {{ sl_icp_workers }}"

    - name: create proxies
      sl_vm:
        hostname: "{{ item }}"
        domain: example.com
        datacenter: "{{ sl_datacenter|default('dal09') }}"
        tags: "{{ item }},icp,proxy"
        hourly: True
        private: False
        dedicated: False
        local_disk: True
        cpus: 2
        memory: 4096
        disks: [100]
        os_code: UBUNTU_LATEST
        private_vlan: "{{ sl_vlan|default(omit) }}"
        wait: no
        ssh_keys: "{{ sl_ssh_key|default(omit) }}"
      with_items: " {{ sl_icp_proxies }}"
```

AnsibleからPlaybook``playbooks/create_sl_vms.yml``を実行してIBM Cloud IaaSに仮想サーバーをデプロイします。

```
$ ansible-playbook playbooks/create_sl_vms.yml
```

5-10分ほどで仮想サーバーのデプロイが完了します。

# AnsibleによるOS構成

各仮想サーバーのOSにログインするためのSSH鍵を追加します。

```
$ ssh-add cluster/ssh_key
```

OSパラメータを設定するためのPlaybook``prepare_sl_vms.yml``の内容を確認します。swapの無効化、Pythonなどのパッケージの追加、dnsmasq（プライベートDNSサーバ）の設定、Docker CEのインストールといったICPのインストールに必要な前提タスクが記述されていることを確認します。

```prepare_sl_vms.yml
$ cat playbooks/prepare_sl_vms.yml
---
- name: ensure connectivity to all nodes
  hosts: all
  gather_facts: false
  pre_tasks:
    - name: Include cluster vars
      include_vars:
        file: ../cluster/config.yaml
    - name: check if python is installed
      raw: test -e /usr/bin/python
      register: python_installed
      changed_when: false
      failed_when: false
    - name: check if debian based OS
      raw: which apt-get
      register: is_ubuntu
      changed_when: false
      failed_when: false
    - name: install python
      raw: (apt-get -y update && apt-get install -y python)
      when: python_installed.rc != 0 and is_ubuntu.rc == 0
    - name: install python
      raw: (yum install -y python)
      when: python_installed.rc != 0 and is_ubuntu.rc != 0
  tasks:
    - name: ansible setup
      action: setup
      tags: ['ping']
  any_errors_fatal: true
  max_fail_percentage: 0

- name: bootstrap nodes
  hosts: all
  gather_facts: true
  tasks:
    - name: Include cluster vars
      include_vars:
        file: ../cluster/config.yaml

    - name: disable swap
      command: swapoff -a

    - name: install extra packages
      yum:
        name: "{{ item }}"
      with_items: [dnsmasq,vim, yum-utils,python-pip]
      when: ansible_os_family == 'RedHat'

    - name: install extra packages
      apt:
        name: "{{ item }}"
      with_items: [dnsmasq,vim,python-pip,nfs-common]
      when: ansible_os_family == 'Debian'

    - name: disable resolvconf updates
      command: resolvconf --disable-updates
      when: ansible_os_family == 'Debian'

    - name: add alternative dnsmasq resolv.conf
      lineinfile:
        line: "resolv-file=/etc/dnsmasq_resolv.conf"
        path: /etc/dnsmasq.conf
        regexp: "^#resolv-file="

    - name: add dns servers to /etc/dnsmasq_resolv.conf
      lineinfile:
        create: yes
        line: "nameserver {{ item }}"
        path: /etc/dnsmasq_resolv.conf
      with_items: [10.0.80.11, 10.0.80.12]

    - name: rewrite /var/run/resolvconf/resolv.conf
      lineinfile:
        create: yes
        line: "nameserver {{ item }}"
        path: /var/run/resolvconf/resolv.conf
        regexp: "nameserver {{ item }}"
        state: absent
      with_items: [10.0.80.11, 10.0.80.12, 127.0.0.1]

    - name: rewrite /var/run/resolvconf/resolv.conf
      lineinfile:
        line: "nameserver {{ primaryBackendIpAddress }}"
        path: /var/run/resolvconf/resolv.conf
        regexp: "nameserver {{ primaryBackendIpAddress }}"

    - name: remove /etc/hosts
      file:
        dest: /etc/hosts
        state: absent

    - name: add localhost to /etc/hosts
      lineinfile:
        create: yes
        line: 127.0.0.1 localhost
        path: /etc/hosts

    - name: add host to /etc/hosts
      lineinfile:
        line: "{{ hostvars[item]['primaryBackendIpAddress'] }} {{ hostvars[item]['hostname'] }} {{ hostvars[item]['ansible_fqdn'] }}"
        path: /etc/hosts
      with_items: "{{ groups['all'] }}"

    - name: enable / start dnsmasq
      service:
        name: dnsmasq
        state: started
        enabled: true

    - name: wait 60 seconds then restart dnsmasq
      shell: sleep 60; systemctl restart dnsmasq

    - block:
      - name: get Docker CE repo
        shell: yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
        args:
          creates: /etc/yum.repos.d/docker-ce.repo

      - name: install Docker CE
        yum:
          name: docker-ce
      when: ansible_os_family == "RedHat"

    - block:
      - name: Import Docker CE repository gpg key
        apt_key:
          url: https://download.docker.com/linux/ubuntu/gpg
          state: present
          id: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
      - name: Add Docker CE repository
        apt_repository:
          repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
          state: present

      - name: Install Docker CE
        apt:
          name: docker-ce
          state: present
          update_cache: yes
      when: ansible_os_family == "Debian"

  any_errors_fatal: true
  max_fail_percentage: 0

- name: install icp
  hosts: master[0]
  gather_facts: true
  tasks:
    - name: Include cluster vars
      include_vars:
        file: ../cluster/config.yaml

    - name: create icp install dir
      file:
        dest: /root/cluster
        state: directory

    - name: Copy icp files up
      copy:
        src: ../cluster
        dest: /root/

    - name: set perms on hosts script
      file:
        dest: /root/cluster/hosts
        mode: 0755

    - name: Build installer image
      command: docker build -t icp-on-sl /root/cluster

    - name: Run the following command to deploy IBM Cloud Private
      debug:
        msg: "ssh {{ ansible_user }}@{{ hostvars[groups['master'][0]]['primaryIpAddress'] }} docker run -e SL_USERNAME=<SL_USERNAME> -e SL_API_KEY=<SL_API_KEY> -e LICENSE=accept --net=host --rm -t -v /root/cluster:/installer/cluster icp-on-sl install"
```

Playbook (prepare_sl_vms.yml)を実行します。

```
$ ansible-playbook -i hosts playbooks/prepare_sl_vms.yml
```

※dnsmasqが原因でエラーになることがありますが、Playbookを再度実行すると通常は解決されます。もしくはマスター・ノードでdnsmasqを再起動することで解決されることもあります。

#ICPコンテナのインストール

仮想サーバーにログインして、Dockerコマンドを使いICPコンテナ``icp-on-sl``をインストールします。

```
$ cat ~/.softlayer

$ ssh root@XXX.XXX.XXX.XXX docker run -e SL_USERNAME=XXXXX -e SL_API_KEY=XXXXX -e LICENSE=accept --net=host \
   --rm -t -v /root/cluster:/installer/cluster icp-on-sl install
...
```

10分ほどでICPコンテナのインストールが完了します。

#ICPコンソールへのアクセス

ProxyノードのパブリックIPアドレスを確認し、ブラウザから``https://<Proxy Node Public IP>:8443/``にアクセスしてコンソールが表示されることを確認します。

![Screen Shot 2018-05-27 at 00.54.29-fullpage.png](https://qiita-image-store.s3.amazonaws.com/0/63451/1e77efd4-fc6b-a4cf-ea4c-d58b549ff004.png)

![Screen Shot 2018-05-27 at 00.56.42-fullpage.png](https://qiita-image-store.s3.amazonaws.com/0/63451/95193cde-40f1-eea8-deb5-0d8105de4b13.png)

IBM Cloud Privateのコンソールが表示されることを確認します。

# ICP初期設定
[Menu] > [Tools] > [Command line]をオープンします。右側にある[Download For Mac OS X]よりThe IBM Cloud Private CLIをダウンロードします。

![Screen Shot 2018-05-29 at 10.31.18-fullpage.png](https://qiita-image-store.s3.amazonaws.com/0/63451/5ffd9984-d4da-5b22-5f9e-c0b370c06a36.png)


MacbookでIBM Cloud CLI (bxコマンド)がインストールされていることを確認します。

```
$ bx --version
bx version 0.6.7+283a906f-2018-05-14T23:39:23+00:00
```

IBM Cloud CLIがインストールされていない場合は、[こちら](https://console.bluemix.net/docs/cli/index.html#cli)を参考に別途インストールしてください。

``bx plugin install``コマンドでダウンロードした``icp-darwin-amd64``をインストールします。


```
$ bx plugin install /<path_to_installer>/icp-darwin-amd64
バイナリーをインストールしています...
OK
プラグイン 'icp 2.1.239' は /Users/asasaki/.bluemix/plugins/icp に正常にインストールされました。 'bx plugin show icp' を使用して詳細を表示してください。
```

The IBM Cloud Private CLI (以下、ICP CLI)がインストールされていることを確認します。

```
$ bx pr help
名前:
   bx pr - IBM Cloud Private Service
使用法:
   bx pr command [arguments...] [command options]

コマンド:
   api                View the API endpoint and API version for the service.
   cluster-config     Download the Kubernetes configuration and configure kubectl for a specified cluster.
   cluster-get        クラスターの詳細を表示します
   clusters           List all the clusters in your account.
...
```

デプロイしたICPのマスター・ノードにログインします。

```
$ bx pr login -a https://<master_ip_address>:8443 --skip-ssl-validation
 Login method invokedAPI endpoint: https://169.56.28.254:8443

Username> admin

Password>
Authenticating...
OK

Select an account:
1. ICP Account (3c43bf7207a6932a043f2eabc6002d70)
Enter a number> 1
Targeted account: ICP Account (3c43bf7207a6932a043f2eabc6002d70)
```

MacbookのプラグインリストにICP CLIがインストールされていることを確認します。

```
$ bx plugin list
インストール済みプラグインをリストしています...

プラグイン名         バージョン
IBM-Containers       1.0.1028
container-registry   0.1.292
container-service    0.1.488
icp                  2.1.239
```

サービスのAPIエンドポイントとAPIバージョンを表示します。

```
$ bx pr api

API エンドポイント:   https://<Master Node IP Address>:8443/api
API バージョン:       v1
SSL 検証のスキップ:   tru
```

Kubernetes設定をダウンロードし、指定されたクラスタに対してkubectlを設定します。

```
$ bx pr cluster-config mycluster
Configuring kubectl: /Users/asasaki/.bluemix/plugins/icp/clusters/mycluster/kube-config
Cluster "master.cfc" set.
Context "master.cfc-context" modified.
User "master.cfc-user" set.
Context "master.cfc-context" modified.
Switched to context "master.cfc-context".

OK
Cluster mycluster configured successfully.
```

クラスタの詳細を表示します。

```
$ bx pr cluster-get mycluster
Retrieving cluster mycluster...
OK

Name:			mycluster
ID:			00000000000000000000000000000001
State:			deployed
Created:		2018-05-29T01:46:29+0000
Datacenter:		default
Master URL:		https://<Master Node IP Address>:8001
Masters:		1
Workers:		3
```

アカウント内のすべてのクラスタを一覧表示します。

```
$ bx pr clusters
OK
Name        ID                                 State      Created                    Masters   Workers   Datacenter
mycluster   00000000000000000000000000000001   deployed   2018-05-29T01:46:29+0000   1         3         default
```

以上