DynamoDB LocalとCognitoを併用する場合の注意点

先に結論

DynamoDB LocalとCognitoを併用する場合は、必ず別々のendpointを定義する。
特にCognito側のendpointを書く事例はほとんど見かけないので忘れがち。

utils/auth.js
import AWS from 'aws-sdk'

const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider({
  apiVersion: '2016-04-18',
  region: 'ap-northeast-1',
  endpoint: `https://cognito-idp.ap-northeast-1.amazonaws.com/`,
})

export default function auth(AccessToken) {
  return new Promise((resolve, reject)=>{
    cognitoIdentityServiceProvider.getUser({AccessToken}, (err, data)=>{
      if (err) return reject(err)
      return resolve(data)
    })
  })
}
utils/dynamoose.js
import dynamoose from 'dynamoose'

dynamoose.AWS.config.update({region:'ap-northeast-1'})
if ( process.env.NODE_ENV === 'dev' ) {
  dynamoose.AWS.config.endpoint = new dynamoose.AWS.Endpoint('http://localhost:8000')
}
export default dynamoose

前提

  • 今回のケースではNode.jsのdynamooseというORMを使っているが、その他のケースでも発生する可能性があるので念のためメモ。
  • ServerlessFrameworkを使用しているが、それは今回の件とは関係ないはず?

経緯

  • ServerlessFrameworkのプラグイン serverless-dynamodb-local を使って、DynamoDB Localを起動し、Cognitoでユーザー管理をしようとした
  • アプリケーションを実行すると、1度目は成功するが2度目で必ず下記エラーが出ることに気づいた
MissingAuthenticationToken: Request must contain either a valid (registered) AWS access key ID or X.509 certificate.
  • AccessTokenが間違えてるのかなと思ったが合ってた
  • リージョン指定が間違えてるのかなと思ったが合ってた
  • DynamoDBの向き先をローカルではなく本番に向けたらエラーが出なかった
  • 向き先をローカルに戻して、CognitoのSDK側にendpointを指定したらエラーが出なかった

推察

  • 恐らくCognito User Poolは内部でDynamoDBを使っていて、DynamoDBのendpointを変えるとCognitoのSDK側にも影響を及ぼしてしまうのではないか?
  • このあたり詳しい人、もしくは中の人、今回の件について何かあれば教えてもらえたら嬉しいです

続きを読む

PythonでSESでemailを、SNSのSMSでショートメッセージを送信する

はじめに

みなさんは、アプリケーションからemailやショートメッセージを送信するような機能を実装したりしていませんか?
筆者も先日そのような機能を実装しました。もちろんAWSのSESとSNSを利用して。
Node.jsで書きたかったけど、諸所諸々の事由がありPythonで書いてます。
SESとかSNSとかSMSとかややこしい!ってなりながら実装しましたが、制限とかで詰まったところがあったので、今後のために記事に残しておこっと・・・

SESって?

SES(Simple Email Service)は、ユーザーが各自のEメールアドレスとドメインを利用して費用効率の高い簡単な方法でEメールを送受信するためのEメールプラットフォームのこと。
とりあえずAWSのサービスでEメールを送受信できるサービス。適当でごめんなさい・・・

SNSって?

SNS(Simple Notification Service)は、サブスクライブしているエンドポイントへのメッセージの配信または送信を調整し、管理するウェブサービスです。サブスクライバーとしてLambdaとかSQSとかSMSなどを使用することができます。
今回はSMSを使ってショートメッセージを送信します。

実装方法

アプリ側・AWSのほかのサービスなど呼び出しもとが複数あったのでLambdaに書くことにしました。
基本的にLambdaをキックしてSESやSMSを送信する感じです。

SES

送信先のメールアドレスは配列で渡してやれば、複数アドレスに送信することができます。
基本的にSESを利用する場合はあらかじめSESで登録・許可しておいたアドレスのみ送受信することができます。
しかしそれではアドレスが増えるたびに、SESに登録・許可をしなければいけないので、サポートに連絡して、サンドボックス環境から移動してもらいましょう。そうするとメール受信者の登録をする必要がなくなります。
(送信者のアドレスは必ず登録する必要がありますので注意)

import boto3

def lambda_handler(event, context):
  try:
    ses = boto3.client("ses", region_name = "us-west-2")
    ses.send_email(
      Source = "from_mailaddress",
      Destination = {
        "ToAddresses": "to_mailaddress"
      },
      Message = {
        "Subject": {
          "Data": "subject_title",
          "Charset": "UTF-8"
        },
        "Body": {
          "Text": {
            "Data": "body_message",
            "Charset": "UTF-8"
          }
        }
      }
    )
  except Exception as e:
    print e

SNS(SMS)

