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

はじめに

つい先日、SORACOMFunnelがAWSIoTに対応したというニュースを耳にしました
ちょうど仕事の関係でSORACOMのシールドが届いたし、会社にLoRaWANのPublicGWもあることだし・・・
ということでちょいと触ってみた
SORACOM公式ブログにも手順が書いてありましたが、ちょっと躓いたところがあったりしたので、まとめてみました

やりたいこと

  1. LoRaデバイスからLoRaゲートウェイを通ってAWSIoTにセンサーデータを投げる
  2. AWSIoTが受け取ったデータを加工するためのLambdaファンクションをキックする
  3. Lambdaがデータを加工してDynamoDBに格納する

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/#」を入力
「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
}

デプロイ

上記ファイルを配置したディレクトリに移動して、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定期実行設定

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

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

実行イメージ

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

まとめ

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

続きを読む

Amazon API Gateway+Cognito+JavaScriptでちょっと気をつけること

(2017.6.23現在の情報です)
Amazon API Gatewayを、cognito認証で使うときの注意点です。

リソース設定後、ステージから「SDKの生成」を選ぶことができます。
ただ、このSDK、Cognito認証を想定してないっぽいです。
(認証なし=NONEか、AWS_IAMの認証しか想定してない)

Cognitoで認証する場合、こんな感じです。
※すでにサインイン済みの想定です。

sample.js
AWS.config.region = 'ap-northeast-1'; // Region

var poolData = { UserPoolId: 'ap-northeast-1_xxxxxxxx',
              ClientId: 'xxxxxxx'
};

var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);

if (cognitoUser != null) {
    cognitoUser.getSession(function(err, sessresult) {
         if (sessresult) {
             var idToken = sessresult.getIdToken().getJwtToken();

             var apigClient = apigClientFactory.newClient ();

             var params = {};//必要なら設定
             var body = {};//必要なら設定
             var additionalParams = {
                 headers: {
                    Authorization:idToken //ここが大事
                 }
             }

             apigClient.methodName(params, body, additionalParams)
             .then(function(result){
                  //成功
             }).catch( function(result){
                  //失敗
             });
         }

    });
}


ヘッダーにちゃんとIDトークンを入れるってだけですが。。。
わかんなかったので書いておきます。

最初、IAMのほうの設定の問題かなぁ。。。とか思って色々やってたけど、
そっちはとくに関係なさそうでした。
cognitoとても便利だけど、ちょっとまだ追いついてない感じもしました。

README.mdに書いといて欲しいな。

続きを読む

Fresh Install bitnami redmine stack on AWS, then migrate old data and database from old instance.

Fresh Install bitnami redmine stack on AWS, then migrate old data and database from old instance.

目的: Redmine のバージョンアップ と HTTPS化

ですが、新環境の構築手順のみやっていただければ新規構築手順としても使えます。

  • 前回書いた記事から1年ちょっと経過しました。
  • bitnami redmine stack AMI のバージョンも以下のように変化しました。(2017/06/21 現在)
    • Old version 3.2.1
    • New version 3.3.3
  • 前回は GUI でぽちぽち作成しましたが、今回は AWS CLI を中心に進めます。
  • ついでに前回書いてなかった HTTPS化 についても簡単に追記します。
  • 以下の AWS の機能を利用します。
    • Route53: ドメイン名取得、名前解決(DNS)
    • ACM(Amazon Certificate Manager): 証明書発行
    • ELB(Elastic Load Balancer): 本来は複数インスタンスをバランシングする用途に使うものですが今回は EC2 インスタンス 1台 をぶら下げ、ACM で取得した証明書を配布し外部との HTTPS通信 のために利用します。

注意と免責

  • 無料枠でない部分は料金が発生します。
  • データの正常な移行を保証するものではありません。

前提

  • 現環境が正常に動作していること。
  • 新環境を同一リージョン、同一VPC、同一サブネット内に新たにたてます。
    • インスタンス間のデータ転送を SCP で簡易に行いたいと思います。
    • 適宜セキュリティグループを解放してください。
  • aws cli version
% aws --version
aws-cli/1.11.47 Python/2.7.12 Darwin/16.6.0 botocore/1.5.10

段取り

  • 大まかに以下の順序で進めます
  1. Version 3.3.3 の AMI を使って EC2 インスタンスを起動
  2. Version 3.2.1 のデータバックアップ
    • Bitnami Redmine Stack の停止
    • MySQL Dump 取得
  3. Version 3.3.3 へのデータ復元
    • Bitnami Redmine Stack の停止
    • MySQL Dump 復元
    • Bitnami Redmine Stack の開始
  4. 動作確認

参考資料

作業手順

1. Newer Bitnami redmine stack インスタンス作成

以下の条件で作成します。

  • Common conditions

    • AMI: ami-15f98503
    • Type: t2.micro
    • Public IP: あり
  • User defined conditions
    • Region: N.Virginia
    • Subnet: subnet-bd809696
    • Security Group: sg-5b5b8f2a
    • Keypair: aws-n.virginia-default001
    • IAM Role: ec2-001
    • EBS: 20GB

WEB GUI から作るも良し、AWS CLI から作るも良し
以下コマンド実行例

set-env
KEY_NAME=aws-nvirginia-default001.pem
echo $KEY_NAME
check-ami
aws ec2 describe-images \
    --filters "Name=image-id,Values=ami-15f98503"
check-ami-name
aws ec2 describe-images \
    --filters "Name=image-id,Values=ami-15f98503" \
    | jq ".Images[].Name" \
    | grep --color redmine-3.3.3
create-instance
aws ec2 run-instances \
    --image-id ami-15f98503 \
    --count 1 \
    --instance-type t2.micro \
    --key-name aws-n.virginia-default001 \
    --security-group-ids sg-5b5b8f2a \
    --subnet-id subnet-bd809696 \
    --block-device-mappings "[{\"DeviceName\":\"/dev/sda1\",\"Ebs\":{\"VolumeSize\":20,\"DeleteOnTermination\":false}}]" \
    --iam-instance-profile Name=ec2-001 \
    --associate-public-ip-address
