CodeBuildでSpecify either ‘DefinitionUri’ or ‘DefinitionBody’ property and not both

DefinitionBodyは書いているのに怒られた

AWS SAMを使用したデプロイを行っていて、ドハマリしたのでメモを共有します

前提環境

  • クロスリージョン&クロスアカウントなサーバレスアプリケーションのデプロイをSAMで行う

    • CodeCommit
    • CodePipeline
    • CodeBuild
      • AmazonLinux node4.4.6 (eb-nodejs-4.4.6-amazonlinux-64:2.1.3)

現象

CloudFormationテンプレート

下記のようなCloudFormationテンプレートを作成

template.yaml
  HelloFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: hello.handler
      Runtime: nodejs6.10
      CodeUri: ./src/
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      DefinitionBody:
        swagger: "2.0"
        info:
          version: "1.0"
          title: "HelloApi"
        schemes:
        - https
        basePath: !Sub /${Stage}
        paths:
          /hello:
            get:
              x-amazon-apigateway-integration:
                uri: !Sub  "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloFunction.Arn}/invocations"
                passthroughBehavior: "when_no_match"
                httpMethod: "POST"
                type: "aws_proxy"

CodeBuild

CodeBuildでテスト&デプロイを行う

buildspec.yml
version: 0.1

phases:
  install:
    commands:
      - curl -o /usr/local/bin/jq -L https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && chmod +x /usr/local/bin/jq
      - mkdir ~/.aws
      - aws sts assume-role --role-arn ${DEV_CFN_ROLE} --role-session-name codebuild | jq -r '"[default]naws_access_key_id = (.Credentials.AccessKeyId)naws_secret_access_key = (.Credentials.SecretAccessKey)naws_session_token = (.Credentials.SessionToken)n"' >> ~/.aws/credentials
      - cd test && npm install && npm install -g mocha
  pre_build:
    commands:
      - echo Test Started on `date`
      - cd test && mocha test.js | tee test.log
  build:
    commands:
      - echo Build started on `date`
      - aws cloudformation --region ap-northeast-1 package --template-file template.yaml --s3-bucket ${DEV_SAM_BUCKET} --output-template-file outputTemplate.yaml
      - aws cloudformation --region ap-northeast-1 deploy --template-file outputTemplate.yaml --stack-name ${PROJECT_ID} --capabilities CAPABILITY_IAM --parameter-overrides Stage=${STAGE}
  post_build:
    commands:
      - echo Build completed on `date`
artifacts:
  files:
    - '**/*'

実行結果

[Container] 2017/04/29 02:07:51 Waiting for changeset to be created..
[Container] 2017/04/29 02:08:02 Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Api] is invalid. Specify either 'DefinitionUri' or 'DefinitionBody' property and not both

template.yamlにはDefinitionBodyが正しく入っているのに怒られた!

解決

https://github.com/awslabs/serverless-application-model/issues/93

aws-cliのバグだったΣ(゚Д゚)

buildspec.ymlの修正

使っていたCodeBuildコンテナのaws-cliが古いことが原因だったようなので、上記issueにも記載の通り、aws-cliをアップデートしてあげる

buildspec.yml
version: 0.1

phases:
  install:
    commands:
      - pip install --upgrade awscli
      - curl -o /usr/local/bin/jq -L https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && chmod +x /usr/local/bin/jq
      - mkdir ~/.aws
      - aws sts assume-role --role-arn ${DEV_CFN_ROLE} --role-session-name codebuild | jq -r '"[default]naws_access_key_id = (.Credentials.AccessKeyId)naws_secret_access_key = (.Credentials.SecretAccessKey)naws_session_token = (.Credentials.SessionToken)n"' >> ~/.aws/credentials
      - cd test && npm install && npm install -g mocha
  pre_build:
    commands:
      - echo Test Started on `date`
      - cd test && mocha test.js | tee test.log
  build:
    commands:
      - echo Build started on `date`
      - aws cloudformation --region ap-northeast-1 package --template-file template.yaml --s3-bucket ${DEV_SAM_BUCKET} --output-template-file outputTemplate.yaml
      - aws cloudformation --region ap-northeast-1 deploy --template-file outputTemplate.yaml --stack-name ${PROJECT_ID} --capabilities CAPABILITY_IAM --parameter-overrides Stage=${STAGE}
  post_build:
    commands:
      - echo Build completed on `date`
artifacts:
  files:
    - '**/*'

実行結果

[Container] 2017/04/29 04:03:34 Waiting for changeset to be created..
[Container] 2017/04/29 04:03:45 Waiting for stack create/update to complete
[Container] 2017/04/29 04:04:16 Successfully created/updated stack - helloproject

無事デプロイ出来ました

教訓

パッケージはアップデートしてから使いましょう。。。

続きを読む

LambdaでRDSチュートリアル

AWSのAPI GatewayとLambdaとRDSを用いて、RDSに保存されているデータを外部に公開するAPIを作成することを想定して、まずは、LambdaからRDSに接続する

想定するテーブル構造

# テーブル名: link_clicks
| カラム名   | description       | 
| path      | urlのパス         |
| clicks    | リンクのクリック数   |
| stat_date | 集計日             |

1. Lambda関数を作成する

lambdaを用いて、RDS(mysql)に接続して、クエリパラメータ(path)に応じた情報を引き出して返すものプログラムをpythonで作成する

1.1 pythonパッケージ(api)を作る

mkdir api

1.2 pythonの依存パッケージをインストールする

mysqlへの接続のためにPyMySQLをインストールする

pip install PyMySQL -t api

1.3 メイン関数を記載する

メイン関数(handler)記載ファイルと設定ファイルを用意する
(YOUR_XXXは置換する)

api/api.py
# -*- coding: utf-8 -*-
import sys
import logging
import rds_config
import pymysql
import datetime as DT
import json

#rds settings
rds_host  = rds_config.db_host
name = rds_config.db_username
password = rds_config.db_password
db_name = rds_config.db_name


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

