tkak's tech blog

This is my technological memo.

2023年振り返りと2024年抱負。

約4年ぶりにブログの更新。2023年の正月に抱負を書きかけて、早一年。そのままほったらかししていたブログの下書きを、まさか2024年正月に更新するとは。今年こそ雑に書いて公開しよう。

2023年振り返り

仕事

2023年の仕事について振り返ってみると、パブリッククラウド関連の仕事をする機会が以前よりぐんと増え、所属するチームも変わり、エンジニアリングマネージャーのロールも任されたので中々変化の年だったと思う。

今までは、基本的にオンプレミス環境を使ってシステムを構築運用する開発チームの技術サポートが中心だったけど、社内でのCloud CoE(CCoE)立ち上げに伴ってパブリッククラウドがメインになった。AWS、Azure、Google Cloud、それぞれのクラウドを触る機会があってクラウドエンジニアを目指す環境としてはとても良いと思う。反面、守備範囲が広くなってどうしても時間の制約上知識が広く浅くになってしまって少し寂しい。自分で手を動かして検証や開発などやっていきたい気持ちはあるものの、その辺はチームメンバーに任せてしまって、自分はそれ以外の仕事を意識するのが多かったように思う。

2024年は自分でも少し手を動かして検証とか開発することを意識しつつも、チームとしてのアウトプットを最大化できるようにしよう。CCoEとしては2023年は立ち上げフェーズということもあり、目に見えるアウトプットがなかったので、2024年は結果を出せるように頑張りたい。

 

買ってよかったもの

夜な夜なエールのクラフトビール定期便

クラフトビールが好きなので試しに夜な夜なエールのサブスクをやってみた。毎月好きなクラフトビールが届くのでかなりよかった。家に帰ると冷蔵庫に色んなクラフトビールがある状況が素晴らしかった。

yonasato.com

 

ランニングシューズ

2023年11月にハーフマラソンを走ることにしたので、ランニングシューズを新調した。反発力とクッションがいい感じ。今まで履いていたシューズだと膝が痛くなることがあったけど、今は全然そういう痛みがなく走れるようになった。ランニングシューズでこんなに走り方変わるとは思ってなかった。

 

サコッシュ

夏に会社の先輩達と1人目の息子(小学一年生)を連れてシロギス釣りに行った。ちょっとした小物を入れるのに便利そうということで、サコッシュを買ってみた。釣りでも活躍し、今では、近くの公園行く時とかに重宝してる。リュックまではいかないんだけど、財布とか何も持ってかないと寂しい時にちょうどいい。

ちなみにシロギス釣りは親子共々初めての船釣りで心配してたけど、息子も楽しんでくれていたのでよかった。2024年もまたチャレンジしたい。

 

映画

息子達が成長して長時間座ることができるようになったので、2023年は割と映画館で映画を観ることができた。

2024年抱負

家内安全、健康第一。

…は、もちろんだけど、それに加えて今年は、アウトプットを増やすことをより意識して日々過ごせるといいかなと思う。

サボりまくってるブログの更新だったり、クラウド認定資格を取ったりと色々チャレンジしたい。

今年も良い年になりますように。

pack (Cloud Native Buildpacks) で Gradle マルチプロジェクトをビルドする

最近 Cloud Native Buildpacks を触って遊んでみたのでTipsを残しておきます。約4年ぶりくらいのブログ更新です😅

Cloud Native Buildpacksとは?

Dockerfileを書かなくても、アプリケーションのソースコードからどんな言語のランタイムかをよしなに判断してコンテナイメージを作成してくれるものです。開発者はDockerfileを頑張って書いたりメンテナンスする作業から解放され、運用者は各アプリケーションチームのDockerfileをいちいちチェックしなくても本番運用に耐えられるコンテナイメージを管理・提供することができます。ざっくり言うとKubernetesなどをベースとしたコンテナプラットフォーム環境で、HerokuやCloud FoundryなどのPaaSに近いことが可能になります。Cloud Native Buildpacks は、CNCFのsandboxプロジェクトになっています。 buildpacks.io

また、Cloud Native Buildpacks については色々な方がスライドや動画をアップしていて、この辺り参考になりました。

Gradle マルチプロジェクトとは?

Gradleは複数のプロジェクトをまとめて管理することができます。それをGradle multi project buildと呼んでいたりします。共通系の処理をまとめたり、各サービスからそれを呼び出したりすることができます。ディレクトリレイアウトは例えばこんな感じになります。

.
├── api
│   └── src
│       ├── main
│       │   └── java
│       │       └── org
│       │           └── gradle
│       │               └── sample
│       │                   ├── api
│       │                   │   └── Person.java
│       │                   └── apiImpl
│       │                       └── PersonImpl.java
│       └── test
│           └── java
│               └── org
│                   └── gradle
│                       └── PersonTest.java
├── build.gradle
├── services
│   └── personService
│       └── src
│           ├── main
│           │   └── java
│           │       └── org
│           │           └── gradle
│           │               └── sample
│           │                   └── services
│           │                       └── PersonService.java
│           └── test
│               └── java
│                   └── org
│                       └── gradle
│                           └── sample
│                               └── services
│                                   └── PersonServiceTest.java
├── settings.gradle
└── shared
    └── src
        └── main
            └── java
                └── org
                    └── gradle
                        └── sample
                            └── shared
                                └── Helper.java

docs.gradle.org

pack コマンド

packコマンドは、Cloud Native BuildpacksのCLIツールになります。

$ pack build foo-app --builder cnbs/sample-builder:bionic

こんな感じでpack buildサブコマンドでコンテナイメージを作成することができます。

しかし、Gradle マルチプロジェクトでpack buildをそのまま使うとエラーになります。