送信先の電話番号は国際電話番号(あたまに+81とかつけるやつ)でないと送信できないようです。
あとショートメッセージなので、メッセージの内容はほどほどにしておかないとエラーを吐いてきます。
またデフォルトの利用料金制限が1$/monthとなっていて、そのままでは数十通しか送信することができないです。
しかもその制限を上げることはできない(上げようとすると許可されてないよというエラー文が表示されます)ので、SES同様サポートに問い合わせて制限を上げられるようにしてもらいましょう。

import boto3

def lambda_handler(event, context):
  try:
    sns = boto3.client("sns")
    phoneNumber = "+818012345678"
    message = "send message"
    sns.publish(
      PhoneNumber = phoneNumber
      Message = message
    )
  except Exception as e:
    print e

さいごに

SESやSNS(SMS)の各制限に少し詰まってしまったことがあったので、今回記事に書きました。
AWS-SDKなどを活用すれば、アプリ側から簡単にメッセージを送信することができたりします。便利ですね。
Lambdaのeventに送信先のアドレスや電話番号とかメッセージの内容とか持たせて、それを参照すれば、ひとつのLambdaで使いまわすこともできます。便利ですね(2回目)。

ではまた!

続きを読む

node-jt400を試してみた(on AWS_EC2)

環境

AWS EC2でnode-jt400が稼働するかテストしました。
マシンイメージ:Amazon Linux AMI
タイプ:t2.micro
node.js:6.9.1
npm:3.10.8
jdk:java:1.8.0_131

インストール

mkdir myfolder
cd myfolder
npm init -y
npm install node-jt400

問題無く、インストールできました。

参考までに、node-jt400でインストールされるモジュールです。

└─┬ node-jt400@1.4.1
  └─┬ java@0.8.0
    ├── async@2.0.1
    ├─┬ find-java-home@0.1.3
    │ └── which@1.0.9
    ├─┬ glob@7.1.1
    │ ├── fs.realpath@1.0.0
    │ ├─┬ inflight@1.0.6
    │ │ └── wrappy@1.0.2
    │ ├── inherits@2.0.3
    │ ├─┬ minimatch@3.0.4
    │ │ └─┬ brace-expansion@1.1.8
    │ │   ├── balanced-match@1.0.0
    │ │   └── concat-map@0.0.1
    │ ├── once@1.4.0
    │ └── path-is-absolute@1.0.1
    ├── lodash@4.16.4
    └── nan@2.4.0

EC2からIBMi(AS400)へのネットワーク接続

いろいろ方法はあるかと思いますが、今回はテストなのでSSHを使用しポート転送を行いました。転送したのは以下のポートです。
449・8470・8471・8473・8475・8476

私は以下のコマンドを実行しました。

ssh -C -N -f -L 449:ibmi:449 sshuser@sshhost
ssh -C -N -f -L 8470:ibmi:8470 sshuser@sshhost
:

EC2のポートを開ける

nodeの待ち受けは8888ポートを使用しているので、解放します。
VPN→セキュリティグループ→インバウンドのルール

接続先ホストの改修

以前のテストで使用したSQLquery.jsのhostパラメータを修正します。

var pool = jt400.pool({ host: '127.0.0.1', user: 'MYUSER', password: 'MYPASS' });

nodeの実行

node SQLquery.js

実行結果

nodejt29.png

続きを読む

LambdaでDynamoDBのデータを操作する(Node&Python)

はじめに

最近はもっぱらIoT関連の案件を担当しています。
AWSを使っているので、DynamoDBにデバイスからのデータを溜め込んだり、そのデータを取得して可視化したりなどすることが多く、その際にはLambdaが大活躍しています。
主にNode.jsを使っており、案件に応じてPythonでも実装をしていて、自分の中でテンプレート化してきたなーと思ったのでまとめておこうっとな。

DynamoDBにデータをPutする

主にデバイスから受けとったデータをDynamoDBに書き込みます。
本来であればデータを加工する必要がありますが、そこはデバイスによって多種多様ですので割愛します。
ここで重要なのは、PartitionKeyとSortKeyの指定です。
ここを間違えてしまうとエラーを吐いてきます。(Key情報が間違ってるぜ!的な感じ)

Node.js

const AWS = require("aws-sdk");
const dynamoDB = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1" // DynamoDBのリージョン
});

exports.handler = (event, context, callback) => {
  const params = {
    TableName: "table-name" // DynamoDBのテーブル名
    Item: {
      "PartitionKey": "your-partition-key-data", // Partition Keyのデータ
      "SortKey": "your-sort-key-data", // Sort Keyのデータ
      "OtherKey": "your-other-data"  // その他のデータ
    }
  }

  // DynamoDBへのPut処理実行
  dynamoDB.put(params).promise().then((data) => {
    console.log("Put Success");
    callback(null);
  }).catch((err) => {
    console.log(err);
    callback(err);
  });
}

Python

import boto3

