Immutable Infrastructure の現実的なポイント(抄訳+アルファ)

Why you should build an Immutable Infrastructure | via @codeship

こないだの Codeship のブログ見てて、自分も今ちょうどまさにそこに取り組んでいたのでいやほんとそうだなーって思って、でもアホだからほっとくと自分でも自分がどういうゴールで仕事してるのかわかんなくなるんですよね…。なのでポイントだけ抜き出してみました。また例によって僕の言葉も入ってるので、実際に彼らが何と言ってるかは原文を確認してください。ちなみに会社にいる時間で20分くらいかけて書きました!!。ありがとうございます!!。

Immutable Infrastructure とは

  • その場で更新されること無く、必要に応じてデプロイ毎に交換される Immutable Component の集まり。
  • Immutable Component は単独及び独立にテスト・バリデート可能。

状態の隔離

  • システム全体としては状態を持つが、状態自体は特定のコンポーネント(群)に隔離する。
  • 状態による振る舞いの変更を予測可能な規模に丸め込むことが大事。
  • 複数のドメインの状態が混じると、何かが起こったときにそのシステムの神しか対応できなくなる。

Atomic なデプロイとバリデーション

  • 論理・物理ホストの構築からアプリケーションの設置までの「デプロイ」が Atomic でないと、その場限りの悪い意味でユニークな状態になってしまうことがある。
  • 今どんな状態か分からない、他に例が無い場合、デバッグが難しい。本番デバッグとかになる。
  • (可能なかぎりにおいて)デプロイを Atomic に近づけ、毎デブロイごとにホストからアプリケーションまで、コードによって構築する。
  • リリースはその Immutable Component を例えばロードバランサで切り替えることで行う。

時間がかかりすぎるのでは?

  • 構築用イメージをレイヤリングする。
  • それでもまだこれまでにくらべて時間がかかる。あるレイヤーの構築が外部リソース( gem, yum, curl... )に依存する場合失敗することもある。
  • Chef, Puppet, CFEngine, Ansible などを使うのだが、これらは Immutable Infrastructure のためのものでは無いので、大抵工夫が必要になる。
  • 新しいツールが待たれる。

速いリカバリー(歴史がそのまま残っているから)

  • 全てのデプロイは新しいイメージを作ることで行われる。
  • 古いイメージはそのまま歴史としてロールバックに利用できる。
  • データスキーマの変更は問題だが、それは未だロールバックというプロセスの問題だ。

実験とか新しいバージョンの導入が楽になるよ

  • なるよ

ログという状態はログ担当 Component に任せろ

  • Component を手で弄れないのは、ログという状態を切り出して隔離するよいきっかけ

まとめ

  • 状態!状態!状態!
  • 状態を把握しやすくする。
  • ある知見がどの状態において有効なのか把握しやすくする。蓄積しやすくする。
  • 状態に根ざした例外を把握しやすくする。

CentOS 7 と Vagant と Packer

CentOS 7 の Vagrant の box で vmware_desktop のものが無かったので Packer でちゃちゃっとつくってみました。2点時間がかかったので書いておきます。

VMware Tools の中の vmhgfs のコンパイルがこける

CentOS 7 、カーネル 3.10.0-123.4.2.el7 、 Tools 9.6.2-1688356 。

VMware Tools Compile Problem | VMware Communities

これです。カーネル 3.11 で入った変更に VMware 側で対応したんだけど、 RedHat がその変更を 3.10 にバックポートしたんで CentOS 7 とかとかでこけるようになりました。という話。そんならあんまり頑張ってももったいないので

$ tar xf vmware-tools-distrib/lib/modules/source/vmhgfs.tar -C vmware-tools-distrib/lib/modules/source/
$ sed -i -e '/KERNEL_VERSION/{s/3, 11, 0/3, 10, 0/}' vmware-tools-distrib/lib/modules/source/vmhgfs-only/shared/compat_dcache.h
$ rm -rf vmware-tools-distrib/lib/modules/source/vmhgfs.tar
$ tar cf vmware-tools-distrib/lib/modules/source/vmhgfs.tar -C vmware-tools-distrib/lib/modules/source/ vmhgfs-only/
$ rm -rf vmware-tools-distrib/lib/modules/source/vmhgfs-only/

この雑な感じで。すぐ VMware が対応しそう。

Vagrant で Forwarding Port 以外のネットワーク設定が使えない

RHEL/CentOS7 guest on VirtualBox with host only interface will not work because configure_networks.rb expects old interface names · Issue #4078 · mitchellh/vagrant · GitHub

