Ansibleを使ってEC2を自分用のyumリポジトリにする

# はじめに
Ansibleを使って1台のEC2をyumリポジトリサーバに構成して、これを使って2台目以降のEC2にRPMパッケージをインストールしました。

## yumリポジトリを自作した目的

* 重要なパッケージを検証済みのバージョンで揃えたい
* インターネットに接続しないサーバにもパッケージをインストールしたい
* 各サーバにパッケージをインストールする際のバージョン指定は全部「latest」とし、リポジトリに配置するパッケージでバージョンをコントロールしたい
* 各サーバにリポジトリ設定を追加したくない

## 環境
使用した環境は以下のとおりですが、物理マシンにCentOSの環境でも特に違いはありません。

* コントロールノード: AWS Cloud9  
  Ansibleを実行するマシンです
* ターゲットノード: AWS EC2 / Amazon Linux 2  
  Ansibleで自動構成される対象ホストです

### コントロールノード
Ansibleをインストールする必要があるノードはコントロールノードだけです。EPELからyumでインストールします。

```Shell:
sudo yum -y install epel-release  # AWS Cloud9はインストール済み
sudo yum -y install ansible
```
### ターゲットノード
自動構成の対象となるターゲットノードに必要な追加ソフトウェアはありません。
必要な環境はPython 2.6以上か3.5以上だけなので Amazon Linux、CentOS 6 以上などがそのまま使用できます。

# インベントリとプレイブックの構成
今回の規模ならもっと単純でもいいのですが、規模によって構成をあまり変えたくないので、公式のベストプラクティスになるべく準拠して以下の構成にしました。

参考: [Best Practices — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)

```:ディレクトリ構成（全体）
.
├── inventories       # インベントリ用ディレクトリ
│   ├── development   # 開発環境用インベントリ（今回の対象）
│   ├── production    # 本番環境用インベントリ
│   └── staging       # ステージング環境用インベントリ
│
├── roles             # ロール用ディレクトリ
│   ├── common        # 共通ロール
│   ├── httpd         # HTTP Serverをインストールするロール
│   ├── openvpn       # OpenVPNをインストールするロール
│   └── repo          # リポジトリサーバを構成するロール
│
├── repo-servers.yml  # リポジトリサーバのグループ用プレイブック
├── site.yml          # マスタープレイブック
└── vpn-servers.yml   # VPNサーバのグループ用プレイブック
```

## インベントリ
`inventories`ディレクトリの直下は環境ごとのディレクトリで、開発環境、ステージング環境、本番環境用のディレクトリを用意しました。今回は開発環境用の`development`だけを使用します。

```:inventories/
├── inventories
│   ├── development  # 開発環境用（今回の対象）
│   ├── production   # 本番環境用
│   └── staging      # ステージング環境用
```
`development`の下は以下のような構成になっています。

```:inventories/development/
├── inventories
│   ├── development
│   │   ├── group_vars      # グループごとの変数定義を格納するディレクトリ
│   │   │   └── all.yml     # グループ「all」（全体）用の変数定義ファイル
│   │   ├── host_vars       # ホストごとの変数定義を格納するディレクトリ
│   │   │   ├── repo-1.yml  # ホスト「repo-1」用の変数定義ファイル
│   │   │   └── vpn-1.yml   # ホスト「vpn-1」用の変数定義ファイル
│   │   └── hosts.yml       # 開発環境用のインベントリ定義ファイル
```
### インベントリ定義ファイル
`hosts.yml`でホストとグループの全体を定義します。
書籍などではiniファイル形式で解説されていることが多いようですが、ここではYAML形式を使用します。

```Yaml:inventories/development/hosts.yml
all:               # グループ「all」（全体）
  children:        # allの子グループ
    repo-servers:  # グループ「repo-servers」
      hosts:       # repo-serversに属するホスト
        repo-1     # yumリポジトリを構成するホスト
    vpn-servers:   # グループ「vpn-servers」
      hosts:       # vpn-serversに属するホスト
        vpn-1      # openvpnをインストールするホスト
```
`all`は全体を表す定義済みのグループ名です。今回はユーザー定義の子グループとして`repo-servers`と`vpn-servers`の2つを定義し、それぞれにターゲットノードの`repo-1`と`vpn-1`を含めました。