set-env
INSTANCE_ID=i-0f8d079eef9e5aeba
echo $INSTANCE_ID
add-name-tag-to-instance
aws ec2 create-tags --resources $INSTANCE_ID \
    --tags Key=Name,Value=redmine-3.3.3

注意書きにもありますが以下に表示される MySQL Database の root パスワードは初期起動時にしか表示されません。
このときに保管しておくか MySQL のお作法にしたがって変更しておいても良いでしょう

check-instance-created
aws ec2 describe-instances --filter "Name=instance-id,Values=$INSTANCE_ID"
wait-running-state
aws ec2 describe-instances \
    --filter "Name=instance-id,Values=$INSTANCE_ID" \
    | jq '.Reservations[].Instances[].State["Name"]'
get-redmine-password
aws ec2 get-console-output \
    --instance-id $INSTANCE_ID \
    | grep "Setting Bitnami application password to"
get-publicip
aws ec2 describe-instances \
    --filter "Name=instance-id,Values=$INSTANCE_ID" \
    | jq '.Reservations[].Instances[].NetworkInterfaces[].Association'
set-env
PUBLIC_IP=54.243.10.66
echo $PUBLIC_IP
site-check
curl -I http://$PUBLIC_IP/
HTTP/1.1 200 OK
(snip)
ssh-connect
ssh -i .ssh/$KEY_NAME bitnami@$PUBLIC_IP
first-login
bitnami@new-version-host:~$ sudo apt-get update -y
bitnami@new-version-host:~$ sudo apt-get upgrade -y
bitnami@new-version-host:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS"
bitnami@new-version-host:~$ sudo ./stack/ctlscript.sh status
subversion already running
php-fpm already running
apache already running
mysql already running
bitnami@new-version-host:~$ sudo ./stack/ctlscript.sh help
usage: ./stack/ctlscript.sh help
       ./stack/ctlscript.sh (start|stop|restart|status)
       ./stack/ctlscript.sh (start|stop|restart|status) mysql
       ./stack/ctlscript.sh (start|stop|restart|status) php-fpm
       ./stack/ctlscript.sh (start|stop|restart|status) apache
       ./stack/ctlscript.sh (start|stop|restart|status) subversion

help       - this screen
start      - start the service(s)
stop       - stop  the service(s)
restart    - restart or start the service(s)
status     - show the status of the service(s)

2. 旧バージョンのバックアップ取得

login_to_oldversion
Welcome to Ubuntu 14.04.3 LTS (GNU/Linux 3.13.0-74-generic x86_64)
       ___ _ _                   _
      | _ |_) |_ _ _  __ _ _ __ (_)
      | _ \ |  _| ' \/ _` | '  \| |
      |___/_|\__|_|_|\__,_|_|_|_|_|

  *** Welcome to the Bitnami Redmine 3.2.0-1         ***
  *** Bitnami Wiki:   https://wiki.bitnami.com/      ***
  *** Bitnami Forums: https://community.bitnami.com/ ***
Last login: Sun May 29 07:33:45 2016 from xxx.xxx.xxx.xxx
bitnami@old-version-host:~$
check-status
bitnami@old-version-host:~$ sudo stack/ctlscript.sh status
subversion already running
php-fpm already running
apache already running
mysql already running
stop
bitnami@old-version-host:~$ sudo stack/ctlscript.sh stop
/opt/bitnami/subversion/scripts/ctl.sh : subversion stopped
Syntax OK
/opt/bitnami/apache2/scripts/ctl.sh : httpd stopped
/opt/bitnami/php/scripts/ctl.sh : php-fpm stopped
/opt/bitnami/mysql/scripts/ctl.sh : mysql stopped
start-mysql
bitnami@old-version-host:~$ sudo stack/ctlscript.sh start mysql
170621 10:04:34 mysqld_safe Logging to '/opt/bitnami/mysql/data/mysqld.log'.
170621 10:04:34 mysqld_safe Starting mysqld.bin daemon with databases from /opt/bitnami/mysql/data
/opt/bitnami/mysql/scripts/ctl.sh : mysql  started at port 3306
check-available-filesystem-space
bitnami@old-version-host:~$ df -h /
Filesystem                                              Size  Used Avail Use% Mounted on
/dev/disk/by-uuid/6cdd25df-8610-4f60-9fed-ec03ed643ceb  9.8G  2.7G  6.6G  29% /
load-env-setting
bitnami@old-version-host:~$ . stack/use_redmine
bitnami@old-version-host:~$ echo $BITNAMI_ROOT
/opt/bitnami
dump-mysql
bitnami@old-version-host:~$ mysqldump -u root -p bitnami_redmine > redmine_backup.sql
Enter password:
bitnami@old-version-host:~$ ls -ltrh
  • scp 準備

手元の作業PCから新Redmine環境へssh接続するときに使用した証明書(pem)ファイルを旧Redmine環境にも作成します。

  • 今回は作業PC上で cat で表示させておいて旧環境のコンソール上にコピペしました。
example
bitnami@old-version-host:~$ vi .ssh/aws-nvirginia-default001.pem
bitnami@old-version-host:~$ chmod 600 .ssh/aws-nvirginia-default001.pem
  • ファイル転送
file_transfer
bitnami@old-version-host:~$ scp -i .ssh/aws-nvirginia-default001.pem redmine_backup.sql <new-version-host-ipaddr>:~
  • 新バージョン側にファイルが届いているか確認
check-transfered-files
bitnami@new-version-host:~$ ls -alh redmine*

3. 新バージョンへの復元

stop-stack
bitnami@new-version-host:~$ sudo stack/ctlscript.sh status
subversion already running
php-fpm already running
apache already running
mysql already running

bitnami@new-version-host:~$ sudo stack/ctlscript.sh stop