try:
    mysql_client = pymysql.connect(rds_host, user=name, passwd=password, db=db_name, connect_timeout=5)
except:
    logger.error("ERROR: Unexpected error: Could not connect to MySql instance.")
    sys.exit()

logger.info("SUCCESS: Connection to RDS mysql instance succeeded")

def handler(event, context):
    """
    This function fetches content from mysql RDS instance
    """

    path = event["path"]

    query = "select sum(clicks) from link_clicks where path = '%(path)s' group by path" % dict(path=path)
    logger.info(query)

    results = []
    with mysql_client.cursor() as cur:
        cur.execute(query)
        for row in cur:
            clicks = int(row[0])
            results.append({"clicks": clicks})

    return json.dumps(results)
api/rds_config.py
db_host = "YOUR_HOST"
db_username = "YOUR_USERNAME"
db_password = "YOUR_PASSWORD"
db_name = "YOUR_DB_NAME"

1.4 lambdaにプログラムをアップロード

deploy.sh
# zip作成
(cd api && zip -r - *) > api.zip

# create lambda function
## YOUR_ROLEにはlambda-vpc-execution-roleを指定すること
aws lambda create-function \
--region YOUR_REGION \
--function-name api  \
--zip-file fileb://api.zip \
--role YOUR_ROLE \
--handler api.handler \
--runtime python2.7 \
--vpc-config SubnetIds=YOUR_SUBNET_IDS,SecurityGroupIds=YOUR_SECURITY_GROUP_ID

# update lambda function
aws lambda update-function-code \
--region YOUR_REGION \
--function-name  api  \
--zip-file fileb://api.zip

2. Lambda関数を確認する

2.1 Lambda関数がアップロードされたことを確認する

以下にアクセスして、apiファンクションが追加されたことを確認する
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/functions?display=list

apiファンクションが追加されてる.png

2.2 テストイベントを設定する

actions > configure test event からテストイベントを登録する
設定したものは以下の通り

{
  "path": "/11111"
}

testイベント設定.png
テストイベントとしてpathを設定.png

2.3 テストする

上記画像の save&test をクリックしてテストする
テスト結果が表示される.png

最後に

以上の手順で、lambda => RDSに接続して、データを参照できるようになりました
詳細はawsのドキュメントにも書いてあるので、そちらを参考にしてください
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc-rds-create-lambda-function.html

続きを読む

G SuiteのアカウントをAWS コンソールへ連携させる

  • 実現したいこと

    • G Suiteで管理しているユーザアカウントをAWSコンソールへのログインユーザと連携したい

設定手順

大きくわけて以下の流れになるかと思います。
1. G Suite上でユーザ管理でカスタム属性の作成
2. G Suite上でAmazon Web Servicesクラウドアプリケーションを作成
3. AWS管理コンソールでIDプロバイダを作成
4. AWS管理コンソールで割当てるロールを作成
5. G Suite上でユーザにSAMLの設定情報を追加する

  1. ユーザのカスタム属性の作成

    1. Google Adminへログインします。
    2. ユーザを選択します。
    3. 画面右上にある[ユーザ属性を管理]をクリック
      ユーザ属性管理.png
    4. カスタムカテゴリを追加をクリック
    5. ここでは、AWSのSAMLへ渡す情報をカスタム属性で作成します。
    6. [カテゴリを追加]をクリックして、AWSアカウントロール属性を作成
      カスタムカテゴリ属性.png
    7. 以下の属性を作成します。

      属性名 タイプ 複数の値 限定公開
      role テキスト はい はい
      SessionDuration 整数 いいえ はい
    8. [完了]をクリックします。

  2. 事前構成済みのAmazon Web ServicesクラウドアプリケーションでG SuiteをSAML ID プロバイダ(idp)として選択する

    1. Google Adminへ特権管理者としてログインします。
    2. [アプリ] > [SAML アプリ]をクリックします。
    3. 下にあるプラス(+)アイコンをクリックします。
      SAMLアプリの追加.png
    4. リストから[Amazon Web Services]の項目を選択します。
    5. ここでGoogle Idp情報のページから、X.509証明書とIDPメタデータをダウンロードしておきます。
    6. [基本的なアプリケーション情報]ウィンドウの[アプリケーション名]と[説明]の値を入力します。
    7. [次へ]をクリックします。
    8. [サービスプロバイダの詳細]の[エンティティID]、[ACSのURL]、[開始URL]欄に次を入力します。
    9. [署名付き応答]チェックボックスはオフのままにします。
    10. [名前のID]の既定値は、メールアドレスです。メールアドレスを使用します。
    11. [次へ]をクリックします。
    12. [新しいマッピングを追加]をクリックし、以下の属性値を設定します。
    13. [完了]をクリックします。
  3. AWS側の設定

    1. IDプロバイダの作成

      1. AWSコンソールへログインする。
      2. [IAM] > [プロバイダ]から[プロバイダの作成]をクリックします。
      3. プロバイダのタイプにSAMLを選択して、プロバイダ名を入力。
      4. メタデータドキュメントへ1.5.でダウンロードしたメタデータをアップロードして、次へ進みます。
      5. [作成]ボタンをクリックして作成します。
      6. プロバイダの概要にてプロバイダのARNを覚えておきます。

      idp_provider.png

    2. ロールの作成

      1. 必要な権限を持ったロールを作成します。
      2. ここでは、一旦AdministratorAccessの権限を持ったロールを作成します。
      3. ロールの概要からロールARNを覚えておきます。

      iam_role_arn.png

  4. G Suiteユーザの設定

    1. ユーザ一覧から、設定するユーザをクリックして詳細を表示します。
    2. [アカウント] > ユーザ属性を管理の[編集]をクリック。
    3. role欄に<ロールARN>,<プロバイダARN>を入力
    4. SessionDurationにタイムアウト(秒)を指定します。

    ユーザ設定.png
    6. [ユーザを更新]をクリックして設定を反映させます。

  5. G SuiteのAmazon Web Servicesアプリケーションの設定

    1. [アプリ] > [SAML アプリ]をクリックします。
    2. 有効にするアプリケーションの右側のメニューからオンにします。
    3. 画面右上のGoogleアプリから、[もっと見る]をクリックしてAWSアプリケーションをクリックして、AWSコンソールへログインできることを確認します。
      Appsアプリ.png