$ pack build greeter --builder=gcr.io/paketo-buildpacks/builder:base
base: Pulling from paketo-buildpacks/builder
Digest: sha256:3284c03370a31854fee91c71c037081406ce2d69b5b7e3926a6a9e134f7e0d2f
Status: Image is up to date for gcr.io/paketo-buildpacks/builder:base
base-cnb: Pulling from paketo-buildpacks/run
f08d8e2a3ba1: Already exists
3baa9cb2483b: Already exists
94e5ff4c0b15: Already exists
1860925334f9: Already exists
e45cbcfb1314: Already exists
01090515ce55: Already exists
8a22ab72e32b: Pull complete
Digest: sha256:86edad85f315d115ca1784c4a72abbde0b12650c9b993be95fd4a7bcc8900f70
Status: Downloaded newer image for gcr.io/paketo-buildpacks/run:base-cnb
0.9.1: Pulling from buildpacksio/lifecycle
Digest: sha256:53bf0e18a734e0c4071aa39b950ed8841f82936e53fb2a0df56c6aa07f9c5023
Status: Image is up to date for buildpacksio/lifecycle:0.9.1
===> DETECTING
[detector] 6 of 17 buildpacks participating
[detector] paketo-buildpacks/bellsoft-liberica 3.2.0
[detector] paketo-buildpacks/gradle            3.1.0
[detector] paketo-buildpacks/executable-jar    3.1.0
[detector] paketo-buildpacks/apache-tomcat     2.2.0
[detector] paketo-buildpacks/dist-zip          2.2.0
[detector] paketo-buildpacks/spring-boot       3.2.0
===> ANALYZING
[analyzer] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jvmkill" from app image
[analyzer] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:helper" from app image
[analyzer] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:java-security-properties" from app image
[analyzer] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jre" from app image
[analyzer] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jdk" from cache
[analyzer] Restoring metadata for "paketo-buildpacks/gradle:application" from cache
[analyzer] Restoring metadata for "paketo-buildpacks/gradle:cache" from cache
===> RESTORING
[restorer] Restoring data for "paketo-buildpacks/bellsoft-liberica:jdk" from cache
[restorer] Restoring data for "paketo-buildpacks/gradle:application" from cache
[restorer] Restoring data for "paketo-buildpacks/gradle:cache" from cache
===> BUILDING
[builder]
[builder] Paketo BellSoft Liberica Buildpack 3.2.0
[builder]   https://github.com/paketo-buildpacks/bellsoft-liberica
[builder]   Build Configuration:
[builder]     $BP_JVM_VERSION              11.*            the Java version
[builder]   Launch Configuration:
[builder]     $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
[builder]     $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
[builder]     $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
[builder]     $JAVA_TOOL_OPTIONS                           the JVM launch flags
[builder]   BellSoft Liberica JDK 11.0.8: Reusing cached layer
[builder]   BellSoft Liberica JRE 11.0.8: Reusing cached layer
[builder]   Launch Helper: Reusing cached layer
[builder]   JVMKill Agent 1.16.0: Reusing cached layer
[builder]   Java Security Properties: Reusing cached layer
[builder]
[builder] Paketo Gradle Buildpack 3.1.0
[builder]   https://github.com/paketo-buildpacks/gradle
[builder]   Build Configuration:
[builder]     $BP_GRADLE_BUILD_ARGUMENTS  --no-daemon -x test build  the arguments to pass to Maven
[builder]     $BP_GRADLE_BUILT_ARTIFACT   build/libs/*.[jw]ar        the built application artifact explicitly.  Supersedes $BP_GRADLE_BUILT_MODULE
[builder]     $BP_GRADLE_BUILT_MODULE                                the module to find application artifact in
[builder]     Creating cache directory /home/cnb/.gradle
[builder]   Compiled Application: Contributing to layer
[builder]     Executing gradlew --no-daemon -x test build
[builder] To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: https://docs.gradle.org/6.5.1/userguide/gradle_daemon.html.
[builder] Daemon will be stopped at the end of the build stopping after processing
[builder] > Task :greeting-library:compileJava NO-SOURCE
[builder] > Task :greeting-library:compileGroovy
[builder] > Task :greeting-library:processResources NO-SOURCE
[builder] > Task :greeting-library:classes
[builder] > Task :greeting-library:jar
[builder] > Task :greeter:compileJava
[builder] > Task :greeter:compileGroovy NO-SOURCE
[builder] > Task :greeter:processResources NO-SOURCE
[builder] > Task :greeter:classes
[builder] > Task :greeter:jar
[builder] > Task :greeter:startScripts
[builder] > Task :greeter:distTar
[builder] > Task :greeter:distZip
[builder] > Task :greeter:assemble
[builder] > Task :greeter:check
[builder] > Task :greeter:build
[builder] > Task :greeting-library:assemble
[builder] > Task :greeting-library:check
[builder] > Task :greeting-library:build
[builder]
[builder] Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
[builder] Use '--warning-mode all' to show the individual deprecation warnings.
[builder] See https://docs.gradle.org/6.5.1/userguide/command_line_interface.html#sec:command_line_warnings
[builder]
[builder] BUILD SUCCESSFUL in 48s
[builder] 7 actionable tasks: 7 executed
[builder] unable to invoke layer creator
[builder] unable to contribute application layer
[builder] unable to resolve artifact
[builder] unable to find single built artifact in build/libs/*.[jw]ar, candidates: []
[builder] ERROR: failed to build: exit status 1
ERROR: failed to build: executing lifecycle. This may be the result of using an untrusted builder: failed with status code: 145

エラーを見ると、ビルドされたjarが見つからないのが原因のようです。packコマンド内で https://github.com/paketo-buildpacks/gradle が使われているようなのでGitHubを見に行くと、BP_GRADLE_BUILT_ARTIFACTで、artifactのパスがセットされていて、デフォルトだとbuild/libs/*.[jw]arになるようです。

github.com

この環境変数を適切なパスに変更することでうまくいきそうですね。ただし、ターミナルに環境変数を渡すだけではうまくセットされなくて、pack buildコマンドの--envオプションを使う必要がありました。

$ pack build greeter --env "BP_GRADLE_BUILT_ARTIFACT=greeter/build/libs/*.[jw]ar" \
    --builder=gcr.io/paketo-buildpacks/builder:base

# ...

[exporter] *** Images (af8ccc778f29):
[exporter]       index.docker.io/library/greeter:latest
[exporter] Reusing cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
[exporter] Adding cache layer 'paketo-buildpacks/gradle:application'
[exporter] Adding cache layer 'paketo-buildpacks/gradle:cache'
Successfully built image greeter

無事、ビルドに成功しました!✌️イメージもちゃんとできてますね。

$ docker images | grep greeter
greeter                                                                         latest                   af8ccc778f29        40 years ago        235MB

今回試した内容は GitHub に上げました。

github.com

InSpecではじめるテスト駆動インフラ

最近、新しくChefのCookbookを書く機会があったので、前から気になっていたInSpecを少し触ってみました。

github.com

InSpecとは何か?

InSpecは、Chef社が開発しているオープンソースのサーバーテストフレームワークです。サーバーのテストフレームワークといえば、Serverspecが有名ですが、InSpecはインフラ管理向けというよりコンプライアンスの担保だったりセキュリティ要件を満たしているかどうかのテスト用作られたツールになっています。InSpecのGithubには、

InSpec is inspired by the wonderful Serverspec project. Kudos to mizzy and all contributors!

と記載があるように、Serverspecにインスパイアされているようです。なので、基本的にServerspecで提供されている代表的なリソースはInSpecでも大体用意されていて、今の所(バージョン1.3.0)以下のようなリソースをサポートしています。

apache_conf apt audit_policy auditd_conf auditd_rules bash bond bridge bsd_service command csv directory etc_group etc_passwd etc_shadow file gem group grub_conf host iis_site inetd_conf ini interface iptables json kernel_module kernel_parameter launchd_service limits_conf login_def mount mysql_conf mysql_session npm ntp_conf oneget os os_env package parse_config parse_config_file pip port postgres_conf postgres_session powershell process registry_key runit_service security_policy service ssh_config sshd_config ssl sys_info systemd_service sysv_service upstart_service user users vbscript windows_feature wmi xinetd_conf yaml yum

例えば、指定したパッケージが適切にインストールされているかチェックしたい場合、packageリソースを使って以下のように書きます。InSpecでもServerspecでも大体似たような感じで書くことができ、バージョンチェックのところ以外は全く同じです。

## Serverspec
describe package('nginx') do
  it { should be_installed.with_version('1.9.5') }
end

## InSpec
describe package('nginx') do
  it { should be_installed }
  its('version') { should eq 1.9.5 }
end

ディレクトリとファイルのテストもほとんど一緒で、modeの箇所以外はそのままInSpecでも動かせます。

## Serverspec
describe file('/usr/local/foo') do
  it { should be_directory }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
  it { should be_mode '755' }
end

describe file('/usr/local/foo/bar') do
  it { should be_file }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
  it { should be_mode '644' }
  its('content') { should match('Hello') }
end

## InSpec
describe file('/usr/local/foo') do
  it { should be_directory }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
  its('mode') { should cmp '0755' }
end

describe file('/usr/local/foo/bar') do
  it { should be_file }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
  its('mode') { should cmp '0644' }
  its('content') { should match('Hello') }
end

ちなみに、よりInSpec風に書くのであれば、以下のようにテストを書くこともできます。

describe directory('/usr/local/foo') do
  its('owner') { should cmp 'root' }
  its('group') { should cmp 'root' }
  its('mode') { should cmp '755' }
end

describe file('/usr/local/foo/bar') do
  its('owner') { should cmp 'root' }
  its('group') { should cmp 'root' }
  its('mode') { should cmp '644' }
  its('content') { should match('Hello') }
end

機能的にServerspecもInSpecもほとんど大差ない感じなんですが、強いて違う点を挙げるとすれば、InSpecは、Serverspec に比べ、よりRubyRSpec、Rakeを意識する必要がない点かなと思います。というのも、InSpecは、spec_helper.rbやRakefileを準備する必要がありません。以下のようなCLIが用意されていて、とりあえずテストケースだけ書いてコマンドを実行すれば、すぐ使うことができます。

# run test locally
inspec exec test.rb

# run test on remote host on SSH
inspec exec test.rb -t ssh://user@hostname -i /path/to/key

# run test on remote windows host on WinRM
inspec exec test.rb -t winrm://Administrator@windowshost --password 'your-password'

# run test on docker container
inspec exec test.rb -t docker://container_id

test-kitchenでInSpecを使ってみる

では、実際にtest-kitchenと一緒に使ってみます。

準備

現時点での最新版のChefDK(バージョン0.19.6)には、test-kichenもInSpec (kitchen-inspec)もすでに含まれてるので、ChefDKをインストールするだけで使えるようになります。Macであれば以下のコマンドだけで環境の準備ができます。(virtualboxVagrantは別途必要)

$ brew cask install chefdk

chef generate cookbookコマンドで適当にcookbookを作って、recipeとテストを用意します。今回触ってみた環境をgithubにあげてますので、よかったら参考にしてください。

https://github.com/tkak/hello-cookbook

git cloneしてきて、kitchen listを実行すると以下のテスト対象を確認できます。

$ chef exec kitchen list
Instance                     Driver   Provisioner  Verifier  Transport  Last Action
with-serverspec-ubuntu-1604  Vagrant  ChefZero     Busser    Ssh        <Not Created>
with-serverspec-centos-72    Vagrant  ChefZero     Busser    Ssh        <Not Created>
with-inspec-ubuntu-1604      Vagrant  ChefZero     Inspec    Ssh        <Not Created>
with-inspec-centos-72        Vagrant  ChefZero     Inspec    Ssh        <Not Created>
with-shell-ubuntu-1604       Vagrant  ChefZero     Shell     Ssh        <Not Created>
with-shell-centos-72         Vagrant  ChefZero     Shell     Ssh        <Not Created>

Serverspecでテスト

まずは、通常の使い方であるServerspec (busser-serverspec)でテストを実行してみます。

$ chef exec kitchen test serverspec
...
...
...
-----> Running serverspec test suite
-----> Installing Serverspec..
Fetching: diff-lcs-1.2.5.gem (100%)
Fetching: rspec-expectations-3.5.0.gem (100%)
Fetching: rspec-mocks-3.5.0.gem (100%)
Fetching: rspec-3.5.0.gem (100%)
Fetching: rspec-its-1.2.0.gem (100%)
Fetching: multi_json-1.12.1.gem (100%)
Fetching: net-ssh-3.2.0.gem (100%)
Fetching: net-scp-1.2.1.gem (100%)
Fetching: net-telnet-0.1.1.gem (100%)
Fetching: sfl-2.3.gem (100%)
Fetching: specinfra-2.64.0.gem (100%)
Fetching: serverspec-2.37.2.gem (100%)
-----> serverspec installed (version 2.37.2)
       /opt/chef/embedded/bin/ruby -I/tmp/verifier/suites/serverspec -I/tmp/verifier/gems/gems/rspec-support-3.5.0/lib:/tmp/verifier/gems/gems/rspec-core-3.5.4/lib /opt/chef/embedded/bin/rspec --pattern /tmp/verifier/suites/serverspec/\*\*/\*_spec.rb --color --format documentation --default-path /tmp/verifier/suites/serverspec

       Package "zsh"
         should be installed

       File "/usr/local/foo"
         should be directory
         should be owned by "root"
         should be grouped into "root"
         should be mode "755"

       File "/usr/local/foo/bar"
         should be file
         should be owned by "root"
         should be grouped into "root"
         should be mode "644"
         content
           should match "Hello"

       Finished in 0.17573 seconds (files took 0.3651 seconds to load)
       10 examples, 0 failures

       Finished verifying <with-serverspec-centos-72> (0m5.60s).
