ようへいの日々精進XP

よかろうもん

もう 2018 年だけど, Jenkins + Docker + itamae + Serverspec でインフラ CI っぽいことをやってるのでメモ

やってること

今更かもしれないけど, ギョームで以下のようなことをやって, ここ数日で運用も回ってきた気がするのでメモしておく.

  1. itamae のレシピを amazonlinux コンテナに適用
  2. 適用したコンテナに対して Serverspec でテスト
  3. 1 と 2 を Jenkins のビルドパイプラインで流す
  4. 3 が正常に終了したら, pull request を作成, 又はマージ
  5. master ブランチのレシピをサーバーに手動で適用

Jenkins のポテンシャルと itamae と Serverspec の使いやすさに改めて感動した次第.

メモ

Jenkins を動かす環境

  • Amazon Linux AMI release 2017.09
  • Docker Server / Client 17.12.0-ce
  • OpenJDK 1.8.0_161

Docker イメージ

  • amazonlinux
  • 素の amazonlinux イメージだと色々と辛かったので, 以下のような Dockerfile を用意してイメージを作成

以下, Dockerfile.

FROM amazonlinux
RUN yum install -y sudo util-linux which diffutils
RUN useradd ec2-user
RUN echo 'ec2-user ALL = NOPASSWD: ALL' > /etc/sudoers.d/ec2-user
ADD clock /etc/sysconfig/clock
ADD cloud.cfg /etc/cloud/cloud.cfg

以下, clock と cloud.cfg の中身.

# clock
ZONE="UTC"
UTC=true

cloud.cfg を直接編集するのは良くないらしい.

# cloud.cfg
# WARNING: Modifications to this file may be overridden by files in
# /etc/cloud/cloud.cfg.d

# If this is set, 'root' will not be able to ssh in and they
# will get a message to login instead as the default user (ec2-user)
disable_root: true

# This will cause the set+update hostname module to not operate (if true)
preserve_hostname: true

datasource_list: [ Ec2, None ]

repo_upgrade: security
repo_upgrade_exclude:
 - kernel
 - nvidia*
 - cudatoolkit

mounts:
 - [ ephemeral0, /media/ephemeral0 ]
 - [ swap, none, swap, sw, "0", "0" ]
# vim:syntax=yaml

clock と cloud.cfg は, itamae の中でこれらを書き換える為のレシピを用意する為, 事前に書き換えられる前のファイルを用意した.

itamae レシピ

以下のようなレシピを用意した.

# cookbooks/common/default.rb

# Package install
%w(wget
   git).each do |pkg|
  package pkg
end

# Edit /etc/sysconfig/clock
file '/etc/sysconfig/clock' do
  action :edit
  block do |content|
    content.gsub!('ZONE="UTC"', 'ZONE="Asia/Tokyo"')
    content.gsub!('UTC=true', 'UTC=false"')
  end
end

# Set LocaleTime
locale_file_exists = 'ls /usr/share/zoneinfo/Japan'
execute 'Set LocalTime' do
  command 'ln -nfs /usr/share/zoneinfo/Japan /etc/localtime'
  only_if locale_file_exists
end

# Disable Package Update and Set Locale Language
remote_file '/etc/cloud/cloud.cfg.d/99_customize.cfg' do
  action :create
  mode '0644'
  owner 'root'
  group 'root'
  source 'files/etc/cloud/cloud.cfg.d/99_customize.cfg'
end

itamae のディレクトリ構造は以下の通り.

$ tree . -L 2
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── cookbooks
│   └── common
├── docker
│   ├── Dockerfile
│   ├── clock
│   └── cloud.cfg
├── node.json
├── roles
│   └── docker-test.rb
└── vendor
    └── bundle

6 directories, 8 files

Serverspec テスト

以下のようなテストを用意.

require "spec_helper"

%w(wget
   git).each do |pkg|
  describe package(pkg) do
    it { should be_installed }
  end
end

describe file('/etc/sysconfig/clock') do
  it { should exist }
  its(:content) { should match /ZONE="Asia\/Tokyo"/ }
  its(:content) { should match /UTC=false/ }
end

describe file('/etc/localtime') do
  it { should be_symlink }
end

describe file('/etc/cloud/cloud.cfg.d/99_customize.cfg') do
  it { should exist }
  it { should be_mode 644 }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
end

describe command('date') do
  its(:stdout) { should contain('JST') }
end

describe command('locale') do
  its(:stdout) { should contain('ja_JP.UTF-8') }