/opt/bitnami/subversion/scripts/ctl.sh : subversion stopped
Syntax OK
/opt/bitnami/apache2/scripts/ctl.sh : httpd stopped
/opt/bitnami/php/scripts/ctl.sh : php-fpm stopped
/opt/bitnami/mysql/scripts/ctl.sh : mysql stopped
start-mysql
bitnami@new-version-host:~$ sudo stack/ctlscript.sh start mysql
initial-database
bitnami@new-version-host:~$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.35 MySQL Community Server (GPL)

(snip)

mysql> drop database bitnami_redmine;

mysql> create database bitnami_redmine;

mysql> grant all privileges on bitnami_redmine.* to 'bn_redmine'@'localhost' identified by 'DATAB
ASE_PASSWORD';

mysql> quit
restore-dumpfile
bitnami@new-version-host:~$ mysql -u root -p bitnami_redmine < redmine_backup.sql
Enter password:
bitnami@new-version-host:~$
edit-line-18
bitnami@new-version-host:~$ vi /opt/bitnami/apps/redmine/htdocs/config/database.yml

    18    password: "DATABASE_PASSWORD"
db-migrate
bitnami@new-version-host:~$ cd /opt/bitnami/apps/redmine/htdocs/
bitnami@new-version-host:/opt/bitnami/apps/redmine/htdocs$ ruby bin/rake db:migrate RAILS_ENV=production
bitnami@new-version-host:/opt/bitnami/apps/redmine/htdocs$ ruby bin/rake tmp:cache:clear
bitnami@new-version-host:/opt/bitnami/apps/redmine/htdocs$ ruby bin/rake tmp:sessions:clear
stack-restart
bitnami@new-version-host:/opt/bitnami/apps/redmine/htdocs$ cd
bitnami@new-version-host:~$ sudo stack/ctlscript.sh restart

bitnami@new-version-host:~$ exit
site-check
curl -I http://$PUBLIC_IP/
HTTP/1.1 200 OK
(snip)

ブラウザでも http://$PUBLIC_IP/ でアクセスして旧環境のユーザー名とパスワードでログイン出来ることを確認してください。

この時点で旧環境のインスタンスを停止させておいて良いでしょう。
いらないと判断した時に削除するなりしてください。

4. おまけ HTTPS化

  • 4-1. Route53 でドメイン名を取得してください

    • .net で 年額 11 USドル程度ですので実験用にひとつくらい維持しておくと便利
  • 4-2. Certificate Manager で証明書を取得します
    • コマンドでリクエストしてます
aws acm request-certificate --domain-name redmine.hogefuga.net
check-status-pending
aws acm describe-certificate \
    --certificate-arn "arn:aws:acm:us-east-1:942162428772:certificate/fdf099f9-ced7-4b97-a5dd-f85374d7d112" \
    | jq ".Certificate.Status"
"PENDING_VALIDATION"
  • 4-3. 承認する

    • ドメインに設定しているアドレスにメールが飛んできます
  • 4-4. ステータス確認
check-status-ISSUED
aws acm describe-certificate \
    --certificate-arn "arn:aws:acm:us-east-1:942162428772:certificate/fdf099f9-ced7-4b97-a5dd-f85374d7d112" \
    | jq ".Certificate.Status"
"ISSUED"
  • 4-5. Classic タイプの HTTPS ELB をつくる
example
aws elb create-load-balancer \
    --load-balancer-name redmine-elb1 \
    --listeners "Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=arn:aws:acm:us-east-1:942162428772:certificate/fdf099f9-ced7-4b97-a5dd-f85374d7d112" \
    --availability-zones us-east-1a \
    --security-groups sg-3c90f343
  • 4-6. インスタンスをくっつける
example
aws elb register-instances-with-load-balancer \
    --load-balancer-name redmine-elb1 \
    --instances i-0f8d079eef9e5aeba
  • 4-7. State が InService になるまで待ちます
example
aws elb describe-instance-health \
    --load-balancer-name redmine-elb1
  • 4-8. DNS Name を確認する(4-10. で使います)
example
aws elb describe-load-balancers \
    --load-balancer-name redmine-elb1 \
  | jq ".LoadBalancerDescriptions[].DNSName"
  • 4-9. 今の設定を確認
example
aws route53 list-resource-record-sets \
    --hosted-zone-id Z3UG9LUEGNT0PE | jq .
  • 4-10. 投入用の JSON をつくる
example
vi change-resource-record-sets.json
example
{
  "Comment": "add CNAME for redmine.hogefuga.net",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "redmine.hogefuga.net",
        "Type":"CNAME",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": <DNSName>
          }
        ]
      }
    }
  ]
}

4-11. 設定投入

example
aws route53 change-resource-record-sets \
    --hosted-zone-id Z3UG9LUEGNT0PE \
    --change-batch file://change-resource-record-sets.json

4-12. 設定確認

example
aws route53 list-resource-record-sets \
    --hosted-zone-id Z3UG9LUEGNT0PE

4-13. ブラウザ確認

https://redmine.hogefuga.net

4-14. EC2 インスタンスのセキュリティグループ再設定

  • グローバルの TCP:80 削除
  • サブネット内の TCP:80 許可
check
aws ec2 describe-security-groups --group-ids sg-b7d983c8
change
aws ec2 modify-instance-attribute \
    --instance-id i-0f8d079eef9e5aeba \
    --groups sg-b7d983c8

4-15. 再度ブラウザからアクセス可能か確認

続きを読む

IAMでクロスアカウントスイッチロール設定メモ

軽くテストしたので個人的な備忘録です。

★クロスアカウントスイッチロールすると嬉しいこと

複数のAWSアカウント間を認証画面介さず行き来できる
個人用IAMアカウント作るのは一か所でよくアカウント毎でなくなる
SDKつかってるような一部のツールではスイッチできないものもあるがawscliくらいならスイッチロールでいける
スイッチ先で権限を限定してスイッチ元でアカウントの増減を制御できるので
別会社間のメンバーの増減のアカウント管理のやり取りが生じなくてたぶんべんり

アカウント番号はサポート画面の右上に出てる。

★参考

