S3 → Lambda → SQS にメッセージを投げるまでの構築手順!

やりたいこと

タイトルの通り、「Lamda」から「Amazon SQS」に「メッセージを送信」すること。

All.jpg

具体的には、こんな感じ。

処理フロー

  1. S3にファイルをアップロードする。( User )
  2. ファイルアップロードをトリガーにAmazon SQSにメッセージを送信する。( Lambda )
  3. Amazon SQS内のキューにメッセージを格納する。( Amazon SQS )
  4. 5分間隔で、Amazon SQSのキューのメッセージ個数をログ出力する。( Lambda )

実装する前に、ちょっとだけAmazon SQSについて紹介します。

What’s Amazon SQS !?

SQS.jpg

そもそもメッセージキューイングって何?

Message Queueing ( MQ : メッセージキューイング )

アプリケーションソフト間でデータを交換して連携動作させる際に、送信するデータをいったん保管しておき、
相手の処理の完了を待つことなく次の処理を行う方式。

メッセージキューイングの仕組みを活用することにより、
メッセージ送信者は受信側のステータスを気にすることなく、
メッセージを送信し、送信側と受信側間で非同期での処理が可能に。

Amazon SQSは、Pull型のMQサービス

MQ.jpg

受信側 (Instance) がSQSに問い合わせて、メッセージを取得します。

Amazon SQSは、AWSフルマネージドな分散キュー

フルマネージドとは ?

サービスの利用に必要な機器などの運用や管理、
導入時に必要な機器の設置や設定なども一体として提供するサービスのこと。

  • 高い信頼性 : 複数のサーバー/データセンターにメッセージを保持
  • スケーラブル : 多数の送信者/受信者に対応
  • 高スループット : メッセージが増加しても高スループットを出し続ける
  • 低コスト : 毎月の無料枠 + 使った分だけの従量課金

動作検証

(1) Lambda関数作成

※ 作成手順の詳細は、下記記事を参考にしていただければと思います。
Lambda関数を作成する方法 ~S3へのファイルアップロードをトリガーにログ出力編~

ここでは、特に重要な部分のみ抜粋しています。

1.トリガーの設定


ここでは、トリガーの対象となるS3のバケット名とイベント(今回は、「オブジェクトの作成」)の指定を行います。
また、作成後すぐに検証が行えるように、「トリガーの有効化」にチェックを入れておきます。

image.png

2.関数コードの編集


続いて、関数コードの編集を行います。ここでは、トリガーの対象となるイベント発生時に
どのような処理をおこなうのかを記述していきます。(Node.js 6.10)

今回は、下記のAmazon SQSへの操作を実装していきます。

(1) Amazon SQSに、新しいキューを作成 (AWS Console上で作成)

Amazon SQSのページを開き、「新しいキューの作成」ボタンを押下します。

image.png

任意の「キュー名」を付けます。

image.png

キューの作成」を押下すると、新しくキューが作成され、一覧に表示されます。

image.png

続いて、作成したキューにメッセージを送信します。

(2) (1)で作成したキューに、メッセージを送信

image.png

sendMessage.js

console.log('Loading function');

const AWS = require('aws-sdk');
const SQS = new AWS.SQS({apiVersion: '2012-11-05'});
/* 宛先QueueのURL */
const QUEUE_URL = 'https://sqs.ap-northeast-1.amazonaws.com/<アカウントID>/<キューの名前>';

exports.handler = function(event, context) {

    // Get Upload FilePath
    var Bucket = event.Records[0].s3.bucket.name;
    var ObjKey = event.Records[0].s3.object.key;
    var FilePath = Bucket + '/' + ObjKey;

    // SendMessage
    var params = {
        MessageBody: FilePath, /* required */
        QueueUrl: QUEUE_URL,   /* required */
        DelaySeconds: 0
    };

    SQS.sendMessage(params, function(err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else     console.log(data);           // successful response
    });

};

※ちなみに、対象キューのURLは、Amazon Console画面上から確認することができます。(SDKでも可。) [下図、赤色部分]

image.png

これで、S3にファイルをアップロードすると、Messageが送信されると思います。

と、その前に、Lambda上で、sendMessageをするためには、
実行ロールにAmazon SQSへの書き込み権限、ポリシーのアタッチをする必要があります。

3.アクセスポリシーのアタッチ


実行RoleにSQSアクセスポリシーをアタッチ

image.png

