EC2(AMI)にnginx + uWSGI + supervisorでDjangoアプリケーションをデプロイする

これがベストプラクティスかどうなのかわかりませんが、おれはこうやりました、って話
もし、これでうまくいかなかったらコメントください

環境

  • Python:3.6.0(anacondaで構築)
  • Django:1.10.2
  • nginx:1.11.10
  • Amazon Linux AMI

今回はAMIでやりましたが、多分CentOSでも大丈夫なはず

大雑把な流れ

1.uWSGIのインストール & uWSGIでDjangoアプリケーションを立ち上げる
2.nginx + uWSGIでアプリケーションを立ち上げる
3.supervisorでプロセスをdaemon化する

uWSGIのインストール & uWSGIでDjangoアプリケーションを立ち上げる

まずはuWSGIをpipでインストール

$ pip install uwsgi

もしもインストールがうまくいなかったりしたらuwsgiをEC2インスタンスにインストールしようとして失敗した話 – Qiitaを参考にしてみましょう

uWSGIからDjangoアプリケーションを立ち上げる

インストールができたら、下記コマンドでuWSGIからDjangoアプリケーションを立ち上げてみる
(実行するディレクトリはDjangoプロジェクトのルートディレクトリで)

$ uwsgi --http :8000 --module プロジェクト名.wsgi

この状態でhttp://[EC2のパブリックIP]:8000にブラウザからアクセスすれば、アプリケーションに接続できるはず

nginx + uWSGIでアプリケーションを立ち上げる

nginxのuwsgiモジュール用の設定を行う

Djangoプロジェクト直下にuwsgi_paramsというファイルを作成する
ファイルの中身は下記の通り

uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;

nginx用のconfファイルを作成

Djangoプロジェクト直下にnginx用のconfファイルを作成
今回はmyweb_nginx.confという名前にしています

django_project/myweb_nginx.conf
upstream django {
    server 127.0.0.1:8000;
}

server { 
    listen      80;
    server_name EC2のパブリックIP or ドメイン;
    charset     utf-8;
    client_max_body_size 100M;

    location /static {
        alias /django_projectのディレクトリ(フルパス)/static;
    }

    location / {
        uwsgi_pass  django;
        include    /django_projectのディレクトリ(フルパス)/uwsgi_params;
    }

}

myweb_nginx.confを/etc/nginx/nginx.confに読み込ませる

