2023年振り返りと2024年抱負。
約4年ぶりにブログの更新。2023年の正月に抱負を書きかけて、早一年。そのままほったらかししていたブログの下書きを、まさか2024年正月に更新するとは。今年こそ雑に書いて公開しよう。
2023年振り返り
仕事
2023年の仕事について振り返ってみると、パブリッククラウド関連の仕事をする機会が以前よりぐんと増え、所属するチームも変わり、エンジニアリングマネージャーのロールも任されたので中々変化の年だったと思う。
今までは、基本的にオンプレミス環境を使ってシステムを構築運用する開発チームの技術サポートが中心だったけど、社内でのCloud CoE(CCoE)立ち上げに伴ってパブリッククラウドがメインになった。AWS、Azure、Google Cloud、それぞれのクラウドを触る機会があってクラウドエンジニアを目指す環境としてはとても良いと思う。反面、守備範囲が広くなってどうしても時間の制約上知識が広く浅くになってしまって少し寂しい。自分で手を動かして検証や開発などやっていきたい気持ちはあるものの、その辺はチームメンバーに任せてしまって、自分はそれ以外の仕事を意識するのが多かったように思う。
2024年は自分でも少し手を動かして検証とか開発することを意識しつつも、チームとしてのアウトプットを最大化できるようにしよう。CCoEとしては2023年は立ち上げフェーズということもあり、目に見えるアウトプットがなかったので、2024年は結果を出せるように頑張りたい。
買ってよかったもの
夜な夜なエールのクラフトビール定期便
クラフトビールが好きなので試しに夜な夜なエールのサブスクをやってみた。毎月好きなクラフトビールが届くのでかなりよかった。家に帰ると冷蔵庫に色んなクラフトビールがある状況が素晴らしかった。
ランニングシューズ
2023年11月にハーフマラソンを走ることにしたので、ランニングシューズを新調した。反発力とクッションがいい感じ。今まで履いていたシューズだと膝が痛くなることがあったけど、今は全然そういう痛みがなく走れるようになった。ランニングシューズでこんなに走り方変わるとは思ってなかった。
サコッシュ
夏に会社の先輩達と1人目の息子(小学一年生)を連れてシロギス釣りに行った。ちょっとした小物を入れるのに便利そうということで、サコッシュを買ってみた。釣りでも活躍し、今では、近くの公園行く時とかに重宝してる。リュックまではいかないんだけど、財布とか何も持ってかないと寂しい時にちょうどいい。
ちなみにシロギス釣りは親子共々初めての船釣りで心配してたけど、息子も楽しんでくれていたのでよかった。2024年もまたチャレンジしたい。
映画
息子達が成長して長時間座ることができるようになったので、2023年は割と映画館で映画を観ることができた。
- https://slamdunk-movie.jp/:THE FIRST SLAM DUNK
- https://www.nintendo.co.jp/smbmovie/index.html:ザ・スーパーマリオブラザーズ・ムービー
- https://www.disney.co.jp/movie/my-element:マイ・エレメント
- https://godzilla-movie2023.toho.co.jp/:ゴジラ-1.0
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 については色々な方がスライドや動画をアップしていて、この辺り参考になりました。
- https://speakerdeck.com/jacopen/cloud-native-buildpackde-mendounakontenaimezizuo-cheng-wo-zi-dong-hua-siyou
- https://speakerdeck.com/pyama86/cn-buildpacksgazuo-ruwei-lai
- https://docs.google.com/presentation/d/1rPM00F3ptJ3m9UHNrdTx2ZPrBoecb4Wca0GujfViBP0/edit?usp=sharing
- https://youtu.be/EVHHyiypiY0
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
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になるようです。
この環境変数を適切なパスに変更することでうまくいきそうですね。ただし、ターミナルに環境変数を渡すだけではうまくセットされなくて、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 に上げました。
InSpecではじめるテスト駆動インフラ
最近、新しくChefのCookbookを書く機会があったので、前から気になっていたInSpecを少し触ってみました。
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 に比べ、よりRubyやRSpec、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であれば以下のコマンドだけで環境の準備ができます。(virtualboxとVagrantは別途必要)
$ 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
コマンドで、以下の処理が一気に動きます。
- VirtualBox上にインスタンスを作る
- Chefを実行する
- Serverspecでテストする
- インスタンスを削除する
ちなみに、上記のログにあるように、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と組み合わせるといったことができるらしいのですが、今回はインフラのテストとしてしか試していないので、機会があればその辺も触ってみたいです。
参考
- InSpec - Audit and Test Framework
- [和訳] InSpecへの道 #getchef - クリエーションライン株式会社
- GitHub - chef/inspec: InSpec: Auditing and Testing Framework
- Test-KitchenでServerspecやInfratasterをShell-Verifierから実行 - Qiita