これ。もう eth じゃねーし!。 eth0 からあがってくのやめるわって奴ですが、僕はこの enc なんたらとかルールをよく調べてないので、一旦放置。

CentOS 7 上で docker の packer build が provision のところでハングする

Permission denied on accessing host directory in docker - Stack Overflow

ついでにこれです。 selinux のなんかです。 packer 使わずに自分で -v してもそこに行くとパーミッション揃ってても permission denied で中身にアクセス出来ないです。まだちゃんと調べてません。

CentOS の kickstart ファイル

スクリプトでインストールするとき、 Debian / Ubuntu だと preseed ってのを仕込むじゃ無いですか。これまで Ubuntu の box 作るのに Packer でもちろん使ってました。

今回 CentOS 7 にしてみたので、同じようなのないかと調べたら kickstart ファイルでありました。 Packer での仕込み方も一緒。

    {
      "type":                "vmware-iso",
      "iso_url":             "http://www.ftp.ne.jp/Linux/packages/CentOS/7/isos/x86_64/CentOS-7.0-1406-x86_64-DVD.iso",
      "iso_checksum":        "713ea7847adcdd1700e92429f212721a",
      "iso_checksum_type":   "md5",
      "ssh_username":        "vagrant",
      "ssh_password":        "vagrant",
      "shutdown_command":    "echo 'vagrant' | sudo -S poweroff",
      "guest_os_type":       "centos-64",
      "tools_upload_flavor": "linux",
      "http_directory":      "preseed",
      "http_port_min":       8100,
      "http_port_max":       9000,
      "output_directory":    "output/vmware-iso/{{isotime}}",
      "vmx_data": {
        "memsize":  "1024",
        "numvcpus": "2"
      },
      "boot_command": [
        "<esc><wait>",
        "linux ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/anaconda-ks.cfg",
        "<enter><wait>"
      ]
    },

問題はどうやって作るんだよってことですが、なんと!

You can create it by using the Kickstart Configurator application, or writing it from scratch. The Red Hat Enterprise Linux installation program also creates a sample kickstart file based on the options that you selected during installation. It is written to the file /root/anaconda-ks.cfg.

って書いてある!やった!。

#version=RHEL7

firstboot --enable
ignoredisk --only-use=sda
bootloader --location=mbr --boot-drive=sda
autopart --type=lvm
clearpart --none --initlabel
eula --agreed

network --bootproto=dhcp
network --hostname=localhost.localdomain

keyboard --vckeymap=jp106 --xlayouts='jp'
lang ja_JP.UTF-8
timezone Asia/Tokyo --isUtc

rootpw kidoibuki
user --groups=wheel --name=vagrant --password=vagrant

repo --name=base --baseurl=http://www.ftp.ne.jp/Linux/packages/CentOS/7/os/x86_64/
url --url="http://www.ftp.ne.jp/Linux/packages/CentOS/7/os/x86_64/"

reboot

%packages --nobase --ignoremissing
@core

%end

出力されたファイルに、 %packages のオプションと、 reboot コマンドを追加しました。 reboot コマンド無いとインストール完了した後「再起動するかい?」って訊いてきたまま止まってるので結構間抜けである。

どうも古いので怪しいんだけどドキュメントにコマンドの一覧やオプションにしっかりまとまってます( 28.3. Creating the Kickstart File )。

ちなみに preseed のも手動で入れたときの結果を元に作った気がする。忘れちゃったけど。あ debconf-get-selections だ( B.3. Creating a preconfiguration file )。

Packer の Shell Provisioner 中で reboot すると再起動が完了する前に次の script がちょっと走っちゃう

Ubuntu でずいぶん前に作ってた Packer の json で CentOS 用の box 作ろうと思ったら途中でハングした感じになってた。どうも reboot したとき、その script は exit するのだけど、 sshd やら network やらはまだちょっとの間生きているので Packer が次の script を走らせてしまうみたいだ。 Ubuntu の時は大丈夫だったのはなんでだ?。ただの奇跡かな。 reboot の実装違うとかかな。

で、ドキュメントとソース見たら、 ssh が切れたらすぐに次のスクリプトには進めなくなる上に、最大で start_retry_timeout 時間分待って再接続して次のスクリプトから進めてくれるということだったので、

#!/bin/bash -eux

systemctl stop sshd
reboot

当然こんな感じになりました。ばっちりになった。

golang の intelliJ 用プラグインの用意してくれる Go SDK に入れるパスって何よ

OSX 10.9.3 で最新の brew で go を入れました。