$ sudo vi /etc/nginx/nginx.confhttp {}内を編集
include /etc/nginx/sites-enabled/*;を追加する

/etc/nginx/nginx.conf
http {
    ...中略...
    include /etc/nginx/sites-enabled/*; #ここを追加

    server {
    ...中略...

sites-enabledにシンボリックリンクを貼る

$ mkidr /etc/nginx/sites-enabledでsites-enabledを作成
さらに下記コマンドでmyweb_nginx.confを/etc/nginx/sites-enabled/にシンボリックを貼る

$ sudo ln -s /django_projectのディレクトリ(フルパス)/myweb_nginx.conf /etc/nginx/sites-enabled/

nginxを再起動し、Djangoアプリケーションを立ち上げる

$sudo /etc/init.d/nginx restartでnginxを再起動
下記のコマンドをDjangoプロジェクト直下で実行する

$ uwsgi --socket :8000 --module プロジェクト名.wsgi

この状態でhttp://[EC2のパブリックIP]にブラウザからアクセスすれば、アプリケーションに接続できるはず

補足:静的ファイルの読み込み

上記の状態でアプリケーションに接続した場合、staticディレクトリに配置しているcssなどが適用されていないはず
settings.pyにSTATC_ROOTを指定する必要がある

settings.py
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

下記コマンドでstaticファイルをSATIC_ROOTで指定したディレクトリにまとめる

$ python manage.py collectstatic

supervisorでプロセスをdaemon化する

python2系への切り替え

supervisorはpython3系では動いてくれないので、anacondaでpython2.7の仮想環境を構築していく

$ conda create -n supervisor python=2.7 anaconda

仮想環境への切り替えは

$ source activate supervisor

でおこない、抜ける場合は

$ source deactivate

でおこなう

supervisorのインストール

python2系の状態でsupervisorをインストールする

$pip install supervisor

supervisord.confの生成

ホームディレクトリで$echo_supervisord_conf > supervisord.confを実行
さらに$ sudo mv supervisord.conf /etcを実行し、supervisord.confを/etcに配置する

supervisord.dディレクトリの作成

/etc配下にsupervisord.dを作成する

$ sudo mkdir /etc/supervisord.d

supervisorログの設定

/var/logにログを吐かせるため、supervisord.confを編集
/etc/supervisord.conf
[supervisord]
;logfile=/tmp/supervisord.log
logfile=/var/log/supervisor/supervisord.log

ディレクトリの作成とlogファイルの作成

$ sudo mkdir /var/log/supervisor
$ sudo vi /var/log/supervisor/supervisord.log

また、下記のコマンドでログローテションを設定する

$ sudo sh -c "echo '/var/log/supervisor/*.log {
       missingok
       weekly
       notifempty
       nocompress
}' > /etc/logrotate.d/supervisor"

pid,includeの設定

supervisord.confを編集する

/etc/supervisord.conf
[supervisord]
...中略...
# pidファイルの配置場所の指定
;pidfile=/tmp/supervisord.pid
pidfile=/var/run/supervisord.pid

# ...中略

# includeセクションがコメントアウトされているので、コメントインして下記の用に修正。
[include]
files = supervisord.d/*.conf

supervisord.d/*.confの作成

supervisord.d配下のconfファイルにデーモン化するプロセスを指定する
今回はuWSGI + nginxでのDjangoアプリケーションの立ち上げをデーモン化する

/etc/supervisord.d/django.conf
[program:django_app] ;
directory=/djangoアプリケーションのディレクトリ(フルパス)/;
command=/home/ec2-user/anaconda3/bin/uwsgi --socket :8000 --module プロジェクト名.wsgi ;
numprocs=1 ;
autostart=true ;
autorestart=true  ;
user=ec2-user  ;
redirect_stderr=true  ;

supervisorの再起動->サービス立ち上げ

$ supervisordでsupervisorのサービスを起動
$ supervisord rereadで設定ファイルの再読み込み
$ supervisord reloadでsupervisorの再起動
$ supervisord start django_appでDjangoアプリケーションの立ち上げるプロセスがデーモン化される

この状態でhttp://[EC2のパブリックIP]にブラウザからアクセスすれば、アプリケーションに接続でき、且つサーバからログアウトした状態でもプロセスが死なない

参考リンク

Supervisorで簡単にデーモン化 – Qiita
EC2上で Django + Nginx + uWSGI を試す – Qiita

続きを読む

AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)

以前の投稿の更新版です。前の仕組みでは一つのデータに対しての集計しかけられなかったのですが、それでは実運用上あまりに不便、と思ったので、指定した値でグルーピングできるようにしてみました。

AWS LambdaでDynamoDBから取得した値に任意の集計をかける

インプットデータのフォーマット変更

ほとんど使い方は以前のものと一緒ですが、以下の点だけ変えました。

  1. IDは配列形式([“sensor1”, “sensor2”])で指定するように仕様変更した。
  2. テーブル名を環境変数から取得するようにした。

IDは配列形式([“sensor1”, “sensor2”])で指定するように仕様変更した

こんな感じが最新のフォーマットです。

{
  "label_id": "id",
  "label_range": "timestamp",
  "id": [
    "sensor1",
    "sensor2"
  ],
  "aggregator": "latest",
  "time_from": "2017-04-30T22:00:00.000",
  "time_to": "2017-04-30T22:06:00.000",
  "params": {
    "range": "timestamp"
  }
}

IDの部分を配列にしました。こうすることで指定したIDの最新値を取得することが可能になります。
戻り値はこんな感じになります。

"[{"timestamp": "2017-04-30T22:05:00.000", "score": 0.0, "id": "sensor1"}, {"timestamp": "2017-04-30T22:06:00.000", "score": 1.0, "id": "sensor2"}]"

ちなみにDynamoDBにはこんな感じの値を用意していました。

スクリーンショット 2017-07-19 18.12.43.png

テーブル名を環境変数から取得するようにした

そのまんまです。 handler.pyの os.environ['TABLE'] の部分です。Lambda実行時に環境変数をこんな感じで指定してください。

スクリーンショット 2017-07-19 18.13.46.png

handler.py
import sys
import boto3
import json
import decimal
import os
from boto3.dynamodb.conditions import Key

from aggregator.lambda_aggregator import LambdaAggregator
from aggregator.latest_aggregator import LatestAggregator
from aggregator.max_aggregator import MaxAggregator
from aggregator.min_aggregator import MinAggregator
from aggregator.sum_aggregator import SumAggregator
from aggregator.avg_aggregator import AvgAggregator
from aggregator.count_aggregator import CountAggregator

dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table(os.environ['TABLE'])

aggregator_map = {}
aggregator_map['latest'] = LatestAggregator()
aggregator_map['max'] = MaxAggregator()
aggregator_map['min'] = MinAggregator()
aggregator_map['sum'] = SumAggregator()
aggregator_map['avg'] = AvgAggregator()
aggregator_map['count'] = CountAggregator()

def run(event, context):
    check_params(event)
    result = []

    for id in event['id']:
        res = table.query(
                KeyConditionExpression=Key(event['label_id']).eq(id) & Key(event['label_range']).between(event['time_from'], event['time_to']),
                ScanIndexForward=False
            )

        return_response = aggregator_map[event['aggregator']].aggregate(res['Items'], event['params'])
        result.append(return_response)

    return json.dumps(result, default=decimal_default)

def decimal_default(obj):
    if isinstance(obj, decimal.Decimal):
        return float(obj)
    raise TypeError

def check_params(params):
    if 'label_id' not in params or 'label_range' not in params or 'id' not in params or 'aggregator' not in params or 'time_from' not in params or 'time_to' not in params or 'params' not in params:
        sys.stderr.write("Parameters for label_id, label_range, id, aggregator, time_from, time_to and params are needed.")
        sys.exit()

ソース

こちらにコミットしています。(以前のものを更新)

https://github.com/kojiisd/lambda-dynamodb-aggregator

続きを読む

AWS-EC2(Amazon Linux)にGoogleChromeをインストールしてNode+Selenium3でスクリーンショットを取る

はじめに

ブラウザでレンダリングされたスクリーンショットを取る必要があり、EC2にインストールする奮闘した備忘録を残しておきます。今後、他のエンジニアの人が苦悩をしないためにも…。

そもそも

Amazon Linuxが提供しているリポジトリは古い。更に他のリポジトリを入れようと競合する。
本当に厄介でした。ローカルの開発環境はCentOS7で、インストールはすんなりいったので、
AmazonLinuxでも普通にいけるでしょーと思っていた私が間違っていました…。
戦いはこれから始まるのです…

Google Chrome 59のインストールにはgtk3, gdk3, atk, libXssが必要

これがまた厄介。AmazonLinux上でgtk3, gdk3, atk, libXssをインストールする文献が全くもってない。
Ubuntuならともかく、Amazon Linuxの情報がインターネット上にない。
適当にrpm引っ張って入れれば行けるかと思いきや依存関係が多すぎて更にその依存関係を入れようとすると更に依存関係のライブラリが必要になる。考えただけで地獄。

Amazon Linux にはそもそもgtk2, gdk2すら入っていない

そりゃそうですよね。だってCLIで動くことだけを考えて作られてるんだもん。
もうやだこの時点で絶望。

それでも私は戦う道を選んだ

Red Hat Enterpriseに逃げる選択肢もあった。でもお金は極力かけたくない。そんな上からの<社会的フィルター>を叶えるために、私はAmazon LinuxにGoogleChromeというブラウザを入れるための戦い挑んだのでした。

インストールするまえの前置き

結論から言うとGoogleChrome 59を入れるのが間違い。GoogleChromeは58から59に切り替わった時点で、gtk2から、gtk3, gdk2からgdk3に使用するライブラリを切り替えています。
でもgtk2, gdk2ならどうだろう?ワンチャン行けそうな気がしてきた。というわけでインストールするのは現状のstable版の59ではなく58です。(そもそも論としてGoogle ChromeのバイナリのバージョンがSelenium3は58以上じゃないと動かなかったです。)

ソースは私。公開されているGoogle Chrome Version 49から59まで片っ端から入れて確認しました。

とりあえず依存関係を入れてGoogle Chromeを入れる

Amazon LinuxはRHELベースなので、それに合わせてrpmを入れまくる。
(海外のミラーサーバーなので一回失敗すると何度もダウンロードするはめになるので、先にwgetで取得しておく。)


cd /tmp

# atk
$ wget ftp://rpmfind.net/linux/centos/7.3.1611/os/x86_64/Packages/atk-2.14.0-1.el7.x86_64.rpm
$ sudo yum install atk-2.14.0-1.el7.x86_64.rpm

# gtk2 (このバージョンじゃないと後ほどGoogleChromeでライブラリに不足がでているというエラーがでます。)
$ wget ftp://rpmfind.net/linux/centos/6.9/os/x86_64/Packages/gtk2-2.24.23-9.el6.x86_64.rpm
$ sudo yum install gtk2-2.24.23-9.el6.x86_64.rpm

# gtk2-devel
$ wget ftp://rpmfind.net/linux/centos/6.9/os/x86_64/Packages/gtk2-devel-2.24.23-9.el6.x86_64.rpm
$ sudo yum install gtk2-devel-2.24.23-9.el6.x86_64.rpm

# libXss
$ wget ftp://rpmfind.net/linux/centos/7.3.1611/os/x86_64/Packages/libXScrnSaver-1.2.2-6.1.el7.x86_64.rpm
$ sudo yum install libXScrnSaver-1.2.2-6.1.el7.x86_64.rpm

# GoogleChrome 58
$ wget http://orion.lcg.ufrj.br/RPMS/myrpms/google/google-chrome-stable-58.0.3029.110-1.x86_64.rpm
$ sudo yum install google-chrome-stable-58.0.3029.110-1.x86_64.rpm

(記憶が曖昧なので上記の依存関係で解決できるかわかりません…足りないようでしたら教えていただければ情報提供致します。)

これで、Google Chromeのインストールは完了です。これを見つけるのに本当に時間がかかりました。泣きたい。

Selenium Standaloneを落としてくる。

Selenium Standaloneを落としてきます。もちろんjar形式のものです。それ以外には目を向けてはいけません。闇が潜んでいます。
http://docs.seleniumhq.org/download/

スクリーンショット_2017-07-18_5_30_23.png

nvmをインストールしてnodeとnpmを使えるようにする

nvmをインストールします。、

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash

~/.bash_profileに下記を追加します。

[ -s "~/.nvm/nvm.sh" ] && . "~/.nvm/nvm.sh"

その後下記で反映させます。

$ source ~/.bash_profile

nvmでnodeをインストールします。

$ nvm install stable

最新版のnodejsがインストールされるので、それを使うように設定します。

$ nvm use 8.1.4 #インストールされたnodeのバージョン

Seleniumを動かすためのWebdriverとNode用のseleniumをインストールします。


$ cd /path/to # プロジェクトなどのディレクトリ
$ npm install selenium
$ npm install selenium-webdriver
$ npm install chromedriver

あと、Javaと仮想ディスプレイも入れないと

Amazon Linuxにはもちろんディスプレイはついていないので、仮想ディスプレイのXvfbを導入します。
ついでにJavaも入れます。Selenium3の必要要件はJDK1.8以上です。

$ sudo yum install Xvfb java-1.8.0-openjdk java-1.8.0-openjdk-devel GConf2

(注釈: 入れたあとにjava -versionでバージョンを確認してください。1.8.0になっていれば問題ありませんが、それ未満だとSeleniumが動作しませんので、$JAVA_HOMEなどでjdk1.8.0までのパスを適切に通してください。)
(注釈2: 日本語フォントがインストールされていないともしかしたら文字化けする可能性があります。無償利用可能なウェブフォントやAdobeが公開しているSource Han Code JPを入れると幸せになれるかもです。フォントのインストールは割愛します)

ここからが楽しいSeleniumでスクショを取る。

ようやく下準備は整いました。いよいよ Node+Selenium3でスクリーンショットを取ります。
まず、仮想ディスプレイを起動します。

$ Xvfb :90 -ac -screen 0 1024x768x24 > /dev/null 2>&1  &

Selenium3を起動します。(ポートは30000にしています。)

$ DISPLAY=:90 java -Dwebdriver.chrome.driver="node_modules/chromedriver/bin/chromedriver" -jar selenium-server-standalone.jar -port 30000 > /dev/null 2>&1 

スクリーンショットを取るためのNodejsを書きます。以下はサンプルです。

example.js

const webdriver = require('selenium-webdriver');
const chrome = require("selenium-webdriver/chrome");
const fs = require('fs');

const driver = new webdriver.Builder()
        .withCapabilities(
            new chrome.Options()
                .addArguments('--no-sandbox')
                .toCapabilities()
        )
        .usingServer('http://127.0.0.1:30000/wd/hub')
        .build();

driver.get('https://google.co.jp');
driver.takeScreenshot().then((base64Image) => {
    // write file
    fs.writeFile('screenshot.png', Buffer.from(base64Image, 'base64'), 'base64', (error) => {});

}, (reject) => {
});

driver.quit();

example.jsができたところで実際にスクリーンショットを取ってみましょう。

$ node example.js

スクリーンショットを取るとこんな感じに撮れます。
screenshot.png

すごくいい感じ!

youtubeとかだと…
screenshot.png

いい感じにHTML5プレイヤーを認識してくれています。(ちなみに、phantomjsだとだめだった…)

おまけ

おまけとして、スマホ版レイアウトをSeleniumでスクリーンショットを撮りたい場面に遭遇します。
スマホ版レイアウトを採用としているサイトには下記の2種類があります。

  • 1) CSS3のmedia queryを用いて画面サイズによって自動的にレイアウトを変えてくれるサイト
  • 2) User-Agentを見てテンプレートファイルの吐き出しなどを分けているサイト

この両方を満たしてスクリーンショットを撮りたいその願い叶えます。

仮想ディスプレイのサイズをスマートフォンに合わせる

今回はiPhone7 Plusでいきます。

# 先ほどのXvfbを終了させます。
$ sudo pkill -f Xvfb

# 新しく設定
$ Xvfb :90 -ac -screen 0 414x736x24 > /dev/null 2>&1  &

さて、ここまでは 1)を満たすことができました。 次に2)を満たす方法が実は、Chrome自体の起動オプションではっ変更できず、拡張機能でUser-Agentを変更するしかないのです。
ですので、User-Agentを変更するChrome拡張を作成します。
これで完了です。次にこの拡張機能を読み込ませるため、example.jsを修正します。

2017/07/18 19:40追記

下記を追加すれば行けることが判明しました。

.addArguments('--user-agent="Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3"')
example.js

const webdriver = require('selenium-webdriver');
const chrome = require("selenium-webdriver/chrome");
const fs = require('fs');

const driver = new webdriver.Builder()
        .withCapabilities(
            new chrome.Options()
                .addArguments('--no-sandbox')
                                 .addArguments('--user-agent="Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3"')
                .toCapabilities()
        )
        .usingServer('http://127.0.0.1:30000/wd/hub')
        .build();

driver.get('https://google.co.jp');
driver.takeScreenshot().then((base64Image) => {
    // write file
    fs.writeFile('screenshot.png', Buffer.from(base64Image, 'base64'), 'base64', (error) => {});

}, (reject) => {
});

driver.quit();

これでスクリーンショットを撮ってみます。
screenshot.png

Youtubeは…
screenshot.png

なんときれいに撮影できました!めでたしめでたし。

続きを読む

Nightmareをec2で頑張って動かした備忘録

ブラウザ操作周りを自動化するのにNightmareでポチポチ作業してたんだけど
いざデプロイする際にec2で動かなくて詰んだ。

参考:
https://gist.github.com/dimkir/f4afde77366ff041b66d2252b45a13db

ほぼ上記のgistどおりで。ただgistもちょっと古いので動かすまでの備忘録

動かないこと確認

ともあれnightmareのインストール

npm i nightmare --save

nightmareのサンプルコードが付いてくるので実行

DEBUG=nightmare node node_modules/nightmare/example.js

以下のようなログが出て来る。electron依存の部分がやっぱり動かない。

...
you may not have electron installed correctly
...

足りないライブラリ確認

nightmareを入れた時に依存で入ったelectronディレクトリへ移動

cd node_modules/electron/dist
ldd electron | grep 'not found'

以下ログ。この辺りが足りないはず

        libgtk-x11-2.0.so.0 => not found
        libgdk-x11-2.0.so.0 => not found
        libatk-1.0.so.0 => not found
        libpangocairo-1.0.so.0 => not found
        libgdk_pixbuf-2.0.so.0 => not found
        libcairo.so.2 => not found
        libpango-1.0.so.0 => not found
        libXcursor.so.1 => not found
        libXdamage.so.1 => not found
        libXrandr.so.2 => not found
        libXfixes.so.3 => not found
        libXss.so.1 => not found
        libgconf-2.so.4 => not found
        libcups.so.2 => not found

ライブラリを追加して回る

参考のgistでツールが提供されてるので引っ張って来る

curl -o- https://gist.githubusercontent.com/dimkir/52054dfca586cadbd0ecd3ccf55f8b98/raw/2b5ebdf28f6a1aad760b5ab9cc581e8ad12a49f5/eltool.sh > ~/eltool.sh && chmod +x ~/eltool.sh 

eltool.shを使ってライブラリインストール

./eltool.sh dev-tools  # installs gcc compiler and some libs
./eltool.sh dist-deps  # we install prebuilt dependencies from Amazon Linux repos by using yum
./eltool.sh centos-deps # we install some  prebuil dependencies we can take from CentOS6 repo

# There's still a number of libraries which need to compile from source
./eltool.sh gconf-compile gconf-install 
./eltool.sh pixbuf-compile pixbuf-install
./eltool.sh gtk-compile  # this will take 3 minutes on t2.small instance
./eltool.sh gtk-install 

gistのままやるとgconf-compileでORBitのインストールうまくいかないので
ORBitだけ先に入れてやる

ORBit取得

wget ftp://195.220.108.108/linux/centos/6.9/os/x86_64/Packages/ORBit2-devel-2.14.17-6.el6_8.x86_64.rpm
wget ftp://195.220.108.108/linux/centos/6.9/os/x86_64/Packages/ORBit2-2.14.17-6.el6_8.x86_64.rpm

取って来たrpmをlocalinstall

yum localinstall ORBit2-2.14.17-6.el6_8.x86_64.rpm
yum localinstall ORBit2-devel-2.14.17-6.el6_8.x86_64.rpm

インストール確認

yum list installed | grep ORBit2
ORBit2.x86_64                        2.14.17-6.el6_8               installed
ORBit2-devel.x86_64                  2.14.17-6.el6_8               installed

再度gconfだけインストール

./eltool.sh gconf-compile gconf-install 

ライブラリをelectronのディレクトリにリンク貼る

libgconf-2.so.4とlibgtk-x11-2.0.so.0とlibgdk-x11-2.0.so.0とlibgdk_pixbuf-2.0.so.0はelectronのdistに持ってく必要があるので

ディレクトリ移動

cd node_modules/electron/dist

リンク貼りまくる

ln -PL /usr/local/lib/libgconf-2.so.4
ln -PL /usr/local/lib/libgtk-x11-2.0.so.0
ln -PL /usr/local/lib/libgdk-x11-2.0.so.0
ln -PL /usr/local/lib/libgdk_pixbuf-2.0.so.0 

確認

以下のコマンド叩いてemptyならライブラリの追加完了

ldd electron | grep 'not found'

Xvfbの追加

再度動作確認

DEBUG=nightmare node node_modules/nightmare/example.js

今度はxvfbが足りないって怒られる

...
you may need xvfb 
...

x-serverとかインストール

sudo yum -y install xorg-x11-server-Xorg xterm   # x-server
sudo yum -y install xorg-x11-drv-vesa xorg-x11-drv-evdev xorg-x11-drv-evdev-devel  # x-drivers
sudo yum -y install Xvfb

nightmareの動作確認

xvfbを起動しつつサンプルプログラムの実行

以下の形でxvfb-runにnodeのプログラム渡してあげる感じで。

xvfb-run -a --server-args="-screen 0 1366x768x24" node node_modules/nightmare/example.js

nightmareとawsでハマった人向け。

続きを読む

CodeStarを使ったAWS lambdaの料金通知Botのデプロイ

AWS Codestarがリリースされて少し経ちましたが、まだ東京リージョンには来ないですね。待ち遠しい。

Codestar自体は知っていたのですがAWSSummitTokyoで思った以上に便利そうな印象を受けたことと、LambdaのServerlessApplicationModel(SAM)を使ってみたくなったため、社内向けのbotのデプロイ作業をこれで置き換えてみました。

※2017/7/11現在、東京リージョンにはまだ来ていませんのでバージニアリージョンで試しています

これからやること

請求情報をSkypeへ毎日通知するBotを作成して、CodeStarでデプロイする。

前提

  • LambdaFunctionのコード自体の説明はしません。
  • 他のChatツールへの通知もLambdaのコードを多少書きかえればすぐできると思います。

手順

1. 請求情報を取得できるように設定を行う

右上のアカウントのプルダウンから「請求ダッシュボード」を選択。
左側のメニューの「設定」で、「請求アラートを受け取る」にチェックを入れる。

billingreport.JPG

2. Codestarでプロジェクトを作成

a. マネジメントコンソールからCodeStarを開く
b. ポップアップでロールを作るか?という質問が出るので「yes」を選択。
  裏で「aws-codestar-service-role」ロールが作られています。
  (初回やったときにこれが出なくて権限エラーが出てはまりました・・)
c. 「create new project」
d. pythonの新規プロジェクトを選択
codestar.JPG

e. プロジェクト名を「billing-bot」にして「create project」
codestar2.JPG

f. 開発ツールを選択する画面はSkipして完了。
codestar3.JPG

なんかかっこいい画面が出てきましたね。

3. CodeCommitからひな形となるファイルをclone

CodeCommitのgitリポジトリにはsshで接続するものとします。

a. マネジメントコンソールのIAMを開く。
b. ユーザ一覧から利用しているユーザを選択する。
c. 認証情報欄で[SSH公開キーのアップロード]を押して、公開鍵を張り付ける。
d. SSHキーIDが表示されるのでメモっておく。
e. SSHキーIDが覚えにくいのでCodeCommitの場合はこのsshキーを使うということをconfigに書いておく。 <SSH_KEY_ID>は自分のSSHキーIDに置き換えること。

$ cat ~/.ssh/config
Host git-codecommit.*.amazonaws.com
User <SSH_KEY_ID>
IdentityFile ~/.ssh/id_rsa

f. git cloneしてパスフレーズを入力するとcloneが実行される。
 (cloneするURLはCodeCommitのページに行けば参照可能です。

$ git clone ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/<REPOSITORY_NAME>
Enter passphrase for key '*****/.ssh/id_rsa':