def lambda_handler(event, context):
  try:
    dynamoDB = boto3.resource("dynamodb")
    table = dynamoDB.Table("table-name") # DynamoDBのテーブル名

    # DynamoDBへのPut処理実行
    table.put_item(
      Item = {
        "PartitionKey": "your-partition-key-data", # Partition Keyのデータ
        "SortKey": "your-sort-key-data", # Sort Keyのデータ
        "OtherKey": "your-other-data"  # その他のデータ
      }
    )
  except Exception as e:
        print e

DynamoDBのデータを取得する(query)

主にDynamoDB内のデータを取得する際にはqueryを使います。
PartitionKeyだけでなくSortKeyもDynamoDBにあれば、ScanIndexForwardをfalseにLimitで取得した件数を指定して、降順(最新)のデータから固定の件数を取得することができます。
よく多いパターンとしては、あるDeviceID(PartitionKey)が送信してきたデータを最新から○○件取得したいなどのパターンがあります。

Node.js

const AWS = require("aws-sdk");
const dynamoDB = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1" // DynamoDBのリージョン
});

exports.handler = (event, context, callback) => {
  const params = {
    TableName: "table-name" // DynamoDBのテーブル名
    KeyConditionExpression: "#PartitionKey = :partition-key-data and #SortKey = :sort-key-data", // 取得するKey情報
    ExpressionAttributeNames: {
      "#PartitionKey": "your-partition-key", // PartitionKeyのアトリビュート名
      "#SortKey": "your-sort-key" // SortKeyのアトリビュート名
    },
    ExpressionAttributeValues: {
      ":partition-key-data": "your-partition-key-data", // 取得したいPartitionKey名
      ":sort-key-data": "your-sort-key-data" // 取得したいSortKey名
    }
    ScanIndexForward: false // 昇順か降順か(デフォルトはtrue=昇順)
    Limit: 1 // 取得するデータ件数
  }

  // DynamoDBへのquery処理実行
  dynamoDB.query(params).promise().then((data) => {
    console.log(data);
    callback(data);
  }).catch((err) => {
    console.log(err);
    callback(err);
  });
}

Python

import boto3
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):
  try:
    dynamoDB = boto3.resource("dynamodb")
    table = dynamoDB.Table("table-name") # DynamoDBのテーブル名

    # DynamoDBへのquery処理実行
    queryData = table.query(
      KeyConditionExpression = Key("your-partition-key").eq("your-partition-key-data") & Key("your-sort-key").eq("your-sort-key-data"), # 取得するKey情報
      ScanIndexForward = false, # 昇順か降順か(デフォルトはtrue=昇順)
      Limit: 1 # 取得するデータ件数
    )
    return queryData
  except Exception as e:
        print e

さいごに

DynamoDBにはよくPutとqueryでデータを扱うことが多いので二つの方法についてまとめました。
場合によってはPutじゃなくてUpdateでなければならなかったり、とりあえず全件取得したいということきはscanを使ったりします。
ちゃんとLambdaにDynamoDBを操作できるポリシーを持たせておくのを忘れずに!
またquery実行時にときたま発生するのですが、「エラーが吐かれずに処理が完了したように見えるのに、DynamoDBにデータが入ってない!」見たいなことがあります。そんなときはタイムアウトを疑ってください。デフォルトでは3秒なので、queryで取得するデータの件数が多い場合はタイムアウトしてしまいます。
これはLambdaの中で使うように書きましたが、普通にSDKを使っているだけなので、WebアプリケーションやSDKを積めるデバイスからデータを扱う場合でも、上記のソースコードを改造すれば使えると思います。

余談ですが、Node書いて、Python書いてとしていると文法等がこんがらがってしまうのが最近の悩みのひとつです・・・

(ソースコードに汚い部分がありましたらご容赦ください・・・)

ではまた!

続きを読む

[AWS]S3のバケットをまとめて削除する

はじめに

AWSを使ってインターンの運営をしているのですが、インターンに参加している人全員のS3バケットがどんどん溜まってしまいます。そんな時やっぱ掃除しなきゃダメだよねという話になり、S3のバケットを一気に削除するスクリプトを書いてみました。

方法

AWS SDKをnodejsで実行することによって実装します。動作環境は以下になります。

  • MacOS Sierra 10.12.4
  • Node.js v6.10.2

S3について

概要

念のためS3について説明しますが、簡単にいうとAWS上にいろんなファイルをおけるストレージサービスです。HTMLファイルなどを置いて静的ウェブホスティングというものをすることによって、静的なウェブサイトを作成することも可能です。
ちなみになぜS3というかというとSimple Storage Serviceの略だからだそうです。試したい方は以下のURLから。
Amazon S3

バケット

S3にはストレージの単位としてバケットというものが用意されています。インターンでは、一人一人のバケットを作成することによって、個人のフロントエンドのアプリケーションを実現していました。

オブジェクト