end

spec_helper.rb にちょっとひと手間加えて, 環境変数に Docker イメージを指定して rspec が走るようにしておく.

require 'serverspec'
require 'net/ssh'
require 'json'

properties = YAML.load_file('properties.yml')

if ENV['DOCKER_IMAGE']
  begin
    require 'docker'
  rescue LoadError
    fail "docker-api is not available. Try installing it."
  end
  set :backend, :docker
  set :docker_image, ENV['DOCKER_IMAGE']
else
... 省略 ...

end

ディレクトリ構造は以下の通り.

$ tree . -L 2
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── properties.yml
├── spec
│   ├── common
│   └── spec_helper.rb
└── vendor
    └── bundle

4 directories, 5 files

Jenkins の準備

プラグイン

以下のプラグインを利用した.

  • rbenv plugin
  • Parameterized Trigger plugin
  • Build Pipeline Plugin

Build Pipeline Plugin は必須では無い.

Docker 周り

jenkins ユーザーで docker コマンド叩けるようにしておく.

sudo gpasswd -a jenkins docker

こんな感じ.

$ sudo -u jenkins docker version
Client:
 Version:       17.12.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    3dfb8343b139d6342acfd9975d7f1068b5b1c3d3
 Built: Mon Mar  5 20:42:27 2018
 OS/Arch:       linux/amd64

Server:
 Engine:
  Version:      17.12.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.9.2
  Git commit:   402dd4a/17.12.0-ce
  Built:        Mon Mar  5 20:43:34 2018
  OS/Arch:      linux/amd64
  Experimental: false

ジョブ

以下のようにジョブを 2 つ用意した. (ジョブ名はなんでもいい)

  • infra-itamae
  • infra-serverspec

個々のジョブで共通して以下の内容を設定した.

  • ソースコード管理 (今回も Backlog Git を利用)
  • rbenv build wrapper で Ruby 2.5.0 を利用するようにした

以下のように infra-itamae が成功したら, infra-serverspec が実行されるように設定した.

infra-itamae の設定

[ビルド] の [シェル実行] に以下のようにシェルスクリプトを書いた.

cd itamae.sandbox
bundle install --path vendor/bundle
cd docker
docker build -t itamae-test .
cd ../
bundle exec itamae docker --image=itamae-test roles/docker-test.rb --log-level=info --no-color 2>&1 | tee test.log
if [ "$(cat test.log | tail -1 | awk -F":" '{print $4}')" != "" ];then
  echo -n "DOCKER_IMAGE=$(cat test.log | tail -1 | awk -F":" '{print $4}')" > ${WORKSPACE}/docker_image.txt
else
  exit 1
fi

itamae は Docker コンテナに対してプロビジョニングして, コンテナイメージを作成することが出来る.

$ bundle exec itamae --help docker
Usage:
  itamae docker RECIPE [RECIPE...]

Options:
      [--recipe-graph=PATH]                            # [EXPERIMENTAL] Write recipe dependency graph in DOT
  -j, [--node-json=NODE_JSON]
  -y, [--node-yaml=NODE_YAML]
  -n, [--dry-run], [--no-dry-run]
      [--shell=SHELL]
                                                       # Default: /bin/sh
      [--login-shell], [--no-login-shell]
      [--ohai], [--no-ohai]                            # This option is DEPRECATED and will be unavailable.
      [--profile=PATH]                                 # [EXPERIMENTAL] Save profiling data
      [--detailed-exitcode], [--no-detailed-exitcode]  # exit code 0 - The run succeeded with no changes or failures, exit code 1 - The run failed, exit code 2 - The run succeeded, and some resources were changed
  -l, [--log-level=LOG_LEVEL]
                                                       # Default: info
      [--color], [--no-color]
                                                       # Default: true
  -c, [--config=CONFIG]
      [--image=IMAGE]                                  # This option or 'container' option is required.
      [--container=CONTAINER]                          # This option or 'image' option is required.
      [--tls-verify-peer], [--no-tls-verify-peer]
                                                       # Default: true
      [--tag=TAG]

Create Docker image

[ビルド後の処理] で [Trigger parameterize build on other projects] を指定し, 以下のパラメータを設定した.

パラメータ名 概要
Projects to build infra-serverspec 下流プロジェクトを指定
Trigger when build is Stable 下流プロジェクトがビルドされる基準
Perameters from properties file 選択 ファイルからパラメータを指定する
Use properties from file docker_image.txt ${WORKSPACE} 以下に生成されるファイル名を指定