httpsでの接続の場合は下記手順 c の下のあたりに認証情報(ユーザ名、パスワード)を生成する手順がありましたので、それでできると思います

初期ファイルはこのようになっていました

$ cd <REPOSITORY_NAME>
$ find -type f | grep -v .git
./buildspec.yml
./index.py
./README.md
./template.yml

READMEに説明が書かれています(英語ですが)

  • buildspec.yml – コードをパッケージングする方法を記載したCodeBuild用設定ファイル
  • index.py – Lambdaの実行コード
  • template.yml – LambdaとAPIGatewayにデプロイするためのSAMの設定ファイル

非常にわかりにくかったのですが、buildspec.ymlの中の

aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.json

にてtemplate.ymlをtemplate-export.jsonに変換して出力されます。
そして、ServerlessApplicationModelでデプロイする際はtemplate-export.jsonを使用しているようです。

template.ymlではGetEvent/PostEventなどからも想像できるようにApiGatewayの設定も行われています。

4. 実行コードの作成

lambdaの実行コードを書いていきます。作成対象は以下の★がついている3つのファイルです。
index.pyはいらないので消しておいてもOKです

$ find  -type f | grep -v .git
./buildspec.yml
./README.md
./requirements.txt   ★
./src/billing_bot.py  ★
./src/skype_adapter.py ★
./template.yml
requirement.txt
requests
pytz
src/billing_bot.py
# -*- coding: utf-8 -*-

