続！Tinderで右スワイプするためにCI/CD環境を作ってみたお話

# はじめに

[前回](https://qiita.com/Healthya_Corp/items/f50a538ec8570331c212)はアプリレイヤーまでの自動化を実現しました。今回は、これに加えテスト用サーバの作成まで含めて自動化してみたので、やったことをメモとして残しておきます。
具体的には、今回は

- [Ansible](https://docs.ansible.com/ansible/2.8/index.html)でブータブルディスクを使いテスト用のVMをデプロイ
- [Serverspec](https://serverspec.org/)でインフラテスト
- Ansibleで使い終わったVMの削除

を実行するジョブを作成しました。

Ansibleは仕事で少し使ってて覚えがあったから＆設定項目をymlでシンプルに書けるから、Serverspecはテスト項目を直感的に書けるから、という理由で採用しました。

VMデプロイに関してはテンプレートからコピーすればいいのでは……？と思う人もいるかもしれません。が、今回のケースでは無償版のvSphereを使っているため、テンプレート・vMotionを始めとする便利な機能をほぼ使えず、ブータブルメディアを使ってOSをインストールするしかなかったのです。つらい。

# システム概要

<img width="1041" alt="スクリーンショット 2019-11-10 11.17.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266022/417da9f8-f6ef-863c-215e-34cef99b08cd.png">


赤字が今回新しく作る項目です。

ちなみに前まで使ってたWin機は買い替えJenkinsもCentOSに移植しました。~~何もしなくてもシェルスクリプトを使える喜びが胸いっぱいに広がります。~~

# やったこと

AnsibleやServerspecのインストールについては公式ドキュメント見たほうが早いので省略します。

## isoファイルにks.cfgを仕込む
[Kickstart による自動インストール用 .iso ファイルを作成するには](http://sig9.hatenablog.com/entry/2015/04/12/113056)を参考に、kickstartを仕込んだブータブルディスクを作成しました。なお、この作業はJenkins入れたCentOS上で実行しました。

[CentOSの公式サイト](http://isoredirect.centos.org/centos/7/isos/x86_64/)からCentOS 7.7(Minimal)のisoファイルをダウンロードし、中身を抜きます。

```bash
# 以下の作業は全てrootで実行
wget http://ftp.riken.jp/Linux/centos/7.7.1908/isos/x86_64/CentOS-7-x86_64-Minimal-1908.iso
# isoをマウント
mkdir /root/mountpoint
mount -t iso9660 CentOS-7-x86_64-Minimal-1908.iso /root/mountpoint
# isoの中身をコピー
mkdir /root/workspace
cp -r /root/mountpoint /root/workspace
```

その後、`/root`直下にある`anaconda-ks.cfg`をコピーし、kickstart用のcfgを作成します。作成した`ks.cfg`は`/root/workspace`直下に置きます。

```cfg:ks.cfg
#version=DEVEL
# System authorization information
auth --enableshadow --passalgo=sha512
# Use CDROM installation media
cdrom
# Use graphical install
text
# Run the Setup Agent on first boot
firstboot --enable
ignoredisk --only-use=sda
# Keyboard layouts
keyboard --vckeymap=jp --xlayouts='jp'
# System language
lang ja_JP.UTF-8

# Network information
network  --bootproto=static --device=link --gateway=<デフォルトGWのip> --ip=<固定ip> --netmask=<サブネットマスク> --nameserver=8.8.8.8 --noipv6
network  --hostname=<ホスト名>

# Root password
rootpw --iscrypted <ハッシュ化したパスワード（/etc/shadowから抜き取る等して作成）>
# System services
services --disabled="chronyd"
# System timezone
timezone Asia/Tokyo --isUtc --nontp
# System bootloader configuration
zerombr
bootloader --location=mbr --boot-drive=sda
# Partition clearing information
clearpart --none --initlabel
# Disk partitioning information
autopart --type="thinp" --fstype="xfs" --nohome

%packages
@^minimal
@core

%end

%addon com_redhat_kdump --disable --reserve-mb='auto'

%end

%anaconda
pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty
pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok
pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty
%end

# reboot when finish installing
reboot --eject
```
次に、`isolinux/isolinux.cfg`を以下の様に書き換えます。

```cfg:isolinux/isolinux.cfg
#default vesamenu.c32
default kickstart
timeout 600

display boot.msg

# Clear the screen when exiting the menu, instead of leaving the menu displayed.
# For vesamenu, this means the graphical background is still displayed without
# the menu itself for as long as the screen remains in graphics mode.
menu clear
menu background splash.png
menu title CentOS 7
menu vshift 8
menu rows 18
menu margin 8
#menu hidden
menu helpmsgrow 15
menu tabmsgrow 13

# Border Area
menu color border * #00000000 #00000000 none

# Selected item
menu color sel 0 #ffffffff #00000000 none

# Title bar
menu color title 0 #ff7ba3d0 #00000000 none

# Press [Tab] message
menu color tabmsg 0 #ff3a6496 #00000000 none

# Unselected menu item
menu color unsel 0 #84b8ffff #00000000 none

# Selected hotkey
menu color hotsel 0 #84b8ffff #00000000 none

# Unselected hotkey
menu color hotkey 0 #ffffffff #00000000 none

# Help text
menu color help 0 #ffffffff #00000000 none

# A scrollbar of some type? Not sure.
menu color scrollbar 0 #ffffffff #ff355594 none

# Timeout msg
menu color timeout 0 #ffffffff #00000000 none
menu color timeout_msg 0 #ffffffff #00000000 none

# Command prompt text
menu color cmdmark 0 #84b8ffff #00000000 none
menu color cmdline 0 #ffffffff #00000000 none

# Do not display the actual menu unless the user presses a key. All that is displayed is a timeout message.

menu tabmsg Press Tab for full configuration options on menu items.

menu separator # insert an empty line
menu separator # insert an empty line

label linux
  menu label ^Install CentOS 7
  kernel vmlinuz
  append initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 quiet

label check
  menu label Test this ^media & install CentOS 7
  #menu default (comment out-ed)
  kernel vmlinuz
  append initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 rd.live.check quiet

# kickstart menu
label kickstart
  menu label ^Kickstart Installation of CentOS 7
  menu default
  kernel vmlinuz
  append initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 inst.ks=cdrom:/ks.cfg

menu separator # insert an empty line

# utilities submenu
menu begin ^Troubleshooting
  menu title Troubleshooting

label vesa
  menu indent count 5
  menu label Install CentOS 7 in ^basic graphics mode
  text help
	Try this option out if you're having trouble installing
	CentOS 7.
  endtext
  kernel vmlinuz
  append initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 xdriver=vesa nomodeset quiet

label rescue
  menu indent count 5
  menu label ^Rescue a CentOS system
  text help
	If the system will not boot, this lets you access files
	and edit config files to try to get it booting again.
  endtext
  kernel vmlinuz
  append initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 rescue quiet

label memtest
  menu label Run a ^memory test
  text help
	If your system is having issues, a problem with your
	system's memory may be the cause. Use this utility to
	see if the memory is working correctly.
  endtext
  kernel memtest

menu separator # insert an empty line

label local
  menu label Boot from ^local drive
  localboot 0xffff

menu separator # insert an empty line
menu separator # insert an empty line

label returntomain
  menu label Return to ^main menu
  menu exit

menu end
```
以上が終わったら下記コマンドでブータブルiso（`centos7_ks.iso`）を作成し、Web Client経由でESXi側に置いておきます。

```bash
cd /root/workspace
mkisofs -J -T -o /root/centos7_ks.iso -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -R -m TRANS.TBL -graft-points -V "CentOS 7 x86_64" /root/workspace
```

## Ansible Playbookを作成
今回は以下の3つのPlaybookを作成します。

- VM作成用
- サーバ基本設定用
- VM削除用

今回は[Ansible公式ドキュメントのBest Practice](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)を元に以下のファイル構成を採用しました。VM作成・基本設定・削除ごとにディレクトリを分けています。

```:基本構成
.
├── ansible.cfg 　# sshで設定流す際のオプション等を記述
├── roles 　　　　 # 設定用のymlを格納
│   ├── <role A>
│   │   └── tasks
│   │       ├── <設定実行用のyml>.yml
│   │       │      :
│   │       └── main.yml
│   └── <role B>
│   :   └── tasks
│           ├── <設定実行用のyml>.yml
│           │      :
│           └── main.yml
├── site.yml　　　# 読み込むplaybookを記述
├── hosts　　　　 # inventoryを記述
└── testservers.yml  # hostsで定義したグループと実行対象のroleの組み合わせを定義
```

ここで、tasks以下には以下のディレクトリを配置します（各ディレクトリに置くymlについては後述）

- VM作成
    - `prepare`: VM作成準備用
    - `esxi`: VM作成用
- VM設定
    - `setting`
- VM削除
    - `tideup`

共通設定として、`ansible.cfg`と`hosts`と、各roleの`main.yml`、そして`site.yml`は以下の様に作成しました。
`ansible.cfg`の`deprecation_warning`は、「将来このモジュールは使えなくなりますよ〜〜〜」という警告を出さなくするものなので、不要っちゃ不要かもしれません。

```cfg:ansible.cfg
[defaults]
deprecation_warnings = False

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

```:hosts
[prepare]
<ESXiのIPアドレス>

[esxi]
<JenkinsサーバのIPアドレス>

[vm]
<テスト用サーバのIPアドレス>

[tideup]
<JenkinsサーバのIPアドレス>
```

```yml:main.yml
- name: <roleの説明>
  include_tasks: <実行するyml>.yml
```

```yml:site.yml
- name: include required playbooks
  import_playbook: testservers.yml
```

### VM作成用
AnsibleにはESXi上にVMを作成するためのモジュールとして、`vmware_guest`と`vsphere_guest`があります。他の記事でも言われていますが、無償版vSphereでは`vsphere_guest`でないとVMの作成が出来ない＆`vsphere_guest`はAnsibleのバージョンが**2.8以下**でないと使えないので注意しましょう。
また、Ansibleの実行サーバで`vsphere_guest`を使うために、
証明書エラーが出た場合は`validate_certs`で`no`を指定してエラーを回避しましょう。
先に.vmdkファイルなどを格納するディレクトリを作成するPlaybookも実行していますが、ぶっちゃけ不要かもしれません。。。

```yml:testservers.yml
- hosts: prepare
  roles:
    - prepare

- hosts: esxi
  roles: 
    - deploy
```

```yml:roles/prepare/tasks/prapare.yml
- name: Create directory for VM
  file:
    path: "/vmfs/volumes/datastore1/<テスト用VMのホスト名>"
    state: directory
    mode: 0755
```

```yml:roles/esxi/tasks/vmware.yml
- name: Create a virtual machine on given ESXi
  vsphere_guest:
    vcenter_hostname: <ESXiのIPアドレス>
    username: root
    password: "<ESXiのパスワード>"
    guest: <VMのホスト名>
    state: powered_on
    vm_extra_config:
      vcpu.hotadd: yes
      mem.hotadd: yes
      notes: This VM is used for app test
    validate_certs: no
    vm_disk:
      disk1:
        size_gb: 50
        type: thin
        datastore: datastore1
    vm_hardware:
      memory_mb: 4096
      num_cpus: 2
      osid: centos64Guest
      scsi: paravirtual
      vm_cdrom:
        type: iso
        iso_path: "datastore1/iso/centos7_ks.iso"
    vm_nic:
      nic1:
        type: vmxnet3
        network: VM Network
        network_type: standard
    esxi:
      hostname: <ESXiのホスト名>
      datacenter: ha-datacenter
```

### 基本設定用
VM作成後に以下の基本設定用のplaybookを作成します。
`rpm_key`モジュールで使用するリポジトリのGPG KEYを追加しておかないと`yum install`時にエラーとなるので注意が必要です。

```yml:testservers.yml
- hosts: vm
  roles: 
    - setting
```

```yml:roles/setting/tasks/install_packages.yml
# epelを追加
- name: enable epel repository
  yum_repository:
    name: epel
    description: EPEL YUM repo
    baseurl: http://ftp.riken.jp/Linux/fedora/epel//7/x86_64/

# base用のGPG KEYを追加
- name: exec rpm --install (base/extras/updates)
  rpm_key:
    state: present
    key: http://ftp.riken.jp/Linux/centos/RPM-GPG-KEY-CentOS-7

# epel用のGPG KEYを追加
- name: exec rpm --install (epel)
  rpm_key:
    state: present
    key: http://ftp.riken.jp/Linux/fedora/epel/RPM-GPG-KEY-EPEL-7

- name: install required packages
  yum:
    name: "{{ packages }}"
    state: latest
  vars:
    packages:
      - "http://ftp.riken.jp/Linux/remi/enterprise/remi-release-7.rpm"
      - python3
      - python2-pip

- name: install required packages with pip
  pip:
    name: "{{ modules }}"
    executable: pip3.6
  vars:
    modules:
      - requests
      - bs4
      - numpy
```

### テスト用VM削除用

```yml:testservers.yml
- hosts: tideup
  roles:
    - tideup
```

```yml:roles/tideup/tasks/tideup.yml
- name: Destroy a virtual machine
  vsphere_guest:
    vcenter_hostname: <ESXiのIPアドレス>
    username: "<ユーザ名>"
    password: "<パスワード>"
    guest: <削除したいVMのホスト名>
    state: absent
    force: yes
```

## Serverspecを使ったインフラテスト自動化

以下の項目について単体テストを書きます。

- IPアドレス・デフォルトGWを適切に設定できているか？
- 必要なyumレポジトリを有効にできているか？
- 必要なserviceを`yum install`できているか？
- 必要なパッケージを`pip install`できているか？

`serverspec-init`コマンドを叩いてServerspec実行ファイルの雛形を作成し、以下の様にテスト用のコードを書きます。

```ruby:spec/ホスト名orIPアドレス/sample_spec.rb
require 'spec_helper'

# ネットワーク設定の確認
describe host('<ホスト名 or IPアドレス>') do
  its(:ipaddress) { should eq '<IPアドレス>' }
end
describe default_gateway do
  its(:ipaddress) { should eq '<デフォルトGW>' }
end


# epelリポジトリがあるか確認
describe yumrepo('epel'), :if => os[:family] == 'redhat' do
  it { should exist }
  it { should be_enabled }
end


# Pythonまわりのyum installが実行されているか確認
describe package('python2-pip'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end
describe package('python3'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end


# 必要なパッケージがpip installされているか確認
describe package('requests') do
  it { should be_installed.by('pip3') }
end
describe package('bs4') do
  it { should be_installed.by('pip3') }
end
describe package('numpy') do
  it { should be_installed.by('pip3') }
end
```

ここで、実行ユーザをrootで指定するため、`spec_helper.rb`を以下の様に変更します。

```ruby:spec/spec_helper.rb
require 'serverspec'
require 'net/ssh'

set :backend, :ssh

# sudoパスワード用の設定
if ENV['ASK_SUDO_PASSWORD']
  begin
    require 'highline/import'
  rescue LoadError
    fail "highline is not available. Try installing it."
  end
  set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
else
  set :sudo_password, ENV['SUDO_PASSWORD']
end

host = ENV['TARGET_HOST']

options = Net::SSH::Config.for(host)

options[:user] = 'root'
options[:paranoid] = false
options[:user_known_hosts_file] = "/dev/null"

set :host,        options[:host_name] || host
set :ssh_options, options

# ログインパスワード用の設定
if ENV['ASK_LOGIN_PASSWORD']
  options[:password] = ask("\nEnter login password: ") { |q| q.echo = false }
else
  options[:password] = ENV['LOGIN_PASSWORD']
end

set :ssh_options, options
```

## Jenkinsジョブの作成・修正
PlaybookをGitLabからcloneし、Ansibleを実行するジョブを作成します。このジョブはパイプラインでの実行を前提とするので、GitLabへのpush/mergeをトリガーとする様な設定は特に行いません。

### VM作成用ジョブ・基本設定用ジョブ・VM削除用ジョブ
以下のスクリプトを実行するジョブを作成します。
基本設定用ジョブではkickstartによるOS自動インストールが完了し、Ansibleによる設定を流せる状態になるまで待機する必要があります。OSインストールにはおおよそ8分かかるため、480秒待機する設定にしてあります。
正直頭の悪い処理だと思うので、より良い方法があったら教えていただきたいです………

```sh:実行スクリプト（Ansible）
sleep 480 # 基本設定用ジョブのみ
# ansibleによる基本設定を実行
ansible-playbook -i hosts site.yml --diff
```

### インフラテスト用ジョブ
以下のスクリプトを実行するジョブを作成します。

```sh:実行スクリプト（インフラテスト）
LOGIN_PASSWORD=<テスト用サーバのログインパスワード> /usr/local/bin/rake spec
```

### アプリテスト用ジョブ
アプリのテストはテストサーバ上で実行するので、前回作成したテスト用ジョブで実行するスクリプトを、以下の様に書き換えます。

```sh:実行スクリプト（アプリテスト）
echo "ファイルを転送"
sshpass -p "<パスワード>" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null * root@<IPアドレス>:/root/
echo "テストスクリプトを実行"
sshpass -p "<パスワード>" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@<IPアドレス> "python3 tinder_auto.py 10"
```


### パイプライン
前回作成したパイプラインを以下の様に書き換えます。

```:パイプライン
# VM作成、VM基本設定実行、VMテスト実行、テスト環境の削除、を追加
node {
    stage("VM作成"){
        build job: "deploy-testserver"
    }
    stage("VM基本設定実行"){
        build job: "setup-testserver"
    }
    stage("VMテスト実行"){
        build job: "check-testserver"
    }
    stage("Tinderコードのテスト"){
        build job: "tinder_test"
    }
    stage("コード配布"){
        build job: "tinder_pull"
    }
    stage("テスト環境の削除"){
        build job: "tideup-testserver"
    }
}
```

# 実行してみようのコーナー
上手く設定できていれば以下の様に実行されます。

<img width="782" alt="スクリーンショット 2019-11-20 16.11.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266022/500b2c34-2e67-6f0c-af4d-ca8cff6d7fa7.png">

# 所感
作っといてアレですが、コードpush→コード配布＆テスト環境消滅まで11分弱かかるので、コードの修正を速やかに反映できているとは正直言い難いなぁという感じがあります……Docker使った方が簡単＆高速にやりたいことを実現出来る気がします（あたりまえ）
何らかの事情でDockerを使えない現場（あるのか？）であれば、この方法を採用する意味もあるのかもしれません。
それから今回作成したPlaybookは余り綺麗でないので、構成などもう少し整理したいです。。。
やっぱりまだまだ初心者なので、知見ある方のコメントツッコミなどお待ちしております。