検索すると比較的活発な intelliJ のプラグインがあったので入れました。

File > Project Structure から新しく SDK を設定せよというのでパスを入れるんですが「スカッ」って感じで何も起こりませんし追加もされませんしエラーも出ません。

悲しくなったので検索したら、 SDK として追加するのは /usr/local/Cellar/go/1.3/libexec でした。

あと GOROOT と GOPATH 入れろってこのプラグインうるさいんです。 .bash_profile とかで入れるといちいちターミナルから起動するのいやなので、 launchd さんが /etc/launchd.conf か $HOME/.launchd.conf を起動時にぶちかましてくれるということなので、

setenv GOROOT /usr/local/Cellar/go/1.3/libexec
setenv GOPATH /Users/oogatta/go

とさせていただきました。

$ launchctl < /etc/launchd.conf

これで即座に反映できますが、当然いまあがってるアプリは要再起動です。

1時間くらいかかってしまって、しょぼくれています。

Gradle の inputs.property にクロージャ渡したときと変数渡したとき

完全に体調を崩してしまいました。まいったなあ…。

ところで Gradle って名前は有名な割に、ブログ記事、 stackoverflow のポスト、少ない。つまり、わかってる人たちだけが使ってる感じなので、そうそうそれ俺も思ってた!みたいな疑問が表に出て来てない。予想以上に辛い。しかし更新検知というか更新判断?の仕組みは好きだし、 Groovy も好きなので便利です。

タスクへの入力定義のほうで、ファイルじゃ無くて値が使えるんですが、変数渡したり、クロージャ渡したり出来るみたいなんです。でも変数だと思ったように動かせなくて、下のような感じで書いて動かしてみたところ

task hoge {
  ext.myProp = 'original'
  doLast {
    myProp = 'rewrited'
  }
}

task fuga(dependsOn: hoge) {
  inputs.properties([
    'closure': {
      println '==inputs.properties closure evaluated.'
      hoge.myProp
    },
    'var': hoge.myProp
  ])
  outputs.file 'test'
  doFirst {
    println '[fuga.doFirst]'
    println fuga.inputs.getProperties()
  }
  doLast {
    println '[fuga.doLast]'
    println fuga.inputs.getProperties()
  }
}

println '[root]'
println fuga.inputs.getProperties()
~/tmp ❯❯❯ gradle fuga
[root]
==inputs.properties closure evaluated.
{var=original, closure=original}
:hoge
:fuga
==inputs.properties closure evaluated.
[fuga.doFirst]
==inputs.properties closure evaluated.
{var=original, closure=rewrited}
[fuga.doLast]
==inputs.properties closure evaluated.
{var=original, closure=rewrited}

BUILD SUCCESSFUL

Total time: 2.929 secs

こんな感じでした。 fuga.inputs.getProperties() を呼ぶたびにクロージャが実行されてることが分かりますが、変数はその時の値になるんじゃなくて、ずっと Configuration Phase で評価した値です。クロージャは Execution Phase 及び取りにいったときに毎回評価されている模様です。

Docker の import とか push みたいに結果がファイルに出ない場合( stdout をファイルに出しても良いけど…)、一つ前のタスクの結果を次のタスクの inputs.property として使いたい。という場合があって、

task importBuild(type: Exec, dependsOn: build) {
  inputs.file  'output/image.tar'
  outputs.upToDateWhen {
    ( importedRevision == revisions.imported )
  }
  commandLine  'sudo', 'bash', '-c', "cat output/image.tar | docker import - oogatta/hoge"

  ext.importedRevision = revisions.imported
  standardOutput = new ByteArrayOutputStream()

  doLast {
    importedRevision = standardOutput.toString().trim()

    ant.propertyfile(file: revisionsFile) {
      entry(key: 'imported', value: importedRevision)
    }
  }
}

task pushBuild(type: DockerPushImage, dependsOn: importBuild) {
  inputs.property 'importedRevision', { importBuild.importedRevision }
  outputs.upToDateWhen {
    def proc = ['sudo', 'bash', '-c', "docker history --no-trunc oogatta/hoge:latest | awk 'FNR>1{print \$1;}'"].execute()
    proc.waitFor()

    def imageId = proc.in.text.trim()

    ( imageId == importBuild.importedRevision )
  }
  imageId = "oogatta/hoge"
}

例えばこういう感じで、クロージャ助かりました。でもなんか、きっともっと良い方法があるに違いありません。なので、これはあくまで inputs.property についての話です。