...
...
...

kitchen testコマンドで、以下の処理が一気に動きます。

ちなみに、上記のログにあるように、busser-serverspec 経由でtest-kitchenを使うと、テスト初回時にインスタンス内でgemのインストールが走り、少し待たされます。ちょっとした待ち時間ですが、何度もインスタンスを作って壊してをやってると、この待ち時間がバカにできないので、そんな時はshell-verifierを使うのがオススメです。shell-verifierを使えば、事前にインストールされたホスト側のServerspecを使ってテストを実行するので、gemインストールの待ち時間なくテストができます。shell-verifierでServerspecを使う方法は以下のとおりです。

まず、ホストにServerspec をインストールしておきます。

$ chef gem install serverspec 

そして、.kitchen.ymlファイルに以下のような設定を追記し、

verifier:
  name: shell
  command: chef exec rspec -c -f documentation -I test/integration/${KITCHEN_SUITE}/serverspec --pattern test/integration/${KITCHEN_SUITE}/serverspec/**/*_spec.rb --pattern test/integration/helpers/serverspec/spec_helper.rb

spec_helper.rbを用意します。

require 'serverspec'

set :backend, :ssh

options = Net::SSH::Config.for(host)
options[:host_name] = ENV['KITCHEN_HOSTNAME']
options[:port] = ENV['KITCHEN_PORT']
options[:user] = ENV['KITCHEN_USERNAME']
options[:keys] = ENV['KITCHEN_SSH_KEY']

set :host,        options[:host_name]
set :ssh_options, options
set :env, :LANG => 'C', :LC_ALL => 'C'

これでbusser-serverspecを使うより短時間でテストが実行できるようになります。ただし、.kitchen.ymlにコマンドの記述とspec_helper.rbの用意をする必要があり、若干面倒です。例えば、複数のCookbookを管理する時とか。

InSpecでテスト