AmazonSQSFullAccess」のポリシーを実行ロールにアタッチします。

image.png

これですべての準備が整いました。
S3にファイルをアップロードしてみましょう。

(2) S3へファイルアップロード

上記で作成したLambda関数が動作をしているか実際に検証してみます。

適当なファイルを対象のバケット内にアップロード

image.png

Lamda関数が実行されていれば、Amazon SQSにメッセージが届いているはずです。

image.png

利用可能なメッセージが1になりました。もう1つS3にファイルをアップロードしてみると、、。

image.png

しっかりと、利用可能なメッセージが2に増えていますね。

終わりに

ひとまず、[S3へファイルアップロード][Lambda関数実行][Amazon SQSにメッセージ追加] までの
一連の流れが動くという検証ができました。

次回記事で、キュー内のメッセージ個数を確認する[Lambda関数]を作成したいと思います。

最後までお読みいただきありがとうございました。

続きを読む

python の関数を AWS Lambda で使用する

次の python の関数を、aws の Lambda にアップロードします。

evening_function.py
# -*- coding: utf-8 -*-
#
#   evening_function.py
#
#                   Oct/18/2017
#
# --------------------------------------------------------------------
import sys
import json

# --------------------------------------------------------------------
def evening_handler(event, context):
    sys.stderr.write("*** evening_handler *** start ***\n")
    print("Received event: " + json.dumps(event, indent=2))
    print("name = " + event['name'])
    print("AAA value1 = " + event['key1'])
    print("BBB value2 = " + event['key2'])
    print("CCC value3 = " + event['key3'])
    print("DDD value4 = " + event['key4'])
    print("EEE value5 = " + event['key5'])
    sys.stderr.write("*** evening_handler *** end ***\n")
#
    return event['name']  # Echo back the first key value
    #raise Exception('Something went wrong')
# --------------------------------------------------------------------

1) zip で、固めます。

zip -r evening.zip evening_function.py

2) aws にアップロードします。
 morning_exec_role というロールが作られているとします。 ロールの作り方は、
aws cli でラムダを使う

function_create.sh
aws lambda create-function \
    --function-name evening_function \
    --runtime python3.6 \
    --role arn:aws:iam::178234159025:role/morning_exec_role \
    --handler evening_function.evening_handler \
    --zip-file fileb://evening.zip \
    --region ap-northeast-1

3) CLIからイベントを発火

cli_exec.sh
aws lambda invoke --invocation-type Event \
    --function-name evening_function \
    --region ap-northeast-1 \
    --payload  '{"name": "島崎藤村","key1":" こんにちは", "key2":"おはよう", "key3":"さようなら","key4": "Hello","key5": "Bonjour"}' \
    outputfile.txt

4) CloudWatchLogsにログが出力されているか確認

続きを読む

CloudFormationでクロスアカウントなVPCPeeringを構築する

概要

別アカウントのVPCとVPCピアリングを行う際のCFnの書き方と、ロールの設定方法について。
なお同一アカウント内でのVPCピアリングだとロール無しでスルッと行ける。

やり方

ざっくり

  1. 前提としてピアリングを行うVPCは各アカウントで用意してあること
  2. VPCPeeringを受け入れる(accepter)アカウントでクロスアカウント許可用ロールを用意
  3. VPCPeeringをリクエストする(requester)アカウントでVPCPeeringリクエストを送信
  4. 完了

ロールの用意

テンプレート

acceptrole.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Accept VPC Peering Role
Parameters:
  AcceptAccountId:
    Type: String

Resources:
  AcceptVpcPeeringRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'AcceptVpcPeeringRole'
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: 'sts:AssumeRole'
            Principal:
              # ここで複数アカウントを指定することも可能
              AWS: !Join
                - ''
                - - 'arn:aws:iam::'
                  - !Ref AcceptAccountId
                  - ':root'
      Policies:
        - PolicyName: 'AcceptVpcPeering'
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: 'ec2:AcceptVpcPeeringConnection'
                # 適宜許可を行いたいVPCを設定
                Resource: '*'

実行コマンド

$ aws cloudformation create-stack 
    --stack-name accept-vpcpeering-role 
    --template-body file://acceptvpcpeeringrole.yaml 
    --parameters ParameterKey=AcceptAccountId,ParameterValue=000000 
    --capabilities CAPABILITY_NAMED_IAM 
    --profile=accepter # profileは適宜