Gradle は公式のドキュメントの向いてる方向が何やらエンタープライズ感があって、自分みたいな目的には、ツール自体は適応しているのだけど、周辺環境の意味で辛いです。今時のツールみたいにふわっとした感じでお願いしたい!。

gradle で Berkshelf こないだの続き

こないだの続きで最終的に Docker のイメージを作るんですけど、途中 Dockerfile じゃなくて Chef で作ってます。同じ構成コードで Docker のイメージ以外も作りたくて。というわけで Berkshelf です。

repositories {
  mavenCentral()
}

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'org.gradle.api.plugins:gradle-docker-plugin:0.2'
  }
}

apply plugin: 'docker'
apply from: '../berks.gradle'

import org.gradle.api.plugins.docker.tasks.*
import org.gradle.api.plugins.docker.tasks.container.*
import org.gradle.api.plugins.docker.tasks.image.*

docker {
  serverUrl = 'http://127.0.0.1:5555'
}

こないだのに apply from: '../berks.gradle' ってのを追加しました。他のビルドプロジェクトでも使いたい。検索したらあんま見当たらなかったので作ってますけどみんな一緒のことをしてるからうまいことプラグインして切り出したいです。まだ方法がわからない。

task berks_install(type: Exec, group: 'berks') {
  inputs.file  'Berksfile'
  outputs.file 'Berksfile.lock'
  commandLine  'berks', 'install'
}

task berks_update(type: Exec, group: 'berks') {
  commandLine 'berks', 'update'
}

task berks_vendor(type: Exec, group: 'berks', dependsOn: berks_install) {
  inputs.file berks_install.outputs.file
  outputs.dir 'berks-cookbooks'
  commandLine 'berks', 'vendor'
  doFirst {
    new File('berks-cookbooks').deleteDir()
  }
}
$ gradle berks_vendor
:berks_install UP-TO-DATE
:berks_vendor UP-TO-DATE

BUILD SUCCESSFUL

Total time: 2.606 secs

これで良い感じと思われます。 UP-TO-DATE って言われると気分が良いです。 berks_update はその必要があるときに直接実行…でいいのかな…。ただこれまでこれ毎回やってたので(毎回やらない運用だと、 cookbook の変更を取り込み忘れてその後のビルドにかかる20分くらいを無駄にする上にはまる)、一回一回のビルドずいぶん早くなりました。

Packer で Chef な Docker に Dockerfile 重ねていきたいので Gradle その1

なにしろいま表題のような感じでいろいろ試してるんです。そうしたら、本当に38.9℃の熱を出して2日間寝込んでしまいました。

寝込んでいる間 Kindle Paperwhite で Manning の『 Gradle in Action 』を途中まで読みました。この作者の Benjamin Muschko さん、 Gradleware 社の方なんですが、なんと Docker のプラグインを書いています。

検索すると出てくるもう1本は build に特化したもので、 Muschko さんのは、うたい文句通り動けば Docker API の gradle クライアントです。ちなみに、僕のところではうたい文句通り動いていない部分があるので Groovy 勉強してパッチ見てもらいたいと思います…。

というわけで入れてみました。 Gradle 初めてなので、おかしなことやってるかもしれません。

Gradle のインストール

バイナリを落としてきて、 /opt/gradle に置いて、 /opt/gradle/bin/gradle に /usr/bin/gradle から ln -s しました。手癖です。

Docker デーモンが tcp 聞くように

Muschko さんのプラグインは Java の Docker API クライアントを使ってるんですが、こいつがまた tcp でしか Docker デーモンと話さないので、 /etc/init.d/docker 開けて、立ち上げてるところで

nohup $exec -d -H tcp://127.0.0.1:5555 -H unix:///var/run/docker.sock $other_args &>> $logfile &

しました。

build.gradle

repositories {
  mavenCentral()
}

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'org.gradle.api.plugins:gradle-docker-plugin:0.2'
  }
}

apply plugin: 'docker'

import org.gradle.api.plugins.docker.tasks.*
import org.gradle.api.plugins.docker.tasks.container.*
import org.gradle.api.plugins.docker.tasks.image.*

docker {
  serverUrl = 'http://127.0.0.1:5555'
}

task info(type: DockerInfo)

次に build.gradle 書きました。一旦ちゃんと動くのか確かめたかったので info を試します。 root のスコープにも repositories 書いてるのは、 gradle-docker-plugin が依存している Docker API Java クライアントを落としてくる用。 serverUrl は http であっております。

$ gradle info
:info
Retrieving Docker info.
Debug                : false
Containers           : 0
Driver               : devicemapper
...