次にInSpecでテストを動かしてみます。使い方は簡単で、InSpecを使うには.kitchen.ymlに以下のようなveriferを追記するだけで使えるようになります。

verifier:
  name: inspec

デフォルトだとtest/integration/<test suites>/以下のディレクトリにテストを配置します。また、以下のようにテストの場所を変更したり、他のリポジトリにあるテストを使うことも可能です。

## local
verifier:
  name: inspec
  inspec_tests:
    - test/recipes

## github
verifier:
  name: inspec
  inspec_tests:
    - https://github.com/dev-sec/tests-ssh-hardening

kitchen testのログは以下のようになります。

$ chef exec kitchen test inspec
...
...
...
-----> Verifying <with-inspec-centos-72>...
       Detected alternative framework tests for `inspec`
       Use `/Users/takaaki.furukawa/src/github.com/tkak/hello-cookbook/test/integration/with-inspec/inspec` for testing

Target:  ssh://vagrant@127.0.0.1:2201


  System Package
     ✔  zsh should be installed
  File /usr/local/foo
     ✔  should be directory
     ✔  should be owned by "root"
     ✔  should be grouped into "root"
     ✔  mode should cmp == "0755"
  File /usr/local/foo/bar
     ✔  should be file
     ✔  should be owned by "root"
     ✔  should be grouped into "root"
     ✔  mode should cmp == "0644"
     ✔  content should match "Hello"

Test Summary: 10 successful, 0 failures, 0 skipped
       Finished verifying <with-inspec-centos-72> (0m1.13s).
...
...
...

特に問題なく動きます。しかも、busser-serverspec のようにgemインストールが走って待たされるということもなく、shell-verifierを使うときのようにspec_helper.rbを用意する必要もないので、なかなかいい感じです。

まとめ

test-kitchenでInSpecを試してみました。後継のツールだけあって思ってたよりシンプルで使いやすかったです。すでにServerspec で書いてあるテストコードは、Serverspec で十分テストをまわせているのですぐにInSpecに置き換えようとは思いませんが、新規で書くならInSpecも選択肢としてありだなと感じました。また、InSpec本来の使い方であるコンプライアンスチェックとしてChef Automateと組み合わせるといったことができるらしいのですが、今回はインフラのテストとしてしか試していないので、機会があればその辺も触ってみたいです。

参考

DevOps導入指南 Infrastructure as Codeでチーム開発・サービス運用を効率化する (DEV Engineer’s Books)

DevOps導入指南 Infrastructure as Codeでチーム開発・サービス運用を効率化する (DEV Engineer’s Books)

Infrastructure As Code: Managing Servers in the Cloud

Infrastructure As Code: Managing Servers in the Cloud

Serverspec

Serverspec

今更ですが、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試しました(Proxy環境下)

Dockerを使って開発していると、本番環境でもDockerを使ってみたいなぁという気持ちになります。ただ、本番環境でDockerを使うのは、個人的にまだまだハードルが高そうな印象です。マルチホスト間のネットワークはどうするか?分散システムのクラスタリングはどうするか?コンテナのスケジューリングは?監視は?ログは?デプロイの仕組みは?負荷が高い時の対応はどうするのか?セキュリティは?など、運用面で色々と考えなければなりません。さらにDocker界隈のツールは様々なツールが存在していてどれから手をつけていいのか選択に困ります。最初はなるべく小さく始めたいところです。なので、今回、本番環境を意識したDocker環境をより少ないツールで作れるかどうか試してみました。ひとまず、クラスタ組んで、マルチホストネットワーキング機能が試せるところまで。使ったツールは以下になります。

今回試した環境のterraformファイルは、githubにあげてますので、よかったら参考にどうぞ。

なお、プライベートな環境下で構築を試したのでProxyの設定をもろもろしていますが、参考にする場合は適当に読み替えていただければと思います。

TerraformによるCoreOSのデプロイとetcdの設定

Dockerを本番環境で使う場合、複数ホストのコンテナ間通信をどうするかというのは気になる問題ですが、いくつか方法が存在します。

Dockerのバージョン1.9からマルチホストネットワーク機能が正式に導入されたので、今回はそれを使ってみます。マルチホストネットワーク機能を使うためには、以下のような条件が必要です。今回、極力カーネルコンパイルとかはしたくなかったので、条件を満たすためにCoreOSを選択しました。

  • DockerホストOSのカーネルが3.16かそれ以降
  • Dockerがサポートしている分散KVS(Consul, Etcd, ZooKeeper)が利用できること

まず、CoreOSサーバ3台を作りetcdクラスタを組みます。Dockerのドキュメントではdocker-machineを使った手順なんですが、普段からTerraformでインフラ管理してるので今回Terraformを使います。Terraformの設定ファイルは以下になります。個々のパラメータは適当に読み替えてください。

## main.tf
provider "openstack" {
    domain_name = "default"
    tenant_name = "project-1"
    auth_url = "<keystone endpoint>"
}

variable "servers" {
    default = "test-1,test-2,test-3"
}

resource "null_resource" "discovery_url_template" {
    provisioner "local-exec" {
        command = "curl -s 'https://discovery.etcd.io/new?size=${length(split(",", var.servers))}' > discovery_url"
    }
}

resource "template_file" "discovery_url" {
    template = "discovery_url"
    depends_on = [
        "null_resource.discovery_url_template"
    ]
}

resource "template_file" "cloud-init" {
    count = "${length(split(",", var.servers))}"
    template = "${file("cloud-config.yml.tpl")}"

    vars {
        hostname = "${element(split(",", var.servers), count.index)}"
        discovery_url = "${template_file.discovery_url.rendered}"
    }
}

resource "openstack_compute_instance_v2" "coreos" {
    count = "${length(split(",", var.servers))}"
    name = "${element(split(",", var.servers), count.index)}"
    image_name = "coreos-stable-899-3-0"
    flavor_name = "flavor-1"
    region = "region-1"
    network {
        name = "network-1"
    }
    key_pair = "core"
    security_groups = ["default"]
    user_data = "${element(template_file.cloud-init.*.rendered, count.index)}"
}

CoreOSは、OS初期設定をcloud-config.ymlで行うのが標準仕様になってます。OpenStack上でCoreOSを立てる時は、user_dataconfig_driveにcloud-configの設定を渡すことで、インスタンス起動時に初期設定が行われます。cloud-config.ymlを動的に設定したいので、Terraformのtemplate_fileリソースを使います。cloud-config.yml.tplファイルは以下のようになります。

#cloud-config

hostname: ${hostname}

coreos:
  etcd2:
    discovery: ${discovery_url}
    discovery-proxy: http://proxy.example.com:1234 <- Discovery URLにアクセスするためのproxyの設定
    advertise-client-urls: http://$private_ipv4:2379,http://$private_ipv4:4001
    initial-advertise-peer-urls: http://$private_ipv4:2380
    listen-client-urls: http://0.0.0.0:2379,http://0.0.0.0:4001
    listen-peer-urls: http://$private_ipv4:2380
  units :
    - name: etcd2.service
      command: start
    - name: docker-tcp.socket
      command: start
      enable: true
      content: |
        [Unit]
        Description=Docker Socket for the API

        [Socket]
        ListenStream=2375
        BindIPv6Only=both
        Service=docker.service

        [Install]
        WantedBy=sockets.target
    - name: docker.service
      drop-ins:
        - name: 20-http-proxy.conf
          content: |
            [Service]
            Environment="HTTP_PROXY=http://proxy.example.com:1234"
      command: restart

