DynamoDBの予約語一覧を無理やり動的に取得してみた

DynamoDBの予約語を対処するために一覧が欲しくなったのですが、いちいちプログラム内で定義したくなかったので、AWSの予約語一覧ページから一覧を取得するサービスを作りました。

前提

  1. AWSが公開しているDynamoDB予約語一覧のWebページ( http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html )をスクレイピングして予約語一覧を抽出します。
  2. 実装はAWS Lambda (Python3)、Webサービスとして動作させるためにAPI Gatewayを利用します。

結果

https://github.com/kojiisd/dynamodb-reserved-words

DynamoDB予約語一覧ページの確認

今回は以下のページをParseしたいと思います。

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ReservedWords.html

HTMLを見てみると、Parseしたい箇所はcodeタグで囲まれているようで、しかもこのページ内でcodeタグが出現するのは1度だけのようです。

スクリーンショット 2017-10-15 18.03.18.png

これであればすぐにパースできそうです。

HTMLの取得とParse

BeautifulSoupを利用すれば、簡単に実装ができます。BeautifulSoupのインストールは他のページでいくらでも紹介されているので、ここでは省略します。

import sys
import os


sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))
from bs4 import BeautifulSoup
import requests

URL = os.environ['TARGET_URL']

def run(event, context):

    response = requests.get(URL)
    soup = BeautifulSoup(response.content, "html.parser")

    result_tmp = soup.find("code")
    if result_tmp == None:
        return "No parsing target"

    result = result_tmp.string

    return result.split('n');

とりあえず申し訳程度にパースできない場合(AWSのページがcodeタグを使わなくなった時)を想定してハンドリングしています。

API Gatewayを定義してサービス化

コードが書けてしまえばAPI Gatewayを定義してサービス化するだけです。

設定は非常に単純なので割愛で。

スクリーンショット 2017-10-15 18.12.11_deco.png

アクセスしてみる

実際にアクセスしてみます。