バケットの中にあるファイルをS3ではObjectと表現します。例えば、index.htmlというファイルとかも全てObjectとなるわけです。そしてこのオブジェクトにはKeyというものが付与されています。Keyは実際にはバケットのルートディレクトリからの絶対パスになります。

削除方法

次の手順で実装します。
S3の仕様上、中身が入っているバケットは中身のファイルを全て削除した状態でないと削除できないので、中身を削除してからバケットを削除します。

  1. 削除したくないバケット名の配列を定義する
  2. AWSアカウントのS3の全てのバケットを取得する
  3. 削除するバケットに入っているファイルのKeyを取得する
  4. 3で取得したKeyを元にObjectを削除する
  5. バケット自体を削除する

下準備

AWSアカウントのクレデンシャル情報をaws configureで設定してください。
詳しくは割愛します。

ソースコード(スクリプト)

s3-delete.js
const AWS = require('aws-sdk');
const s3 = new AWS.S3({region: 'ap-northeast-1'});  //各自指定
const MAX_KEY_SIZE = 1000;
//除外するバケット名を記述
const excludeTables = [
  "hoge",
  "fuga"
];

//Objectを削除するPromiseオブジェクト
var deleteObject = function(bucketName, key) {
  return new Promise((resolve, reject) => {
    s3.deleteObject({  //Objectを削除
      Bucket: bucketName,
      Key: key
    })
    .then((data) => {
      console.log("  [Key]" + key + " DELETED");
      resolve();
    })
    .catch((err) => {
      reject("  [Error]" + key + " NOT DELETED ¥n" + err);
    });
  });
}

s3.listBuckets({})  //Bucketの一覧を取得
.promise()
.then((data) => {
  let buckets = data.Buckets;
  buckets.filter((bucket) => {
    return (bucket.Name && excludeTables.indexOf(bucket.Name) == -1);
  })
  .map((bucket) => {
    s3.listObjects({Bucket: bucket.Name, MaxKeys: MAX_KEY_SIZE})  //Objectの一覧を取得
    .promise()
    .then((object) => {
      Promise.all(object.Contents.map((content) => {
        return deleteObject(object.Name, content.Key);
      }))
      .then(() => {
        s3.deleteBucket({  //Bucketの削除
          Bucket: object.Name
        })
        .then(() => {
          console.log("[Bucket] " + object.Name + "  DELETED");
        });
      })
      .catch((err) => {
        console.log(err);
      });
    })
    .catch((err) => {
      console.log(err);
    });
  })
})
.catch((err)=>{
  console.log(err);
});

実行方法

npm installしてからスクリプトをnode.jsで実行します。

$ npm install
$ node s3-delete.js

まとめ

Dynamoのテーブルの削除と同様に簡単にできるのかと思いきや、バケット自体を空にしなくてはいけないっていうのがちょっと面倒でした。これで、今後のインターン運営が楽になりそうです。

続きを読む

WebSocketをLoadBalancer経由で使う時にsocket-io-sticky-sessionを使うと、keep-aliveを有効にしているとSession ID unknownになることがある

はじめに

npmの socket-io-sticky-session (以降、sio-sticky)が、stickyを有効にしたAWSのALBなどのLoadBalancerの後ろにいて、
LoadBalancerからKeep-Alive接続される場合、X-Forwarded-Forなどのヘッダによって正しいWorkerに割り振られないことがある。

という話です。

説明

症状

sio-stickyX-Forwarded-ForなどのHTTPヘッダをみてNode.jsの Webサーバ/WebSocketサーバ などにStickyに接続を振り分けてくれます。これは、SocketIOのpollingやwebsocketで使われるsidというSessionIDを持つリクエストを同じプロセスのNode.jsに振り分ける時に大変重宝します(sidは異なるプロセスのNode.jsで共有されないので)。

しかし、以下のような構成の場合、
ブラウザがALBのCookieをちゃんと送っていても、Stickyが上手く行かずにエラー(BadRequest: Session ID unknown)になることがあります。

20170829_WebSocket400エラー問題の説明.png

原因

原因は、 ALB と sio-sticky の間の keep-alive 接続だと思われます。

sio-sticky は 新規のTCP接続があった時に、配下のWorker(Node.js)にその接続を渡すので、keep-aliveされたあとの2回目以降のHTTPリクエストのヘッダなどを見たりしません。
また、ALBはどのターゲットにリクエストを送るかは制御していますが、どのkeep-aliveされた接続にどのHTTPリクエストを流すかは気にしてはいないので、基本的に運任せになります。

下の図で説明しますと、

20170829_WebSocket400エラー問題の説明.png

  • ① Browser1 と Browser2 は、異なるWorker(Node.js)に振り分けられていたとする
  • ② Browser2からのリクエストがしばらくなくても、ALBとWorkerの間のTCP接続は維持される(keep-alive)
  • ③ Browser1からALBにリクエストがある場合、この別のWorkerとkeep-aliveされた接続にリクエストが流されることがある。異なるworkerにリクエストが行くと、Session ID unknownと怒られてしまう。