etcdクラスタを組む際、各サーバで動かすetcd同士が同じクラスタに所属することを識別させるための情報(discovery token)が必要になります。手動でやる場合、以下のURLにアクセスしdiscovery tokenを取得してetcdの設定に使用するんですが、今回はTerraformのnull_resourceを使ってtokenを取得し、cloud-config.ymlを動的に生成するようにしています。

$ curl -s 'https://discovery.etcd.io/new?size=3'

以上のファイルの準備が整ったらterraform applyを実行します。インスタンスが立ち、etcdクラスタが作られます。etcdクラスタが組めているか、以下のコマンドで確認します。

$ etcdctl cluster-health
member 39da9b2c59694a93 is healthy: got healthy result from http://xxx.xxx.xxx.xxx:2379
member 53d999311a07d120 is healthy: got healthy result from http://yyy.yyy.yyy.yyy:2379
member d4822f0cf45608fd is healthy: got healthy result from http://zzz.zzz.zzz.zzz:2379
cluster is healthy

Docker Swarmクラスタを組む

次にDocker Swarmクラスタを組みます。Docker Swarmは、Dockerホストをクラスタリングし、Dockerコンテナを起動する時にどのホストで起動させるのかをスケジュールするツールです。Docker社が提供してます。

Docker Swarmは、AgentとManagerの2つの構成になっていて、それぞれDockerコンテナとして動かします。また、Swarm Managerは1台あればSwarmクラスタが組めますが、Swarm Managerが落ちるとDocker Swarm自体が使えなくなってしまうので、Swarm Managerを複数(プライマリとレプリカの構成で)動かします。その構成の場合、サービスディスカバリーとしてConsul, Etcd, ZooKeeperのどれかを使う必要があります。今回は、CoreOSに標準で組み込まれているEtcdを使います。

f:id:keepkeptkept:20160123163205p:plain

以下のコマンドで、Docker Swarm Agentを起動します。

$ docker run -d --name=swarm-agent swarm join --addr=$(cut -d'=' -f2 /etc/environment):2375 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes

次に、Docker Swarm Managerを起動します。4000番ポートをDocker Swarm Managerのポートにしています。

$ docker run -d -p $(cut -d'=' -f2 /etc/environment):4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $(cut -d'=' -f2 /etc/environment):4000 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes

4000番ポートでdocker infoを実行するとクラスタの情報がわかります。

## Host A
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
 xxx: xxx.xxx.xxx.xxx:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
 yyy: yyy.yyy.yyy.yyy:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
 zzz: zzz.zzz.zzz.zzz:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
CPUs: 12
Total Memory: 49.38 GiB
Name: be07ab661423


## Host B
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: replica
Primary: xxx.xxx.xxx.xxx:4000
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

無事にDocker Swarmが組めているのがわかります。試しにSwarm Managerを落としてみると、primaryが別なホストに移るのがわかります。

## Host A
$ docker stop swarm-manager

## Host B
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

ちなみに、環境変数DOCKER_HOSTを設定しておくとオプションを省略できて、シングルホストの時のように使えます。

$ export DOCKER_HOST=$(cut -d'=' -f2 /etc/environment):4000
$ docker info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

また、Dockerコンテナをsystemdから起動できるようにする場合は、以下のような記述をcloud-config.yml.tplに追記します。

    - name: swarm-agent.service
      command: start
      content: |
        [Unit]
        Description=Docker Swarm Agent Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-agent
        ExecStartPre=-/usr/bin/docker rm -f swarm-agent
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run --name=swarm-agent swarm join --addr=$private_ipv4:2375 etcd://$private_ipv4:2379/nodes

        [Install]
        WantedBy=multi-user.target
    - name: swarm-manager.service
      command: start
      content: |
        [Unit]
        Description=Docker Swarm Manager Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-manager
        ExecStartPre=-/usr/bin/docker rm -f swarm-manager
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run -p $private_ipv4:4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $private_ipv4:4000 etcd://$private_ipv4:2379/nodes

        [Install]
        WantedBy=multi-user.target 

こうすることでterraform applyだけで、Docker Swarmクラスタまで組めるようになります。

マルチホストネットワーク機能を試す

Docker Swarmクラスタが組めたので(ようやく)、次にマルチホストネットワーク機能を試します。

マルチホストネットワーク機能は、overlayドライバを使うことで仮想ネットワークを作ることができます。以下のコマンドでoverlayネットワークを作ります。Docker Swarmを使ってる場合、overlayドライバがデフォルトなので--driverオプションは省略可能です。

$ docker network create --driver overlay backend

ここで以下のようなメッセージが出る場合は、Dockerのクラスタ設定--cluster-advertise--cluster-storeが抜けているので、設定を追加します。

$ docker network create backend
Error response from daemon: 500 Internal Server Error: failed to parse pool request for address space "GlobalDefault" pool "" subpool "": cannot find address space GlobalDefault (most likely the backing datastore is not configured)

以下のファイルから、Dockerオプションを設定することができます。ファイルを作ったらdocker.serviceを再起動します。

$ sudo vi /etc/systemd/system/docker.service.d/30-custom.conf
[Service]
Environment="DOCKER_OPTS=--cluster-advertise eth0:2375 --cluster-store etcd://xxx.xxx.xxx.xxx:2379"

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

が、再度試したところ、また違うエラーが・・・。

$ docker network create backend
Error response from daemon: 500 Internal Server Error: error getting pools config from store during init: could not get pools config from store: client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.

今回、だいぶここでハマったんですが、Docker APIの通信がProxyを介してしまってうまくいかなかったようです。NO_PROXYにDockerホストのIPを全部並べてDockerホスト間はProxyさせないようにします。

$ cat /etc/systemd/system/docker.service.d/20-http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:1234"
Environment="NO_PROXY=localhost,127.0.0.1,xxx.xxx.xxx.xxx,yyy.yyy.yyy.yyy,zzz.zzz.zzz.zzz"

再度docker.serviceを再起動させて、無事にネットワークができました。

$ docker network create backend
856e8f470224627390abaaa31db868bd01ba7423e0bf7ea5b020f7e8e0336b34
$ docker network ls
NETWORK ID          NAME                DRIVER
856e8f470224        backend             overlay
f52c2ae81a2a        xxx/bridge     bridge
f9c89af96fad        yyy/none       null
...
...

では、コンテナを2つ立ち上げて疎通が確認できるか試してみます。

$ docker run -itd --net=backend --name test1 --hostname test1 ubuntu /bin/bash
$ docker run -itd --net=backend --name test2 --hostname test2 ubuntu /bin/bash

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
c1b2cd585175        ubuntu              "/bin/bash"         5 minutes ago       Up 49 seconds                           xxx/test2
01f8c3067398        ubuntu              "/bin/bash"         5 minutes ago       Up 5 minutes                            zzz/test1

docker psコマンドから、異なるホストでコンテナ2つが起動してるのがわかります。