from skype_adapter import SkypeAdapter
from datetime import datetime, timedelta

import boto3
import pytz
import os

def post_current_charges():

    startDate = datetime.today() - timedelta(days = 1)
    endDate = datetime.today()

    session = boto3.Session()
    client = session.client('cloudwatch')   
    response = client.get_metric_statistics (
        MetricName = 'EstimatedCharges',
        Namespace  = 'AWS/Billing',
        Period   = 86400,
        StartTime  = startDate,
        EndTime = endDate,
        Statistics = ['Maximum'],
        Dimensions = [
            {
                'Name': 'Currency',
                'Value': 'USD'
            }
        ]
    )

    maximum = response['Datapoints'][0]['Maximum']
    date = response['Datapoints'][0]['Timestamp'].strftime('%Y/%m/%d')

    roomid = os.environ['ROOM_ID']
    message = date + "時点でのAWS利用料は" + str(maximum) + "ドルです" 
    print message

    SkypeAdapter().postConversation(roomid, message)

def lambda_handler(event, context):
    """Lambda使う場合のエントリポイント"""
    post_current_charges()


if __name__ == "__main__":
    """コマンド実行のエントリポイント"""
    post_current_charges()
src/skype_adapter.py
# -*- coding: utf-8 -*-

import requests
import json
import os

