AnsibleでDocker Image作るときのあれこれ

最近はDocker Imageを作る時Dockerfile以外の方法としてPacker + MItamaeとかでDocker Imageを作ることがある。

そのためにPacker用のMItamaeプラグインとかも作ったりもした。

hatappi.hateblo.jp

最近プロビジョニング周りでAnsibleを使うことになったので、改めてDocker Imageを作る方法を調べた。

実現したいこと

  • 任意のミドルウェアなどをいれたDocker Imageを作ってECRにPUSHまでしたい
  • Docker ImageもだけどAMIも作りたい

どう実現するか

まずDocker Imageを作るところにフォーカスをあてると
どうやらAnsibleには Ansible Containerと呼ばれるDocker Imageを作ったりしてくれるものが提供されているらしい。
任意のレジストリへログインしていれば、pushだって出来てしまう。

これを使えばAnsibleだけで完結することが出来そう!!!

Ansible Container

Ansible Container自体はpipを使って簡単にインストールすることが出来る。

$ pip install ansible-container[docker]

インストールが完了するとansible-containerコマンドが使えるようになって、始めに必要なものを作ってくれる ansible-container initコマンドラインで実行する。
すると下記のようなファイルが生成される。

.
├── ansible-requirements.txt # Ansibleを実行する時に必要なPythonモジュールをpip形式で記載する
├── ansible.cfg # ansible コマンドを実行する時の設定を記載する
├── container.yml # コンテナの設定
├── meta.yml # ライセンスとか諸々をかいていく。Galaxyとかのリポジトリで共有される時に使用される
└── requirements.yml # Ansibleを実行する時に外部リポジトリからダウンロードするもの

今回はサンプルとしてnginxをいれてCMDにたいしてnginxを起動させるようなものを作る。

先程生成したファイル群と同じディレクトリに下記のようにファイルを配置する。
Ansibleのディレクトリ構造については公式で Best Practiceが出ているのでこれを参照する。

└── roles
    └── nginx
        └── tasks
            └── main.yml

main.ymlには下記のように記載する。
やっていることはシンプルで、nginxのyumリポジトリを追加してあげて、インストール。
そして起動設定をして、サービスを起動するだけ。
ここで1つポイントしては今回docker上ではserviceで起動に関してコンテナを立ち絵上げる時にするので、スキップするように when: ansible_connection != 'docker' と記載してあげる。

- name: Add nginx repository
  yum_repository:
    name: nginx_repo
    description: nginx repo
    baseurl: http://nginx.org/packages/centos/$releasever/$basearch/
    gpgcheck: 0
    enabled: 1
- name: install nginx
  yum: name=nginx state=present
- name: set auto start nginx
  command: chkconfig nginx on
- service:
    name: nginx
    state: restarted
  when: ansible_connection != 'docker'

これでroleは完成したので最後にconatiner.ymlを作っていく。
container.ymlの書き方や何を意味するかは公式のdocにのっている。
ポイントだけ紹介するとservices配下に作成したいDocker Imageの設定を記載していく。
今回は起動した時にnginxのプロセスを立てたいので、CMDには["nginx", "-g", "daemon off;"]を設定している。
registriesではどこにpushするのかの情報を記載する今回はECR上にtestというネームスペースのtest-nginxがあるとする。
registries配下のnamespace欄がECRのネームスペースにあたる。またrepository_prefixとあるがこれを設定することでAnsible Containerはserivice配下のkeyと組み合わせて test-nginxというものを作成する。

version: "2"
settings:
  conductor:
    base: centos:7
    roles_path:
      - roles
  project_name: hoge
services:
  nginx:
    from: "centos:7"
    roles:
      - nginx
    command: ["nginx", "-g", "daemon off;"]
registries:
  ecr:
    url: https://0000.dkr.ecr.ap-northeast-1.amazonaws.com
    namespace: test
    repository_prefix: test

これで一通りできたのでまずはローカルにdocker imageを作ってみる。

$ ansible-container build
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=hoge
Building service...     project=hoge service=nginx

PLAY [nginx] *******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [nginx]

TASK [nginx : Add nginx repository] ********************************************
changed: [nginx]

TASK [nginx : install nginx] ***************************************************
changed: [nginx]

TASK [nginx : set auto start nginx] ********************************************
changed: [nginx]

TASK [nginx : service] *********************************************************
skipping: [nginx]

PLAY RECAP *********************************************************************
nginx                      : ok=4    changed=3    unreachable=0    failed=0

Applied role to service role=nginx service=nginx
Committed layer as image        image=sha256:612fd9e04b1ce2b1b0ecf2bf8cdc198cb59646f0726c8bd41c6c572e6cf service=nginx
Build complete. service=nginx
All images successfully built.
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=4285231fc80e8625b2e155496c5e033486e767146816f912a8e69 save_container=False

