[JAWS-UG CLI] OpsWorks #2 レイヤの作成

前提条件

OpsWorksへの権限

OpsWorksに対してフル権限があること。

AWS CLIのバージョン

以下のバージョンで動作確認済

  • AWS CLI 1.11.14
コマンド
aws --version

結果(例):

  aws-cli/1.11.102 Python/2.7.12 Linux/4.4.11-23.53.amzn1.x86_64 botocore/1.5.65

バージョンが古い場合は最新版に更新しましょう。

コマンド
sudo -H pip install -U awscli

0. 準備

まず変数の確認をします。

変数の確認
cat << ETX

        AWS_DEFAULT_PROFILE:             (0.1) ${AWS_DEFAULT_PROFILE}
        OPSW_STACK_NAME:                 (0.2) ${OPSW_STACK_NAME}
        OPSW_CUSTOM_SG_GROUP_IDS         (0.3) ${OPSW_CUSTOM_SG_GROUP_IDS}
        OPSW_LAYER_NAME                  (0.4) ${OPSW_LAYER_NAME}
        OPSW_LAYER_SHORTNAME             (0.4) ${OPSW_LAYER_SHORTNAME}
        OPSW_LAYER_TYPE                  (0.5) ${OPSW_LAYER_TYPE}
        OPSW_LAYER_CUSTOM_RECIPES_DEPLOY (0.6) ${OPSW_LAYER_CUSTOM_RECIPES_DEPLOY}
        OPSW_LAYER_LIFECYCLE_TIMEOUT     (0.7) ${OPSW_LAYER_LIFECYCLE_TIMEOUT}
        OPSW_LAYER_LIFECYCLE_ELB_DRAINED (0.7) ${OPSW_LAYER_LIFECYCLE_ELB_DRAINED}

ETX

結果(例):

  AWS_DEFAULT_PROFILE:             (0.1) opsworksFull-prjZ-mbp13
  OPSW_STACK_NAME:                 (0.2) My Sample Stack (Linux)
  OPSW_CUSTOM_SG_GROUP_IDS         (0.3) <AWS-OpsWorks-WebAppセキュリティグループのID>
  OPSW_LAYER_NAME                  (0.4) Node.js App Server
  OPSW_LAYER_SHORTNAME             (0.4) nodejs-server
  OPSW_LAYER_TYPE                  (0.5) custom
  OPSW_LAYER_CUSTOM_RECIPES_DEPLOY (0.6) nodejs_demo
  OPSW_LAYER_LIFECYCLE_TIMEOUT     (0.7) 120
  OPSW_LAYER_LIFECYCLE_ELB_DRAINED (0.7) false

変数が入っていない、適切でない場合は、それぞれの手順番号について作業を
行います。

0.1. プロファイルの指定

プロファイルの一覧を確認します。

コマンド
cat ~/.aws/credentials 
       | grep '[' 
       | sed 's/[//g' | sed 's/]//g'

結果(例):

  iamFull-prjz-mbpr13
  opsworksFull-prjZ-mbp13
変数の設定
export AWS_DEFAULT_PROFILE='opsworksFull-prjZ-mbp13'

0.2. スタック名の指定

変数の設定
OPSW_STACK_NAME='My Sample Stack (Linux)'
コマンド
OPSW_STACK_ID=$( 
        aws opsworks describe-stacks 
          --query "Stacks[?Name ==`${OPSW_STACK_NAME}`].StackId" 
          --output text 
) 
        && echo ${OPSW_STACK_ID}

結果(例):

  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
変数の設定
ARRAY_OPSW_STACK_IDS="${OPSW_STACK_ID}" 
        && echo ${ARRAY_OPSW_STACK_IDS}

0.3. セキュリティグループIDの取得

変数の設定
VPC_ID=$( 
        aws opsworks describe-stacks 
          --stack-ids $ARRAY_OPSW_STACK_IDS 
          --query 'Stacks[].VpcId' 
          --output text 
) 
        && echo ${VPC_ID}
変数の設定
EC2_SG_NAME='AWS-OpsWorks-WebApp'
変数の設定
EC2_SG_ID=$( 
        aws ec2 describe-security-groups 
          --query "SecurityGroups[?VpcId == `${VPC_ID}` && GroupName ==`${EC2_SG_NAME}`].GroupId" 
          --output text 
) 
        && echo ${EC2_SG_ID}
変数の設定
OPSW_CUSTOM_SG_GROUP_IDS="${EC2_SG_ID}" 
        && echo ${OPSW_CUSTOM_SG_GROUP_IDS}

0.4. レイヤ名の指定

変数の設定
OPSW_LAYER_NAME='Node.js App Server'
変数の設定
OPSW_LAYER_SHORTNAME='nodejs-server'

0.5. レイヤタイプの指定

変数の設定
OPSW_LAYER_TYPE='custom'

0.6. カスタムレシピの指定

変数の設定
OPSW_LAYER_CUSTOM_RECIPES_DEPLOY='nodejs_demo'

0.7. ライフサイクルイベントの指定

変数の設定
OPSW_LAYER_LIFECYCLE_TIMEOUT='120'
変数の設定
OPSW_LAYER_LIFECYCLE_ELB_DRAINED='false'

再確認

設定されている変数の内容を再確認します。

変数の確認
cat << ETX

        AWS_DEFAULT_PROFILE:             (0.1) ${AWS_DEFAULT_PROFILE}
        OPSW_STACK_NAME:                 (0.2) ${OPSW_STACK_NAME}
        OPSW_CUSTOM_SG_GROUP_IDS         (0.3) ${OPSW_CUSTOM_SG_GROUP_IDS}
        OPSW_LAYER_NAME                  (0.4) ${OPSW_LAYER_NAME}
        OPSW_LAYER_SHORTNAME             (0.4) ${OPSW_LAYER_SHORTNAME}
        OPSW_LAYER_TYPE                  (0.5) ${OPSW_LAYER_TYPE}
        OPSW_LAYER_CUSTOM_RECIPES_DEPLOY (0.6) ${OPSW_LAYER_CUSTOM_RECIPES_DEPLOY}
        OPSW_LAYER_LIFECYCLE_TIMEOUT     (0.7) ${OPSW_LAYER_LIFECYCLE_TIMEOUT}
        OPSW_LAYER_LIFECYCLE_ELB_DRAINED (0.7) ${OPSW_LAYER_LIFECYCLE_ELB_DRAINED}

ETX

結果(例):

  AWS_DEFAULT_PROFILE:             (0.1) opsworksFull-prjZ-mbp13
  OPSW_STACK_NAME:                 (0.2) My Sample Stack (Linux)
  OPSW_CUSTOM_SG_GROUP_IDS         (0.3) <AWS-OpsWorks-WebAppセキュリティグループのID>
  OPSW_LAYER_NAME                  (0.4) Node.js App Server
  OPSW_LAYER_SHORTNAME             (0.4) nodejs-server
  OPSW_LAYER_TYPE                  (0.5) custom
  OPSW_LAYER_CUSTOM_RECIPES_DEPLOY (0.6) nodejs_demo
  OPSW_LAYER_LIFECYCLE_TIMEOUT     (0.7) 120
  OPSW_LAYER_LIFECYCLE_ELB_DRAINED (0.7) false

1. 事前作業