というイメージです。

検証

sio-stickyのkeep-alive時の動作についての検証は下記を見てください。
https://github.com/mokemokechicken/socket-io-sticky-session/blob/feature/for_test_keep-alive/NOTE-keep-alive.md

ちなみに、ALBからsio-stickyへの接続は通常の設定ではkeep-aliveされますし、今回の現象も確認することができます。

さいごに

sio-stickyのような仕組みだとTCP接続時に振り分けるので辛いのでしょうね。
keep-aliveを無効にするのも非効率ですし、sio-stickyなどを使わずに1つのNode.jsを直接ALBにぶら下げるのがBetterなのかなぁ、と思います。

続きを読む

ECS運用のノウハウ

概要

ECSで本番運用を始めて早半年。ノウハウが溜まってきたので公開していきます。

設計

基本方針

基盤を設計する上で次のキーワードを意識した。

Immutable infrastructure

  • 一度構築したサーバは設定の変更を行わない
  • デプロイの度に新しいインフラを構築し、既存のインフラは都度破棄する

Infrastructure as Code (IaC)

  • インフラの構成をコードで管理
  • オーケストレーションツールの利用

Serverless architecture

  • 非常中型プロセスはイベントごとにコンテナを作成
  • 冗長化の設計が不要

アプリケーションレイヤに関して言えば、Twelve Factor Appも参考になる。コンテナ技術とも親和性が高い。

ECSとBeanstalk Multi-container Dockerの違い

以前に記事を書いたので、詳しくは下記参照。

Beanstalk Multi-container Dockerは、ECSを抽象化してRDSやログ管理の機能を合わせて提供してくれる。ボタンを何度か押すだけでRubyやNode.jsのアプリケーションが起動してしまう。
一見楽に見えるが、ブラックボックスな部分もありトラブルシュートでハマりやすいので、素直にECSを使った方が良いと思う。

ALBを使う

ECSでロードバランサを利用する場合、CLB(Classic Load Balancer)かALB(Application Load Balancer)を選択できるが、特別な理由がない限りALBを利用するべきである。
ALBはURLベースのルーティングやHTTP/2のサポート、パフォーマンスの向上など様々なメリットが挙げられるが、ECSを使う上での最大のメリットは動的ポートマッピングがサポートされたことである。
動的ポートマッピングを使うことで、1ホストに対し複数のタスク(例えば複数のNginx)を稼働させることが可能となり、ECSクラスタのリソースを有効活用することが可能となる。

※1: ALBの監視方式はHTTP/HTTPSのため、TCPポートが必要となるミドルウェアは現状ALBを利用できない。

アプリケーションの設定は環境変数で管理

Twelve Factor Appでも述べられてるが、アプリケーションの設定は環境変数で管理している。
ECSのタスク定義パラメータとして環境変数を定義し、パスワードやシークレットキーなど、秘匿化が必要な値に関してはKMSで暗号化。CIによってECSにデプロイが走るタイミングで復号化を行っている。

ログドライバ

ECSにおいてコンテナはデプロイの度に破棄・生成されるため、アプリケーションを始めとする各種ログはコンテナの内部に置くことはできない。ログはイベントストリームとして扱い、コンテナとは別のストレージで保管する必要がある。

今回はログの永続化・可視化を考慮した上で、AWSが提供するElasticsearch Service(Kibana)を採用することにした。
ECSは標準でCloudWatch Logsをサポートしているため、当初は素直にawslogsドライバを利用していた。CloudWatchに転送してしまえば、Elasticsearch Serviceへのストリーミングも容易だったからである。

Network (3).png

しかし、Railsで開発したアプリケーションは例外をスタックトレースで出力し、改行単位でストリームに流されるためログの閲覧やエラー検知が非常に不便なものだった。
Multiline codec plugin等のプラグインを使えば複数行で構成されるメッセージを1行に集約できるが、AWS(Elasticsearch Service)ではプラグインのインストールがサポートされていない。
EC2にElasticsearchを構築することも一瞬考えたが、Elasticsearchへの依存度が高く、将来的にログドライバを変更する際の弊害になると考えて止めた。
その後考案したのがFluentd経由でログをElasticsearch Serviceに流す方法。この手法であればFluentdでメッセージの集約や通知もできるし、将来的にログドライバを変更することも比較的容易となる。

Network (4).png

ジョブスケジューリング

アプリケーションをコンテナで運用する際、スケジュールで定期実行したい処理はどのように実現するべきか。
いくつか方法はあるが、1つの手段としてLambdaのスケジュールイベントからタスクを叩く方法がある(Run task)。この方法でも問題はないが、最近(2017年6月)になってECSにScheduled Taskという機能が追加されており、Lambdaに置き換えて利用可能となった。Cron形式もサポートしているので非常に使いやすい。

運用

ECSで設定可能なパラメータ

