Aimingインフラチームを支え(てくれてい)る技術と設計(3/3)

Aimingインフラチームを支え(てくれてい)る技術と設計(3/3)
この記事を読むのに使用する時間の目安 = 5〜10分 你好! Magandang hapon po! こんにちわ! インフラチームの 小熊 です。 前回 から大分時間が経ってしまいましたが、Aimingのインフラ事情をいくつか紹介したいと思います。

TL;DR

必要だと思える時にTDIでインフラ構築をするようにしています。 また、インフラのテストもCIで回すようにしています。 普段はGithubで、その結果とReview commentを見てmergeといった流れになります。 とくに新規性のある内容はありませんが、誰かの何かのヒントになればということで、Tipsなども少し載せます。

本題

TDI

TDI (Test driven infrastructure) = テスト駆動インフラストラクチャ を指します。 インフラの「品質管理, 事故防止, 安定したリリース管理」を行うため 定義したインフラが適切に動作するかをテストしながら構築していく手法になります。 その考え方自体は古くからあり、以下のようなサイトが参考になります 前回紹介したとおり、弊社ではServerspecを使用しています。 AnsibleやKubernetes など、もともとインフラを定義するもので、さらに重ねてインフラを定義してテストを行う必要はあるのか? という議論もあると思いますが、以下の条件の際は出来る限りテストを書くようにしています。
  • private関数, local変数 のような scopeが限定的なものは基本的にテストしないが、逆にscopeが広い場合
    • Ansibleは基本的にglobal scopeになります
  • roleなどの汎用性を高くしていて組み合わせて利用している上で、それぞれのroleで依存関係もある場合
  • 全てのエラーで停止するわけではなく、楽観的なエラーハンドリングが存在する場合
    • e.g,) Ansibleだと ignore_errors, failed_whenを使っている箇所がある
  • 複数のtoolや環境を組み合わせていて、統合的なテストがしたい場合
    • e.g,) Ansible on the Vagrant, Containers + VM Instances
他にも、重要な箇所で安心感を得るために書いた方が良いときは書くようにしていて、Jenkins等のCIでテストを回すようにしています。 pipelineで全ての環境変数による設定のテストを行い、それで問題を発見した場合はmerge前にfix commit を入れるようにしています。

Serverspec Tips

基本

以下を参考にroleで分けるようにしておくと扱いやすいです。 http://serverspec.org/advanced_tips.html#how-to-use-host-specific-properties また、一般的ではありますが環境変数で制御するようにすると汎用的に扱えて良いです。 例えば、Jenkins によるテストの場合はどうするとか、タイトルにより特別な設定があるといったこともテストができるようにした方が良いと思います。 Rakefile サンプルコード
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
require 'highline/import'
require 'json'

unless ENV['ANSIBLE']
ENV['TARGET_HOST_YML'] = ask("Enter servers.yml path (spec/env/servers.yml): ")\
  if ENV['JENKINS_SERVER_COOKIE'].nil?
end
ENV['TARGET_HOST_YML'] = 'spec/env/servers.yml'\
  if ENV['TARGET_HOST_YML'].nil? || ENV['TARGET_HOST_YML'].empty?

desc "Run serverspec to all hosts"
task :spec => 'serverspec:all'

namespace :serverspec do
  properties = YAML.load_file(ENV['TARGET_HOST_YML'])
  task :all => properties.keys.map {|key| 'serverspec:' + key.split('.')[0] }

  properties.keys.each do |key|
    msg_for_test="on Vagrant" if "#{key}" === "wordpress"
    desc "Run serverspec to #{key} #{msg_for_test}"

    RSpec::Core::RakeTask.new(key.to_sym) do |t|

      # 環境変数チェック #########

      # Setされているか検証したい環境変数
      verify_env_vars = %w[PJCODE TARGET_ENV SERVER_NAME]

      # verify_env_vars の環境変数 を使用していないServerspec roles = 環境変数Set有無の検証をしたくないrole
      no_verify_roles = %w[vuls] # このroleは上記の環境変数は使っていないから存在チェックもしない

      properties[key]['roles'].each do |role|
        no_verify_roles.each do |no_verify_role|
          if !role.include?(no_verify_role)
            verify_env_vars.each { |env_var| raise NameError, "環境変数 #{env_var} がセットされていません" if ENV[env_var].nil? }
          end
        end
      end

      ENV['TARGET_HOST'] = key
      t.pattern = 'spec/{' + properties[key]['roles'].join(',') + '}/*_spec.rb'
    end
  end

  properties.values.each do |role|
    json = JSON.load(role.to_json)
    ENV['SERVERSPEC_ROLES'] = json['roles'].to_s
  end
end

Serverspecで、あいまい(範囲)判定がしたいとき

厳密性が必要なく、あいまいな判定にしておきたいときもあると思います。 たとえば、サーバのSpecで自動的に設定される値をテストしたい場合 (e.g,  リソースが異なるサーバが数種類あるけど、計算上は全てこの範囲に収まっていれば問題ない。など) そのようなときは以下のようにすると判定ができます。
describe command("awk '\$1~/start_servers/{ print \$3}' /etc/php-fpm.d/#{ENV['PJCODE']}.conf") do
  its('stdout.to_i') { should be_within(14).of(20) }
end
この場合、14が差分になります。 20に対して、+-14 以内が許容される という意味になります。 上記の例ですと、 下が6 までok, 上が 34 まで ok となります。 mariadbのinnodb_buffer_pool_size を自動的に設定している場合も範囲で判定しています。 mariadb10のspec sample
# spec/mariadb_10/variables_spec.rb
require 'spec_helper'