class SkypeAdapter:
    """ Skypeに投稿するためのAdapterClass """

    def postConversation(self, roomid, message):
        """ Skypeへメッセージを投稿する """
        token = self.__auth()
        self.__post( token, roomid, message )

    def __auth(self):
        """ MicrosoftBotFrameworkのOAuthClient認証を行いaccess_tokenを取得する """

        headers = { 'Content-Type' : 'application/x-www-form-urlencoded' }
        data = {
            'grant_type' : 'client_credentials',
            'client_id' : os.environ['CLIENT_ID'],
            'client_secret' : os.environ['CLIENT_SECRET'],
            'scope' : 'https://graph.microsoft.com/.default'
        }

        access_token_response = requests.post( 'https://login.microsoftonline.com/common/oauth2/v2.0/token', headers=headers, data=data )

        if access_token_response.status_code != 200 :
            print access_token_response.headers
            print access_token_response.text
            raise StandardError('Skype OAuth Failed')

        tokens = json.loads(access_token_response.text)
        return tokens['access_token']

    def __post(self, token, roomid, message):
        """ MicrosoftBotFrameworkのチャット投稿用RESTAPIを叩く """

        headers = { 
            'Authorization' : 'Bearer ' + token,
            'Content-Type' : 'application/json'
        }

        data = {
            'type' : 'message/text',
            'text' : message
        }

        url = 'https://api.skype.net/v3/conversations/' + roomid + '/activities/'

        response = requests.post( url, headers=headers, json=data)

        if response.status_code != 201 :
            print response.status_code
            print response.headers
            print response.text
            raise StandardError('Skype Post Failed')

        return