2. 本作業

作成

変数の設定
OPSW_LAYER_CUSTOM_RECIPES_STRING="Deploy=${OPSW_LAYER_CUSTOM_RECIPES_DEPLOY}" 
        && echo ${OPSW_LAYER_CUSTOM_RECIPES_STRING}
変数の設定
OPSW_LAYER_LIFECYCLE_STRING="Shutdown={ExecutionTimeout=120,DelayUntilElbConnectionsDrained=false}" 
        && echo ${OPSW_LAYER_LIFECYCLE_STRING}
変数の確認
cat << ETX

        AWS_DEFAULT_REGION:               ${AWS_DEFAULT_REGION}
        OPSW_STACK_DI:                    ${OPSW_STACK_ID}
        OPSW_LAYER_NAME:                  ${OPSW_LAYER_NAME}
        OPSW_LAYER_TYPE:                  ${OPSW_LAYER_TYPE}
        OPSW_LAYER_SHORTNAME:             ${OPSW_LAYER_SHORTNAME}
        OPSW_CUSTOM_SG_GROUP_IDS:         ${OPSW_CUSTOM_SG_GROUP_IDS}
        OPSW_LAYER_CUSTOM_RECIPES_STRING: ${OPSW_LAYER_CUSTOM_RECIPES_STRING}
        OPSW_LAYER_LIFECYCLE_STRING:      ${OPSW_LAYER_LIFECYCLE_STRING}

ETX
コマンド
aws opsworks create-layer 
        --stack-id ${OPSW_STACK_ID} 
        --name "${OPSW_LAYER_NAME}" 
        --type ${OPSW_LAYER_TYPE} 
        --shortname ${OPSW_LAYER_SHORTNAME} 
        --custom-security-group-ids ${OPSW_CUSTOM_SG_GROUP_IDS} 
        --custom-recipes ${OPSW_LAYER_CUSTOM_RECIPES_STRING} 
        --no-auto-assign-elastic-ips 
        --enable-auto-healing 
        --auto-assign-public-ips 
        --no-use-ebs-optimized-instances 
        --lifecycle-event-configuration ${OPSW_LAYER_LIFECYCLE_STRING}

結果(例):

  {
    "LayerId": "eb94325c-8b81-4ee8-87e0-8e9df3a4e4ee"
  }

2.2. レイヤIDの取得

変数の設定
OPSW_LAYER_ID=$( 
        aws opsworks describe-layers 
          --stack-id $OPSW_STACK_ID 
          --query "Layers[?Name == `${OPSW_LAYER_NAME}`].LayerId" 
          --output text 
) 
        && echo ${OPSW_LAYER_ID}

3. 事後作業

変数の設定
ARRAY_OPSW_LAYER_IDS="${OPSW_LAYER_ID}" 
        && echo ${ARRAY_OPSW_LAYER_IDS}
コマンド
aws opsworks describe-layers 
        --layer-ids ${ARRAY_OPSW_LAYER_IDS}

完了

続きを読む

AWS Lambda で動的HTMLコンテンツを配信する(Lambdaプロキシ統合を利用)

静的な内容ならS3に置いてWEBホスティングを有効にすればいいのですが、動的にHTMLを生成する場合にLambdaでHTMLを生成してレスポンスする方法です。この方法だとEC2等のサーバを運用する必要がありません。

Lambda

例として適当なHTMLの内容を返すコードは次のとおりです。

exports.handler = (event, context, callback) => {
  // もしQueryStringから値を取り出したい場合はevent.queryStringParametersから取得する

  // 動的にHTMLの内容を作成
  const html = `
  <html>
    <meta http-equiv="Content-Type" content="text/html" charset="utf-8">
    <body>
      <h1>Test Page</h1>
      こんにちわ!
    </body>
  </html>`;

  // Lambdaプロキシ統合の場合は下記のようなObjectでレスポンスを返せる
  const response = {
    statusCode: 200,
    headers: {
      'Content-Type': 'text/html',
    },
    body: html,
  };

  callback(null, response);
};

API Gateway

Lambdaプロキシ統合の使用にチェックをつけて、先程 作成したLambdaを指定します。

スクリーンショット_2017-06-26_11_05_26.png

動作確認

上記のAPIをAWSのコンソールからデプロイして、

スクリーンショット 2017-06-26 11.12.27.png

ブラウザでアクセスしてみます。

スクリーンショット 2017-06-26 11.14.52.png

補足

Lambdaが起動するのに時間がかかるので、実際の運用で利用する場合はAPI Gatewayでキャッシュを設定するか、CloudFront等のCDNを利用するのがよいかと思います。

以上

続きを読む

LoRaWANとSORACOMFunnelのAWSIoTアダプタを使ってDynamoDBにデータを書き込む

はじめに

SORACOMFunnelがAWSIoTに対応しましたね!
ちょうど仕事の関係でSORACOMのシールドが届いたし、近くにLoRaWANのPublicGWもあることだし・・・
ということでちょいと触ってみやした

筆者は今まで、主にKinesisアダプターを利用してデータの収集を行っています
簡単にまとめると、
1. KinesisStreamにセンサーからデータを投げて
2. Lambdaをキックして
3. DynamoDBに突っ込む
といった構成です

また普段からAWSは使っているのですが、AWSIoTを使ってみたことがほとんどなかったので、勉強がてらAWSIoTアダプターでデータの収集をしてみた

やりたいこと

  1. LoRaデバイスからLoRaゲートウェイを通ってFunnel AWSIoTアダプターを利用してAWSIoTにセンサーデータを投げる
  2. AWSIoTが受け取ったデータの中にHEX形式でセンサーデータが格納されているので、デコードするためのLambdaファンクションをキックする
  3. Lambdaでデータをデコードして必要なデータをJSON形式にまとめて、DynamoDBにPUTする

SORACOM Funnelって?

SORACOM Funnel(以下、Funnel) は、デバイスからのデータを特定のクラウドサービスに直接転送するクラウドリソースアダプターです。
Funnel でサポートされるクラウドサービスと、そのサービスの接続先のリソースを指定するだけで、データを指定のリソースにインプット
することができます。

http://soracom.jp/services/funnel/より抜粋
要するに、デバイスからAWSなどのクラウド上に閉域網でデータを送信することができるサービス(合ってるかな・・・)

AWSIoTって?

AWS IoT によって、さまざまなデバイスを AWS の各種 Services や他のデバイスに接続し、データと通信を保護し、
デバイスデータに対する処理やアクションを実行することが可能になります。
アプリケーションからは、デバイスがオフラインの状態でもデバイスとのやり取りが可能です。

https://aws.amazon.com/jp/iot-platform/how-it-works/より抜粋
うーん、なるほどわからん。とりあえず使ってみよう

デバイス側の設定

同じ部署の電気系強いお方が気づいたらセッティングしていただいていましたので割愛
この時点でSORACOM Harvestにてデータが送信されているのを確認できている状態

AWSIoTの設定

Funnelでデータを送信する先のAWSIoTを作成します

エンドポイントを控える

Funnelを設定する際に必要なAWSIoTのエンドポイントを控えておきます

AWSIoT_TOP.PNG

Ruleを作成する

左のサイドメニューから「Rule」を選択し、「Create a rule」をクリック

AWSIoT_Rule.PNG

