【Ansible on Docker】Ansibleのハンズオン環境をDockerで構築

Ansibleのハンズオン環境をDockerを用いて構築してみたいと思います。
構成は以下の通りです。

- コントロールノード(Ansible) x 1
- ターゲットノード x 3

ターゲットノードは調整可能ですので、適当な数に置き換えて実施して下さい。

※**説明不要の方は以下を参照してください**
https://github.com/Shoma-progr-0210/ansible-training

# 前提条件

- Docker on Desktopがインストールされている
- Dockerの基礎知識がある
- Linuxコマンドの基礎知識がある

# ディレクトリ構成

```bash:
ansible-training
  │  docker-compose.yml
  │
  ├─ansible
  │      ansible.cfg
  │      Dockerfile
  │
  ├─node
  │      Dockerfile
  │
  └─work
          hosts
          playbook.yml
```


# コントロールノードの構築

```bash:
ansible-training
  ├─ansible
  │      ansible.cfg
  │      Dockerfile
```

コントロールノードのDockerfileを作成します。
今回はCentOS7のイメージを使用します。

最低限必要なものとして、`sudo`とSSH接続のために`openssh-clients`をインストールしておきます。
CentOS7のイメージにはansibleが存在しないため、`eptl-release`をインストールしてから、`ansible`のインストールを行います。

```Dockerfile:コントロールノードのDockerfile
# CentOS7
FROM centos:7

# system update
RUN yum -y update && yum clean all

# install sudo
RUN yum -y install sudo
# install ssh clients
RUN yum -y install openssh-clients

# install ansible
RUN yum -y install epel-release && \
yum -y install ansible && \
yum clean all

# setttings
ADD ansible.cfg /etc/ansible
```

ハンズオンのためにAnsibleの設定変更を行います。
そのため、同じ階層に`ansible.cfg`を作成して、DockerfileのADDで上書き出来るようにします。

内容は、以下です。

- デフォルトのインベントリファイルを`/root/work/hosts`に変更
- ssh接続の設定変更

```config:Ansible.cfg
[defaults]
inventory = /root/work/hosts

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
```

# ターゲットノードの追加

```bash:
ansible-training
  ├─node
  │      Dockerfile
```

ターゲットノードのDockerfileを作成します。
こちらもCentOS7のイメージを使用します。

ssh接続のために`openssh-server`をインストールします。

その他に以下を行います。

- SSHの公開鍵作成
- rootのパスワード設定
- 22番ポートの公開
- SSHデーモンの起動

```Dockerfile:ターゲットノードのDcokerfile
# CentOS7
FROM centos:7

# install ssh server
RUN yum -y install openssh-server && yum clean all

# create Public key
RUN ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa
RUN ssh-keygen -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa

# set to login as root
RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config

# set a password for root
RUN echo "root:" | chpasswd

# expose 22 port
EXPOSE 22

# start up sshd
CMD ["/usr/sbin/sshd", "-D"]
```

# サービスの定義(docker-compose.yml)の作成

```bash:
ansible-training
  │  docker-compose.yml
```

サービスの定義を記述する`docker-compose.yml`を作成します。

コントロールノードは`ansible`です。ボリュームとして、ホストの`./work`をコントロールノードの`/root/work`と共有します。

ターゲットノードは`node0x`(例の場合、xは1-3)です。
version3.4よりサポートされたExtension Fieldを用いて、ターゲットノードの定義を`x-node: &node`と書くことで、変数のように使用しています。呼び出し側は`*node`です。

ターゲットノードの`ports`は必要ではありませんが、ハンズオンの結果の確認で使用するために定義しています。

:::note info
ターゲットノード数を調整したい場合は、servicesを調整してください。
:::
:::note warn
ターゲットノードは、ハンズオンの際にsystemdでhttpdを起動するため、/sbin/initで実行します。
:::

```yml:docker-compose.yml
version: '3.8'

x-node: &node
  build: ./node
  privileged: true
  command: /sbin/init
  tty: true

services:
  ansible:
    container_name: 'ansible'
    build: ./ansible
    tty: true
    working_dir: '/root/work'
    volumes:
      - ./work:/root/work

  node01:
    container_name: node01
    <<: *node
    ports:
      - '8081:80'
  node02:
    container_name: node02
    <<: *node
    ports:
      - '8082:80'
  node03:
    container_name: node03
    <<: *node
    ports:
      - '8083:80'
```

# 作業ディレクトリの作成

```bash:
ansible-training
  └─work
          hosts
          playbook.yml
```

コントロールノードで作業するディレクトリを作成します。
このディレクトリをホスト側とDocker側で共有しますので、Dockerを削除しても作業内容が保存されます。

### インベントリファイルの作成

`hosts`はAnsibleに設定するインベントリファイルです。
先ほど作成した`ansible.cfg`でデフォルトのインベントリとして指定されおり、ターゲットノードをnodeグループとして設定しています。

:::note warn
docker-compose.ymlでターゲットノードを調整した場合は、hostsでも指定してください。
:::

```config:hosts
[node]
node01
node02
node03
```
### playbookの作成

Ansibleでは、Playbookでターゲットノードの状態を定義することで管理を行います。
定義ファイルはYAML形式で行います。ファイル名は任意ですが、今回はわかりやすいように`playbook.yml`とします。

以下のPlaybookは、Apache(httpd)をインストールして起動した状態を定義しています。
`hosts: node`で、先ほどのインベントリで定義したnodeグループに対して実行されます。