$ curl -X GET https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod
["", "ABORT", "ABSOLUTE", "ACTION", "ADD", "AFTER", "AGENT", "AGGREGATE", "ALL", "ALLOCATE", "ALTER", "ANALYZE", "AND", "ANY", "ARCHIVE", "ARE", "ARRAY", "AS", "ASC", "ASCII", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", "ATOMIC", "ATTACH", "ATTRIBUTE", "AUTH", "AUTHORIZATION", "AUTHORIZE", "AUTO", "AVG", "BACK", "BACKUP", "BASE", "BATCH", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BIT", "BLOB", "BLOCK", "BOOLEAN", "BOTH", "BREADTH", "BUCKET", "BULK", "BY", "BYTE", "CALL", "CALLED", "CALLING", "CAPACITY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHECK", "CLASS", "CLOB", "CLOSE", "CLUSTER", "CLUSTERED", "CLUSTERING", "CLUSTERS", "COALESCE", "COLLATE", "COLLATION", "COLLECTION", "COLUMN", "COLUMNS", "COMBINE", "COMMENT", "COMMIT", "COMPACT", "COMPILE", "COMPRESS", "CONDITION", "CONFLICT", "CONNECT", "CONNECTION", "CONSISTENCY", "CONSISTENT", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", "CONSUMED", "CONTINUE", "CONVERT", "COPY", "CORRESPONDING", "COUNT", "COUNTER", "CREATE", "CROSS", "CUBE", "CURRENT", "CURSOR", "CYCLE", "DATA", "DATABASE", "DATE", "DATETIME", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DEFINE", "DEFINED", "DEFINITION", "DELETE", "DELIMITED", "DEPTH", "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETACH", "DETERMINISTIC", "DIAGNOSTICS", "DIRECTORIES", "DISABLE", "DISCONNECT", "DISTINCT", "DISTRIBUTE", "DO", "DOMAIN", "DOUBLE", "DROP", "DUMP", "DURATION", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "EMPTY", "ENABLE", "END", "EQUAL", "EQUALS", "ERROR", "ESCAPE", "ESCAPED", "EVAL", "EVALUATE", "EXCEEDED", "EXCEPT", "EXCEPTION", "EXCEPTIONS", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXPLAIN", "EXPLODE", "EXPORT", "EXPRESSION", "EXTENDED", "EXTERNAL", "EXTRACT", "FAIL", "FALSE", "FAMILY", "FETCH", "FIELDS", "FILE", "FILTER", "FILTERING", "FINAL", "FINISH", "FIRST", "FIXED", "FLATTERN", "FLOAT", "FOR", "FORCE", "FOREIGN", "FORMAT", "FORWARD", "FOUND", "FREE", "FROM", "FULL", "FUNCTION", "FUNCTIONS", "GENERAL", "GENERATE", "GET", "GLOB", "GLOBAL", "GO", "GOTO", "GRANT", "GREATER", "GROUP", "GROUPING", "HANDLER", "HASH", "HAVE", "HAVING", "HEAP", "HIDDEN", "HOLD", "HOUR", "IDENTIFIED", "IDENTITY", "IF", "IGNORE", "IMMEDIATE", "IMPORT", "IN", "INCLUDING", "INCLUSIVE", "INCREMENT", "INCREMENTAL", "INDEX", "INDEXED", "INDEXES", "INDICATOR", "INFINITE", "INITIALLY", "INLINE", "INNER", "INNTER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", "INSTEAD", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "INVALIDATE", "IS", "ISOLATION", "ITEM", "ITEMS", "ITERATE", "JOIN", "KEY", "KEYS", "LAG", "LANGUAGE", "LARGE", "LAST", "LATERAL", "LEAD", "LEADING", "LEAVE", "LEFT", "LENGTH", "LESS", "LEVEL", "LIKE", "LIMIT", "LIMITED", "LINES", "LIST", "LOAD", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATION", "LOCATOR", "LOCK", "LOCKS", "LOG", "LOGED", "LONG", "LOOP", "LOWER", "MAP", "MATCH", "MATERIALIZED", "MAX", "MAXLEN", "MEMBER", "MERGE", "METHOD", "METRICS", "MIN", "MINUS", "MINUTE", "MISSING", "MOD", "MODE", "MODIFIES", "MODIFY", "MODULE", "MONTH", "MULTI", "MULTISET", "NAME", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", "NULL", "NULLIF", "NUMBER", "NUMERIC", "OBJECT", "OF", "OFFLINE", "OFFSET", "OLD", "ON", "ONLINE", "ONLY", "OPAQUE", "OPEN", "OPERATOR", "OPTION", "OR", "ORDER", "ORDINALITY", "OTHER", "OTHERS", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "OVERRIDE", "OWNER", "PAD", "PARALLEL", "PARAMETER", "PARAMETERS", "PARTIAL", "PARTITION", "PARTITIONED", "PARTITIONS", "PATH", "PERCENT", "PERCENTILE", "PERMISSION", "PERMISSIONS", "PIPE", "PIPELINED", "PLAN", "POOL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVATE", "PRIVILEGES", "PROCEDURE", "PROCESSED", "PROJECT", "PROJECTION", "PROPERTY", "PROVISIONING", "PUBLIC", "PUT", "QUERY", "QUIT", "QUORUM", "RAISE", "RANDOM", "RANGE", "RANK", "RAW", "READ", "READS", "REAL", "REBUILD", "RECORD", "RECURSIVE", "REDUCE", "REF", "REFERENCE", "REFERENCES", "REFERENCING", "REGEXP", "REGION", "REINDEX", "RELATIVE", "RELEASE", "REMAINDER", "RENAME", "REPEAT", "REPLACE", "REQUEST", "RESET", "RESIGNAL", "RESOURCE", "RESPONSE", "RESTORE", "RESTRICT", "RESULT", "RETURN", "RETURNING", "RETURNS", "REVERSE", "REVOKE", "RIGHT", "ROLE", "ROLES", "ROLLBACK", "ROLLUP", "ROUTINE", "ROW", "ROWS", "RULE", "RULES", "SAMPLE", "SATISFIES", "SAVE", "SAVEPOINT", "SCAN", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SECTION", "SEGMENT", "SEGMENTS", "SELECT", "SELF", "SEMI", "SENSITIVE", "SEPARATE", "SEQUENCE", "SERIALIZABLE", "SESSION", "SET", "SETS", "SHARD", "SHARE", "SHARED", "SHORT", "SHOW", "SIGNAL", "SIMILAR", "SIZE", "SKEWED", "SMALLINT", "SNAPSHOT", "SOME", "SOURCE", "SPACE", "SPACES", "SPARSE", "SPECIFIC", "SPECIFICTYPE", "SPLIT", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "START", "STATE", "STATIC", "STATUS", "STORAGE", "STORE", "STORED", "STREAM", "STRING", "STRUCT", "STYLE", "SUB", "SUBMULTISET", "SUBPARTITION", "SUBSTRING", "SUBTYPE", "SUM", "SUPER", "SYMMETRIC", "SYNONYM", "SYSTEM", "TABLE", "TABLESAMPLE", "TEMP", "TEMPORARY", "TERMINATED", "TEXT", "THAN", "THEN", "THROUGHPUT", "TIME", "TIMESTAMP", "TIMEZONE", "TINYINT", "TO", "TOKEN", "TOTAL", "TOUCH", "TRAILING", "TRANSACTION", "TRANSFORM", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRUE", "TRUNCATE", "TTL", "TUPLE", "TYPE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNIT", "UNKNOWN", "UNLOGGED", "UNNEST", "UNPROCESSED", "UNSIGNED", "UNTIL", "UPDATE", "UPPER", "URL", "USAGE", "USE", "USER", "USERS", "USING", "UUID", "VACUUM", "VALUE", "VALUED", "VALUES", "VARCHAR", "VARIABLE", "VARIANCE", "VARINT", "VARYING", "VIEW", "VIEWS", "VIRTUAL", "VOID", "WAIT", "WHEN", "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WORK", "WRAPPED", "WRITE", "YEAR", "ZONE "]

うまくできました。

まとめ

思いつきで作成してみましたが、AWSのページが閉鎖されたりHTML構造が変更されない限り、仕様変更などは気にせずに使えそうです。

あとBeautifulSoup初めて使いましたが、かなり便利。

続きを読む

CloudFormationで変数の埋め込みはしたいが、IAMポリシ-変数を使いたい場合

例えばこんな感じにユーザ自身しかパスワード変更させないようにする場合を考えてみます。

Type: "AWS::IAM::ManagedPolicy"
Properties:
  ManagedPolicyName: IAMChangePassword
  Description: "パスワード変更"
  PolicyDocument:
    Version: "2012-10-17"
    Statement:
      -
        Effect: Allow
        Action:
          - iam:ChangePassword
        Resource: !Sub arn:aws:iam::${AWS::AccountId}:user/${aws:username}

このままCloudFormationで実行しようとすると、テンプレートの検証エラーが出てしまいます。

Template format error: Unresolved resource dependencies [aws:username] in the Resources block of the template

どうすればよいのかはStackOverFlowに書いてありました。

${!}を使って、以下のようにやるとうまくいきました。

Type: "AWS::IAM::ManagedPolicy"
Properties:
  ManagedPolicyName: IAMChangePassword
  Description: "パスワード変更"
  PolicyDocument:
    Version: "2012-10-17"
    Statement:
      -
        Effect: Allow
        Action:
          - iam:ChangePassword
        Resource: !Sub arn:aws:iam::${AWS::AccountId}:user/${!aws:username}

続きを読む

CloudFormation Tips for つまづきやすいところ

何この記事

CloudFormationでそれなりな規模の構成を作成することになったので、困って調べて分かったことをつらつらと書いていって後で見直せるようにした記事。なので自分用メモの色が強いです。が、たまたま記事を見て解決になったら良いかなともちょっと思います。
そんな性質の記事なのでふとした瞬間に増えたり減ったりします。

CloudFormationとは

AWSの公式ドキュメントかQiitaでCloudFormationタグを漁ってください

Tips

スタックを小分けにする際の1案

今はいくつか方法がありますが、1案として。

  • 共通のIDを決定し、Parametersに設定してスタック作成時に入力をする

    • 同じスクリプトを使って2つ以上の環境を作成する際、共通のIDを各インスタンスの名前に利用すると楽
  • 他のスタックで必要そうな情報をOutputsに記載してエクスポート
  • 他のスタックでエクスポートした情報はFn::ImportValue!Subを用いてインポート

VPC作成

Parameters:
  ProjectId:
    Description: "Project name id."
    Type: String
    AllowedPattern: '^[a-zA-Z0-9-/:-@[-`{-~]+$'
    ConstraintDescription: "InvalidValue[ProjectId]"

Resources:
  MyVPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: "default"
      Tags:
        - Key: "Name"
          Value: !Sub "${ProjectId}-vpc"

Outputs:
  MyVPC:
    Value: !Ref MyVPC
    Export:
      Name: !Sub "${ProjectId}-vpc"

SecurityGroup作成

Parameters:
  ProjectId:
    Description: "Project name id."
    Type: String
    AllowedPattern: '^[a-zA-Z0-9-/:-@[-`{-~]+$'
    ConstraintDescription: "InvalidValue[ProjectId]"

Resources:
  MySecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: {"Fn::ImportValue": !Sub "${ProjectId}-vpc"}
      GroupName: !Sub "${ProjectId}-sg"
      GroupDescription: "pass ssh from specific IP"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: "x.x.x.x/32"
      Tags:
        - Key: Name
          Value: !Sub "${ProjectId}-sg"

Parametersとの文字結合

Fn::Joinの方がネット上のノウハウ的に多いけど、せっかく新しいFn::Subができたんだし使っていきましょ

Fn::Sub
${Param}Paramパラメータの文字列が展開される。
複雑な文字結合の場合はListを作成すると楽。
1つ目に、生成する文字列
2つ目に、生成する文字列に対してマッピングする変数定義
(たぶん例を見た方が早い)


簡単な結合
MyParam=testparamと設定した状態でtestparam-instanceという文字列を作成

!Sub "${MyParam}-instance

複雑な結合
DBEngine=postgres DBVersion=9.6.3と設定した状態でpostgres9.6という文字列を作成

!Sub
  - ${DBEngine}${DBVersion1}.${DBVersion2}
  - DBEngine: !Ref DBEngine
    DBEngineVersion1: !Select [0, !Split [".", !Ref DBEngineVersion] ]
    DBEngineVersion2: !Select [1, !Split [".", !Ref DBEngineVersion] ]

Joinの場合より可読性も高い(と思う)

Policy作る時は基本的には管理ポリシーを作成

CFnに限った話ではないですが、インラインポリシーではなく管理ポリシーを作りましょうという話。

インラインポリシー : AWS::IAM::Policy
Role/User/Groupと1対1の関係。作成した後に別のエンティティにアタッチできない
管理ポリシー : AWS::IAM::ManagedPolicy
ポリシーは単体で存在し、複数のエンティティにアタッチできる

IAMRoleをEC2につけたい

InstanceProfileを作成してIAMRoleとEC2をつなげる必要がある。
AWSコンソールからはInstanceProfileを意識せずアタッチできるので気づきにくい。

(抜粋 parametersで指定して!Refで参照してるところもあります)

Resources:
  MyRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "my-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
               - "ec2.amazonaws.com"
            Action: "sts:AssumeRole"
      Path: "/"

  MyInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    DependsOn: MyRole
    Properties:
      InstanceProfileName: "my-instance-profile"
      Path: "/"
      Roles:
        - !Ref MyRole

  MyEC2:
    Type: "AWS::EC2::Instance"
    DependsOn: MyInstanceProfile
    Properties:
      ImageId: !Ref MyEC2AmiId
      IamInstanceProfile: !Ref MyInstanceProfile
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          PrivateIpAddress: "10.0.0.250"
          SubnetId: !Ref MySubnet
          GroupSet:
            - !Ref MySecurityGroup
      InstanceType: !Ref MyEC2Type
      KeyName: !Ref EC2KeyPair
      Tags:
        - Key: "Name"
          Value: "MyEC2"

RDSのセキュリティグループ

AWS::RDS::DBInstanceのPropertiesにセキュリティグループ関連のものは二つ

  • DBSecurityGroups
  • VPCSecurityGroups

ちなみにどちらも指定することはできない。

DBSecurityGroups
DB用に独自にSecurityGroupを作成して反映したい場合

VPCSecurityGroups
AWS::EC2::SecurityGroupをアタッチする場合
こちらの方が利用シーンは多そう

DBParameterGroup, DBSubnetGroupのName

  • AWS::RDS::DBParameterGroup
  • AWS::RDS::DBSubnetGroup

の名前が現時点では設定できなさそう・・・?
AWSのドキュメントにも名前を設定するプロパティはなさげ。
aws-cliで作成する場合は名前設定する用のプロパティあるのに。

ちなみに作成すると論理IDとランダム文字列が結合した名前が生成されて設定されます。

ELBのLogを保管する先のS3のBucketPolicy

これもCFnに限った話ではないですが。
ELBのログ出力を設定する場合(AccessLoggingPolicyプロパティにて設定)、S3にログファイルを吐くのは特定のAWSアカウントなので、それを許可するようS3のBucketyPolicyを設定する必要があります。
詳しくはこのあたり
AWS – Classic Load Balancer のアクセスログの有効化 – ステップ 2: S3 バケットにポリシーをアタッチする

(抜粋, ap-northeast-1リージョンの場合)

Resources:
  ELBLogS3:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: "hogehoge-server-log"

  ELBLogS3BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    DependsOn: ELBLogS3
    Properties:
      Bucket: !Ref ELBLogS3
      PolicyDocument:
        Version: "2012-10-17"
        Id: "ELBLogS3BucketPolicy"
        Statement:
          - Sid: "ELBLogWriteAccess"
            Action:
              - "s3:PutObject"
            Effect: "Allow"
            Resource: !Sub "arn:aws:s3:::${ELBLogS3}/AWSLogs/${AWS::AccountId}/*"
            Principal:
              AWS: "arn:aws:iam::582318560864:root"

以降、何かあったら追記予定

続きを読む

Raspi(デバイス)とAWSIoT(クラウド)間でMQTTを使ってメッセージを送受信する

はじめに

AWSのIoTのサービスといえばAWSIoTですよね!
つい先日大阪で行われてた「AWS Cloud Roadshow 2017」の中で、AWS Greengrassが東京リージョンに上陸したことが発表されました!
ということでですね、デバイスとクラウド間のやり取りの基本的な部分をAWSIoTを利用して実装していこうと思います

※前半部分にはsakura.io ✕ AWS IoTハンズオン [AWS編]の内容を投稿者の方の許可をいただいた上で、使わせていただいております

[0] 主な流れ

AWSIoTとラズパイ間の通信をMQTTでつなぎ、送受信する手順を紹介します
おおまかな流れはこんな感じです

  1. AWSIoTで証明書を発行する
  2. 発行した証明書を用いてデバイス(Raspi)とAWSIoTを接続する
  3. メッセージを送受信する
  4. (おまけ)Lambda経由でDynamoDBにデータをPUTする

[1] AWS IoTの設定(Thing)

AWS IoTは概念が少々複雑ですが、ある程度把握しておかないとこれ以降の設定内容が理解できないと思います。AWS Black Belt Online Seminar 2016 AWS IoTで学習すると良いでしょう。

  1. 証明書Thingポリシーを紐付ける
  2. 証明書をデバイスにインストールする
  3. 流れてきたトピックに対するアクション(連携先等)をルールで定義する

ポリシーの作成

AWS IoTコンソールを開く

スクリーンショット 2017-09-18 00.46.54.png

リージョンが「Asia Pacific (Tokyo)」でない場合は変更する

4bd6b0f2-3593-3b10-1839-62f3c2f13173.png

[Get started]をクリックする(下図は表示されない場合もある)

スクリーンショット 2017-09-11 17.23.40.png

ポリシーを作成する

スクリーンショット 2017-09-18 01.26.53.png

下表の通り入力して[Create]をクリックする

項目 入力内容
Name AllPubSub
Action iot:*
Resource ARN *
Effect Allow

スクリーンショット 2017-09-18 01.12.18.png

Thingと証明書の作成

Thingを作成する

スクリーンショット 2017-09-18 00.49.51.png

device01を入力して[Create thing]をクリックする

awsiot_test_6.PNG

作成したThingをクリックする

awsiot_test_7.PNG

証明書を作成する

スクリーンショット 2017-09-18 00.55.14.png

クライアント証明書/クライアント秘密鍵/ルート証明書の3種類をダウンロードする(ルート証明書のみ、右クリック -> リンク先を別名で保存)

[Activate](押し忘れ注意!) -> [Attach a policy]の順にクリックする

スクリーンショット 2017-09-18 00.57.16.png

[AllPubSub]を選択して[Done]をクリックする

スクリーンショット 2017-09-18 01.18.49.png

戻る

スクリーンショット 2017-09-18 01.24.11.png

エンドポイントを確認する

歯車マークをクリックするとエンドポイントが表示されるので控えておく

スクリーンショット 2017-09-18 01.38.48.png

[2] RaspiとAWSIoTを接続する

前章で発行した証明書を使って、RaspiとAWSIoTを接続します

使用したRaspiの情報は以下のとおり

  • Raspbian GNU/Linux 8.0 (jessie)
  • Linux raspberrypi 4.4.34

MQTTの実装

MQTTの実装には「mosquitto」を利用します。下記コマンドを実行することでAWSIoTとの通信を行います

  • Pub mosquitto_pubコマンドで光量データを送信
  • Sub mosquitto_subコマンドで光量データを受信

mosquittoのインストール

Raspberry Piのデフォルトリポジトリのmosquitto-clientsはバージョンが古く、TLSのエラーが発生するため最新のmosquitto-clientsをインストールします

$ wget  http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
$ sudo apt-key add mosquitto-repo.gpg.key
$ wget http://repo.mosquitto.org/debian/mosquitto-wheezy.list
$ sudo cp mosquitto-wheezy.list /etc/apt/sources.list.d/

$ sudo apt-get update
$ sudo reboot

$ sudo apt-get install  mosquitto-clients
$ mosquitto_sub --help |grep version
 mosquitto_sub version 1.4.14 running on libmosquitto 1.4.14.

2017/09/25時点で最新のバージョンは1.4.14です

先ほど発行した証明書(クライアント証明書/クライアント秘密鍵/ルート証明書の3種類)をRasberry Piにコピーします
※この際コピーミスを防ぐために、SFTPなどで確実にコピーするようにしてください。scpでホームディレクトリにファイルを置く例

$ scp file-name pi@IP_ADDRESS:~

AWSIoT MQTTクライアントを開く

[Test]を選択する

awsiot_menu.PNG

[Subscription topic]に「$aws/things/device01/test」と入力する

awsiot_test_1.PNG

[Subscribe to topic]をクリックする

awsiot_test_2.PNG

下記のような画面になると思います

awsiot_test_3.PNG

ラズパイからAWSIoTへのメッセージ送信(publish)

例)トピック「$aws/things/device01/test」に対して文字列「test message」を送信します