「Name」と「Description」を入力する(Descriptionは任意)

AWSIoT_Rule_name.PNG

「Attribute」に「*」、「Topic filter」に「IoTDemo/#」を入力
AWSIoTはエンドポイントいかにTopic(今回では「IoTDemo/#」)を指定してリクエストを送ることで、それと一致するTopicFilterを持つRuleが呼ばれます
「Using SQL version」は「2016-03-23」で問題なければそのままでOK

AWSIoT_Rule_massage.PNG

「Set one or more actions」の「add action」をクリック

AWSIoT_Rule_set_action.PNG

今回はLambdaでデコードする必要があるため「Invoke a Lambda function passing the message data」を選択

AWSIoT_Rule_select_lambda.PNG

「Configure action」を選択

AWSIoT_Rule_select_lambda_button.PNG

キックするLambda Functionを選択
今回は初めて作成するので、Lambdaが呼ばれたときのeventの中身をログに吐き出すLambdaを作成して、それをキックするようにします
※DynamoDBに格納する処理は後ほど実装

「Create a new resouce」をクリック。Lambdaのページに遷移します

AWSIoT_Rule_lambda_create.PNG

「Blank Function」を選択

Lambda_create.PNG

Lambdaのトリガーを設定
「IoTタイプ」は「カスタムIoTルール」を選択
「ルール名は」現在作成中のルール名
「SQLステートメント」は作成中の「Rule query statement」の中身をコピー
「次へ」をクリック

Lambda_trigger.PNG

「名前」はお好きなFunction名をつけてください
「ランタイム」は筆者の好みによりNode.jsです
コードには

exports.handler = (event, context, callback) => {
    console.log(event);
};

と書いておいてください。

Lambda_setting.PNG

あとは、DynamoDBの権限を持ったロールを選択(作成)して、ページ下部の「次へ」をクリックしてLambdaFunctionを作成してください

AWSIoTのページに戻って、先ほど作成したLambdaFunctionを選択し、「Add action」をクリック

AWSIoT_Rule_add_lambda.PNG

その後「create Rule」をクリックするとRuleが作成されます
これでAWSIoTのRule作成が完了です

SORACOM Funnelの設定

まず、SORACOMコンソールにログインし、再度メニューから「LoRaグループ」⇒「追加」をクリックします
ポップアップが出てきてグループ名を入力するように言ってくるので、任意のグループ名を入力しグループを作成します

作成したグループを選択し、設定画面に移動します

転送先サービス:AWS IoT
転送先URL:https:///rule内で作成したSQLTopicFilter/#{deviceId}
認証情報:AWSIoTの権限を持ったIAMアカウント情報で作成したもの
送信データ形式:無難にJSON

funnel_setting.PNG

※転送先URLにはプレースホルダーを作成することができます
  - SIMを利用する場合:{imsi}
  - LoRaデバイスを利用する場合:{deviceId}

これでFunnelの設定は完了です

Lambdaの実装

デバイスの電源を入れ、データが送信されるようになると、Lambdaが起動してeventの中身をログに吐き出していると思います
↓こんな感じ

2017-06-23T04:13:59.850Z 62014535-57ca-11e7-b4e4-9fbd147f2037 { 
  operatorId: '0123456789',
  timestamp: 1498191237793,
  destination: { 
    resourceUrl: 'https://xxxxxxxxx.iot.ap-northeast-1.amazonaws.com/xxxxxxx/#{deviceId}',
    service: 'aws-iot',
    provider: 'aws' 
  },
  credentialsId: 'iot-sys',
  payloads: { 
    date: '2017-06-23T04:13:54.276320',
    gatewayData: [ [Object] ],
    data: '7b2268223a36312e367d',
    deveui: '1234567890' 
  },
  sourceProtocol: 'lora',
  deviceId: '1234567890' 
}

センサーから送られてくるデータはevent[“payloads”][“data”]にHEX形式で格納されているので、取り出してデコードする必要があります。


const data = event["payloads"]["data"];
const decodeData = new Buffer(data, "hex").toString("utf8");

デコードすると「7b2268223a36312e367d」⇒「{“h”: 61.6}」のようなString型になります(これは一例)

Object型のほうが使い勝手がよいので、parseしてしまいましょう


const parseData = JSON.parse(decodeData); // {h : 61.6}

あとはDynamoDBにputで投げつけます

index.js
"use strict";

const AWS = require("aws-sdk");
const co = require("co");
const moment = require("moment-timezone");

const dynamodb = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1"
});

const dynamoPutData = require("./lib/dynamo_put_data");

exports.handler = (event, context, callback) => {
  // UTCなのでJSTに変換
  const date = event["payloads"]["date"];
  const time = moment(date).tz("Asia/Tokyo").format();
  // HEX形式をデコード
  const data = event["payloads"]["data"];
  const decodeData = new Buffer(data, "hex").toString("utf8");
  // Object型に変換
  const parseData = JSON.parse(decodeData);
  // deviceIdを取得
  const deviceId = event["deviceId"];

  // DynamoDBにPUTするItem
  const item = [{
    deviceId: deviceId,
    time: time,
    value: parseData
  }];

  co(function *() {
    yield dynamoPutData.putDynamoDB(dynamodb, item[0]);
  }).then(() => {
    console.log("success!")
  }).catch((err) => {
    console.log(err);
  });
};

dynamo_put_data.js
"use strict";

class dynamoPutData {
  /**
   * DynamoDBへのPUT処理
   * @param {DocumentClient} dynamoDB
   * @param item
   * @returns {Promise}
   */
  static putDynamoDB(dynamoDB, item) {
    const params = {
      TableName: "TABLE_NAME",
      Item: item
    };
    return dynamoDB.put(params).promise();
  }
}

module.exports = dynamoPutData;

dynamo_put_data.js中の”TABLE_NAME”にはデータを投げつけるテーブル名を書いてください
関数を外だしして複数ファイルがあるので、Lambdaにはソースコード一式をZIPに固めてアップする方法でデプロイを行います
データが送られてきてLambdaがキックされると、DynamoDBにデータが格納されていると思います

まとめ

日ごろからAWSのサービスを使っていましたが、AWSIoTを利用する機会がなくとてもいい経験になりました。
今回はデバイスからクラウドといった方向でしたが、AWSIoTを利用すればその逆方向も実現することができるらしいので、近々そういった実装もしてみたと思います

では!

続きを読む

LambdaでAWSの料金を毎日Slackに通知する(Python3)

はじめに

個人アカウントは基本的に無料枠で運用しているので、少しでも請求がある場合はいち早く気づきたいです。
先日、とあるハンズオンイベントで使ったリソースを消し忘れて、最終的に$30ぐらい請求が来てしまいました。。。

CloudWatchで請求アラートは設定していますが、閾値超えが想定の場合、当然見逃すことになり、最終的な請求額に驚くハメになります。

これを防ぐためにLambdaで毎日SlackにAWS料金を通知することにします。

先日LambdaがPython3に対応したので、せっかくだし勉強がてらPython3で実装したい。
ネット上にはNode.jsでの実装例が多いようで、今回はこちらを参考にPython3で実装してみます。