上記手順でAWSへ個別にIAMユーザを作成して管理する必要はなくなります。
権限ロールなどは要件にあわせて複数作成するといいかもしれません。
GSuiteのIdp情報のダウンロードが記載されているヘルプの場所と違い、しばらく探しました。。。
今後も、双方の管理コンソールが変わる可能性があるので適宜読み替えてください。

続きを読む

IAM Database AuthenticationをPythonから試す

背景

RDS for MySQLとAuroraのDB接続にIAMが使えるようになったそうです。
Manage access to your RDS for MySQL and Amazon Aurora databases using AWS IAM
IAM Database Authentication for MySQL and Amazon Aurora
サンプルがJavaだったのでPythonから試してみました。

環境

  • Amazon Linux 2017.03
  • Aurora 1.12
  • Python 2.7.12
  • boto3-1.4.4

準備

Aurora

RDSのクラスターから「クラスターの変更」を開き「IAMのDB認証を有効にする」を「はい」に設定します。Auroraの場合db.t2.smallはIAMデータベース認証をサポートしていないためdb.t2.medium以上で試してください。
RDS_·_AWS_Console.png

DBユーザーの作成

IAMアクセス用のDBユーザーを作成し、必要な権限を付与します。

mysql> CREATE USER iam_auth_user@'testdb-cluster.cluster-abcdefghijkl.ap-northeast-1.rds.amazonaws.com' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS';
mysql> GRANT SELECT ON `testdb`.* TO iam_auth_user@'%';

公開鍵のダウンロード

IAMデータベース認証はSSL接続が必須という事なので公開鍵をダウンロードしてec2上の適当なパスに配置しておきます
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.Overview.html#Aurora.Overview.Security.SSL

IAM

ドキュメントを参考にIAM Roleに権限を付与します。リソースIDはクラスターのものを指定しています。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "rds-db:connect"
      ],
      "Resource": [
        "arn:aws:rds-db:ap-northeast-1:12345678:dbuser:cluster-12ABC34DEFG5HIJ6KLMNOP78QR/iam_auth_user"
      ]
    }
  ]
}

接続

iam_db_auth.py
#  -*- coding: utf-8 -*-
from __future__ import print_function
import boto3
import mysql.connector
from mysql.connector.constants import ClientFlag

rds = boto3.client('rds', region_name='ap-northeast-1')

user = 'iam_auth_user'
host = 'testdb-cluster.cluster-abcdefghijkl.ap-northeast-1.rds.amazonaws.com'
db_auth_token = rds.generate_db_auth_token(host, 3306, user, 'ap-northeast-1')

config = {
    'user': user,
    'password': db_auth_token,
    'host': host,
    'db': 'testdb',
    'client_flags': [ClientFlag.SSL],
    'ssl_ca': 'rds-combined-ca-bundle.pem'
}

cnx = mysql.connector.connect(**config)
cur = cnx.cursor(buffered=True)

cur.execute('SELECT AURORA_VERSION();')
print(cur.fetchone())

cur.close()
cnx.close()
$ python iam_db_auth.py 
[(u'1.12',)]

以上です。

参考

続きを読む

AmazonAlexaのサンプルを動かしながら、じっくりと基本を学んでみよう

はじめに

Amazon Alexaってご存知でしょうか。
残念ながら現在は日本語には対応していませんが、EchoやTapなどのデバイスに喋りかけると、あらかじめ紐付けておいたスキルに応じて色々な処理を動かすことができます。

  • 「今日の東京の天気はどんな感じ?」
  • 「明日の東京は晴れですよ!」

というステートレスでオープンな簡単な応答もできますし、Alexaとの会話を通じて難しい応答を返すことも作りこみ次第では可能です。

  • 私「旅行に行きたいなぁ」
  • Alexa「どこに行きたいですか?」
  • 私「アジアがいいなぁ」
  • Alexa「アジアのどの辺ですか?」

みたいなやり取りから旅行プランをおすすめしてくれて、チケットを予約してくれる日も近そうです。

この「スキル」は自分で作って公開することもできますし、公開しないでクローズドに動かすこともできます。
今回は入門編ということでAlexaのサンプルを動かしながら、Alexaで出てくる用語や、少しソースの解説をしてみます。

私は少し遠回りしましたが1時間もあれば試すことができます。
サンプルをひとつ動かすだけで、「あれ。ちょっとしたことなら簡単にできるかも。」と思えてしまいますので是非お試しください。


Alexaを構成する登場人物たちを抑えよう

ドキュメントを読むにも登場人物を抑えないとはじまりません。

  • デバイス

    • Echo / Tap / dot / KindleFireTV / Amazonアプリなど色々ありますが、Echoなどは日本では技適が通ってませんのでAmazon.comで購入しても動かしてはいけません。
    • 今のところはiPhoneアプリや公式のシミュレータであるEchosimを使いましょう。
  • Alexa App

    • Alexaのスキルの検索、登録 / カードの表示 / 追加したスキルの設定ができます。
    • PCからでもスキルの追加・変更が可能。http://alexa.amazon.com/spa/index.html
    • アプリを入手するにはアメリカのAppleIDが必要で、ログインにはAmazon.comのアカウントが必要です。
  • Alexa Service

    • デバイスからのデータを受け取り、Lambdaイベントを発火するサービス
  • AWS Lambda

    • Alexa Serviceからデータを受け取り、解析して処理を行いレスポンスを返却します。
  • Amazon Developer Console

    • カスタムスキルを作る場合はDeveloper Consoleで追加しテストなどを行います。ログインにはAmazon.comのアカウントが必要です。
    • スキルの登録には審査が必要で、7日くらいかかるらしいです。
  • AVS(Alexa Voice Service)

    • 自分で作ったデバイスに組み込むことができるSDK。今回は直接関係ありません。