$ mosquitto_pub --cafile [ルート証明書] --cert [クライアント証明書] --key [クライアント秘密鍵] -h [AWSIoTのエンドポイント] -p 8883 -q 1 -t '$aws/things/device01/test' -i device01 --tls-version tlsv1.2 -m '{"test": "message"}' -d

例)トピック「$aws/things/device01/test」に対してJSONファイル「data_sample.json」を送信します

data_sample.json
{
  "data": [
    {
      "d1": "0000",
      "d2": "9999",
      "d3": "0003",
      "d4": "0004",
      "d5": "0005",
      "d6": "0006",
      "d7": "0007",
      "d8": "0008",
      "d9": "0009",
      "d10": "0010",
      "serial": "00000001",
      "time": "201706010100"
    }
  ]
}
$ mosquitto_pub --cafile [ルート証明書] --cert [クライアント証明書] --key [クライアント秘密鍵] -h [AWSIoTのエンドポイント] -p 8883 -q 1 -t '$aws/things/device01/test' -i device01 --tls-version tlsv1.2 -f data_sample.json -d

問題なく送信が完了すれば、先ほど開いたテスト画面にデバイスからのメッセージが表示されます
↓こんな感じ(data_sample.jsonを送信した際の結果)

awsiot_test_5.PNG

AWSIoTからのメッセージ受信(subscribe)

例)トピック「$aws/things/device01/test」に対するメッセージを待ち受けます

$ mosquitto_sub --cafile [ルート証明書] --cert [クライアント証明書] --key [クライアント秘密鍵] -h [AWSIoTのエンドポイント] -p 8883 -q 1 -t '$aws/things/device01/test' -i device01 --tls-version tlsv1.2

上記コマンドでsubscribeしている状態で、テスト画面からメッセージをデバイスへ送信します

awsiot_test_4.PNG

問題なければデバイス側のターミナルに、こんなメッセージが表示されます

{
  "message": "Hello from AWS IoT console"
}

Device shadowを更新する

AWSIoTにはすでに予約されているトピックがあり「$aws/things/device01/shadow/update」に対してメッセージを送信するとshadowを更新することができます

shadow_sample.json
{
  "state":
    {
      "desired":
        {
          "status_data": [
            {
              "status1": "1101",
              "status2": "1202",
              "status3": "1303",
              "status4": "1404",
              "status5": "1005",
              "status6": "1006"
            }
          ]
        }
    }
}
$ mosquitto_pub --cafile [ルート証明書] --cert [クライアント証明書] --key [クライアント秘密鍵] -h [AWSIoTのエンドポイント] -p 8883 -q 1 -t '$aws/things/device01/shadow/update' -i device01 --tls-version tlsv1.2 -f shadow_sample.json -d

[3]Lambda経由でDynamoDBにデータをPUTする(おまけ)

前章でデバイスからAWSIoTにメッセージを送信することができるようになりましたので、デバイスからJSONのデータを送信し、それをDynamoDBに格納してみます

簡単な流れとしては
1. AWSIoTのルールを設定し、特定のトピックにメッセージが送信されたらLambdaをキックする
2. Lambdaが受け取ったデータを加工しDynamoDBにPUTする

送信するデータは前章で作成した「data_sample.json」を利用します

Lambdaの作成

関数名「IoT-Lambda-DynamoDB」というLambda関数を作成していきます
今回はRuntimeをNode.js6.10でLambdaを作成しています

Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": "dynamodb:PutItem",
            "Resource": "arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxx:table/iot-data-lambda"
        }
    ]
}

※DynamoDBにPUTする部分のARNはご利用されているAWSのアカウントIDに書き換えてください

Code(Node.js)

const AWS = require("aws-sdk");
const dynamoDB = new AWS.DynamoDB.DocumentClient({
    region: "ap-northeast-1"
});

exports.handler = (event, context, callback) => {
    const item = event["data"][0];
    item["deviceID"] = event["deviceID"];
    const nowTime = Date.now();
    item["time"] = String(nowTime);

    const params = {
        TableName: "iot-data-lambda",
        Item: item
    };

    dynamoDB.put(params).promise();
};

DynamoDBの作成

LambdaがPUTするDynamoDBを作成します

DynamoDBの情報

項目 入力内容
TableName iot-data-lambda
Primary partition key deviceID (String)
Primary sort key time (String)

AWSIoTルールの作成

  1. AWSIoTコンソール画面左メニューからルール画面に移動し、画面右上の「create」をクリックしてルール作成画面に移動する
  2. 「Name」に任意の文字列を入力する
  3. 「Attribute」「Topic filter」に下記の内容を入力する
項目 入力内容
Attribute *,topic(3) AS deviceID
Topic filter $aws/things/+/data

「topic(3) AS deviceID」と書いておくことで、トピックの3番目をdeviceIDとしてデータの中に含ませて次のActionに渡します
今回の場合はトピックの「+」に入る文字列が、Lambdaに渡されるeventの中にdeviceIDが含まれるようになります

awsiot_test_8.PNG

4. [Set one or more actions]の「Add action」をクリックし、Lambdaを選択、画面下部の[Configure action]をクリック

awsiot_test_9.PNG

awsiot_test_10.PNG

5. 先ほど作成したLambda関数を選択し[Add action]をクリック

awsiot_test_11.PNG

6. 最後に[Create rule]をクリック

これで「IoT_Lambda_DynamoDB」というルールが作成されました
デバイスからトピック「$aws/things/+/data」(+にはdeviceIDとなる文字列を入れる)にメッセージが送信されたときに、Lambdaにデータが渡され、加工してDynamoDBに格納します

実際に実行してみる

デバイスからデータを送信する

$ mosquitto_pub --cafile [ルート証明書] --cert [クライアント証明書] --key [クライアント秘密鍵] -h [AWSIoTのエンドポイント] -p 8883 -q 1 -t '$aws/things/device01/data' -i device01 --tls-version tlsv1.2 -f data_sample.json -d

問題なく処理が実行されればDynamoDBにデータがPUTされる

awsiot_test_12.PNG

さいごに