Swith Roleで複数のAWSアカウント間を切替える – Qiita
超簡単!今すぐ使える「クロスアカウントアクセス」 | Developers.IO
AWS Black Belt Techシリーズ AWS IAM
【小ネタ】複数のSwitch Roleでのクロスアカウントアクセスをブラウザのブックマークで管理する | Developers.IO

一番したのやつ履歴が5こくらいまでできえることに憤慨している人は幸せになれそう。

★実際のクロスアカウントスイッチロール実装手順の簡易なメモ

0.テストするアカウントを2つようい

アカウント1
※スイッチ元
Account Number 1234zzzzzzzz

アカウント2
※スイッチ先
アカウント番号 5678xxxxxxxx

1.スイッチ先でロールを作成する

※お客様先にスイッチする場合お客様作業

IAMサービスを選択してロールを作る

新しいロールの作成
 >ロールの選択(クロスアカウントアクセスのロールで外部IDの使用を許可しないほうを選択)
  (※外部ID許可とはldapやadなどのIAMクレデンシャルでないID連携を許可するものと思われ)
  >このアカウントにアクセスできる IAM ユーザーの AWS アカウントの ID を入力(スイッチ元のIDを入力)
   (MFAが必要にチェックはデバイスやアプリの用意が可能な場合に入れる)
   >ポリシーのアタッチ(既存から選ぶのでカスタムにしたいならあらかじめ調べておく)
    (とりあえず試験用なので適当な権限にする(arn:aws:iam::aws:policy/AdministratorAccess ))
    >ロール名を入力:mygroup-admin

2.スイッチ元でロールを設定する

とりあえずユーザとグループを作る

グループ:mygroup
ユーザ:とりあえず二人くらいを作成

グループのインラインポリシーを作成しスイッチ先のアカウントとロールを設定する
ポリシー名:switch-to-otheraccount-name

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "sts:AssumeRole",
    "Resource": "arn:aws:iam::5678xxxxxxxx:role/mygroup-admin"
  }
}

3.スイッチ元で自分のユーザで入りなおしてから右上からスイッチロールを選択してスイッチする

アカウント:5678xxxxxxxx
ロール:mygroup-admin

お客様先にスイッチする場合、
・スイッチ元のアカウントIDをお伝えする
・作成したロール名とわたる先のアカウント名とアカウントIDを聞いて設定
・スイッチロールしてみる
・スイッチ履歴は5個くらいしか残らないので便利なリンクを作っておく

ということになります。

★証跡を追えるようにするためにスイッチ元でCloudTrailの設定

見た感じすでに設定済みな模様でござったのでリンク先をどうぞ。S3もみたところ数年前からログがあった。
Amazon Web Services ブログ: 【AWS発表】 AWS CloudTrail – AWS APIコールの記録を保存
AWSの操作履歴を記録するCloudTrailを試してみた « サーバーワークス エンジニアブログ

★アカウントのエイリアスの設定

あんまり関係ないがIDだと視認性が微妙なので設定したほうがよさそう(なくてもいい)

AWS アカウント ID とその別名 – AWS Identity and Access Management

変えたたらサブドメインがアカウント番号からエイリアス名になる(アカウント番号でもアクセスできるまま)
https://my-alias.signin.aws.amazon.com/console

★アカウント設定(パスワードポリシー)

ISMS的なアレ(または顧客要望)にのっとって適宜。
Account settingsから実施。

Minimum password length: x(x文字を要する)
Require at least one non-alphanumeric character(記号を要する)
Allow users to change their own password (自分で更新する)
Enable password expiration
Password expiration period (in days): xx(xx(日)で期限がきれる)

★ルートアカウントでアクセスしない

スイッチロールの設定時にrootアカウントにアクセスできるように設定しなければ
メニューにスイッチロールでないので物理的にルートアカウントにアクセスは不可能。
クラスメソッドのリンクが詳しい(rootでも設定するといけるけどやらないほうがいい))
単に運用上パスワード変えて限定共有する、クレデンシャル無効化する、MFAデバイス用意等。
あとCloudTrail的に個人IAMで操作したほうが証跡が追いやすい。

★クレデンシャルの書き方

たぶん以下のようになる。

[account2]
role_arn = arn:aws:iam::5678xxxxxxxx:role/mygroup-admin
source_profile = account1
region=us-xxxx-x

★スイッチロールのポリシーアタッチされてるグループにいるユーザをcliでだす

$ aws iam get-group --group-name mygroup --profile account1|jq -c -r '.Users[].UserName'

以上

続きを読む

AmazonECRとEC2を使って手元でビルドしたDockerイメージをAWS上でサクッと動かす

ECR(EC2 Container Registry)に登録したDockerイメージをEC2上でコンテナとして起動するまでの一通りの流れを書いてみた
ECSも一通り検証終わっていて、サービスではそちらを使う予定だが、基礎を振り返るという意味でのまとめ。

Docker

ここ一ヶ月ひたすらdockerを触っているが、やはり手元の開発環境で動いたものが、別の環境でそのまま動くというのは他にないメリット。
これまでだと、開発環境でOK→STでまた一から作る→本番でも同じくみたいなことしてたけど、ホスト側にDockerエンジン入れるだけで、実際のプロセスは開発環境のものをそのまま移植出来るというところはかなり熱い。といった印象。

やること

  • ECRを使う準備
  • ECRへのDockerイメージの登録
  • EC2作成
  • EC2上にECRからpullしたDockerコンテナを立てる

ECRとは

正式名称 EC2 Container Registry
Amazonが提供するフルマネージドのDockerコンテナレジストリ。
Dockerイメージを管理して、ECS(EC2 Container Service)やEB(Elastic Beanstalk)に簡単にデプロイすることが出来るソリューション

ECR使うと何がうれしい

  • EC2インスタンスにIAMroleを付与するだけ、EC2側で面倒な認証をせずにdockerイメージを使える
  • S3がバックエンドなので、可用性高い
  • 自動的に暗号化されたり、https通信されるのでセキュリティも安心

ECRを使う準備(ローカルマシンで実施)