$ docker exec -it zzz/test1 /bin/bash
root@test1:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:2/64 scope link
       valid_lft forever preferred_lft forever
29: eth1@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link
       valid_lft forever preferred_lft forever

eth0についている10.0.0.0/24のネットワークがoverlayネットワークです。

root@test1:/# ping test2
PING test2 (10.0.0.3) 56(84) bytes of data.
64 bytes from test2 (10.0.0.3): icmp_seq=1 ttl=64 time=0.950 ms
64 bytes from test2 (10.0.0.3): icmp_seq=2 ttl=64 time=0.544 ms
^C
--- test2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.544/0.747/0.950/0.203 ms
root@test1:/#

おぉ、疎通も無事確認できました!

ところで、コンテナ同士の名前解決はどうやってるんでしょうか。/etc/resolv.confなどをみてみましたが、どうやら/etc/hostsでやっているようです。

root@test1:/# cat /etc/hosts
10.0.0.2        test1
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.3        test2
10.0.0.3        test2.backend
root@test1:/# 

試しに、新しくtest3を作ってみると/etc/hostsに新しい設定が追加されてました。

root@test1:/# cat /etc/hosts
10.0.0.2        test1
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.3        test2
10.0.0.3        test2.backend
10.0.0.4        test3
10.0.0.4        test3.backend
root@test1:/# 

まとめ

Dockerを本番環境で使うために、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試してみました。DockerをProxy環境下で動かす場合は、いろんな場所にproxy設定を記述しないといけないのがアレですね。ここまで動かすのにProxyのおかげでだいぶ消耗しました・・・。あと、コンテナを立ち上げるのにdocker runコマンド経由なのが少し煩わしく感じました。その辺はスクリプトにするか、もう少し宣言的に出来ないかと思うのでdocker-composeやnomadなどを検討する必要がありそうです。でも、まぁ、ひとまず、環境ができてよかったです。

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

Docker 実践ガイド (impress top gear)

Docker 実践ガイド (impress top gear)

sinatraとsidekiqを組み合わせて、簡単なWeb APIを作ってみた

sidekiqは、複数のjobを非同期実行させることができるrubyライブラリです。割と簡単にjobの並列処理がrubyで書けて、便利そうです。今回、年末から年始にかけて、ゆる〜くsinatraとsidekiqを組み合わせて、非同期型のjob実行Web APIを作ってみたので、まとめておきます。

背景

GUIしか存在しない、いにしえのツールをWeb API化(poltergeistを使ってブラウザ操作部分を自動化し、sinatraとsidekiqを組み合わせてAPIを作成)したかったからです。

なお、今回作ったものはGitHubにあげました。

github.com

開発環境

今回Dockerも触ってみたかったので、ローカル開発環境をDockerで作りました。

  • OS X 10.11.1 (15B42)
  • Docker version 1.8.2, build 0a8c2e3
  • docker-machine version 0.4.1 (e2c88d6)
  • docker-compose version: 1.4.2

docker-machine

docker-machineでDocker環境(VirtualBox上にDocker用VM)を作ります。

$ docker-machine start dev

docker-machine envの結果を環境変数に設定します。

$ eval "$(docker-machine env dev)"

application用のDockerfile

sinatraとsidekiqを動かすapplication用コンテナのDockerfileを、以下のように作ります。一度/tmpにGemfileとGemfile.lockをコピーしてbundle installすることで、docker runするたびにgem installが動くことを回避し、うまくDockerのキャッシュを使えるようにしています。また、sinatraで使うポート5000をEXPOSEし、外からアクセスできるようにします。

FROM ruby:2.2.4
RUN gem install bundler

WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install

WORKDIR /opt/sinatra-sidekiq-example

EXPOSE 5000

docker-compose

sidekiqは、jobのキューをためるのにredisを使っているので、applicationコンテナとは別にredisコンテナを立ち上げます。ただ、applicationコンテナとredisコンテナ、複数のコンテナを立ち上げようとすると、linkオプションを使わないといけなかったり、コンテナの起動順序を意識したりと、dockerコマンドだけで管理しようとすると極端に辛くなってきます。そこでdocker-composeを使います。docker-compose.ymlファイルにコンテナの定義を書くことで、コンテナを管理しやすくします。コンテナが多くなってくると、コマンドラインで短縮化されたオプションを書きまくるより、設定ファイルでより直感的に管理できる方がいいですからね。

docker-composeのヘルプはこんな感じ。

$ docker-compose -h
Define and run multi-container applications with Docker.

Usage:
  docker-compose [options] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
  -f, --file FILE           Specify an alternate compose file (default: docker-compose.yml)
  -p, --project-name NAME   Specify an alternate project name (default: directory name)
  --verbose                 Show more output
  -v, --version             Print version and exit

Commands:
  build              Build or rebuild services
  help               Get help on a command
  kill               Kill containers
  logs               View output from containers
  port               Print the public port for a port binding
  ps                 List containers
  pull               Pulls service images
  restart            Restart services
  rm                 Remove stopped containers
  run                Run a one-off command
  scale              Set number of containers for a service
  start              Start services
  stop               Stop services
  up                 Create and start containers
  migrate-to-labels  Recreate containers to add labels
  version            Show the Docker-Compose version information

今回のdocker-compose.ymlファイルはこんな感じにしました。

web:
  build: .
  command: foreman start
  ports:
    - "5000:5000"
  volumes:
    - .:/opt/sinatra-sidekiq-example
  links:
    - redis
  environment:
    - REDIS_HOST=redis
    - REDIS_PORT=6379
redis:
  image: redis

buildでカレントディレクトリを指定してますが、そうすることでカレントディレクトリからDockerfileを探してきて勝手に読み込んでくれます。commandは、コンテナを起動するときに実行するコマンドです。portsはポートフォワーディングの設定です。volumesは、コンテナの内部とカレントディレクトリを共有するために設定します。linksは、コンテナ同士の関連付けです。environmentはコンテナ内部の環境変数設定です。redisコンテナは特にカスタマイズせずに公式のredisイメージを立ち上げるだけにしてます。

Dockerfileからコンテナをビルドします。

$ docker-compose build

コンテナを起動させます。

$ docker-compose up -d

コンテナの情報を確認します。

$ docker-compose ps
            Name                          Command             State           Ports
--------------------------------------------------------------------------------------------
sinatrasidekiqexample_redis_1   /entrypoint.sh redis-server   Up      6379/tcp
sinatrasidekiqexample_web_1     foreman start                 Up      0.0.0.0:5000->5000/tcp

コンテナの中に入ります。

## application用 (web)
$ docker-compose run web /bin/bash

## redis用
$ docker-compose run redis /bin/bash

参考

sidekiq

開発環境が整ったら、ようやくsidekiqです。Gemfileにsidekiqを指定し、bundle installします。

gem 'sidekiq'

sidekiqを使った実装は、jobをキューに入れるところと、jobを実行するところの2つです。

redisの接続設定

sidekiqはバックエンドにredisを使うので、redisに接続するための設定を記述します。configure_clientとconfigure_server、両方必要です。

## jobをキューにためるのに必要
Sidekiq.configure_client do |config|
  config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end