Use properties from file で指定するファイルは以下のようなフォーマットである必要がある.

KEY=value

今回だと, 下流プロジェクトである infra-serverspec で利用する Docker イメージ名を環境変数に指定しておきたいので, 以下のような内容になる.

DOCKER_IMAGE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

尚, itamae 実行時に --no-color オプションを付与しておかないと, ログ出力にカラーコードが含まれてしまい, 意図しない環境変数が定義されるので注意する.

infra-serverspec の設定

[General] の [ビルドのパラメータ化] にチェックを入れて, 以下のように設定した.

  • テキストの [名前] には DOCKER_IMAGE を指定

[ビルド]の [シェル実行] に以下のようにシェルスクリプトを書いた.

cd serverspec.sandbox
bundle install --path vendor/bundle
bundle exec rake serverspec:docker-test && docker rmi -f ${DOCKER_IMAGE}

動いてる図

Build Pipeline

f:id:inokara:20180321153022p:plain

ログ

以下, infra-itamae の実行ログ (コンソール出力).

Started by user xxxxxxx
Building in workspace /var/lib/jenkins/workspace/infra-itamae
 > git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository

...

 INFO : Image created: sha256:fc7d8989c992a4035da943f19ff13758d659dc2c66e85b354a81ed35eac092a9
++ cat test.log
++ tail -1
++ awk -F: '{print $4}'
+ '[' fc7d8989c992a4035da943f19ff13758d659dc2c66e85b354a81ed35eac092a9 '!=' '' ']'
++ cat test.log
++ tail -1
++ awk -F: '{print $4}'
+ echo -n DOCKER_IMAGE=fc7d8989c992a4035da943f19ff13758d659dc2c66e85b354a81ed35eac092a9
Triggering a new build of infra-serverspec
Finished: SUCCESS

以下, infra-serverspec の実行ログ (コンソール出力).

Started by upstream project "infra-itamae" build number 3
originally caused by:
 Started by user xxxxxxxx
Building in workspace /var/lib/jenkins/workspace/infra-serverspec
 > git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository

...

Command "date"
  stdout
    should contain "JST"

Command "locale"
  stdout
    should contain "ja_JP.UTF-8"

Finished in 1.39 seconds (files took 0.36337 seconds to load)
12 examples, 0 failures

+ docker rmi -f 21f1cc87aee8a670710128fc65699f8554de4b38027c355dc39048a026850819
Deleted: sha256:21f1cc87aee8a670710128fc65699f8554de4b38027c355dc39048a026850819
Deleted: sha256:6605e21eb4b68cca7fa9c95da3f4f927f40988798c038ed7f42fc68caabb5f84
Deleted: sha256:1e12645118a9dbb47e9963ee37669022e91bb0a07c2a5757f30aaf88e83fde46
Deleted: sha256:2e643788655c8c760d1e980eca0b46251b7301af1b759b5df5ab7bbe642b0c9d
Deleted: sha256:00f5d50a957784ab20fd7b550fdfeca505dc4e550959ba7f73f2def824041d44
Deleted: sha256:e0f2d8168d9eaa1ed9592791b8a4e436b5476db41b99fa12bf32076891b9fd22
Deleted: sha256:b7937fd11c3afb70b655e281ef6acdc4d3afc3cd912a19fe14cfb3b93c0e7688
Deleted: sha256:9989831a58db4d3c31f5f18d590c5ffc08ad41ac4f424b3ea870fded4440e591
Deleted: sha256:4ac2d011c778bf4d55459f2549bb80bc89b3327e747384446c1f08652997bbfb
Deleted: sha256:7522d03ea4bb3188185376f4aa675724bc8c46e961fa0625dc73b164408b5736
Deleted: sha256:f0a3ff27c8c1d0e4242beb150786fae71a8b52a328b3810581c0a50d9101c89d
Deleted: sha256:95f9b360e9435265a8e1888110af30e93a90e563fe969c8249f14d446791c996
Deleted: sha256:be94791a1fc4eb636d443b0fb18b56eed7c32b0cd3a2b3912b10b7945d840179
Deleted: sha256:fff1c0462adfdaa1e8c815e4fc17c228b205479a23161cd717342dad5d9345b2
Finished: SUCCESS

テストが通った時だけ, コンテナイメージを削除しちゃう.

以上

ざっくりとしたメモでした.