docker images で見るとちゃんと作られている!!

f:id:hatappi1225:20170915090224p:plain

あとはECRにpushするだけで

# --tagで任意のタグをうつことが出来る
$ ansible-container deploy --push-to ecr --username AWS --password xxx --tag latest

これで無事ECRにnginxのDocker Imageがpushされる。

後は docker run -d -p 8080:80 0000.dkr.ecr.ap-northeast-1.amazonaws.com/test/test-nginx とかで80ポートを8080にポーティングしてあげれば、ブラウザでいつもの画面をみることが出来る。

f:id:hatappi1225:20170915091317p:plain

やってみて

Ansible完結できた!
ただやってみて個人的に2つ問題があって、
1つ目が今回はしなかったがAMIを作ることを考えた時にAnsibleにはec2 moduleec2_ami moduleがあるので、これらを組み合わせてec2でインスタンスをたてて => sshで接続できるまで待ってあげて => Ansibleでプロビジョニングしてあげて => ec2インスタンスからAMIを作ってあげてみたいなことをしないといけない。
2つ目がpushする際の認証で、このAnsible Containerの仕組みとしてはdocker loginなどした時に~/.docker/config.jsonなどに認証情報が記載されているのだが、このファイルを読み込んで対象レジストリの認証情報をBase64でデコードしてユーザー情報とパスワードを取り出している。コードだとこのへん この実装だとECRの認証をよしなにやってくれる公式のamazon-ecr-credential-helperも使えない。
これはわりと面倒。

この2つはPackerが解決してくれる。 1つ目のAMIの部分もPackerにはプラグインが提供されており、面倒なインスタンスの制御とかをよしなにやってくれる。
2つ目の認証部分に関してはPackerは裏側でgoのaws sdkを使用するようにしているので、環境変数とか~/.aws配下とかをよしなに見に行ってくれる。

結論

Packer最高じゃん。

irbとかpryとかで => で出力されるのを消す方法

最近Rubyで大きいな配列を使うことがあってそんな時に起きたこととその解決策

どんな問題があったか

僕はよくpryとかirbを使ってRubyでちょっとした操作をしたりpry-byebugを使ってbinding.pryでブレークポイントをおいてデバッグしたりする。

例えばpryを使ってa, b, c各ワードを10個並べたものを出力する。

[1] pry(main)> %w(a b c).each { |w| puts w * 10 }
aaaaaaaaaa
bbbbbbbbbb
cccccccccc
=> ["a", "b", "c"]

いつもなら何も違和感がないのだが、オブジェクトがechoされる=> ["a", "b", "c"]が厄介になる。
今回はa b cと3つだがこれが1000, 2000と大きな数をまわすとオブジェクトがechoされるものが最後に出るので本当にみたいものはスクロールしないといけない。

なので便利ではあるが、場合によっては消したい時がある。

どうやって消すか

irbの場合とpryの場合でコマンドが少し違うのでそれぞれ書く。

irb

class IRB::Context (Ruby 2.4.0)
irbにはIRB::Context#echoがあってここで出力するかどうかを制御している。
boolで設定が出来るので、必要ない場合はfalseにしてあげる。

irb(main):001:0> %w(a b c).each { |w| puts w * 10 }
aaaaaaaaaa
bbbbbbbbbb
cccccccccc
=> ["a", "b", "c"]
irb(main):002:0> conf.echo = false
irb(main):003:0> %w(a b c).each { |w| puts w * 10 }
aaaaaaaaaa
bbbbbbbbbb
cccccccccc

pry

Customization and configuration · pry/pry Wiki · GitHub pryでは出力ではprocで定義されているものにしたがっているので空にしてあげる。

[1] pry(main)> %w(a b c).each { |w| puts w * 10 }
aaaaaaaaaa
bbbbbbbbbb
cccccccccc
=> ["a", "b", "c"]
[2] pry(main)> Pry.config.print = proc {}
[3] pry(main)> %w(a b c).each { |w| puts w * 10 }
aaaaaaaaaa
bbbbbbbbbb
cccccccccc

軽量Ruby普及・実用化促進フォーラム2017で発表してきました

今日は技術ネタというよりかは登壇レポート的な記事

昨日9/1に福岡で行われた 軽量Ruby普及・実用化促進フォーラム2017で登壇させていただきました。
場所はハイアットリージェンシー福岡で結婚式とかも行われるようできらびやかな場所でした。

