JavaScriptをがんばるブログ

React,OSS,ソフトウェア開発が中心のブログです👨‍💻

Packer + Docker + Ansibleでハマる

Packer + Ansible + Dockerの組み合わせは茨の道

Scalaが動くDockerコンテナを作りたくてトライしてみました。
rebuild.fm 167でもDockerfileを使いたくないという話がポロっと出ていましたが、

rebuild.fm

私もインフラのセットアップは愛用しているAnsibleに統一したく、Dockerfileをあまり使いたくなかったのでPackerを使ってみました。
(後で書きますがDockerfileでもAnsibleは使用出来ます。この時はまだ知らなかった...)

PackerはHashiCorp製のプロビジョナと出力されるビルドイメージを自由に組み合わせられるツールとして有名ですよね。

ぶっちゃけDocker For Mac入れてPackerのjsonファイルにbuilders: dockerみたいな事書いてplaybookのパス指定すれば楽勝だと思っていたんですよ。
(そもそもPackerってそのためのツールですよねw)

個人的にPackerを利用するモチベーションとして、

  • VagrantBox、AMI、Docker等ビルド対象固有の技術的マネジメントをせずとも、プロビジョナの記述に注力していれば目的のイメージをうまいことビルドしてくれる。
  • ビルド対象によって構成管理の形式やツール(プロビジョナ側)が左右されない。

という利点を期待していたのですが、
実際にはエラーに躓いた場合、そのツールの知識を用いて問題を解決して行かなければならず、
最低限のplaybookを流すだけでも苦労したので備忘録として残しておきます。
(長々とログ出力を貼っており全体としてかなり読みにくくなっております。)

環境

  • Docker for Mac 1.12.3
  • Packer 0.10.0
  • Ansible 2.0.1.0

前提

  • packer buildコマンドでのdockerコンテナイメージ作成で躓いている
  • packerの設定ファイルはpacker-scala.json

DockerコンテナへのSSH Connectionが上手くいかない

AnsibleのPlaybook実行時、Dockerホスト → DockerコンテナへのSSHが失敗するようです。

{
    "variables": {
        "ansible_host": "default"
    },
    "builders": [{
        "type": "docker",
        "image": "ubuntu",
        "commit": "true"
    }],
    "provisioners": [
    {
        "type": "ansible",
        "playbook_file": "playbook.yml"
    }]
}
  • packer build packer-scala.jsonのコンソール出力
ryota.murakami@murakami ~/r/a/otsukimi> packer build packer-scala.json
docker output will be in this color.

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
    docker: Using default tag: latest
    docker: latest: Pulling from library/ubuntu
    docker: Digest: sha256:7a64bc9c8843b0a8c8b8a7e4715b7615e4e1b0d8ca3c7e7a76ec8250899c397a
    docker: Status: Image is up to date for ubuntu:latest
==> docker: Starting docker container...
    docker: Run command: docker run -v /Users/ryota.murakami/.packer.d/tmp/packer-docker968054136:/packer-files -d -i -t ubuntu /bin/bash
    docker: Container ID: ef33e308d6bfde09f42c42665f0d1d4fd816e8fcd5d56866f73a5c0b4a608a2d
==> docker: Provisioning with Ansible...
==> docker: SSH proxy: serving on 127.0.0.1:64026
==> docker: Executing Ansible: ansible-playbook /Users/ryota.murakami/repository/ansible/otsukimi/playbook.yml -i /var/folders/b0/_7l4w13s7vl3vl9h107skyvh0000gp/T/packer-provisioner-ansible048561848 --private-key /var/folders/b0/_7l4w13s7vl3vl9h107skyvh0000gp/T/ansible-key800601981
    docker:
    docker: PLAY ***************************************************************************
    docker:
    docker: TASK [setup] *******************************************************************
    docker: SSH proxy: accepted connection
==> docker: authentication attempt from 127.0.0.1:64027 to 127.0.0.1:64026 as ryota.murakami using none
==> docker: unauthorized key
==> docker: authentication attempt from 127.0.0.1:64027 to 127.0.0.1:64026 as ryota.murakami using publickey
==> docker: unauthorized key
==> docker: authentication attempt from 127.0.0.1:64027 to 127.0.0.1:64026 as ryota.murakami using publickey
==> docker: authentication attempt from 127.0.0.1:64027 to 127.0.0.1:64026 as ryota.murakami using publickey
==> docker: starting sftp subsystem
    docker: fatal: [default]: UNREACHABLE! => {"changed": false, "msg": "SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh", "unreachable": true}
    docker:     to retry, use: --limit @/Users/ryota.murakami/repository/ansible/otsukimi/playbook.retry
    docker:
    docker: PLAY RECAP *********************************************************************
    docker: default                    : ok=0    changed=0    unreachable=1    failed=0
    docker:
==> docker: shutting down the SSH proxy
==> docker: Killing the container: ef33e308d6bfde09f42c42665f0d1d4fd816e8fcd5d56866f73a5c0b4a608a2d
Build 'docker' errored: Error executing Ansible: Non-zero exit status: exit status 3

==> Some builds didn't complete successfully and had errors:
--> docker: Error executing Ansible: Non-zero exit status: exit status 3

==> Builds finished but no artifacts were created.

unauthorized keySSH Error: data could not be sent to the remote host.といったフレーズでSSH Connectionが上手く行っていない事が分かります。
ログをより詳細に出力する-vvvvオプションをAnsibleに与えて再度実行してみましょう。

  • packer-scala.json(extra_argumentsを追記)
{
    "variables": {
        "ansible_host": "default"
    },
    "builders": [{
        "type": "docker",
        "image": "ubuntu",
        "commit": "true"
    }],
    "provisioners": [
    {
        "type": "ansible",
        "playbook_file": "playbook.yml",
        "extra_arguments" : "-vvvv"
    }]
}

packer build packer-scala.jsonを実行すると-vvvvにより新たに以下のメッセージが出力されました。
実行されたsshコマンドの詳細です。(横長)

docker: <127.0.0.1> SSH: EXEC ssh -C -vvv -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o Port=65453 -o 'IdentityFile="/var/folders/b0/_7l4w13s7vl3vl9h107skyvh0000gp/T/ansible-key210798578"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ryota.murakami -o ConnectTimeout=10 -o ControlPath=/Users/ryota.murakami/.ansible/cp/ansible-ssh-%h-%p-%r -tt 127.0.0.1 '/bin/sh -c '"'"'( umask 22 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1482417280.49-114181904662247 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1482417280.49-114181904662247 `" )'"'"''

使用されていたsshコマンドのオプションは以下です。

オプション 効果
-C データを圧縮して通信
-vvv 冗長表示(verbose)
-o ssh_configで設定可能なオプションを指定できる
-o ControlMaster=auto Master接続が存在すれば使用、なければ作成
-o ControlPersist=60s Master接続の切断後の保持秒数
-o StrictHostKeyChecking=no 使用した鍵が~/.ssh/known_hostsに自動登録される
-o Port=65453 使用するポート
-o 'IdentityFile="filepath"' 使用する鍵ファイル
-o KbdInteractiveAuthentication=no キーボード対話的認証を使用しない
-o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey 認証方法の優先順位を定義
-o PasswordAuthentication=no パスワード認証を禁止
-o User=ryota.murakami ログインユーザー
-o ConnectTimeout=10 ssh接続時のタイムアウト秒数
-o ControlPath=filepath ssh接続のセッション制御用ソケットのファイルパス

これを見る限りsshコマンドには特に問題がある訳ではなさそうです。

Dockerコンテナでsshdが起動していない

コンソール出力のdocker runコマンドを見れば直ぐに分かった話なのですがDockerコンテナでsshdが起動していないのでssh接続出来るはずがありません。

docker: Run command: docker run -v /Users/ryota.murakami/.packer.d/tmp/packer-docker685009806:/packer-files -d -i -t ubuntu /bin/bash

重要なところは/bin/bashの部分で、docker runコマンドで立ち上がるコンテナはここで指定した1つのプロセスのみ動いている状態となります。
コンテナの中でtopコマンドを叩いた結果がこちらです。

ご覧のようにbashしか起動していない事が分かります。

top - 08:39:38 up 3 days, 21:02,  0 users,  load average: 0.00, 0.00, 0.00
Tasks:   2 total,   1 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni, 99.3 id,  0.7 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  2045844 total,  1514688 free,    77800 used,   453356 buff/cache
KiB Swap:   987960 total,   987960 free,        0 used.  1664680 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                           
    1 root      20   0   18240   3240   2760 S   0.0  0.2   0:00.07 bash                                              
    9 root      20   0   36692   3120   2640 R   0.0  0.2   0:00.01 top              

公式ドキュメントではbuildersにAmazon AMIを用いたサンプルのみが掲載されていますが、
Amazon AMIの部分をdockerに変更しただけでは動かない仕組みのようです。

不親切な事に公式ドキュメントにはこの点についての説明が一切ありません。

www.packer.io

AnsibleをDockerに適用するにはdocker-connection-driverが必要

この件についてググったところ以下のスレッドにたどり着きました。

Ansible remote provisioner error when used with Docker builder · Issue #3260 · mitchellh/packer · GitHub

コメントを読んでいくとdockerコンテナに対してansibleを実行する場合はdocker-connection-driverが必要であるというヒントが。

