non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

アメッシュの画像を表示するslackのボットをbotkitを使ってDockerイメージにしてherokuで動かした

こんにちは、次男が熱を出して、インフルエンザも疑われましたが、扁桃腺の炎症に よるものでした。長男はすっかり熱とは無縁で、ただ、年始から、乾燥による影響が高いとの診断を 受けた肌のかゆみがまだ、すこし残っているようです。 @kjunichiです。

前提

が参考になったのですが、herokuの無料枠でbotkitを動かすには、Webサーバーを動かしておくことが無難な模様。

アメッシュのサイトの画像表示すれば簡単と思いきや

に書いてあるが、単純に一枚のPNGでなく、レイヤーに分かれた個別のPNGファイルとして公開 されているので、レイヤーを合成する必要があり、結構面倒。

幸い、node.jsではnode-canvasがあり、 HTML5Canvas APIと同様のAPIが利用可能。

というわけで、drawImageで複数のPNGファイルを重ねたイメージを 簡単に生成出来る。

しかも、昨年に自作したライブラリを流用することで、

const ameshUtil = require('amesh-cli')

ameshUtil.getImage(770,480,function(pngimg){
    // pngimgにレイヤーを重ねたアメッシュの画像がPNG形式で取得可能。
})

slackはDataURLを展開してくれないので、Webサーバー上のURLが必要

PNGのデータが得られたので、これをDataURLにして、botが発言、いい感じにアメッシュ画像が表示されと 期待したが。。。

自前で生成したPNGをDataURLにて、slackに貼りつけようとしたらデータがデカすぎると 怒られ、サイズを小さくすると、今度は単なる文字列扱いとなってしまった。

ということで、herokuの制約で動かしているWebサーバー(express)が意味を持つことになる。

botアメッシュの画像表示のメッセージを受けたら、このサーバーのURLを返すことで、 TL上にアメッシュの画像を表示できることになる。

expressで/ameshへアクセスしたらアメッシュの画像を返す処理は、 前述のライブラリを利用すれば、以下の様に数行で記述できる。

app.get('/amesh', function(req, res) {
  ameshUtil.getImage(770,480,function(img){
    res.type('png')
    res.send(img)
  })
})

botkit側の実装

controller.hears(['雨どう'],'ambient',function(bot,message) {
  bot.reply(message,"http://xxxx/amesh");
});

という具合にアメッシュ画像のURLを返すだけでOK。

heroku向けのDockerイメージを作るまでの茨の道のり

これまで事、つま以下

  • npm install amesh-cli
  • web.jsの編集

をDockerに詰め込んだイメージを作れば、herokuでこのイメージを動かすことで、 アメッシュボットの完成となるわけだが、意外とこの先も難関が待っていた。

nodejsのイメージのnode.jsのバージョンが古すぎ

constがエラーとなってしまい、nodeのバージョン調べたら、0.12系だった。 ただこの問題は、heroku/nodejsイメージを自前で作るのでそれほど最終的には問題にはならなかった。

node.jsを新しくするには

以下の様にheroku/nodejsのソースをクローンして、環境変数のNODE_ENGINEに利用したいバージョンを記述すればOKだった。

ENV NODE_ENGINE 4.2.4
RUN curl -s https://s3pository.heroku.com/node/v$NODE_ENGINE/node-v$NODE_ENGINE-linux-x64.tar.gz | tar --strip-components=1 -xz -C /app/heroku/node

node-canvasモジュールのインストール問題

amesh-cliが内部で使っているnode-canvasモジュールは意外と依存ライブラリが多く、 ソースから入れるDockerfile書くには大変。apt-getコマンドでインストールしたいところ。 しかし、herokuでDockerイメージを動かす場合、apt-getコマンドでインストールするだけでは駄目で、 /app配下にコピーする必要がある。

apt-getでインストールされたファイルを取得

単純にnode-canvasモジュールのREADMEに書かれているパッケージをdpkg -Lするだけでは、 不完全かもしれない。apt-getでは依存するパッケージがインストールされていない場合、それらも インストールされるので。仮にnode-canvasではREADME記載のパッケージだけで済んでもこの方法は 汎用性に欠ける。

ということで、汎用的なapt-getでインストールしたファイルを取り出すDockerイメージを作成した。

apt-getを実行する直前の状態はなるべくherokuのイメージ近いものする必要がある。