AWS Command Line Interface のインストール を参考にAWS CLIを手元のマシンにインストールしておく。

基本的には、AWS CLIで操作する。

1.リポジトリの作成&確認

$aws ecr create-repository --repository-name tst-shnagai
{
    "repository": {
        "registryId": "xxxxx",
        "repositoryName": "tst-shnagai",
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxx:repository/tst-shnagai",
        "createdAt": 1496229520.0,
        "repositoryUri": "xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai"
    }
}

リポジトリは、GUIから見ると、ECSサービスの中の[リポジトリ]に出来る

Amazon_EC2_Container_Service.png

2.ecrにログイン

セッションは12時間なので、感覚的に翌日には切れてる感じ。

## これで一発

$ $(aws ecr get-login --region ap-northeast-1)
Flag --email has been deprecated, will be removed in 17.06.
Login Succeeded

## $()の式展開を使わない場合

$ aws ecr get-login --region ap-northeast-1
docker login -u AWS -p eyJwYXlsb2FkIjoicURLTkxCTFhobUJuSTRxSDRNSUFBOEprc0txSnVuTVgrdzRzNkl4NU5rRDUxM0N...
### 標準出力の結果を貼り付けてログイン
$ docker login -u AWS -p eyJwYXlsb2FkIjoicURLTkxCTFhobUJuSTRxSDRNSUFBOEprc0txSnVuTVgrdzRzNkl4NU5rRDUxM0N...
Login Succeeded

ECRへのDockerイメージの登録(ローカルマシンで実施)

手元にある何かしらのDockerイメージをECRにpushする手順
手元で、Dockerイメージに対して、ECR用のタグづけを行ってから、ECRにpushする

1.docker tagコマンドでタグづけをする

今回は例として元々手元にある[apache_td]というdockerイメージに対して、ECRのルールに沿った名前でタグ付け(aliasつけるようなもの)する

## 元々のイメージ
$ docker image list |grep apache_td
apache_td                                                         latest              2c42dd3f5e5c        13 days ago         1.4GB

## タグ付けを実施
$ docker tag apache_td:latest  xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest

## imageIDは変わらないので、下記のような検索するとapache_tdがECRに対応したイメージとしてタグ付けされたことがわかる
$ docker image list |grep 2c42dd
xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai     latest              2c42dd3f5e5c        13 days ago         1.4GB
apache_td                                                         latest              2c42dd3f5e5c        13 days ago         1.4GB

2. 1でタグづけしたDockerImageをECRにpushする

$ docker push xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest
The push refers to a repository [xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai]
47d1cbb6b480: Layer already exists
...
latest: digest: sha256:14b7a5d491fa694c00f026bbc6c6cd09e0ddc63d0e586569a0de42a8ce7ec5d1 size: 2411

GUIで、タグ名とプッシュされた日時を確認して無事イメージがアップされていることを確認する

Amazon_EC2_Container_Service.png

ここまでで、ECRへのDockerイメージの登録は完了!!

EC2インスタンスの作成

1.通常通りEC2インスタンスを作成する(OSはデフォルトでawscliが入っているamazon linuxだと楽)

ポイントは、IAMRoleに[AmazonEC2ContainerRegistryReadOnly]ポリシを付与しておくことのみ

IAM_Management_Console.png

2. dockerのインストール

AWSの公式ドキュメントに沿ってやるだけなので、コマンドだけ羅列
Docker のインストール

ec2-userでdockerコマンドがsudoなしでうてるとこまでやっておく。

$ sudo yum update -y
$ sudo yum install -y docker
$ sudo service docker start
### ec2-userでsudoなしでdockerコマンドを打てるようにするため
$ sudo usermod -a -G docker ec2-user
###再ログイン
$ docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.03.1-ce

EC2上にECRからpullしたDockerコンテナを立てる(EC2上で実施)

1. ECRへのログイン

IAMRoleがついていない場合は、ログインで弾かれる

$ $(aws ecr get-login --region ap-northeast-1)
Login Succeeded

2. ECRからDockerイメージをpullする

## ECRにアップロードしたイメージをpull
$ docker pull xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest
latest: Pulling from tst-shnagai
996fe98f55d8: Pull complete
...
e6b377ddca6e: Pull complete
Digest: sha256:14b7a5d491fa694c00f026bbc6c6cd09e0ddc63d0e586569a0de42a8ce7ec5d1
Status: Downloaded newer image for xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest

## 手元のイメージとして登録されたことを確認
$ docker image ls
REPOSITORY                                                      TAG                 IMAGE ID            CREATED             SIZE
xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai   latest              2c42dd3f5e5c        13 days ago         1.4 GB

3. dockerコンテナを起動する

pullしてきたイメージからコンテナを起動する

## ホストの8080ポートにマッピングするtestという名前のコンテナを起動する
$ docker run -d --name test -p 8080:80 xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest
dbbb74b6ebe95666d356250de8310c19403078f53e020069e9a6d10e479b2873

## -lオプションで最後に起動したコンテナを表示
$ docker ps -l
CONTAINER ID        IMAGE                                                                  COMMAND                  CREATED             STATUS              PORTS                  NAMES
dbbb74b6ebe9        xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/tst-shnagai:latest   "/bin/sh -c '/bin/..."   4 seconds ago       Up 4 seconds        0.0.0.0:8080->80/tcp   test

## 動作確認として、ホストの8080に対してcurlでリクエストしてみる
$ curl localhost:8080
version 1.2

まとめ

オーソドックスな、AWSでECRを使ってdockerコンテナを起動する一通りの流れをやってみた。dockerを手元で触ってる人だったら、特に躓くことなくやれる内容だと思う。
ECSは、基本オペレーション(この投稿でいうEC2以降の話)を抽象化して、クラスタというEC2集合体の上で、ELB,AutoScaling等を付加して使えるサービスなので、ココら辺をちゃんと理解してやるとやらないでは進みがだいぶ違うという印象を受ける。
裏で何が行われてるのかなという道理を理解することは大事。

続きを読む