サンプルを動かしてみよう

  • HelloWorldのようなステートレスなものではつまらないので、SessionというAmazonが提供しているサンプルを動かしてみます。
  • このサンプルは自分の好きな色をAlexaに教えるとAlexaが記憶してくれて、「私の好きな色は何?」と聞くと先ほど記憶してくれた色を答えてくれるサンプルです。

環境を用意する

  • Java8とMavenをインストールしましょう(mvnwくらいサンプルに置いといてくれたらいいのに。。)

LambdaFunctionを作る

  • Githubからサンプルをダウンロードする
git clone https://github.com/amzn/alexa-skills-kit-java.git
  • パッケージングする
cd alexa-skills-kit-java/samples
mvn assembly:assembly -DdescriptorId=jar-with-dependencies package
  • target/alexa-skills-kit-1.3.0-jar-with-dependencies.jarができていることを確認する

  • ManagementConsole – Lambdaを開く。この時リージョンは必ず「バージニア北部」であることを確認する

  • LambdaFunctionをデプロイする(難しくないので割愛)

    • トリガーには「Alexa Skills Kit」を選択
    • Java8
    • Handler:session.SessionSpeechletRequestStreamHandler
    • Roleは初回の場合は「テンプレートから新しいRolwを作成」を選択し、適当な名前付けておきます。
  • デプロイが成功したら後で必要となるARNをコピーしておきましょう。

image

  • LambdaFunctionのテストをしたいところですが、DeveloperConsoleから行うのがおススメですのでここからは行いません。

Developer Console

  • [Alexa Skills Kit] – [Get Started]を選択
  • [Add New Skill]を選択し、SkillInformationにこんな感じで登録する

image

  • Interaction Modelに「IntentSchema」「CustomSlotType」「Sample Utterances」を登録する

    • Githubのサンプル内に入っているのでコピペしよう
  • Configuration
    • Endpointsは「AWS Lambda ARN (Amazon Resource Name)」を選択
    • North Americaを選択し、先ほどコピーしたARNを入力します。
  • Test
    • ここではLambdaFunctionのテストができます。
    • ためしに「alexa, open session」と入力してみましょう。
    • うまくレスポンスが返ってくればLambdaResponseにjsonが返却されるはずです。
    • 右下のListenというボタンを押すと実際にechoなどのデバイスから聞こえてくる音声が聞こえてくるはずです。

image

さぁここまでできればデバイスから呼べる準備が完了です。
なお、更改するには審査にパスする必要があり、DeveloperConsoleの内容を全部埋める必要があります。

スキルを追加する

  • スキルを追加するにはAlexaのポータルか、AlexaAppを利用して登録します。
  • 画面左の「Skills」をクリックし、画面右上の「YourSkill」を選択すると先ほどDeveloperConsoleで作成したSkillが表示されるはずです。

image

デバイスを用意する

  • 開発中はechosim.ioを利用すると良いでしょう。
  • Amazonアカウントでログインすることで使用できます。
  • 本物のデバイスでは「Alexa」と呼びかけるのですが、Echosimはボタンを押すことで「Alexa」と呼んだことと同じ意味になります。

image

  • ちなみに私だけかもしれませんがEchosimはMacだと快調に動くのですが、Windows10だとなぜか認識しないことがありました。また、スマホアプリでAVSで作られた無料のものがありますが、これもうまくいったりいかなかったりしました。
  • 色々試した結果、良い感じだったのはAmazonモバイルアプリでした。これのロケールをアメリカにすると音声検索でAlexaが利用できますよ。

しゃべってみましょう

こんな感じのやりとりができますのでやってみましょう。

  • 私「Alexa,open session」(Echo使っていない場合はAlexaは不要です)
  • Alexa「Hello. Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by saying, my favorite color is blue」
  • 私「my color is red」
  • Alexa「I now know that your favorite color is red. You can ask me your favorite color by saying, what’s my favorite color?」
  • 私「whats my color」
  • Alexa「Your favorite color is red」

Alexa用語を覚えておこう

  • そんなに多くないので覚えるのはそんなに大変じゃありません。

Invocation Name

  • Developer Consoleで設定するSkillを特定する一意な名前。
  • Invocation Nameをsessionという名前にしておくと、「Alexa, tell session」でSkillを呼び出すことができます。
  • tell / ask / openなどのあとにInvocationNameを話すとLambdaFunction#onLaunchが呼ばれます
  • 今回のサンプルでは#getWelcomeResponseが呼ばれ、「Hello. Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by saying, my favorite color is blue」を喋らせています。

Intent Schema

  • 説明が難しいですが、Alexaとのやりとりの枠組みを定義します。(そのままじゃん・・・)
  • やり取りの中でSlotを使う場合は合わせてそれを定義します。
  • 今回の場合はMyColorIsIntentとWhatsMyColorIntentというIntentが使われ、MyColorIsIntentではLIST_OF_COLORSというカスタムSlotが利用されることを示しています。このIntentで会話される内容はUtteranceに定義します。
{
  "intents": [
    {
      "slots": [
        {
          "name": "Color",
          "type": "LIST_OF_COLORS"
        }
      ],
      "intent": "MyColorIsIntent"
    },
    {
      "intent": "WhatsMyColorIntent"
    }
  ]
}

Utterance

  • 日本語では「発声」です。IntentとAlexaが認識できる文章を紐付ける役目を持ちます。