ECSコンテナインスタンスにはコンテナエージェントが常駐しており、パラメータを変更することでECSの動作を調整できる。設定ファイルの場所は /etc/ecs/ecs.config
変更する可能性が高いパラメータは下表の通り。他にも様々なパラメータが存在する。

パラメータ名 説明 デフォルト値
ECS_LOGLEVEL ECSが出力するログのレベル info
ECS_AVAILABLE_LOGGING_DRIVERS 有効なログドライバの一覧 [“json-file”,”awslogs”]
ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION タスクが停止してからコンテナが削除されるまでの待機時間 3h
ECS_IMAGE_CLEANUP_INTERVAL イメージ自動クリーンアップの間隔 30m
ECS_IMAGE_MINIMUM_CLEANUP_AGE イメージ取得から自動クリーンアップが始まるまでの間隔 1h

パラメータ変更後はエージェントの再起動が必要。

$ sudo stop ecs
$ sudo start ecs

クラスタのスケールアウトを考慮し、ecs.configはUserDataに定義しておくと良い。
以下はfluentdを有効にしたUserDataの記述例。

#!/bin/bash
echo ECS_CLUSTER=sandbox >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS=["fluentd"] >> /etc/ecs/ecs.config

CPUリソースの制限

現状ECSにおいてCPUリソースの制限を設定することはできない(docker runの--cpu-quotaオプションがサポートされていない)。
タスク定義パラメータcpuは、docker runの--cpu-sharesにマッピングされるもので、CPUの優先度を決定するオプションである。従って、あるコンテナがCPUを食いつぶしてしまうと、他のコンテナにも影響が出てしまう。
尚、Docker 1.13からは直感的にCPUリソースを制限ができる--cpusオプションが追加されている。是非ECSにも取り入れて欲しい。

ユーティリティ

実際に利用しているツールを紹介。

graph.png

ルートボリューム・Dockerボリュームのディスク拡張

ECSコンテナインスタンスは自動で2つのボリュームを作成する。1つはOS領域(/dev/xvda 8GB)、もう1つがDocker領域(/dev/xvdcz 22GB)である。
クラスタ作成時にDocker領域のサイズを変更することはできるが、OS領域は項目が見当たらず変更が出来ないように見える。

Screen_Shot_2017-08-31_at_11_00_03.png

どこから設定するかというと、一度空のクラスタを作成し、EC2マネージメントコンソールからインスタンスを作成する必要がある。

また、既存ECSコンテナインスタンスのOS領域を拡張したい場合は、EC2マネージメントコンソールのEBS項目から変更可能。スケールアウトを考慮し、Auto scallingのLaunch Configurationも忘れずに更新しておく必要がある。

補足となるが、Docker領域はOS上にマウントされていないため、ECSコンテナインスタンス上からdf等のコマンドで領域を確認することはできない。

デプロイ

ECSのデプロイツールは色々ある(ecs_deployerは自分が公開している)。

社内で運用する際はECSでCIを回せるよう、ecs_deployerをコアライブラリとしたCIサーバを構築した。

デプロイ方式

  • コマンド実行形式のデプロイ
  • GitHubのPushを検知した自動デプロイ
  • Slackを利用したインタラクティブデプロイ

Screen_Shot_2017-08-31_at_11_31_15.png

デプロイフロー

ECSへのデプロイフローは次の通り。

  1. リポジトリ・タスクの取得
  2. イメージのビルド
    • タグにGitHubのコミットID、デプロイ日時を追加
  3. ECRへのプッシュ
  4. タスクの更新
  5. 不要なイメージの削除
    • ECRは1リポジトリ辺り最大1,000のイメージを保管できる
  6. サービスの更新
  7. タスクの入れ替えを監視
    • コンテナの異常終了も検知
  8. Slackにデプロイ完了通知を送信

現在のところローリングデプロイを採用しているが、デプロイの実行から完了までにおよそ5〜10分程度の時間を要している。デプロイのパフォーマンスに関してはまだあまり調査していない。

ログの分類

ECSのログを分類してみた。

ログの種別 ログの場所 備考
サービス AWS ECSコンソール サービス一覧ページのEventタブ APIで取得可能
タスク AWS ECSコンソール クラスタページのTasksタブから”Desired task status”が”Stopped”のタスクを選択。タスク名のリンクから停止した理由を確認できる APIで取得可能
Docker daemon ECSコンテナインスタンス /var/log/docker ※1
ecs-init upstart ジョブ ECSコンテナインスタンス /var/log/ecs/ecs-init.log ※1
ECSコンテナエージェント ECSコンテナインスタンス /var/log/ecs/ecs-agent.log ※1
IAMロール ECSコンテナインスタンス /var/log/ecs/audit.log タスクに認証情報のIAM使用時のみ
アプリケーション コンテナ /var/lib/docker/containers ログドライバで変更可能

