まちゅダイアリー


2015-05-20 (水)

さくらVPSでdocker環境 (2) 遠隔操作の落とし穴

さくらVPSサーバにCoreOSをインストールできたので、いよいよ docker を使ってコンテナを立てていく。

dockerをどこから操作するか

コンテナ上でアプリを動かすためには、以下のステップを経る。

  1. GitHubなどからアプリのソースコードとDockerfileを入手する (git clone)
  2. ソースコードからdockerイメージを作る (docker build)
  3. dockerイメージを実行する (docker run)

このうち、1と2のdockerイメージを作るところはDockerHubやQuay.ioなどのリポジトリサービスを使って自動化できる。とはいえ、自動化するためのDockerfileを作るまでは、何度もDockerfileを編集してbuildのサイクルを繰り返すことになる。

普通にやると、さくらVPSサーバにsshでログインし、ターミナル上でDockerfileを編集したりdockerコマンドを実行したりすることになる。でも、さくらVPSサーバにインストールしたCoreOSは軽量OSなので、ホスト側には最小限のパッケージしか入っていない。やっぱり慣れた環境から操作したいよね、ということでさくらVPSサーバ上のdockerを手元のMacから操作したくなる。幸い、dockerには遠隔から操作するためのリモートAPIが用意されているので、これを使えるようにする。ざっくり言うと、herokuコマンドを実行するようにdockerコマンドを実行したいということ。

落とし穴その1: 認証

ざっとググってみると、以下のような情報が見つかる。

まずはCoreOSの公式ドキュメントを参考に、 /etc/systemd/system/docker-tcp.socket を作成する。新たにTCP 2375ポートでサービスを待ち受けるための設定となる。

[Unit]
Description=Docker Socket for the API

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

[Install]
WantedBy=sockets.target

これはdockerがポート2375を直接bindするのではなく、systemdのsocket activationという仕組みを使っている。xinetdのようなもので、systemdを経由してdockerを呼び出している。そのため、systemctlコマンドを使ってこの設定を有効にする。

# systemctl enable docker-tcp.socket
# systemctl stop docker
# systemctl start docker-tcp.socket
# systemctl start docker

さくらVPSサーバ上からtcp経由でdockerに接続すると、ちゃんと応答が返ってくる。

$ docker -H 127.0.0.1:2375 ps -q
c1c5882fd47b
f0aa9b77616e
8a7cfde533d5

次に手元のMac環境からさくらVPS上のdockerに接続すると、同じように応答が返ってくる。

$ docker -H 160.16.81.168:2375 ps -q
c1c5882fd47b
f0aa9b77616e
8a7cfde533d5

これでめでたしめでたし、とはならない。dockerコマンドが実行できるということは、任意のイメージを作ったり、そのイメージを動かしたりできるということ。つまり、root権限をフリーで与えていることと同じになる。僕以外の誰かが勝手にイメージを立てて、Bitcoinの採掘やサーバの踏み台に使われかねないということだ。

dockerデーモンを実行するときにも、ちゃんとTLSを使えと警告がでる。