MyColorIsIntent  my color is {Color}
MyColorIsIntent  my favorite color is {Color}
WhatsMyColorIntent whats my color
WhatsMyColorIntent what is my color
WhatsMyColorIntent say my color
WhatsMyColorIntent tell me my color
WhatsMyColorIntent whats my favorite color
WhatsMyColorIntent what is my favorite color
WhatsMyColorIntent say my favorite color
WhatsMyColorIntent tell me my favorite color
WhatsMyColorIntent tell me what my favorite color is
  • 「whats my color」と発音するとIntentRequestのintentNameにWhatsMyColorIntentが入りますので、以下のコードのように分岐することでintentに対応する応答を実現することができます。
  • ここでは対応するintentが無い場合は例外を発声させていますが、「再度言ってください」みたいな流れにしてもよさそうですね
        if ("MyColorIsIntent".equals(intentName)) {
            return setColorInSession(intent, session);
        } else if ("WhatsMyColorIntent".equals(intentName)) {
            return getColorFromSession(intent, session);
        } else {
            throw new SpeechletException("Invalid Intent");
        }
  • {Color}というのはSlotを表します。Intent SchemaのNameに対応しており、slotの型はDeveloperConsoleで設定が必要です。

Slot

  • LIST_OF_COLORSに以下のように定義するとUtteranceで{Color}となっている部分が取り得る値を定義することができます。
green
blue
purple
red
orange
yellow

Buildin Intent/Slots

SSML(Speech Synthesis Markup Language)

  • 読み方とかしゃべる間隔などをマークアップで記載することができます。(TwilioのTwiMLとは違うマークアップのようです)
  • 強く読んだり、アルファベットを読み上げたりS3に挙げたmp3を再生することも可能です。
  • マークアップの書き方はSpeech Synthesis Markup Language (SSML) Referenceをどうぞ。
  • 例えば、リストを読み上げるときに一定の間隔をあける場合は以下のように書くことができます。
        String repromptText = "Please choose a category by saying, " +
                "books <break time="0.2s" /> " +
                "fashion <break time="0.2s" /> " +
                "movie <break time="0.2s" /> " +
                "kitchen";
  • どんな感じでしゃべるのかな~というのはDeveloperConsoleのVoiceSimulatorで試すことができます。結構遊べます。

image

Cards

  • 「Alexa, open session」なんて話した後に、スマホのAlexaAppを開いてみます。
  • そうするとやりとりした内容が表示されていると思います。これをCardと言います。

alexaapp.jpg

  • Cardに表示させるにはこんな感じで書くことで表示されます。
        SimpleCard card = new SimpleCard();
        card.setTitle("Session");
        card.setContent("表示したいコンテンツ");
  • Cardは3種類あって、テキストのみ表示する場合はSimple、画像などのイメージを使って表示したい場合はStandard、既存サイトのアカウントとリンクするときのみ使えるのがLinkAccountとなっています。
  • さらに詳しく理解したい場合はIncluding a Card in Your Skill’s Responseを読みましょう。

その他

他は一旦紹介だけしておきます。

Link Account

  • OAuth2.0(RFC6749RFC6750)を使ってアカウントをリンクすることができます。
  • 簡単に流れを説明するとこんな感じです。
    • AlexaAppなどでスキルをEnableにする
    • LinkAccountをクリックすることで認証させたいサイトのログイン画面が開く
    • ユーザがアプリの画面などでID/PWを入力する。
    • 認証をクリアしscopeを承諾すると、AccessTokenが保管される。
    • ここまでがリンクの仕組みで以降のやりとりは音声と一緒にAccessTokenが流れてくるのでこれをLambdaで取得後、認可&リソースにアクセスすることで実現します。
  • 「Authorization code grant」と「Implicit grant」に対応しています。どちらの方式にするか検討が必要ですが、通常はよりセキュアな「Authorization code grant」を選択します。
  • 「Authorization code grant」はcodeを取得し、client_id,secretを使ってAccessTokenを取得する仕組みとなります。client_id,secretの値はあらかじめDeveloperConsoleで登録しておく必要があります。
  • また、tokenの有効期限切れの場合の為にrefresh_tokenの仕組みがOAuthには定義されていますが、こちらはAlexaのドキュメントによればOptionalとなっているので利用するか、tokenの有効期限が切れたら再認証させるかについては決めておく必要があります。
  • Linking an Alexa User with a User in Your System

Alexa BestPractice


おわりに

どうでしたか?結構簡単に作れることがわかると思います。
英語でもかなり色々遊べますので触ってみてはどうでしょうか。

次回はいつになるかわかりませんが、LinkAccountについて書こうかと思います。

続きを読む

Poweshellでsshチックなことをする

経緯

aws で、最近 iam role の付け替えが出来るようになりました。
早速 windows の iam role を起動したまま切り替えたのですが、その際に ssm が connection timeout になってしまいました。
Restart-Service Ec2ConfigOnline になるのですが、対象サーバがそれなりにあったので、もう少し楽にできないかと思い、 powershell を使うことにしました。

方法

winrm を使用すれば、 powershell でリモート接続しコマンドを叩ける。
ざっくりと二通りあったのでメモしておきます。

wirm でリモート接続するためには、 Enable-PSRemoting で有効化する必要があるので、詳しくは参考 URL を見ていただければと。。。

方法其ノ一

セッションをはる

New-PSSession -ComputerName <Public DNS or Private DNS>
※ __Public DNS__ , __Private DNS__ どちらでも行けました。検証済み

下記でリモート接続しているサーバに対してコマンドを叩く

Invoke-Command -Session $sess -ScriptBlock {ls;}

添付画像のように、変数でうけるのがベターっぽいです。

jump.png

補足

New-PSSession を使用すると、サインアウトしても session は残るので、 Remove-Session してあげましょう。

方法其ノ弐

New-PSSession だけではなく、 Enter-PSSession でも出来るようです。

Enter-PSSession -ComputerName <Public DNS or Private DNS>

jump 2.png

ほぼ見えてませんが、下記のようになってます。
多段sshしている気分になります