※1: ECSコンテナインスタンス上の各種ログは、CloudWatch Logs Agentを使うことでCloudWatch Logsに転送することが可能(現状の運用ではログをFluentdサーバに集約させているので、ECSコンテナインスタンスにはFluentdクライアントを構築している)。

サーバレス化

ECSから少し話が逸れるが、インフラの運用・保守コストを下げるため、Lambda(Node.js)による監視の自動化を進めている。各種バックアップからシステムの異常検知・通知までをすべてコード化することで、サービスのスケールアウトに耐えうる構成が容易に構築できるようになる。
ECS+Lambdaを使ったコンテナ運用に切り替えてから、EC2の構築が必要となるのは踏み台くらいだった。

トラブルシュート

ログドライバにfluentdを使うとログの欠損が起きる

ログドライバの項に書いた通り、アプリケーションログはFluentd経由でElasticsearchに流していたが、一部のログが転送されないことに気付いた。
構成的にはアプリケーションクラスタからログクラスタ(CLB)を経由してログを流していたが、どうもCLBのアイドルタイムアウト経過後の最初のログ数件でロストが生じている。試しにCLBを外してみるとロストは起きない。

Network (1).png

ログクラスタ(ECSコンテナインスタンスの/var/log/docker)には次のようなログが残っていた。

time="2017-08-24T11:23:55.152541218Z" level=error msg="Failed to log msg "..." for logger fluentd: write tcp *.*.*.*:36756->*.*.*.*:24224: write: broken pipe"
3) time="2017-08-24T11:23:57.172518425Z" level=error msg="Failed to log msg "..." for logger fluentd: fluent#send: can't send logs, client is reconnecting"

同様の問題をIssueで見つけたが、どうも現状のECSログドライバはKeepAliveの仕組みが無いため、アイドルタイムアウトの期間中にログの送信が無いとELBが切断してしまうらしい(AWSサポートにも問い合わせた)。

という訳でログクラスタにはCLBを使わず、Route53のWeighted Routingでリクエストを分散することにした。

Network (2).png

尚、この方式ではログクラスタのスケールイン・アウトに合わせてRoute 53のレコードを更新する必要がある。
ここではオートスケールの更新をSNS経由でLambdaに検知させ、適宜レコードを更新する仕組みを取った。

コンテナの起動が失敗し続け、ディスクフルが発生する

ECSはタスクの起動が失敗すると数十秒間隔でリトライを実施する。この時コンテナがDockerボリュームを使用していると、ECSコンテナエージェントによるクリーンアップが間に合わず、ディスクフルが発生することがあった(ECSコンテナインスタンスの/var/lib/docker/volumesにボリュームが残り続けてしまう)。
この問題を回避するには、ECSコンテナインスタンスのOS領域(※1)を拡張するか、コンテナクリーンアップの間隔を調整する必要がある。
コンテナを削除する間隔はECS_ENGINE_TASK_CLEANUP_WAIT_DURATIONパラメータを使うと良い。

※1: DockerボリュームはDocker領域ではなく、OS領域に保存される。OS領域の容量はデフォルトで8GBなので注意が必要。

また、どういう訳か稀に古いボリュームが削除されず残り続けてしまうことがあった。そんな時は次のコマンドでボリュームを削除しておく。

# コンテナから参照されていないボリュームの確認
docker volume ls -f dangling=true

# 未参照ボリュームの削除
docker volume rm $(docker volume ls -q -f dangling=true)

ECSがELBに紐付くタイミング

DockerfileのCMDでスクリプトを実行するケースは多々あると思うが、コンテナはCMDが実行された直後にELBに紐付いてしまうので注意が必要となる。

bundle exec rake assets:precompile

このようなコマンドをスクリプトで実行する場合、アセットがコンパイル中であろうがお構いなしにELBに紐付いてしまう。
時間のかかる処理は素直にDockerfile内で実行した方が良い。

続きを読む

Amazon AlexaとFire TVでHello Worldを試してみる

前回まではiPhoneのアプリでAlexaと繋いで試していたんですが、やっぱりハードウェアが無いとモチベーション上がらないんです。

Amazon Alexa 事始め – Qiita

でも、未だにAmazon Echo関連製品が国内では発売されていません。
しかし先日、Amazon Fire TVがAlexaに対応していることを知りました。
ということでFire TVを購入してHello Worldを試してみました。

はじめに

今回は下記の様な構成です。
Fire TVに「tell hello world say hello」と声で指示をだします。
その結果をLambdaで処理してFire TVに返してFire TVに発声させます。
また今回の手順ではAmazonのUSアカウントが必要です。

alexa.png

Hello world

今回はAmazonが提供しているAlexaのHello worldのnode.js版を利用します。
git cloneでファイルを持ってきます。

git clone git@github.com:alexa/skill-sample-nodejs-hello-world.git