describe command("awk '\$1~/innodb_buffer_pool_size/{ print \$3}' /etc/my.cnf") do
  its('stdout.to_i') { should_not be_within(99.9).of(100) }
end
上記の例は、should_not判定なので 俺の innodb_buffer_pool_size が 0.1 から199.9 の範囲に収まるわけはない もっと多く割当たるはずだ という判定になります。 ちなみに上記で、 innodb_buffer_pool_size         = 2000MB など、MBなどの単位が入っていても問題なく判定できます。 https://ja.stackoverflow.com/questions/23703/serverspec-%E3%81%A7-%E7%AF%84%E5%9B%B2%E6%8C%87%E5%AE%9A%E3%82%92%E3%81%97%E3%81%9F%E3%81%84-rspec-%E3%81%A7%E3%81%84%E3%81%86-be-within-of-%E3%82%84-start-with

その他のテストツール: infrataster

インフラ面から見てエンドツーエンドのテストを簡単に行えるように出来るツールです。 CapybaraやSelenium のようなインフラ用のツールです。 deploy後のチェックに使ったりします。 こちらも割と多くの記事が出ていますので 細かい説明や手順は載せません。 この記事は、要所だけということで。

infratasterの設定について

通常は以下のように設定します。
Infrataster::Server.define(
  # Name of the server, this will be used in the spec files.
  :proxy,
  # IP address of the server
  '192.168.0.0/16',
  # If the server is provided by vagrant and this option is true,
  # SSH configuration to connect to this server is got from `vagrant ssh-config` command automatically.
  vagrant: true,
)
これを汎用的に使いたい場合は、同じく環境変数でハンドリングするようにしますが、設定情報はyamlでまとめておいた方が楽なので、以例えば、以下の様に環境変数 SERVER_NAME にSETするようにして、該当するYAML内の残りの設定を読み込ませるようにすると良いと思います。 spec/spec_helper.rb の例
# spec/spec_helper.rb
require 'infrataster/rspec'
require 'highline/import'
require 'yaml'

raise "Error: 環境変数 SERVER_NAME が入っていません\n\
`コマンドラインからecho $SERVER_NAME` で確認の上、`export SERVER_NAME=xxx を実行してください`\n\
e.g.) export SERVER_NAME=wordpress  # for vagrant"\
  if ENV['SERVER_NAME'].nil? || ENV['SERVER_NAME'].empty?

# Jenkinsのテストでは対話式にしない
ENV['TARGET_HOST_YML'] = ask("Enter servers.yml path (spec/env/servers.yml): ")\
  if ENV['JENKINS_SERVER_COOKIE'].nil?

ENV['TARGET_HOST_YML'] = 'spec/env/servers.yml'\
  if ENV['TARGET_HOST_YML'].nil? || ENV['TARGET_HOST_YML'].empty?

@properties = YAML.load_file(ENV['TARGET_HOST_YML'])

Infrataster::Server.define(:"#{ENV['SERVER_NAME']}") do |server|
  server.address = @properties[ENV['SERVER_NAME']]['address']
  server.vagrant = @properties[ENV['SERVER_NAME']]['vagrant']
end
spec/env/servers.yml の例
# For infrataster

example.com:
  address: '192.168.1.1/24'
  vagrant: false

# { Vagrant
wordpress:
  address: 'wordpress'
  vagrant: true

# } End of Vagrant
例えばこれで、 SERVER_NAME=wordpress bundle exec rspec など。 こうしておくことで汎用的かつ、特定の対象をテストしやすくなります。 もちろんspec側でもその環境変数は有効に利用できます。
require 'spec_helper'

_uri = "http://#{ENV['SERVER_NAME']}/"

describe server(:"#{ENV['SERVER_NAME']}")do
  describe http(_uri) do
    it "reponse status" do
      expect(response.status).to eq(301)
    end

    it "responds with content inflated automatically" do
      expect(response.headers['content-encoding']).to be_nil
    end
  end

  describe capybara(_uri) do
    it 'shows welcome page' do
      visit '/'
      expect(page).to have_content 'WordPress へようこそ'
    end

    it 'shows sample page' do
      visit '/?page_id=2'
      expect(page).to have_content '新しく WordPress ユーザーになった方は、ダッシュボードへ行ってこのページを削除し、独自のコンテンツを含む新しいページ
  作成してください。'
    end

    it 'shows unclassified page from sample page' do
      visit '/'
      click_on '未分類'
      expect(page).to have_content 'カテゴリー: 未分類'
    end
  end
end
各ツール共通で使える値は、共通で扱える環境変数名にしておくとintegrationしやすいです。 e.g,) export SERVER_NAME=”wordpress” docker, k8s, ansible, serverspec, infrataster でも同じ環境変数を利用

Security check

vuls や専用ツールで定期的にチェックを入れています。

Kubernetes

最近、弊社でもKubernetesを扱うようになってきました。 Kubernetes-helmで YAML生成するのも良いと思うのですが、以下のようなsnippetツールを見つけたので、紹介させていただきます。 https://github.com/ipedrazas/kubernetes-snippets 少しは楽できるかな?と思い、使い始めたところです。

さいごに

誰かにとって少しでもお役に立てれば幸いです。 Aimingではインフラエンジニアも募集しております。 ご興味のある方がいましたら、ぜひご応募ください。