AWSIoTで発行した証明書ベースでメッセージの送受信ができるようになりました
今回は証明書の発行、デバイスへの証明書のコピーなどすべて手作業で行いましたが、デバイスが複数台になってくるとこんなことしれいられないので、このあたりを自動化するのが必要になってくるなと実感しましたね
そこらへんの自動化を実現してくれたすばらしいお方から資料をいただいたので、試していこうと思います
もし許可をいただいたら記事にまとめようと思いますー

ではまた!

続きを読む

AutoScaling環境においてGitを使わないでEC2を冗長化する方法

制約

Git禁止(協力会社がFTPしか使えないため)

結論

  1. Master Serverを建てる
  2. Master Serverのファイルの更新をlsyncdで監視し、常にS3と同期する
  3. 任意のタイミングでSlave Server(ELB配下の全EC2インスタンス)をS3と同期する

イメージ図

だいたいこんな感じ

AutoScaling環境においてGitを使わないでEC2を冗長化する方法

Master Server編

1. S3バケットを作成する

公式サイトに詳しい方法が書いてあります。
S3 バケットを作成する方法 – Amazon Simple Storage Service

2. AWS CLIを設定する

公式サイトに詳しい方法が書いてあります。
AWS CLI の設定 – AWS Command Line Interface

3. lsyncdをインストールする

EPELリポジトリを利用してインストールします。

$ sudo yum install --enablerepo=epel lsyncd

4. lsyncdの設定を変更する

普通の書き方だとファイル名の末尾に/(スラッシュ)が混入してエラーが出たりうまくsyncしてくれなかったので、
下記のような感じでパースしてあげる必要があります。
komeda-shinjiさんの記事がとても参考になりました。ありがとうございます。

$ sudo vim /etc/lsyncd.conf
/etc/lsyncd.conf

source_dir = "/var/www/source"
s3bucket = "files.hoge.com"
prefix = "source"

snscmd = "echo !!! error "

settings {
    logfile    = "/var/log/lsyncd.log",
    statusFile = "/var/log/lsyncd.status",
    nodaemon = false,
    statusInterval = 1,
    delay = 5,
}

cp = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end
  local s3cmd = "aws s3 cp '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"


  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

rm = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end

  local s3cmd = "aws s3 rm '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"
  local runcmd = "rc=0 && [ ! -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

mv = function(event)
  local src_path = event.o.targetPathname
  local dst_path = event.d.targetPathname

  if (string.sub(event.o.target, -1, -1) == "/") then
      src_path = string.sub(event.o.target, 1, -2) .. event.o.pathname
  end
  if (string.sub(event.d.target, -1, -1) == "/") then
      dst_path = string.sub(event.d.target, 1, -2) .. event.d.pathname
  end
  local s3cmd = "aws s3 mv '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"

  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .." || :"
  spawnShell(event, runcmd)
end

s3sync = {
    maxProcesses = 1,
    onCreate = cp,
    onModify = cp,
    onDelete = rm,
--  onMove = mv,
}

sync {
    s3sync,
    source = source_dir,
    target = "s3://" .. s3bucket .. "/" .. prefix,
}

5. lsyncdを起動する

$ sudo /etc/rc.d/init.d/lsyncd start
$ sudo chkconfig lsyncd on
$ /etc/rc.d/init.d/lsyncd status
lsyncd (pid  12345) is running...

6. Sourceを変更する

$ vim /var/www/source/hoge.txt
/var/www/source/hoge.txt
hogehoge

7. Sourceの変更がS3に反映しているか確認する

$ tailf /var/log/lsyncd.log

Slave Server編

1. AWS CLIを設定する

公式サイトに詳しい方法が書いてあります。
AWS CLI の設定 – AWS Command Line Interface

2. S3のSourceをローカルに同期するシェルスクリプトを作成する

$ vim /usr/local/bin/s3sync.sh
/usr/local/bin/s3sync.sh
#!/bin/sh
SOURCE_DIR="/var/www"
S3_BUCKET="files.hoge.com"
PREFIX="source"

TARGET_MASTER="s3://${S3_BUCKET}/${PREFIX}/"
TARGET_LOCAL="${SOURCE_DIR}/${PREFIX}/"

aws s3 sync ${TARGET_MASTER} ${TARGET_LOCAL} --delete

3. 同期スクリプトをEC2インスタンス起動時に実行するように設定する

$ vim /etc/rc.local
/etc/rc.local
s3sync.sh
exit 0

※ちなみに、rc.localに書かれたスクリプトはroot権限で実行されます。

4. 任意のタイミングで同期スクリプトを実行する

 cronするなり、execするなり

ボツ案

Master Server+rsyncパターン

s3Syncパターン

s3fsパターン

NFSパターン

AMIを最新にしてから起動するパターン

続きを読む

RaspberryPiに接続したセンサの情報をCloud Watchでモニタリングする

構想

RaspberryPiから送信したセンサの情報をAWSIoTを使って、DynamoDBに格納する。
格納した情報をLambdaを使ってCloudWatchに流し、モニタリングする。

AWS IoTにモノを登録する

まずはAWSIoTに接続するモノを登録する。

接続をクリック

1.png

デバイスの設定の今すぐ始めるをクリック

2.png

手順を確認して今すぐ始めるをクリック

3.png

接続するモノの環境を選択

今回はRaspberryPiを使用するのでOSはLinux、言語はPythonを選択する。

4.png

モノの名前を設定する

自分の好きな名前で良い。

5.png

接続キットをダウンロードする

今回登録したモノ専用の証明書などが同梱されているので、取扱に気をつける。

6.png

画面のコマンドを実行する

以前の記事を参考にRaspberryPiのPython環境を整えてから、画面に表示されたコマンドを実行してみる。

7.png

$ ./start.sh

2017-09-10 14:30:53,709 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 2 in the TCP stack.
2017-09-10 14:30:53,710 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 2 succeeded.
Received a new message: 
b'New Message 0'
from topic: 
sdk/test/Python
--------------


2017-09-10 14:30:54,713 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 3 in the TCP stack.
2017-09-10 14:30:54,714 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 3 succeeded.
Received a new message: 
b'New Message 1'
from topic: 
sdk/test/Python
--------------


2017-09-10 14:30:55,717 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 4 in the TCP stack.
2017-09-10 14:30:55,718 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 4 succeeded.
Received a new message: 
b'New Message 2'
from topic: 
sdk/test/Python
--------------

上記のようなメッセージが流れ出したら成功。

送信内容を変更する

上記で実行したサンプルプログラムでは、永遠とNew Message Xというメッセージを送り続けているだけなので、これを温度/湿度センサ(DHT11)の出力に変える。

まずは先程ダウンロードしたconnect_device_packageの直下にhttps://github.com/szazo/DHT11_Python.gitをクローンする。

そして、./start.shで呼び出されているbasicPubSub.pyの中身を改変する。

connect_device_package/aws-iot-device-sdk-python/samples/basicPubSub/basicPubSub.py

'''
/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
 '''

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from DHT11_Python import dht11
import RPi.GPIO as GPIO
import sys
import logging
import time
import argparse
import datetime
import json


# Custom MQTT message callback
def customCallback(client, userdata, message):
    print("Received a new message: ")
    print(message.payload)
    print("from topic: ")
    print(message.topic)
    print("--------------\n\n")

# Read in command-line parameters
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--endpoint", action="store", required=True, dest="host", help="Your AWS IoT custom endpoint")
parser.add_argument("-r", "--rootCA", action="store", required=True, dest="rootCAPath", help="Root CA file path")
parser.add_argument("-c", "--cert", action="store", dest="certificatePath", help="Certificate file path")
parser.add_argument("-k", "--key", action="store", dest="privateKeyPath", help="Private key file path")
parser.add_argument("-w", "--websocket", action="store_true", dest="useWebsocket", default=False,
                    help="Use MQTT over WebSocket")
parser.add_argument("-id", "--clientId", action="store", dest="clientId", default="basicPubSub", help="Targeted client id")
parser.add_argument("-t", "--topic", action="store", dest="topic", default="sdk/test/Python", help="Targeted topic")

args = parser.parse_args()
host = args.host
rootCAPath = args.rootCAPath
certificatePath = args.certificatePath
privateKeyPath = args.privateKeyPath
useWebsocket = args.useWebsocket
clientId = args.clientId
topic = args.topic

if args.useWebsocket and args.certificatePath and args.privateKeyPath:
    parser.error("X.509 cert authentication and WebSocket are mutual exclusive. Please pick one.")
    exit(2)

if not args.useWebsocket and (not args.certificatePath or not args.privateKeyPath):
    parser.error("Missing credentials for authentication.")
    exit(2)

# Configure logging
logger = logging.getLogger("AWSIoTPythonSDK.core")
logger.setLevel(logging.DEBUG)
streamHandler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)

# Init AWSIoTMQTTClient
myAWSIoTMQTTClient = None
if useWebsocket:
    myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId, useWebsocket=True)
    myAWSIoTMQTTClient.configureEndpoint(host, 443)
    myAWSIoTMQTTClient.configureCredentials(rootCAPath)
else:
    myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId)
    myAWSIoTMQTTClient.configureEndpoint(host, 8883)
    myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)

# AWSIoTMQTTClient connection configuration
myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)  # Infinite offline Publish queueing
myAWSIoTMQTTClient.configureDrainingFrequency(2)  # Draining: 2 Hz
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)  # 10 sec
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)  # 5 sec

# Connect and subscribe to AWS IoT
myAWSIoTMQTTClient.connect()
myAWSIoTMQTTClient.subscribe(topic, 1, customCallback)
time.sleep(2)

# initialize GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# read data using pin 2
instance = dht11.DHT11(pin=2)

while True:
    result = instance.read()
    if result.is_valid():
        data = {'timestamp': str(datetime.datetime.now()),
                'clientId': clientId,
                'temperature': result.temperature,
                'humidity': result.humidity
                }
        myAWSIoTMQTTClient.publish(topic, json.dumps(data), 1)
        time.sleep(1)

