LambdaとCloudWatchでサーバレスなTwitterBot作ってみた

こんにちは。駆け出しインフラエンジニアの(@k4ri474)です。
この記事はVASILY Advent Calendar 2017 11日目の記事です。

Twitterでbotを作ろうと思った時、ざっくり二つ選択肢が頭に浮かびますよね?
巷に溢れているbot作成サービスに登録するか、
はたまた自作するか。

エンジニアの方は、とりあえずプログラミングしてみっか〜となることが多いような気がします。
Twitter APIに関してはもちろん公式ドキュメントがありますし、プログラムもググれば山ほど先人たちのものが見れるので、
なんちゃらキーやらなんちゃらトークンさえ取得できれば、肝心のツイートはチョチョイのジョイかと思います。

ただ、継続的に動かすbotとして考えるとベースのサーバはどうしても必要になるのでちと悩みますよね。
自分のサイトをホスティングしてメールサーバ動かしてついでにbotを回す、などといったような状況ならクラウドでサーバを立てとけばいいんですが、botだけでいい時には少々大げさです。

そこでAWS Lambda + Amazon CloudWatch Eventsを使ってサーバレスに、かつ継続的に実現します。

概要

1postするためのプログラムをpythonで書き、それをLambdaにセットします。
そしてLambdaをCloudWatch Eventsで定期的に発火させ、botの体をなすようにします。

実装(Lambda)

まず、こちらが普通のつぶやきプログラムです。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

tweet = "Hello World"
session = OAuth1Session(CK, CS, AT, AS)

params = {"status": tweet }
session.post(URL, params = params)

Twitterに接続するにはOAuth認証が必要なのでrequests_oauthlib(https://github.com/requests/requests-oauthlib)
というPython用のOAuth認証ライブラリを利用しました。
Lambdaではライブラリとプログラムコードを一まとめにzip圧縮する必要があるので、プログラムと同じディレクトリにインストールしておきます。

% pip install requests requests_oauthlib -t ./

各種キーは苦労して取得したら、それらを各々セットしていただければあとはつぶやきをいじって実行するだけです。

さて、上のプログラムをLambda用に書き換えます。
Lambdaから関数を呼び出してもらうためには、ハンドラー関数を作成する必要があります。
パラメータとして eventcontext を取る関数で、Lambdaを呼び出したイベントデータやランタイム情報を内部で使えますが、今回はシンプルに使わない方向でいきます。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

def my_handler(event, context):
    tweet = "Hello World"
    session = OAuth1Session(CK, CS, AT, AS)

    params = {"status": tweet }
    session.post(URL, params = params)

これで準備は完了です。
こちらのファイル群をzip化し、コマンドでアップロードしてみます。

% aws lambda create-function \
--region ap-northeast-1 \
--function-name "sample" \
--runtime "python3.6" \
--role "MY_ROLE" \
--handler "sample.my_handler" \
--zip-file "fileb://./sample.zip" \
--profile PROFILE

注意したいのはhandlerオプションです。構文は実行ファイル名.ハンドラー関数名ですので、作成したプログラム名とハンドラー関数名に合わせて適宜編集してください。

また、セットするroleにCloudWatchLogsの権限を与えておくと実行結果をログ出力できるようになるのでオススメです。
今回は定義済みのポリシー、arn:aws:iam::aws:policy/CloudWatchLogsFullAccessをアタッチしたロールを事前に作成していたので、そちらをセットしてみました。
ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

以上でLambdaへのアップロードは完了です。

実装(CloudWatch Events)

さて、あとはこのLambda関数を定期実行するだけです。
まずはCloudWatch Eventsをトリガーにセットします。

スクリーンショット 2017-12-10 23.09.08.png

Lambdaのダッシュボードから関数を選択し、設定画面でトリガーを設定します。上図のようにCloudWatch Eventsをトリガーにセットしたら、下にスクロールしてトリガーの詳細を設定します。

スクリーンショット 2017-12-10 23.09.56.png

例としてはこんな感じでしょうか。
ルールタイプをスケジュール式に選択すると、おなじみのcron記述で定期実行の間隔をコントロールできます。
僕は試しにもう一方のrate式を使ってみることにしました。

このような感じで設定して保存すると無事トリガーとして機能します。
お疲れ様です。

成果物

このままのプログラムでは全く同じ内容をつぶやくばかりで面白くないので、
プログラムをちょちょっといじってimgurから猫画像のリンクを拾ってきてpostする感じのbotを作ってみました。

https://twitter.com/k4ri474/status/937562270476812290

自分の猫postで心が洗われます。
皆さんもサーバレスTwitter botでぜひ遊んでみてくださいb

続きを読む

Docker + Nginx + Let’s EncryptでHTTPS対応のプロキシサーバーを構築する

Docker上にNginxコンテナをプロキシサーバーとして構築し、Let’s EncryptでHTTPS対応しました。構築にあたって かなり苦戦した ので、そのノウハウを記事としてまとめました。

「Nginx」とは

Apacheなどの従来のWebサーバーは、クライアントの数が多くなるとサーバーがパンクする 「C10K問題(クライアント1万台問題)」 を抱えていました。「Nginx」はこの問題を解決するために誕生した、静的コンテンツを高速配信するWebサーバーです。2017年10月現在、そのシェアは Apacheとほぼ同等 となっています。

wpid-wss-share13.png

Webサーバー シェア
Micosoft IIS 49.44%
Apache 18.78%
Nginx 18.40%

「Let’s Encrypt」とは

「Let’s Encrypt」は すべてのWebサーバへの接続を暗号化する ことを目指し、SSL/TLSサーバ証明書を 無料 で発行する認証局(CA)です。シスコ、Akamai、電子フロンティア財団、モジラ財団などの大手企業・団体がスポンサーとして支援しています。


本稿が目指すシステム構成

本稿ではAmazon EC2、Dockerコンテナを使用して以下のようなシステムを構築することを目標とします。

DockerでNgixのプロキシサーバーを構築する.png

前提条件

  • 独自ドメインを取得していること(本稿で使用するドメインはexample.comとします)
  • IPv4パブリックIP(Elastic IP)がEC2インスタンスに設定されていること
  • EC2インスタンスにDocker、docker-composeがインストールされていること

事前に準備すること

DockerでHTTPS対応のプロキシサーバーを構築するにあたり、事前に以下の設定をしておく必要があります。

  • EC2のインバウンドルールで443ポートを開放する
  • DNSのAレコードを設定する
  • プロキシ用のネットワークを構築する

EC2のインバウンドルールで443ポートを開放する

インバウンドルールを以下のように設定し、443ポートを外部へ公開します。

タイプ プロトコル ポート範囲 ソース
HTTPS TCP 443 0.0.0.0/0
HTTPS TCP 443 ::/0

DNSのAレコードを設定する

DNSの設定方法は利用しているドメイン取得サービスによって異なります。例えばバリュードメインの場合、DNSの設定方法は「DNS情報・URL転送の設定 | VALUE-DOMAIN ユーザーガイド」に記載されています。

DNSのAレコードを以下のように設定します。xx.xx.xx.xxにはEC2インスタンスに割り当てられているIPv4パブリックIPを設定します。

a @ xx.xx.xx.xx
a www xx.xx.xx.xx

上記設定は以下を意味します。

  • example.com(サブドメイン無し)をIPアドレスxx.xx.xx.xxにポイントする
  • www.example.com をIPアドレスxx.xx.xx.xxにポイントする

プロキシ用のネットワークを構築する

プロキシサーバーとWebサーバー間のネットワークは外部との通信を行う必要がありません。そこで
プロキシサーバーとWebサーバー間の 内部ネットワーク を構築するため、EC2のインスタンスにログインし、以下のコマンドを入力します。

$ docker network create --internal sample_proxy_nw

上記コマンドは以下を意味します。

  • --internal: ネットワーク外との通信が行えないネットワークを作成します。
  • sample_proxy_nw: 任意のネットワーク名です。

以下のコマンドを入力し、ネットワークの設定情報がコンソールに出力されていることを確認しましょう。

$ docker network inspect sample_proxy_nw

Dockerコンテナの定義ファイルを作成する

事前準備が完了したら、Dockerコンテナの定義ファイルを作成しましょう。本稿におけるディレクトリ構成は以下のとおりです。

/path/to/dir/

.
├── docker-compose.yml // プロキシサーバーとWebサーバーのコンテナを定義するファイル
└── proxy
    ├── default.conf // プロキシサーバー上にあるNginxのデフォルト定義ファイル
    ├── Dockerfile // プロキシサーバーのイメージを構築するためのファイル
    └── entrypoint.sh // プロキシサーバーにSSL証明書を取得するためのファイル

以下では、各ファイルの内容を解説します。

./docker-compose.yml

docker-compose.ymlでは、以下のコンテナを定義しています。

  • proxy: プロキシサーバー(Nginxベース)
  • web1: Webサーバー(httpdベース)
  • web2: Webサーバー(httpdベース)
version: '3'
services:
  proxy:
    build: ./proxy
    tty: true
    image: sample_proxy
    container_name: sample_proxy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    ports:
      - "443:443"
    volumes:
      - '/srv/letsencrypt:/etc/letsencrypt'
    networks:
      - default
      - sample_proxy_nw
    depends_on:
      - "web1"
      - "web2"
    command: ["wait-for-it.sh", "sample_web1:80", "--", "./wait-for-it.sh", "sample_web2:80"]
  web1:
    image: httpd
    container_name: sample_web1
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
  web2:
    image: httpd
    container_name: sample_web2
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
networks:
  proxy_nw:
    external: true

上記コマンドは以下を意味します。

  • サービスproxyports: 外部からのHTTPSアクセスとproxyサーバーの内部ポートを疎通させるため、443:443を定義します。
  • サービスproxyvolumes: /srv/letsencrypt:/etc/letsencryptを定義します。/etc/letsencryptLet’s Encryptで取得した証明書が生成されるディレクトリ です。
  • networks: 上述の説明で生成したsample_proxy_nwを各サービス(proxy, web1, web2)に定義します。
  • depends_on: コンテナの起動順序を制御するオプションです。 Nginxのproxy_passに設定されているWebサーバーが起動していない状態でプロキシサーバーが起動した場合にエラーとなる ため、web1, web2を設定します。

./proxy/default.conf

./proxy/default.confはNginxのデフォルト定義ファイル(/etc/nginx/conf.d/default.conf)を書き換えるためのファイルです。

server{

    server_name example.com www.example.com;

    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://sample_web1/;
    }

    location /example/ {
        proxy_pass    http://sample_web2/;
    }

}