EC2 インスタンスを Chat Bot で管理する

こないだ Amazon EC2 でディープラーニングできる GPU インスタンス1を作ったのだが、趣味で使うにしてはまぁ料金が高いので常時起動させておくのはもったいなく、使わないときはインスタンスを停止させるようにしている。

しかし使い始めるときと使い終わったときにいちいち AWS 管理コンソールにログインしてインスタンスの起動/停止をするのが面倒だったので、我が家の Slack Bot から EC2 インスタンスを管理できるようにしてみた。

最終的にできたものはこういう感じ。

以下、作り方。

Chat Bot 用の AWS ユーザを作成する

既存のユーザのアクセスキーを利用してもできるが、セキュリティのために専用のユーザを作成して必要な権限のみを付与するのが好ましい。

今回は API を叩くためのユーザなので「プログラムによるアクセス」にチェックを入れる。

ユーザを作成すると、API を叩く際に必要なアクセスキー (AccessKeyId, SecretAccessKey) が生成されるので控えておく。

EC2 を操作できるポリシーを割り当てる

作成したユーザに必要な権限を付与する。

今回やりたいことは

  • EC2 インスタンスの一覧とステータスを取得する
  • EC2 インスタンスを起動させる
  • EC2 インスタンスを停止させる

の3つなので、それに合わせて以下のようなインラインポリシーを割り当てる。

AllowEC2InstanceManagement
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstanceStatus",
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "*"
        }
    ]
}

今回はすべてのインスタンスを操作できるようにするために "Resource": "*" としたが、ARN を指定することで「特定のインスタンスのみを操作可能」というような制限をすることも出来る。

Chat Bot から AWS API を叩く

我が家の Chat Bot は Node.js で動作しているので、AWS JavaScript SDK を利用して AWS API を叩く。

AWS JavaScript SDK をインストール

npm からインストールできる。

$ npm install aws-sdk

アクセスキーの設定

AWS SDK を読み込んでアクセスキーを設定する。

const AWS = require('aws-sdk');

// アクセスキーは環境変数から読み込む
AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: 'ap-northeast-1', // 東京リージョン
});

// EC2 インスタンスを操作するためのオブジェクトを生成
const ec2 = new AWS.EC2();

アクセスキーをハードコートせずに環境変数から読み込んでいるので、Bot を動作させる環境の環境変数 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY にアクセスキーを設定しておく必要がある。

インスタンスの名前をつける

チャットから ec2 gpgpu start のようにインスタンスの名前を指定して起動/停止できるようにしたいので、インスタンス ID と名前を関連付けられるようにしておく。

// 名前から起動/停止できる EC2 インスタンスの一覧
const instances = [
  { id: "i-01315ab8f6f7015e5", name: "GPGPU" },
];

// 名前からインスタンス ID を返す
function getInstanceId(name) {
  const instance = instances.filter(i => i.name.toLowerCase() === name.toLowerCase())[0];
  return instance ? instance.id : null;
}

// インスタンス ID から名前を返す
function getInstanceName(id) {
  const instance = instances.filter(i => i.id === id)[0];
  return instance ? instance.name : null;
}

Bot のアクションを定義する

チャットから Bot にコマンドメッセージが投稿されたときに AWS API を叩くようなアクションを定義する。

以下のコードは自作の Bot フレームワーク2で動くコードだが、インタフェースを Hubot に似せて作っているので Hubot でもだいたい同じようなノリで書けると思う。

module.exports = (bot) => {

  // 指定した EC2 インスタンスを起動する
  bot.respond(/^ec2 ([w_]+) start$/i, (msg) => {
    const id = getInstanceId(msg.match[1]);
    if (!id) return msg.send('知らないインスタンスですね・・・');
    ec2.startInstances({ InstanceIds: [id] }, (err) => {
      if (err) {
        bot.logger.error(err);
        msg.send('インスタンスの起動に失敗しました...');
      } else {
        msg.send('インスタンスを起動しました');
      }
    });
  });

  // 指定した EC2 インスタンスを停止する
  bot.respond(/^ec2 ([w_]+) stop$/i, (msg) => {
    const id = getInstanceId(msg.match[1]);
    if (!id) return msg.send('知らないインスタンスですね・・・');
    ec2.stopInstances({ InstanceIds: [id] }, (err) => {
      if (err) {
        bot.logger.error(err);
        msg.send('インスタンスの停止に失敗しました...');
      } else {
        msg.send('インスタンスを停止しました');
      }
    });
  });

  // すべての EC2 インスタンスの起動状況を取得する
  bot.respond(/^ec2 status$/i, (msg) => {
    const stateIcons = {
      running: 'large_blue_circle',
      terminated: 'red_circle',
      stopped: 'white_circle',
    };
    ec2.describeInstanceStatus({ IncludeAllInstances: true }, (err, data) => {
      if (err) {
        bot.logger.error(err);
        return msg.send('インスタンス情報の取得に失敗しました...');
      }
      const text = data.InstanceStatuses.map((instance) => {
        const id = instance.InstanceId;
        const state = instance.InstanceState.Name;
        const icon = stateIcons[state] || 'large_orange_diamond';
        const name = getInstanceName(id);
        const nameLabel = name ? ` (*${name}*)` : '';
        return `:${icon}: `${id}`${nameLabel} is ${state}`;
      });
      msg.send(text.join('n'));
    });
  });

  // インスタンスが起動中の場合は1時間に1回通知する
  bot.jobs.add('0 0 * * * *', () => {
    ec2.describeInstanceStatus((err, data) => {
      if (err) {
        bot.logger.error(err);
        return bot.send('インスタンス情報の取得に失敗しました...');
      }
      const count = data.InstanceStatuses.length;
      if (count > 0) bot.send(`${count} 台の EC2 インスタンスが起動中です`);
    });
  });

};

今回は start / stop / status の3つのコマンドに加えて、停止し忘れを防ぐために「一時間に一回通知する」機能も実装した。