5. buildspec.yml

CodeBuildでの処理内容を定義するbuildspec.ymlを編集していきます。
installフェーズを追加して、pipを使って関連ライブラリの取得を行います。

version: 0.1

phases:
  install:
    commands:
      - pip install -r requirements.txt -t ./src
  build:
    commands:
      - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.json
artifacts:
  type: zip
  files:
    - template-export.json

ただ、これをCodeCommitにpushしてBuildを実行しても落ちました・・

CodeBuildを詳しく見てみると、「ビルド環境」というもので実行環境のイメージを選択する部分があり、なぜか nodejs のイメージが選択されている!!!

codebuild2.JPG

怒りを抑えて、python用のイメージに変えておきましょう。

codebuild3.JPG

6. template.ymlの設定

最後はLambdaをデプロイするためのtemplate.ymlです。
環境変数の部分は自分の環境にあったものを指定してください。アカウント取得方法は過去記事を参考に。

AWSTemplateFormatVersion: 2010-09-09
Transform:
- AWS::Serverless-2016-10-31
- AWS::CodeStar
Description: AWS利用料を通知するbot

Parameters:
  ProjectId:
    Type: String
    Description: CodeStar projectId used to associate new resources to team members

Resources:
  billingbot:
    Type: AWS::Serverless::Function
    Properties:
      Handler: billing_bot.lambda_handler
      Runtime: python2.7
      CodeUri: src
      Description: AWS利用料を通知するbot
      MemorySize: 128
      Timeout: 60
      Role: arn:aws:iam::860559588436:role/CodeStarWorker-billing-bot-Lambda
      Events:
        Schedule1:
          Type: Schedule
          Properties:
            Schedule: cron(0 1 * * ? *)
      Environment:
        Variables:
          CLIENT_SECRET: ********
          ROOM_ID: '19:*******@thread.skype'
          CLIENT_ID: *******