上記設定は以下を意味します。

  • server_name: ユーザーから要求されるHTTPリクエストのヘッダに含まれるHostフィールドとserver_nameが一致した場合、該当するサーバ設定を採用します。Nginxではキャッチオールサーバーとして_を定義することもできますが、 certbot-autoがサーバー情報を正しく取得することができない ため、上記のようにドメイン名を入力します。
  • location: ルートディレクトリ(example.com/)とサブディレクトリ(example.com/example/)にアクセスした際の振り分け先URIを設定します。proxy_passには、http://[コンテナ名]/を設定します。コンテナ名はdocker-compose.ymlのcontainer_nameで設定した名前となります。
    また、http://sample_web1/のように 末尾に/を入れる ことに注意しましょう。例えばlocation /example/において、プロキシパスの末尾に/が含まれていない(http://sample_web2)場合、振り分け先は http://sample_web2/example/となってしまいます。

./proxy/Dockerfile

FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
RUN apt-get update && apt-get install -y \
        wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh
ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto
RUN chmod a+x /usr/local/bin/certbot-auto
RUN certbot-auto --os-packages-only -n
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

上記設定は以下を意味します。

  • ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto: Let’s Encryptが発行するSSL/TLSサーバ証明書を自動で取得・更新するツール「 certbot-auto 」をダウンロードします。

./proxy/entrypoint.sh

#!/bin/bash
certbot-auto --nginx -d example.com -d www.example.com -m your-account@gmail.com --agree-tos -n
certbot-auto renew
/bin/bash

上記設定は以下を意味します。

  • --nginx: プロキシサーバーにNginxを使用する場合のオプションです。default.confの設定を自動的に書き換えます。(2017年12月現在、アルファ版のプラグイン)
  • -d example.com -d www.example.com: SSL/TLSサーバ証明書の取得を申請するドメイン名を指定します。
  • -m your-account@gmail.com: アカウントの登録や回復などに使用する電子メールアドレスを指定します。
  • --agree-tos: Let’s Encryptの利用規約に同意します。
  • -n: インタラクティブ設定をオフにします。
  • ./certbot-auto renew: 3ヶ月で失効する SSL/TLSサーバ証明書を自動で更新します。

以下のコマンドを入力してentrypoint.shに 実行権限を付与する ことを忘れないようにしましょう。

$ chmod +x entrypoint.sh

Dockerコンテナを起動する

それでは以下のコマンドを入力してDockerコンテナを起動しましょう。

docker-compose up -d

しばらく時間をおいてから、以下のコマンドを入力します。

docker-compose logs

以下のように出力されていれば成功です。

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://example,com and
https://www.example.com

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
https://www.ssllabs.com/ssltest/analyze.html?d=www.example.com
-------------------------------------------------------------------------------

HTTPSでアクセスする

ブラウザを起動し、実際に以下のURLにアクセスしてみましょう。