必要なもの

  • Slack

    • incoming-webhooks URL

    • 適当なchannel
  • lambda-uploader
    • requestsモジュールをLambda上でimportするために利用

      • カレントディレクトリにモジュールをインストールして、モジュールごとZipに固めてアップロードでもいけるはずですが、私の環境だとうまくいかなかったので
  • aws cli
    • lambda-uploaderで必要
  • AWS
    • Lambda関数用IAM Role

      • CloudWatchReadOnlyAccessポリシーをアタッチ

事前準備

lambda-uploaderをインストール

$ pip install lambda-uploader 

こちらを参考にさせていただきました。

aws cliをインストール

$ pip install awscli

credential、リージョン設定

$ aws configure

確認
$ aws configure list

コード

ディレクトリ構成

ディレクトリ名は任意です。関数名とは無関係です。

ディレクトリ構成
awscost_to_slack/
|--lambda_function.py
|--requirements.txt
|--lambda.json

lambda_function.py

超過金額に応じて色をつけるようにしています。
\$0.0なら緑、超過したら黄色、\$10超えで赤になります。

lambda_function.py
#!/usr/bin/env python
# encoding: utf-8

import json
import datetime
import requests
import boto3
import os
import logging

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

# Slack の設定
SLACK_POST_URL = os.environ['slackPostURL']
SLACK_CHANNEL = os.environ['slackChannel']

response = boto3.client('cloudwatch', region_name='us-east-1')

get_metric_statistics = response.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'])

cost = get_metric_statistics['Datapoints'][0]['Maximum']
date = get_metric_statistics['Datapoints'][0]['Timestamp'].strftime('%Y年%m月%d日')

def build_message(cost):
    if float(cost) >= 10.0:
        color = "#ff0000" #red
    elif float(cost) > 0.0:
        color = "warning" #yellow
    else:
        color = "good"    #green

    text = "%sまでのAWSの料金は、$%sです。" % (date, cost)

    atachements = {"text":text,"color":color}
    return atachements

def lambda_handler(event, context):
    content = build_message(cost)

    # SlackにPOSTする内容をセット
    slack_message = {
        'channel': SLACK_CHANNEL,
        "attachments": [content],
    }

    # SlackにPOST
    try:
        req = requests.post(SLACK_POST_URL, data=json.dumps(slack_message))
        logger.info("Message posted to %s", slack_message['channel'])
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)

requirements.txt

pip installしたいモジュール名を書きます。

requirements.txt
requests

lambda.json

Lambda関数名、IAM RoleのARNなどは環境に合わせてください。
スクリプト本体のファイル名とハンドラの前半を一致させないと動きません。地味にハマるので注意!

lambda.json
{
  "name": "LAMBDA_FUNCTION_NAME",
  "description": "DESCRIPTION",
  "region": "ap-northeast-1",
  "handler": "lambda_function.lambda_handler",
  "role": "arn:aws:iam::XXXXXXX:role/ROLE_NAME_FOR_LUMBDA",
  "timeout": 300,
  "memory": 128,
  "variables":
    {
      "slackPostURL":"https://hooks.slack.com/services/XXX",
      "slackChannel":"#XXX"
    }
}

デプロイ

上記ファイルを配置したディレクトリに移動して、lambda-uploaderを実行します。

$ cd awscost_to_slack
$ lambda-uploader
λ Building Package
λ Uploading Package
λ Fin

Lambda環境変数設定

今回のLambda関数では、通知先SlackチャネルとWebhooks URLを環境変数で渡すようにしたので、設定します。

スクリーンショット 2017-06-23 14.51.30.png

lambda-uploaderのlambda.jsonに書けそうなのですが、書式が分からず、今回はマネコンで設定しました。
lambda-uploaderでLambda関数を更新すると消えてしまうので注意。

上記のようにlambda.jsonのvariablesで定義すれば環境変数も設定可能です。uploadのたびに消えることもありません。

Lambda定期実行設定

CloudWatchのスケジュールイベントを定義して、lambda関数をターゲットに指定します。
時刻はUTCなので注意しましょう。
毎日UTC0:00に実行されるよう設定しました。

スクリーンショット 2017-06-23 15.01.27.png

実行イメージ

スクリーンショット 2017-06-23 14.15.54.png
こんな感じで毎朝9:00に通知がきます。
今日も無料!

まとめ

Lambdaはほぼ無料でプログラムが動かせるので楽しいですね!
Python初心者なのでコードが見苦しい点はご容赦ください。

続きを読む

AWS LambdaにMecabを乗せてPythonで動かす

実装するにあたりこちらのサイトを参考にさせて頂きました!
ありがとうございます!

pythonのバージョンは2.7
localの環境はWindows bashです。

はじめに