DHT11の結果をディクショナリ形式で格納し、myAWSIoTMQTTClient.publish(topic, json.dumps(data), 1)json.dumpsしてパブリッシュする。

また、start.shも次の設定で扱いやすいように、
basicPubSub.pyの呼び出しオプションを追加する。

start.sh
# stop script on error
set -e

# Check to see if root CA file exists, download if not
if [ ! -f ./root-CA.crt ]; then
  printf "\nDownloading AWS IoT Root CA certificate from Symantec...\n"
  curl https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem > root-CA.crt
fi

# install AWS Device SDK for Python if not already installed
if [ ! -d ./aws-iot-device-sdk-python ]; then
  printf "\nInstalling AWS SDK...\n"
  git clone https://github.com/aws/aws-iot-device-sdk-python.git
  pushd aws-iot-device-sdk-python
  python setup.py install
  popd
fi

# run pub/sub sample app using certificates downloaded in package
printf "\nRunning pub/sub sample application...\n"
python aws-iot-device-sdk-python/samples/basicPubSub/basicPubSub.py -e a33hl2ob9z80a1.iot.us-west-2.amazonaws.com -r root-CA.crt -c RaspberryPi01.cert.pem -k RaspberryPi01.private.key -t dht11

追加したのは-t dht11という部分で、
これはトピックの名前を設定する部分である。

この名前を設定しておくことで、受信したメッセージがどのトピックのものか判別がつき、
AWSIoT側で受信後の処理を振り分けることができる。

AWS IAMで必要な権限を持ったロールを作成しておく

本来は機能毎にロールを分けて必要以上に権限を持たせないのが良いが、
面倒くさいので今回は一つのロールに全て持たせる。
(どの権限が必要になるかわからなかったので、多めにアタッチした。
あとで勉強しておく必要あり。)

  • CloudWatchFullAccess
  • AmazonDynamoDBFullAccess
  • AmazonDynamoDBFullAccesswithDataPipeline
  • AWSLambdaDynamoDBExecutionRole
  • AWSLambdaInvocation-DynamoDB

AWSIoTでルールを設定する

ルールをクリック

8.png

右上の作成ボタンをクリック

9.png

名前と説明を設定する

10.png

どのメッセージにルールを適用するかを設定する

属性は*、トピックフィルターは先程、basicPubSub.pyの呼び出しに追加した-tオプションの値を入力する。

こうすることで-tオプションで指定した名前と一致したもののみにこのルールを適用することができる。

11.png

メッセージに対してどのような処理を行うか決める。

今回はまず、AWS IoTで受信したメッセージをDynamoDBに送り込みたいので、そのように設定する。

12.png

13.png

新しいリソースを作成するをクリック。

14.png

DHT11のデータを格納するテーブルを作成する

15.png

16.png

こんな感じに設定して、テーブルを作成する。

テーブルを選択する

もとの画面を戻りテーブルを選択する。

17.png

ハッシュキーの値とレンジキーの値が空欄になっているので、
そこには上記のように${timestamp}などを指定する。

なお、メッセージとして送信されるjsonの各メンバーには${member}という形でアクセスできる。

ロールを指定する

18.png

ここには前もって用意しておいたロールを指定する。

19.png

これでアクションが追加できたので、ルールを作成するをクリックしてルール作成は完了。

確認

実行してみて、DynamoDBにデータが入るか確認する。

$ ./start.sh
 .
 .
 .

2017-09-09 23:32:23,411 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 1394 in the TCP stack.
2017-09-09 23:32:23,412 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 1394 succeeded.
Received a new message: 
b'{"timestamp": "2017-09-09 23:32:23.409885", "clientId": "basicPubSub", "temperature": 27, "humidity": 70}'
from topic: 
dht11
--------------


2017-09-09 23:32:25,715 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 1395 in the TCP stack.
2017-09-09 23:32:25,716 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 1395 succeeded.
Received a new message: 
b'{"timestamp": "2017-09-09 23:32:25.714083", "clientId": "basicPubSub", "temperature": 27, "humidity": 70}'
from topic: 
dht11
--------------

20.png

うまくいってるようだ。
NとかSとかは、NumberStringの略だそう。
最初NorthSouthかと思ってなんのこっちゃってなったけど。

DynamoDBからCloudWatchにデータを流す

トリガーの作成

長いけどあと一息。
DynamoDBのテーブルの画面から、トリガー => トリガーの作成 => 新規関数と選択。

21.png

22.png

トリガーの設定

ステップ1でdynamodb-process-streamが選択された状態で、ステップ2から始まるが、ステップ1に一旦戻る。

23.png

今回はdynamodb-process-streamのPython3版を選択した。

24.png

28.png

テーブルを指定し、開始位置は水平トリムにする。

25.png

トリガーの有効化は後でするので、チェックを入れずに次へ。

関数を定義する

26.png

適当に基本情報を埋める。

27.png

上図のように関数を定義できるので、以下のコードに変更する。

from __future__ import print_function

import json
import boto3
from decimal import Decimal

print('Loading function')


def lambda_handler(event, context):
    # print("Received event: " + json.dumps(event, indent=2))
    client = boto3.client('cloudwatch') 
    for record in event['Records']:
        # print(record['eventID'])
        # print(record['eventName'])
        # print("DynamoDB Record: " + json.dumps(record['dynamodb'], indent=2))
        print(record['dynamodb']['NewImage'])
        print(record['dynamodb']['NewImage']['timestamp']['S'])
        response = client.put_metric_data(
            Namespace='dht11',
            MetricData=[
                {
                    'MetricName': 'temperature',
                    'Dimensions': [
                        {
                            'Name': 'clientId',
                            'Value': record['dynamodb']['NewImage']['payload']['M']['clientId']['S'],
                        },
                    ],
                    'Value': Decimal(record['dynamodb']['NewImage']['payload']['M']['temperature']['N']),
                    'Unit': 'None'
                },
            ]
        ) 
        response = client.put_metric_data(
            Namespace='dht11',
            MetricData=[
                {
                    'MetricName': 'humidity',
                    'Dimensions': [
                        {
                            'Name': 'clientId',
                            'Value': record['dynamodb']['NewImage']['payload']['M']['clientId']['S'],
                        },
                    ],
                    'Value': Decimal(record['dynamodb']['NewImage']['payload']['M']['humidity']['N']),
                    'Unit': 'Percent'
                },
            ]
        )

編集したら、確認をして関数を保存する。

トリガーの有効化

DynamoDBのトリガーの画面に戻ると、作成した関数が出現している。
作成した関数を選択肢してトリガーの編集ボタンをクリック。

29.png

トリガーを選択してトリガーを有効化する。

30.png

CloudWatchでモニタリングする

あとはCloudWatch側で表示の設定をすれば、温度や湿度の推移が見えるようになる。
ダッシュボード => ダッシュボードの作成でダッシュボードを作成した後、
ウィジェットの追加からtemperaturehumidityを検索する。

Lambdaで作成されたメトリクスが出てくるので、それを選択すると・・・

31.png

無事、確認できる。

まとめ

  • AWSIoT => DynamoDB => Lambda => CloudWatchの連携で、DHT11センサの値をモニタリングすることができた。

続きを読む

Ubuntu Server 16.04 LTS(HVM) 起動のCloudFormationテンプレート

EC2起動の時に選択肢に表示されるUbuntu起動のテンプレートです.
CloudFormationのメタデータ取得や変更検知、状態通知などを行うヘルパースクリプトのインストールに手間取ったのであげておきます.
pipでインストールしてるので長くなっちゃってますが、wgetでファイル取って来る方法も取れそうです.

参考:

AWSTemplateFormatVersion: '2010-09-09'
Description: CFn template for ubuntu
Parameters:
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t2.nano
    AllowedValues:
      - t2.nano 
    ConstraintDescription: must be a valid EC2 instance type.
  KeyName: 
    Description: Name of an existing Amazon EC2 key pair for SSH access
    Type: AWS::EC2::KeyPair::KeyName
  SSHLocation:
    Description: The IP address range that can be used to SSH to the EC2 instances
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.  
Mappings:
  AWSRegion2AMI:
    ap-northeast-1: 
      ubuntu: ami-ea4eae8c
Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Simple example for cfn-init
      AWS::CloudFormation::Init:
        config:
          packages:
            apt:
              httpd: [] 
          files:
            /etc/cfn/cfn-hup.conf:  # CFn側の変更検知のための設定
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:  # 自動で更新を実行するための設定
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.DeepLeargingServerInstance.Metadata.AWS::CloudFormation::Init
                action=/usr/local/bin/cfn-init -s ${AWS::StackId} -r ServerInstance --region ${AWS::Region}
                runas=root
          services:
            sysvinit:
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegion2AMI, !Ref 'AWS::Region', ubuntu]      
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'ServerSecurityGroup'
      KeyName: !Ref 'KeyName'
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe
          apt-get update
          # python 2.7, pipインストール
          apt-get -y install build-essential
          apt-get -y install python-dev python-pip
          # aws-cloudformation-bootstrapのインストール
          pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
          # cfn-hupの設定
          cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
          chmod u+x /etc/init.d/cfn-hup
          update-rc.d cfn-hup defaults  # シンボリックリンク作成
          service cfn-hup start
          # メタデータからのファイルとパッケージのロード
          /usr/local/bin/cfn-init -v --stack ${AWS::StackName} --resource ServerInstance --region ${AWS::Region}
          # cfn-init から取得したの状態の通知
          /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ServerInstance --region ${AWS::Region}
    CreationPolicy:
      ResourceSignal:
        Timeout: PT10M
  ServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 22
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref 'SSHLocation'
Outputs:
  WebsiteURL:
    Description: Application URL(Not Work, Just Sample)
    Value: !Sub 'http://${ServerInstance.PublicDnsName}'

続きを読む

CloudFormationウォークスルーにYAMLで入門してみる