```yml:playbook.yml
---
- name: deploy httpd server
  hosts: node
  become: yes
  gather_facts: no
  tasks:
    - name: install httpd
      yum:
        name: httpd
        state: latest

    - name: start & enabled httpd
      service:
        name: httpd
        state: started
        enabled: yes
```

# Ansibleハンズオン

### コンテナのビルド・起動

まずは、ansible-taining配下に移動してください。
(`<BASE_DIR>`はansible-trainingまでのフォルダパス置き換えてください。)

```bash:ansible-trainingに移動
$ cd <BASE_DIR>/ansible-training
```
各ノードをビルドします。

```bash:コンテナのビルド
$ docker-compose build --no-cache
```

各ノードを起動します。

```bash:コンテナの起動
$ docker-compose up -d
```

### コントロールノードの動作確認

起動が出来たら、コントロールノード(ansibleコンテナ)に入りましょう。

```bash:コントロールノードに入る
$ docker-compose exec ansible bash
```

インストールしたAnsibleの動作確認を行います。
pingモジュールを使用して、localhost(コントロールノード自身)に疎通確認を行います。

```bash:Ansibleの動作確認
$ ansible localhost -m ping
```

以下の通りに結果が表示されればAnsibleが正常にインストールされています。

```bash:確認結果
localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
```

### ターゲットノードを操作

各ターゲットノードに対して、SSHで接続確認を行います。  
この作業で、予めターゲットノードをSSHのknown_hostsに登録します。
  
:::note info
node0xのxはターゲットノードの番号に置き換えてください。
:::

:::note info
接続を続けるかを聞かれた場合はyesと入力して下さい。
:::

```bash:SSHのknown_hostsに登録
$ ssh node0x
$ exit
```

pingモジュールを使用して、ターゲットノードに対して疎通確認を行います。

```bash:ターゲットノードとの疎通確認
$ ansible node -m ping
```

以下のように、各ターゲットノードの結果が表示されれば接続は成功です。

```bash:確認結果
node01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
node03 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
node02 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
```

### Apache(httpd)をターゲットノードで起動

AnsibleはPlaybookという単位で、ターゲットノードの状態を定義することで操作を行えます。
今回は事前にhttpdのインストールと起動を行うPlaybookを作成してるので、実行しましょう。

playbook.yml を実行して、ターゲットノードに httpd をセットアップします。

```bash:Playbookの実行
$ ansible-playbook playbook.yml
```

```:実行結果
PLAY [deploy httpd server] *********************************************************************************************

TASK [install httpd] ***************************************************************************************************
fatal: [node01]: FAILED! => {"changed": false, "msg": ["Could not detect which major revision of yum is in use, which is required to determine module backend.", "You can manually specify use_backend to tell the module whether to use the yum (yum3) or dnf (yum4) backend})"]}
fatal: [node02]: FAILED! => {"changed": false, "msg": ["Could not detect which major revision of yum is in use, which is required to determine module backend.", "You can manually specify use_backend to tell the module whether to use the yum (yum3) or dnf (yum4) backend})"]}
fatal: [node03]: FAILED! => {"changed": false, "msg": ["Could not detect which major revision of yum is in use, which is required to determine module backend.", "You can manually specify use_backend to tell the module whether to use the yum (yum3) or dnf (yum4) backend})"]}

PLAY RECAP *************************************************************************************************************
node01                     : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
node02                     : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
node03                     : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
```

各ターゲットノードで、`failed=1`になるはずです。
エラーには色々書いてありますが、`sudo`がインストールされていないために、httpdのインストールで権限周りの問題が起きています。

ターゲットノードに`sudo`をインストールします。
Playbookを用意してもよいですが、ここではshellモジュールを使用して、直接sudoのインストールを実行します。

```bash:sudoのインストール
ansible node -m shell -a 'yum -y install sudo'
```

インストールが完了したら、再度Playbookを実行してみてください。

:::note info
Ansibleは"冪等性"の概念があるため、Playbookの実行が途中で失敗しても、再実行することで整合性を保つことができます。切り戻しなどは必要ありません。
:::

```bash:Playbookの再実行
$ ansible-playbook playbook.yml
```

```:実行結果
PLAY [deploy httpd server] *********************************************************************************************

TASK [install httpd] ***************************************************************************************************
changed: [node02]
changed: [node03]
changed: [node01]

TASK [start & enabled httpd] *******************************************************************************************
changed: [node03]
changed: [node01]
changed: [node02]

PLAY RECAP *************************************************************************************************************
node01                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
node02                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
node03                     : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

各ターゲットノードで、`ok=2 changed=2`になれば成功です。

これだけで全てのターゲットノードでhttpdのセットアップが完了です。
ターゲットノードで httpd が正常に動作しているかをブラウザで確認してみましょう。

:::note info
xはターゲットノードの番号に置き換えてください。
:::

**node0x: http://localhost:808x**

この画面が開けば正常に動作しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1166959/24cf4e37-cca6-dc8c-d609-1bbbfb489981.png)

# さいごに

Ansibleを手軽に触ることができるように、Dockerを用いてローカル環境構築する方法と簡単なハンズオンをまとめました。
ターゲットノード数の変更したり、httpdのセットアップ以外のPlaybookを作成したりして、Ansibleの理解に役立てていただければ幸いです。


# 参考

https://github.com/irixjp/katacoda-scenarios

https://dev.classmethod.jp/articles/practice_ansible/