にイメージのソースが置いてあり、これを元にイメージを作成。

ubuntu-debootstrap:14.04

とベースイメージがubuntu-debootstrap:14.04と分かったので、これに 必要なパッケージを入れ、インストールされたファイルを取り出してそれをheroku向けのイメージに追加 すれば、うごくのでは?ということで、レッツトライ!

FROM ubuntu-debootstrap:14.04
COPY ./cedar.sh /tmp/build.sh
RUN LC_ALL=C DEBIAN_FRONTEND=noninteractive /tmp/build.sh
RUN curl -L http://cpanmin.us | perl - App::cpanminus
RUN cpanm File::NCopy
WORKDIR /root
ONBUILD ADD aptlist.txt /root/aptlist.txt
ADD aptlist.pl /root/aptlist.pl
RUN chmod +x /root/aptlist.pl
RUN find /usr |sort > /root/before.txt
ONBUILD RUN cat aptlist.txt|xargs apt-get install -y --no-install-recommends
ONBUILD RUN find /usr |sort > after.txt
ONBUILD RUN diff before.txt after.txt > list.txt || exit 0
ONBUILD RUN grep '^>' list.txt| sed -e 's/> //g' > src.txt
ONBUILD RUN ./aptlist.pl src.txt && tar jcf bin.tar.bz2 app
CMD ["bash"]

cedar.shは以下の通り。

で必要なところを、切り貼りしただけ。

#!/bin/bash

exec 2>&1
set -e
set -x

cat > /etc/apt/sources.list <<EOF
deb http://archive.ubuntu.com/ubuntu trusty main
deb http://archive.ubuntu.com/ubuntu trusty-security main
deb http://archive.ubuntu.com/ubuntu trusty-updates main
deb http://archive.ubuntu.com/ubuntu trusty universe
deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main
EOF

apt-key add - <<'PGDG_ACCC4CF8'
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1

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
IIP9up4xwgje9LB7fMxsSkCDTHOk
=s3DI
-----END PGP PUBLIC KEY BLOCK-----
PGDG_ACCC4CF8

cd /
apt-get update
#apt-get upgrade -y --force-yes
apt-get install -y --force-yes \
    autoconf \
    bind9-host \
    bison \
    build-essential \
    coreutils \
    curl \
    daemontools \
    dnsutils \
    ed \
    git \
    imagemagick \
    iputils-tracepath \
    language-pack-en \
    libbz2-dev \
    libcurl4-openssl-dev \
    libglib2.0-dev \
    libjpeg-dev \
    libmagickwand-dev \
    libmysqlclient-dev \
    libncurses5-dev \
    libpq-dev \
    libpq5 \
    libreadline6-dev \
    libssl-dev \
    libxml2-dev \
    libxslt-dev \
    netcat-openbsd \
    openjdk-7-jdk \
    openjdk-7-jre-headless \
    openssh-client \
    openssh-server \
    postgresql-client-9.4 \
    postgresql-server-dev-9.4 \
    python \
    python-dev \
    socat \
    stunnel \
    syslinux \
    tar \
    telnet \
    zip \
    zlib1g-dev \
    #

# locales
apt-cache search language-pack \
    | cut -d ' ' -f 1 \
    | grep -v '^language\-pack\-\(gnome\|kde\)\-' \
    | grep -v '\-base$' \
    | xargs apt-get install -y --force-yes --no-install-recommends

exit 0

差分からエクスポート用のファイルを作成するツール

/usr配下のファイルを対象として、Herokuで動かすDockerイメージに必要なファイルの 抽出を行うツールを作った。

.pcファイルはheroku向けのコンテナイメージ内のパスに合わせて /usrから/app/usr/localへの書き換えも行う必要があった。

#!/usr/bin/env perl

use File::Path qw(make_path);
use File::NCopy qw(copy);
use File::Basename qw(dirname);

while(<>) {
    chomp($_);
    $destPath = $_;
    $destPath =~ s/\/usr/\.\/app\/usr\/local/;
    #print $_;
    if(-e $_) {
        if(-d $_) {
          #print  "$_ is directory.\n";
          print "$destPath\n";
          make_path($destPath);
        }
        if(-f $_) {
            make_path(dirname($destPath));
            copy($_,$destPath);
            if(/\.pc$/) {
                print ".pc! $destPath\n";
                $cmd = "perl -pi.bak -e 's/prefix=\\/usr/prefix=\\/app\\/usr\\/local/' $destPath";
                system("$cmd");
            }
        }
    } else {
        print "not exists $_\n";
    }
    
}