DevOps導入指南 Infrastructure as Codeでチーム開発・サービス運用を効率化する (DEV Engineer’s Books)
- 作者: 河村聖悟,北野太郎,中山貴尋,日下部貴章,株式会社リクルートテクノロジーズ
- 出版社/メーカー: 翔泳社
- 発売日: 2016/10/14
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る

Infrastructure As Code: Managing Servers in the Cloud
- 作者: Kief Morris
- 出版社/メーカー: Oreilly & Associates Inc
- 発売日: 2016/06/27
- メディア: ペーパーバック
- この商品を含むブログを見る

- 作者: 宮下剛輔
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/01/17
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (6件) を見る
今更ですが、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試しました(Proxy環境下)
Dockerを使って開発していると、本番環境でもDockerを使ってみたいなぁという気持ちになります。ただ、本番環境でDockerを使うのは、個人的にまだまだハードルが高そうな印象です。マルチホスト間のネットワークはどうするか?分散システムのクラスタリングはどうするか?コンテナのスケジューリングは?監視は?ログは?デプロイの仕組みは?負荷が高い時の対応はどうするのか?セキュリティは?など、運用面で色々と考えなければなりません。さらにDocker界隈のツールは様々なツールが存在していてどれから手をつけていいのか選択に困ります。最初はなるべく小さく始めたいところです。なので、今回、本番環境を意識したDocker環境をより少ないツールで作れるかどうか試してみました。ひとまず、クラスタ組んで、マルチホストネットワーキング機能が試せるところまで。使ったツールは以下になります。
- CoreOS - DockerホストOS(betaチャンネル 899.3.0)
- cloud-config - OS初期設定
- etcd - 分散KVS + クラスタリング
- Docker - v1.9.3
- Docker Swarm - コンテナスオーケストレーション
- Terraform - インフラデプロイ
今回試した環境の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_data
かconfig_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を使います。
以下のコマンドで、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教科書 インフラの基礎知識&コードによる環境構築の自動化
- 作者: WINGSプロジェクト阿佐志保,山田祥寛
- 出版社/メーカー: 翔泳社
- 発売日: 2015/11/20
- メディア: 大型本
- この商品を含むブログ (1件) を見る

Docker実践入門――Linuxコンテナ技術の基礎から応用まで (Software Design plus)
- 作者: 中井悦司
- 出版社/メーカー: 技術評論社
- 発売日: 2015/09/26
- メディア: 大型本
- この商品を含むブログ (2件) を見る

Docker 実践ガイド (impress top gear)
- 作者: 古賀政純
- 出版社/メーカー: インプレス
- 発売日: 2015/12/17
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
sinatraとsidekiqを組み合わせて、簡単なWeb APIを作ってみた
sidekiqは、複数のjobを非同期実行させることができるrubyライブラリです。割と簡単にjobの並列処理がrubyで書けて、便利そうです。今回、年末から年始にかけて、ゆる〜くsinatraとsidekiqを組み合わせて、非同期型のjob実行Web APIを作ってみたので、まとめておきます。
背景
GUIしか存在しない、いにしえのツールをWeb API化(poltergeistを使ってブラウザ操作部分を自動化し、sinatraとsidekiqを組み合わせてAPIを作成)したかったからです。
なお、今回作ったものはGitHubにあげました。
開発環境
今回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
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教科書 インフラの基礎知識&コードによる環境構築の自動化
- 作者: WINGSプロジェクト阿佐志保,山田祥寛
- 出版社/メーカー: 翔泳社
- 発売日: 2015/11/20
- メディア: 大型本
- この商品を含むブログ (1件) を見る

Docker実践入門――Linuxコンテナ技術の基礎から応用まで
- 作者: 中井悦司
- 出版社/メーカー: 技術評論社
- 発売日: 2015/09/26
- メディア: Kindle版
- この商品を含むブログを見る

- 作者: 古賀政純
- 出版社/メーカー: インプレス
- 発売日: 2015/12/17
- メディア: Kindle版
- この商品を含むブログを見る
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!