AWSリソースの構築を自動化できるCloud Formationについて学んで見ます.

まずは既存のVPC内にSG切ってインスタンス立てて…ぐらいができればいいんだけど…と思っていたのですが、

ウォークスルー: スタックの更新

がある程度まとまってたので、とりあえず走らせて見て、何が起きてるかを学んでくことにしようと思います.

下準備: JSON => YAMLへの変換

このウォークスルーではスタック(CloudFormationで構築されるひとかたまり)を作成したのち、それを更新していく…というチュートリアルですが、一番上のセクション「単純なアプリケーション」に下地となるテンプレートが記載されているのでそれをコピーして進める…

前にこれをJSONからYAMLへ変換します (JSONしんどいため&コメントつけられるようにするため)

AWS CloudFormation Template FlipというツールがAWSから公開されているので、pipでインストールします.

pip install cfn-flip

次に下地となる、「単純なアプリケーション」セクションの一番下にある、長ーいJSONをwalkthrough01.jsonなど適当な名前でローカルに保存して、下記のコマンドを実行します.

cfn-flip -c walkthrough01.json walkthrough01.yaml

するとYAMLへ変換されたファイルが生成されます.

この際、通常のJSON => YAMLの変換だけでなく、組み込み関数宣言部Fn::が短縮形!に変換されます. (3文字減るだけですが…ドキュメントをでは短縮形が併記されています)
また、-c, --cleanオプションをつけると、Fn::Join からFn::Subへと書き換えてくれます. 文字列を連結する際[]で括らなくても良くなるようで、見やすかったのでこちらで進めていきます.

(最初は関数って何してんのと思ったのですが、ユーザーの入力受け取ったり文字列を結合したりできる、といった感じなのでひとまずそのまま進めれば大丈夫です. )

生成されたテンプレートはこんな感じです(長いですねー…)

walkthrough01.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS CloudFormation Sample Template: Sample template that can be used
  to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
  will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: m1.small
    AllowedValues:
      - t1.micro
      - t2.micro
      - t2.small
      - t2.medium
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - g2.2xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - hi1.4xlarge
      - hs1.8xlarge
      - cr1.8xlarge
      - cc2.8xlarge
      - cg1.4xlarge
    ConstraintDescription: must be a valid EC2 instance type.
Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
    t2.micro:
      Arch: HVM64
    t2.small:
      Arch: HVM64
    t2.medium:
      Arch: HVM64
    m1.small:
      Arch: PV64
    m1.medium:
      Arch: PV64
    m1.large:
      Arch: PV64
    m1.xlarge:
      Arch: PV64
    m2.xlarge:
      Arch: PV64
    m2.2xlarge:
      Arch: PV64
    m2.4xlarge:
      Arch: PV64
    m3.medium:
      Arch: HVM64
    m3.large:
      Arch: HVM64
    m3.xlarge:
      Arch: HVM64
    m3.2xlarge:
      Arch: HVM64
    c1.medium:
      Arch: PV64
    c1.xlarge:
      Arch: PV64
    c3.large:
      Arch: HVM64
    c3.xlarge:
      Arch: HVM64
    c3.2xlarge:
      Arch: HVM64
    c3.4xlarge:
      Arch: HVM64
    c3.8xlarge:
      Arch: HVM64
    g2.2xlarge:
      Arch: HVMG2
    r3.large:
      Arch: HVM64
    r3.xlarge:
      Arch: HVM64
    r3.2xlarge:
      Arch: HVM64
    r3.4xlarge:
      Arch: HVM64
    r3.8xlarge:
      Arch: HVM64
    i2.xlarge:
      Arch: HVM64
    i2.2xlarge:
      Arch: HVM64
    i2.4xlarge:
      Arch: HVM64
    i2.8xlarge:
      Arch: HVM64
    hi1.4xlarge:
      Arch: HVM64
    hs1.8xlarge:
      Arch: HVM64
    cr1.8xlarge:
      Arch: HVM64
    cc2.8xlarge:
      Arch: HVM64
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
    us-west-2:
      PV64: ami-af86c69f
      HVM64: ami-8786c6b7
      HVMG2: ami-47296a77
    us-west-1:
      PV64: ami-c7a8a182
      HVM64: ami-cfa8a18a
      HVMG2: ami-331b1376
    eu-west-1:
      PV64: ami-aa8f28dd
      HVM64: ami-748e2903
      HVMG2: ami-00913777
    ap-southeast-1:
      PV64: ami-20e1c572
      HVM64: ami-d6e1c584
      HVMG2: ami-fabe9aa8
    ap-northeast-1:
      PV64: ami-21072820
      HVM64: ami-35072834
      HVMG2: ami-5dd1ff5c
    ap-southeast-2:
      PV64: ami-8b4724b1
      HVM64: ami-fd4724c7
      HVMG2: ami-e98ae9d3
    sa-east-1:
      PV64: ami-9d6cc680
      HVM64: ami-956cc688
      HVMG2: NOT_SUPPORTED
    cn-north-1:
      PV64: ami-a857c591
      HVM64: ami-ac57c595
      HVMG2: NOT_SUPPORTED
    eu-central-1:
      PV64: ami-a03503bd
      HVM64: ami-b43503a9
      HVMG2: ami-b03503ad
Resources:
  WebServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
              php: []
          files:
            /var/www/html/index.php:
              content: '<?php

                echo ''<h1>AWS CloudFormation sample PHP application</h1>'';

                ?>

                '
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:
              content: !Sub '[main]

                stack=${AWS::StackId}

                region=${AWS::Region}

                '
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub '[cfn-auto-reloader-hook]

                triggers=post.update

                path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init

                action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance  --region     ${AWS::Region}

                runas=root

                '
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      UserData: !Base64
        Fn::Sub: '#!/bin/bash -xe

          yum install -y aws-cfn-bootstrap

          # Install the files and packages from the metadata

          /opt/aws/bin/cfn-init -v          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          # Start up the cfn-hup daemon to listen for changes to the Web Server metadata

          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

          # Signal the status from cfn-init

          /opt/aws/bin/cfn-signal -e $?          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          '
    CreationPolicy:
      ResourceSignal:
        Timeout: PT5M
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 80
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
Outputs:
  WebsiteURL:
    Description: Application URL
    Value: !Sub 'http://${WebServerInstance.PublicDnsName}'

起動

ともあれ、生成されたテンプレートを元に「初期スタックの作成」を参考にしてCloudFormationをマネジメントコンソールから起動して見ます.
するとデフォルトVPC内にインスタンスが立ち上がっていることが確認できるかと思います.
CloudFormationコンソールの[Output]タブに表示されたアドレスへアクセスすると、AWS CloudFormation sample PHP applicationというメッセージが表示されているかと思います.

テンプレートを読む

一旦起動し終わったところで、どんな構造になっているか読んでいきたいと思います.
長いようでやってることはシンプルなテンプレートなので、そんに項目数は多くないかと思います.

YAMLの最上位には

  • AWSTemplateFormatVersion
  • Description
  • Parameters
  • Mappings
  • Resources
  • Outputs

が並んでいました. それぞれ、

項目 内容
AWSTemplateFormatVersion テンプレートフォーマットのバージョン、基本固定?
Description テンプレートの説明文
Parameters CloudFormation実行時にユーザーが与えるパラメーター
Mappings 与えらた条件に応じて変更される値の定義
Resources 起動されるAWSリソースの設定
Outputs 起動完了後に出力されるメッセージ

といった感じです. Version, Description, Outputは簡単そうなので脇に置いて、残りについて見ていきたいと思います.

Parameters

実行するユーザーが与えるパラメータです.
テンプレートでは下記の通り.

Parameters:
  InstanceType:                               # 項目名
    Description: WebServer EC2 instance type  # 説明
    Type: String                              # 入力される値の型
    Default: m1.small                         # デフォルト値
    AllowedValues:                            # 受け付ける値
      - t1.micro
      # ...
      # 中略
      # ...
    ConstraintDescription: must be ...        # 入力値違反時のエラーメッセージ

これが反映されるとCloudFormation起動画面ではこんな感じになります.

Screen Shot 2017-09-07 at 11.42.15.png

Prameters 以下にセレクトボックスができているのがわかります.
AllowedValueの他にも、AllowedPatternで正規表現で入力値を規定することができます(SSHアクセスを許可するIPアドレスレンジを入力させたいときなど)

Mappings

CloudFormationが実行される条件に応じて、この次のResourcesを変更したいときに利用するための値のマッピングです.

Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
      # ...
      # 中略
      # ...
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
      # ...
      # 後略
      # ...

今回はParameterでユーザーから与えられたインスタンスタイプに応じて仮想化の方式を決定したり、実行されたリージョン(+インスタンスタイプ)に応じて選択するAMIを決定しています.

(ParameterとMappingがこのサンプルテンプレートを長くしちゃってる原因、ですね…)

Resources

CloudFormationの本体部分になる、起動されるリソースの設定です.

Resources:
  WebServerInstance:
  # ...
  # 中略
  # ...
  WebServerSecurityGroup:
  # ...
  # 後略
  # ...

と今回はEC2インスタンスとセキュリティグループの2つになります.
ここではEC2の設定について、読んでいこうと思います.
(VPCとサブネットは指定しないとデフォルトが選択される模様)

EC2

ここが一番重たい…気がします.

  WebServerInstance:          # 項目名
    Type: AWS::EC2::Instance    # リソースタイプ
    Metadata:                   # 追加コメントの記載や起動スクリプトの定義や
      # ...
      # 中略
      # ...
    Properties:                 # EC2自身の起動パラメータ
      # ...
      # 中略
      # ...
    CreationPolicy:             # CloudFormation側とのやりとりの設定
      # ...
      # 後略
      # ...