Docker Hub

次にプライベートなイメージを pull とか push とかしたいと思いましたがもう記憶に無いですがログインしろよとか出た気がします。 docker -H :5555 login します。 $HOME/.dockercfg が出来ます。ちなみに上の設定だと socket 経由なら sudo しますよね。もし万が一自分しかユーザのいないホストなら… sudo docker login もしておくとはまらないがちですが、やらないほうが良さそうです。

次に、ここがなんかアホな話なんですが、 Java クライアントが $HOME/.docker.io.properties を作れというので作ります。中身は Docker Hub のアカウント情報です。生パスワード様ご登場なので chmod 600 です。

docker.io.username=oogatta
docker.io.password=unko
docker.io.email=oogatta@gmail.com

これで理論上 pull と push ができますが、ちなみに、自分のところでは push は出来ても pull 失敗します。一見成功してるように見えるんですが、 /var/log/docker 見ると失敗してます。同じ状態で docker -H :5555 pull したら成功するので、 API 経由だと失敗するみたい。 Docker デーモンを -D で起動してログ見ると、全く同じリクエストを Docker Hub に飛ばして、 API からのだけこけているという。良い感じですね。

そもそも Docker Gradle プラグインが依存している Java クライアントのバージョンが古いので、つぶされてないバグがありそうです。この辺は気づいた人がつぶそうって感じでしょうか。ちなみに pull / push はそれぞれ

task pull(type: DockerPullImage) {
  imageId = 'oogatta/kidoibuki'
}

task push(type: DockerPushImage) {
  imageId = 'oogatta/kidoibuki'
}

こんな感じです簡単です。設定できる項目はプラグインのソース見るのが早そうです。

そうそうあと、最後になってしまったんですが、僕のところでは build も動かないんですよね…。これも一見通ってるんですが、 write tcp 172.31.2.33:46951: connection reset by peer みたいな感じです。なんか API クライアントのせいっぽいですね。

まあそんな感じでぐだぐだなんですが、 gradle の依存設定便利だし groovy 読みやすいし berkshelf なんかも一緒に出来たし得たこともありました。が、思ったより長くなってもう寝る時間なので続きは明日。

Dockerfile でどうやるのと思って調べた小さい話

このところ Docker やってます。前にやってたのが去年の10月でさすがに半年以上立ってるので進化していて Docker Hub ができてほんと助かります。 private な image の共有はサーバ自分で立てろ、とかだったので…。

Dockerfile 内で、 build を実行している環境の環境変数を参照したい

docker - Get environment variable value in Dockerfile - Stack Overflow

cat Dockerfile | envsubst | docker build -t my-target -

これ冴えてますよねえー。 envsubst yum も apt も gettext 入れると一発で入りました。ただ、標準入力から Dockerfile の内容を受け取る上の書式だと context 渡せないので、これで一回作って、さらに重ねる感じになりますかね…。

Dockerfile 内で、 build を実行している環境から SSH のエージェントフォワーディングをして RUN git pull とかしたい

コマンドの run ならできる。というか中に sshd 上がってるイメージに対して ssh -A すればいいんですけど、

How to SSH agent forward into a docker container

docker run --volume $SSH_AUTH_SOCK:/ssh-agent --env SSH_AUTH_SOCK=/ssh-agent ubuntu ssh-add -l

自分、 Docker コンテナの作成は今 EC2 上の Amazon Linux でやっているんですが、例えば build 中にコンテナ中で仕事の private リポジトリを git clone したいときに、鍵は手元の OSX にしかなくて、もちろんコンテナにも入れたくないし、という感じなのですよね…。 Dockerfile の VOLUME はホストとコンテナの共有には対応していないので、何かいい方法があるに違いない。

packer の既存 VM から box を作り直すやつは罠が多い

virtualbox-ovf

Ubuntu precise では、おそらく

Waiting for network configuration

が出てネットワークインターフェースを見失う。

sudo rm /etc/udev/rules.d/70-persistent-net.rules

消して、シャットダウンし、また起動すると再生成されてしまうので起動しないまま export して ova を作り、それを元に virtualbox-ovf builder を動かすと順調に遷移した。 packer 側が行っていると推測する再生成に、 OS 側がついていけてないということなのかな。いやいやちがった、 mac address 再生成しているのは virtualbox の clone 処理だと言うことです。

vmware-vmx

packer が使っている既存 vmx の clone コマンドが VMware Fusion 6 にはない。 VMware Fusion 6 Professional にはある。

patch 書いて送るか、あきらめるか、 Pro を買う。