VPCPeeringの作成

テンプレート

vpcpeering.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC Peering
Parameters:
  VpcId: 
    Type: String
    Default: 'vpc-xxxxxxxxx'
  PeerOwnerId:
    Type: String
    Default: '000000000000000'
  PeerVpcId:
    Type: String
    Default: 'vpc-xxxxxxxxx'
  PeerRoleArn:
    Type: String
    Default: 'arn:aws:iam::000000000000000:role/AcceptVpcPeeringRole'

Resources:
  VpcPeering:
    Type: "AWS::EC2::VPCPeeringConnection"
    Properties:
      VpcId: !Ref VpcId
      PeerOwnerId: !Ref PeerOwnerId
      PeerVpcId: !Ref PeerVpcId
      PeerRoleArn: !Ref PeerRoleArn
      Tags:
        - Key: Name
          Value: 'TestPeering'

コマンド

$ aws cloudformation create-stack 
    --stack-name vpcpeering 
    --template-body file://vpcpeering.yaml 
    --profile=requester # profileは適宜

続きを読む

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初めて使いましたが、かなり便利。

続きを読む

AWSのIAMロール・ポリシーをソースコード管理するのに役立つツールライブラリ

AWS の IAM ロールとポリシーをどうやって管理していますか?

JSON ファイルでロールとポリシー定義を管理できるのに便利なツール(npmライブラリ)を作りました。
github.com/tilfin/aws-iam-policy-tool

できること

  • ローカルの JSON ファイルと IAM のロールとポリシー定義の相互インポート・エクスポート
  • ローカルの定義ファイルに沿って IAM ロールとポリシーが構成されているかの検証
  • 正規表現マッチするカスタムポリシーの複数削除(AWS Management Console では一個ずつしか消せない)

できないこと

  • ロールのインラインポリシーはサポートしていない

どのように使えるのか

  • IAM ロールとポリシー定義を JSON ファイルでソースコード同様にGit等のリポジトリで管理できる。
  • npm ライブラリなので Lambda から定期的にデプロイ環境のロールとポリシーが正しいかの検証ができる。

定義ファイルの仕組み

  • IAM のブラウザ上で編集する JSON とほぼ同じです。
  • デプロイ環境別にリソース名に prefix/suffix を付けている場合は ENV としておくことで、コマンド実行時に引数で指定します。
  • AWS アカウント ID も ACCOUNT_ID でコマンド実行時に引数指定することで変えることができます。

ロール

{
    "Role": {
        "RoleName": "yourapp-ec2-api-ENV",
        "Path": "/",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ec2.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    },
    "AttachedPolicies": [
        {
            "PolicyName": "yourapp-s3-storage-ENV",
            "PolicyArn": "arn:aws:iam::ACCOUNT_ID:policy/yourapp-s3-storage-ENV"
        }
    ]
}

ポリシー

