Ansible+serverspec-runner+PackerでAWSとGCPに対応したシンプルなマルチクラウドインフラCI環境を構築する

<!-- ![eyecatch_AdventCalendar2018_11.png](https://qiita-image-store.s3.amazonaws.com/0/14093/04ea51b3-682b-2228-6446-6b4b3b8b5df2.png) -->

<!-- これは ドリコム [ドリコム Advent Calendar 2018](https://adventar.org/calendars/3421) の11日目です。
10日目は北原（ちっそ）さんによる、 [「私と仕事どっちが大事なの！？」にクリシン的に回答してみる](https://tech.drecom.co.jp/whats_more_important_me_or_work/)  です。 -->

# 自己紹介
こんにちは。ドリコムでインフラ周りをやっているひらしーです。
今回はマルチクラウドに対応したCI環境を手軽に作れる手法の1つとして、Ansible+serverspec-runner+Packerを使った手法を紹介します。

# インフラCI環境の構成について

サーバアプリケーション、インフラ周りの状態のテストを行うツールは数多く存在します。
世の中には複雑な構成をプラグラマブルに記載することが必要なケースもありますが、アプリケーションのロジック以外では比較的簡素な状態の確認のみで済む場合が多く、それよりもクラウドに依存した構成に柔軟に対応するためにインフラ構成を作り直すスピードが重要なケースが増えていると思います。
今回の構成はインフラ周りで比較的シンプル且つクラウド、ネットワークに依存しない記述で実現できる構成を検討しました。

# サンプルの動作環境
本内容では以下の環境での動作を確認しています。
各ツールの詳細については以下で紹介している公式ページやgithubのリポジトリをご参照下さい。
また、クラウド側は事前設定にてVMインスタンスが画面操作等により作成可能、プライベートIPにてsshで接続できることとします。

- ツール
    - OS: CentOS 7.2, macOS 10.13.6(ローカル環境のみ)
    - ruby: 2.4.5(ローカル環境)
    - python: 2.7.10
    - ansible: 2.6.2
    - packer: 1.3.2
    - serverspec-runner: 1.3.8

- 対応クラウド
    - AWS
    - GCP

# 各ツールの簡単な説明

## Ansible
[[official](https://www.ansible.com/)][[Github](https://github.com/ansible/ansible)]

[Chef](https://github.com/chef/chef),[itamae](https://github.com/itamae-kitchen/itamae)といったいわゆる構成管理ツールで、レッドハット社が開発しているOSSです。
本体はPythonで作られていますが拡張はPython以外の言語でも開発可能で、基本的にYAMLで作ることになります。
エージェントレスですが、リモート実行先にはPythonが実行できる環境が必要で、拡張モジュールの使用するライブラリによっては実行先のPythonのバージョンに注意が必要です。

## Packer
[[official](https://www.packer.io/)][[Github](https://github.com/hashicorp/packer)]

HachiCorp社の開発しているマシンイメージを管理するツールです。手軽に各クラウド業者に対応した仮想マシンイメージを作成することができます。

## serverspec-runner

[[Github](https://github.com/hiracy/serverspec-runner)][[Qiita](https://qiita.com/hiracy/items/1ebc7b82f1a6ffa69ed0)]

本家(serverspec): [[official](https://serverspec.org/)][[Github](https://github.com/mizzy/serverspec)]

インフラ環境の状態を管理するツールとしてはデファクトスタンダードとなっているserverspecを手軽に管理・実行するツールです。

# Ansibleでプロビジョニング

## インストール

``` sh
# CentOS
$ yum install ansible
# macOS
$ brew install ansible
```

## roleの作成

Ansibleで実際に構成管理の内容を作る際には[Ansible Roles](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)に準拠するのが一般的です。
role構造のスケルトンの作成はansibleをインストールに付随されるansible-playbookのコマンドにて実現できます。
サンプルとしてnginxの設定を作ってみます。

``` sh
$ mkdir advent-calendar-2018-ansible-serverspecrunner-packer-sample
$ cd advent-calendar-2018-ansible-serverspecrunner-packer-sample
$ ansible-galaxy init nginx
$ tree nginx
nginx
├── README.md
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
```

## 構成管理設定の記述

Chef,Itamaeで言う所の"レシピ"をYAMLにて記載します。

``` nginx/files/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1
```

``` nginx/tasks/main.yml
---
- name: set nginx repo file
  copy:
    src: nginx.repo
    dest: /etc/yum.repos.d/nginx.repo
    mode: 0644
    owner: root
    group: root

- name: install nginx
  package:
    name:  nginx
    state: present
```

role呼び出し元のYAMLファイルを作ります。

``` boot.yml
---
- name: bootstrap configuration
  hosts: all
  become: yes
  roles:
    - nginx
```

## ansibleの実行テスト

今回はpacker内で自動で接続設定されるのですが、事前に[インベントリファイル](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html)を用意することで既存サーバにて事前に実行テストすることも可能です。

``` image_vm.yml
---
default:
  hosts:
    image-vm:
  vars:
    ansible_host: xxx.xxx.xxx.xxx
    ansible_connection: ssh
    ansible_user: your_name
    ansible_port: 22
    ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
    ansible_ssh_private_key_file: /home/your_name/.ssh/id_rsa
```

``` sh
$ ansible-playbook -i image_vm.yml boot.yml

PLAY [bootstrap configuration] *********************************************

TASK [Gathering Facts] *****************************************************
ok: [image-vm]

TASK [nginx : set nginx repo file] *****************************************
changed: [image-vm]

TASK [nginx : install nginx] ***********************************************
changed: [image-vm]

PLAY RECAP *****************************************************************
image-vm                   : ok=1    changed=2    unreachable=0    failed=0
```

# serverspec-runnerによるインフラテスト

## インストールと初期設定

``` sh
# CentOS,macOS
$ gem install serverspec-runner
```

serverspec-runnerの初期設定を行うためにansible-galaxyで作成したディレクトリにて以下を実行します。

``` sh
$ serverspec-runner --activate-specroot --specroot .
```

## テストの作成

serverspec-runnerではserverspecと全く同じspecファイルが利用できます。
尚、スケルトン生成時にtestsディレクトリが自動生成されますがこちらは[公式のtestsディレクトリのドキュメント]((https://docs.ansible.com/ansible/latest/reference_appendices/test_strategies.html))に記載されているようにシェルスクリプトでのテスト方法が例示されています。

``` sh
$ mkdir -p spec/roles
```

``` spec/roles/nginx_spec.rb
require 'spec_helper'

describe package('nginx') do
  it { should be_installed }
end
```

## serverspec-runnerの実行

serverspec-runnerを実行するためには初期設定にて生成されたscenario.ymlというファイルを上書きしてテストシナリオを記載します。
こちらも既存のサーバに対して事前に実行できます。

``` scenario.yml
---
roles:
  - image-vm
```

serverspec-runnerはansibleのインベントファイルを扱えるので前述で作成されたものを指定します。

``` sh
$ serverspec-runner --inventory image_vm.yml --scenario scenario.yml
```

``` sh
### start [roles@image-vm] (xxx.xxx.xxx.xxx) serverspec... ###

Package "nginx"
  should be installed

Finished in 1.58 seconds (files took 1.87 seconds to load)
1 example, 0 failures

+-----------------------------------------+
|description                     | result |
+-----------------------------------------+
|roles@image-vm(xxx.xxx.xxx.xxx) |        |
|  Package "nginx"               |        |
|    should be installed         |   OK   |
+-----------------------------------------+
```

このようにシンプルな設定でサーバの状態をチェックすることができます。

# Packerによる仮想マシンイメージの作成

## インストールと初期設定

こちらの[公式ページ](https://www.packer.io/intro/getting-started/install.html)に従ってインストールして下さい。
ワンバイナリによる実行ファイルなのでプラットフォームにあった実行ファイルをコピーするだけでも問題ありません。

``` sh
# CentOS
$ wget https://releases.hashicorp.com/packer/1.3.2/packer_1.3.2_linux_amd64.zip
$ unzip packer_1.3.2_linux_amd64.zip
$ sudo cp packer /usr/local/bin
$ sudo chown root: /usr/local/bin/packer
$ sudo chmod 755 /usr/local/bin/packer
# macOS
$ brew install packer
```

AWSでの操作のためにcredentialを設定します。既に設定済みの方は無視して下さい。

``` sh
# ~/.aws/config にアクセスキー、シークレットキー設定が追加される
aws configure
```

## 設定ファイル

Packerはシンプルなkey-valueのみで構成されます。

今回は既に各クラウドにてVMインスタンスを起動できることが前提となっているため詳細は省かせて頂きます。
ポイントはpackerが実行するベースイメージに依存しないために[shell-local provisioner](https://www.packer.io/docs/provisioners/shell-local.html)を使用してローカル環境からserverspec-runnerを実行している点です。packerで一時的に作られるVMインスタンスのIPアドレスはpacker実行中のログからegrepで取得します。

``` machine.json
{
  "builders": [
    {
      "type": "amazon-ebs",
      "ssh_username": "ec2-user",
      "ssh_private_key_file": "/path/to/your/private_key",
      "ssh_keypair_name": "your_keypair_name",
      "ssh_port": "22",
      "ssh_pty": true,
      "temporary_key_pair_name": "temp_your_keypair_name_{{isotime}}",
      "region": "ap-northeast-1",
      "availability_zone": "ap-northeast-1a",
      "associate_public_ip_address": true,
      "instance_type": "t2.micro",
      "ami_name": "{{template_{{isotime | clean_ami_name}}",
      "user_data": "#!/bin/sh -xe\nsed -i 's/^.*requiretty/#Defaults requiretty/' /etc/sudoers\nservice sshd reload",
      "source_ami": "ami-xxxxxxxx",
      "security_group_ids": ["{{user `aws_security_group_id`}}"],
      "subnet_id": "sg-xxxxxxxx",
      "ssh_interface": "private_ip",

      "ami_block_device_mappings": [
        {
          "device_name": "/dev/sda1",
          "delete_on_termination": true,
          "volume_type": "gp2"
        }
      ]
    },
    {
      "type": "googlecompute",

      "ssh_pty": true,
      "ssh_port": "22",
      "ssh_username": "your_gcp_account_user",
      "ssh_private_key_file": "/path/to/your/private_key",
      "disable_default_service_account": true,
      "account_file": "/path/to/your/accout_file_path.json",
      "zone": "asia-northeast1-a",
      "project_id": "your_project_id",
      "machine_type": "n1-standard-1",
      "source_image_family": "centos-7",
      "disk_size": "20",
      "network": "your_gcp_network",
      "subnetwork": "your_gcp_subnetwork",
      "use_internal_ip": false,
      "tags": [
        "your_tag_1",
        "your_tag_2"
      ],
      "preemptible": true,
      "on_host_maintenance": "TERMINATE",
      "image_name": "template_{{isotime | clean_image_name}}"
    }
  ],
  "provisioners": [
    {
      "type": "ansible",
      "playbook_file": "./boot.yml"
    },
    {
      "type": "shell-local",
      "command": "export PACKER_TARGET_IP=$(egrep -m1 -o '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}' ./packer.log); sed -i -e \"s/xxx.xxx.xxx.xxx/$PACKER_TARGET_IP/g\" ./image_vm.yml"
    },
    {
      "type": "shell-local",
      "command": "serverspec-runner --inventory image_vm.yml --scenario scenario.yml"
    },
    {
      "type": "shell-local",
      "command": "export PACKER_TARGET_IP=$(egrep -m1 -o '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}' ./packer.log); sed -i -e \"s/$PACKER_TARGET_IP/xxx.xxx.xxx.xxx/g\" ./image_vm.yml"
    }
  ]
}
```

## Packerの実行

``` sh
# AWS
$ packer build -only="amazon-ebs" machine.json | tee packer.log
# GCP
$ packer build -only="googlecompute" machine.json | tee packer.log
```

尚、ansibleの実行やテストに失敗する場合は、以下のようなオプションを付ければイメージを破棄しないので作られたVMインスタンスを調査をすることが可能です。

``` sh
$ packer build -on-error=abort -only="googlecompute" machine.json | tee packer.log
```

# 最後に

本記事ではAnsible+serverspec-runner+Packerでマルチクラウドに対応したシンプルなインフラCI環境構築を紹介しました。
以下に今回の全てのコンテンツがありますので是非参考にして頂ければと思います。

[github.com/hiracy/advent-calendar-2018-ansible-serverspecrunner-packer-sample](https://github.com/hiracy/advent-calendar-2018-ansible-serverspecrunner-packer-sample)