http://blog.oddbit.com/2015/10/13/ansible-20-the-docker-connection-driver/

なるほど、Ansible設定の問題だったわけですね。
Packerでエラーが出た、という捉え方をしていたのでこの情報にたどり着くまでに時間が掛かりました。

playbookにconnection: dockerの設定を追加して再度packer build packer-scala.jsonコマンドを実行してみます。

  • playbook.yml
- hosts: all
  connection: docker
  roles:
    - { role: scala }

dockerコンテナにsshコマンドを実行する事は無くなりましたが、ディレクトリ作成時にパーミッションエラーが発生しました。

  • コンソール出力
docker: fatal: [default]: UNREACHABLE! => {"changed": false, "msg": "Authentication or permission failure. In some cases, you may have been able to authenticate and did not have permissions on the remote directory. Consider changing the remote temp path in ansible.cfg to a path rooted in \"/tmp\". Failed command was: ( umask 22 && mkdir -p \"` echo $HOME/.ansible/tmp/ansible-tmp-1482486284.49-101790217276259 `\" && echo \"` echo $HOME/.ansible/tmp/ansible-tmp-1482486284.49-101790217276259 `\" ), exited with result 1", "unreachable": true}

docker execコマンドがrootで実行されていないからでしょうか...
良く分からないので上に貼ったgithub issueのコメントに投下されていたサンプル通りにファイルを編集してみました。(最初からやっとけ感)

{
  "variables": {
    "ansible_host": "default",
    "ansible_connection": "docker"
  },
  "builders":[
    {
      "type": "docker",
      "image": "centos:7",
      "commit": true,
      "run_command": [ "-d", "-i", "-t", "--name", "{{user `ansible_host`}}", "{{.Image}}", "/bin/bash" ]
    }
  ],
  "provisioners": [
    {
      "type": "ansible",
      "groups": [ "webserver" ],
      "playbook_file": "./webserver.yml",
      "extra_arguments": [
        "--extra-vars",
        "ansible_host={{user `ansible_host`}} ansible_connection={{user `ansible_connection`}}"
      ]
    }
  ]
}

packer build packer-scala.jsonを実行するとumask 22~の下りは無事成功したようです。
変更前とは--nameオプションが追加されただけに見えますが、それでパーミッション関連のエラーが解決する理由は分からんですね。

ubuntuのDockerコンテナにpythonが入っていない

ご存知の通りansibleはリモートホストpythonを使用してタスクを実行するのでpythonが必要です。
ubuntuは最初から/usr/bin/pythonpythonが入っているはずなのですが、packerがfetchしてきたubuntuのdockerコンテナには入っていないようです。

  • コンソール出力
docker: fatal: [default]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_name": "setup"}, "module_stderr": "/bin/sh: 1: /usr/bin/python: not found\n", "module_stdout": "", "msg": "MODULE FAILURE", "parsed": false}

"type": "shell"プロビジョナでpythonをインストールします。

{
    "variables": {
        "ansible_host": "default",
        "ansible_connection": "docker"
    },
    "builders": [{
        "type": "docker",
        "image": "ubuntu",
        "commit": "true",
        "run_command": [ "-d", "-i", "-t", "--name", "{{user `ansible_host`}}", "{{.Image}}", "/bin/bash" ]
    }],
    "provisioners": [{
        "type": "shell",
        "inline": [
            "apt-get -y update",
            "apt-get -y install python-dev"
        ]},
    {
        "type": "ansible",
        "groups": ["scala"],
        "playbook_file": "playbook.yml",
        "extra_arguments" : [
            "-vvv",
            "--extra-vars",
            "ansible_host={{user `ansible_host`}} ansible_connection={{user `ansible_connection`}}"
        ]
    }]
}

もう一度packer build packer-scala.jsonを実行...

やっとこさ成功

だいぶ苦戦しましたがdockerにplaybookを流す事が出来ました!
(1個failedしているのはjdkが入っていなくsbtインストールtaskがコケている様子です)

docker: PLAY RECAP *********************************************************************
docker: default                    : ok=12   changed=9    unreachable=0    failed=1

今更ですがDockerfileでもAnsibleを使えるみたいです

Dockerfileを使うとAnsibleを使えないと思い込んでいたのですが、
RUNでansible-playbookコマンドを実行すれば普通に使えるんですね...

github.com

最初にこちらを選択していればここまでハマらなかったかもしれませんね。

まとめ

Ansilbeプロビジョナの公式ドキュメントを見てAmazon AMIをdockerに替えただけだと同じような現象でハマる方が多いのでは、と思い書きました。

高レベルなインフラツールを使ってもエラーにぶち当たった時は結局Linux周りの自力と英語力が要求されるという事を実感しました。