取得ソースの/src/index.jsがLambda側で処理するプログラムです。
Alexaから HelloWorldIntent が呼び出され this.emit(':tell', 'Hello World!'); で結果として返すテキストを指定しています。

#index.js
'use strict';
var Alexa = require("alexa-sdk");

exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
};

var handlers = {
    'LaunchRequest': function () {
        this.emit('SayHello');
    },
    'HelloWorldIntent': function () {
        this.emit('SayHello')
    },
    'SayHello': function () {
        this.emit(':tell', 'Hello World!');
    },
};

Lambdaを作る

先程、取得したindex.jsをZIPで圧縮してLambdaに登録できるようにします。
下記のコマンドを実行します。

cd skill-sample-nodejs-hello-world/src/
npm install --save alexa-sdk
zip -r helloWorld.zip index.js node_modules

AWS Lambdaを開き、「一から作成」を選択。
スクリーンショット_2017-08-27_16_54_35.png

「Alexa Skills Kit」を選択して「次へ」
スクリーンショット_2017-08-27_16_56_31.png

名前には任意の名前を入力します。今回は「helloWorld」としました。
ランタイムは「Node.js 4.3」を選択。
コードエントリータイプは「.ZIPファイルをアップロード」を選択して、関数パッケージに先程圧縮したZIPファイルを選択。
スクリーンショット_2017-08-27_16_59_39.png

ハンドラは「index.handler」そのまま。
ロールを「テンプレートから新しいロールを作成」を選択。
ロール名に適当な名前を入力します。今回は「lambdaRole」としました。
そして「次へ」
スクリーンショット_2017-08-27_17_05_33.png

確認画面で先程設定した内容になっているか確認して「関数の作成」
スクリーンショット 2017-08-27 17.07.44.png

Lambdaの右上のarnは後ほどAlexaの接続先として設定するので控えておきます。
スクリーンショット_2017-08-27_17_12_00.png

Alexa Appを作る

Amazon Developer Consoleのログインします。

「Alexa Skills Kit」を選択。
スクリーンショット_2017-08-27_17_23_23.png

「Add a New Skill」を選択。
スクリーンショット_2017-08-27_17_24_13.png

Skill Typeに「Custom Interaction Model」を選択。
Languageに「English(U.S.)」を選択。
Nameには任意の名前を入れます。今回は「Hello world」としました。
Invocation Nameに「hello world」と入力します。Invocation NameはAlexaに音声で指示する時のアプリ名になります。
「Alexa tell hello world …」という風に「tell」の後にInvocation Nameに設定したアプリ名を指示するとAlexaのアプリが起動します。
「save」して「Next」を選択
スクリーンショット_2017-08-27_17_41_20.png

Intent Schemaに下記のように入力します。
これはどういったIntentがあるかの定義になります。
今回は index.js に書かれた HelloWorldIntent という名前のIntentが定義されているということです。

{
  "intents": [
    {
      "intent": "HelloWorldIntent"
    }
  ]
}

Sample Utterancesには下記のように入力します。
Utterancesは入力音声に紐づくIntentのセットを表しています。
「Hello」「say hello」「say hello world」と話しかけると「HelloWorldIntent」が呼び出すという組み合わせを指定します。

HelloWorldIntent hello
HelloWorldIntent say hello
HelloWorldIntent say hello world

スクリーンショット_2017-08-27_17_52_35.png

Endpoinには「AWS Lambda ARN (Amazon Resource Name)」を選択
DefaultにはLambda作成時に控えたarnを入力します。
「Next」を選択
スクリーンショット_2017-08-27_18_15_30.png

これでAlexa Appも完成したのでAlexaからLambdaを正しく呼び出せるか確認します。
HTTPS endpointに設定したarnが選択されていることを確認。
Enter Utteranceに「hello」と入力して「Ask Hello World」を押すとService Responseが返ってきます。
スクリーンショット_2017-08-28_8_13_31.png

Service Responseの中の<speak> Hello World! </speak> の部分がFire TVが話す部分です。

"outputSpeech": {
  "ssml": "<speak> Hello World! </speak>",
  "type": "SSML"
},

それでは実際にFire TVに話しかけて結果を喋らせてみましょう。
Fire TVもAmazon USアカウントでログインするとAlexaが利用できます。
“tell hello world say hello.”と発話した動画はこちらです。(2回目発音が悪いのか正しく認識されていませんが。。。

AlexaでHello world作ってみた。https://t.co/9EFxYkpLJJ

— tochi (@aguuu) 2017年8月28日

Fire TVはAlexaを起動してリモコンのボタンを押して始めてAlexaに音声で指示が出せます。
しかし、Echo showなどは音声だけで操作ができます。
日本での発売が楽しみです。

参考

Amazon AlexaのCustom SkillのサンプルをService Simulatorで試してみる | Developers.IO
http://dev.classmethod.jp/cloud/aws/amazon-alexa-custom-skill-service-simulator/

続きを読む