7. IAMロールに足りていない権限を付与

今回の用途に応じた権限をIAMのロールの設定画面から付与します。

  • CloudFormationのロール:スケジュール実行用トリガ作成用にCloudWatchEventFullAccessが必要
  • Lambdaのロール:請求情報を参照するためにCloudWatchReadOnlyAccessが必要

これらがないとビルドが当然ビルドは落ちます。
ここもはまりポイントでした。落ちてからじゃないとロールが作られていることにすら気づかなかったです。

ここも怒りを抑えてポチポチ設定。

8. CodeCommitにコードをpush

最後にこれらのコードをpushするとCodePipelineが動き出します。あとは、じっと待ちましょう。

9. テスト

Lambdaの画面を開いて、テスト!(パラメータは適当でいいです)

Skypeに通知が届けばテスト成功です!
bot.JPG

あとは毎日朝10時に自動投稿してくれます。

感想

なんか遅い

  • コードをCodeCommitにプッシュしてからのPipelineの起動に少し間が空く
  • CodeCommitがInProgress状態になる(単にpushされただけなのに何が動いているのか不明)

逆にCodeBuildはDockerベースなのに思ったより早い。

完成度がまだ微妙

  • CodeCommitはプルリクができなかったり
  • CodeBuild用のDockerイメージがなぜかnodeJSで作られていたり
  • 裏で勝手に作られているIAMロールに対して権限をつけることがわかりにくかったり
  • (Cloudformationの機能ですが)失敗したときのデバッグ・リトライがつらかったり

と微妙なところも多いですが、トータルで見れば今後に期待が持てるサービスですね。

続きを読む

AWS請求額を日次でSlackに通知する方法(Lambda Blueprint + CloudWatch Events)

概要

AWS請求情報をCloudWatchから取得して、日次でSlackに通知させます。

  1. Slack APIの設定
    外部サービスからSlackに投稿できるようAPIの設定をします。
  2. KMSの設定
    Slackの投稿用URLを暗号化するため、KMSを使用します。
  3. Lambda/CloudWatch Eventの設定
    Lambdaの公式サンプルコードを修正し、CloudWatch Eventで定期的に実行するように設定します。

Slack APIの登録