## jobをキューから取得するのに必要
Sidekiq.configure_server do |config|
  config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end

workerの実装

jobを実行するworkerの実装は、以下のような感じです。Sidekiq::Workerモジュールをincludeしたclassを作り、performメソッド内にjobを記述します。ドキュメント読む限り、performメソッドの引数は、redisの容量を少なくするためになるべく少なくした方がいいようです。

require 'sidekiq'

class SomeWorker
  include Sidekiq::Worker

  def perform(id)
    thing = $redis.hget('things', id)
    sleep 10
    p "The job is done: #{thing}"
  end
end

キューにためたjobを実行するためには、sidekiqコマンドを使います。

$ bundle exec sidekiq -h
2016-01-02T15:21:45.078Z 7 TID-origl9uvo INFO: sidekiq [options]
    -c, --concurrency INT            processor threads to use
    -d, --daemon                     Daemonize process
    -e, --environment ENV            Application environment
    -g, --tag TAG                    Process tag for procline
    -i, --index INT                  unique process index on this machine
    -q, --queue QUEUE[,WEIGHT]       Queues to process with optional weights
    -r, --require [PATH|DIR]         Location of Rails application with workers or file to require
    -t, --timeout NUM                Shutdown timeout
    -v, --verbose                    Print more verbose output
    -C, --config PATH                path to YAML config file
    -L, --logfile PATH               path to writable logfile
    -P, --pidfile PATH               path to pidfile
    -V, --version                    Print version and exit
    -h, --help                       Show help

## 実行例
$ bundle exec sidekiq -r ./app.rb -C ./sidekiq.yml

オプションを色々と指定することもできますが、yml形式の設定ファイルを使うこともできます。sidekiq.ymlはこんな感じ。

:verbose: true
:pidfile: /var/run/sidekiq.pid
:logfile: /var/log/sidekiq.log
:concurrency: 10
:queues:
    - default

sidekiqのプロセス数は、concurrencyで指定します。デフォルトは、25です。また、-dオプションでプロセスをデーモン化できますが、本番環境ではsystemdなどを使った方がいいとドキュメントに書いてあります。

jobをキューに登録

非同期で実行したいjobをキューに登録するのには、perform_asyncクラスメソッドを呼びます。

    SomeWorker.perform_async(req['id'])

5分後に実行したいjobは以下のように、perform_inを使います。

    SomeWorker.perform_in(5.minutes, req['id'])

dashboard

f:id:keepkeptkept:20160103102740p:plain

sidekiqは、jobのステータスをブラウザから確認するためのdashboardを提供してます。ドキュメントには、railsと一緒に使う方法しか書いてないんですが、sinatraと一緒に使う場合は、以下のようにすれば使えました。

require './app'
require 'sidekiq/web'

run Rack::URLMap.new('/' => SomeApp, '/sidekiq' => Sidekiq::Web)

参考

foreman

foremanは複数のプロセスを管理するツールです。今回sinatraとsidekiqのプロセス管理をforemanでやってみました。以下のようにProcfileにプロセスの定義を書きます。

web: bundle exec rackup -p $PORT --host 0.0.0.0
worker: bundle exec sidekiq -r ./app.rb -C sidekiq.yml

起動は以下のコマンド。

$ foreman start

参考

まとめ

sinatraとsidekiqを組み合わせることで、簡単に非同期型のjob実行Web APIが書けました。半分くらいDockerで遊んで得た知見ですが、これでひとまずやりたいことが実現できそうなことが分かってよかったです。

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

Docker実践ガイド

Docker実践ガイド

Terraform moduleあるある

このエントリは HashiCorp Advent Calendar 2015 - Qiita 10日目の記事です。今回はTerraformのmodule機能に関する知見をご紹介します。

moduleとは?

module "consul" {
    source = "github.com/hashicorp/consul/terraform/aws"
    servers = 3
}

moduleは、Terraform resourceを抽象化するためのものです。よく使うパラメータをmodule内に隠蔽して入力項目を減らしたり、複数のresourceをまとめたり、tfファイルの見通しを良くするために使います。

生のresourceを使ってTerraformの設定ファイルを書くのには、ある程度インフラの知識が必要です。module機能を使って、可能な限り入力項目を簡潔にすれば、インフラの知識がない人でもTerraformを使ってインフラの構築作業ができるようになります。例えば、インフラインフラエンジニアがmoduleの作成と管理を行い、アプリケーションエンジニアがmoduleを利用する、というような具合です。moduleを制するものはTerraformを制す、と言っても過言ではないくらい、重要な機能だと思います。

moduleの作り方

moduleの作り方ですが、ドキュメントを読めばなんとなく概要はつかめると思います。ただ、正直色んな書き方ができてしまうので、最初は困ると思います(自分がそうでした…)。実はHashicorpがterraform-community-modulesというのを作っているので、最初はそれを参考にするのがいいかと思います。自分が作ってるterraform moduleもこれを参考にして作っていて、大体このようなファイル構成にしてます。

.
├── CHANGELOG.md
├── README.md
├── init.sh
├── main.tf
├── outputs.tf
├── templates
│   └── node.json.tpl
└── variables.tf

基本的にmoduleはgitリポジトリごとに分けて管理していて、main.tfにメイン処理、variables.tfに変数、outputs.tfにモジュール外で呼び出したいパラメータを記述するようにしています。

では実際に試行錯誤して得られた知見をみていきましょう。

Try 1: moduleの変数に配列を使いたい