Chromeブラウザの場合はデベロッパーツール > Security > View certificateからSSL/TLSサーバ証明書を確認することができます。

「発行元: Let’s Encrypt Authority X3」となっているはずです。

続きを読む

CloudAutomatorでSQSを利用せずジョブの数珠つなぎを実現する

Cloud Automatorのアドベントカレンダー2017年の10日目を担当します。

Cloud Automatorとは

Cloud Automatorでは、AWSのリソースを操作するための内容を「ジョブ」と呼ばれる単位でWeb上の操作画面からGUIで登録し、日時指定やHTTPリクエストをトリガーにして実行させることが可能なサービスです。
https://cloudautomator.com/

ジョブの連続実行

これまでCloud Automatorでは、複数のジョブを連続して実行させるためには、AmazonSQSを利用して数珠つなぎ状態にする方法をサポートしていました。(マニュアル:「ジョブを数珠つなぎにして複数のジョブを連携する」参照)
しかしこれは、SQSを事前に準備する必要があり、少々手間でした。

スクリーンショット.png


2017年10月に、「Webhook後処理」という機能(マニュアル:「Webhook後処理」参照)がリリースされました。ざっくり言うと、ジョブの実行後にその結果を任意のURLにPOSTリクエストで受け取ることが可能になります。
今回はこの機能を利用して、 SQSを使わずに Cloud Automatorのジョブを連続実行させてみます。

スクリーンショット 2017-12-10 22.26.24.png

サンプル:EC2インスタンスのインスタンスタイプの変更

ここでは、先ほどのSQSを使ったインスタンスタイプを変更するための一連ジョブを、HTTPトリガーとWebhook後処理で置き換える例を示します。

例えばマネージメントコンソール上で、EC2のインスタンスタイプを手動で変更&起動したい場合は

  1. 該当のインスタンスを「停止」にする(→完全に停止するまでしばらく待つ)
  2. インスタンスタイプを変更する
  3. インスタンスを起動する

これらの操作が必要になりますが、ここで地味に面倒なのが、インスタンスが完全に停止するまで待たないと、インスタンスタイプが変更できないことです。Cloud Automatorを利用すると、これら一連の作業をジョブとして登録&連続実行させることで、不毛な停止待ち時間もすべてCloud Automatorがハンドリングしてくれます。

👉 STEP1: インスタンス起動ジョブを登録

連続するジョブを作成する際は、最後のジョブから登録していくと手間が少なくて済みます。(2017年12月現在)
まずは、 運用ジョブ > ジョブの追加 メニューから、新規にジョブを作成します。

step1.png

項目 設定値
トリガー HTTPトリガー
アクション EC2:インスタンスを起動
AWSアカウント ※CloudAutomatorに登録しているAWSアカウントを指定
インスタンス 該当のインスタンスを指定(タグの指定でもOK)
リソースの終了ステータスをチェックする ※ここはチェックしてもしなくてもどちらでもOK
後処理 ※必要に応じて(指定しなくてもOK)
ジョブ名 「インスタンス起動ジョブ」など任意の名前

このような形で登録します。

登録完了後は、 運用ジョブ > 運用ジョブ一覧 メニューから、先ほど作成したジョブの詳細画面を開いて、以下の情報をメモします。

step1_2.png

👉 STEP2: インスタンスタイプ変更ジョブの登録

次に、インスタンスタイプの変更ジョブを登録していきます。STEP1と同じように、ジョブの新規登録画面から、以下のように登録します。

項目 設定値
トリガー HTTPトリガー
アクション EC2:インスタンスタイプを変更
AWSアカウント ※CloudAutomatorに登録しているAWSアカウントを指定
インスタンス 該当のインスタンスを指定(タグの指定でもOK)
インスタンスタイプ ※変更したいタイプを指定
後処理 ※新規に後処理を作成して指定(後述)
ジョブ名 「インスタンスタイプ変更ジョブ」など任意の名前

🔰 後処理の指定
後処理の指定箇所で、「後処理を新たに作成する」ボタンを押して、作成画面を開きます。
step2_1.png

ここで、サービスには Webhook を選択し、先ほどメモしておいた「インスタンス起動ジョブ」の、 HTTPトリガーのURL と、 HTTPヘッダーのAuth情報 を入力し、判別しやすい後処理名を入力して登録します。
step2_2.png

後処理作成後、今作成した後処理を「成功時」の後処理に指定します。
step2_3.png

以上の情報で「インスタンスタイプ変更」のジョブを作成します。
STEP1と同様に、作成したジョブの詳細画面を開き、 HTTPトリガーのURLHTTPヘッダーのAuth情報 を再度メモします。

👉 STEP3: インスタンス停止ジョブの作成

これで最後です。STEP2と同様に、ジョブの作成画面で次のように登録します。

項目 設定値
トリガー ※利用したいトリガーを指定
アクション EC2:インスタンスを停止
AWSアカウント ※CloudAutomatorに登録しているAWSアカウントを指定
インスタンス 該当のインスタンスを指定(タグの指定でもOK)
リソースの終了ステータスをチェックする ※ここは必ずチェックしてください
後処理 ※新規に後処理を作成して指定(後述)
ジョブ名 「インスタンス停止ジョブ」など任意の名前

【⚠️注意事項】
「リソースの終了ステータスをチェックする」にチェックを入れることで、インスタンスが完全に停止するのを待ってから、指定した後処理が実施されるようになります。チェックを入れないと、インスタンスが停止準備の状態のままインスタンスタイプ変更のジョブが実行されてしまい、ジョブがエラー終了してしまうのでご注意ください。

🔰 後処理の指定
STEP2と同様に、新規に後処理を作成します。
サービスには Webhook を選択し、先ほどメモしておいた「インスタンスタイプ変更ジョブ」の、 HTTPトリガーのURL と、 HTTPヘッダーのAuth情報 を入力し、判別しやすい後処理名を入力して登録し、ジョブ作成画面上「成功時」の後処理に、作成した後処理を指定してジョブを作成します。

以上で終了です。

まとめ

インスタンスタイプの変更処理などは、自動化しようとすると、インスタンスが完全に停止したことをハンドリングするのが地味に大変だったりしますが、これらはすべてCloud Automator側で判断してくれるため大変便利です。

Cloud Automator上で「インスタンスの停止・変更・再起動」を行うためには、SQSで各ジョブ間を繋ぐ方法しかありませんでしたが、Webhook後処理の登場によって、その作業がかなり簡略化できるようになりました。
・・・とは言っても、後処理を毎回作成するのも大変ではあります。そのため、これらの作業すらも不要になるような便利な機能のリリースが、今後予定されています。
詳しくはサービスのロードマップページを参照してください。
https://cloudautomator.com/roadmap/

続きを読む

AutoScalingGroupのインスタンス起動・停止をSlackに通知する