ポリシー名は内容に含まれないため JSON ファイル名そのものが適用されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::yourapp-storage-ENV/*"
        }
    ]
}

導入と使い方

npm からインストールします。

$ npm install -g aws-iam-policy-tool

現状のロールを ./roles とポリシーを ./policies にそれぞれ書き出します。

$ awsiamtool export-role ./roles
$ awsiamtool export-policy ./policies

各ファイルの ARN 内の AWS アカウント ID の部分を ACCOUNT_ID に置換します。動作環境を示すもの (例. dev, staging など) が名前に含まれていれば、ENV に置換します。ポリシーの場合はファイル名もリネームします。

検証してみます。

$ awsiamtool validate-role -i 111122223333 -e dev roles
$ awsiamtool validate-policy -i 111122223333  -e dev policies

本番環境に定義を作成します。

$ AWS_PROFILE=prod awsiamtool import-policy -i 111122224444 -e prd policies
$ AWS_PROFILE=prod awsiamtool import-role -i 111122224444 -e prd roles

※ポリシー修正時 import-policy に上書きオプションを --overwrite を指定します。

ライブラリとして使う(別の変数を適用するには)

現状コマンド引数ではサポートしていませんがライブラリとして使えば可能です。

const awsIamPolicyLib = require('aws-iam-policy-tool');

const opts = {
  json: true,
  overwrite: true
};

const varSets = {
  ACCOUNT_ID: '000011112222',
  ENV: 'stg',
  COMMON_ACCOUNT_ID: '333344445555'
};

awsIamPolicyLib.importPolicy('./policies', varSets, opts);
.then(() => {
  return awsIamPolicyLib.importRole('./roles', varSets, opts);  
})
.then(() => { console.info('Importing done') })
.catch(err => { console.error(err) });

その他、コマンドの実行結果例などは README を見てください。

続きを読む

logmonとALBのdrainingでリロード攻撃(F5アタック)へのパッシブ対策

以下の記事で紹介した小ネタの、具体的な利用例です。

1. 対策の内容

リロード攻撃(F5アタック)への対策としては、前段のApacheにmod_dosdetectorやmod_evasiveを入れる、WAFを導入する等があります。
このように、前段ですべて対応できれば良いのですが、

  • リロード攻撃を行っているのが正規ユーザである
  • リクエストによって(また、ユーザ毎に処理に必要なデータ量の多寡によって)レスポンスタイムにばらつきがある

というような場合、なかなか前段だけでの対処は難しいのではないかと思います。
そこで、Webアプリケーションサーバ(Tomcatなど)の処理が詰まってしまったときに、サービスの完全停止を避けるためにWebアプリケーションサーバの再起動を行うことがあると思いますが、

  • 「処理が詰まった」といっても、詰まり始めのうちは「特定の遅い処理」だけが「詰まる」のであり、それ以外のリクエストに対する処理は正常にレスポンスを返すことができている
  • 正常なレスポンスを返すことができるリクエストまで、再起動で中断するのは(なるべく)避けたい

ということで、Apacheのgraceful restartのような処理をしよう…というのが今回の内容です。

※きちんとgracefulな処理をするためには、Webアプリケーションサーバは複数台必要です。

2. ポリシー・IAM Roleの準備

まずは、先ほどの記事にある通り、以下の作業を行います。

  • ポリシーを設定する
  • 設定したポリシーをEC2用IAM Roleにアタッチする
  • そのIAM RoleをEC2(Webアプリケーションサーバ)にアタッチする

なお、今回のケースでは、「ec2:DescribeInstances」に対する権限は不要ですので、この部分はカットしても良いでしょう。

3. EC2上の設定

以下の記事を参考に、EC2(Webアプリケーションサーバ)にlogmonを導入します。

logmon.confには、以下の内容を設定します。

  • 1行目 : 監視対象のログファイル(Apacheのエラーログなら「:/var/log/httpd/error_log」など)
  • 2行目 : 監視対象のキーワード(正規表現/Tomcatのレスポンスが返らない場合を拾うのなら「[error] (70007)The timeout specified has expired」にマッチする内容)
  • 3行目 : 先の記事に示されているとおり

続いて、最初の記事で紹介したスクリプト「aws_utils.sh」を配置し(私の例では「/usr/local/sbin/」内)、「get_instance_id()」の部分だけ以下の内容に置き換えます。

aws_utils.sh(変更部分のみ)
#######################################
# 自身のインスタンス ID を取得する
# Returns:
#   INSTANCE ID
#######################################
get_my_instance_id() {
  instance_id=`/usr/bin/curl http://169.254.169.254/latest/meta-data/instance-id`
  if [ -z "$instance_id" ]; then
    echo 'host not found'
    exit 2
  fi
  echo ${instance_id}
}

※draining(登録解除の遅延)時間の長さに合わせて「SLEEP」の秒数も調整します。

それから、crontabから一定間隔で呼び出すスクリプト(私の例では「/usr/local/sbin/check_count.sh」)を配置します。

check_count.sh
#! /bin/sh

# スクリプトをインポートする
. /usr/local/sbin/aws_utils.sh

# トリガ判定
if [ `cat /tmp/logmon_count` -ge 20 ]; then

  # 閾値越え -> logmonサービス停止
  /sbin/service logmon stop

  # 二重トリガ起動防止(countを0に)
  echo 0 > /tmp/logmon_count

  # ALBでターゲットグループから外す
  ALB_TARGET_GROUP_ARN=('arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz' 'arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz')
  INSTANCE_ID=$(get_my_instance_id)

  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_deregister ${arn} ${INSTANCE_ID}
  done

  # ALBでdraining完了待ち
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_waiter ${arn} ${INSTANCE_ID} 'unused' > /dev/null
  done

  # Webサービス停止
  /sbin/service tomcat8 stop

  /bin/sleep 10

  # サービス起動
  /sbin/service tomcat8 start

  /bin/sleep 10

  /sbin/service logmon start

  # ALBでターゲットグループに戻す
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_register ${arn} ${INSTANCE_ID}
  done

  # ターゲットグループに戻ったことを確認する
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_waiter ${arn} ${INSTANCE_ID} 'healthy' > /dev/null
  done

fi

# countを0に
echo 0 > /tmp/logmon_count

if文の「20」は閾値です。適切な値に調整してください。
この例ではWebアプリケーションサーバとしてTomcat8を使っていますが、適切なものに置き換えてください。Tomcatの場合は、draining前後でPrintClassHistogramの出力などもしておくと良いです。

また、この例ではEC2(Webアプリケーションサーバ)を複数のターゲットグループ(配列「ALB_TARGET_GROUP_ARN」)に登録しています。
それぞれのターゲットグループでdraining時間が違う場合は、時間が短いものを先に記述すると良いです(後述の通りログを記録する場合は特に)。
1つの場合は配列にせず、for文で回す必要もありません。

なお、この例ではログを /dev/null に捨てていますが、実際に使うときにはきちんとログファイルに記録しておいたほうが良いです(時刻などとあわせて)。

最後に、このスクリプトを、実行ユーザ(rootなど)のcrontabに登録します(私の例では1分間隔で実行⇒閾値は1分当たりのカウントに対して設定)。このとき、1行目に「SHELL=/bin/bash」を挿入しておきます。

crontab登録例
SHELL=/bin/bash

*/1 * * * * /bin/sh /usr/local/sbin/check_count.sh