動かし方

Dockerfileの作成

先ほどつくったイメージkjunichi/aptgetを使うだけ。

FROM kjunichi/aptget

node-canvasに必要なパッケージ

node-canvasのREADMEに書いてある依存パッケージは以下。 これをaptlist.txtに書く。

libcairo2-dev
libjpeg8-dev
libpango1.0-dev
libgif-dev
build-essential
g++

以下のようにコンテナイメージ作成し、動かして、ファイルを取り出す。

docker build -t kjunichi/getapt .
docker run -d -t kjunichi/getapt --name getapt
docker exec getapt cat bin.tar.bz2 > dest.tar.bz2

依存ライブラリが入ったheroku/nodejsイメージを作る

heroku/nodejsイメージを自前で作ることで、heroku docker:initで生成されたDockerファイルがそのまま使える。

git clone https://github.com/heroku/docker-nodejs.git

以下のように抽出したファイル(dest.tar.bz2)を自前のheroku/nodejsイメージに組み込む。

# Inherit from Heroku's stack
FROM heroku/cedar:14

# Internally, we arbitrarily use port 3000
ENV PORT 3000
# Which version of node?
ENV NODE_ENGINE 4.2.4
# Locate our binaries
ENV PATH /app/heroku/node/bin/:/app/user/node_modules/.bin:$PATH

# Create some needed directories
RUN mkdir -p /app/heroku/node /app/.profile.d
WORKDIR /app/user

# Install node
RUN curl -s https://s3pository.heroku.com/node/v$NODE_ENGINE/node-v$NODE_ENGINE-linux-x64.tar.gz | tar --strip-components=1 -xz -C /app/heroku/node

# Export the node path in .profile.d
RUN echo "export PATH=\"/app/usr/local/bin:/app/heroku/node/bin:/app/user/node_modules/.bin:\$PATH\"" > /app/.profile.d/nodejs.sh

COPY dest.tar.bz2 /app/user/dest.tar.bz2
RUN cd / && tar xf /app/user/dest.tar.bz2
ENV CXXFLAGS "-I/app/usr/local/include"
ENV LDFLAGS "-L/app/usr/local/lib/x86_64-linux-gnu"
ENV LD_LIBRARY_PATH /app/usr/local/lib/x86_64-linux-gnu
ENV PKG_CONFIG_PATH /app/usr/local/lib/x86_64-linux-gnu/pkgconfig
ENV PATH $PATH:/app/usr/local/bin
RUN chmod +x /app/usr/local/bin/jq

ONBUILD ADD package.json /app/user/
ONBUILD RUN /app/heroku/node/bin/npm install
ONBUILD ADD . /app/user/

heroku向けのコンテナイメージ向けの対応

heroku向けのコンテナイメージでは/usr配下は手出しできない仕様のようなので、/app配下に使いたいファイルを 配置する必要がある。このため、配置したファイルをビルドツールやランタイムで参照できるように 環境変数を設定する必要がある。以下の環境変数をDockerfileで設定している。

環境変数 設定値
CXXFLAGS -I/app/usr/local/include
LDFLAGS -L/app/usr/local/lib/x86_64-linux-gnu
LD_LIBRARY_PATH /app/usr/local/lib/x86_64-linux-gnu
PKG_CONFIG_PATH /app/usr/local/lib/x86_64-linux-gnu/pkgconfig
PATH $PATH:/app/usr/local/bin

タイムゾーンの設定

今回利用しているamesh-cliモジュールはJSTで前提なので、herokuもJSTで動かす必要があった。

heroku config:add TZ=Asia/Tokyo

で設定出来た。

まとめ

やったこと

今回の作業を通じて以下のことができた。

わかったこと

  • slackのトークン等の環境変数はherokuでは無効化されているようだが、LD_LIBRARY_PATHは今のところ、Dockerfileでの設定が効いている模様。
  • PerlのFile::Copyだと実行属性がコピーされないので、File::NCopyを使うと良さそう
  • herokuのタイムゾーンの設定でDockerコンテナのタイムゾーンも設定される模様
  • boot2dockerをv1.10.0-rc1にあげないとSetting up ca-certificates-javaで進めなくなる

参考資料

関連記事

1年後の記事