[Public DNS]: PS C:¥Users¥hoge¥Documents>

まとめ
RDP したくないから、ssm使ってるのになんでやねん!ってなりました。
ただ、一つ知見が増えて良かったです。

参考

http://hensa40.cutegirl.jp/archives/677

続きを読む

[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

iam-role.tf
## For EC2 instance Role
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

セキュリティグループの作成

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        }
    ]

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む

RDSをAnsibleで管理する

はじめに

AnsibleにはAWSのリソースを操作できるモジュールが豊富に用意されています。

今回は、RDSをAnsibleで管理してみます。
RDSインスタンスを作成するためには、

  • サブネットグループ
  • パラメータグループ

が必要になるので、一気通貫で作成できるようにします。

やること

  • サブネットグループ作成
  • パラメータグループ作成
  • RDSインスタンス作成

ポイント

サブネットグループを作成するためにサブネットIDが、RDSインスタンスを作成するためにセキュリティグループIDがそれぞれ必要となりますが、IDをAnsibleのYAMLに書きたくないので、それぞれ名前からIDを取得する実装とします。

前提

  • AWS関連のモジュール実行にはbotoが必要です。
  • credential情報は環境変数かaws configureでセットしてある必要があります。
  • ec2_group_factsが必要です。

下記リソースを前提に進めます。

  • VPC

    • AnsibleVPC
  • サブネット
    • private-a
    • private-c
  • セキュリティグループ
    • db_server

sample

以下のようなRDSインスタンスを作成します。

  • サブネットグループ

    • private

      • private-a(AZ-a)
      • private-c(AZ-c)
  • パラメータグループ
    • mysql57-sample
  • RDSインスタンス
    • sample-db

      • セキュリティグループ

        • db_server
      • サブネットグループ
        • private
      • パラメータグループ
        • mysql57-sample

ディレクトリ構成

ディレクトリ構成
site.yml
roles/
|--rds/
|  |--tasks/
|  |  |--main.yml
hosts/aws    #inventory
host_vars/
|--localhost.yml

inventory

AWSリソース関連モジュールはすべてlocalhostで実行するので、下記のようなインベントリファイルを用意します。

hosts/aws
[aws]
localhost

vars

こんな感じに変数を定義します。今回はhost_varsで定義しました。
RDSインスタンスのパスワードをそのまま記載していますが、実際はansible-vaultで暗号化したファイルなどに記載してください。

host_vars/localhost.yml
---
my_vars:
  aws:
    common:
      region: ap-northeast-1
    vpc:
      name: AnsibleVPC    # ターゲットのVPC名
    rds:
      subnet_group:
        - name: private
          description: 'private subnet group'
          subnets:
            - private-a #サブネット名
            - private-c #サブネット名
      param_group:
        - name: mysql57-sample
          description: 'MySql5.7 sample'
          engine: mysql5.7
          params:
            character_set_database: utf8
            character_set_server: utf8
      instance:
        - name: sample-db
          db_engine: MySQL
          engine_version: 5.7.16
          multi_zone: no
          zone: a
          size: 5
          instance_type: db.t2.micro
          parameter_group: mysql57-sample
          subnet_group: private
          security_group:
            - db-server #セキュリティグループ名
          username: mysql_admin
          password: mysql_admin
          tags:
            Role: sample-db

Role

まずVPCを特定するためにidが必要ですが、こちらと同様、VPC名でidを取得します。

後続タスクで必要となる、サブネットIDとセキュリティグループIDを取得し、それぞれ名前からIDを参照するためにディクショナリを生成します。

roles/vpc/tasks/main.yml
---
- name: vpc_id取得
  ec2_vpc_net_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      "tag:Name": "{{ my_vars.aws.vpc.name }}"
  register: vpc_net_fact

- name: subnet id取得
  ec2_vpc_subnet_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      "tag:Name": "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.subnet_group }}"
    - subnets
  register: subnet_fact
  when: my_vars.aws.rds.subnet_group is defined