AutoScalingGroupの作成からSNSとLabmdaと連携してイベントをSlackに通知するところまで。

LaunchConfigurationの作成

AutoScalingGroupingを作るためにはLaunchTemplate, LaunchConfigurationNameまたはInstanceIdが必要なので今回はLaunchConfigurationを作成します。

$ aws autoscaling create-launch-configuration 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --instance-type t2.micro 
  --image-id ami-bf4193c7
  --key-name xxxxx

AutoScalingGroupの作成

先ほど作成したLaunchConfigurationを指定してAutoScalingGroupを作成します。

$ aws autoscaling create-auto-scaling-group 
  --auto-scaling-group-name sample-auto-scale 
  --min-size 1 --max-size 5 
  --launch-configuration-name sample-auto-scale-launch-configration 
  --availability-zones us-west-2a us-west-2b

インスタンスの起動・停止を通知する

AutoScalingGroupからインスタンスのイベントを受け取る方法は LifeCycleHookでもできそうですが、今回はSNSを使った方法でやってみます。

SNSのトピックを作成する

AutoScalingGroupのイベントを通知するためのTopicを作成します。

$ aws sns create-topic --name sample-auto-scale-notification

AutoScalingGroupのイベントをSNSと紐付ける

NotificationConfigurationを追加します。
今回は以下の4つのイベントをSNSで通知するようにしました。

  • EC2_INSTANCE_LAUNCH
  • EC2_INSTANCE_LAUNCH_ERROR
  • EC2_INSTANCE_TERMINATE
  • EC2_INSTANCE_TERMINATE_ERROR
$ aws autoscaling put-notification-configuration 
  --auto-scaling-group-name sample-auto-scale 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --notification-types "autoscaling:EC2_INSTANCE_LAUNCH" "autoscaling:EC2_INSTANCE_LAUNCH_ERROR" "autoscaling:EC2_INSTANCE_TERMINATE" "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"

SNSからSlackに通知するLambdaを作成する

pythonでSlackへ通知するLambdaを作成していきます。

sns-to-slack-notificatioin-lambda.png

import urllib.request
import json

def lambda_handler(event, context):
    url = 'https://hooks.slack.com/services/AAAAAAAAA/BBBBBBBBB/CCCCCCCCCCCCCCCCCCCCCCCC'
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    obj = {"text":json.dumps(event)}
    json_data = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(url, data=json_data, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")

    return response_body

LambdaにSNSをsubscribeさせる

$ aws sns subscribe 
  --topic-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification 
  --protocol lambda 
  --notification-endpoint arn:aws:lambda:us-west-2:999999999999:function:sns-to-slack-notification

SNSがLambdaを起動できるようにpermissionを与えます。

$ aws lambda add-permission 
  --function-name sns-to-slack-notification 
  --statement-id autoscaling-sns-to-lambda 
  --action "lambda:InvokeFunction" 
  --principal sns.amazonaws.com 
  --source-arn arn:aws:sns:us-west-2:999999999999:sample-auto-scale-notification

インスタンスを追加して通知を受け取る

AutoScalingGroup -> SNS -> Lambdaの設定ができたので実際にインスタンスを追加して通知を確認します。
desired-capacityを増やし、インスタンスを1つ起動します。

$ aws autoscaling set-desired-capacity 
  --auto-scaling-group-name sample-auto-scale 
  --desired-capacity 2

インスタンスが起動されSlackに通知がきました。

スクリーンショット 2017-12-10 14.50.32.png

続きを読む

AWSで運用しているウェブサイトが不正な大量アクセスにあった場合

AWSで運用しているウェブサイトが不正な大量アクセスにあった場合、その分料金は増えるのでしょうか?それとも、その分の料金は支払わずに済むのでしょうか? どなたかお詳しい方、参考URLでも良いのでご回答お願い申し上げます。 続きを読む

カテゴリー 未分類 | タグ

consul-template & supervisorでプロセスの可視化

こちらはフロムスクラッチ Advent Calendar 2017の9日目の記事です。

はじめに

ポプテピピック

もうすぐ、ポプテピピック始まりますね。
どうも、jkkitakitaです。

概要

掲題通り、consul + supervisordで
プロセス監視、管理に関して、可視化した話します。

きっかけ

どうしても、新規サービス構築や保守運用しはじめて
色々なバッチ処理等のdaemon・プロセスが数十個とかに増えてくると
↓のような悩みがでてくるのではないでしょうか。

  1. 一時的に、daemonをstopしたい
  2. daemonがゾンビになってて、再起動したい
  3. daemonが起動しなかった場合の、daemonのログを見る
  4. daemonが動いているのかどうか、ぱっとよくわからない。
  5. ぱっとわからないから、なんか不安。 :scream:

個人的には
5.は、結構感じます。笑
安心したいです。笑

ツールとその特徴・選定理由

簡単に本記事で取り扱うツールのバージョン・特徴と
今回ツールを選んだ選定理由を記載します。

ツール 特徴 選定理由
supervisor
v3.3.1
1. プロセス管理ツール
2. 2004年から使われており、他でよく使われているdaemon化ツール(upstart, systemd)と比較して、十分枯れている。
3. 柔軟な「プロセス管理」ができる。
4. APIを利用して、プロセスのstart/stop/restart…などが他から実行できる。
1.今までupstartを使っていたが、柔軟な「プロセス管理」ができなかったため。

※ upstartは「プロセス管理」よりかは、「起動設定」の印象。

consul
v1.0.1
1. サービスディスカバリ、ヘルスチェック、KVS etc…
2. その他特徴は、他の記事参照。
https://www.slideshare.net/ssuser07ce9c/consul-58146464
1. AutoScalingするサーバー・サービスの死活監視

2. 単純に使ってみたかった。(笑)

3. 本投稿のconsul-templateを利用に必要だったから(サービスディスカバリ)

consul-template
v0.19.4
1. サーバー上で、consul-templateのdaemonを起動して使用
2. consulから値を取得して、設定ファイルの書き換え等を行うためのサービス
ex.) AutoScalingGroupでスケールアウトされたwebサーバーのnginx.confの自動書き換え
1. ansibleのようなpush型の構成管理ツールだと、AutoScalingGroupを使った場合のサーバー内の設定ファイルの書き換えが難しい。

2. user-data/cloud-initを使えば実現できるが、コード/管理が煩雑になる。保守性が低い。

cesi
versionなし
1. supervisordのダッシュボードツール
2. supervisordで管理されているdaemonを画面から一限管理できる
3. 画面から、start/stop/restartができる
4. 簡易的なユーザー管理による権限制御ができる
1. とにかく画面がほしかった。

2. 自前でも作れるが、公式ドキュメントに載っていたから

3. 他にもいくつかOSSダッシュボードあったが、一番UIがすっきりしていたから。(笑)

実際にやってみた