ec2 オブジェクトのメソッドを使っている箇所が API を叩いている箇所なわけだが、メソッドのオプションなどの詳細は AWS JavaScript SDK のドキュメント3を参照して欲しい。

動かす

Bot をデプロイしてコマンドを投げてみると、こういう感じ。

これで AWS の管理コンソールにログインしなくても EC2 インスタンスを操作できるようになった。便利。

続きを読む

APIgatewayとS3でRestAPIのスタブを作る

背景

  • コーディング課題用にAPIを2本用意する必要があった
  • S3へのProxy程度なので、APIgatewayの勉強がてら実装

API

エンドポイント

  • GET /areas
  • GET /stores?region_number={地方を示す番号}

    • クエリ文字があるので、単にS3 Proxyでは不十分

構成図

aws.png

  • S3

    • jsonファイルを設置
  • API gateway
    • IAMでS3にReadOnlyのアクセス権を付与

S3内の物理ファイル配置

/areas/all.json
/stores/region/{region_number}.json

API gatawayの設定

GET areas は S3の /areas/all.json に固定でマッピング

Screen Shot 2017-06-12 at 20.56.39.png

GET /stores は region_numberのクエリ文字列をパスにマッピング

Screen Shot 2017-06-12 at 20.58.42.png

Screen Shot 2017-06-12 at 20.59.32.png

両方のAPIともに文字コードはutf-8に設定

Screen Shot 2017-06-12 at 21.02.12.png

実例

API gatewayのstage名 v1 としてデプロイした

  • GET /areas

    • https://cyc6gmq1i4.execute-api.ap-northeast-1.amazonaws.com/v1/areas
  • GET /stores

    • https://cyc6gmq1i4.execute-api.ap-northeast-1.amazonaws.com/v1/stores?region_number=3

所感

  • ドメイン名当てるのが面倒だった

    • USのAWSリージョンに証明書を置いておく必要がある
  • 変換ルールは単純なものだけ
    • 例えば、ヘッダに固定値を付ける
    • 例えば、クエリをパスに変換する (今回の実装)
  • 「このクエリがないとき〜」 のような条件判定には向かない
    • GET /stores の region_numberがクエリにないときは
      本当はS3の /stores/all.json を返したかった
  • 複数のクエリが付くときには不向き
    • S3に用意するファイルが組み合わせ爆発する
    • offsetpage のクエリで、追加読み込みを模擬する程度が落とし所

続きを読む

Go言語でDynamoDB接続を試みたときの備忘録

前提

最近、Go言語を触り始めた。

GoからDynamoDBに接続するにあたって、 aws-sdk-go/aws/service/dynamodb を使おうとし、ドキュメントを読んでたら結構難しそうだったので、 guregu/dynamo を使うことにした。

また、DynamoDBも触ったことがなかった。

ゴールは、GoでDynamoDBに読み書きすること!

DynamoDBの設定

まず、AWSコンソール画面からDynamoDBのテーブルを作ります。

スクリーンショット 2017-06-17 16.47.12.png

上記の画面で、「テーブル名」を Test とします。

「プライマリーキー」を user_id とします。型は「数値」にします。

他のattributeにかんしては、時間に関する値を想定してますが、実際にGoからItemをPUTするときに足されるので、定義する必要はありません。(そもそも登録もできない。)

(キャメルケースがいいのか、パスカルケースがいいのか等の命名規則もあるかと思いますが、一旦これでいきます。)

これでAWS側の用意は出来ました。

あと、もう一つ大事なこととして
IAMで、DynamoDBにアクセスできる権限をもったユーザーを作成し、

  • アクセスキー
  • シークレットキー

を入手しましょう。

Go言語をかく!

user_dynamo.goという名前でファイルを作成します。

必要なモジュール

以下のリアブラリをimportします。

user_dynamo.go
import (
  "fmt"
  "time"
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/aws/credentials"
  "github.com/guregu/dynamo"
)
ライブラリ名 説明
fmt 標準出力に使う。
time 現在時刻を取得するのに使う。
github.com/aws/aws-sdk-go/aws configに関するオブジェクト作成につかう
github.com/aws/aws-sdk-go/aws/session sessionに関するオブジェクト作成につかう
github.com/aws/aws-sdk-go/aws/credentials awsへのアクセス情報に関するオブジェクト作成につかう
github.com/guregu/dynamo github.com/aws/aws-sdk-go/aws/service/dynamodbに対する操作を便利にしてくれるライブラリ

DynamoDBのスキーマ定義

この言い回しが正しいかわかりませんが、DynamoDBのUserテーブルに登録する予定の項目を構造体として定義します。

user_dynamo.go
type User struct {
  UseId int `dynamo:"use_id"`
  CreatTime time.Time `dynamo:"created_time"`
}

inttime.Time のあとのバッククウォートで囲まれた部分はGoで「タグ情報」と呼ばれるものになります。

DynamoDBのUserテーブルにアクセス

user_dynamo.go
func main(){
  c := credentials.NewStaticCredentials("<アクセスキー>", "<シークレットキー>", "") // 最後の引数は[セッショントークン]

  db := dynamo.New(session.New(), &aws.Config{
      Credentials: c,
      Region: aws.String("<region名>"), // "ap-northeast-1"等
  })
  table := db.Table("User")

 // 続く
}

のように書きます。

データをPUTする