- name: subnet dict作成
  set_fact:
    subnets_dict: >-
      {%- set dict = {} -%}
      {%- set inc = 0 -%}
      {%- set subnet_group_cnt = my_vars.aws.rds.subnet_group|length -%}
      {%- for i in range(subnet_group_cnt) -%}
      {%-   set list = [] -%}
      {%-   for j in range(my_vars.aws.rds.subnet_group[i].subnets|length) -%}
      {%-     set _ = list.append(subnet_fact.results[inc+j].subnets[0].id) -%}
      {%-   endfor -%}
      {%-   set _ = dict.update({my_vars.aws.rds.subnet_group[i].name: list}) -%}
      {%-   set inc = inc + subnet_group_cnt -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.subnet_group is defined

- name: securitygroup id取得
  ec2_group_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      group_name: "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.instance }}"
    - security_group
  register: sg_fact
  when: my_vars.aws.rds.instance is defined

- name: securitygroup dict作成
  set_fact:
    sg_dict: >-
      {%- set dict = {} -%}
      {%- for i in range(sg_fact.results|length) -%}
      {%-   set _ = dict.update({sg_fact.results[i].security_groups[0].group_name: sg_fact.results[i].security_groups[0].group_id}) -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.instance is defined

- name: RDS subnet-group作成
  rds_subnet_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    subnets: >-
      {%- set subnetname = item.name -%}
      {%- set list = subnets_dict[item.name] -%}
      {{ list }}
  with_items: "{{ my_vars.aws.rds.subnet_group }}"
  register: rds_subnet_group

- debug: var=rds_subnet_group

- name: RDS パラメータグループ作成
  rds_param_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    engine: "{{ item.engine }}"
    params: "{{ item.params }}"
  with_items: "{{ my_vars.aws.rds.param_group }}"
  register: rds_param_group
  when: my_vars.aws.rds.param_group is defined

- debug: var=rds_param_group

- name: RDS インスタンス作成
  rds:
    command: create
    instance_name: "{{ item.name }}"
    db_engine: "{{ item.db_engine }}"
    engine_version: "{{ item.engine_version }}"
    region: "{{ my_vars.aws.common.region }}"
    multi_zone: "{{ item.multi_zone }}"
    zone: "{{ my_vars.aws.common.region }}{{ item.zone }}"
    size: "{{ item.size }}"
    instance_type: "{{ item.instance_type }}"
    parameter_group: "{{ item.parameter_group }}"
    subnet: "{{ item.subnet_group }}"
    vpc_security_groups: >-
      {%- set list = [] -%}
      {%- for i in range(item.security_group|length) -%}
      {%-   set _ = list.append(sg_dict[item.security_group[i]]) -%}
      {%- endfor -%}
      {{ list }}
    username: "{{ item.username }}"
    password: "{{ item.password }}"
  with_items: "{{ my_vars.aws.rds.instance }}"
  register: rds_instance
  when: my_vars.aws.rds.instance is defined

- debug: var=rds_instance

site.yml

site.yml
---
- name: rds
  hosts: localhost
  connection: local
  roles:
    - role: rds

実行

Command
$ ansible-playbook -i hosts/aws -l localhost site.yml

注意

Ansibleのrdsモジュールにはなぜかストレージタイプを指定するためのオプションがなく、作成されたインスタンスのストレージはマグネティックになってしまいますので適宜変更してください。

まとめ

RDSは、関連リソースを合わせて作成する必要がありますが、Ansibleで自動化できると楽です。
また、今回のplaybookはインスタンスの変更には対応していません。

参考になれば幸いです。

参考

続きを読む

AWSIoT ラージデータアップロードパターン実装

AWSIoT ラージデータアップロードパターン実装

このBlackbeltセミナー の「ラージデータアップデートパターン」を実装しようと思ったら、未熟ゆえに結構色々詰まったので、やりかたをまとめときます。

image

AWSIoT Thing作成

このへんは普通に作るだけなので、適当に流します。

デバイス名はdevice00, 以下のようなPolicyを作成し、attachしておきます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": [
        "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:client/device00"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Publish",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/req"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topicfilter/device00/token/res"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Receive",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
    }
  ]
}

また、証明書のダウンロードもおこなっておきます。

IAMロールの設定

この後、LambdaでToken発行処理とデバイスからのS3へのアップロード処理を作成するのですが、
関数作成時に該当処理の実行権限をもつIAMロールが必要になるので、先に作っておきます。

S3アップロード処理ロール

デバイスに渡すSTSに許可する権限を付与したロールを”s3-role”として定義します。
ここでは、任意バケット以下にPutObjectのみをおこなえる権限を与えておきます。

実際には、バケット以下のデバイス名のフォルダにのみアップロード可能にする予定ですが、それは
STS発行時にロール権限を制限する形で加えるので、ここではバケット以下への書き込み権限を与えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::[YOUR_BACKET_NAME]/*"
            ]
        }
    ]
}

STS発行処理ロール

“s3-role”のロール権限を付与したSTSを作成するLambda関数に与えるロール権限を
“create-token”ロールとして作成します。

Lambda関数を実行するので、既存ポリシーのアタッチで”AWSLambdaBasicExecutionRole”を付与します。

また、作成したSTSをAWSIoTの機能でPublishするので、そのための権限も付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
            ]
        }
    ]
}

そして、STSにs3-roleの権限を付与するための権限も追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role"
        }
    ]
}

さらに、s3-role側でcreate-tokenロールを信頼しAssumeRoleを許可する旨の設定をおこなう必要があります。

s3-roleの設定画面で「信頼関係」タブを選択し、「信頼関係の編集」をおこなってエディタで
JSONの”Statement”要素の配列に以下を追加します。

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/create-token"
  },
  "Action": "sts:AssumeRole"
}

STS作成とデバイスへのトークン送信処理

STSを発行し、AWSIoT経由で指定のTopicにTokenをpublishするLambda処理を作成します。

今回はスライドの構成にしたがってTopic経由のpublishにしましたが、STSを使った通信が頻繁に
発生することが想定される場合は、Lambdaをスケジュール起動にしてDeviceShadowを用い定期的に
Pushで通知するほうがいいかもしれません。

import boto3
import json

def lambda_handler(event, context):

    sts = boto3.client('sts')
    iotData = boto3.client('iot-data')

    thingName=event['name']

    token = sts.assume_role(
        RoleArn="arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role",
        RoleSessionName=thingName,
        Policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "",
                    "Effect": "Allow",
                    "Action": [
                        "s3:PutObject"
                    ],
                    "Resource": [
                        "arn:aws:s3:::large-data-upload-pattern/"+thingName+"/*"
                    ]
                }
            ]
        })
    )

    iotData.publish(
        topic=thingName+'/token/res',
        qos=1,
        payload=json.dumps({
            "AccessKeyId": token['Credentials']['AccessKeyId'],
            "SecretAccessKey": token['Credentials']['SecretAccessKey'],
            "SessionToken": token['Credentials']['SessionToken']
        })
    )

eventオブジェクトのメンバー’name’からデバイス名を取得していますが、ここにデバイス名をセットするための
設定は次項でおこないます。

AWSIoTでSTS発行リクエストを受ける

AWSIoTで”*/token/req”のTopicにトークン発行リクエストがpublishされたら、上記のLambdaを起動する
Ruleを作成します。

“Rule query statement” が以下になるように設定し、ActionにはLambda起動を選択して先述の
関数を指定してください。

SELECT topic(1) AS name FROM '+/token/req'

ここまでの設定で、AWSIoTコンソールの”Test”から、”device00/token/req”にメッセージ(空でよい)を
送信するとTokenが”device00/token/res”にpublishされることが確認できます。

あとは動作確認です。

STSを取得してみる