# docker -d -H tcp://0.0.0.0:2376
INFO[0000] +job init_networkdriver()
INFO[0000] +job serveapi(tcp://0.0.0.0:2376)
INFO[0000] Listening for HTTP on tcp (0.0.0.0:2376)
INFO[0000] /!\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\
INFO[0000] -job init_networkdriver() = OK (0)

Docker command line - Docker Documentationにも、認証しないことの危険性について書かれている。

If you need to access the Docker daemon remotely, you need to enable the tcp Socket. Beware that the default setup provides un-encrypted and un-authenticated direct access to the Docker daemon - and should be secured either using the built in HTTPS encrypted socket, or by putting a secure web proxy in front of it.

ということで、ローカルの仮想化環境や、信頼できるネットワーク環境で動かすのでない限り、dockerを遠隔操作するときはTLSでクライアント認証は必須となる。ちなみに、boot2dockerはローカル環境にも関わらず、ちゃんとTLS認証を使うように設定されている。えらい。

$ boot2docker shellinit
Writing /Users/machu/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/machu/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/machu/.boot2docker/certs/boot2docker-vm/key.pem
    export DOCKER_HOST=tcp://192.168.59.103:2376
    export DOCKER_CERT_PATH=/Users/machu/.boot2docker/certs/boot2docker-vm
    export DOCKER_TLS_VERIFY=1

落とし穴その2: systemdのsocket activation経由ではTLSが使えない

TLSクライアント認証の手順は、やっぱりdockerの公式ドキュメントが参考になる。

Running Docker with HTTPS - Docker Documentation

やっていることはシンプル。

  1. オレオレ認証局を作る
  2. サーバ証明書を発行する
  3. クライアント証明書を発行する

もちろん、サーバ証明書はサーバ側に、クライアント証明書はクライアント側に配置する。オレオレ認証局だけど、自分しか使わないので問題ない。

TLSを使う設定は、コマンドラインからだとこう。 --tldverify などのオプションをつけている。

$ docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \
  -H=0.0.0.0:2376

CoreOSなので、まずは2376ポートを待ち受けるように先ほど作った docker-tcp.socket ファイルを修正する。ListenStreamを2375から2376へ書き換えた。

[Unit]
Description=Docker Socket for the API

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

[Install]
WantedBy=sockets.target

docker起動時に --tlsverify などをオプションで渡せるようにするために、 /etc/systemd/system/docker.service.d/tls.conf ファイルを新たに作る。

[Service]
Environment='DOCKER_OPTS=--tlsverify --tlscacert=/home/core/tls/ca.pem --tlscert=/home/core/tls/server-cert.pem --tlskey=/home/core/tls/server-key.pem'

さっきと同じように設定ファイルの再読み込みとdockerサービスの再起動を実行。ちゃんとTLSオプション付きでdockerが実行されている。

$ ps aux | grep docker
root     29146  0.3  0.9 205164 18628 ?        Ssl  00:16   0:00 docker --daemon --host=fd:// --tlsverify --tlscacert=/home/core/tls/ca.pem --tlscert=/home/core/tls/server-cert.pem --tlskey=/home/core/tls/server-key.pem

ところが、TLSを有効にしているはずなのに、クライアントからは接続できない。tls: oversized record received というエラーメッセージが出ている。

$ env | grep DOCKER
DOCKER_HOST=tcp://160.16.81.168:2376
DOCKER_TLS_VERIFY=1
$ docker ps -a
FATA[0001] An error occurred trying to connect: Get https://160.16.81.168:2376/v1.18/containers/json?all=1: tls: oversized record received with length 20527

しかもTLSを使わないと、接続できてしまう。

$ unset DOCKER_TLS_VERIFY
$ env | grep DOCKER
DOCKER_HOST=tcp://160.16.81.168:2376
$ docker ps -q
c1c5882fd47b
f0aa9b77616e
8a7cfde533d5

オプションの指定が間違っているのだろうか、とここでかなり悩んだ。

問題の切り分け

boot2docker環境を参考にして、systemdを使わずにdockerを起動した場合と比べてみる。

  • systemdのsocket activationを使った場合 (--host=fd://)
    • TLSを使わない … 接続できる
    • TLSを使う … 接続できない
  • dockerデーモンを直接起動した場合 (--host=tcp://0.0.0.0:2376)
    • TLSを使わない … 接続できる
    • TLSを使う … 接続できる

どうも、systemdのsocket activation経由の場合だけ、TLSが有効にならないようだ。

socket activation経由ではTLSが有効にならない理由

試行錯誤した挙句、ふと気がついた。 systemdを経由してdockerを起動する場合、docker側は fd:// というプロトコルで待ち受けている。ローカル接続でのUNIX domain socketや遠隔接続でのTCPソケットはsystemd側が受け付けてdockerに渡す仕組み。図にするとこうなる。

                                             systemd
                                         +-------------------+
docker client <- (unix domain socket) -> | docker.socket     |
                                         |                   | <- (fd://) -> docker server
docker client <- (tcp port 4376)      -> | docket-tcp.socket |
                                         +-------------------+

一方、systemdを経由せずにdockerを起動する場合は、こうなる。

                                             docker server
                                         +---------------------------+
docker client <- (unix domain socket) -> | --host=fd://              |
                                         |                           |
docker client <- (tcp port 4376)      -> | --host=tcp://0.0.0.0:2376 |
                                         +---------------------------+

TLSが有効になるのは、遠隔接続するTCPソケットの場合のみ。UNIXドメインソケットを使うローカル接続ではTLSは使わない。dockerデーモンになったつもりで考えてみると、systemdから fd:// 経由で接続を受け取ると、その接続がUNIXドメインソケットからきたのか、それともTCPソケットから来たのか分からないのではないか。分からないから、TLSが有効にならないのではないか。

dockerのソースコード (server_linux.go)を読むと、プロトコルごとに分岐していて、tcpソケットの場合のみtlsverifyの警告をだしている。

    switch proto {
    case "fd":
        ls, err := systemd.ListenFD(addr)
        // 中略
    case "tcp":
        if !s.cfg.TlsVerify {
            logrus.Warn("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
        }
        if l, err = NewTcpSocket(addr, tlsConfigFromServerConfig(s.cfg), s.start); err != nil {
            return nil, err
        }
        // 中略
    case "unix":
        if l, err = NewUnixSocket(addr, s.cfg.SocketGroup, s.start); err != nil {
            return nil, err
        }
    default:
        return nil, fmt.Errorf("Invalid protocol format: %q", proto)
    }
CoreOS上でdockerを遠隔操作するための設定

話が長くなった。systemdを経由するとTLSが使えないのが分かったので、TCPソケットはdockerが直接待ち受けるようにすればいい。

まず、 docker-tcp.socket は無効化し、ファイルも削除する。

# docker disable docker-tcp.socket
# rm /etc/systemd/system/docker-tcp.socket

次に、 /etc/systemd/system/docker.service.d/tls.conf を編集し、起動オプションに --host=tcp://0.0.0.0:2376 を追加する。

[Service]
Environment='DOCKER_OPTS=--host=tcp://0.0.0.0:2376 --tlsverify --tlscacert=/home/core/tls/ca.pem --tlscert=/home/core/tls/server-cert.pem --tlskey=/home/core/tls/server-key.pem'

dockerデーモンを再起動して、TCPソケットでの待ち受けが有効になっている (--host=tcp://) ことを確認する。

# systemctl daemon-reload
# systemctl stop docker
# systemctl start docker
# systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib64/systemd/system/docker.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/docker.service.d
           └─tls.conf
   Active: active (running) since Mon 2015-06-08 09:13:50 JST; 1min 44s ago
     Docs: http://docs.docker.com
 Main PID: 14729 (docker)
   Memory: 17.1M
   CGroup: /system.slice/docker.service
           ├─14729 docker --daemon --host=fd:// --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert=/home/core/tls/ca.pem ...
           └─14768 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80

これで手元のMac環境からもTLS接続でさくらVPSサーバ上のdockerに接続できるようになった。もちろん、クライアント証明書がない環境からは繋がらない。

$ env | grep DOCKER
DOCKER_HOST=tcp://160.16.81.168:2376
DOCKER_TLS_VERIFY=1
$ docker ps -q
c1c5882fd47b
f0aa9b77616e
8a7cfde533d5

dockerデーモンは常時起動しているので、systemdを経由して起動するメリットはあまりない…はず。

まとめ

長くなったけど、CoreOS上でdockerを遠隔操作する場合は、以下2点に気をつけなければいけない。

  1. 必ずTLSを有効にすること (CoreOSサイトのサンプルは参考にしない)
  2. TCP接続はsystemdを経由せず、docker側で待ち受ける

つまり、以下の内容の /etc/systemd/system/docker.service.d/tls.conf を作ればいい。まとめちゃうと簡単だね。

[Service]
Environment='DOCKER_OPTS=--host=tcp://0.0.0.0:2376 --tlsverify --tlscacert=/home/core/tls/ca.pem --tlscert=/home/core/tls/server-cert.pem --tlskey=/home/core/tls/server-key.pem'


2015-05-16 (土)

さくらVPSでdocker環境 (1) CoreOSをインストール

前の日記に書いたとおり、引越し後のサーバはdocker環境で動かしたい。boot2dockerを使って docker にも慣れてきたので、引越し先の環境を準備する。

引越し先を選ぶ (VPS vs クラウドIaaS)

まずはサーバをどこで動かすか。このままさくらのVPSを使い続けてもいいし、AWSやDigitalOceanなどのクラウドIaaSを使う選択肢もある。ここは、趣味開発のためのVPS/クラウド活用術 // Speaker Deckが参考になった。

サーバ環境を頻繁に変更するなら、セットアップが楽で従量課金のAWSやDigitalOceanを使うのが良さそう。一方で、常に動かしておくなら、さくらのVPSがコストパフォーマンスが良い(今は円安なので特に価格差ある)。今回はdocker環境を作ろうとしているので、さくらVPS + dockerで複数コンテナを動かすのがいいと判断した。ということで、さくらVPSの2GB + SSD 50GBのプランを選択した。コストは17,380円/年(税抜)。

OSを選ぶ (定番OS vs 軽量OS)

環境が決まったので、次はOSを選ぶ。これまでは Ubuntu 14.04 LTS を使っていた。選択肢としては Ubuntu や CentOS のような定番OSを使うか、 CoreOS のような軽量OSを使うか。

  • VPS + コンテナ環境で動かすのでメモリ貴重
  • 個人サーバなので運用を楽にしたい
  • 個人サーバーなので新しいモノを試してみたい

という理由から、軽量OSを使ってみることにする。最初はUbuntuつながりでSnappy Ubuntuを試してみた。ところが、思った以上に自由度がなく、これはどうも苦労しそう。定番のCoreOSは分散環境を前提としているのが気になったけど、etcd や fleet などの分散技術を使わずにシングルホストで運用してみることにした。

軽量OSの選定では、Docker向けの軽量Linux OS 主要3種を比較する | Think IT(シンクイット)が参考になった。

さくらのVPSにCoreOSをインストール

さくらのVPSにCoreOSをするためには、2つ気をつけなければいけないポイントがある。

そこで、インストール手順はこうなる。

  1. ISOイメージインストール機能を使ってCoreOSのインストールイメージをアップデート
  2. インストーラを起動し、VNCコンソール経由で接続(ネットワーク設定が終わっていないのでsshでは繋がらない)
  3. インストール設定を記述したcloud-configファイルを作成し、CoreOSをインストールする

ところが、VNCコンソールではキーボードが英字配列だったり、コピペができなかったりと、cloud-configファイルを作るのが大変。そこで、2のインストーラを起動した時点でVNCコンソールからネットワーク設定を手動で追加する。coreアカウントにパスワードを設定して、一時的にパスワードでログインできるようにしている。

$ sudo ifconfig eth0 160.16.81.168 netmask 255.255.254.0
$ sudo route add default gw 160.16.80.1
$ sudo vim /etc/resolv.conf
$ sudo passwd core

サーバへ繋がるようになったら、ローカル環境で作ったcloud-configファイルをサーバ側へアップロードし、インストーラを起動する。以下のコマンドは手元のMacBook Proから実行している。

$ scp cloud-config.yml core@160.16.81.168:
$ ssh core@160.16.81.168
$ sudo coreos-install -d /dev/vda -c cloud-config.yml -C Stable

cloud-configファイルの中身はこれ。static.networkというネットワーク設定が書かれたunitsの作成と、ssh接続のための公開鍵の登録を指示している。

#cloud-config

coreos:
  units:
    - name: static.network
      content: |
        [Match]
        Name=eth0

        [Network]
        Address=160.16.81.168/23
        Gateway=160.16.80.1
        DNS=210.188.224.10
        DNS=210.188.224.11

ssh_authorized_keys:
  - ssh-dss AAAAB3NzaC1kc3MAAACBAJdjSrRt+g7qk6McucfUo9IYGkQ1eMnli5qyT3GjYwxw1DxL8MrbgDtjbL5PLSa0n+c9IAiWbnh3hsRpQd7xU6mqAFhjfTzNS5tFF/dq4o6Pz1OxiBoqwq0UJL4qzgAvjMzTflNCtwml3X4ObJMgRWRuARa1F2+ukRJapBKjOR3pAAAAFQCp99SugvMxgXo4w9QlXCboBUPqcwAAAIAPbmr1TLsyaicjEAE18vYrytxJ+GQjztv3LDXBIdb6CZMO1o8UyuiodXfT3kGk6m84gPwJzf/dQdFBsrfI3fhRxL5cQ46c47jIbtp3YBUyzhaTmiSOX+kh05sXeJPdCapjmMnWJxZAbHkPvkM0gaNt2W1HG6qIYf76mFl5qHZ8EwAAAIBZpbYWbSUa0RM3Tic9YrzF2ANK7KeYjZpkPgZG7SNEyojiFWUQxJCipRq51rLPtuCN2m8zHt+0QaHBv+OldevIRIEAc1eARM3gqlzBGFEyY1qFnzbtkNHCyQWlYP3crsv0ZTa2FWPmgzP/K5+0moTZ+XzK3lz9JGEv8yAWHgM6BA== kohei@machu.jp

ちなみにunitsはsystemdの機能の一部。ここの設定は、インストール後には /etc/systemd/network/static.network となる。この辺の設定は、CoreOSとcloud-initとsystemdの仕組みが混在していて、慣れるまでは混乱しちゃう。

インストール処理が完了したらサーバを再起動。再起動後にちゃんとsshできればインストール成功。

さくらVPSへのインストール手順は、以下のサイトが参考になった。

タイムゾーンの設定

CoreOSのタイムゾーンはUTCに設定されている。タイムゾーンを変えるのは推奨されていない(複数ホストで動かした時にタイムゾーンが違うとトラブルの元になるらしい)ようだけど、変えるのであれば以下のコマンドを実行する。

$ sudo timedatectl set-timezone Asia/Tokyo
$ date
Sat May 16 23:04:17 JST 2015

更新チャネルの変更

CoreOSのStable版をインストールしたら、dockerのバージョンが古かったのでα版を使うように変更した。更新チャネルの変更は、以下のサイトの手順を参考にした。

使ってみる

CoreOSはsystemdを採用している。サービスの起動やログの表示などの方法がだいぶ違うので、まずはsystemdの使い方に慣れるのが良い。サービスの起動・停止は systemctl コマンドを使い、ログの表示は journalctl コマンドを使う。ディストリビューション違うけど、Red Hat Enterprise Linux 7 の systemd コマンドチートシート - Red Hat Customer Portalを印刷しておくといいんじゃないかな。

基本的な使い方に慣れたら、次はいよいよdockerを使ってサービスを動かしていく。


2015-05-08 (金)

一眼レフの写真をLightroomとPhotosでいい感じに管理

一眼レフで撮った写真はこれまでWindows8 + Picasaで管理していた。でも、Windowsをほとんど使わなくなったので、写真の管理もMacに移行したい。1年くらい試行錯誤して、ようやくいい感じに管理できるようになった。

基本的な仕組みはこう。写真はSDカード経由でLightroomに読み込んで管理し、選別した写真をPhotos *1 に書き出す。PhotosからはiCloudフォトライブラリを経由して、iPhoneやiPadで写真を見られるようにした。

Managing photos with LightRoom and Photos.app

  1. SDカードをMacBook Proのスロットに挿すと、Lightroomが起動して写真を読み込み
  2. Lightroom上で写真を選別(xボタンで不要な写真に除外フラグをつけて一括で削除)
  3. 残った写真を選択してフォルダに書き出し、Photosへインポート
  4. Photosへインポートされた写真は自動的にiCloudフォトライブラリへアップロードされる
  5. iPhoneやiPadの写真アプリで一眼レフの写真が見られる!
  6. AirPlayを使ってAppleTV経由でテレビでも一眼レフの写真が見られる!

iPhoneユーザーなのでPhotosとiCloudフォトライブラリを使っているけど、4以降はGoogleフォトでもFlickrでも応用が効くはず。また、ここでのポイントは3番の書き出しのところ。LightroomはFlickrやFacebookへの書き出しプラグインはあるけど、Photosへの書き出しプラグインはない。そこで、Adobe LightroomからiPhotoへワンクリックで出力して共有する方法を参考に、Photosへもワンクリックで出力できるようにした。その設定がこちら。

Lightroom photos export settings

  1. 書き出し場所は適当なフォルダでOK。
  2. 写真をフルサイズで書きだすとあっという間にiCloudの容量 (5GB) がいっぱいになるので、見たい端末の解像度に合わせて写真を縮小する。僕はiPad Retinaを使うので2048ピクセルにした。これならファイルサイズは大きくても1MB/枚なので、数千枚まではiCloudに保存できる。いっぱいになったら20GBの有料プランにするか、古い写真を削除するか、その時に考える。
  3. 必要に応じて写真から位置情報を削除する。外部に公開せずに自分と家族だけで使うならそのままでも良い。
  4. 書きだしたあとに自動的にPhotosアプリを起動する設定。

あとはこの設定を「Photosへの書き出し」などの名前でプリセット登録しておけばよい。書き出しが終わるとPhotosが起動して、いま書きだした写真がそのまま読み込めるようになっている。

写真の自動読み込み

これでMacでの写真管理のワークフローはひと通り整ったよ。普段なかなかPCに向かう時間が取れないので、こうやってスマホやタブレット、それにテレビで家族と一緒に写真が見られるのはとても満足度が高い。このためにずっとiOS7だったiPad(第3世代)もiOS8にアップデートしちゃったよ。

iCloudフォトライブラリの代わりにFlickrを使う場合は、手順3で書きだしたフォルダをFlickr Uploaderの監視対象に指定すれば良い。Flickrは無料ユーザでも1TBまで使えるし、フォルダ内のファイルは自動でアップロードしてくれるので便利。ただ、iPhone用のアプリが日本のApp Storeで公開されていないのが欠点。

Lightroomは6が発売された。僕は5のパッケージ版を使っているけど、本格的に使うなら定額制のCC版を使うのもいいかも。

Adobe Photoshop Lightroom 6 日本語版 Windows/Macintosh版

アドビシステムズ
¥ 14,472

参考

このワークフローに至るまでの経緯(自分用メモ)

  • Eye-fiでの自動アップデートは使わなくなった。ただのSDカードとして使っている。動画の同期に時間がかかるのと、Picasaよりもフォルダの自動読み込みに制約があった(年月日ごとのサブフォルダを作れない)ことが理由。
  • LightroomとApertureで迷ってApertureを購入したのに、数カ月後にApertureのサポート終了が発表されてLightroomを買い直した…
  • Photosだけで管理しないのは、写真の元ファイルを外付けHDDにそのまま保存したいから(PhotosはiPhotoと同じように独自形式で保存される)
  • iPhoneで撮った写真はLightroomを使わずにiCloudフォトライブラリでそのまま管理することにした。写真をバックアップしたいときはDropBoxなどの使うことにする。

*1 正式名称は「写真」だけど紛らわしいのでここではPhotosに統一する