と大きく分けて4つ、要素があります.
Typeはそのままであり、CreationPolicyはこのテンプレートではシンプルなので、ここではMetadataとPropatiesの2つについて見ていきます.

Metadata
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:  # 起動スクリプトの設定
        config:
          packages:  # 利用するパッケージ、今回はyumでapacheとphpをインストール
            yum:
              httpd: []
              php: []
          files:     # 設置する設定ファイル類
            /var/www/html/index.php:  # デモ用phpファイル
              content:  # 略
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:  # Stackを通じてEC2のアップデートをするための設定
              content: !Sub '[main]  # !Subで文字列の結合

                stack=${AWS::StackId}  # Stack IDの取得

                region=${AWS::Region}  # リージョンの取得

                '
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:  # 同じくアップデートのための設定
              content:  # 略
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf

ドキュメントにあるように、実装の詳細をコメントとして含めたり、AWS::CloudFormation::Initを利用した起動スクリプトの設定ができる箇所になります。

AWS::CloudFormation::Initではyumなどによるインストール、設定ファイルの配置、起動するサービスの選択ができます. 今回はApacheと、CloudFormationスタックを通してEC2の方に変更を反映するためのエージェント(cfn-hup)の設定をしている形になります. ウォークスルーを進めると早速index.phpファイルを変更しスタックをアップデートするのですが、その際CloudFormation側からインスタンス内のindex.phpファイルを触りに行くのにこれを通じて行う、ようです.

参考: cfn-hup

また!Subという組み込み関数を用いて文字列の連結を行なっています.

Properties

EC2本体の設定値やスクリプトの実行を行ないます.

    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]  # AMIのID取得、Mappingsで設定した値を利用
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      UserData: !Base64  # スクリプト実行
        Fn::Sub: '#!/bin/bash -xe

          yum install -y aws-cfn-bootstrap

          # Metadataからのファイルとパッケージのインストール

          /opt/aws/bin/cfn-init -v          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          # cfn-hup daemon の起動とStackの変更のリッスン開始

          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

          # インスタンスの状態をCloudFormation側へ通知

          /opt/aws/bin/cfn-signal -e $?          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          '

!RefでParameterで指定された値を受け取り、!FindInMapでMappingで設定した値を取得し、EC2の設定パラメータを指定しています. (こうすることでどのリージョンでも起動可能)

また、UserDataにCloudFormation起動時に実行するスクリプトを渡しています. Metadataで記載したファイルやパッケージもここで読み込まれ、インストールが実行されます.

(UserData側に全てインストール項目記載しても良さそうですが、後からスタックを通しての変更ができない、テンプレートの見通しが悪くなる等々デメリットがあるのだと思います.

(追記)改行など整えたもの

自動変換の際、改行を表現するのに1行あける、ということがされていましたが、YAMLでは|が置かれた後の属性は改行を評価する、ということになっているそうです.
これを用いるともう少しスッキリ書けます

リソースのプロパティの追加のSSHによるアクセス追加の部分を加えてあります.

AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS CloudFormation Sample Template: Sample template that can be used
  to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
  will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: m1.small
    AllowedValues:
      - t1.micro
      - t2.micro
      - t2.small
      - t2.medium
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - g2.2xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - hi1.4xlarge
      - hs1.8xlarge
      - cr1.8xlarge
      - cc2.8xlarge
      - cg1.4xlarge
    ConstraintDescription: must be a valid EC2 instance type.
  KeyName: 
    Description: Name of an existing Amazon EC2 key pair for SSH access
    Type: AWS::EC2::KeyPair::KeyName
  SSHLocation:
    Description: The IP address range that can be used to SSH to the EC2 instances
    Type: String
    MinLength: 9
    MaxLength: 18
    Default: 192.168.0.1/32
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.  
Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
    t2.micro:
      Arch: HVM64
    t2.small:
      Arch: HVM64
    t2.medium:
      Arch: HVM64
    m1.small:
      Arch: PV64
    m1.medium:
      Arch: PV64
    m1.large:
      Arch: PV64
    m1.xlarge:
      Arch: PV64
    m2.xlarge:
      Arch: PV64
    m2.2xlarge:
      Arch: PV64
    m2.4xlarge:
      Arch: PV64
    m3.medium:
      Arch: HVM64
    m3.large:
      Arch: HVM64
    m3.xlarge:
      Arch: HVM64
    m3.2xlarge:
      Arch: HVM64
    c1.medium:
      Arch: PV64
    c1.xlarge:
      Arch: PV64
    c3.large:
      Arch: HVM64
    c3.xlarge:
      Arch: HVM64
    c3.2xlarge:
      Arch: HVM64
    c3.4xlarge:
      Arch: HVM64
    c3.8xlarge:
      Arch: HVM64
    g2.2xlarge:
      Arch: HVMG2
    r3.large:
      Arch: HVM64
    r3.xlarge:
      Arch: HVM64
    r3.2xlarge:
      Arch: HVM64
    r3.4xlarge:
      Arch: HVM64
    r3.8xlarge:
      Arch: HVM64
    i2.xlarge:
      Arch: HVM64
    i2.2xlarge:
      Arch: HVM64
    i2.4xlarge:
      Arch: HVM64
    i2.8xlarge:
      Arch: HVM64
    hi1.4xlarge:
      Arch: HVM64
    hs1.8xlarge:
      Arch: HVM64
    cr1.8xlarge:
      Arch: HVM64
    cc2.8xlarge:
      Arch: HVM64
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
    us-west-2:
      PV64: ami-af86c69f
      HVM64: ami-8786c6b7
      HVMG2: ami-47296a77
    us-west-1:
      PV64: ami-c7a8a182
      HVM64: ami-cfa8a18a
      HVMG2: ami-331b1376
    eu-west-1:
      PV64: ami-aa8f28dd
      HVM64: ami-748e2903
      HVMG2: ami-00913777
    ap-southeast-1:
      PV64: ami-20e1c572
      HVM64: ami-d6e1c584
      HVMG2: ami-fabe9aa8
    ap-northeast-1:
      PV64: ami-21072820
      HVM64: ami-35072834
      HVMG2: ami-5dd1ff5c
    ap-southeast-2:
      PV64: ami-8b4724b1
      HVM64: ami-fd4724c7
      HVMG2: ami-e98ae9d3
    sa-east-1:
      PV64: ami-9d6cc680
      HVM64: ami-956cc688
      HVMG2: NOT_SUPPORTED
    cn-north-1:
      PV64: ami-a857c591
      HVM64: ami-ac57c595
      HVMG2: NOT_SUPPORTED
    eu-central-1:
      PV64: ami-a03503bd
      HVM64: ami-b43503a9
      HVMG2: ami-b03503ad
Resources:
  WebServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
              php: []
          files:
            /var/www/html/index.php:
              content: |
                <?php
                echo '<h1>AWS CloudFormation sample PHP application</h1>';
                echo 'Updated virsions via UpdateStack';
                ?>
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance --region ${AWS::Region}
                runas=root
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      KeyName: !Ref 'KeyName'
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe
          yum install -y aws-cfn-bootstrap
          # Install the files and packages from the metadata
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
          # Start up the cfn-hup daemon to listen for changes to the Web Server metadata
          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''
          # Signal the status from cfn-init
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
    CreationPolicy:
      ResourceSignal:
        Timeout: PT5M
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 80
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref 'SSHLocation'
Outputs:
  WebsiteURL:
    Description: Application URL
    Value: !Sub 'http://${WebServerInstance.PublicDnsName}'

phpのところで、''で括られていた箇所を'に直していますが、これもYAMLの規定で、文字列中でシングルクォーテーションのエスケープには2つ連ねる、というのを変換のスクリプトが利用していたからでした.


なかなか手が出せなかったCloudFormationだったのですが、ウォークスルーの一番最初の部分について、ドキュメント読み読み進めて見ました.

ウォークスルーでいきなりこの長さはちょっとしんどいですね…

続きを読む

Terraformでインフラのコード化を体験してみよう ~ その④SNSを作ってみる

前提知識

SNSとは・・?

Amazon Simple Notification Service (SNS) は柔軟なフルマネージド型の pub/sub メッセージング/モバイル通知サービスで、登録中のエンドポイントやクライアントに配信するメッセージを調整できます。SNS を使用すると、分散システムやサービス、モバイルデバイスなど、多数の受信者にメッセージをファンアウトすることができます。 任意の規模のすべてのエンドポイントで通知をセットアップ、操作、および確実に送信できます。 AWS マネジメントコンソール、AWS コマンドラインインターフェイス、または AWS SDK のわずか 3 つのシンプルな API を使用して、ほんの数分で SNS を使い始めることができます。 SNS を使用することにより、専用メッセージングソフトウェアやインフラストラクチャの管理や運用に関連した複雑さやオーバーヘッドが排除されます。

AWSのSNS解説部より引用

端的に2つのサービスがあります。

  • 1. pub/subメッセージング

    • 特定の通知先を登録しとくと、SNSに通知が来たときにそこへ通知する機能
  • 2. モバイル通知
    • SNSに通知が来たときに、iosやAndroid等のモバイル端末へPush通知出来る機能

どんな時に使われるのか

例えば、ユーザから問い合わせが来た時に、SNSに通知するようにしていて、
その通知先としてEmailを選択しておけば自分のメールアドレスに通知する。

とかも可能です。

いきなり通知すれば良いじゃん?

と思うかもしれませんが、SNSを挟んでおくと、Subscriptionといって通知先を複数登録することが出来るので、例えば急に「やっぱメールも良いんだけど、その場でDBに登録もしてくんね?」等の要望が出てきた場合、SNSなら通知先をLambaやSQSにすることも可能なので、そこへ通知することで、メール通知と同時に登録等も簡単に出来ます。

つまり、イベント起きた後にxxする部分を抽象化出来る訳ですので、
使う側(SNSに通知を送る人)としては、通知先と通知方法さえ知っとけば、その先に何が行われるか知ってる必要も無ければ、要件が変わったときにこちら側を修正する必要もない。

早速SNSのTopicを作ってみる

作り方は、これも公式みれば書いてあるので参考にしてみます。

snsのtopicを作る時にはnameだけが必須で、他はオプションとなってます。
policyの設定方法は、これも公式を参照すると分かりやすいです。

さらに、sns_topic_subscriptionで、SNSの通知先も設定出来ます。

サポートされてるプロトコルを見るとわかりますが、ここでは、email, email-json, smsは未サポートでした。
(これらをsubscriptionにしたい場合は、コンソール上から設定する必要があります)

なので今回は、SQSをsubscriptionに設定したSNS Topicを作ります。

# SNS
resource "aws_sns_topic" "test_sns" {
  name = "test_sns_topic"
}

# SQS
resource "aws_sqs_queue" "test-sqs" {
  name     = "test_sqs"
}

# 全ての権限のpolicyを与える
resource "aws_sqs_queue_policy" "sqs_queue_polycy" {
  queue_url = "${aws_sqs_queue.test-sqs.id}"

  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Id": "sqspolicy",
  "Statement": [
    {
      "Sid": "First",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "sqs:*",
      "Resource": "${aws_sqs_queue.test-sqs.arn}"
    }
  ]
}
POLICY
}