設定できたら、カウントファイル(私の例では「/tmp/logmon_count」)に閾値以上の値を書き出して、正しくdraining→ターゲットから削除→Webアプリケーションサーバ再起動→ターゲットに登録が行われるか、確認します。

テスト
echo 20 > /tmp/logmon_count

4. 注意点

drainingすると再起動には時間が掛かるので、Webアプリケーションサーバは最低でも4台程度は必要です。

続きを読む

CodeBuildのbuildspec.ymlを別の名前にしたいときの手順

TL;DR

CodeBuildを使っていると、ビルドの成果物が違うからビルドスペックのファイル(buildspec.ymlというもの)の単位で成果物を分けたいということがあります。調べてみると、ドキュメントに1にやり方が載っていたのでやってみました。方向性としては、AWS CLIで変更操作をする手順です。2

(2017-10-13 22:00 追記。 今さっき見てみたら、Consoleでファイル名を指定できるようになっていました。ちょろっと直すならConsoleでいいですね。)

概要

nantoka-buildというプロジェクトのビルドスペックのファイルをnantoka-kantoka-buildspec.ymlに変えたいとします。windowsの例ですが、mac でも linuxでも概ね同じでしょう。

プロジェクトの名前を確認する

Consoleから確認することもできますが、CLIと同じものが見えているか確認する意味も込めてやってみましょう。

>aws codebuild list-projects
{
    "projects": [
        "nantoka-build",
        "kantoka-build-project",
        "test-test-project"
    ]
}

>

各環境とかリージョンとかで色々あると思いますが、Console 
https://ap-northeast-1.console.aws.amazon.com/codebuild/home?region=ap-northeast-1#/projects
で、見えるプロジェクトとおなじビルドプロジェクトが見えていればOKです。nantoka-build以外のプロジェクトも、存在するなら見えていることでしょう。

プロジェクト名を指定して、プロジェクト構造のJSONファイルを取得する

更新の操作はプロジェクト構造のJSONファイルをアップロードする形になります。アップロードするファイルを作るため、現状のファイルをダウンロードします。

>aws codebuild batch-get-projects --names nantoka-build
{
    "projectsNotFound": [],
    "projects": [
        {
            "name": "nantoka-build",
            "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
            "tags": [],
            "artifacts": {
                "packaging": "NONE",
                "type": "CODEPIPELINE",
                "name": "nantoka-artifact"
            },
            "lastModified": 1512341234.783,
            "timeoutInMinutes": 60,
            "created": 1512341234.68,
            "environment": {
                "computeType": "BUILD_GENERAL1_SMALL",
                "privilegedMode": false,
                "image": "aws/codebuild/java:openjdk-8",
                "type": "LINUX_CONTAINER",
                "environmentVariables": []
            },
            "source": {
                "type": "CODEPIPELINE"
            },
            "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3",
            "arn": "arn:aws:codebuild:ap-northeast-1:1234123412341234:project/nantokanantoka"
        }
    ]
}