下記サンプルスクリプトを実行すると、”device00/token/req”にトークン発行リクエストを送信し、
“device00/token/res”からトークンを取得して標準出力に表示します。

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import json
import time

def cb(client, userdata, message):
    print("got message")
    print(message.payload)

host="[IOT_ENDPOINT_DOMAIN]"
rootCAPath = "[ROOT_CA_FILE_PATH]"
certificatePath = "[CERTIFICATE_FILE_PATH]"
privateKeyPath = "[PRIVATE_KEY_FILE_PATH]"

myAWSIoTMQTTClient = None
myAWSIoTMQTTClient = AWSIoTMQTTClient("device00")
myAWSIoTMQTTClient.configureEndpoint(host, 8883)
myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)

myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)
myAWSIoTMQTTClient.configureDrainingFrequency(2)
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)

myAWSIoTMQTTClient.connect()
myAWSIoTMQTTClient.subscribe("device00/token/res", 1, cb)

time.sleep(2)

myAWSIoTMQTTClient.publish("device00/token/req", json.dumps({}), 1)

while True:
    time.sleep(5)

S3へのアップロード

取得したトークンを用いて、下記サンプルスクリプトで適当にS3にアップロードしてみます。

import boto3

AWS_S3_BUCKET_NAME = '[YOUR_BACKET_NAME]'

s3 = boto3.resource(
    's3',
    aws_access_key_id="[ACCESS_KEY]",
    aws_secret_access_key="[SECRET_KEY]",
    aws_session_token="[SESSION_TOKEN]"
)
bucket = s3.Bucket(AWS_S3_BUCKET_NAME)

obj = bucket.Object("device00/hogehoge")

response = obj.put(
    Body="hogehogehogehoge".encode('utf-8'),
    ContentEncoding='utf-8',
    ContentType='text/plane'
)

まとめ

まとめてみると大したことではないのですが、AssumeRoleとか権限まわりで色々詰まってよくわかんなくなるので大変でした。

続きを読む

[JAWS-UG CLI] CodeBuild: #5 プロジェクトの削除

前提条件

CodeBuildへの権限

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

AWS CLIのバージョン

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

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

結果(例):

  aws-cli/1.11.70 Python/2.7.12 Linux/4.4.11-23.53.amzn1.x86_64 botocore/1.5.33

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

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

0. 準備

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

変数の確認
cat << ETX

        AWS_DEFAULT_PROFILE: (0.1) ${AWS_DEFAULT_PROFILE}
        AWS_DEFAULT_REGION:  (0.2) ${AWS_DEFAULT_REGION}

ETX

結果(例):

  AWS_DEFAULT_PROFILE: (0.1) <CodeBuildにフル権限のあるプロファイル>
  AWS_DEFAULT_REGION:  (0.2) ap-northeast-1

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

0.1. プロファイルの指定

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

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

結果(例):

  iamFull-prjz-mbpr13
  <CodeBuildにフル権限のあるプロファイル>
変数の設定
export AWS_DEFAULT_PROFILE='<CodeBuildにフル権限のあるプロファイル>'

0.2. リージョンの指定

変数の設定
export AWS_DEFAULT_REGION='ap-northeast-1'

最終確認

変数の確認
cat << ETX

        AWS_DEFAULT_PROFILE: (0.1) ${AWS_DEFAULT_PROFILE}
        AWS_DEFAULT_REGION:  (0.2) ${AWS_DEFAULT_REGION}

ETX

結果(例):

  AWS_DEFAULT_PROFILE: (0.1) <CodeBuildにフル権限のあるプロファイル>
  AWS_DEFAULT_REGION:  (0.2) ap-northeast-1

1. 事前作業

1.1. プロジェクト名の指定

変数の設定
CODEB_PROJECT_NAME='codebuild-demo-java-20170417'
変数の設定
ARRAY_CODEB_PROJECT_NAMES="${CODEB_PROJECT_NAME}" 
        && echo ${ARRAY_CODEB_PROJECT_NAMES}
コマンド
aws codebuild batch-get-projects 
        --names "${ARRAY_CODEB_PROJECT_NAMES}"

結果(例):

  {
      "projectsNotFound": [],
      "projects": [
          {
              "name": "codebuild-demo-java-20170417",
              "serviceRole": "arn:aws:iam::XXXXXXXXXXXX:role/hoge",
              "tags": [],
              "artifacts": {
                  "namespaceType": "NONE",
                  "packaging": "NONE",
                  "type": "S3 ",
                  "location": "artifact-20170417-XXXXXXXXXXXX",
                  "name": "codebuild-demo-java-20170417"
              },
              "lastModified": 14xxxxxxxx.000,
              "timeoutInMinutes": 60,
              "created": 14xxxxxxxx.000,
              "environment": {
                  "computeType": "BUILD_GENERAL1_SMALL",
                  "image": "aws/codebuild/java:openjdk-8",
                  "type": "LINUX_CONTAINER",
                  "environmentVariables": []
              },
              "source": {
                  "type": "S3",
                  "location": "src-20170417-XXXXXXXXXXXX/MessageUtil.zip"
              },
              "encryptionKey": "arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:alias/aws/s3",
              "arn": "arn:aws:codebuild:ap-northeast-1:XXXXXXXXXXXX:project/codebuild-demo-java-20170417",
              "description": "codebuild demo (java)"
          }
      ]
  }

2. プロジェクトの削除

変数の確認
cat << ETX

        CODEB_PROJECT_NAME: ${CODEB_PROJECT_NAME}

ETX
コマンド
aws codebuild delete-project 
        --name ${CODEB_PROJECT_NAME}

結果(例):

  (戻り値なし)

3. 事後作業

同名のプロジェクトの不存在確認します。

コマンド
aws codebuild batch-get-projects 
        --names "${ARRAY_CODEB_PROJECT_NAMES}"

結果(例):

  {
    "projectsNotFound": [
      "codebuild-demo-java-20170417"
    ],
    "projects": []
  }

完了

続きを読む