# SNSのsubscriptionにSQSを登録
resource "aws_sns_topic_subscription" "sub_to_sqs" {
  topic_arn = "${aws_sns_topic.test_sns.arn}"
  protocol  = "sqs"
  endpoint  = "${aws_sqs_queue.test-sqs.arn}"
}```

簡単にやるとこんな感じですね。

## 実際にSNSに通知して、SQSにキューが貯まるかチェック

$ aws sns publish –target-arn “arn:aws:sns:ap-northeast-1:138710906407:test_sns_topic” –message “hello world”
{
“MessageId”: “39995d19-8000-5e5d-ab10-0f3eb8c4dabc”
}
“`

スクリーンショット 2017-09-04 20.19.28.png

良い感じです。
このように、SNSやSQSもterraformを使えば簡単に作成や設定が可能です。

続きを読む

AnsibleでAWS操作 Certificate Manager編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編
  5. Simple Email Service編
  6. Certificate Manager編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • ACMにて独自ドメインへのSSL証明書を発行
  • SSL証明書をCloudFrontに設定
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み
  • 独自ドメイン取得済み
  • SESの設定済み
  • CFとS3の設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. SSL証明書発行リクエストの作成

command
ansible-playbook -i inventory/production create-aws-acm-request.yml

戻り値の値を控えます。

2. SESに保存された認証確認メールから認証用URLを取得し、ブラウザからapproveする

command
ansible-playbook -i inventory/production view-aws-acm-notice-mail-url.yml

戻り値のURLにブラウザからアクセスする必要あり

3. CloudFrontの設定を更新して、httpsでアクセス出来るようにする

command
ansible-playbook -i inventory/production update-aws-cf.yml


## ディレクトリ構成
```text

├── ansible.cfg
├── create-aws-acm-request.yml
├── templates
│   └── production
│       └── cf
│           └── update.j2
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── create-aws-acm-request
│   │   └── tasks
│   │       └── main.yml
│   ├── update-aws-cf
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-acm-notice-mail-url
│       └── tasks
│           └── main.yml
├── update-aws-cf.yml
├── view-aws-acm-notice-mail-url.yml
└── vars
    └── all.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
AWS:
  ACM:
    ARN: ${ACMリクエストのARN}
    REGION: us-west-2
    VERIFICATION_MAIL_OBJECT: ${認証メールオブジェクト名}
  CF:
    DISTRIBUTION:
      ID: ${ディストリビューションID}
      ORIGIN_ID: ${オリジンID}
      E_TAG: ${Eタグ}
  S3:
    BUCKET:
      NAME: ${バケット名}
    END_POINT: ${S3バケットの静的ホスティングエンドポイント}
DOMAIN:
  MAIN:
    NAME: ${ドメイン名}
  SUB:
    NAME: ${サブドメイン名}

templates

production/cf/update.j2
{
  "DistributionConfig": {
    "Comment": "Https Bucket.",
    "CacheBehaviors": {
      "Quantity": 0
    },
    "IsIPV6Enabled": true,
    "Logging": {
      "Bucket": "",
      "Prefix": "",
      "Enabled": false,
      "IncludeCookies": false
    },
    "WebACLId": "",
    "Origins": {
      "Items": [
        {
          "OriginPath": "",
          "CustomOriginConfig": {
            "OriginSslProtocols": {
              "Items": [
                "TLSv1",
                "TLSv1.1",
                "TLSv1.2"
              ],
              "Quantity": 3
            },
            "OriginProtocolPolicy": "http-only",
            "OriginReadTimeout": 30,
            "HTTPPort": 80,
            "HTTPSPort": 443,
            "OriginKeepaliveTimeout": 5
          },
          "CustomHeaders": {
            "Quantity": 0
          },
          "Id": "{{ AWS.CF.ID }}",
          "DomainName": "{{ AWS.S3.END_POINT }}"
        }
      ],
      "Quantity": 1
    },
    "DefaultRootObject": "index.html",
    "PriceClass": "PriceClass_All",
    "Enabled": true,
    "DefaultCacheBehavior": {
      "TrustedSigners": {
        "Enabled": false,
        "Quantity": 0
      },
      "LambdaFunctionAssociations": {
        "Quantity": 0
      },
      "TargetOriginId": "{{ AWS.CF.ID }}",
      "ViewerProtocolPolicy": "redirect-to-https",
      "ForwardedValues": {
        "Headers": {
          "Quantity": 0
        },
        "Cookies": {
          "Forward": "all"
        },
        "QueryStringCacheKeys": {
          "Quantity": 0
        },
        "QueryString": true
      },
      "MaxTTL": 604800,
      "SmoothStreaming": false,
      "DefaultTTL": 604800,
      "AllowedMethods": {
        "Items": [
          "HEAD",
          "GET"
        ],
        "CachedMethods": {
          "Items": [
            "HEAD",
            "GET"
          ],
          "Quantity": 2
        },
        "Quantity": 2
      },
      "MinTTL": 604800,
      "Compress": false
    },
    "CallerReference": "2017-07-29_06-25-01",
    "ViewerCertificate": {
      "SSLSupportMethod": "sni-only",
      "ACMCertificateArn": "{{ AWS.ACM.ARN }}",
      "MinimumProtocolVersion": "TLSv1",
      "Certificate": "{{ AWS.ACM.ARN }}",
      "CertificateSource": "acm"
    },
    "CustomErrorResponses": {
      "Quantity": 0
    },
    "HttpVersion": "http2",
    "Restrictions": {
      "GeoRestriction": {
        "RestrictionType": "none",
        "Quantity": 0
      }
    },
    "Aliases": {
      "Items": [
        "{{ DOMAIN.MAIN.NAME }}"
      ],
      "Quantity": 1
    }
  },
  "Id": "{{ AWS.CF.DISTRIBUTION.ID }}",
  "IfMatch": "{{ AWS.CF.DISTRIBUTION.E_TAG }}"
}

playbook

create-aws-acm-request.yml
- hosts: cliservers
  roles:
    - create-aws-acm-request
  vars_files:
    - vars/all.yml
update-aws-cf.yml
- hosts: cliservers
  roles:
    - update-aws-cf
  vars_files:
    - vars/all.yml
view-aws-acm-notice-mail-url.yml
- hosts: cliservers
  roles:
    - view-aws-acm-notice-mail-url
  vars_files:
    - vars/all.yml

tasks

role/create-aws-acm-request/tasks/main.yml
- name: "Create Certificate"
  shell: |
    aws acm request-certificate 
    --domain-name {{ DOMAIN.MAIN.NAME }} 
    --subject-alternative-names {{ DOMAIN.SUB.NAME }} 
    --domain-validation-options DomainName={{ DOMAIN.SUB.NAME }},ValidationDomain={{ DOMAIN.MAIN.NAME }} 
    --region={{ AWS.ACM.REGION }} 
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-acm-notice-mail-url/tasks/main.yml
- name: Download Verification Mail Object
  shell: |
    aws s3 cp 
    s3://${AWS.S3.BUCKET.NAME}/{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }} 
    {{ TEMP.DIRECTORY }}.{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

- name: View URL
  shell: |
    grep https {{ TEMP.DIRECTORY }}.{{ AWS.ACM.VERIFICATION_MAIL_OBJECT }} | grep context
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/update-aws-cf/tasks/main.yml
- name: Create Json
  template: 
    src={{ ENV }}/cf/update.j2
    dest={{ TEMP.DIRECTORY }}/update.json
  tags:
    - always

- name: Update Distribution
  shell: |
    aws cloudfront update-distribution 
    --cli-input-json file://files/{{ ENV }}/cf/update.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

これで今までhttpでアクセスしてたS3バケット(CloudFront経由)にアクセスすると、httpsにリダイレクトされた上で正常に表示されると思います。

AWS+CloudFrount+ACM+S3を使えばほぼ無料でSSL証明証の発行や紐付けが出来、自動更新の設定や複数ドメインのSSL証明書をまとめて管理することが出来ます。

自分は以下のようなWordpressプラグインとAWSサービスで静的サイトを運用していますが、今のところ問題なく運用出来ています。

Wordpress+staticpress+staticpresss3+S3+CloudFront+ACM

Wordpressを静的サイト化することで一部機能が制限されてしまいますが、セキュリティ観点がかなり向上します。

また、制限される部分も特に不要な機能だと思いますし、JavaScript等である程度保管することも可能なので動的サイトを切り捨てて静的サイトにするメリットは十分あると思いますので、興味のある方は是非お試し下さい♪

じゃあの。

続きを読む