>

(arnなどは内容を適当にいじっています。表示される項目はあなたのプロジェクトの内容と同じはずです。)

プロジェクト構造のJSONファイルを

コマンドプロンプトに主力されたJSONをファイルに落として修正を入れます。

  • JSONのルートの"projects"の配下を取り出す。
  • "buildspec": "nantoka-kantoka-buildspec.yml"という1行を入れる。JSONなのでカンマ忘れずに。
  • "lastModified" "created" "arn" は消す。JSONの構造が違うというエラーになるので。無くても多分実害ないと想像する次第。本当は正しい書き方あると思いますが未確認です。
update.buildproject.json
{
    "name": "nantoka-build",
    "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
    "tags": [],
    "artifacts": {
        "packaging": "NONE",
        "type": "CODEPIPELINE",
        "name": "nantoka-artifact"
    },
    "timeoutInMinutes": 60,
    "environment": {
        "computeType": "BUILD_GENERAL1_SMALL",
        "privilegedMode": false,
        "image": "aws/codebuild/java:openjdk-8",
        "type": "LINUX_CONTAINER",
        "environmentVariables": []
    },
    "source": {
        "type": "CODEPIPELINE",
        "buildspec": "nantoka-kantoka-buildspec.yml"
    },
    "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
}

修正をアップロード

update.buildproject.jsonをカレントディレクトリに置いて、下記コマンドを実行する。

>aws codebuild update-project --cli-input-json file://update.buildproject.json
{
    "project": {
        "name": "nantoka-build",
        "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
        "tags": [],
        "artifacts": {
            "packaging": "NONE",
            "type": "CODEPIPELINE",
            "name": "nantoka-artifact"
        },
        "timeoutInMinutes": 60,
        "environment": {
            "computeType": "BUILD_GENERAL1_SMALL",
            "privilegedMode": false,
            "image": "aws/codebuild/java:openjdk-8",
            "type": "LINUX_CONTAINER",
            "environmentVariables": []
        },
        "source": {
            "type": "CODEPIPELINE",
            "buildspec": "nantoka-kantoka-buildspec.yml"
        },
        "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
    }
}

>

Consoleで確認する。

Consoleからプロジェクトを選択し、プロジェクト詳細 -> ビルド仕様の表示 とクリックして進むと、nantoka-kantoka-buildspec.yml と表示されます。


  1. http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-name-storage 今のところ日本語にはなってないようです。 

  2. ぶっちゃけた話、Consoleがすぐにでも対応しそうな気もしますが、今時点での手順を残します。 

続きを読む

ALB・EC2小ネタ/AWS CLIでALB配下のTGにEC2インスタンスを登録/削除するためのポリシー(メモ)

kakakakakkuさんが以下のブログ記事で公開されているALBのターゲットグループにEC2インスタンスを登録/削除するシェルスクリプトを実行するために必要なポリシーです。

1. 指定するポリシー

以下のポリシーを登録し、スクリプトを動かすEC2インスタンスに付与するIAM Roleにアタッチします。

AlbRegistrationPolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:RegisterTargets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances"
            ],
            "Resource": "*"
        }
    ]
}

特に何のひねりもないですが、AWSユーザーガイドの以下のページにある通り、

  • ALBの3つのAPIアクション
  • EC2の1つのAPIアクション(これはkakakakakkuさんの記事中「aws_utils.sh」の「get_instance_id()」で使用)

とも、リソースレベル権限の詳細な指定に対応していないので、Resourceには「*」を指定する必要があります(最初見落としていて、AWSパートナーさんに教えていただきました)。
本番(プロダクト)環境とステージング/開発環境を同一のアカウントで運用している場合は、指定するターゲットグループのARNを間違えないよう注意が必要です。

2. おまけ1

私の場合、登録/削除するEC2インスタンス自身でAWS CLIのスクリプトを実行しているので、前述の「get_instance_id()」は使わず、curlで http://169.254.169.254/latest/meta-data/instance-id をGETしてインスタンスIDを取得しています。
その場合は「ec2:DescribeInstances」の権限は不要です。

3. おまけ2

crontabから呼び出すスクリプトの場合、初期設定のままでは「.」や「source」で「aws_utils.sh」をインポートできないので、1つのスクリプトにまとめるか、以下の記事を参考にしてインポートできるようにします。

続きを読む