user_dynamo.go
func main(){
  // 続き

  u := User{UserId: 3, CreatTime: time.Now().UTC()}
  fmt.Println(u)

  if err := table.Put(u).Run(); err != nil {
    fmt.Println("err")
    panic(err.Error())
  }

上記をまとめると

user_dynamo.go
package main

import (
  "fmt"
  "time"
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/aws/credentials"
  "github.com/guregu/dynamo"
)


type User struct {
  UseId int `dynamo:"use_id"`
  CreatedTime time.Time `dynamo:"created_time"`
}


func main(){
  cred := credentials.NewStaticCredentials("<アクセスキー>", "<シークレットキー>", "") // 最後の引数は[セッショントークン]

  db := dynamo.New(session.New(), &aws.Config{
      Credentials: cred,
      Region: aws.String("<region名>"), // "ap-northeast-1"等
  })

  table := db.Table("User")

  u := User{UserId: 100, CreatedTime: time.Now().UTC()}
  fmt.Println(u)

  if err := table.Put(u).Run(); err != nil {
    fmt.Println("err")
    panic(err.Error())
  }

それではterminalから

$ go run user_dynamo.go

とうってみてください。DynamoDBに

user_id created_time
100 2017-06-02T09:25:56.000147134Z

と登録されたかと思います。これで、DynamoDBへのデータ登録は完了です。

データの読み取り

DynamoDBより

「user_idが100のデータ全て」

データを読み取る場合は以下のようになります。

user_dynamo.go
func main(){
  // 続き
  var users []User
  err := table.Get("user_id", 100).All(&users)
  if err != nil {
    fmt.Println("err")
    panic(err.Error())
  }

  fmt.Println(users) // [{100 2017-06-02T09:25:56.000147134 +0000 UTC}]と出力される。

  for i, _ := range users {
    fmt.Println(users[i]) // {100 2017-06-02T09:25:56.000147134 +0000 UTC}
  }
}

また全データを取得したい場合は

err := table.Get("user_id", 1).All(&users)

というところを

err := table.Scan().All(&users)

とすれば大丈夫です。

さらに created_time によって、絞りたい場合は以下のようにすれば大丈夫です。

err := table.Get("user_id", 1).Filter("created_time >= ?", "2017-06-01 00:00:00").All(&users)

注意点

DynamoDBでは、プライマリーキーは、DBで一つしか存在できないので、本コードでは書き込みをするたびに書き換わります。

参考にしたサイト

http://www.geocities.jp/m_hiroi/golang/abcgo07.html
https://github.com/guregu/dynamo
http://qiita.com/guregu/items/2e9ac305791935b86f5d
https://developers.eure.jp/tech/aws-sdk-go-tutorial-sqs-dynamodb/

続きを読む

WebpackでAWS SDKをバンドルするとサイズが大きいのでダイエットする

経緯

サーバレス、流行ってますね。
ウェブホスティングしたS3上にReactやRiot.jsなどでSPAを作って、認証はCognito UserPoolというのがテッパン構成でしょう。

そして、SPAを作るならWebpackでES6で書いたソースをトランスパイルして、バンドル(ひとつの*.jsファイルにまとめること)して…というのが通例ですね。(サーバーサイドの人にはこの辺がツラい)

しかしながら、いざやってみると…でかいよバンドルされたJSファイル。minifyして2MB近くとか…。

何が大きいのかwebpack-bundle-size-analyzerを使って見てみましょう。

$ $(npm bin)/webpack --json | webpack-bundle-size-analyzer 
aws-sdk: 2.2 MB (79.8%)
lodash: 100.65 KB (3.56%)
amazon-cognito-identity-js: 94.85 KB (3.36%)
node-libs-browser: 84.87 KB (3.00%)
  buffer: 47.47 KB (55.9%)
  url: 23.08 KB (27.2%)
  punycode: 14.33 KB (16.9%)
  <self>: 0 B (0.00%)
riot: 80.64 KB (2.85%)
jmespath: 56.94 KB (2.02%)
xmlbuilder: 47.82 KB (1.69%)
util: 16.05 KB (0.568%)
  inherits: 672 B (4.09%)
  <self>: 15.4 KB (95.9%)
crypto-browserify: 14.8 KB (0.524%)
riot-route: 8.92 KB (0.316%)
debug: 8.91 KB (0.316%)
events: 8.13 KB (0.288%)
uuid: 5.54 KB (0.196%)
process: 5.29 KB (0.187%)
querystring-es3: 5.06 KB (0.179%)
obseriot: 4.43 KB (0.157%)
<self>: 27.78 KB (0.983%)

なるほど、AWS SDKですね、やっぱり。

なぜ大きくなっちゃうのか

Webpackによるバンドルではnpm installしたモジュールが全て闇雲にバンドルされるわけではありません。
結構賢くて実際に使われているものだけがバンドルされるように考えられています。具体的にはimport(require)されているモジュールだけを再帰的にたどって1つのJSファイルにまとめていきます。
つまり、importしているファイル数が多ければ多いほどバンドルされたファイルは大きくなります。

import AWS from "aws-sdk"

普通のサンプルだとAWS SDKを使うときはこのようにimportすると思います。これが大きな罠なのです。
AWS SDKのソースを見てみましょう。

lib/aws.js
// Load all service classes
require('../clients/all');

ここがサイズを肥大化させる要因となっています。全てのモジュールを読み込んでいるので使いもしないAWSの機能も含めて全て読み込んでしまっているのです。

どうしたらダイエットできるのか

http://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/webpack.html#webpack-importing-services

を見てみると書いてあります。

The require() function specifies the entire SDK. A webpack bundle generated with this code would include the full SDK but the full SDK is not required when only the Amazon S3 client class is used.

require("aws-sdk")と書いてしまうと、たとえS3しか使わなくても全部の機能を読み込んでしまいます。
注) import from "aws-sdk"と書いても同じ意味になります。

その下の方に、

Here is what the same code looks like when it includes only the Amazon S3 portion of the SDK.

// Import the Amazon S3 service client
var S3 = require('aws-sdk/clients/s3');

// Set credentials and region
var s3 = new S3({
    apiVersion: '2006-03-01',
    region: 'us-west-1', 
    credentials: {YOUR_CREDENTIALS}
  });

と答えが載っています。

clientsディレクトリ以下のファイルを個別にimportすることができるようになっているということです。

なるほど、試してみた

https://github.com/NewGyu/riot-sample/compare/cognito-login…fat-cognito-login

$ $(npm bin)/webpack --json | webpack-bundle-size-analyzer 
aws-sdk: 325.01 KB (36.3%)
lodash: 100.65 KB (11.2%)
amazon-cognito-identity-js: 94.85 KB (10.6%)
 :

2.2MB -> 325KB と激ヤセです!

続きを読む