Mecabはコードにネイティブバイナリを使用しているため、Lambdaの実行環境と同じ環境でデプロイパッケージを作成する必要があります。
(ソースはここです。Lambdaの実行環境ドキュメント
なので、EC2でAmazon Linuxインスタンスを立て、その中でデプロイパッケージを作成していきます。

必要なファイル及び作成環境の準備

まず必要なファイルをローカル上にDLします。
① mecab-0.996.tar.gz
② mecab-ipadic-2.7.0-20070801.tar.gz
上記2つはここからDLできます
③ mecab-python-0.996.tar.gz
ここからDLできます。

次にlinuxインスタンスにsshでログインします。
やり方分からないよって方は、AWSのドキュメントの「Linuxインスタンスへの接続」の項を参照していただけると分かると思います!

Linuxインスタンスは、立てたばかりの状態ではコンパイラなどが入っていないのでインストールします。

Linuxインスタンス上
[ec2-user ~]$ sudo yum groupinstall "Development Tools"

そしてLinuxインスタンスのホームディレクトリにmecab-functionディレクトリを作成します。

Linuxインスタンス上
$mkdir mecab-function

先ほどDLした①〜③のファイルを全てLinuxインスタンスのホームディレクトリに送信します。
(こちらも詳細はドキュメントの「scpを利用してファイルを転送するには」の項を参照していただけると分かると思います。)

ローカル上
scp -i /path/秘密キーファイルの名前 /path/mecab-0.996.tar.gz ec2-user@インスタンスのパブリックDNS名:~

scp -i /path/秘密キーファイルの名前 /path/mecab-ipadic-2.7.0-20070801.tar.gz ec2-user@インスタンスのパブリックDNS名:~

scp -i /path/秘密キーファイルの名前 /path/mecab-python-0.996.tar.gz ec2-user@インスタンスのパブリックDNS名:~

ここまで出来たら、Linuxインスタンスのホームディレクトリには以下のファイルやディレクトリが存在してると思います。

mecab-function/
mecab-0.996.tar.gz
mecab-ipadic-2.7.0-20070801.tar.gz
mecab-python-0.996.tar.gz

次は、今落としたmecabなどをディレクトリにインストールしていきます。

ディレクトリへのインストール

ここからは、Linuxインスタンス上での操作となります。
まず、mecabとmecab-ipadicを解凍してインストールしていきます。

① mecabのインストール

$tar zvxf mecab-0.996.tar.gz
$cd mecab-0.996
$./configure --prefix=$DIR_HOME/local --with-charset=utf8
$make
$make install

“$DIR_HOME”は、mecab-functionディレクトリのパスです。
mecab-functionディレクトリ内で以下のコマンドを実行すると、パスを取得できます。

$pwd

“–prefix=ディレクトリのパス”で、mecabをインストールするディレクトリを指定しています。

“–with-charset=utf8″は、mecabをutf8で使用するという宣言をしています。
この宣言をしないと、mecabが上手くparseできなかったり、エラーが出たりするので注意してください。
ちなみに、”–enable-utf8-only”とは違うのでこちらも注意してください。

“make install”が完了したら、mecab-0.996ディレクトリから、ホームディレクトリに戻ります。

② mecab-ipadicのインストール

$tar zvxf mecab-ipadic-2.7.0-20070801.tar.gz
$cd mecab-ipadic-2.7.0-20070801

# mecabの場所をPATHに追加
$export PATH=$DIR_HOME/local/bin:$PATH

$./configure --prefix=$DIR_HOME/local --with-charset=utf8
$make
$make install

“$DIR_HOME”など、①と同じです。
“make install”が終了したら、ホームディレクトリに戻ります。
ここで、”mecab”コマンドを実行した時に動作すればmecabとmecab-ipadicのインストールは正しくできています。

③mecab-pythonのインストール

$pip install mecab-python-0.996.tar.gz -t $DIR_HOME/lib

pipは、-tでライブラリのインストール先を指定できます。

ここまで出来ると、mecab-function内は以下の構造になっていると思います。

mecab-function
|- lib/
|- local/
|- exclude.lst
|- function.py

function.pyとexclude.lstって何?ってなりますよね?
次でその2つのファイルについて説明します。

function.pyの説明

Lambdaのハンドラー関数を含むpythonのコードを説明します。
今回は、入力された日本語を単に分かち書きして出力するハンドラー関数を作成しました。

function.py
#coding: utf-8

import json
import os
import ctypes

#ライブライのパスを取得
libdir = os.path.join(os.getcwd(), "local", "lib")
libmecab = ctypes.cdll.LoadLibrary(os.path.join(libdir, "libmecab.so.2"))

import MeCab

#mecabの辞書(ipadic)へのパスを取得
dicdir = os.path.join(os.getcwd(), "local", "lib", "mecab", "dic", "ipadic")
#mecabのrcファイルへのパスを取得
rcfile = os.path.join(os.getcwd(), "local", "etc", "mecabrc")
tagger = MeCab.Tagger("-d{} -r{}".format(dicdir, rcfile))


def handler(event, context):
    """
    event = {
        "sentence": 分かち書きしたい文章
    }
    """
    sentence = event.get("sentence")
    encode_sentence = sentence.encode("utf_8")
    node = tagger.parseToNode(encode_sentence)
    result = []
    while node:
        surface = node.surface
        if surface != "":
            test_list = [surface]
            print surface
            print test_list
            decode_surface = surface.decode("utf_8")
            result.append(decode_surface)
        node = node.next
    return result

“ctypes.cdll.LoadLibrary()”で、動的リンクライブラリをロードしています。
また、MeCab.Taggerクラスのオブジェクトを作成する際に、辞書とmecabrcファイルへのパスを指定してあげる必要があります。

exclude.lstの説明

zipファイル作成時に除外するファイルの一覧です。

exclude.txt
*.dist-info/*
*.egg-info
*.pyc
exclude.lst
local/bin/*
local/include/*
local/libexec/*
local/share/*

デプロイパッケージの作成

いよいよデプロイパッケージの作成です。
mecab-function/libにある

_MeCab.so
MeCab.py
MeCab.pyc

の3つのファイルをmecab-fucntionに移します。
ここまで行えば、以下のような感じになってると思います。

mecab-function
|- lib/
|- local/
|- _MeCab.so
|- exclude.lst
|- function.py
|- MeCab.py
|- MeCab.pyc

そして、mecab-functionディレクトリで以下のコマンドを実行すると、mecab-function.zipが作成されます。

$zip -r9 mecab-function.zip * -x@exclude.lst

mecab-function.zipが以下のような構造になっていたら完成です。

mecab-function
|- lib/
|- local/
|- _MeCab.so
|- function.py
|- MeCab.py
|- MeCab.pyc

あとは、mecab-function.zipをLambdaにあげれば完了です!
Linuxインスタンス上にあるmecab-function.zipをscpコマンドを使ってローカルに転送してAWS Lambdaのコンソール画面から直接あげてもいいですし、S3に飛ばしてからあげてもいいと思います。
自分は、ローカルに一旦転送しました。

ローカルに転送するには、ローカルで以下のコマンドを実行します。

ローカル上
scp -i /path/秘密キーファイルの名前 ec2-user@インスタンスのパブリックDNS名:~/mecab-function/mecab-function.zip /転送したい場所のpath(ローカル上)/mecab-function.zip

このコマンドの詳しい内容は、おなじみAWSのドキュメントの「scpを利用してファイルを転送するには」を参照していただければと思います。

まとめ

最後に重要なポイントをまとめたいと思います。

① デプロイパッケージは、Linuxインスタンス上で作成する!
② mecabをimportする時に、動的リンクライブラリをロードする。
③ MeCab.Taggerでオブジェクト作成時に、辞書とmecabrcファイルへのパスを渡してあげる。
④デプロイパッケージの構造は、この記事で示したようにする。

ここまで読んで頂きありがとうございました。

続きを読む

SlackからAWS API Gatewayを通してLambdaを起動するまで

AWS LambdaとAPI Gatewayを使いこなすためにいい練習になるかなと思って
SlackからLambdaをキックしたり、起動したLambda functionのログとかをSlackに通知するような仕組みを作ってみる。

構成はこんな感じ
スクリーンショット 2017-06-11 23.57.06.png

この構成ができれば、いわゆるサーバレスアーキテクチャが出来るってことになるのでなかなかアツい。
Lambdaが出来てからサーバレスアプリケーションが最近注目を集めてますよね。
それにbotのためにサーバー(コンテナ)を立てる必要もなくなるのでエコな構成になるんじゃないだろうか。
それではさっそくチャレンジしてみる。

AWS Lambdaについて

Lambdaを触ったことがないって人にはこの記事がおすすめ。細かく丁寧に説明されています。
http://qiita.com/s_s_k/items/b584435120e99d63975b

Lambdaを準備する

まず初めにLambdaに処理を書いていきます。
Node.jsで書く必要があります。javaでも書けるそうですがNode.jsの方がレスポンス早そうなのでこちらを採用します。
内容は簡単で、いくつかのStringメッセージを json 形式に格納して、ランダムに返却するというもの。

index.js
exports.handler = (event, context, callback) => {

    var res = {};
    res.username = "outgoing-webhook-from-lambda";
    var array = [
        'わからないことは<https://api.slack.com/custom-integrations|こちら>を参照してください。',
        'こんにちは、私は'+ res.username +'です。nAWSのLambda functionで処理されています。n',
        'API Gatewayを使ってLambda functionを起動する良い練習になりますね。',
        'outgoing-webhookを使用すればLambda functionをAPI Gateway経由でキックすることができます。'
    ];
    res.text = array[Math.floor(Math.random() * array.length)];

    callback(null, res);
};

今回はSlackに返却するのでOutgoing WebHooksが受けられるような形式にしています。以下がその形式。


{
"username": "発言するbotのユーザ名";
"text": "表示されるテキスト";
}

API Gatewayの準備

Lambda functionを起動するためにAPI GatewayからAPI(POST)してみましょう。
testというメソッドを作成しています。統合リクエストには先ほど設定したLambdaを書いておきましょう。

スクリーンショット 2017-06-13 00.03.11.png

統合リクエストの詳細をみると以下のようになっています。
スクリーンショット 2017-06-13 00.06.03.png

注意しなければいけないのが「本文マッピングテンプレート」の部分。
Slackから飛んでくるPOSTは形式が jsonではなく、form-urlencodedで送られてくるらしい。
そこで「application/x-www-form-urlencoded」の時は json 型に変換するマッピングを定義します。
以下のようにしてあげれば良いと思います。

mapping_template.txt
## convert HTML POST data or HTTP GET query string to JSON

## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path('$'))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}

スクリーンショット 2017-06-13 00.13.00.png
この部分ですね。

ここまでできればAPIをデプロイしてやって、URLを作成しましょう。

Slack側の設定

Slack Outgoing WebHooks

integrationメニューから Outgoing WebHooks を検索してください。

image.png

流れにしたがって設定していってください。
ここに先ほどAPI Gatewayで作成したURLを貼り付けるだけでOK

image.png

動かして見ましょう。
いい感じにできました。

image.png

次回はLambdaをDBに繋ぐ or Cognito の UserPool を使って認証を簡単に作成したりしてみようと思います。

続きを読む

[stripe][AWS] Stripe の Event を CloudWatch custom metrics に記録

Stripe では、各種イベントを Webhook としてぶん投げてくれる仕組みがあります。
以前、その Webhook を Serverless Framework 使って API Gateway + Lambda で処理する記事書きました。
Stripe の Webhook を API Gateway で受け取って Lambda で処理する

それを応用して、Event Type ごとに CloudWatch のカスタムメトリクスに記録しておくとテンションあがるかなーと思って、こんなん書いてみました。

Stripe の Event を CloudWatch custom metrics に記録

Lambda で Webhook 受け取って CloudWatch にカスタムメトリクス登録

Lambda は node.js 6.10 でやりましょう。
ソースはこんな感じです。

handler.js
'use strict';

module.exports.incoming = (event, context, callback) => {
    const stripe = require('stripe')(process.env.STRIPE_API_KEY); // eslint-disable-line
    let response = {
        statusCode: 200,
        body: ''
    };

    try {
        // Parse Stripe Event
        const jsonData = JSON.parse(event.body); // https://stripe.com/docs/api#event_object

        // Verify the event by fetching it from Stripe
        console.log("Stripe Event: %j", jsonData); // eslint-disable-line
        stripe.events.retrieve(jsonData.id, (err, stripeEvent) => {
            const AWS = require('aws-sdk');
            const cloudwatch = new AWS.CloudWatch({apiVersion: '2010-08-01'});
            const eventType = stripeEvent.type ? stripeEvent.type : '';
            const params = {
                Namespace: process.env.NAMESPACE,
                MetricData: [
                    {
                        MetricName: eventType,
                        Dimensions: [{
                            Name: process.env.DIMENSION,
                            Value: process.env.STAGE
                        }],
                        Timestamp:  new Date,
                        Unit: 'Count',
                        Value: '1'
                    }
                ]
            };
            cloudwatch.putMetricData(params, (err) => {
                if (err) {
                    console.log('PutMetricData Error: %j', err);  // eslint-disable-line
                }
                response.body = JSON.stringify({
                    message: 'Stripe webhook incoming!',
                    stage: process.env.STAGE,
                    eventType: eventType
                });
                return callback(null,response);
            });
        });
    } catch (err) {
        response.statusCode = 501;
        response.body = JSON.stringify(err);
        callback(err,response);
    }
};

stripe.events.retrieve() で、送られてきたデータが本当に Stripe からのデータかどうかをチェックして、問題なければ CloudWatch のカスタムメトリクスに登録してる感じすね。

Serverless framework で API Gateway の endpoint 用意

API GateWay + Lambda の設定は苦行でしたが、Serverless framework を使用することで途端に簡単になりました。
こんな感じの serverless.yml を用意してやればいいです。

serverless.yml
service: put-stripe-event-to-custom-metrics

custom:
  stage:  ${opt:stage, self:provider.stage}
  logRetentionInDays:
    test: "14"         # stage[test]は14日
    live: "90"         # stage[live]は90日
    default: "3"       # stageが見つからなかったらこれにfallbackするために設定
  stripeAPIKey:
    test: "__STRIPE_TEST_API_SECRETKEY_HERE___"         # stage[test]用の Stripe API Key
    live: "__STRIPE_LIVE_API_SECRETKEY_HERE___"         # stage[live]用の Stripe API Key
    default: "__STRIPE_TEST_API_SECRETKEY_HERE___"      # stageが見つからなかったらこれにfallbackするために設定

provider:
  name: aws
  runtime: nodejs6.10
  stage: test
  region: us-east-1
  memorySize: 128
  timeout: 60

  iamRoleStatements:
    - Effect: Allow
      Action:
        - cloudwatch:PutMetricData
      Resource: "*"

  environment:
    STAGE: ${self:custom.stage}

# you can add packaging information here
package:
  include:
    - node_modules/**
  exclude:
    - .git/**
    - package.json

functions:
  incoming:
    handler: handler.incoming
    events:
      - http:
          path: stripe/incoming
          method: post
    environment:
      NAMESPACE: "___SERVICENAME__HERE__"
      DIMENSION: "Stripe"
      STRIPE_API_KEY: ${self:custom.stripeAPIKey.${self:custom.stage}, self:custom.stripeAPIKey.default}

resources:
  Resources:
    IncomingLogGroup:
      Properties:
        RetentionInDays: ${self:custom.logRetentionInDays.${self:custom.stage}, self:custom.logRetentionInDays.default}

serverless でのデフォルトに従うと、開発環境の stage は dev になりますが、ここは Stripe のお作法に従って開発環境は test、本番環境は live としましょうか。
environment として、以下の 4つを渡してますので、これを Lambda 内部では process.env.{environment name} で参照できます。

  • STAGE
  • NAMESPACE
  • DIMENSION
  • STRIPE_API_KEY

サンプルなので Stripe API KEY もそのまま渡してますが、必要なら暗号化するなりしてください。

environmentSTRIPE_API_KEY とか、Resources のロググループの期限とかは stage によって切り替えています。
この辺は @sawanoboly の以下の記事が参考になります。
Serverless Frameworkで、AWS Lambda function用に作られるCloudWatch Logsの期限を指定する

Deploy !

前準備

deploy したいところですが npm パッケージ stripe を内部で使用しているので、まずは npm install で stripe パッケージを取ってきましょう。

$ npm install stripe

いよいよデプロイ

もう、何も恐れる必要はありません。deploy してあげてください。

$ serverless deploy -v
 :
Serverless: Stack update finished...
Service Information
service: put-stripe-event-to-custom-metrics
stage: test
region: us-east-1
api keys:
  None
endpoints:
  POST - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/test/stripe/incoming
functions:
  incoming: put-stripe-event-to-custom-metrics-test-incoming

Stack Outputs
ServiceEndpoint: https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/test
ServerlessDeploymentBucketName: put-stripe-event-to-cust-serverlessdeploymentbuck-xxxxxxxxxxxx
IncomingLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:000000000000:function:put-stripe-event-to-custom-metrics-test-incoming:1

endpoint (https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/test/stripe/incoming) に表示される URL が API Gateway の endpoint です。
これを Stripe の Webhook 登録画面 で登録してあげればオッケーです。

本番環境( live )にデプロイする場合は、以下の感じで

$ serverless deploy -v -s live

やったー、CloudWatch のカスタムメトリクスに登録されてるよ!
Screen Shot 2017-06-13 at 17.17.25.png

これで、Stripe からのチャリンチャリンが CloudWatch で確認できるようになりました。
可視化大事ですね。

現場からは以上です。

続きを読む

Run CommandとStep Functionsでサーバレス(?)にEC2のバッチを動かす

動機

cronやdigdag-serverは便利で使い勝手がいいですが、常時サーバを動作させておかないといけないのが難点です。一日一回の業務のために、インスタンスを立ち上げっぱなしにしたり冗長構成にするのはコスト的にやめたいなという場合もあると思います。
そこで本記事では、EC2 Run CommandとStep Functionsを使って、EC2インスタンス起動、Dockerを使ったバッチ起動、EC2シャットダウンまでの一連の処理の設定の仕方を説明していこうと思います。

処理実装

State Machine

State Machineの概要は下記になります。基本的には、処理開始→数秒待つ→状態を監視→状態がOKなら次へ進む、という構成になっています。

※ Run Command実行時にたまにエラー(InvalidInstanceId)になります。実行前のWaitの時間を長くするなど対策が必要かもしれないです。

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

下記のJSONをStep Functionsへ登録します。

step-functions.json
{
  "Comment": "Invoke job",
  "StartAt": "StartInstance",
  "States": {
    "StartInstance": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:889476386829:function:qiita-sample-start-instance",
      "Next": "WaitInstanceState"
    },
    "WaitInstanceState": {
      "Type": "Wait",
      "Seconds": 60,
      "Next": "ConfirmInstanceState"
    },
    "ConfirmInstanceState": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:889476386829:function:qiita-sample-confirm-instance-state",
      "Next": "ChoiceInstanceState"
    },
    "ChoiceInstanceState": {
      "Type": "Choice",
      "Default": "FailInstanceState",
      "Choices": [
        {
          "Variable": "$.instance_state",
          "StringEquals": "pending",
          "Next": "WaitInstanceState"
        },
        {
          "Variable": "$.instance_state",
          "StringEquals": "running",
          "Next": "StartJob"
        }
      ]
    },
    "FailInstanceState": {
      "Type": "Fail",
      "Cause": "Failed to launch instance"
    },
    "StartJob": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:889476386829:function:qiita-sample-start-job",
      "Retry": [
        {
          "ErrorEquals": [
            "States.TaskFailed"
          ],
          "IntervalSeconds": 30,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "WaitJob"
    },
    "WaitJob": {
      "Type": "Wait",
      "Seconds": 10,
      "Next": "ConfirmJobStatus"
    },
    "ConfirmJobStatus": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:889476386829:function:qiita-sample-confirm-job-status",
      "Next": "ChoiceJobStatus"
    },
    "ChoiceJobStatus": {
      "Type": "Choice",
      "Default": "WaitJob",
      "Choices": [
        {
          "Or": [
            {
              "Variable": "$.job_status",
              "StringEquals": "Failed"
            },
            {
              "Variable": "$.job_status",
              "StringEquals": "TimedOut"
            },
            {
              "Variable": "$.job_status",
              "StringEquals": "Cancelled"
            },
            {
              "Variable": "$.job_status",
              "StringEquals": "Success"
            }
          ],
          "Next": "TerminateInstance"
        }
      ]
    },
    "TerminateInstance": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:889476386829:function:qiita-sample-terminate-instance",
      "End": true
    }
  }
}

Lambda関数

前準備

Lambda関数へ割り当てられているrole(大抵の場合はlambda-default-roleという名前になっている)へ、下記の権限を割り振ってください。

  • ec2:RunInstances
  • iam:PassRole
  • ec2:DescribeInstances
  • ssm:SendCommand
  • ec2:TerminateInstances
  • states:StartExecution

また、同じroleへ下記のポリシーもアタッチしてください。

  • AmazonSSMReadOnlyAccess

さらに、EC2インスタンスにアタッチするIAM roleを作成し、下記ポリシーをアタッチしておいてください。(本記事ではssm-roleと名前をつけます)

  • AmazonSSMFullAccess

EC2インスタンスを起動する

EC2インスタンスを起動します。取得したインスタンスIDを次のLambdaへ渡します。

start_instance.py
import boto3
import base64


def lambda_handler(event, context):
    client = boto3.client('ec2', region_name='us-east-1')
    resp = client.run_instances(
        ImageId='ami-898b1a9f',
        MinCount=1,
        MaxCount=1,
        KeyName='my-key',
        SecurityGroups=['default'],
        UserData=_make_user_data(),
        InstanceType='m3.medium',
        IamInstanceProfile={
            'Name': 'ssm-role',
        },
    )

    event['instance_id'] = resp['Instances'][0]['InstanceId']

    return event

def _make_user_data():
    with open('user-data.txt', 'rb') as f:
        return base64.b64encode(f.read())
user-data.txt
#!/bin/bash -xe

cd /tmp
curl https://amazon-ssm-us-east-1.s3.amazonaws.com/latest/linux_amd64/amazon-ssm-agent.rpm -o amazon-ssm-agent.rpm
yum install -y amazon-ssm-agent.rpm

EC2インスタンスの起動状況を確認する

confirm_instance_state.py
import boto3


def lambda_handler(event, context):
    ec2 = boto3.resource('ec2', region_name='us-east-1')
    instance = ec2.Instance(event['instance_id'])

    event['instance_state'] = instance.state['Name']

    return event

バッチを起動する

EC2 Run CommandでEC2インスタンスへコマンドを発行していきます。本記事では説明のためにpublicなrepositoryからイメージをpullしていますが、本番利用ではprivate repositoryからイメージを利用することが多いと思うので、そのための処理も入れています。
また、dockerの起動オプションで、標準出力をCloudWatch Logsへログ転送するように設定すると便利です(あらかじめLog Groupを作っておく必要があると思います)。

start_job.py
import boto3


def lambda_handler(event, context):
    client = boto3.client('ssm', region_name='us-east-1')

    command = "docker run --log-driver=awslogs" \
              " --log-opt awslogs-group=hello-world" \
              " --log-opt awslogs-region=us-east-1 --rm" \
              " -i alpine:latest /bin/echo 'hello, world'"

    resp = client.send_command(
        InstanceIds=[event['instance_id']],
        DocumentName='AWS-RunShellScript',
        MaxConcurrency='1',
        Parameters={
            'commands': [
                "yum install -y docker",
                "service docker start",
                "eval $(aws ecr get-login --no-include-email --region us-east-1)",
                command,
            ],
        },
        TimeoutSeconds=3600,
    )

    event['command_id'] = resp['Command']['CommandId']

    return event

バッチ処理の状態を確認する

confirm_job_status.py
import boto3


def lambda_handler(event, context):
    client = boto3.client('ssm', region_name='us-east-1')

    resp = client.get_command_invocation(
        CommandId=event['command_id'],
        InstanceId=event['instance_id'],
    )

    event['job_status'] = resp['Status']

    return event

EC2インスタンスをシャットダウンする

terminate_instance.py
import boto3


def lambda_handler(event, context):
    ec2 = boto3.resource('ec2', region_name='us-east-1')
    instance = ec2.Instance(event['instance_id'])
    resp = instance.terminate()

    return event

Step Functionsを呼び出す

下記Lambda関数を定期実行するよう設定します。State Machine名は変わりやすいので環境変数にしておくと便利です。

invoke-step-functions.py
import boto3
import uuid
import os


def lambda_handler(event, context):
    account_id = event.get('account')

    state_machine_arn = 'arn:aws:states:us-east-1:%s:stateMachine:%s'\
                        % (account_id, os.environ.get("STATE_MACHINE_NAME"))
    client = boto3.client('stepfunctions', region_name='us-east-1')
    resp = client.start_execution(
        stateMachineArn=state_machine_arn,
        name=str(uuid.uuid4()),
    )

    return 'invoked step functions'

まとめ

本記事では、EC2 Run CommandとStep Functionsを使って定期的にバッチ処理を起動するやり方を説明しました。今回は簡単な実装にしましたが、スポットインスタンスの価格を調べて10%上乗せして落札できたらそちらを利用するなど、工夫すれば複雑な処理も可能です。
従来のPaaSサービスでは待ち時間もCPU課金されますが、Step Functionsは状態遷移回数での課金ですので新たな使い方ができると思います。

追記

Githubへ公開しました
https://github.com/runtakun/qiita-sample-step-functions

続きを読む

AWS Lambda&cloudwatchのルールでインスタンスを自動停止させる

最近AWSを勉強するために、個人でアカウントを作り色々いじって遊んでいます。
インスタンスを停止し忘れることがしょっちゅうあるので、自動で停止するようにしようと思い、調べました。

インスタンスのcronにawscliのコマンドを登録してもよかったのですが、せっかくなのでAWSの機能を色々使うことにしました。

以下の記事と公式のドキュメントを参考にして作業を進めました。

https://geeknavi.net/aws/ec2インスタンスをスケジュールで自動起動・自動
https://aimless.jp/blog/archives/2681/

前提

EC2インスタンスが既に存在しており、起動した状態であること。

Lambdaの登録

AWSにログインをしたらLambdaの画面を開き、新規関数を作成します。

設計図の選択

設計図は Blank Function をクリックします。

スクリーンショット 2017-06-11 14.05.12.png

トリガーの設定

何もせずに次へをクリックします。

スクリーンショット 2017-06-11 14.06.51.png

関数の設定

画面上部

スクリーンショット 2017-06-11 14.08.14.png

名前、説明はお好きなように。
とりあえず

  • 名前
    stop_ec2_instance

  • 説明
    インスタンスを停止する関数

としておきます。

  • ランタイム

Node.jsのままにします。

  • コード

参考記事に記載されている以下の内容にします。
SDKについてはドキュメントがありますので、詳細を知りたい場合は目を通しておくとよいと思います。
以下はJavascript用です。
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html

YOUR_INSTANCE_ID , YOUR_REGION は自身の環境に合わせて書き換えてください。
idはArrayで渡すので、複数指定が可能なようです。
(ドキュメントを見ると Array<String> となっています)

const INSTANCE_ID = 'YOUR_INSTANCE_ID';

var AWS = require('aws-sdk'); 
AWS.config.region = 'YOUR_REGION';

function ec2Stop(cb){
    var ec2 = new AWS.EC2();
    var params = {
        InstanceIds: [
            INSTANCE_ID
        ]
    };

    ec2.stopInstances(params, function(err, data) {
        if (!!err) {
            console.log(err, err.stack);
        } else {
            console.log(data);
            cb();
        }
    });
}
exports.handler = function(event, context) {
    console.log('start');
    ec2Stop(function() {
        context.done(null, 'Stoped Instance');
    });
};

画面下部

スクリーンショット 2017-06-11 14.20.35.png

  • 環境変数

特に設定しないので空欄のままにします。

  • ハンドラ

そのままにします。

  • ロール

カスタムロールの作成を選びます。
そうすると別タブにIAM ロール作成画面が開くので作成します。

IAM ロール

スクリーンショット 2017-06-11 14.25.59.png

  • IAM ロール

そのままにします。

  • ロール名

お好きなように。
とりあえず stop_instance_lambda とします。

  • ポリシードキュメント

クリックするとテキストエリアが開くので、以下の内容に書き換えます。
ポリシードキュメントについてはこちらの公式ドキュメントを読むとよいと思います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ec2:StopInstances"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*",
                "arn:aws:ec2:*"
            ]
        }
    ]
}

入力し終わったら右下に 許可 ボタンがあるのでクリックします。
作成が完了するとlambdaの画面に戻ります。

既存のロールで先ほど作成したロールが選べるようになっているので選択します。

  • タグ
  • 詳細設定

こちらはデフォルトのままにするので何もせず。
画面右下の次へをクリックします。

確認画面に遷移するので、タイポなどがないか確認し、問題なければ 関数の作成 ボタンをクリックします。

関数確認

スクリーンショット 2017-06-11 14.35.19.png

作成が完了するとこちらの画面に遷移します。
作った関数が動くか確認するためテストをクリックします。

  • テスト

スクリーンショット 2017-06-11 14.37.17.png

このような画面が開くので、そのまま何もせず 保存してテスト をクリックします。
そうすると関数が実行されます。

実行した結果が画面の下部に表示されるので、ログにエラーがないことを確認してください。
そして指定したインスタンスが動作を停止しているかどうか確認し、停止していればOKです。

Lambdaのスケジュール実行

毎日0時にlambdaを実行するために、cloudwatchの設定をします。
cloudwatchの画面を開き、左メニューの イベント をクリックして画面を開き、ルールの作成をクリックしてください。

イベントソース

スクリーンショット 2017-06-11 14.48.26.png

  • イベントパターン or スケジュール

スケジュール のラジオボタンを選択してください。

  • CloudWatch イベントスケジュール

好みの問題になりそうなのですが、 cron式 を選択して以下の内容に書き換えてください。
イベントスケジュールの詳細はこちらに目を通しておくとよいです。

0 15 * * ? *

注意事項としては、時間は GMT グリニッジ標準時 で実行されるそうなので、9時間マイナスして考える必要があります。
(上記の場合、日本時間のAM0:00に実行させます)

ターゲット

ターゲットの追加をクリックし、先ほど作成したLambdaを選びましょう。

  • バージョン/エイリアスの設定
  • 入力の設定

はデフォルトのままにします。
最終的に以下のようになると思います。

スクリーンショット 2017-06-11 15.00.52.png

右下の設定の詳細をクリックします。

ルールの詳細の設定

スクリーンショット 2017-06-11 15.01.52.png

  • 名前
  • 説明

こちらはお好きなように。
入力が完了したら右下のルールの作成をクリックして完了します。

さいごに

インスタンスを起動させたままにして0時を迎えましょう。
lambdaが起動するはずなので、

  • cloudwatchのログ
  • インスタンスの状態

を確認し、lambdaが実行されていることと、インスタンスが停止していればOKです。

続きを読む