### グループ変数定義ファイル
`group_vars`ディレクトリにはグループごとの変数定義ファイルを配置します。
今回は全ターゲットノードへのSSH接続に同じ秘密鍵を使用するので、全体用の変数として`all.yml`に定義しました。

```Yaml:inventories/development/group_vars/all.yml
ansible_ssh_private_key_file: ~/my-keypair.pem
```
`ansible_ssh_private_key_file`は、SSH秘密鍵のパスを格納するための定義済み変数です。

### ホスト変数定義ファイル
`host_vars`ディレクトリにはホストごとの変数定義ファイルを配置します。
例えば以下のように定義済み変数`ansible_host`に各ホストのIPアドレスを設定することで、「repo-1」「vpn-1」という名前で接続できるようになります。

```Yaml:inventories/development/host_vars/repo-1.yml
ansible_host: 172.16.1.4
```

```Yaml:inventories/development/host_vars/vpn-1.yml
ansible_host: 172.16.1.5
```

## ロール
プレイブック内の処理を再利用しやすくするために、分割してロールとして定義します。例えば Apache HTTP Server をインストールするロールは、リポジトリサーバの他に Webサーバでもそのまま利用できます。

`roles`ディレクトリの下にユーザー定義のロール名でディレクトリを作ります。今回は以下の4つのロールを定義しました。

```:roles/
├── roles
│   ├── common    # 共通ロール
│   ├── httpd     # HTTP Serverをインストールするロール
│   ├── openvpn   # OpenVPNをインストールするロール
│   └── repo      # リポジトリサーバを構成するロール
```
参考: [Roles — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)

### 共通ロール
共通ロール`common`は、全ターゲットノードに共通の構成のために作りました。
ロール名のディレクトリの下に、転送するファイルを格納する`files`とタスク定義ファイルを格納する`tasks`ディレクトリを配置します。

```:roles/common/
├── roles
│   ├── common
│   │   ├── files        # ターゲットノードに転送するファイルを格納するディレクトリ
│   │   │   └── etc
│   │   │       ├── yum.repos.d
│   │   │       │   └── myrepo.repo
│   │   │       └── hosts
│   │   └── tasks        # タスク定義ファイルを格納するディレクトリ
│   │       └── main.yml
```
#### 共通ロールのファイル
`files`ディレクトリの下はディレクトリ構成もファイル名も自由です。今回はターゲットノードに配置するパスのとおりに、`/etc/yum.repos.d/myrepo.repo`（リポジトリ設定ファイル）と`/etc/hosts`（ホスト名とIPアドレスの設定ファイル）を配置しました。

自作のリポジトリサーバを他のホストから利用できるようにするために、以下のリポジトリ設定ファイルを用意します。
`yum-plugin-priorities`がインストールされていれば、`priority`に10より小さい値を設定することで標準設定のリポジトリよりも優先されるようになります。

```:roles/common/files/etc/yum.repos.d/myrepo.repo
[myrepo]
name=myrepo
baseurl=http://repo-1.local/repo/
priority=2
gpgcheck=0
enabled=0
report_instanceid=yes
```
他のホストが`repo-1.local`という名前でリポジトリサーバに接続できるようにするために、以下の`/etc/hosts`ファイルも用意します。DNSを使用する場合は不要です。

```:roles/common/files/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost6 localhost6.localdomain6

172.16.1.4  repo-1.local
```
#### 共通ロールのタスク
1. `copy`モジュールを使用して、`files`で用意した`/etc/hosts`ファイルを転送して配置します
2. `copy`モジュールを使用して、`files`で用意したリポジトリ設定ファイルを転送して配置します
3. `yum`モジュールを使用して、インターネットのリポジトリから指定のパッケージをインストールします

```Yaml:roles/common/tasks/main.yml
- name: Put hosts file
  copy:
    dest=/etc/hosts
    src=roles/common/files/etc/hosts
- name: Put repo file
  copy:
    dest=/etc/yum.repos.d/myrepo.repo
    src=roles/common/files/etc/yum.repos.d/myrepo.repo
- name: Install common packages
  yum:
    name="{{item}}"
    state=latest
  with_items:
    - yum-plugin-priorities
    - bash-completion
```
`with_items:`以下に定義したパッケージが順にインストールされます。
`yum-plugin-priorities`は自作リポジトリを全ホストに優先利用させるために必須のパッケージです（Amazon Linux 2にはデフォルトでインストールされていますが、必須であることをここで明示します）。