Incoming WebHooksの登録をします。
https://my.slack.com/services/new/incoming-webhook/

  • 投稿するチャンネルを選択して、Add Incoming WebHooks Integration
  • 登録後、Webhook URLを控えておきます。

KMSの設定

IAMよりKMS keyを作成します。
http://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html

Lambdaの設定

Lambdaには、Slack連携用のテンプレート(Blueprint)が用意されています。
Slack APIの登録手順、KMSの設定手順もテンプレート内に記載されています。

今回は、cloudwatch-alarm-to-slack-python(Python2.7)に沿ってやってみます。
Configure triggersSNS topicは使用しないのでRemoveして進みます。
※ Python3.6版も用意されてますが、デフォルトだと動作しませんでした。(原因判明したら追記予定)

テンプレートから追加/変更した箇所サマリ

  • 元々AWS SNSをトリガーにしている作りなので、該当箇所をコメントアウト
  • CloudWatchからAWS請求情報を取得
    • import datetimeを追加
  • Slackメッセージのカスタマイズ
    • 投稿者の表示名を変更
    • タイトルにAWS請求ダッシュボードへのリンクを付与
    • 請求額しきい値超過でメッセージを色分けするように変更

Lambda function code

# coding:utf-8

from __future__ import print_function

import boto3
import json
import logging
import os
# CloudWatchコマンド用
import datetime

from base64 import b64decode
from urllib2 import Request, urlopen, URLError, HTTPError

# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']

HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# CloudWatchからAWS請求情報を取得(昨日から今日にかけて1日分の最大値)
# 2017/7現在: バージニア北部(us-east-1)リージョンのみ請求情報を取得可能
cloud_watch = boto3.client('cloudwatch', region_name='us-east-1')
get_metric_statistics = cloud_watch.get_metric_statistics(
                        Namespace='AWS/Billing',
                        MetricName='EstimatedCharges',
                        Dimensions=[
                            {
                                'Name': 'Currency',
                                'Value': 'USD'
                            }
                        ],
                        StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
                        EndTime=datetime.datetime.today(),
                        Period=86400,
                        Statistics=['Maximum']
                        )

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    #message = json.loads(event['Records'][0]['Sns']['Message'])

    # AWS請求情報をフィルタ1
    message = get_metric_statistics['Datapoints'][0]
    logger.info("Message: " + str(message))

    #alarm_name = message['AlarmName']
    #old_state = message['OldStateValue']
    #new_state = message['NewStateValue']
    #reason = message['NewStateReason']

    # AWS請求情報をフィルタ2
    currency_statistics = message['Maximum']
    time_statistics = message['Timestamp'].strftime('%Y/%m/%d')

    # しきい値超過でSlackメッセージの色を変更する
    if currency_statistics > 15.0:
        notify_color = "danger"
    else:
        notify_color = "good"

    # Slack投稿メッセージ
    # username,color,title,title_linkを追加
    slack_message = {
        'channel': SLACK_CHANNEL,
        # Slack上のusername
        'username': "AWS BillingMan",
        'attachments': [
            {
                # メッセージを色分けする
                'color': notify_color,
                # タイトルを追加
                "title": "AWS Billing & Cost",
                # AWS請求ダッシュボードへのリンクを設定
                "title_link": "https://console.aws.amazon.com/billing/home?#/",
                # メッセージ本文
                'text': "EstimatedCharges is now %s USD in %s" % (currency_statistics, time_statistics)
            }
        ]
    }

    req = Request(HOOK_URL, json.dumps(slack_message))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

環境変数の設定

Enable encryption helpersにチェックして、事前に作成したKMSを選択します。
kmsEncryptedHookUrlのみEncryptをクリックして暗号化します。

slackChannel: 投稿するチャンネル名(e.g. #test)
kmsEncryptedHookUrl: Webhook URL(e.g. "hooks.slack.com/services/abc123")

Lambda function handler and role

Lambda用の新しいRoleを作成します。ここではRoleの名前だけ入力します。
Lambda function作成後、IAMに移動しRoleのPermissionsを変更します。

http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_manage_modify.html

To change the permissions allowed by a role

  1. Attach PolicyCloudWatchReadOnlyAccessを追加します。
  2. Inline PoliciesCustom Policyを作成します。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1443036478000",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "<your KMS key ARN>"
            ]
        }
    ]
}

CloudWatch Eventsの設定

CloudWatch EventsのRulesを作成します。
http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/RunLambdaSchedule.html

ステップ 2: ルールを作成する

cron(UTC)を設定します。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html

Rate または Cron を使用したスケジュール式

例: 日本時間09:00に通知

0 0 ? * * *

例: 日本時間10:00に通知

0 1 ? * * *

実行結果(サンプル)

monitor | onox Slack 2017-07-08 11-27-23.png

CloudWatch 参考リンク

AWS Billing and Cost Management のディメンションおよびメトリックス
http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/billing-metricscollected.html

AWS SDK for Python (Boto3)を使ってCloudWatchの値を取得してみた
http://dev.classmethod.jp/cloud/aws/get_value_of_cloudwatch_using_boto3/

Slack API 参考リンク

https://api.slack.com/docs/message-formatting

Formatting and Attachments

https://api.slack.com/docs/message-attachments

Attaching content and links to messages

続きを読む