私は今までは知らなかったのですが、福岡は県をあげてRubyに力をいれているとのことでした。
実際に今回はイベントの中でも実際に県内の軽量Rubyの使用事例や実際に企業の方が参加されていました。
僕は軽量RubyだとMItamaeしかさわったことがなかったので、組み込みとして使用されている事例を直接聞いたり質問できて非常に良かった。

そんな中僕はRubyでデータ分析ができる未来という内容でお話をさせていただきました。
軽量Rubyとは直接的には関係ないのです、今後軽量Rubyを使った組み込みシステムが復旧した際に今度はそこでとれたデータをどのように分析するかがくると思っています。
そんな時にRubyで出来たら楽しくないですかといって内容です。

speakerdeck.com

参加してみてどうだったか

人生で初めての50分発表だったので良い経験ができた。
後は福岡がとても住みやすそうなので、福岡に住みたい。

Apache Arrowの凄さを体感する

データ分析とかをしていると大規模データを扱うことがある。
複数のライブラリを使う際にデータ連携を行う際に一度CSVJSONに出力して連携先ではそれをパースしてといった方法をとることがある。
数メガくらいのファイルであれば問題にはならないが、これがギガなどになってくるとこのデータ連携コストが無視できなくなってくる。

これを解決する方法の1つとしてApache Arrowというものがある。
今回はこれを紹介して実際にどれくらい早いのかを検証してみる。

Apache Arrowとは?

  • 2016年の10月に0.1.0がリリース
  • メモリ上でカラム型データを扱うためのフォーマットとアルゴリズム

カラム型でデータを格納するので効率よく圧縮することが出来、メモリ上に書き込むことで読み書きの速さを実現している。
昔はメモリなどのリソースは潤沢に使うことは用意ではなかったが、昨今ではAWSなどで何十Gものメモリを積んだマシンを使用することが出来るため、このようなものが生きてくる。
このApache ArrowはPython, RubyやC, C++などから扱うことが現状出来る。

実際に速さを体験してみる

検証について

今回はEC2のt2.largeを使って検証を行った

Ubuntu: 16.04.2  
CPU: 2
メモリ: 8G   

Python: 3.6.2  
Ruby: 2.4.1  
Arrow: 0.6.0

今回はこれを使ってPythonCSV、Arrowで2.3Gのデータを出力しRubyでそれぞれを読み込んだ時の速度を見る。

CSV

Pythonでの書き込み

import pandas as pd
import pyarrow as pa
from time import time

df = pd.DataFrame({"a": ["a" * i for i in list(range(40000))],
                   "b": ["b" * i for i in list(range(40000))],
                   "c": ["c" * i for i in list(range(40000))]})

start_time = time()
df.to_csv("test.csv", header=False)
print("write : {0}s".format(time() - start_time))

Rubyでの読み込み

require "time"
require "csv"

start_time = Time.now
CSV.open("test.csv", headers: false) do |row|
   puts "batch size : #{row.count}"
end
puts "read : #{Time.now - start_time}s"

Apache Arrow

Pythonでの書き込み

#-*- using:utf-8 -*-
import pandas as pd
import pyarrow as pa
from time import time

df = pd.DataFrame({"a": ["a" * i for i in list(range(40000))],
                   "b": ["b" * i for i in list(range(40000))],
                   "c": ["c" * i for i in list(range(40000))]})
start_time = time()
record_batch = pa.RecordBatch.from_pandas(df)
with pa.OSFile("/dev/shm/pandas.arrow", "wb") as sink:
    schema = record_batch.schema
    writer = pa.RecordBatchFileWriter(sink, schema)
    writer.write_batch(record_batch)
    writer.close()
print("write : {0}s".format(time() - start_time))

Rubyでの読み込み

require "arrow"
require "time"
require "csv"

Input = Arrow::MemoryMappedInputStream

start_time = Time.now
Input.open("/dev/shm/pandas.arrow") do |input|
  reader = Arrow::RecordBatchFileReader.new(input)
  puts "batch size : #{reader.get_record_batch(0).count}"
end
puts "read : #{Time.now - start_time}s"

検証

CSVの時

Write (s) Read (s)
1 51.62104869 4.85522798
2 51.43777633 4.864032677
3 51.35622239 4.885216672
4 51.46343231 4.865155221
5 51.39711213 4.870614692

Apache Arrowの時

Write (s) Read (s)
1 3.39462328 0.013111846
2 3.312984943 0.013321837
3 3.332154036 0.013229807
4 3.342362165 0.013370328
5 3.306367636 0.012740001

それぞれの場合で5回ほど実行して平均をとると
CSVの時が書き込みに51.46秒、読み込みに4.87秒
Apache Arrowの時が書き込みに3.34秒、読み込みに0.01秒

書き込みも読み込みも早くなってますね!!