その他、全ホストにインストールしたいパッケージをここにいくつでも追加定義することができます。私は`bash-completion`が入っていないと耐えられないので追加しました。

各モジュールの公式ドキュメント:
[copy – Copy files to remote locations](https://docs.ansible.com/ansible/latest/modules/copy_module.html#copy-module)
[yum – Manages packages with the yum package manager](https://docs.ansible.com/ansible/latest/modules/yum_module.html#yum-module)

### HTTP Serverロール
ロール`httpd`はApache HTTP Serverをインストールするために定義しました。
ターゲットノードに転送するファイルはないので、`tasks`ディレクトリだけを配置します。

```:roles/httpd/
├── roles
│   ├── httpd
│   │   └── tasks
│   │       └── main.yml
```
#### HTTP Serverロールのタスク
1. `yum`モジュールを使用して、パッケージ`httpd`をインストールします
2. `service`モジュールを使用して、サービス`httpd`を起動状態にして自動実行にします

```Yaml:roles/httpd/tasks/main.yml
- name: Install HTTPD packages
  yum:
    name="{{item}}"
    state=latest
  with_items:
    - httpd
- name: Start and enable the service
  service:
    name=httpd
    state=started
    enabled=yes
```

各モジュールの公式ドキュメント:
[yum – Manages packages with the yum package manager](https://docs.ansible.com/ansible/latest/modules/yum_module.html#yum-module)
[service – Manage services](https://docs.ansible.com/ansible/latest/modules/service_module.html#service-module)

### OpenVPNロール
ロール`openvpn`はOpenVPNをインストールするために定義しました。

```:roles/openvpn/
├── roles
│   ├── openvpn
│   │   └── tasks
│   │       └── main.yml
```

#### OpenVPNロールのタスク
自作リポジトリ`myrepo`を有効化して`openvpn`パッケージをインストールするだけです。

```Yaml:roles/openvpn/tasks/main.yml
- name: Install VPN packages
  yum:
    enablerepo=myrepo
    name="{{item}}"
    state=latest
  with_items:
    - openvpn
```

### リポジトリロール
ロール`repo`は共通ロールおよびHTTP Serverロールとの組み合わせで、リポジトリサーバを構成するためのロールです。
`files`と`tasks`に加えて、タスクからの通知で実行されるハンドラを格納するための`handlers`ディレクトリを持ちます。

```:roles/repo/
├── roles
│   └── repo
│       ├── files
│       │   └── var
│       │       └── www
│       │           └── html
│       │               └── repo
│       │                   └── Packages
│       │                       └── openvpn
│       │                           ├── lzo-2.06-8.amzn2.0.3.x86_64.rpm
│       │                           ├── openvpn-2.4.7-1.el7.x86_64.rpm
│       │                           └── pkcs11-helper-1.11-3.el7.x86_64.rpm
│       ├── handlers
│       │   └── main.yml
│       └── tasks
│           └── main.yml
```
#### リポジトリロールのファイル
yumリポジトリで公開するRPMパッケージファイルです。今回はOpenVPNと依存パッケージのみを配置します。HTTP Serverで公開するので、配置場所は`/var/www/html/`の下にします。

#### リポジトリロールのタスク
1. `yum`モジュールを使用して、リポジトリデータベースを更新するためのコマンド`createrepo`をインストールします
2. `synchronize`モジュール（rsync）を使用して、RPMパッケージファイルをディレクトリごと`/var/www/html/repo/`に配置します
3. ファイルが追加または変更されたら、通知`packages changed`を発行してハンドラを起動します。

```Yaml:roles/repo/tasks/main.yml
- name: Install commands for repository server
  yum:
    name="{{item}}"
    state=latest
  with_items:
    - createrepo
- name: Put repository packages
  synchronize:
    delete=yes
    dest=/var/www/html/repo/
    src=roles/repo/files/var/www/html/repo/Packages
  notify: "packages changed"
```

各モジュールの公式ドキュメント:
[yum – Manages packages with the yum package manager](https://docs.ansible.com/ansible/latest/modules/yum_module.html#yum-module)
[synchronize – A wrapper around rsync to make common tasks in your playbooks quick and easy](https://docs.ansible.com/ansible/latest/modules/synchronize_module.html#synchronize-module)

#### リポジトリロールのハンドラ
タスクから`packages changed`が通知されたら、`listen:`でこの通知をリスニングしている以下のハンドラが実行されます。ここでは`createrepo`コマンドを実行してリポジトリデータベースを更新します。

```Yaml:roles/repo/handlers/main.yml
- name: Run createrepo
  command: /usr/bin/createrepo /var/www/html/repo
  listen: "packages changed"
```
コマンド`createrepo /var/www/html/repo`を実行することで、指定のパスの直下にレポジトリデータベースが作成または更新されます。

```:createrepo未実行
var
└── www
    └── html
        └── repo
            └── Packages
                └── openvpn
                    ├── lzo-2.06-8.amzn2.0.3.x86_64.rpm
                    ├── openvpn-2.4.7-1.el7.x86_64.rpm
                    └── pkcs11-helper-1.11-3.el7.x86_64.rpm
```
`/var/www/html/repo/repodata/`にデータベースが作成されます。

```:createrepo実行後
var
└── www
    └── html
        └── repo
            ├── Packages
            │   └── openvpn
            │       ├── lzo-2.06-8.amzn2.0.3.x86_64.rpm
            │       ├── openvpn-2.4.7-1.el7.x86_64.rpm
            │       └── pkcs11-helper-1.11-3.el7.x86_64.rpm
            └── repodata
                ├── 4b2f....7463.xml.gz
                ├── 637d....275f.xml.gz
                ├── 6484....cacf-filelists.sqlite.bz2
                ├── e80e....9d8f-primary.sqlite.bz2
                ├── e9d8....df04-other.xml.gz
                ├── ffc4....ee24-other.sqlite.bz2
                └── repomd.xml
```
これで`/var/www/html/repo`がyumリポジトリになります。
`/var/www/html`はHTTP Serverのドキュメントルートなので、他のホストに対して`http://ホスト名/repo/`のURLでyumリポジトリを公開できるようになります。

## プレイブック
リポジトリサーバのグループ用、VPNサーバのグループ用、およびそれら2つを順に実行するマスタープレイブックの、計3つのプレイブックを作成しました。

```:プレイブック
.
├── repo-servers.yml  # リポジトリサーバのグループ用
├── site.yml          # マスタープレイブック
└── vpn-servers.yml   # VPNサーバのグループ用
```
2つのホストグループはインベントリ定義ファイルで以下のように定義したので、`repo-servers`を対象として`ansible-playbook `を実行するとホスト`repo-1`が、`vpn-servers`対象として実行するとホスト`vpn-1`が自動的に構成されます。

```Yaml:inventories/development/hosts.yml
    repo-servers:  # グループ「repo-servers」
      hosts:       # repo-serversに属するホスト
        repo-1     # yumリポジトリを構成するホスト
    vpn-servers:   # グループ「vpn-servers」
      hosts:       # vpn-serversに属するホスト
        vpn-1      # openvpnをインストールするホスト
```

### repo-serversグループ用プレイブック
ターゲットノードでsudo を使用して、ロールを`httpd`、`repo`、`common`の順に実行します。
このプレイブックが正常に終了すると自作のyumリポジトリが利用可能になります。

```Yaml:repo-servers.yml
- name: Deploy repo servers
  hosts: repo-servers
  become: yes
  roles:
    - httpd
    - repo
    - common
```

### vpn-serversグループ用プレイブック
ターゲットノードでsudo を使用して、ロールを`common`、`openvpn`の順に実行します。
`openvpn`は自作リポジトリからパッケージを取得してインストールします。

```Yaml:vpn-servers.yml
- name: Deploy VPN servers
  hosts: vpn-servers
  become: yes
  roles:
    - common
    - openvpn
```

### マスタープレイブック
上記2つのプレイブックを順に実行します。

```Yaml:site.yml
- import_playbook: repo-servers.yml
- import_playbook: backgate-servers.yml
```

# プレイブックの実行
`ansible-playbook`コマンドの`-i`オプションでインベントリ定義ファイルを指定し、引数でマスタープレイブックを指定して実行します。
エラーが発生する場合は`-vvv`などのオプションを追加して詳細を確認してください。

```Shell:
ansible-playbook -i inventories/development/hosts.yml site.yml
```

# あとがき
Ansibleの進化が速いので、書籍などの情報がだんだん古くなっているようです。がんばって公式ドキュメントをもっと読もうと思います。

- 公式ドキュメント: [Ansible Documentation](https://docs.ansible.com/ansible/latest/index.html)