上記ツールを使って
daemonを可視化するために必要な設定をしてみました。
本記事は、全て、ansibleを使って設定していて
基本的なroleは
ansible-galaxyで、juwaiさんのroleを
お借りしています。
https://galaxy.ansible.com/list#/roles?page=1&page_size=10&tags=amazon&users=juwai&autocomplete=consul

supervisor

クライアント側(実際に管理したいdaemonが起動するサーバー)

supervisord.conf
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
;  - Shell expansion ("~" or "$HOME") is not supported.  Environment
;    variables can be expanded using this syntax: "%(ENV_HOME)s".
;  - Comments must have a leading space: "a=b ;comment" not "a=b;comment".

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

[inet_http_server]         ; inet (TCP) server disabled by default
port=0.0.0.0:9001        ; (ip_address:port specifier, *:port for all iface)
username=hogehoge              ; (default is no username (open server))
password=fugafuga               ; (default is no password (open server))
;セキュリティ観点から、ここのportは絞る必要有。

[supervisord]
logfile=/tmp/supervisord.log        ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB               ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10                  ; (num of main logfile rotation backups;default 10)
loglevel=info                       ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid        ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024                         ; (min. avail startup file descriptors;default 1024)
minprocs=200                        ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files=/etc/supervisor.d/*.conf

/etc/supervisor.d/配下に
起動するdaemonを設定します。

daemon.conf
[group:daemon]
programs=<daemon-name>
priority=999

[program:<daemon-name>]
command=sudo -u ec2-user -i /bin/bash -c 'cd /opt/<service> && <実行コマンド>'
user=ec2-user
group=ec2-user
directory=/opt/<service>
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/daemon.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/daemon.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10


[eventlistener:slack_notifier]
command=/usr/bin/process_state_event_listener.py
events=PROCESS_STATE
redirect_stderr=false
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/event_listener.stdout.log
stdout_logfile_maxbytes=2MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/event_listener.stderr.log
stderr_logfile_maxbytes=2MB
stderr_logfile_backups=10
environment=SLACK_WEB_HOOK_URL="xxxxxxx"

eventlistener:slack_notifierは、下記投稿を参考に作成。
https://qiita.com/imunew/items/465521e30fae238cf7d0

[root@test02 ~]# supervisorctl status
daemon:<daemon-name>              RUNNING   pid 31513, uptime 13:19:20
slack_notifier                    RUNNING   pid 31511, uptime 13:19:20

server側(daemonの管理画面を表示するwebサーバー)

supervisord.conf
クライアント側と同様

consul

server側

[root@server01 consul_1.0.1]# pwd
/home/consul/consul_1.0.1

[root@server01 consul_1.0.1]# ll
total 16
drwxr-xr-x 2 consul consul 4096 Dec  3 04:49 bin
drwxr-xr-x 2 consul consul 4096 Dec  3 06:06 consul.d
drwxr-xr-x 4 consul consul 4096 Dec  3 04:50 data
drwxr-xr-x 2 consul consul 4096 Dec  3 04:50 logs

[root@server01 consul.d]# pwd
/home/consul/consul_1.0.1/consul.d

[root@server01 consul.d]# ll
total 16
-rw-r--r-- 1 consul consul 382 Dec  3 06:06 common.json
-rw-r--r-- 1 consul consul 117 Dec  3 04:49 connection.json
-rw-r--r-- 1 consul consul  84 Dec  3 04:49 server.json
-rw-r--r-- 1 consul consul 259 Dec  3 04:49 supervisord.json
/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // consul keygenで発行した値を使用。
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "server01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true, // ここtrueでないと、check script実行できない
  "rejoin_after_leave": true
}
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/server.json
{
  "server": true, // server側なので、true
  "server_name": "server01",
  "bootstrap_expect": 1 // とりあえず、serverは1台クラスタにした
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-server01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

consul自体もsupervisordで起動します。

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d -ui // -uiをつけて、uiも含めて起動。
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

agent側(管理したいdaemonが起動するサーバー側)

/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // server側と同じencrypt
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "agent01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true,
  "rejoin_after_leave": true,
  "retry_join": ["provider=aws tag_key=Service tag_value=consulserver region=us-west-2 access_key_id=xxxxxxxxxxxxxx secret_access_key=xxxxxxxxxxxxxxx"
  // retry joinでserver側と接続。serverのcluster化も考慮して、provider=awsで、tag_keyを指定。
]
  }
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/daemon.json
{
  "services": [
        {
      "id": "<daemon-name>-agent01",
      "name": "<daemon-name>",
      "tags" : [ "daemon" ],
      "checks": [{
        "script": "supervisorctl status daemon:<daemon-name> | grep RUNNING",
        "interval": "10s"
      }]
    }
  ]
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-agent01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

agent側もsupervisordで管理

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d // -uiは不要
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

cesi

image2.png

こちらのrepoから拝借させていただきました :bow:
基本的な設定は、README.mdに記載されている通り、セットアップします。

/etc/cesi.conf
[node:server01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[node:test01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

(ansibleのroleにもしておく。)
cesiのコマンドも簡単にsupervisordで管理する様に設定します。

/etc/supervisor.d/cesi.conf
[program:cesi]
command=python /var/www/cesi/web.py
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/root/cesi.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/root/cesi.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

スクリーンショット 2017-12-10 1.51.12.png

うん、いい感じに画面でてますね。
ただ、この画面の欠点としてnodeが増えるたびに、
都度、 /etc/cesi.confを書き換えては
webサーバーを再起動しなければならない欠点がありました。
なので
今生きているサーバーは何があるのかを把握する必要がありました。
 → まさにサービスディスカバリ。
そこで、設定ファイルの書き方もある一定柔軟にテンプレート化できる
consul-tamplteの登場です。

consul-template

ここも同様にして、ansibleで導入します。
https://github.com/juwai/ansible-role-consul-template
あとは、いい感じに公式ドキュメントをみながら、templateを書けばok。

[root@agent01 config]# ll
total 8
-rwxr-xr-x 1 root   root    220 Dec  4 05:16 consul-template.cfg
/home/consul/consul-template/config/consul-template.cfg
consul = "127.0.0.1:8500"
wait = "10s"

template {
  source = "/home/consul/consul-template/templates/cesi.conf.tmpl"
  destination = "/etc/cesi.conf"
  command = "supervisorctl restart cesi"
  command_timeout = "60s"
}
/home/consul/consul-template/templates/cesi.conf.tmpl
{{range service "supervisord"}}
[node:{{.Node}}]
username = hogehoge
password = fugafuga
host = {{.Address}}
port = 9001

{{end}}

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

上記のように、consul-tamplateの中で
{{.Node}}という値を入れていれば
consulでsupervisordのnode追加・更新をトリガーとして
consul-templateが起動し

  1. /etc/cesi.confの設定ファイルの更新
  2. cesiのwebserverの再起動

が実現でき、ダッシュボードにて、supervisordが、管理できるようになります。

また
consul-templateは、daemonとして起動しておくものなので
consul-templateもまた、supervisordで管理します。

/etc/supervisor.d/consul-template.conf
[program:consul-template]
command=/home/consul/consul-template/bin/consul-template -config /home/consul/consul-template/config/consul-template.cfg
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul-template/logs/stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul-template/logs/stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

早速、実際サーバーを立ててみると…

スクリーンショット 2017-12-10 1.48.57.png

うん、いい感じにサーバーの台数が8->9台に増えてますね。
感覚的にも、増えるとほぼ同時に画面側も更新されてるので
結構いい感じです。(減らした時も同じ感じでした。)

めでたしめでたし。

やってみて、感じたこと

Good

  1. 各サーバーのプロセスの可視化できると確かに「なんか」安心する。
  2. サーバー入らずに、プロセスのstart/stop/restartできるのは、運用的にもセキュリティ的にも楽。
  3. supervisordは、探しても記事とかあまりない?気がするが、本当にプロセスを「管理」するのであれば、感覚的には、まぁまぁ使えるんじゃないかと感じた。
  4. consul-templateの柔軟性が高く、consulの設計次第でなんでもできる感じがよい。
  5. 遊び半分で作ってみたが、思ったより評判はよさげだった笑

Not Good

  1. supervisord自体のプロセス監視がうまいことできていない。
  2. まだまだsupervisordの設定周りを理解しきれていない。。。
     ※ ネットワーク/権限/セキュリティ周りのところが今後の課題。。usernameとかなんか一致してなくても、取れちゃってる・・・?笑
  3. consulもまだまだ使えていない。。。
  4. cesiもいい感じだが、挙動不審なところが若干ある。笑
    ※ 他のダッシュボードもレガシー感がすごくて、あまり、、、supervisordのもういい感じの画面がほしいな。
    http://supervisord.org/plugins.html#dashboards-and-tools-for-multiple-supervisor-instances

さいごに

プロセスって結構気づいたら落ちている気がしますが
(「いや、お前のツールに対する理解が浅いだけだろ!」っていうツッコミはやめてください笑)

単純にダッシュボードという形で
「可視化」して、人の目との接触回数が増えるだけでも
保守/運用性は高まる気がするので
やっぱりダッシュボード的なのはいいなと思いました^^

p.s.
色々と設定ファイルを記載していますが
「ん?ここおかしくないか?」というところがあれば
ぜひ、コメントお願いいたします :bow:

続きを読む

CodeBuildの実行結果をslackに通知する

はじめに

Globis Advent Calendar10日目は、弊社のエンジニアチームを支えるインフラ技術をお伝えいたします。
開発スピードをさらに促進するために、Blue Green Deployment, Infrastrucure as Code, ChatOps など新しい運用思想を積極的に取り入れて、手動でのオペレーションを極力なくしています。
今回は、弊社で実運用しているChatOpsの一例として、CodeBuildの実行結果をslackに通知する方法を、可能な範囲で具体的にお伝えいたします。

設定方法

AWS Lambdaの設定

コード

import os
import json
import urllib.request

URL = os.environ['WEBHOOK_URL']


def send_slack(obj):  ## slackに通知
    method = "POST"
    headers = {"Content-Type" : "application/json"}
    js = json.dumps(obj).encode("utf-8")
    request = urllib.request.Request(URL, data=js, method=method, headers=headers)
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")


def check_status(status):
    fail = False
    if status == 'IN_PROGRESS':
        color = '#008000'  ## green
    elif status == 'SUCCEEDED':
        color = '#00BFFF'  ## deepskyblue
    else:
        color = '#FF00FF'  ## magenta
        fail = True
    return color, fail


def parse_event(event):  ## cloudwatch event から渡された event(dict型)をパース、slackに渡すobjectを生成。
    detail = event['detail']
    status = detail['build-status']
    initiator = detail['additional-information']['initiator']
    log = detail.get('additional-information', {}).get('logs', {}).get('deep-link', 'Not Exist')
    color, fail = check_status(status)
    fields = [{'title': 'Initiator', 'value': initiator, 'short': False},
              {'title': 'Status', 'value': status, 'short': False},
              {'title': 'LogLink', 'value': log, 'short': False}]
    obj = {'attachments': [{'color': color, 'fields': fields}]}
#    if fail == True:  ## Fail時にチャンネルメンション飛ばしたい時はコメントを外す。
#        obj['attachments'][0]['text'] = '<!channel>'
    return obj


def lambda_handler(event, context):  ## lambdaから最初にcallされる関数。
    obj = parse_event(event)
    send_slack(obj)
    return

環境変数の設定

CloudWatch Event の設定

slackでの通知結果

  • ワンクリックでビルドログを眺めることができます。

運用事例のご紹介

  • AWS Athenaのテーブルにクエリを実行し、中間テーブルを生成するETL処理のバッチ
  • Ruby on Rails アプリケーションデプロイ時の db:migrate 処理
  • 最近はCodeBuildをVPC内で実行できるようになったので、利用できる幅が広がっています!

おわりに

いかがでしたでしょうか。python初心者(僕)でもChatOpsに貢献できます。
グロービスではSREエンジニアを募集しています!
インフラの運用担当だけど手作業を自動化したいと考えている方、開発者だけどインフラも含めてコードで管理したい方、一緒に理想のインフラを作ってみませんか?

続きを読む

AWS Lambda in VPC

この記事はDMM.com #2 Advent Calendar 2017 – Qiitaの9日目です。

カレンダーはこちら
DMM.com #1 Advent Calendar 2017 – Qiita
DMM.com #2 Advent Calendar 2017 – Qiita

はじめに

こんにちは、@funa1gと申します。
社内で共通で利用されるAPIなどの開発をやっています。
技術選定のために、AWSについても色々と試しています。
今回は、その中でAWS LambdaとVPCをつないだ、アンチパターンについてです。

全体の構成

API Gateway + LambdaでAPIを作成する時、RDSにアクセスしたい場合があります。
しかもRDSとなるとやっぱりVPC内に置きたいです。
この組み合わせがアンチパターンなのは、すでに色々検証記事が出ていますが、
多少遅くても動くなら許せるかなと思って、検証してみました。

全体の構成です
AWS Networking (1).png

動作テスト

雑に動けばいいので、こんなエンドポイントにしました
レスポンスはDB内のデータです

METHOD: GET
URL: /LambdaTest
Response

[
  {
    "id": 1,
    "name": "test1"
  },
  {
    "id": 2,
    "name": "test2"
  }
]

Lambda側のコードも、DBにアクセスして、JSONを返すだけです

const { Client } = require('pg');

exports.handler = (event, context, callback) => {
    // 接続先のPostgresサーバ情報
    const client = new Client()

    client.connect()
    client.query("SELECT * FROM test", (err, res) => {
        if (err) throw err
        client.end()
        callback(null, res.rows)
    })
};

負荷テストにはGatlingを使いました。
利用したコードは以下です。

constantTest.scala
  setUp(
    scn.inject(
      // 50人のアクセスを40秒間継続
      constantUsersPerSec(50) during(40 seconds)
    )
  ).protocols(httpConfig)

これでテストの準備は整いました。
あとは実行結果を待つだけです。

テスト結果

上記の設定でテストした結果が以下になります。

---- Global Information --------------------------------------------------------
> request count                                       2000 (OK=1839   KO=161   )
> min response time                                      0 (OK=144    KO=0     )
> max response time                                  57613 (OK=57613  KO=0     )
> mean response time                                   471 (OK=512    KO=0     )
> std deviation                                       2902 (OK=3023   KO=0     )
> response time 50th percentile                        259 (OK=262    KO=0     )
> response time 75th percentile                        279 (OK=281    KO=0     )
> response time 95th percentile                        454 (OK=479    KO=0     )
> response time 99th percentile                       1304 (OK=1410   KO=0     )
> mean requests/sec                                     20 (OK=18.39  KO=1.61  )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                          1802 ( 90%)
> 800 ms < t < 1200 ms                                   1 (  0%)
> t > 1200 ms                                           36 (  2%)
> failed                                               161 (  8%)
---- Errors --------------------------------------------------------------------
> j.u.c.TimeoutException: Request timeout to not-connected after    161 (100.0%)
 60000 ms
================================================================================

いくつか問題がありますね。

  • かなり遅いレスポンスが一部発生している
  • タイムアウトが発生している

何度か試しましたが、1700前後のシナリオを実行したところで、処理ができなくなっていました。
実行側の問題なのか、AWS側の問題なのかまで確認できず。
そこの検証を続けていたんですが、わからないまま、日付が変わりそうなので(12/9 22:00)、一旦の結果を放流です。
寂しい結果になりましたが、検証だとこういうこともありますね。
引き続き調査は続けて、わかったら追記したいと思います。

二ヶ月ほど前に試した際には、300ユーザーのアクセスを60秒間ほどやることで、API GatewayとVPC間のENIを限界(300)まで到達させることができたのですが、そこまでたどり着きませんでした。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc.html
実際にそうなると、この記事の一番下のような現象が発生します。
ENIがこれ以上作成できないため、Lambdaの起動ができず、その分が全て失敗します。
しかも、CloudWatchにログが出ないので、かなり厳しいです。
やはりアンチパターンはアンチパターンですね。

最後に

さて、AWS側からアンチパターンということになっているのは、それなりの理由があることがわかりました。
今後もこの方法は取れないかというと、まだわからないかと思っています。

先日のre:InventでAPI Gateway VPC integrationが発表されました。
これはAPI GatewayからVPCのリソースにアクセスする手段を提供したものです。
アーキテクチャの問題上、Lambdaには明示的なエンドポイントがあるわけではないので、現状はアクセスできません。
しかし、VPCへのアクセス手段が用意されたということで、展開に希望が持てないかなと考えています。
今後の発表が楽しみなところです。

続きを読む

【CS Hack 第1弾!!】アプリケーション内でファイルの受け渡しを実装する

はじめに

皆さん、カスタマーサクセスしてますか?価値あるプロダクトを作っていますか?
日々自分たちのカスタマーがより良い活動をできるよう、プロダクトを開発・運用しているかと思いますが、活動の中でのヒューマンエラーを無くす動きも大切です。

カスタマーサクセスの為に、検証作業も含め、頻繁にカスタマーとデータのやり取りをする場合(例: 提案資料、データの受け渡し…)、うっかりミスが致命的な問題につながり兼ねません。
メールやFBで気軽に連絡できる時代において、ヒューマンエラーによる情報漏えいはかなり大きな割合を占めております。

leak07.jpg

引用:情報漏洩の原因を徹底解析!原因と結果から学ぶ意識改革

カスタマーのより良い活動を支援しようと思ったのに・・・、その活動がサービスの停止へと導いてしまった、、、なんて事があったら悲しすぎますね。
そんなミスを防ぐ為に、今日はアプリケーション内でファイルをDLする機構を作ることで、セキュアな環境下でのやり取りを実現してみたいと思います。

イメージ図

スクリーンショット 2017-12-09 12.05.27.png

実装

サーバ側

まずは、AWS側の設定を記述します。

config/initializers/aws_s3.rb
Aws.config.update({
    credentials: Aws::Credentials.new(
      #ACCESS_KEY,
      #SECRET_ACCESS_KEY
    ),
    region: #region情報を
    endpoint: #endpoint情報を
})

# もし参照パスにルールがあるなら記述しておきます
module AwsS3
  BUCKET = 'test'.freeze
  BASIC_URL = "https://s3.console.aws.amazon.com/s3/object/#{BUCKET}".freeze
end

ベースのモデルを作成。

class S3Client
  def initialize(*_)
    @client = Aws::S3::Client.new
  end
end

まずはS3にあるファイルを引っ張ってくる処理。
例えばログインしているユーザに応じて、S3のパスを分けます。

class S3Reader < S3Client
  def initialize(user)
    super
    @user_id  = user.id
  end

  def prefix
    "#{Rails.env}/#{@user_id}/"
  end

  def objects
    contents = @client.list_objects(bucket: AwsS3::BUCKET, prefix: prefix).contents
    contents.map { |content| S3Reader::Object.new(content, prefix) }
  end
end

# objectの加工用
class S3Reader::Object
  attr_reader :key, :name, :modified_at

  def initialize(content, prefix)
    @key = content.key
    @name = content.key.gsub(prefix, '')
    @modified_at = content.last_modified
  end
end

controller側に下記を実装。するとS3からDL可能なObjectが引っ張られてきて表示されます。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end
end

S3にアップロードしたファイルが・・・

スクリーンショット 2017-12-09 12.23.08.png

アプリケーション側でも閲覧可能に。

スクリーンショット 2017-12-09 12.23.50.png

引っ張るところまで出来れば次はクリック後、DLできるようにします。
DLの方法としては、一定時間だけオブジェクトにアクセスできるURLを作成し、send_dateをすることで、ブラウザ側にDLされるようにします。

参照)
一定時間だけS3のオブジェクトにアクセスできるURLを生成する

class S3Downloader < S3Client
  # 一時的なURLは120秒に設定
  def download_url(key)
    Aws::S3::Presigner.new(client: @client).presigned_url(
      :get_object, bucket: AwsS3::BUCKET, key: key, expires_in: 120
    )
  end

  def content(key)
    S3Downloader::Object.new(open(download_url(key)), key)
  end
end

class S3Downloader::Object
  attr_reader :content, :key

  def initialize(content, key)
    @content = content
    @key = key
  end

  def name
    @key.split('/').present? ? @key.split('/')[-1] : 'download'
  end

  def read
    @content.read
  end

  # ファイルの種類によってはsend_date時に上手くcontent_typeが出せない場合があるので、オーバーライドしておく
  def type
    @content.content_type
  end
end

後は、リストをクリックした際のアクションをController側に実装。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end

  def download
    downloader = S3Downloader.new
    content = downloader.content(params[:key])
    send_data content.read, filename: content.name, type: content.type
  end
end

これらを実施し、クリックすると・・・

ダウンロード.png

無事DLができました。

最後に

今回は第1弾ということで、まずはファイルをDLできる機構を作ってみました。
ログインすることでDLできるので、カスタマーには、『サービス内にアップロードしたので、DLしてくださいね。』と伝えるだけでいけますね。

ヒューマンエラーを出来る限り排除し、価値にフォーカスできる体制をつくるのもプロダクト開発において非常に重要だと感じています!

第二弾はこの機構に自動でファイルをアップロードしていく仕組みを構築します。
これで活用レポートだったり、定期的に送るデータは自動化してしまいましょう。

続きを読む

AWS Cloud9でLambdaアプリの開発をしたり共同編集をしてみた

この記事はAmazon Web Services Advent Calendar 2017の9日目の記事になります。

結構気になっていたAWS re:Invent 2017での発表があります。

AWS Cloud9

です。実際どの程度便利なのか、使ってみました。

環境作成

利用開始をしようとすると、環境作成のページに飛ばされました。まずはAWS Cloud9が動く環境を準備する必要があるんですね。(そういえばオリジナルのCloud9もそうだったような。。。)

スクリーンショット 2017-12-05 19.31.16.png

とりあえずテスト環境を作成します。
テスト環境なので、設定はデフォルトで。30分以上放置してたら作成したインスタンスを止めてくれるとか、気が利いてますね!

スクリーンショット 2017-12-05 19.48.41.png

環境作成の直前に推奨の設定やらが案内されましたが、とりあえず無視します。

スクリーンショット 2017-12-05 19.52.29.png

環境が作成されるまでしばし待ち。。。(しかし画面かっこいいな)

スクリーンショット 2017-12-05 19.53.49.png

コーディングしてみる

環境が整えばいよいよコーディングです。が、色々と便利そうなビューになってます。
タブでファイルが分割できたり、

スクリーンショット 2017-12-05 20.29.42.png

ターミナルがあったり。これもうVisual Studio Codeと変わらないな。

スクリーンショット 2017-12-05 20.30.45.png

ちなみに実態はEC2インスタンスなので、上記のようにコマンドでライブラリをインストールすることができます。実際にインストールすると、左側のフォルダツリーにリアルタイムに構成が反映されます。

スクリーンショット 2017-12-05 20.30.58.png

で、コーディングはエディタのキャプチャの通り書いてみました。GoogleにRequestを飛ばす一般的な処理です。

実行してみる

もちろんこのままだとファイルパスがマッチせずに実行時にインポートエラーが発生するので

スクリーンショット 2017-12-05 20.30.31.png

こんな感じでライブラリをインストールするようにして、

$ mkdir lib
$ pip install requests -t ./testrequests/testrequests/lib/

ソースと同じディレクトリにライブラリをインストールするようにします。

スクリーンショット 2017-12-06 7.30.06.png

コードも少し修正しました。

import sys
import os

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))

import requests

def lambda_handler(event, context):
    res = requests.get("http://www.google.co.jp/")
    return res.status_code

これでうまく動きそうです。

もう一回実行してみる

スクリーンショット 2017-12-06 7.30.36.png

きちんと結果が得られました。無事動いたようです。
ただ、1点気をつけないといけないのは、あくまでこれはEC2インスタンス上のエディタで実行した、というだけなので、実際のLambdaには何も保存されていません。

ちなみに今回はPythonを使ったのでデバッグはサポートされていませんが、Node.jsやPHP、Goであればデバッグ実行もできるようです。これは便利。

スクリーンショット 2017-12-07 1.05.31.png

最初にファンクションを作成時、Lambdaは登録されるのですが、デプロイするまでは中身は初期化されたままになっています。(当然ではあるのですが、忘れやすいかもこれ)

スクリーンショット 2017-12-07 1.13.48.png

Lambdaでファンクションを選択しても、先ほど書いたコードは存在していません。デプロイをするとLambdaに実際に反映されるようになります。

デプロイをしてLambdaに更新を反映する

Cloud9上からLambdaへのデプロイを行います。やり方はとても簡単で、ファンクションリストから該当のファンクションを右クリックして「Deploy」を選択するだけ。正直Serverless Frameworkなどを使うよりずっと簡単です。

スクリーンショット 2017-12-07 1.16.42.png

これだけの操作でLambdaが更新されます。

共同編集をしてみる

他にアカウントを作成して、共同編集を試してみました。テスト用に「test」というアカウントを作成します。画面右上の「Share」より共有設定を開きます。

スクリーンショット 2017-12-07 1.22.19_dec.png

するとこんな感じの共有用URLが表示されますので、これを別アカウントのブラウザで開いてみます。が、共有の設定をしないともちろんアクセスできません。400で怒られます。

スクリーンショット 2017-12-07 1.23.02_dec.png

共有設定で自分以外に共有したいIAMユーザーのアカウントを追加すれば設定は完了です。

スクリーンショット 2017-12-07 1.23.21.png

testユーザーでアクセスすると、共有設定画面のユーザーのステータスがOfflineからOnlineに変更になります。

スクリーンショット 2017-12-07 1.23.47.png

あとはお互いに編集しているポイントがリアルタイムに反映されていくので、とても便利ですね。キャプチャだけだとなんのことかわかりづらいですが、自アカウントで編集したものがリアルタイムでtestユーザーの画面にも反映されます。

スクリーンショット 2017-12-07 1.24.44.png

スクリーンショット 2017-12-07 1.25.00.png
※キャプチャ右上のアカウントの並びが異なるので、それぞれ異なるユーザーの画面だということは見てわかるかな(^^;。

実際にAWS Cloud9を使ってみての感想

思った以上にできることが多いので、かなり可能性を感じました。自分は今仕事の関係上ミャンマーにいるのですが、多少ネットが遅くてもリアルタイムにインターネットにアクセスできれば、開発していて不便だと感じることはありませんでした。実際に開発したパッケージをデプロイしようとした際には、ローカル10MBなどの重いファイルをアップロードするために何分も待たされる、というのもCloud9ならなさそうですし、ありがたい新サービスです。

続きを読む