module "example" {
  source = "..."
  servers = ["foo", "bar", "buz"]
  ...
  ...

ある時、上の設定のようにmoduleの変数に配列を渡したい時がありました。

試しにこの書き方で使ってみると、エラーが返ってきます。現在の最新バージョンv0.6.8でも、まだサポートされていないようです。moduleの変数に配列は渡せません。

module "example" {
  source = "..."
  servers = "foo,bar,buz"
  ...
  ...

ではどうしたかというと、色々とgithubのissueを探した結果、上の設定のようにカンマを使って文字列として変数を渡し、module内で配列に変換するという方法を見つけました。

module内部の記述は以下のようになります。

resource "openstack_compute_instance_v2" "default" {
    count = "${length(split(",", var.servers))}"
    name = "${element(split(",", var.servers), count.index)}"
    ...
    ...

countパラメータでは、splitで文字列を配列に変換し、lengthで配列のサイズを求めています。nameパラメータではsplitで文字列を配列に変換し、elementで配列の要素を取得しています。正直余り綺麗ではないですが、なんとかやりたいことは実現できました…。

Try 2: クラスタを組む時にAZを分けたい

ある時、サービスの可用性を高めるために異なるAZに分けてインスタンスを構築したい、そんな時がありました。

これは割と知られている方法かもしれませんが、count.indexと余りを計算する%を組み合わせて実現できました。

variables.tfファイルの中で、AZの定義を書き、

variable "availability_zones" {
  default = {
    "0" = "az-foo"
    "1" = "az-bar"
    "2" = "az-buz"
  }
}

main.tfの中で、lookupを使って、availability_zones変数から対応するAZを取得してます。こうすることで、1台目はaz-fooに、2台目はaz-barに、3台目はaz-buzに、4台目はaz-fooに、といった具合になります。

resource "openstack_compute_instance_v2" "default" {
  ...
  availability_zone = "${lookup(var.availability_zones, count.index % 3)}"
  ...
  ...

Try 3: Serverspecと連携させたい

ある時、Terraformで作ったインスタンスをServerspecで確認したい、Serverspecで使うyamlファイルをTerraformで生成したい、という時がありました。

そんな時は、template_file resourceを使って対象インスタンスごとにyamlファイルを生成し、Terraform実行後にServerspecを実行するというようなことで実現できました。以下は、設定例です。

resource "template_file" "example" {
    template = "${file("./templates/os_base.yml.tpl)}"

    vars {
        target = "${openstack_compute_instance_v2.example.access_ip_v4}"
    }

    provisioner "local-exec" {
        command = "echo '${template_file.example.rendered}' > /var/tmp/os_base.yml"
    }
}

local-execでechoする、…なかなか渋い方法になりました。

Try 4: module内の変数をmoduleの外で使いたい

ある時、moduleの中で生成される変数を他のresourceの変数として使いたい、というような時がありました。

普通にmodule内部のresourceパスを書けばできるだろうと思って、色んな書き方を試したんですが、うまくいきません。色々試した結果、moduleの外から呼び出したい変数はmoduleの中でoutput設定を使って、明示的に定義してやらないといけないことに気がつきました。moduleを作る際は、意外とoutput設定が重要です。

output "ipv4_address" {
    value = "${openstack_compute_instance_v2.default.access_ip_v4}"
}

Try 5: moduleはきちんとtagを打つ

moduleを使う際は、バージョンロックさせるためにgitのtagでバージョンを指定するようにしましょう。tagを指定しないとmoduleの入力パラメータが変わってしまった時にエラーになってしまうので、利用者にきちんと説明をした方がいいです。

まとめ

以上、Terraformのmoduleに関する知見を紹介しました。Terraformがリリースされて1年以上になりますが、まだまだノウハウが足りません。かゆい所にもうちょっとで手が届くか届かないかっていう感じなので、tfファイルのグッドプラクティス情報、ぜひ知りたいです。よろしくお願いします。

HashiConf 2015に参加してきました

9月28, 29にポートランドで開催されたHashiConf 2015に参加してきたのでレポートします。

セッションを聞いて

カンファレンス全体を通して、一日目のMitchell Hashimoto氏のKeynoteがやはり一番エキサイティングでした。事前に新しいプロダクトがリリースされる噂はあったんですけど、まさか二つ発表されるとは…。しかも、発表の流れがめちゃくちゃ良くて、みんなワクワクしながら新プロダクトのリリースを待ってたところにNomadが出てきて、おー、ってなったところに、さらにその後『One more thing』でOttoが発表されて、会場はだいぶ盛り上がってました。え、それどこの、スティーブジョブズですか?ってなってました。カリスマ性半端なかった。

個人的に新プロダクトの中でもOttoはとても興味深くて、開発者と運用者の間の責任領域をうまく抽象化し、開発、インフラの構築、デプロイまでを一つのツールで管理できるプロダクトだと思います。そこそこ人数がいて開発と運用で組織が分かれてるような会社だと、各組織間で責任領域が分かれていて越えられない壁みたいなのがあって、ワークフローや調整などで結構コストがかかるっていう悲しい問題があるんですけど、Ottoはその辺りをいい感じで解決出来そうです。運用者側はOttoのインフラモジュールをPackerやTerraormなどで定義しておいて、開発者側はあまりインフラを意識せずにそのモジュールを使って、開発からアプリケーションのデプロイまでできると。まさにプライベートなPaaSを実現できそうなツール。まだまだリリースされたばかりで足りてないと思うけど、HashiCorpの今までやってきたこととこれからのビジョンを具現化したようなツールで、今後の展開が本当に楽しみです。

午後のセッションでは、『VC's on line 1』っていうタイトルでTerraformとChefを使ったインフラ自動化の話が面白かったですね。chef provisioningとTerraformの性能比較とか、Terraformのモジュールの書き方とか、個人的にめちゃめちゃ参考になりました。みんながみんなCodeを理解できるわけではないから、そういう人に対してインフラ自動化を可視化できるようにGUIツールを作ったっていうようなこと言ってて、そこに至る背景とか苦労とか考えると、共感してしまって他人ごとでは聞けない発表でした。

2日目は、朝からまたHashimoto氏によるKeynote。The Tao of Hashicorpの話を一つ一つ説明しくれてました。話自体は知ってる内容でしたが、改めて本人から説明を聞くと、なんだかもう本当ありがたくて、Hashimoto氏から後光さしてましたね。もうここまで来ると、まさにHashiCorp道の教祖、いや、DevOps界の神のお言葉。ありがたや、ありがたやと思いながら聞いてました。あと、AtlasとOttoの関係をGitHubとGitに例えて説明をしてたのも面白かったです。一年でエンジニアが30人くらいまで増えたらしく、今後Atlasを使ってどうマネタイズしてくのかなぁなんて考えたりしてました。

2日目の午後のセッションはAmazonのMicroserviceの話が特に面白かったです。Microserviceをどこにどう導入していったかっいう話で、それぞれのフェーズの課題とそれに対する解決法みたいなことを説明してました。前半は、これからはMicroserviceの時代だぜ的なノリで面白かったけど、後半は自社製品の説明で少し退屈な内容でした。

参加者との交流

参加者との交流は、わざわざ現地まで行ってカンファレンスに参加することの醍醐味で、HashiCorpプロダクトに携わるエンジニアと実際に会って話せて楽しかったです。

Hashimoto氏と日本から来てるメンバーで写真撮りました。

@yoshidashingoさんは、会場で配ってたHashiCorpのロゴ入りノートにサインもらってました。

1日目夜のパーティーでTerraformのプロダクトマネジメントしてる人と話す機会があって、次のメジャーバージョンいつ出そうとしてるのかとか、今作ってるTerraformのVMwareプラグインを本家に取り込んでもらえるか的な相談とか、個人的にかなり有意義な話ができたので良かったです。

また、日本のWebインフラ界隈で有名なエンジニアの方々とも知り合いになることもできました。特に2日目夜は一緒にレバノン料理を食べながら、NomadやOttoがどうだったとか、面白いセッションあったかとか、HashiConfの話から始まりいろんな情報交換ができて楽しい時間を過ごせました。一応名刺持ってったけど、結局一枚も使わずにTwitter のアカウント教えてもらっただけでした。その辺はなんかエンタープライズ系のエンジニアは少ないのかなぁと思いました。

ポートランドの街

ポートランドクラフトビールが有名な街らしく、ふらっと街を散策してたらブリュワリーがやってるレストランを見つけて入ってみました。9ドルで6種類のクラフトビールの飲み比べができるメニューがあってだいぶお得で、味はかなり癖のあるものから飲みやすいものまで様々で、ビール好きにはたまらない感じでした。

日本でポートランドっていうと全然知名度低いけど(アメリカでは住みたい街ナンバーワンらしい)、意外にもいい街でした。

と、まぁ、そんな感じでHashiConf 2015はむちゃくちゃ楽しかったです!ありがとうHashiCorp!ありがとうHashiConf!