20191215のAWSに関する記事は30件です。

[AWS] VPCでネットワーク構築2

[AWS] VPCでネットワーク構築1
の続きです

ここでは作成したパブリックサブネットにWEBサーバ用のEC2インスタンスを作成していきます

1.インスタンスの作成

  • サービス > EC2 > インスタンス > インスタンスの作成 image.png

・ネットワークはVPC領域を選択
・サブネットはパブリックサブネットを選択
・プライマリIPは10.0.1.10を割り当てときます
・次のステップをクリック

2.ストレージの追加

image.png
・今回はデフォルトのままで次のステップをクリック

3.タグの追加

image.png
・タグの追加をクリック、Webサーバとして使うのでkeyにName、valueにWebサーバーと入力
・次のステップをクリック

4.セキュリティグループの設定

image.png

・セキュリティグループにWEB-SGと入力
・確認と作成をクリックし、インスタンス作成の確認画面に移動するので起動をクリック

5.キーペアの作成

image.png
・新しいキーペアの作成を選択
・キーペア名をmy-keyとしておきます
・キーペアのダウンロードをクリックして作成したキーペアをダウンロード
・インスタンスの作成をクリック

6.Elastic IPを割り当てて、ssh接続してみる

こちらを参考にしてください

7.DNSサーバーを構成する

  • VPC > アクション > DNSホスト名の編集 image.png

・サービス > EC2 > インスタンスに戻ってインスタンスの説明タグを確認してみると、パブリックDNSとプライベートDNSが設定されているのが確認できます
 
これでWEBサーバの準備は完了です
次にプライベートサブネットを作ってDBサーバを配置していきたいと思います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[AWS] VPCでネットワーク構築1

AWSでVPCネットワークを構築するメモ
今回はVPC領域をパブリックとプライベートのサブネットに分割して、それぞれにWEBサーバとDBサーバを構築してみました
参考:Amazon Web Services 基礎からのネットワーク&サーバー構築

ちなみにVPCとは

Virtual Private Cloud (VPC) は、AWS アカウント専用の仮想ネットワークです。VPC は、AWS クラウドの他の仮想ネットワークから論理的に切り離されており、AWS のリソース(例えば Amazon EC2 インスタンス)を VPC 内に起動できます。

完成イメージ

Untitled Diagram.jpg

1.VPCを作る

  • サービス > ネットワーキングとコンテンツ配信 > VPC > VPCの作成 image.png

・リージョンを東京にしときます
・今回はIPv4で作成
・10.0.0.0 ~ 10.0.255.255までのipアドレスを割り振れるようにしておきました

2.パブリックサブネットを作る

  • サービス > ネットワーキングとコンテンツ配信 > サブネット > サブネットの作成 image.png

・VPCには1で作った領域を設定

3.インターネットゲートウェイを作る

  • サービス > ネットワーキングとコンテンツ配信 > インターネットゲートウェイ > インターネットゲートウェイの作成 image.png

・インターネットに接続するためにインターネットゲートウェイを作成
・今回は名前タグを空のまま

4.インターネットゲートウェイとVPCを紐づける

  • サービス > ネットワーキングとコンテンツ配信 > インターネットゲートウェイ > アクション > VPCにアタッチ image.png

・1で作ったVPCを選択

5.ルートテーブルを作る

  • サービス > ネットワーキングとコンテンツ配信 > ルートテーブル > ルートテーブルの作成 image.png

・ネットワークにデータを流すために「ルーティング情報」の設定が必要
・宛先IPアドレスの値を見て、どのネットワークに流すべきかという設定がルートテーブル
・1で作ったVPCを選択

6.ルートテーブルをサブネットに割り当てる

  • サービス > ネットワーキングとコンテンツ配信 > ルートテーブル > サブネットの関連付けの編集 image.png

・2で作ったサブネットを5で作ったルートテーブルに関連付ける

7.デフォルトゲートウェイにインターネットゲートウェイを設定する

  • サービス > ネットワーキングとコンテンツ配信 > ルートテーブル > ルートの編集 image.png

・デフォルトゲートウェイ(0.0.0.0/0)を3で作成したゲートウェイに設定する

 
 
これでVPC領域とパブリックサブネットの設定は終わりです
次はパブリックサブネットにWEBサーバ用のEC2インスタンスを作成していきたいと思います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SlackのWebhookをプロキシする仕組みを作る

Slackはさまざまなカスタマイズ機能を持っているのが魅力のツールです。例えばBotを作ったり、カスタムのslash commandを作ったりすることで、プラットフォームの拡張ができます。

Slack Botの作り方はいくつかあるのですが、Slackのリッチな機能を最大限に引き出すには、SlackからのWebhookを受けることが必要になってきます。すなわち、ボタンなどが付いたリッチなメッセージの投稿は難しくないのですが、投稿したメッセージのボタンやメニュー操作は、SlackからWebhookの形で通知される仕組みになっています。

※ この辺りの仕組みの詳細については、まとまっている記事がいくつもあるので省略します。

Slackは当然publicなサービスなので、インターネット経由でWebhookが飛んできます。これを受けて処理するためには、Bot用の公開APIサーバを書くことになるでしょう。

公開サーバ? 何か問題でも?

それじゃあ、単にサーバを立てればいいじゃないかというのはもっともですが、サーバを公開するのはいくつか面倒な点があります。

  • 公開サーバはセキュリティリスクがある
    • もちろん、ちゃんと管理すればいいのですが、publicに露出するものが増えると相応にリスクが増加してしまうのはどうしようもないことです。できればやりたくありません
  • とりわけ会社業務で使う場合、イントラネットにある社内システムや社内ネットワークとの接続性を考慮する必要が出てくる
    • 当然、Botであるからには、社内のいろいろなものと連携させたくなります。

SlackのRTM APIというWebSocketベースのAPIでchatメッセージを受信する場合には、こういった悩みは発生しませんでした(メッセージをpullするAPIを使うので、基本的にインターネットへ接続できるBot実行環境がありさえすればよかった)。できる限りそのお手軽さを保ったまま、セキュリティなどの煩雑な問題をコントロールする方法がないか、ということを考えたくなります。

そうだプロキシしよう

というわけで、次のようなシステムを考えることにしました。

  • Webhookを直接Botサーバに流すのではなく、プロキシ(リバースプロキシ)を経由させる
  • プロキシは単にリクエストを横流しするのではなく、リクエストの検証を行い、不正なリクエストを弾く
    • Slackから事前に払い出された秘密鍵を元に、指定の手順でHMACを計算することで、リクエストの正当性の検証が可能です
    • Verifying requests from Slack
    • SDKがあるので、Slack社オフィシャルの実装をそのまま引っこ抜いてくることができます

具体的には、AWSのAPI GatewayとLambdaを組み合わせた、簡単なプロキシを書きます。

slack-to-intranet.png

パターンとして変則的ではありますが、結果的にAWSのDDoS対策ホワイトペーパーで述べられている、API Gatewayに公開箇所を絞るというプラクティスに沿っているように思います。

Typically, when you must expose an API to the public, there is a risk that the API frontend could be targeted by a DDoS attack. To help reduce the risk, you can use Amazon API Gateway as a “front door” to applications running on Amazon EC2, AWS Lambda, or elsewhere.
By using Amazon API Gateway, you don’t need your own servers for the API frontend and you can obfuscate other components of your application. By making it harder to detect your application’s components, you can help prevent those AWS resources from being targeted by a DDoS attack.

実装例

今回は(ちょっと慣れない言語なのですが)Node.jsでlambdaを書いてみることにします。
フレームワークとしてserverless frameworkを使うとお手軽にAPI Gateway + Lambdaの構成を作ることができました。

https://github.com/saka1/slack-webhook-gatekeeper

バックエンドのSlack App名に対応したURLパスに対してWebhookが飛んでくると、秘密鍵を使ってWebhookの検証をします。あらかじめ、Slack App名と秘密鍵の対応関係はparameter storeに入れておきます。正当なWebhookだった場合にはupstreamにリクエストを飛ばしてプロキシ動作をします。URLパスは複数登録することができるように作りました。

学んだこと

  • AWSのparameter storeはあんまり高速ではなさそう(getに数百msかかる?)だったので、lambdaでキャッシュしたほうが無難そうでした
  • プロキシとBotは1:Nの関係にあるため、プロキシシステムは全てのBot Appの秘密情報を管理しないといけなくなります(そうしなければリクエストの検証ができません)
    • 理想的ではないので、何かうまい回避方法があればなあと思っています

API Gatewayからlambdaを呼ぶには「lambda統合」「lambdaプロキシ統合」の2つがあるらしいのですが、前者を使ったほうがよさそうでした。というか飛んでくるWebhookの検証でHMAC計算が入るため、1バイトでもゴミが入ると正しく動きません。そもそも前者は用途に対して複雑すぎました。

まとめ

この記事では、Slack Botの前段に置くプロキシシステムを検討し、その実装までをやってみました。ざっくり机上で検討 → ざっと実装した割には案外使えそうなものが考案できて、個人的には満足しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2で名前解決の回数が多くて、名前解決に失敗するときの対策

とある大規模WEBシステムをオンプレからAWSに移行したとき遭遇した題名の件のTIPSです。

データストアにエンドポイント名(=固定IPをもたない)、RDSやElastiCahceなどを使う、中~大規模系のシステムをEC2で利用する際は、必須の対策かもです。

システム構成

システム構成としては、よくあるパターンのものとなります。

WEBアプリケーションサーバは、Elastic Beanstalk や Lambdaなどがよいとおもいつつも、WEBアプリケーションの複雑性からEC2をオートスケールで用意しました。

一方で、データストアは、ElastiCacheとAurora Mysqlを利用しています。

EC2及びデータストアは、潤沢なインスタンスサイズを利用していました。

処理量

ざっくりですが、WEBアプリケーションに対するアクセス、そのときに発生するElastiCahce及びAurora Mysqlといったデータストアのエンドポイントへのアクセスが、秒間数千を超えるときがあるレベルです。

事前対策

マネージドサービスであるElastiCacheとAurora Mysqlはパラメータグループを微調整した程度でした。

EC2に関しては、主要ミドルウェアのPHPとApacheの一般的な制限系の項目(memory_limitとかmax_clientなど)の上限値変更と、大規模系のためのカーネルの設定(ipv4.tcp_syncookiesとかipv4.ip_local_port_rangeなど)を行っていました。

事象

今回、下記のようにデータストアに接続しようとしたが名前解決に失敗して接続できなかった旨のエラーが、処理量が多くなるとログに出力されていた。

PHP Warning:  Memcache::connect(): php_network_getaddresses: getaddrinfo failed: Name or service not known

PHP Warning:  PDO::__construct(): php_network_getaddresses: getaddrinfo failed: Name or service not known

PHP Warning:  mysql_connect(): php_network_getaddresses: getaddrinfo failed: Name or service not known

対策

AWSさんが2019年10月に対策を公開してくれたました。

参考:https://aws.amazon.com/jp/premiumsupport/knowledge-center/dns-resolution-failures-ec2-linux/

OSの種類などによって微妙に対策が異なりますが、私の場合は、AmazonLinuxだったので、AutomateDnsmasq.sh をダウンロードして、実行して、ほぼ対策は完了しました。

最初、それだけだと、Linuxのコンソールでローカルで動いているdnsmasqに名前解決にいっても、WEBアプリケーションは利用しないという罠にかかりましたが、これはPHPを再起動したら治りました。

キャッシュの時間は、基本的に、DNSレコードのTTLを利用するそうなので、キャッシュ時間をそれ以上伸ばしたい場合は、下記を利用ください。この設定はうまく動作しないときがあるという情報がありましたが、いじったらキャッシュ時間伸びていたので、この環境では動いているみたいでした。

max-cache-ttl

ただ、もちろん、マネージドサービス側はIPが変わる可能性があるるので、そのリスクは考慮して調整してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2で、秒間数千回~レベルでElastiCacheやRDSに接続して、名前解決に失敗するときの対策

とある大規模WEBシステムをオンプレからAWSに移行したとき遭遇した題名の件のTIPSです。

データストア(RDSやElastiCahceなど)の接続に、エンドポイント(=固定IPをもたない)を使う、EC2を利用した中~大規模のシステムでは、必須の対策かもです。

システム構成

システム構成としては、よくあるパターンのものとなります。

WEBアプリケーションサーバは、Elastic Beanstalk や Lambdaなどがよいとおもいつつも、WEBアプリケーションの複雑性からEC2をオートスケールで用意しました。

一方で、データストアは、ElastiCacheとAurora Mysqlを利用しています。

EC2及びデータストアは、潤沢なインスタンスサイズを利用していました。

処理量

ざっくりですが、WEBアプリケーションに対するアクセス、そのときに発生するElastiCahce及びAurora Mysqlといったデータストアのエンドポイントへのアクセスが、秒間数千を超えるときがあるレベルです。

事前対策

マネージドサービスであるElastiCacheとAurora Mysqlはパラメータグループを微調整した程度でした。

EC2に関しては、主要ミドルウェアのPHPとApacheの一般的な制限系の項目(memory_limitとかmax_clientなど)の上限値変更と、大規模系のためのカーネルの設定(ipv4.tcp_syncookiesとかipv4.ip_local_port_rangeなど)を行っていました。

事象

今回、下記のようにデータストアに接続しようとしたが名前解決に失敗して接続できなかった旨のエラーが、処理量が多くなるとログに出力されていました。

PHP Warning:  Memcache::connect(): php_network_getaddresses: getaddrinfo failed: Name or service not known

PHP Warning:  PDO::__construct(): php_network_getaddresses: getaddrinfo failed: Name or service not known

PHP Warning:  mysql_connect(): php_network_getaddresses: getaddrinfo failed: Name or service not known

対策

AWSさんが2019年10月に対策を公開してくれたました。

参考:https://aws.amazon.com/jp/premiumsupport/knowledge-center/dns-resolution-failures-ec2-linux/

OSの種類などによって微妙に対策が異なりますが、私の場合は、AmazonLinuxだったので、AutomateDnsmasq.sh をダウンロードして、実行して、ほぼ対策は完了しました。

最初、それだけだと、Linuxのコンソールでローカルで動いているdnsmasqに名前解決にいっても、WEBアプリケーションは利用しないという罠にかかりましたが、これはPHPを再起動したら治りました。

キャッシュの時間は、基本的に、DNSレコードのTTLを利用するそうなので、キャッシュ時間をそれ以上伸ばしたい場合は、下記を利用ください。この設定はうまく動作しないときがあるという情報がありましたが、いじったらキャッシュ時間伸びていたので、この環境では動いているみたいでした。

max-cache-ttl

ただ、もちろん、マネージドサービス側はIPが変わる可能性があるるので、そのリスクは考慮して調整してください。

※基本的には「max-cache-ttl」はいじらない方がいいとおもいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS の利用料金の合計を Cost Explorer APIで取得する

概要

AWS の利用料総額を、Slack 通知する Lambda 関数を Python 3 で作成します。
Python/AWS 共に初心者レベルで修行中の身のため、極力単純な内容にしています。
勉強のため軽く見たいけど応用例みたいな記事しかなくて困ったので、そういった方の参考になれば幸いです。

AWS 使用料金について

Cost Explorer API を利用するため、1回の実行当たり 0.01 USD が最低でも発生します。
連続実行などはされないかと思いますがご注意ください。

また、この記事では AWS Cost Explorer 画面の(おそらく)デフォルトで選択されている 非ブレンドコスト ではなく、ブレンドコスト を Cost Explorer API にて取得します。

Slack 設定

Slack 通知するための Incomming Webhook 設定は こちらの記事 がわかりやすいので、詳細は省きます。
Incomming Webhook の URL をテキストエディタなどに控えておけば OK です。

ポリシーの作成

Cost Explorer の読み取り権限が必要なので、その権限がついたポリシーを作成します。
AWS IAM 画面の ポリシーの作成 ボタンを押下して、以下を設定。

  • サービス: Cost Explorer Service
  • アクション: 読み込みにチェック

(読み取り権限をすべて付与していますが、利用状況によるものの以下でも十分そうです)

"ce:GetReservationUtilization",
"ce:GetDimensionValues",
"ce:GetCostAndUsage",
"ce:GetReservationCoverage",
"ce:GetReservationPurchaseRecommendation",
"ce:GetCostForecast",
"ce:GetTags"

ポリシーの確認 ボタンを押下して、以下を入力。

  • 名前: CostExplorerReadOnly
  • 説明: CostCheck By lambda

ポリシーを作成して、この作業は完了です。

Lambda の設定

Lambda 関数の作成

Lambda の ダッシュボード or 関数 の、関数の作成 ボタンを押下。
以下の設定をして 関数の作成 ボタンを押下。

  • 一から作成 を選択。
  • 関数名: cost_check などそれっぽい名前
  • ランタイム: Python 3.7

実行ロール設定

作成された Lambda 関数の 実行ロールの部分で、特に設定をしていなければ service-role/cost_check-role-hoge のようなロールが作成&設定されているかと思います。
このロールに Cost Explorer の読み取りポリシーをアタッチします。

  • IAM コンソールで cost_check-role-hoge ロールを表示します のリンクを押下。
  • 表示された画面で、ポリシーをアタッチします ボタンを押下。
  • 先ほど作成した CostExplorerReadOnly ポリシーを選択して、ポリシーのアタッチを押下。

環境変数

実行ロールの少し上にある 環境変数 にて、以下の2つを設定します。
ドル円換算に利用する1ドル何円かと、事前に設定した Slack の Webhookk URL です。

  • JPY: 110
  • WEBHOOK_URL: https://hooks.slack.com/services/(以下略)

関数コード

以下となります。
各々の処理でそれほど難しいことはしていないので、ちょっと悩んだ所だけ解説します。

  • 費用を取得する cost_check
  • Slack で表示するメッセージ作成の create_message
  • Slack に通知する post_slack
import json
import urllib.request
import os
import boto3
import math
from datetime import datetime, date, timedelta

def cost_check():
    client = boto3.client('ce')
    today = datetime.today()

    # 合計金額を知りたいので、フィルタは最小限
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': datetime.strftime(today.replace(day=1), '%Y-%m-%d'),
            'End': datetime.strftime(today, '%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['BlendedCost'],
    )

    return response['ResultsByTime'][0]['Total']['BlendedCost']['Amount']

def create_message(cost, jpy):
    # cost と jpy から日本円換算。日本円表記なので切り上げで処理。
    cost_jpy = str(math.ceil(float(cost) * int(jpy)))
    message = "当月のAWS使用料金は `$" + cost + "` です。" \
+ "\n日本円換算で `約" + cost_jpy + "円`となります"

    return message

def post_slack(webhook_url, message):

    # ユーザ名、アイコン含み Slack 投稿内容を設定
    send_data = {
        "username": "Slack 通知時に表示される名前",
        "icon_emoji": ":moneybag:",
        "text": message,
    }
    #print(send_data)
    send_text = "payload=" + json.dumps(send_data)
    request = urllib.request.Request(
        webhook_url, 
        data=send_text.encode("utf-8"), 
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")

def lambda_handler(event, context):
    # 環境変数取得
    webhook_url = os.environ['WEBHOOK_URL']
    jpy = os.environ['JPY']

    # 当月費用を取得
    cost = cost_check()

    # Slack 通知するメッセージ生成
    message = create_message(cost,jpy)

    # Slack 通知
    post_slack(webhook_url,message)

ちょっと悩んだところ

cost_check 内の get_cost_and_usage のレスポンス処理です。
return の前で print(response) などしてレスポンスの中身を見ると、以下のような形となっています。

{
    'ResultsByTime': 
    [
        {
            'TimePeriod': 
            {
                'Start': '2019-12-01', 
                'End': '2019-12-15'
            }, 
            'Total': 
            {
                'BlendedCost': 
                {
                    'Amount': '0.2763438289', 
                    'Unit': 'USD'
                }
            }, 
            'Groups': [], 
            'Estimated': True
        }
    ], 
    'ResponseMetadata': (略)
}

JSON 形式のレスポンスから、ResultsByTime の Total の BlendedCost の Amount を取得する!
ということで、当初は
response['ResultsByTime']['Total']['BlendedCost']['Amount']
なんて指定をしていてうまくいかず。
JSONを分解してみて [0] が必要なことに気づきました。。。
response['ResultsByTime'][0]['Total']['BlendedCost']['Amount']

テスト

画面上部の テスト ボタンを押下した後、イベントテンプレート Hello World のテストが表示されているかと思います。
以下の設定をして、作成 ボタンを押下します。

  • イベント名: costchecktest
  • テストの中身: {} のみ

画面上部の 保存 ボタンで内容を保存した後、テスト ボタンを押下して、テスト実行します。

テスト完了後、画面上部に実行結果が表示されます。
成功 ではない場合、表示されているログから修正箇所を探り、修正します。

実行トリガの設定

実行結果のすぐ下、Designer の トリガーを追加 ボタンを押下します。
CloudWatch Events を選択した後、新規ルールの作成を選択します。
今回は毎月1日から5日おき(1,6,11...,31日)の日本時間 10:00 に Lambda 関数が呼び出されるようなルールを作成します。
具体的には以下の内容を設定します。

  • ルール名: 5Day_1000JST など、わかりやすい名前
  • ルールの説明: execute 1,6,11...,31 1000JST など、説明っぽいもの。
  • ルールタイプ: スケジュール式
  • スケジュール式: cron(0 1 */5 * ? *)
  • トリガーの有効化: チェックを入れる

上記設定をして 追加 ボタンを押せば、あとは CloudWatch Events から呼び出されて Lambda 関数が自動実行される日を待つだけです。

曜日部分を * ではなく ? にするのが若干ハマりどころですが、公式ドキュメント を読んでおけば迷いはないはずです(迷った)
また、Cron 式は UTC にて実行されるため、JST で実行するにはマイナス9時間する必要があります(当初はしてなくて思いもよらない時間に実行されてびっくりした)

終わりに

Python 自体の辞書やリストの扱いにも不慣れで、JSON を扱うにも一苦労でした。
色々細かなものを作成して、このあたりの処理については慣れていきたいですね。

油断していたら日付ギリギリぐらいになってしまいました。
来週は余裕をもって投稿したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[AWS][CFn]IAMの設定

はじめに

こんにちは。なじむです。
引き続き、クラスメソッドさんのブログ記事、AWSアカウントを作ったら最初にやるべきこと ~令和元年版~を参考にさせていただき、AWSでの初期設定をCloudFormationで実施していきます。
今回は「IAMの設定」をCloudFormationで実施していきます。IAMはAWSを使用する上で一番重要と言っても過言ではないと思います。その設定が少しでも楽になれば。

前提

今回は特定のIPからのアクセスのみを有効にします。ただし、特定のIPからのアクセスのみに制限してしまうと、AWS内部の通信(例えばEBSの暗号化)も制限してしまうことになるため以下の条件で設定します。

  • コンソールにログインした後、スイッチロールを行い任意のロールにスイッチする
  • 上述のスイッチロールは特定のIPからしか実行できないよう制限する
  • スイッチロールした後はIPアドレスの制限を設けず、任意の操作を実行可能とする

今回は例としてAdministartorAccessのIAMポリシーを使用して、すべての権限を持つグループを作成します。
サンプルコードで実施しているのは以下です。

  • IAMグループ(Administrator)を作成する
  • IAMポリシー(OnlyMyIPAccess, AssumeAdministrator)を作成する
  • IAMロール(Administrator)を作成する

サンプルコード

---
AWSTemplateFormatVersion: 2010-09-09
Description: IAM - Group, Role, Policy for Administrator

#------------------------------
# Resources: Your resource list
#------------------------------
Resources:
  # IAM(Group) Resource
  ## Administrator
  GroupAdministrator:
    Type: AWS::IAM::Group
    Properties:
      GroupName: Administrator
      ManagedPolicyArns: 
      - !Ref AssumeAdministrator
      - !Ref OnlyMyIPAccess

  # IAM(Policy) Resource
  ## Group's Policy -- MyIPAccess
  OnlyMyIPAccess:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Deny
          Action: "*"
          Resource: "*"
          Condition:
            NotIpAddress:
              aws:SourceIp:
              - xxx.xxx.xxx.xxx/32 # ここは任意のIPアドレスに修正してください。

  ## Group's Policy -- Administrator
  AssumeAdministrator:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Action: sts:AssumeRole
          Resource: !GetAtt RoleAdministrator.Arn

  # IAM(Role) Resource
  ## Administrator
  RoleAdministrator:
    Type: AWS::IAM::Role
    Properties:
      RoleName: Administrator
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Action: sts:AssumeRole
          Principal:
            AWS: !Sub ${AWS::AccountId}
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AdministratorAccess
      Path: "/"

実行結果

では、いつも通り作成したリソースを見ていきます。

  • IAMグループ(Administrator)を作成する

  • IAMポリシー(OnlyMyIPAccess, AssumeAdministrator)を作成する
    20191215_212819.jpg

  • IAMロール(Administrator)を作成する
    20191215_212847.jpg

今回は作成したリソースではなく、動作を見ていきましょう。

  • MyIPからログインした直後 ※describeInstancesの権限がないことが分かります。
    20191215_212443.jpg

  • スイッチロール後 ※describeInstancesの権限が付与されていることが分かります(EC2を作成していないので何も表示されません…笑)
    20191215_212530.jpg

まとめ

今回は自社のオフィス以外からはアクセスさせたくないという場面を想定して特定のIPからのみにログインを絞る方法でIAMを構築してみました。ただ、この方法だと1度スイッチロールした後、その状態を保持したままIPが変わってもログインは保持されるため操作が可能となります(そんな場面はまれだと思いますが)。その場合、IAMロールの認証が切れる1時間後か、ユーザがログアウトするのを待つしかないかなと思います。
IAMの構成については、個人的にはネストスタックを使用すると少し構造が分かりやすくなるかなと思うので、次回はIAMの設定をネストスタックでやってみるをお送りしようと思いますノシ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CDKを使ってVPC/EC2/S3バケットを構築する

AWS CDKを使った基本的な環境を構築したくなったので、記事を投稿してみました。
また、退職して少しの間ニートになったので、CDK関連の記事を今後投稿してみようと思ってます。

作成する環境構成

以下のような環境を構築するためのCDKソースコードを作成します。
基本的な構成なのですが、CDKでコーディングすると楽しいのでおすすめです。

SgF9dAJ.png

  • CDKバージョン: v1.18.0
  • TypeScript: v3.7.3

AWS CDKのインストール

sudo npm install -g aws-cdk
cdk ---version
1.18.0 (build bc924bc)
mkdir mini-cdk
cd mini-cdk
cdk init app --language=typescript
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-iam
# 作成するスタッックのファイル
touch lib/mini-vpc-stack.ts
touch lib/mini-ec2-stack.ts
touch lib/mini-s3-stack.ts

# 都合上削除
rm test/mini-cdk.test.ts

VPCスタックの作成

VPCスタックを作成します。
注意点として、利用するAZの数とNatGatewayに気を付けた方がよいです。natGateways: 0として明示しないと、AZごとにNatGatewayが作成されてしまいます。個人で使う分には利用コストが高くなってしまうので注意が必要です。

lib/mini-vpc-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");

export class MiniVpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCを作成する
    this.vpc = this.createVpc();

    // GatewayEndpointをセットアップする
    this.setupGatewayEndpoint();

    // InterfaceEndpointをセットアップする
    this.setupInterfaceEndpoint();
  }

  private createVpc() {
    return new ec2.Vpc(this, "MiniVpc", {
      cidr: "172.17.0.0/16",
      natGateways: 0, // NatGatewayはお金がかかるので使わない
      maxAzs: 1, // Availability Zoneの数
      subnetConfiguration: [
        { name: "Public", cidrMask: 20, subnetType: ec2.SubnetType.PUBLIC },
        { name: "Isolated", cidrMask: 20, subnetType: ec2.SubnetType.ISOLATED }
      ]
    });
  }
  private setupGatewayEndpoint() {
    // IsolatedのサブネットからS3に直接アクセスできるようにVPCエンドポイントを利用する
    this.vpc.addGatewayEndpoint("S3EndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });

    // IsolatedのサブネットからDynamoDBに直接アクセスできるようにVPCエンドポイントを利用する
    // 今回は利用しません。
    this.vpc.addGatewayEndpoint("DynamoDbEndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });
  }

  private setupInterfaceEndpoint() {
    // InterafceEndpoint用のセキュリティグループ
    const interfaceEndpointSecurityGroup = new ec2.SecurityGroup(
      this,
      "InterfaceEndpointSecurityGroup",
      {
        securityGroupName: "InterfaceEndpointSecurityGroup",
        vpc: this.vpc
      }
    );
    interfaceEndpointSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443)
    );

    // Isolated SubnetからECRに接続するためのエンドポイント
    // 今回は利用しません。
    this.vpc.addInterfaceEndpoint("ECRInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: {
        subnetType: ec2.SubnetType.ISOLATED
      }
    });
    this.vpc.addInterfaceEndpoint("ECR_DockerInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
    });
  }
}

setupInterfaceEndpointメソッドでECRにアクセスするためのエンドポイントを作成していますが、今回は関係ありません。実装の参考情報として解釈してください。

EC2スタックの作成

Publicサブネットに1つ、Isolatedサブネットに1つインスタンスを作成するスタックを作成します。
IsolatedサブネットのEC2インスタンスからS3バケットにアクセスできるようにロールをアタッチしています。

lib/mini-ec2-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");
import iam = require("@aws-cdk/aws-iam");

interface MiniEc2StackProps extends cdk.StackProps {
  readonly vpc: ec2.Vpc; // MiniVpcStackのプロパティをvpcを受け取りたいので設定している
  readonly keyName: string; // キーペア
}

export class MiniEc2Stack extends cdk.Stack {
  private readonly vpc: ec2.Vpc;
  private readonly keyName: string;

  private publicEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2Role: iam.Role;

  constructor(scope: cdk.Construct, id: string, props: MiniEc2StackProps) {
    super(scope, id, props);

    this.vpc = props.vpc;
    this.keyName = props.keyName;

    // PublicSubnetのEC2インスタンスをセットアップする
    this.setupPublicEc2Instance();

    // IsolatedSubnetのEC2インスタンスをセットアップする
    this.setupIsolatedEc2Role();
    this.setupIsolatedEc2Instance();
  }

  // PublicSubnetにEC2インスタンスを作成する
  private setupPublicEc2Instance() {
    this.publicEc2SecurityGroup = this.createPublicEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniPublicEc2Instance", {
      vpc: this.vpc,
      instanceName: "MiniPublicEc2Instance",
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC
      },
      userData,
      securityGroup: this.publicEc2SecurityGroup
    });
  }

  // IsolatedSubnetにEC2インスタンスを作成する
  private setupIsolatedEc2Instance() {
    this.isolatedEc2SecurityGroup = this.createIsolatedEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniIsolatedEc2Instance", {
      instanceName: "MiniIsolatedEc2Instance",
      vpc: this.vpc,
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.ISOLATED
      },
      securityGroup: this.isolatedEc2SecurityGroup,
      role: this.isolatedEc2Role
    });
  }

  private setupIsolatedEc2Role() {
    this.isolatedEc2Role = new iam.Role(this, "IsolatedEc2InstanceRole", {
      roleName: "IsolatedEc2InstanceRole",
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com")
    });

    // ロールにS3へのアクセスするポリシーをアタッチする
    this.isolatedEc2Role.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket",
          "s3:ListAllMyBuckets"
        ],
        resources: ["*"]
      })
    );
  }

  // PublicサブネットのEC2インスタンスのセキュリティグループを作成する
  private createPublicEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniPublicEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniPublicEc2InstanceSg"
    });
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));
    return sg;
  }

  // IsolatedサブネットのEC2インスタンスのセキュリティグループを作成する
  private createIsolatedEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniIsolatedEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniIsolatedEc2InstanceSg"
    });

    sg.addIngressRule(this.publicEc2SecurityGroup, ec2.Port.allTraffic());

    return sg;
  }
}

S3バケットスタックの作成

ここでは、バケットを2つ作成することにしましたw。
1つはホスティング用のバケット、もうひとつは内部用のバケットです。

データをあとで入れるのがめんどくさかったので、都合上、@aws-s3-deploymentを使うことにしました。
以下のコマンドで適当なファイルを初期生成しています。

mkdir -p data/hosting
mkdir -p data/internal
echo "Hello MiniCdk!" > data/hosting/index.html
echo "This is Internal Bucket!" > data/internal/test.txt

S3バケットスタックを作成するソースコードは以下の通りです。

lib/mini-s3-stack.ts
import cdk = require("@aws-cdk/core");
import s3 = require("@aws-cdk/aws-s3");
import s3Deploy = require("@aws-cdk/aws-s3-deployment");

export class MiniS3BucketStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.setupInternalBucket();
    this.setupHostingBucket();
  }

  // 内部で使うバケットを作成する
  setupInternalBucket() {
    const bucket = new s3.Bucket(this, "InternalBucket");

    new s3Deploy.BucketDeployment(this, "DeployInternalBucket", {
      sources: [s3Deploy.Source.asset("./data/internal")],
      destinationBucket: bucket
    });
  }

  // ホスティング用に使うバケットを作成する
  setupHostingBucket() {
    const bucket = new s3.Bucket(this, "HostingBucket", {
      websiteIndexDocument: "index.html",
      publicReadAccess: true,
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

    new s3Deploy.BucketDeployment(this, "DeployHostingBucket", {
      sources: [s3Deploy.Source.asset("./data/hosting")],
      destinationBucket: bucket
    });
  }
}

エンドポイントの作成

ここまで作成したスタックのエンドポイントとなるファイルを作成します。

bin/mini-cdk.ts
#!/usr/bin/env node
import "source-map-support/register";
import cdk = require("@aws-cdk/core");
import { MiniVpcStack } from "../lib/mini-vpc-stack";
import { MiniEc2Stack } from "../lib/mini-ec2-stack";
import { MiniS3BucketStack } from "../lib/mini-s3-stack";

main();

function main() {
  const app = new cdk.App();

  // EC2インスタンスにアクセスするためのキーペアを指定する
  // マネジメントコンソールから事前にキーペアを作成しておいてください。
  const keyName = app.node.tryGetContext("keyName");

  // VPCスタック
  const miniVpcStack = new MiniVpcStack(app, "MiniVpcStack");

  // EC2スタック
  new MiniEc2Stack(app, "MiniEc2Stack", {
    vpc: miniVpcStack.vpc,
    keyName
  });

  // S3バケットスタック
  new MiniS3BucketStack(app, "MiniS3BucketStack");

  app.synth();
}

キーペアをデプロイ時に指定するので、マネジメントコンソールかCLIから作成しておいてください。
ここでは、mini-cdkという名前で作成しました。

スクリーンショット 2019-12-15 20.50.33.png

ビルドとデプロイ

ビルドしてjsファイルを生成します。

npm run build

デプロイ対象のスタック一覧を確認します。

cdk list

MiniS3BucketStack
MiniVpcStack
MiniEc2Stack

ここまでで準備が整ったので、スタックをまとめてデプロイして環境を構築します。

export AWS_DEFAULT_REGION=ap-northeast-1
cdk -c keyName=mini-cdk deploy *Stack --require-approval never --profile dev
  • -cオプションでコンテキストパラメータとしてキーペア名を指定しています。
  • --require-approval neverは[y/n]を手動で承認するのが手間だった(CodeBuildとかを使った自動化などを想定)ので付けています。
  • --profileは各自のものに読み替えてください。

構築したあとの確認作業

IsolatedサブネットのEC2インスタンスから確認します。

$ aws s3 ls --region ap-northeast-1
2019-12-15 12:03:53 minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr
2019-12-15 12:03:27 minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7

# 内部用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7/ --region ap-northeast-1
2019-12-15 12:04:52         24 test.txt

# ホスティング用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr/ --region ap-northeast-1
2019-12-15 12:04:52         15 index.html

他にもホスティングURLでバケットにアクセスできるか等確認がありますが、割愛させていただきます。

以上、CDKを使ったVPC/EC2/S3の構築でした。
次回は、Fargateについて書いてみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CDKを使ってVPC/EC2/S3バケットを作成する

AWS CDKを使った基本的な環境を構築したくなったので、記事を投稿してみました。
また、退職して少しの間ニートになったので、CDK関連の記事を今後投稿してみようと思ってます。

作成する環境構成

以下のような環境を構築するためのCDKソースコードを作成します。
基本的な構成なのですが、CDKでコーディングすると楽しいのでおすすめです。

SgF9dAJ.png

  • CDKバージョン: v1.18.0
  • TypeScript: v3.7.3

AWS CDKのインストール

sudo npm install -g aws-cdk
cdk ---version
1.18.0 (build bc924bc)
mkdir mini-cdk
cd mini-cdk
cdk init app --language=typescript
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-iam
# 作成するスタッックのファイル
touch lib/mini-vpc-stack.ts
touch lib/mini-ec2-stack.ts
touch lib/mini-s3-stack.ts

# 都合上削除
rm test/mini-cdk.test.ts

VPCスタックの作成

VPCスタックを作成します。
注意点として、利用するAZの数とNatGatewayに気を付けた方がよいです。natGateways: 0として明示しないと、AZごとにNatGatewayが作成されてしまいます。個人で使う分には利用コストが高くなってしまうので注意が必要です。

lib/mini-vpc-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");

export class MiniVpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCを作成する
    this.vpc = this.createVpc();

    // GatewayEndpointをセットアップする
    this.setupGatewayEndpoint();

    // InterfaceEndpointをセットアップする
    this.setupInterfaceEndpoint();
  }

  private createVpc() {
    return new ec2.Vpc(this, "MiniVpc", {
      cidr: "172.17.0.0/16",
      natGateways: 0, // NatGatewayはお金がかかるので使わない
      maxAzs: 1, // Availability Zoneの数
      subnetConfiguration: [
        { name: "Public", cidrMask: 20, subnetType: ec2.SubnetType.PUBLIC },
        { name: "Isolated", cidrMask: 20, subnetType: ec2.SubnetType.ISOLATED }
      ]
    });
  }
  private setupGatewayEndpoint() {
    // IsolatedのサブネットからS3に直接アクセスできるようにVPCエンドポイントを利用する
    this.vpc.addGatewayEndpoint("S3EndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });
  }

  private setupInterfaceEndpoint() {
    // InterafceEndpoint用のセキュリティグループ
    const interfaceEndpointSecurityGroup = new ec2.SecurityGroup(
      this,
      "InterfaceEndpointSecurityGroup",
      {
        securityGroupName: "InterfaceEndpointSecurityGroup",
        vpc: this.vpc
      }
    );
    interfaceEndpointSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443)
    );

    // Isolated SubnetからECRに接続するためのエンドポイント
    this.vpc.addInterfaceEndpoint("ECRInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: {
        subnetType: ec2.SubnetType.ISOLATED
      }
    });
    this.vpc.addInterfaceEndpoint("ECR_DockerInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
    });
  }
}

setupInterfaceEndpointメソッドでECRにアクセスするためのエンドポイントを作成していますが、今回は関係ありません。実装の参考情報として解釈してください。

EC2スタックの作成

Publicサブネットに1つ、Isolatedサブネットに1つインスタンスを作成するスタックを作成します。
IsolatedサブネットのEC2インスタンスからS3バケットにアクセスできるようにロールをアタッチしています。

lib/mini-ec2-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");
import iam = require("@aws-cdk/aws-iam");

interface MiniEc2StackProps extends cdk.StackProps {
  readonly vpc: ec2.Vpc; // MiniVpcStackのプロパティをvpcを受け取りたいので設定している
  readonly keyName: string; // キーペア
}

export class MiniEc2Stack extends cdk.Stack {
  private readonly vpc: ec2.Vpc;
  private readonly keyName: string;

  private publicEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2Role: iam.Role;

  constructor(scope: cdk.Construct, id: string, props: MiniEc2StackProps) {
    super(scope, id, props);

    this.vpc = props.vpc;
    this.keyName = props.keyName;

    // PublicSubnetのEC2インスタンスをセットアップする
    this.setupPublicEc2Instance();

    // IsolatedSubnetのEC2インスタンスをセットアップする
    this.setupIsolatedEc2Role();
    this.setupIsolatedEc2Instance();
  }

  // PublicSubnetにEC2インスタンスを作成する
  private setupPublicEc2Instance() {
    this.publicEc2SecurityGroup = this.createPublicEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniPublicEc2Instance", {
      vpc: this.vpc,
      instanceName: "MiniPublicEc2Instance",
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC
      },
      userData,
      securityGroup: this.publicEc2SecurityGroup
    });
  }

  // IsolatedSubnetにEC2インスタンスを作成する
  private setupIsolatedEc2Instance() {
    this.isolatedEc2SecurityGroup = this.createIsolatedEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniIsolatedEc2Instance", {
      instanceName: "MiniIsolatedEc2Instance",
      vpc: this.vpc,
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.ISOLATED
      },
      securityGroup: this.isolatedEc2SecurityGroup,
      role: this.isolatedEc2Role
    });
  }

  private setupIsolatedEc2Role() {
    this.isolatedEc2Role = new iam.Role(this, "IsolatedEc2InstanceRole", {
      roleName: "IsolatedEc2InstanceRole",
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com")
    });

    // ロールにS3へのアクセスするポリシーをアタッチする
    this.isolatedEc2Role.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket",
          "s3:ListAllMyBuckets"
        ],
        resources: ["*"]
      })
    );
  }

  // PublicサブネットのEC2インスタンスのセキュリティグループを作成する
  private createPublicEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniPublicEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniPublicEc2InstanceSg"
    });
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));
    return sg;
  }

  // IsolatedサブネットのEC2インスタンスのセキュリティグループを作成する
  private createIsolatedEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniIsolatedEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniIsolatedEc2InstanceSg"
    });

    sg.addIngressRule(this.publicEc2SecurityGroup, ec2.Port.allTraffic());

    return sg;
  }
}

S3バケットスタックの作成

ここでは、バケットを2つ作成することにしましたw。
1つはホスティング用のバケット、もうひとつは内部用のバケットです。

データをあとで入れるのがめんどくさかったので、都合上、@aws-s3-deploymentを使うことにしました。
以下のコマンドで適当なファイルを初期生成しています。

mkdir -p data/hosting
mkdir -p data/internal
echo "Hello MiniCdk!" > data/hosting/index.html
echo "This is Internal Bucket!" > data/internal/test.txt

S3バケットスタックを作成するソースコードは以下の通りです。

lib/mini-s3-stack.ts
import cdk = require("@aws-cdk/core");
import s3 = require("@aws-cdk/aws-s3");
import s3Deploy = require("@aws-cdk/aws-s3-deployment");

export class MiniS3BucketStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.setupInternalBucket();
    this.setupHostingBucket();
  }

  // 内部で使うバケットを作成する
  setupInternalBucket() {
    const bucket = new s3.Bucket(this, "InternalBucket");

    new s3Deploy.BucketDeployment(this, "DeployInternalBucket", {
      sources: [s3Deploy.Source.asset("./data/internal")],
      destinationBucket: bucket
    });
  }

  // ホスティング用に使うバケットを作成する
  setupHostingBucket() {
    const bucket = new s3.Bucket(this, "HostingBucket", {
      websiteIndexDocument: "index.html",
      publicReadAccess: true,
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

    new s3Deploy.BucketDeployment(this, "DeployHostingBucket", {
      sources: [s3Deploy.Source.asset("./data/hosting")],
      destinationBucket: bucket
    });
  }
}

エンドポイントの作成

ここまで作成したスタックのエンドポイントとなるファイルを作成します。

bin/mini-cdk.ts
#!/usr/bin/env node
import "source-map-support/register";
import cdk = require("@aws-cdk/core");
import { MiniVpcStack } from "../lib/mini-vpc-stack";
import { MiniEc2Stack } from "../lib/mini-ec2-stack";
import { MiniS3BucketStack } from "../lib/mini-s3-stack";

main();

function main() {
  const app = new cdk.App();

  // EC2インスタンスにアクセスするためのキーペアを指定する
  // マネジメントコンソールから事前にキーペアを作成しておいてください。
  const keyName = app.node.tryGetContext("keyName");

  // VPCスタック
  const miniVpcStack = new MiniVpcStack(app, "MiniVpcStack");

  // EC2スタック
  new MiniEc2Stack(app, "MiniEc2Stack", {
    vpc: miniVpcStack.vpc,
    keyName
  });

  // S3バケットスタック
  new MiniS3BucketStack(app, "MiniS3BucketStack");

  app.synth();
}

キーペアをデプロイ時に指定するので、マネジメントコンソールかCLIから作成しておいてください。
ここでは、mini-cdkという名前で作成しました。

スクリーンショット 2019-12-15 20.50.33.png

ビルドとデプロイ

ビルドしてjsファイルを生成します。

npm run build

デプロイ対象のスタック一覧を確認します。

cdk list

MiniS3BucketStack
MiniVpcStack
MiniEc2Stack

ここまでで準備が整ったので、スタックをまとめてデプロイして環境を構築します。

export AWS_DEFAULT_REGION=ap-northeast-1
cdk -c keyName=mini-cdk deploy *Stack --require-approval never --profile dev
  • -cオプションでコンテキストパラメータとしてキーペア名を指定しています。
  • --require-approval neverは[y/n]を手動で承認するのが手間だった(CodeBuildとかを使った自動化などを想定)ので付けています。
  • --profileは各自のものに読み替えてください。

構築したあとの確認作業

IsolatedサブネットのEC2インスタンスから確認します。

$ aws s3 ls --region ap-northeast-1
2019-12-15 12:03:53 minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr
2019-12-15 12:03:27 minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7

# 内部用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7/ --region ap-northeast-1
2019-12-15 12:04:52         24 test.txt

# ホスティング用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr/ --region ap-northeast-1
2019-12-15 12:04:52         15 index.html

他にもホスティングURLでバケットにアクセスできるか等確認がありますが、割愛させていただきます。

以上、CDKを使ったVPC/EC2/S3の構築でした。
次回は、Fargateについて書いてみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS re:Invent 2019で発表されたサービス

AWS re:Invent 2019

  • 公式サイト:re:Invent
  • 期間:2019/12/01 - 12/06
  • 場所:ラスベガス

発表されたサービス

コンピューティング

  • EC2
    • Inf1 インスタンス
      • 機械学習推論用
    • M6g / C6g / R6g インスタンス
      • Arm ベースのインスタンス
    • Image Builder
      • AMI 作成
  • Nitro
    • EC2 のディスクアクセス / ネットワークを高速化
  • ECS
    • Cluster Auto Scaling
    • キャパシティプロバイダー
  • Fargate
    • Fargate Spot
      • コスト削減
    • Kubernetes サポート
  • Compute Optimizer
    • 最適なリソースを利用しているかアドバイス
  • Outposts
    • オンプレに AWS を配置
    • 一般提供開始
  • Local Zone
    • 各地域に AWS を配置
    • カリフォルニアで提供開始
  • Wavelength
    • 5G ネットワークのエッジに配置
  • Lambda
    • Provisioned Concurrency

アナリティクス

  • Redshift
    • RA3 ノード
  • Federated Query
  • Elasticsearch Service
    • UltraWarm
  • Kinesis Video Stream
    • WebRTC による双方向通信
  • Outposts - EMR
    • オンプレで EMR が利用可能
  • Redshift data lake export
    • クエリ結果を S3 に保存

アプリケーション統合

  • Step Functions Express Workflows
  • EventBridge
    • スキーマレジストリ

SaaS

  • Chime
    • 参加者によって低遅延になるリージョンの自動選択
    • Chime Meetings App for Slack
      • Slack から直接 Chime を開始
  • Connect
    • Contact Lens
      • 機械学習によりお客様の感情、傾向等を分析

データベース

  • Managed Apache Cassandra
    • プレビュー
  • Outposts - RDS
    • オンプレで RDS が利用可能
  • RDS Proxy
    • データベースプロキシ
  • Neptune
    • Neptune
    • Jupyter ノートブックからクエリ

開発ツール

  • Builders' Library
    • Amazon.com ではどのようにやっているか
  • Amplify
    • iOS版、Android版をリリース

IoT

  • IoT Site Wise
    • プレビュー
    • 産業機器からのデータの大規模な収集、保存、整理、モニタリング
    • IoT Site Wise

機械学習

  • Rekognition
    • Custom Labels
      • 独自のラベルを付与
  • CodeGuru
    • コードレビュー自動化
  • DeepRacer
    • マルチカーレース
    • オブジェクト回避
  • Augmented AI(A2I)
    • 機械学習推論の人間によるレビューワークフローを追加
  • SageMaker
    • Processing
      • 前処理、後処理のマネージドサービス
    • Experiments
      • 学習の実験を作成、比較
    • Notebook Experience
      • プレビュー
      • ワンクリックで機械学習用 Notebook を起動
    • Studio
      • 機械学習向け IDE
      • 学習、デバッグ、デプロイ、モニタリング
    • Debugger
      • 機械学習の問題を分析、デバッグ、修正
    • Autopilot
      • 自動学習
    • Model Monitor
      • モデルを監視
      • データドリフトを検知
    • Operators for Kubernetes
      • Kubernetes 環境からモデルをトレーニング、調整、デプロイ
    • Deep Graph Library サポート
  • Deep Java Library
    • Java によるディープラーニングライブラリ
  • ディープラーニング AMI
    • 最新のフレームワークに対応
  • DeepComposer
    • 音楽作成
  • Fraud Detector
    • 不正アクティビティの検出
  • Kendra
    • マネージドエンタープライズサーチ
    • 社内ドキュメントを検索
  • Transcribe
    • Medical
      • 臨床医と患者の音声をテキストに変換

マネージメント / ガバナンス

  • License Manager
    • ライセンス使用状況を自動検出

Marketplace

  • Discovery API
    • Marketplace を検索できる API を提供
  • Seller Private Offer
    • 個別契約

マイグレーション

  • Windows Server 移行サポート

モバイル

  • Amplify DataStore
    • デバイスのオフラインストレージ
  • API Gateway HTTP API
    • コスト削減
    • シンプル化

量子コンピュータ

  • Braket
    • 量子コンピュータのアルゴリズムを構築、テスト、実行

ネットワーク

  • Transit Gateway
    • 異なるリージョン間でピアリングが可能に
    • VPC 間のマルチキャストルーティングが可能に
  • Transit Gateway Network Manager
    • AWS 全体とオンプレでネットワークを一元管理
  • Global Accelerator
    • VPN パフォーマンスを向上
  • VPC Ingress Routing
    • VPC で送受信されるトラフィックを仮想アプライアンスを通してリダイレクト

セキュリティ

  • Detective
    • セキュリティ問題や不正アクティビティの原因を分析、調査
  • IAM Access Analyzer
    • リソースへの意図したアクセスのみがポリシーで提供されているかを簡単に確認
  • Security Hub
    • IAM Access Analyzer と統合

ストレージ

  • S3 Access Points
    • S3 にアクセスポイントを追加
  • Access Analyzer for S3
    • アクセスポリシーによって S3 リソースへの意図したアクセスのみが提供されているかを簡単に確認
  • EBS Direct API
    • EBS スナップショットコンテンツへのアクセス
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCIを利用してAWS SAMで自動デプロイする(Python)

概要

AWS SAM + Pythonで開発したサーバーレスアプリケーションをCircleCIからデプロイする機会があったので、解説したいと思います。

下記の記事を参考にさせて頂きましたが、今回の設定では一部修正しないとうまく動作しない部分がありましたので、その点について記載したいと思います。
CircleCI と GitHub で AWS SAM のサーバーレスアプリを自動デプロイしてみた (開発環境 & 本番環境) | Developers.IO

Pythonでの経験が少ないため、間違った記載がありましたらご指摘いただけると嬉しいです?‍♀️
普段はAWS SAM + Goを利用することが多いです。

ソースコード

https://github.com/kobayashi-m42/aws-sam-circleci-deploy-sample

解説

CircleCIの設定ファイルはこんな感じで作成しました。

実行タイプにmachine(VM)を選択し、pipenvを利用した仮装環境でビルドとデプロイを行なっています。

.circleci/config.yml
deploy_template: &deploy_template
  machine: true
  working_directory: ~/project/app
  steps:
    - checkout
    - run:
        name: install python 3.6.0
        command: |
          pyenv install 3.6.0
          pyenv global 3.6.0
    - run:
        name: sam build
        command: |
          pip install pipenv
          python -m venv venv
          source venv/bin/activate

          pipenv install

          make build
    - run:
        name: deploy AWS Key
        command: |
          source venv/bin/activate

          make package
          make deploy

version: 2.1

jobs:
  deploy-dev:
    <<: *deploy_template
  deploy-prd:
    <<: *deploy_template

workflows:
  version: 2.1
  aws-sam-deploy:
    jobs:
      - deploy-dev:
          context: aws-dev
          filters:
            branches:
              only:
                - /develop/
      - deploy-prd:
          context: aws-prd
          filters:
            branches:
              only: /master/

Python仮装環境の構築

pipenvを利用して仮装環境を構築します。

pipenvについては下記の記事が参考になりましたので、掲載させて頂きます。
Pipenvを使ったPython開発まとめ

仮装環境にAWS CLIとAWS SAM CLIを追加します。
CircleCIでもこの仮装環境を利用してAWS SAMのビルドとデプロイを行います。

$ pipenv install awscli
$ pipenv install aws-sam-cli

ビルド

sam buildコマンドを利用して、ビルドを行います。
sam build - AWS Serverless Application Model

sam build --template template.yaml --use-container

今回は、オプションに--use-containerを指定します。
このオプションを指定することで、Lambdaの動作環境と同等のコンテナ内でビルドすることができます。

サンプルのソースコードには追加していませんが、Curatorを利用する際に--use-containerオプションを追加しなかった場合、ビルドでエラーとなりました。

AWS SAM CLI、SAM ビルドコマンドを導入

関数がネイティブでコンパイルされたプログラムを持つパッケージに依存している場合は、SAM ビルドコマンドに --use-container フラグを指定することもできます。この --use-container フラグは、Lambda のような環境でローカルに関数をコンパイルするので、それらをクラウドにデプロイするときには正しいフォーマットになります。

パッケージ&デプロイ

sam packagesam deployコマンドを利用してデプロイを行います。
AWS SAMをデプロイするためには、事前にS3バケットを用意しておく必要があります。

CircleCIの設定ファイル

ポイントは、下記の2点です。

  • 実行タイプにmachine(VM)を選択
  • コンテキストの使用して複数のAWSアカウントにデプロイを行う

詳細はconfig.ymlをご確認ください。

実行タイプにmachine(VM)を選択

sam build--use-containerオプションを追加しているため、コンテナ内でビルドが実行されます。

実行タイプにDockerを指定し、setup_remote_dockerを設定することで、CircleCIでコンテナを使用することができると思ったのですが、ファイルをマウントすることができずにビルドが失敗してしまいました。そのため、machineを指定しています。

参考:Docker コマンドの実行手順 - CircleCI

複数のAWSアカウントにデプロイを行う

開発環境を本番環境でAWSアカウントを分けているケースが多いと思います。
今回はCircleCIのContexts機能を利用することで、両方のアカウントにデプロイできるように設定を行なっています。

こちらの記事を参考にさせて頂きました。
CircleCIで複数のAWSアカウントを扱う方法

参考記事

CircleCI と GitHub で AWS SAM のサーバーレスアプリを自動デプロイしてみた (開発環境 & 本番環境) | Developers.IO
Pipenvを使ったPython開発まとめ
CircleCIで複数のAWSアカウントを扱う方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Actions上でTerraform CI/CD環境を構築する

はじめに

Github Actionsの正式版がリリースされて1ヶ月ほど経ちました。中々さわれてなかったのですが、TerraformのCI/CD環境構築の調査で動かしたのでその時のまとめです。

サンプルプロジェクト

とりあえず試すだけであれば、Terraform公式が用意しているTerraform GitHub Actionsを使えばすぐに終わってしまいます。
それだけだと面白くないのでなるべく実際の運用を想定して動かしていきます。とはいえあらゆるパターンを試すのは難しいので以下のようなサンプルプロジェクトを仮定します。

  • providerはAWS
  • moduleを使う
  • workspaceを使う
  • リポジトリ内に複数のmain.tfがある

ディレクトリ構成は以下。(ファイルの中身は本題ではないので割愛)

├── modules
│   ├── ec2
│   │   ├── main.tf
│   │   └── variables.tf
│   └── iam-role
│       ├── main.tf
│       └── variables.tf
├── service1
│   ├── ec2
│   │   ├── main.tf
│   │   └── variable.tf
│   └── iam-role
│       ├── main.tf
│       └── variable.tf
└── service2
    ├── ec2
    │   ├── main.tf
    │   └── variable.tf
    └── iam-role
        ├── main.tf
        └── variable.tf

ゴール

上に書いた構成のサンプルに対して以下の1〜4を行う。

  1. masterブランチへのプルリクエスト作成をトリガーに以下の3つ(以降、自動テストと呼ぶ)を実行する。
    • terraform fmt
    • terraform validate
    • terraform plan
  2. 一度のプルリクエストで複数のworkspaceに対してまとめて自動テストを実行する。
  3. masterブランチへのプッシュ(マージ)をトリガーに自動テストとterraform applyを実行する。自動テストのwrokflowが成功した場合のみapplyを実行する。
  4. 自動テストおよびapplyは毎回リポジトリ内の全てのmain.tfに対して実行するのではなく、変更があったファイルに関係するmain.tfに対して実行する。

1. masterブランチへのPR作成をトリガーに自動テストを実行

workflowの作成

早速workflowを作成していきますが、Terraformの公式が用意してくれているTerraform GitHub Actionsを使っていきます。
以下の公式のサンプルを修正していく形で進めます。

.guthub/workflows/terraform.yml
name: 'Terraform GitHub Actions'
on:
  - pull_request
jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@master
      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'fmt'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'init'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'validate'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'plan'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

AWSのcredential

AWSのリソースを扱うためのcredentialをsecretsから取得するようにします。secrets.xxx でGitHub上で設定したSecretsの情報できます。
GitHub側の設定はリポジトリの Settings > Secrets で設定してあげればOKです。
スクリーンショット 2019-12-15 11.39.44.png

stepsの中にあったenvを一つ上の階層に移動させ、そこにAWSのクレデンシャルの情報を設定しています。Checkoutのstepでは使用しませんが、何度も書くのも冗長なので上に持ってきています。

.guthub/workflows/terraform.yml
name: 'Terraform GitHub Actions'
on:
  - pull_request
jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    env: # 追加
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 移動
      AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }} # 追加
      AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }} # 追加
    steps:
      - name: 'Checkout'
        uses: actions/checkout@master
      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'fmt'
      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'init'
      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'validate'
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'plan'

workspaceを指定する

まずはdevというworkspaceで動くようにしてみます。 Terraform GitHub ActionsではTF_WORKSPACEという環境変数でworspaceを指定できるのでenvに追加します。

.guthub/workflows/terraform.yml(一部抜粋)
    env:
      TF_WORKSPACE: dev # 追加

実行するディレクトリを指定する

main.tfが複数に別れているので、どのmain.tfがあるディレクトリで実行するかを指定します。
Github Actionsに準備されているstrategy.matrixを指定してあげれば簡単に複数の設定に対してjobを実行することができます。
今回のサンプルだと4つmain.tfがあるのでそれぞれstrategy.matrixを指定してそれをtf_actions_working_dirで渡します。

.guthub/workflows/terraform.yml(一部抜粋)
jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    strategy: # 追加 strategyのmatrixでworkspace設定。この組み合わせの数だけjobが実行される
      matrix:
        workdir: [./service1/ec2, ./service2/ec2, ./service1/iam-role, ./service2/iam-role]
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      TF_WORKSPACE: dev
    steps:
      - name: 'Checkout'
        uses: actions/checkout@master
      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'fmt'
          tf_actions_working_dir: ${{ matrix.workdir }} # 追加
      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'init'
          tf_actions_working_dir: ${{ matrix.workdir }} # 追加
      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'validate'
          tf_actions_working_dir: ${{ matrix.workdir }} # 追加
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'plan'
          tf_actions_working_dir: ${{ matrix.workdir }} # 追加

masterへのプルリクエストをトリガーにする

以下のようにbranchesのフィルターを設定すればOK。(以下の設定の場合はPR作成時以外にもプルリクエストを作成したブランチにプッシュした場合などいくつかのイベントでトリガーされます。)

.guthub/workflows/terraform.yml(一部抜粋)
name: 'Terraform GitHub Actions'
on:
  pull_request:
    branches:
      - master

動作確認

ここまでの設定で動作を確認してみます。masterブランチに対してプルリクエストを作成すると、無事にActionsが実行されるはずです。strategy.matrixで指定したディレクトリの数だけjobが実行されているのがわかります。

スクリーンショット 2019-12-15 12.23.01.png

2. 一度のプルリクエストで複数のworkspaceに対してまとめて自動テストを実行する

実行ディレクトリを指定した時と同じようにstrategy.matrixを使えば簡単に実現できます。
devとprodの二つのworkspaceがあると想定して以下のように設定します。

.guthub/workflows/terraform.yml(一部抜粋)
terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    strategy:
      matrix: 
        env: [dev, prod] # 追加
        workdir: [./service1/ec2, ./service2/ec2, ./service1/iam-role, ./service2/iam-role]
    env:
      TF_WORKSPACE: ${{ matrix.env }} # matrixから値を取得するように修正

動作確認

ここまでの設定で動作確認します。masterブランチに対してプルリクエストを作成すると今度は 4(workdir) × 2(env) = 8のjobが動きます。

スクリーンショット 2019-12-15 14.33.25.png

3. masterブランチへのプッシュ(マージ)をトリガーに自動テストとapplyを実行する

masterブランチへのプッシュ(マージ)をトリガーに実行

pull_requestと同じ階層にpushを追加してbranchesにmasterブランチを指定します。
これによりmasterブランチへのプルリクエスト or masterブランチへのプッシュの条件でjobが実行されるようになります。

.guthub/workflows/terraform.yml(一部抜粋)
name: 'Terraform GitHub Actions'
on:
  pull_request:
    branches:
      - master
  push: # 追加
    branches:
      - master

今回は一つのymlで定義しましたが、プルリクエストの場合とプッシュの場合でymlを複数に分けてしまうのもありだと思います。

apply用のstepを追加

applyも他と同様にTerraform GitHub Actionsに用意されているのでそれを使います。
ただしapplyはmasterへ修正が反映された時のみ動いてほしいので if で条件を追加しています。github contextでjobを実行のトリガーとなったイベント情報などを取得できるので、masterブランチへのイベントの場合のみ実行されるようにします。

.guthub/workflows/terraform.yml(一部抜粋)
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'plan'
          tf_actions_working_dir: ${{ matrix.workdir }}
      - name: 'Terraform Apply' # ここから下を追加
        if: github.ref == 'refs/heads/master'  # プルリクエストの作成では動かないように
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.17
          tf_actions_subcommand: 'apply'
          tf_actions_working_dir: ${{ matrix.workdir }}

planまでのstepが成功した場合のみapplyを実行

デフォルトで前のstepが失敗したは次のstepは実行されないため特に設定しなくてもplanが成功した時のみapplyが実行されます。

動作確認

ここまでの設定で動作確認します。プルリクエストを作成してマージするとapplyまで実行されています。
スクリーンショット 2019-12-15 15.45.18.png

4. ディレクトリ内のファイルが修正されたmain.tfに対してのみjobを実行する

ここまででプルエスト作成でplanを実行してmasterブランチをマージしてapplyするという流れができましたが、実際の運用を想定した時に以下のような課題で出てきそうです。

  • main.tfの数やworkspaceのが増えると一度の修正で実行されるjobの数が増え、jobの並列実行数が増加し続けGitHub Actionsの上限までいってしまう。
  • 修正と関係ないmain.tfに対してもplan、applyが実行されるので、修正内容に関わらず一番実行時間が長いjobの分だけ時間がかかってしまう。

これらを解決するためにディレクトリ内の修正されたファイルに関連するjobのみ実行するような設定をしていきます。

pathsフィルタの設定

pathsフィルタを使うことで特定のディレクトリやファイルに変更が合った場合にjobを実行することができます。例えば以下のように設定するとmasterブランチへのプルリクエスト かつ service1ディレクトリとmodulesディレクトリ内に変更が合った場合にjobが実行されます。

.guthub/workflows/terraform.yml(一部抜粋)
name: 'Terraform GitHub Actions'
on:
  pull_request:
    branches:
      - master
    paths: # 追加
      - service1/**
      - modules/**

ちなみに差分をどのように検知しているかは、プルリクエストやすでにあるブランチへのプッシュ、新規ブランチへのプッシュによって以下のように動作するようです。

on

Pull requests: Three-dot diffs are a comparison between the most recent version of the topic branch and the commit where the topic branch was last synced with the base branch.
Pushes to existing branches: A two-dot diff compares the head and base SHAs directly with each other.
Pushes to new branches: A two-dot diff against the parent of the ancestor of the deepest commit pushed.

pathsフィルタを利用して

  • ./service1ディレクトリ以下のファイルと./modulesディレクトリ以下のファイルに変更がある場合、./service1以下のmain.tfを実行する
  • ./service2ディレクトリ以下のファイルと./modulesディレクトリ以下のファイルに変更がある場合、./service2以下のmain.tfを実行する

のように動作させるため元のterrafrom.ymlを以下のように二つに分割していきます。

.github/workflows/service1.yml
name: 'Terraform Service1'
on:
  pull_request:
    branches:
      - master
    paths: # service1に関係する変更のみを検知
      - service1/**
      - modules/**
  push:
    branches:
      - master
    paths: # service1に関係する変更のみを検知
      - service1/**
      - modules/**
jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [dev, prod]
        workdir: [./service1/ec2, ./service1/iam-role] # service1のディレクトリのみを指定
    # env以下は元のterraform.ymlと同じ
.github/workflows/service2.yml
name: 'Terraform Service2'
on:
  pull_request:
    branches:
      - master
    paths: # service2に関係する変更のみを検知
      - service2/**
      - modules/**
  push:
    branches:
      - master
    paths: # service2に関係する変更のみを検知
      - service1/**
      - modules/**
jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [dev, prod]
        workdir: [./service2/ec2, ./service2/iam-role] # service1のディレクトリのみを指定
    # env以下は元のterraform.ymlと同じ

各main.tfの依存関係などを考慮し出すと複雑になりそうなど課題はありますが、pathsフィルタを使えば修正ファイルに応じたjobを実行できそうです。

動作確認

修正後の状態(service1.ymlservice2.yml)で試しにservice1/ec2/variable.tfのみ修正してプルリクエストを作成すると./service1ディレクトリ内のmain.tfに関するjobが実行されることが確認できました。

スクリーンショット 2019-12-15 17.31.27.png

まとめ

簡単なTerraformリポジトリを想定してGitHub Actions上でCI/CDを作り、どんな設定ができるか試していきました。
初めてGitHub Actionsを使いましたがドキュメントもしっかり書かれていてつまることも少なく進めらかなり使いやすいと感じました。他にも以下のあたりがいい感じでした。

  • GitHub上のサービスだけあって、GitHubのコンテキストの情報を簡単に取得できる
  • 無料で並列実行までできるのすごい
  • maxrixによる並列実行がかなり便利
  • pathsフィルタがかなり便利

個人的にはapplyする時はCircle CIで用意されているManual Approvalのような手動で実行を承認する機能が欲しいところですが、要望も多そうなのでそのうち追加されるのではと思っています。

今回想定したTerraformリポジトリはシンプルなので、実運用にそのまま使えるかという難しいかもしれませんが、どんなことができそうかイメージをつけられたのがよかったです。

参考

GitHub Actionsドキュメント
terraform-github-actionsドキュメント
terraform-github-actions

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Personalize/Forecastハンズオンの私的補講

  • Amazon Personalize
  • Amazon Forecast

こちらの2つのサービスはAWSで提供されている機械学習サービスで、機械学習に詳しくなくても簡単に始められることが特徴です。
この2つのサービスを体験するためのハンズオン資料がAWSより公開されています。
https://pages.awscloud.com/event_JAPAN_Hands-on-Amazon-Personalize-Forecast-2019.html

先日こちらのハンズオンを資料を元に実際にやってみて、個人的に気になった箇所などを補足としてまとめてみます。

Amazon Personalize

ソリューションのレシピ(学習アルゴリズム)

  • Automatic (AutoML)
    • 適切なアルゴリズムをPersonalizeが判断して採用する
    • 指定したレシピリストの中から選択する設定も可能
      • 現状はHRNNとHRNN-Metadataの2択?今後レシピが増えると選択肢が増すかも
  • HRNN
    • Hierarchical Recurrent Neural Networks
    • UserとItemの関係(Interactions)のみを利用
    • 時間経過で重み付けされる
  • HRNN-Metadata
    • Interactionsの他にUsersやItemsの属性情報を利用
    • datasetに少なくともUsersかItemsのどちらかをインポートしておかないと選択出来ない
  • HRNN-ColdStart
    • 頻繁にItemsやInteractionsを追加する場合に利用
    • 関連性の低いItemやInteractionを除外したサブセットを作成することで高速に新しいItemのレコメンドが可能になる
    • datasetにItemsをインポートしておかないと選択出来ない
  • Popularity Count
    • 人気のItemをそのまま返す
    • 全てのユーザに同じ結果を返す
  • Personalized Ranking
    • インプットとしてItemのリストを渡すと、ユーザごとに最適化されたランキングを返す
  • SIMS
    • Itemの協調フィルタリングとして動作する
    • インプットとしてItemを渡すと類似のItemを返す

レシピごとのハイパーパラメータの詳細についてはドキュメントを参照
参考: HRNN レシピ

APIでの推論実行

レコメンドの実行はAWS APIで実行出来ます。
現在のところHTTPエンドポイントとしてデプロイする機能などはなさそうですので、リアルタイム推論のAPIを構築したい場合はServerlessAPIなどでLambdaなどから呼び出す形がよさそうです。
Personalizeのリアルタイム実行コマンドはpersonalize-runtimeで、推論のサブコマンドはget-recommendations、ランキングのサブコマンドはget-personalized-rankingです。

HRNN推論

AWS CLIでリクエスト実行

aws personalize-runtime get-recommendations \
 --campaign-arn arn:aws:personalize:ap-northeast-1:xxxxxxx:campaign/my-campaign \
 --user-id 1 \
 --num-results 3

レスポンス

{
    "itemList": [
        {
            "itemId": "163"
        },
        {
            "itemId": "2355"
        },
        {
            "itemId": "353"
        }
    ]
}

SIMS

AWS CLIでリクエスト実行

aws personalize-runtime get-recommendations \
 --campaign-arn arn:aws:personalize:ap-northeast-1:xxxxxxx:campaign/sims-campaign \
 --item-id 163 \
 --num-results 3

レスポンス

{
    "itemList": [
        {
            "itemId": "198"
        },
        {
            "itemId": "22"
        },
        {
            "itemId": "173"
        }
    ]
}

Personalized Ranking

AWS CLIでリクエスト実行

aws personalize-runtime get-personalized-ranking \
 --campaign-arn arn:aws:personalize:ap-northeast-1:xxxxxxx:campaign/ranking-campaign \
 --user-id 1 \
 --input-list 22 192 239

レスポンス

{
    "personalizedRanking": [
        {
            "itemId": "22"
        },
        {
            "itemId": "239"
        },
        {
            "itemId": "192"
        }
    ]
}

料金

  • S3

0.023USD/GB/mo

少額なので基本気にする額ではないですが、データセットが大きい場合はそれなりに掛かってくる可能性もあります。
不要であればハンズオン終了時に削除は忘れずに。

  • Amazon Personalize

サインアップ直後であれば無料期間が適用される可能性があります。
継続的に課金されそうなのはデータストレージ料金くらいのようです。
エンドポイント維持費用などは無いようなので慌てて削除する必要もなさそうですが、こちらもハンズオンが終わり次第ひととおり削除しておくのが無難です。

以下は2019年12月現在の料金抜粋です。
ハンズオンを超えて利用する場合は必ず公式を確認してください。

料金
データ取り込み 0.05 USD/GB
トレーニング時間 0.24 USD/トレーニング時間
推論実行(リアルタイム) 0.20 USD/TPS時間
推論実行(バッチ) 0.067 USD/レコメンデーション1000件

参考: 料金表

トレーニング時間

4v CPU と 8 GiB メモリを使用する 1 時間のコンピューティング能力
実際のインスタンスタイプはデータ等に応じてPersonalizeの方で自動選択するため、実際のトレーニング実行時間とは異なる

TPS時間

TPS = 1秒あたりのリクエストの平均 (5分ごとの平均)
TPS時間 = 最小プロビジョンドTPS、または実際のTPSのどちらか大きい値(1時間単位での平均)

バッチ推論

UserごとのPersonalizeやRankingの場合はUser数に対してカウント、SIMSなどのItemの場合はItem数に対してカウント

Amazon Forecast

サンプルデータの注意点

ハンズオン資料のsmallデータを利用しましたが、このsmallデータには時系列データが2015-01-01 00:00:00の分まで存在します。
資料では予測フェーズでのLookup forecastの際に、予測範囲の開始指定が以下のように記述されていますが、

Start: 2015/01/01 0時0分
(インポートしたデータ
2014/12/31 23時の次)

実際にインポートしたデータとは1時間のズレがあります。
ConsoleからLookup forecastを実行する分には問題なく実行できていますが、実際に生成される予測値は2015-01-01 01:00:00からのデータとなっています。
AWS APIからこの範囲で実行した場合は、指定範囲エラーとなり実行できません。
start-dateは2015-01-01 01:00:00を指定する必要があります。
ちなみにコンソールで開始時刻を2014-12-31 23:00:00を指定して実行すると以下のようになります。

forecast.png

Consoleでは2015-01-01 00:00:00までの実データと、2015-01-01 01:00:00からの予測データが合わせて表示されていることが分かります。

Predictorのアルゴリズム

  • Automatic (AutoML)
    • 適切なアルゴリズムをForecastが判断して採用する
    • Personalizeにあったような選択肢機能はなさそう
  • ETS
    • 指数平滑法
    • 季節性のあるデータに強い
    • 時間の経過とともに重みが減少する
  • ARIMA
    • 自己回帰和分移動平均
    • 季節性、定常性のあるデータに強い
    • 時間経過に依存しない
  • DeepAR+
    • RNNを利用した学習アルゴリズム
    • ARIMAやETSのような比較的シンプルな統計アルゴリズムよりも複雑な予測が可能
  • NPTS
    • ノンパラメトリック時系列アルゴリズム
    • 時系列データが断続的であったりスパースなデータの場合に強い
  • Prophet
    • 局所的なベイズ構造時系列モデル
    • 複数の強い季節性を持つ場合や大きな外れ値がある場合に強い

アルゴリズムごとの詳しい特性やハイパーパラメータの詳細についてはドキュメントを参照
参考: ETS

APIでの予測実行

予測実行のコマンドはforecastquery、サブコマンドはquery-forecastです。

AWS CLIでリクエスト実行

aws forecastquery query-forecast \
 --forecast-arn arn:aws:forecast:ap-northeast-1:xxxxxxx:forecast/test_forecast \
 --start-date 2015-01-01T01:00:00Z \
 --end-date 2015-01-01T02:00:00Z \
 --filters "{\"item_id\":\"client_1\"}"

レスポンス

{
    "Forecast": {
        "Predictions": {
            "p10": [
                {
                    "Timestamp": "2015-01-01T01:00:00",
                    "Value": 18.008272171020508
                },
                {
                    "Timestamp": "2015-01-01T02:00:00",
                    "Value": 16.235431671142578
                }
            ],
            "p50": [
                {
                    "Timestamp": "2015-01-01T01:00:00",
                    "Value": 19.338420867919922
                },
                {
                    "Timestamp": "2015-01-01T02:00:00",
                    "Value": 17.989978790283203
                }
            ],
            "p90": [
                {
                    "Timestamp": "2015-01-01T01:00:00",
                    "Value": 20.668569564819336
                },
                {
                    "Timestamp": "2015-01-01T02:00:00",
                    "Value": 19.744525909423828
                }
            ]
        }
    }
}

料金

こちらも初めてForecastを使う場合は無料範囲で利用できる可能性があります。

料金
データ取り込み 0.088 USD/GB
トレーニング時間 0.24 USD/トレーニング時間
予測生成 0.60 USD/予測1000件

トレーニング時間

内部で並列に学習が行われる場合などもあるため、実際の実行時間とは異なる
Predictorの作成とForecastの作成どちらもこの価格が適用される

予測

予測件数は、予測期間に依らず、1つの時系列で1件
予測を生成したユーザの数と、作成した分位点(Forecast types)に依存する
Forecast typesは、デフォルトでP10,P50,P90の3分位点
5ユーザに対してデフォルトの3分位点で予測を生成した場合、予測件数は15件

こちらもストレージ以外に維持費用がかかるものはなさそうですが、不要であればハンズオン終了時点で忘れずに一通り削除しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CLIのコマンド補完する設定

Zshに設定する

パスを確認する

$ which aws_completer 
/usr/local/bin/aws_completer

.zshrcに以下追加する

source '/usr/local/bin/aws_zsh_completer.sh'

.zshrcの内容を反映する。

source ~/.zshrc

Bashに設定する

パスを確認する。

$ which aws_bash_completer
/usr/local/bin/aws_bash_completer

.bashrcに以下追加する。

complete -C '/usr/local/aws/bin/aws_completer' aws

.bashrcの内容を反映する。

source ~/.bashrc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ECSでTCPソケットエラー 事象と対応

ECS + Nginx,GunicornでTCPソケットエラー

  • Jmeterで負荷をかけると503 Bad Gatewayが多発した
  • どうやらECSには以下の制約があるそう。

net.core.somaxconn=128
現時点では Fargate では変更できないので、タスクを多く起動して頂くことで解決して下さい。
【週刊 Ask An Expert #04】AWS Loft Tokyo で受けた質問まとめ #AWSLoft

  • 他にもDBのチューニングも考慮した方が良い

ディスクI/O
RDSのパフォーマンスインサイトでIndexの分析
  パフォーマンスインサイトではIndexの使用率やクエリの結果が確認可能

対応方法の検討

1 ECSを予め増やしておく
一番ナンセンスだが、即時で対応が可能。
ELB + ECSの環境であれば、最小コンテナ数と最大コンテナ数の設定を変えるだけで済む。必要に応じて、ELBのPre-Warmingをすること!

2 ELBでのコネクション数を閾値にコンテナを動的に増やす
ELBのActiveConnectionCountを閾値にして、コンテナ数を増やす。
ECSのポリシーの追加-アラームを追加で設定できる

3 スケジューリングでECSをオートスケールする
利用開始時間の前にECSをスケールアウトさせておく。
アプリケーションオートスケーリング-lambda-コンテナの数

余談

EC2の監視間隔はデフォルトで5分間隔だが、ECSはデフォルト1分間隔で監視できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Cloudwatchチートシート

アラームを評価する

  • [期間]アラームの各データポイントを作成するためにメトリクスや式を評価する期間です。これは秒単位で表されます。期間として 1 分を選択した場合、1 分ごとに 1 つのデータポイントが存在します。

  • [評価期間] は、アラームの状態を決定するまでに要する直近の期間 (データポイント) の数です。

  • [Datapoints to Alarm (アラームを発生させるデータポイント数)] は、アラームが ALARM 状態に移るためにしきい値を超える必要がある評価期間内のデータポイントの数です。しきい値を超えたデータポイントは連続している必要はありませんが、すべてが [評価期間] に相当する直近のデータポイント数に含まれている必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

典型的なwebサービスのawsを使ったインフラ構成を考えてみるS3, ECS

概要

フロントエンドをVueを使って作り、APIなどのバックエンドをpythonのFlaskという軽量なフレームワークを使って作りました。今回は、ローカル環境下で動いていたアプリケーションをgithubから自動的にAWSの方にdeployすることができるようなインフラ構成を設計してみます。

不十分な点などありましたら、アドバイスいただけると幸いです。

使うもの

AWS関連

  • EC2
  • RDS
  • ECR
  • ECS(fargate)
  • S3
  • CloudFront
  • Route53
  • IAM

CI/CD関連

  • Circle CI

システム構成図

インフラ のコピー-Page-1.png

まず、public subnetとprivate subnetを持つVPCを作成します。外部からのアクセスを許容するpublic subnetには、ロードバランサーと踏み台(ログイン)サーバーを設置します。外部から直接アクセスできないprivate subnetには、APIサーバーとデータベースを置きます。

次に、API serverであるFlaskアプリとproxy serverとして利用するNginxは、コンテナ化して、ECS(Forgate)で管理します。Forgateを使うことで、自動でコンテナのスケーリングや再起動などをしてくれます。RDSは、AWSのamazon Auroraを利用します。こちらも、定期的にレプリカを作成してくれるので、万が一データを損失する場合やデータベースにアクセスできなくなった場合にも安心です。amazon Auroraにアクセスできるのは、(APIサーバーと)踏み台サーバーのEC2からのみで、外部からデータベースをいじることはできません。Login serverのEC2のインスタンスは、秘密鍵が保存されているローカルPCからのみアクセスできます。

フロントエンドのコードは、S3にデプロイし、CloudFront経由で配信します。あとは、Route53でのドメイン設定や、Certificate Managerでの証明書取得、ロードバランサーの設置など、細々した設定をしてあげると完成です。

CI/CD関連

インフラ のコピー-Page-2 (1).png

Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。

config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@0.0.2
  aws-ecs: circleci/aws-ecs@0.0.3
workflows:
  build-and-deploy:
    jobs:
      - aws-ecr/build_and_push_image:
          account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          region: ${AWS_DEFAULT_REGION}
          tag: "${CIRCLE_SHA1}"
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build_and_push_image
          aws-region: ${AWS_DEFAULT_REGION}
          family: "${AWS_RESOURCE_NAME_PREFIX}-service"
          cluster-name: "${AWS_RESOURCE_NAME_PREFIX}-cluster"
          container-image-name-updates: "container=${AWS_RESOURCE_NAME_PREFIX}-service,tag=${CIRCLE_SHA1}"

上記.circleci/config.ymlをgithubにpushするとcircleCIの設定が適応されます。CircleCIで定めた環境変数は、CircleCIのダッシュボードのEnvironment Variables
から設定します(後述)。

S3へのdeploy

  • IAMでS3へのアップロード用のuserを作り、AmazonS3FullAccess権限を与える
  • aws cliからuploadするscriptsを書く

こちらに関しては、たくさん説明記事があるので、細かい内容は省略します。
参考: Amazon S3でSPAをサクッと公開する

インフラ構築までの流れ

VPCの作成

  • IPv4 CIDR 10.2.0.0/16

subnetの作成

  • IPv4 CIDR 10.2.0.0/20(public-subnet-a)
  • IPv4 CIDR 10.2.16.0/20(public-subnet-c)
  • IPv4 CIDR 10.2.32.0/20(private-subnet-a)
  • IPv4 CIDR 10.2.48.0/20(private-subnet-c)

アベイラビリティゾーンA, Cに2つずつ、public subnetと private subnetを設置します。CIDRの設定に気をつけてください。

Internet Gatewayの生成

  • Internet Gatewayを生成して、先ほど生成したVPCにアタッチする
  • 2つのpublic subnetをInternetGWに紐付ける

NAT Gatewayの作成

  • NAT Gatewayを作成して、public subnetの一方にアタッチする
  • これにより、private subnetへアクセスできるようになる

デフォルトでは、subnetを生成するとプライベートルートテーブルが選択されます。他の使用可能なルートテーブルを生成し、送信先 0.0.0.0/0 がインターネットゲートウェイ (igw-xxxxxxxx) にルーティングされるようにします。その後、public subnetとそのルートテーブルを紐付けます。

※VPCとsubnet,Internet GWなどの詳細な設定方法は公式ドキュメントを参照してください。

ECRでレポジトリを作成

  • circleciに権限を付与する
    IAMでcircleciによるdeploy用のuserを作成します。
    今回は、ECR/ECSに関する権限を付与します。

    • AmazonEC2ContainerRegistryFullAccess
    • AWSCodeDeployRoleForECS
    • AmazonEC2ContainerServiceFullAccess
    • AmazonECSTaskExecutionRolePolicy
    • AWSDeepRacerCloudFormationAccessPolicy
  • Environment Variablesの設定

    • AWS_ACCOUNT_ID(ex:754569708956)
    • AWS_DEFAULT_REGION(ex:ap-northeast-1)
    • AWS_RESOURCE_NAME_PREFIX(ex:flask-app)
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY

AWS_RESOURCE_NAME_PREFIXは、ECRのレポジトリの名前と同じにしないとエラーが出ます

nginxコンテナをアップロード

  • こちらは、頻繁に変更しないと思うので、circleCIには含めず手動で行う
  • 詳細は公式ページを参照のこと
  • confファイルで、nginxはproxy serverとしての設定する
ディレクトリ構成
nginx
├── Dockerfile
└── conf
    └── default.conf
Dockerfile
FROM nginx
COPY conf/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT nginx -g 'daemon off;'
default.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        #root   /usr/share/nginx/html;
        #index  index.html index.htm;
        proxy_pass      http://127.0.0.1:5000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

ECS Fargateの作成

  • fargateでclusterを作成する VPCは先ほど作った物を使いますので、ここで新しく作成する必要はありません。
  • fargateでtaskを作成する
    タスク実行ロールは、ecsTaskExecutionRoleを指定します。ECRで登録したdocker imageのURLを貼り付けます。コンテナのportを公開するのを忘れないようにしましょう(flask container port:5000)。

  • fargateでserviceを作成する

    • subnetは先ほど作ったprivate subnetを二つ割り当てます
    • fargate service用のsecurity groupを作成する(port:80)
    • EC2でロードバランサーを作成する
    • ELB用のsecurity groupを作成する(port:80)
    • target groupを作成する(port:80, ターゲットの種類:ip)
    • ロードバランス用のコンテナではnginxのcontainerを指定して、ターゲットグループは先ほど作った物を指定

※ flask-app用のコンテナの名前は、AWS_RESOURCE_NAME_PREFIX-serviceと同じ物にしないと、circleCIのdeployの時にエラーになります

RDBとの連携

  • 踏み台サーバーを立てる(EC2)
    • mysql-clientをinstallする
  • RDSでamazon Auroraを選択する
    • aurora DB用のsecurity groupを作成する(port:3306)
    • private subnetをまとめたsubnet groupを作成する
  • 踏み台サーバーからendpointに向けてログインできるか確認する
  • ECSのコンテナの方で環境変数を設定する
    • DB_NAME
    • DB_USER
    • PASSWORD
    • HOST

エラーハンドリング

  1. CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
    NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考

  2. Task failed ELB health checks in (target-group...)
    ELBのhealth checkに失敗すると表示されます。ELBを設定した時に、defaultでは、/がhealth checkのendpointになります。APIサーバーで/のPATHでGETを用意していていなかったら、ここでエラーになるので、ELBの設定の時に、PATHを変更するか、APIサーバーの方で、Health check用のAPIを作るようにします。

まとめ

AWSのサービスをうまく利用することで、再現性が高くスケーラブルなアプリケーションを作成することができました。また、terraformなどを使って、awsの設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS環境構築(実装編)~VPC~

概要

AWSについての設定の備忘録。今回はVPCの実装を試みる。
そもそもVPCって何って人はこちら(今回使用する用語の解説もあります。)。

実装

VPC作成

スクリーンショット 2019-12-15 16.00.30.png

  1. コンソールからVPCを選択。
  2. VPCの作成。

スクリーンショット 2019-12-15 16.03.12.png

CIDRに10.0.0.0/16
テナンシーはデフォルト
このテナンシーは専有にすると料金上昇。

サブネット作成

  1. コンソールからサブネットを選択。
  2. サブネットの作成。
  3. 先ほど作ったVPC内にサブネット作成。 スクリーンショット 2019-12-15 16.14.36.png

VPCに先ほどのVPC
Availability Zoneにap-northeast-1a
CIDRに10.0.0.0/24

さらにもう一つサブネットを追加
VPCに先ほどのVPC
Availability Zoneにap-northeast-1c
CIDRに10.0.1.0/24

インターネットゲートウェイ作成

  1. コンソールからインターネットゲートウェイを選択。
  2. インターネットゲートウェイの作成。

スクリーンショット 2019-12-15 16.20.38.png

  1. このままだとVPCがattachされてないので、attachする。
  2. 作成したインターネットゲートウェイを選択して、アクションから「VPCにアタッチ」を選択

ルートテーブル作成

  1. コンソールからルートテーブルを選択
  2. ルートテーブルの作成
  3. 先ほどのVPCを選択して作成。
  4. 画面下のルートの部分からルートの編集を選択。

スクリーンショット 2019-12-15 16.24.39.png

  1. アドレス0.0.0.0/0, ターゲットにインターネットゲートウェイを選び、先ほど作成したものを選ぶ。
  2. アクションからサブネットを関連づける

以上で設定を完了。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

イベント風景の自動Tweetとパラパラ漫画をつくろう from 長野

こんにちは!IoTLT Advent Calendar 2019 19日目担当のchinoppyです!

今回は、長野版IoT縛りの勉強会時に使用した、イベント風景自動Tweet/その写真を使って作ったパラパラ漫画と、構築したシステムについて紹介しますー

長野版IoT縛りの勉強会 IoTLT長野 vol.2

めざすもの

イベント風景自動Tweet

パラパラ漫画

撮影間隔が30分だったので、ちょっと物足りないですが・・・

ezgif.com-video-to-gif_001.gif

システム構築

つかったもの

ハード

IMG_20191217_130411.jpg

クラウド

  • SORACOM
    • SORACOM Beam
  • AWS
    • IoT Core
    • Lambda
    • STS
    • S3

構成

スクリーンショット 2019-12-15 17.14.19.png

処理内容

  1. Raspberry Piで写真撮影
  2. SORACOM経由でIoT Coreへトークンの取得リクエストをする
  3. STS経由でトークンを取得し、Raspberry Piへ送る
  4. 2で取得したトークンを使用して、S3へアップロード
  5. S3のTriggerでLambdaを実行し、Tweetする!

※ちなみに、今回、SORACOMを使用して、IoT Core経由でトークンを取得するようにしていますが、、、
※Raspberry Pi側にAWSのセキュリティ情報(シークレットキーなど)を保持させても問題ない環境でWifiが利用可能であれば、
※2、3は不要で、直接、S3へアップロードするのが楽かと思います!

環境構築

【Raspberry Pi】

ソースの詳細はこちら

https://github.com/peacemaker07/iot_making_for_raspberry_pi

※READMEなどちゃんと整備できていなくてすみません。。。

主になるソースはこちら

https://github.com/peacemaker07/iot_making_for_raspberry_pi/blob/master/main_camera.py

前提

  • OSはインストールしてあり、SSHで接続できる
  • カメラは接続済み
  • USBドングルにSOARCOMのSIMがはいっていて、SORACOMへ接続済み

Raspberry Piで写真撮影

この部分で、撮影タイミングの場合は撮影し、撮影タイミングではない場合は処理終了

main_camera.py
・・・
    # カメラ撮影
    camera_obj = Camera(camera_shadow)
    if not camera_obj.is_shooting():
        return
    camera_obj.shooting()
・・・

撮影時に使用するライブラリはpicameraし、撮影してます

utils/camera.py
from picamera import PiCamera
・・・
     def _shooting(self):
・・・
        camera = PiCamera()

・・・
        # プレビュー開始
        camera.start_preview()

        # Camera warm-up time
        time.sleep(5)

        # 撮影
        camera.capture(image_path, quality=self.shadow.quality)

        # プレビュー終了しカメラクローズ
        camera.stop_preview()
        camera.close()
・・・

SORACOM経由でIoT Coreへトークンの取得リクエストをする

トークンの取得のリクエストと、取得はここで行っています

main_camera.py
・・・
    # tokenの取得要求
    msg_token_req = MsgTokenReq(imsi=imsi)

    # token取得するためsubscribe実行
    result_sub = tasks_mqtt.run_subscribe_by_mqtt.delay(host, port, msg_token_req.get_sub_topic())
    time.sleep(2)

    try:
        mqtt_client = CommMqtt(host, port)
        mqtt_client.connect()

        result = mqtt_client.publish(msg_token_req)

        mqtt_client.disconnect()
    except:
        print("error")

    while not result_sub.ready():
        time.sleep(1)

    value = redis_client.get('token')
・・・

SORACOM→IoT Core→Lambda→STS の戻りを待たないといけないので、celeryを使用して待ち、取得したトークンをredis経由で取得します。

S3へアップロード

アップロードはこの部分で行っています

main_camera.py
・・・
    # 撮影画像をS3へputする
    send_images_data = SendImagesData(imsi, camera_obj)
    # TODO S3 bucket名を可変にする
    comm_aws_s3 = CommAwsS3('sample-iot-making-dev', [send_images_data])
    ok_list, ng_list = comm_aws_s3.put_s3(payload_str)
・・・

取得したトークンを使用して、アップロードしてます

定期的に実行

今回は、cronを使用しています
※リポジトリをpiユーザのホームディレクトリにした場合です

$ cd /home/pi/iot_making_for_raspberry_pi

# ライブラリインストール
$ pip3 install -r requirements.txt

# cron
$ crontab -e


*/10 * * * * python3 ~/iot_making_for_raspberry_pi/main_camera.py

【AWS】

severless frameworkを使って構築しました

ソースの詳細はこちら

https://github.com/peacemaker07/iot_making_for_sls

※こちらもREADMEなど整備できていなくすみません。。。

READMEのInstall部分、Usageの「deploy」を行い環境を構築
これで構成図のAWS部分は完成

その他:IoT Coreでモノの登録を行う

デバイス証明書(あとにでてくるSORACOMでも使用)とShadowの機能を使うため、モノの登録を行います

こちらを参照に、モノの名前は「SIMのIMSI」

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/register-device.html

IoT CoreのShadowを使う

設定値をShadowに持たせることにより、クラウドから設定を変更することができるようになります

IoT Coreのモノの中にある「シャドウ」に以下を登録

{
  "desired": {
    "sensor": {
      "interval": 10
    },
    "camera": {
      "quality": 10,
      "shoot_timing": 1,
      "resolution_width": 800,
      "resolution_height": 600,
      "tmp_dir": "/tmp",
      "is_shooting": true,
      "is_change_status": true
    }
  },
  "reported": {
    "sensor": {
      "interval": 10
    },
    "camera": {
      "resolution_width": 800,
      "is_shooting": true,
      "resolution_height": 600,
      "tmp_dir": "/tmp",
      "shoot_timing": 1,
      "quality": 10,
      "is_change_status": true
    }
  }
}
  • desired
    • デバイスへ要求する値
  • reported
    • デバイスの状態

「shoot_timing」を変更すると撮影タイミングを変えられます
ただし、Raspberry Pi側のcronでの実行間隔に依存します。。。

例)
  • cronが10分毎
    • ⇒shoot_timing=1 だと10分毎に撮影
    • ⇒shoot_timing=3 だと30分毎に撮影

S3のTriggerでLambdaを実行し、Tweetする!

S3へ画像がputされたときに、Lambdaを起動するようにしています

serverless.yml
・・・
  #
  # S3 TriggerでLambdaを起動
  # https://serverless.com/framework/docs/providers/aws/events/s3/
  #
  notification_twitter:
    handler: handler.notification_twitter
    role: S3TriggerRoleForLambda
    events:
      - s3:
          bucket: ${file(./serverless.env.yml):NAME_PREFIX}-iot-making-${opt:stage}
          event: s3:ObjectCreated:*
          rules:
            - suffix: .jpg
          existing: true
・・・

Lambdaはこちら

https://github.com/peacemaker07/iot_making_for_sls/blob/master/handler/notification_twitter.py

「event」内にbucket、objキーの情報があるので、S3から取得しTweetしてます

【SORACOM】

前提

  • Webのコンソールは使用できる状態
  • 使用するSIMは登録済み

SORACOM経由でIoT Coreへトークンの取得リクエストをする

この部分でBeamを使用するため、設定を行います

なのですが、まず、IoT Coreで取得したデバイス証明書の登録をします

image.png

image.png

「X.509証明書」を選択し必須項目を入力して登録

image.png

Beamの設定をします

  • ホスト名は、IoT Coreのカスタムエンドポイントを入力
  • 認証情報は、先ほど登録した二章情報を選択し設定

image.png

撮影に関しては、これで設定完了しTweetできるようになりました?

パラパラ漫画について

こちらは、S3へ保存された写真をダウンロードして、iMovieで動画にしましたw

AWS CLIを使用すると簡単にダウンロードできます

$ aws s3 cp --recursive --exclude '*' --include 'camera_20190420*' s3://[bucket名]/[IMSI]/camera/ ./ --profile [pfofile名]

こんなかたちにすると、正規表現でダウンロードできるので楽です!

課題

  • Raspberry Pi部分:S3へ画像送信時、bucket名が固定となっている
  • ツイートの内容はコードを修正しないといけない。。。
  • 送信間隔がcronでの間隔単位でしか設定できない。。。

別のイベントでも重宝してます?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

lambdaのNode.jsバージョンを上げるときはログのフォーマット変更にも注意

バージョンでログ出力が違うから注意

2019年末にlambdaのNode.js 8.10がEOLを迎えます。
ログ出力の部分で微妙に動作が違うので念の為確認してからバージョンアップしましょう。
特にログ出力をライブラリで行っている場合、そちらの実装がどうなっているか見ておいた方が良いです。
kibanaとかでパースするロジックに変更が必要になるかも。

Node.js 8.10

関数コード

exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    // デフォルトのテンプレートにconsole.log入れただけ
    console.log(`"This is Node.js 8.10 log."`);
    return response;
};

Execution Result

Function Logs:
START RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe Version: $LATEST
2019-12-15T06:42:13.112Z fcfedef6-ed16-4236-a407-5cc3d38bb2fe "This is Node.js 8.10 log."
END RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe
REPORT RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe Duration: 0.49 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 58 MB

2019-12-15T06:42:13.112Z fcfedef6-ed16-4236-a407-5cc3d38bb2fe "This is Node.js 8.10 log."

実行日時 リクエストID console.logで出力した文字列

になってますね。

Node.js 10.x

関数コード

exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    console.log(`"This is Node.js 10.x log."`);
    return response;
};

Execution Result

Function Logs:
START RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419 Version: $LATEST
2019-12-15T06:46:11.635Z b7b8a64d-0bcf-405e-8257-0fc6d3f62419 INFO "This is Node.js 10.x log."
END RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419
REPORT RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419 Duration: 59.33 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 76 MB Init Duration: 174.25 ms

2019-12-15T06:46:11.635Z b7b8a64d-0bcf-405e-8257-0fc6d3f62419 INFO "This is Node.js 10.x log."

実行日時 リクエストID ログレベル console.logで出力した文字列

と、出力されるようになっています。
8.10と比較するとログレベルが出力されるようになっていることに注意しましょう。

Node.js 12.x

Execution Result

Function Logs:
START RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f Version: $LATEST
2019-12-15T06:47:52.767Z 786d807b-6bb9-4267-88f7-4df1f4955a7f INFO "This is Node.js 12.x log."END RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f
REPORT RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f Duration: 2.80 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 70 MB Init Duration: 111.41 ms

2019-12-15T06:47:52.767Z 786d807b-6bb9-4267-88f7-4df1f4955a7f INFO "This is Node.js 12.x log."END RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f

実行日時 リクエストID ログレベル console.logで出力した文字列END RequestId: リクエストID

と、10.xとも違う、おそらくAWS側も意図していない動作をしてるように見受けられます。
(コピペミスとかでもなかったです)

2019-12-15-15-52-35.png

とはいえ、CloudWatchには意図どおりに出力されてそうなので特に問題にはならないと思います。

補足

consoleオブジェクトの関数に応じてログレベルを出力するようになっているようです。

Node.js 8.10

関数コード

exports.handler = async (event) => {
  // TODO implement
  const response = {
      statusCode: 200,
      body: JSON.stringify('Hello from Lambda!'),
  };
  console.log(`"This is Node.js 8.10 log."`);
  console.info(`"This is Node.js 8.10 info log."`);
  console.warn(`"This is Node.js 8.10 warn log."`);
  console.error(`"This is Node.js 8.10 error log."`);
  return response;
};

Execution Result

Function Logs:
START RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d Version: $LATEST
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 info log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 warn log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 error log."
END RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d
REPORT RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d Duration: 0.50 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 58 MB

ログを見てもどんなレベルのログかが分からない。

Node.js 10.x

関数コード

exports.handler = async (event) => {
  // TODO implement
  const response = {
      statusCode: 200,
      body: JSON.stringify('Hello from Lambda!'),
  };
  console.log(`"This is Node.js 10.x log."`);
  console.info(`"This is Node.js 10.x info log."`);
  console.trace(`"This is Node.js 10.x trace log."`);
  console.debug(`"This is Node.js 10.x debug log."`);
  console.warn(`"This is Node.js 10.x warn log."`);
  console.error(`"This is Node.js 10.x error log."`);
  console.fatal(`"This is Node.js 10.x fatal log."`);
  return response;
};

Execution Result

Function Logs:
START RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed Version: $LATEST
2019-12-15T07:15:02.460Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed INFO "This is Node.js 10.x log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed INFO "This is Node.js 10.x info log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed TRACE "This is Node.js 10.x trace log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed DEBUG "This is Node.js 10.x debug log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed WARN "This is Node.js 10.x warn log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed ERROR "This is Node.js 10.x error log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed FATAL "This is Node.js 10.x fatal log."
END RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed
REPORT RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed Duration: 47.48 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 76 MB Init Duration: 151.14 ms

どんなレベルのログが一目瞭然だし、ログのパースするときに便利そう。
8.10と比較すると種類も増えてます。他にもあるかも。

リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

300回プログラムじゃんけんでを戦わせるWebアプリを作った

作ったアプリ

https://janken-programmer.web.app/

遊び方

これはじゃんけんの出す"手"をプログラミングして戦わせるアプリです。

例えばグーだけを出すA君のプログラムと
グーとパーを交互に出すB君のプログラム
を300回戦わせると

B君が勝ちます

こんな感じでじゃんけんのアルゴリズムを考えて戦わせるアプリです

A君のプログラム

HAND = 0

B君のプログラム

HAND = 1
if COUNT % 2 == 0:
    HAND = 0

戦わせると・・・
スクリーンショット 2019-12-10 12.36.10.png
こんな結果になります。

これをWebで遊べるアプリです。

システム構成

今回使ったものは以下の通りになりました。

  • Vue.js
  • Buefy
  • AWS
  • serverless framework
  • dynamodb
  • firebase
  • circleCI

フロントエンドの部分はVueとBeufyを使いました。
同じような画面を作ることが多かったのでサクサク作れました。

バックエンドではAWSのサービスをserverless frameworkで構築しました。
アクセスが不安定なサービスになりそうなのでこちらのフレームワークを使いました。

ホスティングと認証にはfirebaseを使用しました。
本当はAWSなのですべてまとめたほうがいいのかもしれませんが
firebaseは無料で結構使えるのでこちらを採用しました。

無料枠などもあり、すべて無料でできました。
本当はRDBを使いたかったのですが、お金がないので無理やりdynamodbで作りました(反省)

機能

じゃんけんプログラム

じゃんけんのアルゴリズムを実装するにあたり、システム変数的なものを用意することにしました。

変数名 概要
HAND Intger 0~2 の数値を代入することでグーチョキパーを出せる
WIN Intger 勝った数
LOSE Intger 負けた数
DROW Intger あいこの数
P List 自分が出した手の履歴
E List 相手が出した手の履歴
COUNT Intger 対戦数

これを実装したことにより100回負けたらグーを出すみたいなプログラムを書けます

HAND = 1
if LOSE >= 100:
    HAND = 0

コーディング画面

ハイライトやインデントの機能が欲しかったため今回は「Codemirror」を使用させていただきました。

Vueで使用する場合は「vue-codemirror」を使うと単一コンポーネントで使用できるため、気持ちよく作れました。

ランキング機能

ランキングの実装には「イロレーティング」を使いました。

イロレーティングは対戦型の競技に使用されており、相対的な強さを評価します。

そのため、今回のランキングではこちらを使用し投稿されたプログラムを総当たりさせて順位を決定しました。

引用:イロレーティング
https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%AD%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

認証機能

今回は一人が複数のプログラムを投稿できないように認証機能を追加しました。

Firebase Authenticationを使用しGithubで認証できるようにしました。

感想

自分の想定しているものが一通りできたので、だいぶ満足しています。

ただセキュリティやじゃんけんのプログラムに不備があるかもしれないのでこれからも運営、開発をしていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS環境構築(用語編)~VPC~

AWSの基本サービスVPC

以下に自分用のメモもかねてAWSの基本的なサービスの一つであるVPCについてまとめてみた。

VPC(Virtual Private Cloud)

公式ドキュメントを参考に簡単にまとめました。

Amazon VPCとはAWSリソースを起動するための仮想的なネットワークです。(そもそもネットワークとはなんぞやという方はTCP/IPあたりの知識を身に着けることをお勧めします。)

VPCでIPアドレス範囲を設定できます。このIPアドレスは大まかにいうと、入出国審査での分類みたいなものです。君は入れる、入れない的な。
さらにサブネットというものを追加することで、より細かい設定が可能です。(空港でも日本人とその他の国籍の方で審査別れますよね。)

各サブネットではさらにそれぞれセキュリティの設定をすることができますが、ここでは詳細は省きます。

ちなみにVPCの設定で出てくる用語について簡単に説明すると以下の通りです。
主にアルファベットの略語についてです(覚えられない...)。

-AZ
アベイラビリティーゾーンの略。実際にデータが保存されているデータセンターの集まりです。
最近日本でスマホゲームなどがプレイできなくなったのは物理的にAZ障害が起きたからです。
AWSのAZ(アベイラビリティーゾーン)とは?AZ障害が起きたときどうすればよいのかここの記事は面白いです。

-CIDR
Classless Inter-Domain Routingの略(サイダー)。IPアドレスのブロックを任意単位で区切ることができる。従来より柔軟性があった便利だよねという感じ。
わかりやすい。

-tenancy
インスタンスをどこで実行するかを決める。

-Internet Gateway(公式)
その名の通り。役割は公式の和文から引用しますが
"1 つは、インターネットでルーティング可能なトラフィックの送信先を VPC のルートテーブルに追加することです。もう 1 つは、パブリック IPv4 アドレスが割り当てられているインスタンスに対してネットワークアドレス変換 (NAT) を行うことです。"
とあります。要は入出国における実際のcheckみたいなイメージ。

その他の関連記事

AWS環境構築(実装編)~VPC~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3で署名Verを4にしたらエラー

AWS SDK for PHP v2を使ってるもんで、署名Verを2から4にする必要があった
そしたらファイルのアップロードで以下の様なエラーが出てアップロードできない

The request signature we calculated does not match the signature you provided. Check your key and signing method.

修正自体はfactoryするときに'signature' => 'v4'を渡すくらいで大した作業でもなく。
キーが間違ってるから確認して的なエラーメッセージだけど、ファイル一覧は正常に動くしそもそも今まで動いてたのでACCESS_KEYとSECRET_KEYが間違ってる訳でもなさそう。

ファイルアップのソースはこんな感じ

    // S3バケット名
    $backet = 'backet_name';
    // アップするS3のパス
    $s3dir = '/hoge/fuga';
    // アップ時のファイル名
    $file = 'aaa.txt';

    // アップするローカル側のファイルフルパス
    $upfilepath = ’/tmp/tmp.txt';

    $s3->upload(
        $backet.'/'.$s3dir,
        $file,
        fopen($upfilepath, 'rb'),
        'public-read'
    );

uploadメソッドの第一引数は本来バケット名を渡すはずがバケット名 + ファイルパスになってる・・・
正しい引数に修正することでエラーは出なくなった

    $s3->upload(
        $backet,
        $s3dir.'/'.$file,
        fopen($upfilepath, 'rb'),
        'public-read'
    );

署名Ver2は第一引数と第二引数はただのファイルパスの情報だったので、バケット名とファイルパスをどこで区切って渡してもアップロードできた。多分
署名Ver4はこれらの引数を元にハッシュ値を作成するので、バケットとファイルパスをちゃんと渡さないとハッシュ値が変わっちゃってアップロードに失敗する。多分

解決したものの、署名Ver2が使えなくなるのは延期になったし、以前に作ったバケットはずっとVer2使えるみたいな話もあるので対応する必要なかったかも?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Step Functions & Fargate バッチでやらかしたこと

この記事について

この記事は Opt Technologies Advent Calendar 2019 17日目の記事です。
担当プロダクトで利用したStep Functionsにて、失敗を通じて得た知見をまとめたものです。

自分について

中途入社して1年ちょっとのエンジニア。チームマネージャになって半年くらい。
入社してからクラウドインフラを学習し始め現在もチーム内で主に担当をしていますが、アプリケーションコードも状況に応じて実装する感じです。
昨年はこんな記事書きました。

やらかし背景

著者の担当プロダクトにて、外部APIから取得できるそこそこのサイズのデータをプロダクト側のDBに格納する処理があります。
ELT的なステップを踏んで処理することを検討しStep Functionsを採用しました。

image.png

  • Extractorが外部APIからデータファイル(csv)を取得しS3バケットに格納
  • Loaderがデータファイルを中間テーブルに格納
  • Transformerが変換SQLを実行し成形済みデータにしてテーブルに格納 image.png

一般的にはStep FunctionsではLambdaで処理を記述すると思うのですが、

  • 当時Lambdaの起動時間限界が5分と短く不安が大きかった
  • Webサーバや、今回の処理とは別のいくつかのバッチ処理をECS(Fargate)で実現していたため、そちらに寄せておいた方がコード管理上楽そうだった

という理由で、デファクトから外れることを認識しつつも、ここではFargateバッチを採用しました。

ところが、これが原因でいくつかのつらみを生んでしまったのでした。

つらみ1: 立ち上がりが遅い

当たり前といえば当たり前なのですが、Fargateは起動に時間がそこそこかかります。我々のプロダクトのバッチだとおおよそ45sec程度。
Step Functionsの立ち上がり直後にSlackへ通知をするような処理が必要なのですが、通知が来るまでに約1min程度かかりました。

つらいといえばつらいんですが、要件的に致命的な問題にならないため、ひとまずこちらは気にせずそのまま突き進みました。

つらみ2: 処理結果を次処理で利用することができない

当初はこんな感じで処理を進める予定でした。

image.png

ところが、Fargateバッチの出力は以下のようになります。

Fargate出力
{
  "name": "Extractor",
  "output": {
    "Attachments": [
      {
        "Details": [
          {
            "Name": "subnetId",
            "Value": "subnet-XXXXXXXXXXXXXXXXX"
          },
          {
            "Name": "networkInterfaceId",
            "Value": "eni-XXXXXXXXXXXXXXXXX"
          },
          {
            "Name": "macAddress",
            "Value": "XX:XX:XX:XX:XX:XX"
          },
          {
            "Name": "privateIPv4Address",
            "Value": "xx.xx.xx.xx"
          }
        ],
        "Id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "Status": "DELETED",
        "Type": "eni"
      }
    ],
    (略)
  }
}

標準出力に吐いた結果とか出てくることを期待しましたが、淡い期待でした。
ECSタスクに関する詳細情報が出てきます。
なんとか他の手段で次のステートへ受け渡す方法を考えてみましたが、結局見つからず。

苦肉の策として、ステートマシンの入力側で対処しました。

ステートマシンの入力
{
  "StagingTable": "STAGING_XXXXXXXX_YYYYMMDDHHmmssfff",
  "S3Path": "Sync/Staging/XXXXXXXX/YYYYMMDDHHmmssfff/"
}

ステートマシンへの入力の時点で取得ファイルの格納S3ディレクトリ名と、作成する中間テーブル名を渡しています。
衝突を避けるため、外部APIから取得するデータのアカウントID(XXXXXXXX)と
実行日時(ミリ秒単位:YYYYMMDDHHmmssfff)で分けました。
既にだいぶつらい感じの作りですが…なんとかこれにより共通のデータに対してアクセスすることには成功できました。

さて、これでようやく実装できるぞ、と思って動かしてみたら、次の問題が発生しました。

つらみ3: ThrottlingExceptionの大量発生

さてこれで元気に動いてくれそうだなということで実装しまして。
では…ということで動かしてみたら、狙い通りうまくいきました!
ここでめでたしめでたし、となればいいのですが、現実は非情なものでした。

今回の処理は並行実行を結構な数で行うケースが想定されます。
その確認のため負荷試験を行ったところ、ボコボコとエラーで停止してしまいました。

そのときのエラーがこちら。

{
  "resourceType": "ecs",
  "resource": "runTask.sync",
  "error": "ECS.AmazonECSException",
  "cause": "Rate exceeded. (Service: AmazonECS; Status Code: 400; Error Code: ThrottlingException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx)"
}

調べてみると、どうもECSタスク起動のためのECS APIコール数が秒間1回までという制限にひっかかっていたらしく、
リトライ設定をしても結構な確率で再衝突しました。結果リトライ上限に達し無事死亡……。

取り急ぎの対応としては、リトライのバックオフレート設定です。

"Retry": [
  {
    "ErrorEquals": [ "States.Timeout" ],
    "MaxAttempts": 0
  },
  {
    "ErrorEquals": [ "States.ALL" ],
    "IntervalSeconds": 10,
    "MaxAttempts": 5,
    "BackoffRate": 3.0
  }
]

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html#error-handling-retrying-after-an-error

リトライタイミングがそれなりに散ってくれたので、ステートマシン全体の処理完了時間を犠牲にはしましたが、動作するようになりました。

ちなみにこちら、Lambdaの場合はほぼ無制限…同時実行数も1,000など、圧倒的に有利ですね。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html

まとめ

Lambdaを選択できるときは迷わずLambdaを選択するのが良さそうですね……。
とはいえ現在もLambdaの制限時間は15分で、これを超えそうなバッチもいくつかある現状。
激重なバッチについては残念ながら現状維持になりそうです。
ですが、例えばSlackへの通知だとか、今回は説明都合で割愛したDBへの軽微な変更処理バッチなど、
速度面に不安のない処理については、チームで粛々とLambda化の対応を進めています。

Step FunctionsはLambda,Fargateの他にも、AWS BatchやSQS、EMR、DynamoDBなどが利用可能ですが、
組み合わせるとどんなことが起きるかについては利用前に事前調査をしなくちゃな、と思いました。
当然といえば当然のことですが、いやー、反省です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSマネジメントコンソールに「IAMユーザー」でのログイン後「IAMロール」にスイッチする為の設定メモ

IAMユーザで、AWSマネジメントコンソールにログイン後、特定のIAMロールにスイッチロールできるようにする為の設定メモ

記載内容

  1. マネジメントコンソールログイン用IAMユーザーの作成
  2. スイッチ先のIAMロールの作成
  3. IAMユーザからIAMロールにスイッチ許可する為のAssumeRoleの設定
  4. 上記設定後のIAMユーザログイン/IAMロールへのスイッチ確認

前提

  • 個人利用を想定し、ルートアカウントしか存在しない状態からの設定を想定
  • ログイン用IAMユーザーは「login_user」、スイッチ先IAMロールは「admin_role」とする

1. マネジメントコンソールログイン用IAMユーザの作成

  • マネジメントコンソールログイン
    1.png

  • IAMコンソールより「ユーザー」を選択
    2.png

  • マネジメントコンソールログイン用として「login_user」を作成
    3.png

  • ログインする為だけのユーザのため、基本的に権限は付与せずデフォルト設定で作成
    4.png
    5.png
    6.png

2. スイッチ先のIAMロールの作成

  • IAMコンソールより「ロール」を選択
    7.png

  • とりあえす「信頼されたエンティティの種類」より「AWSサービス」「EC2」を指定
    8.png

  • ロールに付与するポリシーを選択(ここでは、管理者権限として「AdministratorAccess」「Billing」ポリシーを付与)
    9.png

  • ロール名として「admin_role」を指定
    10.png

3. IAMユーザーからIAMロールにスイッチできるようにする為のAssumeRoleの設定

  • 最初に作成したIAMユーザー(login_user)に対し、スイッチロールを許可する為のポリシーの作成 11.png 12.png
JSON
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "arn:aws:iam::アカウントID:role/admin_role"
    }
}
  • 作成するポリシー名は「AllowAssumeRole」と指定
    13.png

  • IAMユーザ「login_user」に対し、「AllowAssumeRole」ポリシーの設定
    14.png
    15.png
    16.png

  • IAMロール「admine_role」をIAMユーザー「login_user」が使えるようにする為の信頼関係(信頼ポリシー)の設定
    17.png
    18.png
    19.png

Policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::アカウントID:user/login_user",
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

4. 上記設定後のIAMユーザログイン/IAMロールへのスイッチ確認

  • マネジメントコンソールからIAMユーザ「login_user」でのログイン確認
    20.png

  • 作成した管理者ロール「admin_role」へのスイッチロールの確認
    21.png
    22.png
    23.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS CloudWatch - Alarm 異常検出 のテンプレート

こちらの記事はケーシーエスキャロット Advent Calendar 2019の15日目の記事です。
ちょっと遡って5日前の記事はgaramanさんのくずし字を読む (準備編)でした。
子供の頃、百人一首をやった記憶はありますが、普通の日本語で書かれていました。
その為、くずし字をあまり見た記憶がありませんが、プログラムで解析しようとする発想がスゴいです。

さて、本日はメトリクスの値を監視する AWS CloudWatch - Alarm に関して記載してみようと思います。

CloudWatch - Alarm の監視方法としては、指定した閾値を

  • 以上 (GreaterThanOrEqualToThreshold)
  • 以下 (LessThanOrEqualToThreshol)
  • より上 (GreaterThanThreshold)
  • より下 (LessThanThreshold)

となった場合にアラーム状態へ遷移する静的しきい値のみだったのですが、今年の夏ごろから異常検出という方法も可能になりました。

この異常検出をコンソールから設定する方法は、異常検出に基づいて CloudWatch アラームを作成する等のサイトに記載がありますが、CloudFormation でデプロイする際のテンプレート記載例が見当たらなかった(探し方が悪いかもですが)ので記載してみようと思います。

CPU 使用率アラームを異常検出へ書き換えた例。

template.yml
CPUAlarmForAnomaly:
  Type: AWS::CloudWatch::Alarm
  Properties:
    AlarmName: anomaly-alarm
    AlarmDescription: CPU alarm for my instance
    AlarmActions: Ref: "logical name of an AWS::SNS::Topic resource"
    Metrics:
    - Id: m1
      ReturnData: true
      MetricStat:
        Period: 60
        Stat: Average
        Metric:
          MetricName: CPUUtilization
          Namespace: AWS/EC2
          Dimensions:
          - Name: InstanceId
            Value: Ref: "logical name of an AWS::EC2::Instance resource"
    - Id: a1
      Expression: ANOMALY_DETECTION_BAND(m1, 2)
    EvaluationPeriods: 3
    ThresholdMetricId: a1
    ComparisonOperator: GreaterThanUpperThreshold

以上! …だとそっけなさ過ぎるので、簡単に説明を記載すると、

    - Id: m1
      ReturnData: true
      MetricStat:
        Period: 60
        Stat: Average
        Metric:
          MetricName: CPUUtilization
          Namespace: AWS/EC2
          Dimensions:
          - Name: InstanceId
            Value: Ref: "logical name of an AWS::EC2::Instance resource"

の部分が監視するメトリクスの宣言となります。
注意点としては、静的しきい値と比較すると統計方法の記載が異なる(StatisticStat)ことです。

そして、

    - Id: a1
      Expression: ANOMALY_DETECTION_BAND(m1, 2)

の部分で、監視対象メトリクスの値を異常検出と見なす想定値の範囲を指定(2)しています。
Anomaly_Detection_Graph2.png
異常検出方式で Alarm として設定してみると、上図のような感じで実際のメトリクスの値に沿って想定値の範囲(グレーの部分)が設定(想定)されます。

詳細はCloudWatch の異常検出の使用を参照。

因みに、監視方法(ComparisonOperator)も静的しきい値と異なっていて

  • 想定値の範囲を上回った場合 (GreaterThanUpperThreshold)
  • 想定値の範囲を下回った場合 (LessThanLowerThreshold)
  • 想定値の範囲を上回った or 下回った場合 (LessThanLowerOrGreaterThanUpperThreshold)

の3つから指定可能です。

この異常検出の使いどころとしては、しきい値が設定しにくい(時間帯や日によって異なる等)場合に良いかもしれませんね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3バケットポリシーでAssumeロールのアクセス設定を行う

はじめに

S3のバケットポリシーでAssumeロールのアクセス制御設定の方法をまとめる。

詳細

アカウントAでIAMユーザー hoge からAssumeロールしアカウントBのS3バケットにアクセスする状況を想定。
S3バケットのバケットポリシーでは、そのAssumeロールでのみアクセス許可しその他のアクセスは拒否する設定を行う。

バケットポリシー設定方法は以下の2つ
1.アクセス制御にAssumeロール元のIAMユーザーarnを使用する
2.アクセス制御にAssumeロールのIAMロールIDを使用する

方法1

アクセス制御にAssumeロール前のIAMユーザーarnを使用する場合。

バケットポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<バケット名>/*",
            "NotPrincipal": {
                "AWS": [
                    "arn:aws:iam::<アカウントID>:role/<ロール名>",
                    "arn:aws:sts::<アカウントID>:assumed-role/<ロール名>/<元のIAMユーザー名>"
                ]
            },
        }
    ]
}

Principal または NotPrincipal ではワイルドカード * を使用できないため、
会社等、複数のユーザーが同じAssumeロールを使用する場合、ユーザーごとに NotPrincipal を書く必要があり、この方法は向かない。

方法2

アクセス制御にAssumeロールのIAMロールIDを使用する場合。
IAMロールIDはコンソール上では確認できず、AWS CLIでのみ確認可能。
以下のコマンドを実行し、IAMロールIDを確認する。

$ aws iam get-role --role-name <IAMロール名>

バケットポリシーに以下を記載する。

バケットポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "2",
            "Effect": "Deny",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<バケット名>/*",
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "<IAMロールID>:*"
                }
            }
        }
    ]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cognito のIDプールでS3のユーザー毎フォルダにアクセスしてみる

背景

AWS使ってサーバーレスで自分用の家計簿的なwebサービスを勉強も兼ねて開発中。
2つ前の記事
1つ前の記事
せっかくCognito使ったので、ユーザー毎のS3フォルダにアクセスする事をしてみたい。

構成

フロント:Vue.js + Element.ui
サーバーサイド:CloudFront + S3 + APIGateway + Lambda + DynamoDB + Cognito

やってみた

今回、様々なページを参考にさせてもらい、その情報を組み合わせました。もはやどの部分がどのページの参考なのかこんがらがっています。結構トライ&エラー発生し、各種設定行ったり戻ったりしました。今回は結果のみ共有させてもらいます。

まず、ブラウザからS3にアクセスする為、CORSの設定をします。

S3バケットのアクセス権限=>CORSの設定
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
    <AllowedHeader>Content-*</AllowedHeader>
    <AllowedHeader>Host</AllowedHeader>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

ログインユーザー毎にS3へのアクセスを許可する為、CognitoIDプール、認証済みユーザー用のIAMポリシーに以下設定。※以下の指定でhogehoge-bucketと指定していますが既に存在するようです。が、そのバケットを意味してないです。実際には自分作成のバケット名です。

認証済みユーザー用のIAMポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cognito-identity:*",
                "mobileanalytics:PutEvents",
                "cognito-sync:*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::hogehoge-bucket/cognito/myhome-account/${cognito-identity.amazonaws.com:sub}",
                "arn:aws:s3:::hogehoge-bucket/cognito/myhome-account/${cognito-identity.amazonaws.com:sub}/*"
            ]
        }
    ]
}

そして処理本体、VueファイルのjavaScript部分

テスト本体
import AWS from 'aws-sdk'
import awsconfig from '../cognito/config'
import cognito from '@/cognito'

const S3_USERBACKETNAME = 'hogehoge-bucket'
const PROVIDER_KEY = 'cognito-idp.' + awsconfig.Region + '.amazonaws.com/' + awsconfig.UserPoolId

/* ・・中略・・ */

    s3test: function () {
      const cognitoUser = this.$cognito.userPool.getCurrentUser()
      cognitoUser.getSession((err, session) => {
        if (!err && session.isValid()) {
          const itoken = session.getIdToken().getJwtToken()

          // Initialize the Amazon Cognito credentials provider
          AWS.config.region = awsconfig.Region
          AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: awsconfig.IdentityPoolId,
            Logins: {
              [PROVIDER_KEY]: itoken
            }
          })
          var identityId = AWS.config.credentials.identityId
          var s3key = 'cognito/myhome-account/' + identityId + '/testfile.txt'

          // refresh AWS credentials
          AWS.config.credentials.refresh((err) => {
            if (err) {
              console.log(err)
            } else {
              var s3 = new AWS.S3({
                params: { Bucket: S3_USERBACKETNAME }
              })

              const updateParams = {
                Key: s3key,
                Body: 'teststring',
                ContentType: 'application/text'
              }

              s3.upload(updateParams, function (err, data) {
                if (err) {
                  console.log('error : ', err)
                } else {
                  console.log('success : ' + S3_USERBACKETNAME + '/' + s3key)
                }
              })
            }
          })
        }
      })
    }

これでテスト用の適当な文字列のファイルをS3のユーザー毎フォルダにアップ出来ました。間違ったフォルダを指定した時は AccessDenied: Access Denied で失敗する事も確認できました。

特にはまったポイント

  • credentialを取得する時に、一時トークンを渡すが、cognitoを使う場合にどんなキー(上記ソースのPROVIDER_KEY部分)を指定するのか情報を見つけるのに非常に時間がかかった。参考
  • ポリシーで指定するcognito-identity.amazonaws.com:subが実際には何の値を示すのか見つけるのに非常に時間がかかった。参考

参考にさせてもらったページ

ブラウザから Amazon S3 への写真のアップロード
サインイン後に ID プールを使用して AWS サービスへアクセスする
[ Serverless ] Cognito、S3、Lambdaで認証機能付きのWebサイトを作ってみました
【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #2/2【JavaScript】
WebブラウザからAmazon S3に直接ファイルをアップロードする
AWS Cognitoを使ってサーバレスアプリケーションに認証をつける

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS SAM CLI 再入門 2019.12

この記事は AWS Advent Calendar 2019 16日目の記事です。

はじめに

2018年の5月にaws-sam-local 改め aws-sam-cli の新機能 sam init を試すという記事を投稿しました。
AWS SAM CLI は GitHub 上で開発がおこなわれていますが、リリースのペースが早く
日々機能追加や改善が行われています。結果として上記の記事の内容は一部古いものとなっています。
本記事では12/15時点の最新バージョンである v0.37.0 までのリリースを踏まえた使い方をご紹介します。

AWS SAM と AWS SAM CLI

Serverless Application Model(SAM)はLambdaやAPI Gateway, DynamoDBのようなサービスを活用した
サーバーレスアプリケーションのデプロイに特化した、AWS CloudFormation の拡張機能です。
通常のCloudFormationのテンプレートと同様にYAML/JSONでテンプレートを定義しますが、
素のテンプレートより簡潔に記載できるのが特徴です。

AWS SAM CLI は SAM を使用したサーバーレスアプリケーションの作成と管理を簡単に行うことができる
コマンドラインツールです。

AWS SAM CLIのインストール

AWS SAM CLI のすべての機能を活用するには、前提としてAWS CLI と Dockerの
インストールが必要ですが、本記事では割愛します。

macOS

Homebrewを使用してAWS SAM CLIをインストールすることができます。

$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 0.37.0

Windows

以下のURLからインストーラーをダウンロードできます。

https://github.com/awslabs/aws-sam-cli/releases/latest/download/AWS_SAM_CLI_64_PY3.msi

> sam --version
SAM CLI, version 0.37.0

その他の環境

pip でもインストールできます。

$ pip install -U aws-sam-cli
$ sam --version
SAM CLI, version 0.37.0

ローカルでの開発とテスト

sam init

sam init コマンドを使用すると、アプリケーションのプロジェクトを初期化できます。
v0.30.0 以降では対話型にサンプルアプリケーションをセットアップしたり、
GitHubやローカル上のテンプレートを読み込んで初期化することもできるようになっています。

以下は対話型でnode.js12.xのランタイムを指定したサンプルアプリケーションを作成している例です。

$ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Which runtime would you like to use?
        1 - nodejs12.x
        2 - python3.8
        3 - ruby2.5
        4 - go1.x
        5 - java11
        6 - dotnetcore2.1
        7 - nodejs10.x
        8 - nodejs8.10
        9 - nodejs6.10
        10 - python3.7
        11 - python3.6
        12 - python2.7
        13 - java8
        14 - dotnetcore2.0
        15 - dotnetcore1.0
Runtime: 1

Project name [sam-app]:

Quick start templates may have been updated. Do you want to re-download the latest [Y/n]: n

-----------------------
Generating application:
-----------------------
Name: sam-app
Runtime: nodejs12.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./sam-app/README.md

その他のバリエーション

# すべてのパラメータを指定
$ sam init --runtime nodejs12.x --dependency-manager npm --app-template hello-world --name sam-app

# GitHub上のサンプルプロジェクトを指定
$ sam init --location gh:aws-samples/cookiecutter-aws-sam-python

# ZIPファイルやローカルのテンプレートフォルダを指定
$ sam init --location /path/to/template.zip
$ sam init --location https://example.com/path/to/template.zip
$ sam init --location /path/to/template/folder

nodejs12.x でのサンプルアプリケーションの場合以下のようなファイル構成となります。

sam-app
│  .gitignore
│  README.md
│  template.yaml  # SAMテンプレート
│
├─events
│   event.json  # イベントソースがLambda関数に送信するサンプルペイロード
│
└─hello-world  # Lambda関数を配置
    │  .npmignore
    │  app.js
    │  package.json
    │
    └─tests #ユニットテストファイルを配置
        └─unit
            test-handler.js 

sam build

Lambda関数のソースコードをビルドし、デプロイ用のアーティファクトを作成することができます。

$ sam build
Building resource 'HelloWorldFunction'
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrc
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUpNpmrc

Build Succeeded

Built Artifacts  : .aws-sam\build
Built Template   : .aws-sam\build\template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

sam local invoke

ローカルでLambda関数を起動できます。
ローカルに docker-lambda のイメージがない場合は最初にダウンロードされるので少し時間がかかります。
docker-lambda が sam build で作成されたアーティファクトをマウントして
AWS上でのLmabda関数の実行をシュミレートします。

$ sam local invoke
Invoking app.lambdaHandler (nodejs12.x)

Fetching lambci/lambda:nodejs12.x Docker container image......
Mounting C:\test\sam-app\.aws-sam\build\HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 42ac82fe-710e-1c38-13c8-ad22f0117ab1 Version: $LATEST
END RequestId: 42ac82fe-710e-1c38-13c8-ad22f0117ab1[0m
REPORT RequestId: 42ac82fe-710e-1c38-13c8-ad22f0117ab1     Init Duration: 169.23 ms        Duration: 4.30 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 37 MB

{"statusCode":200,"body":"{\"message\":\"hello world\"}"}

sam local generate-event

特定のイベントソースのイベント情報をLambda関数に渡して処理したい場合
--event オプションでjsonファイルを指定します。
もしくは sam local generate-event コマンドでさまざまなイベントソースのペイロードを
作成することができます。
その際、サービスごとに複数のイベントタイプを指定することができます。

DynamoDB ストリームのupdateイベント情報を生成し、Lambda関数を起動する例

$ sam local generate-event dynamodb update > ./events/dynamodb_event.json
$ sam local invoke HelloWorldFunction --event ./events/dynamodb_event.json

その他 v0.37.0 時点で生成に対応しているイベントソース

$ sam local generate-event --help
~省略~

Commands:
  alexa-skills-kit
  alexa-smart-home
  apigateway
  batch
  cloudformation
  cloudfront
  cloudwatch
  codecommit
  codepipeline
  cognito
  config
  connect
  dynamodb
  kinesis
  lex
  rekognition
  s3
  sagemaker
  ses
  sns
  sqs
  stepfunctions

sam local start-lambda

Lambda関数の呼び出しをエミュレートするローカルンドポイントを起動することができます。
AWSCLIでローカルエンドポイントを指定したり、DynamoDB Localなどの開発ツールと
連携させたりすることで開発、テストを便利におこなうことができます。

$  sam local start-lambda
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2019-12-12 14:00:04  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)

# AWSCLIから呼び出し
$  aws lambda invoke --function-name HelloWorldFunction  --endpoint http://127.0.0.1:3001/ outfile.txt
{
    "StatusCode": 200
}

参考: Step Functions Local が出た!SAMやDynamoDB Localと一緒に使ってみる
https://qiita.com/hayao_k/items/14ff145a5d5c5191a18f

sam local start-api

API呼び出しを含めたサーバーレスアプリケーション全体のテストを行うために
API Getewayのローカルインスタンスを起動することができます。
起動中にLambda関数への変更を行った場合は自動的にリロードされるため
start-apiを再実行する必要はありません。
SAMテンプレート自体を変更した場合はリスタートする必要があります。

$ sam local start-api
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2019-12-12 14:18:08  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

# 起動したエンドポイントに curl で接続すると正常に応答し、hello worldを返す
$ curl http://127.0.0.1:3000/hello
{"message":"hello world"}

sam validate

SAMテンプレートがSAMの仕様に沿った記述になっているか検証することができます。

$ sam validate
C:\test\sam-app\template.yaml is a valid SAM Template

AWSへのデプロイ

sam deploy

SAMで作成したアプリケーションのデプロイを行うことができます。
以前はAWS SAM CLIでデプロイを行うには複数のステップが必要だったのですが、v0.33.1 以降
単一の sam deploy コマンドでデプロイを操作できるようになりました。

具体的には必要な設定を事前にプロジェクトルートディレクトリにファイル(samconfig.toml)
として保続しておくか、-g, --guided オプションで対話的にデプロイをすすめることが可能です。
初回にこのオプションを使用してデプロイすることで、選択した構成を samconfig.toml
として保存させることもできます。
Lambdaデプロイパッケージ用のS3バケットもAWS SAM CLI が自動で作成し、管理します。


$ sam deploy -g

Configuring SAM deploy
======================

        Looking for samconfig.toml :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: mystack              ★CloudFormation スタック名の指定
        AWS Region [us-east-1]: ap-northeast-1     ★リージョンの指定
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y     ★デプロイ前にCloudformationの変更セットを確認するか
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: y   ★SAM CLI に IAMロールの作成を許可するか(CAPABILITY_IAM)
        Save arguments to samconfig.toml [Y/n]: y  ★この設定をsamconfig.tomlとして保存するか

        Looking for resources needed for deployment: Not found.
        Creating the required resources...
        Successfully created!

                Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx
                A different default S3 bucket can be set in samconfig.toml

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

        Deploying with following values
        ===============================
        Stack name                 : mystack
        Region                     : ap-northeast-1
        Confirm changeset          : True
        Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx
        Capabilities               : ["CAPABILITY_IAM"]
        Parameter overrides        : {}

Initiating deployment
=====================
Uploading to mystack/c04c27151371faba23ccb6ca6ae9bbdf  127490 / 127490.0  (100.00%)
Uploading to mystack/63baf4f76aff04a2c9ecb11bc3918a7d.template  1130 / 1130.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                        LogicalResourceId                                ResourceType
------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                            HelloWorldFunctionHelloWorldPermissionProd       AWS::Lambda::Permission
+ Add                                            HelloWorldFunctionRole                           AWS::IAM::Role
+ Add                                            HelloWorldFunction                               AWS::Lambda::Function
+ Add                                            ServerlessRestApiDeployment47fc2d5f9d            AWS::ApiGateway::Deployment
+ Add                                            ServerlessRestApiProdStage                       AWS::ApiGateway::Stage
+ Add                                            ServerlessRestApi                                AWS::ApiGateway::RestApi
------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:0123456789012:changeSet/samcli-deploy1576228579/a9ee2685-452f-47d0-851f-92e2ff217c9c


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y     ★変更セットを確認し、デプロイ実行

2019-12-13 18:16:38 - Waiting for stack create/update to complete

CloudFormation events from changeset
-------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                       ResourceType                         LogicalResourceId                    ResourceStatusReason
-------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                   AWS::IAM::Role                       HelloWorldFunctionRole               -
CREATE_IN_PROGRESS                   AWS::IAM::Role                       HelloWorldFunctionRole               Resource creation Initiated
CREATE_COMPLETE                      AWS::IAM::Role                       HelloWorldFunctionRole               -
CREATE_IN_PROGRESS                   AWS::Lambda::Function                HelloWorldFunction                   -
CREATE_IN_PROGRESS                   AWS::Lambda::Function                HelloWorldFunction                   Resource creation Initiated
CREATE_COMPLETE                      AWS::Lambda::Function                HelloWorldFunction                   -
CREATE_IN_PROGRESS                   AWS::ApiGateway::RestApi             ServerlessRestApi                    -
CREATE_IN_PROGRESS                   AWS::ApiGateway::RestApi             ServerlessRestApi                    Resource creation Initiated
CREATE_COMPLETE                      AWS::ApiGateway::RestApi             ServerlessRestApi                    -
CREATE_IN_PROGRESS                   AWS::Lambda::Permission              HelloWorldFunctionHelloWorldPermis   Resource creation Initiated
                                                                          sionProd
CREATE_IN_PROGRESS                   AWS::Lambda::Permission              HelloWorldFunctionHelloWorldPermis   -
                                                                          sionProd
CREATE_IN_PROGRESS                   AWS::ApiGateway::Deployment          ServerlessRestApiDeployment47fc2d5   -
                                                                          f9d
CREATE_IN_PROGRESS                   AWS::ApiGateway::Deployment          ServerlessRestApiDeployment47fc2d5   Resource creation Initiated
                                                                          f9d
CREATE_COMPLETE                      AWS::ApiGateway::Deployment          ServerlessRestApiDeployment47fc2d5   -
                                                                          f9d
CREATE_IN_PROGRESS                   AWS::ApiGateway::Stage               ServerlessRestApiProdStage           -
CREATE_IN_PROGRESS                   AWS::ApiGateway::Stage               ServerlessRestApiProdStage           Resource creation Initiated
CREATE_COMPLETE                      AWS::ApiGateway::Stage               ServerlessRestApiProdStage           -
CREATE_COMPLETE                      AWS::Lambda::Permission              HelloWorldFunctionHelloWorldPermis   -
                                                                          sionProd
CREATE_COMPLETE                      AWS::CloudFormation::Stack           mystack                              -
-------------------------------------------------------------------------------------------------------------------------------------------------

Stack mystack outputs:
-------------------------------------------------------------------------------------------------------------------------------------------------
OutputKey-Description                                                    OutputValue
-------------------------------------------------------------------------------------------------------------------------------------------------
HelloWorldFunctionIamRole - Implicit IAM Role created for Hello World    arn:aws:iam::0123456789012:role/mystack-
function                                                                 HelloWorldFunctionRole-1OUZSV1ENAMCB
HelloWorldApi - API Gateway endpoint URL for Prod stage for Hello        https://xxxxxxxxxx.execute-api.ap-
World function                                                           northeast-1.amazonaws.com/Prod/hello/
HelloWorldFunction - Hello World Lambda Function ARN                     arn:aws:lambda:ap-northeast-1:0123456789012:function:mystack-
                                                                         HelloWorldFunction-1IFZY75A4HM3V
-------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - mystack in ap-northeast-1

作成された samconfig.toml は以下のようになります。
tomlとは設定ファイルを定義するためのフォーマットの1種で、CloudWatch Agentなどでも採用されています。

samconfig.toml
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "mystack"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx"
s3_prefix = "mystack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"

samconfig.tomlがプロジェクトのルートディレクトリに存在していれば
sam deploy コマンドだけでデプロイを行うことができます。

$ sam deploy

        Deploying with following values
        ===============================
        Stack name                 : mystack
        Region                     : ap-northeast-1
        Confirm changeset          : True
        Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx
        Capabilities               : ["CAPABILITY_IAM"]
        Parameter overrides        : {}

Initiating deployment
=====================
Uploading to mystack/65113397247fb02f6ff4bad985a3240c  2421730 / 2421730.0  (100.00%)
Uploading to mystack/38b6c0616d6e55bdcc1a0863b917b078.template  1130 / 1130.0  (100.00%)

Waiting for changeset to be created..
~以下省略~

OutputValueに出力されたAPI Gatewayのエンドポイントに接続すると、正常に応答が得られました。

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world"}

sam package

SAMで作成したアプリケーションをパッケージ化できます。
具体的には以下のことを行っています。

  • Lambda関数デプロイ用のZIPファイルを作成し、指定したS3バケットにアップロード
  • CodeUriの参照を、ローカルからアップロードしたS3のパスに書き換え

これらの処理は v0.33.1 以降であれば、sam deploy コマンド内で暗黙的実行されているため
個別にsam package コマンドを使用する必要はありません。
v0.33.1 以前のように sam package と sam deploy 個別に実行しデプロイすることも可能です。

$ sam package --template-file template.yaml \
  --s3-bucket <bucket-name> --output-template-file packaged.yaml
Uploading to 65113397247fb02f6ff4bad985a3240c  2421730 / 2421730.0  (100.00%)

Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
sam deploy --template-file C:\test\sam-app\packaged.yaml --stack-name <YOUR STACK NAME>

$ sam deploy --template-file packaged.yaml --stack-name mystack --capabilities CAPABILITY_IAM

返されたSAMテンプレートを確認するとCodeUriがS3のパスに書き換わっていることがわかります。

 Resources:
   HelloWorldFunction:
     Type: AWS::Serverless::Function
     Properties:
-      CodeUri: hello-world/
+      CodeUri: s3://<bucket-name>/65113397247fb02f6ff4bad985a3240c
       Handler: app.lambdaHandler
       Runtime: nodejs12.x

sam publish

SAMで作成したアプリケーションをAWS Serverless Application Repositoryに公開できます。
publish を実行するには SAMテンプレート内のMetadataに公開のために必要な情報を
定義する必要があります。LICENSE.txt および、README.md も用意しておく必要があります。

template.yaml
# 以下のメタデータが必要
Metadata:
  AWS::ServerlessRepo::Application:
    Name: my-app
    Description: hello world
    Author: user1
    SpdxLicenseId: Apache-2.0
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README.md
    Labels: ['tests']
    HomePageUrl: https://github.com/user1/my-app-project
    SemanticVersion: 0.0.1
    SourceCodeUrl: https://github.com/user1/my-app-project

またServerless Application Repository が アーティファクトが格納されているs3バケットを
参照することができるように、対象のバケットに以下のようなポリシーの追加が必要です。

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service":  "serverlessrepo.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<your-bucket-name>/*"
        }
    ]
}

テンプレートファイル内のLicenseUrl, ReadmeUrl, CodeUriはS3上のファイルを指定する必要があるため
前述の sam package コマンドでパッケージ化後、publishを行います。

$ sam package --template-file template.yaml \
  --s3-bucket <bucket-name> --output-template-file packaged.yaml
~出力は省略~

LICENSE.txt および README.md も package コマンドにより S3にアップロードされ
Urlが書き換わっていることがわかります。

 Metadata:
   AWS::ServerlessRepo::Application:
     Name: my-app
     Description: hello world
     Author: user1
     SpdxLicenseId: Apache-2.0
-     LicenseUrl: LICENSE.txt
+     LicenseUrl: s3://<bukcet-name>/d41d8cd98f00b204e9800998ecf8427e
-     ReadmeUrl: README.md
+     ReadmeUrl: s3://kosu-test/2a258235395cbaff804bbaab61a6d4ec
~以下略~

パッケージ後のテンプレートとリージョンを指定して publish します。
アプリケーションはプライベートアプリケーションとしてSARに登録されます。

$ sam publish --template packaged.yaml --region ap-northeast-1
Created new application with the following metadata:
{
  "Name": "my-app",
  "Description": "hello world",
  "Author": "user1",
  "SpdxLicenseId": "Apache-2.0",
  "LicenseUrl": "s3://<bucket-name>/d41d8cd98f00b204e9800998ecf8427e",
  "ReadmeUrl": "s3://<bucket-name/2a258235395cbaff804bbaab61a6d4ec",
  "Labels": [
    "tests"
  ],
  "HomePageUrl": "https://github.com/user1/my-app-project",
  "SemanticVersion": "0.0.1",
  "SourceCodeUrl": "https://github.com/user1/my-app-project"
}
Click the link below to view your application in AWS console:
https://console.aws.amazon.com/serverlessrepo/home?region=ap-northeast-1#/published-applications/arn:aws:serverlessrepo:ap-northeast-1:0123456789012:applications~my-app

SARのコンソールを確認すると、正常にアプリケーションが登録され
プライベートアプリケーションとして利用可能になっています。
image.png
image.png

その他の機能

sam logs

デプロイ済みの Lambda 関数のログを取得できます。
--tail でログをたれ流したり、--filter "文字列" でフィルタリングすることもできます。
以下では CloudFormation のスタック名と関数名を指定しています。

$ sam logs -n HelloWorldFunction --stack-name mystack
2018/07/20/[$LATEST]c1e4c40fbbf2495aa91ecff5b21ce24d 2018-07-20T07:24:37.835000 START RequestId: f5664465-8bed-11e8-994b-8d4b4bdcc800 Version: $LATEST
2018/07/20/[$LATEST]c1e4c40fbbf2495aa91ecff5b21ce24d 2018-07-20T07:24:38.395000 END RequestId: f5664465-8bed-11e8-994b-8d4b4bdcc800
2018/07/20/[$LATEST]c1e4c40fbbf2495aa91ecff5b21ce24d 2018-07-20T07:24:38.395000 REPORT RequestId: f5664465-8bed-11e8-994b-8d4b4bdcc800  Duration: 542.93 ms     Billed Duration: 600 ms        Memory Size: 128 MB     Max Memory Used: 22 MB

もしくは関数名を直接指定してもよいので、AWS SAM CLI や CloudFormation で
デプロイされたかどうかは関係なく、全てのLanmbda関数に対して実行できます。
また フィルタリングした結果を tail するなどの組み合わせもできるので
トラブルシューティングを迅速に行うことができます。

$ sam logs -n ECRScanToSlack --tail --filter "INFO"
2019/12/12/[$LATEST]76c0c056a02541a8bf7b2568e9e8b06d 2019-12-12T08:32:26.793000 [INFO]  2019-12-12T08:32:26.792Z
        506de805-fb06-426f-b6b0-e89f2eb23660    Found credentials in environment variables.
2019/12/12/[$LATEST]76c0c056a02541a8bf7b2568e9e8b06d 2019-12-12T08:32:28.411000 [INFO]  2019-12-12T08:32:28.408Z
        506de805-fb06-426f-b6b0-e89f2eb23660    Message posted.
2019/12/12/[$LATEST]76c0c056a02541a8bf7b2568e9e8b06d 2019-12-12T08:34:32.572000 [INFO]  2019-12-12T08:34:32.572Z
        3dcde807-f150-4056-bb12-b791b77d0731    Message posted.
2019/12/12/[$LATEST]76c0c056a02541a8bf7b2568e9e8b06d 2019-12-12T08:34:35.812000 [INFO]  2019-12-12T08:34:35.811Z
        5ac31616-be63-4402-90ab-4d9b0b74a1a6    Message posted.

Lambda Layersへの対応

AWS SAM CLI は Labmda Layersを使用した関数のローカル実行にも対応しています。(v0.8.1以降)
以下は Lambda Layers を含むSAMテンプレートの例です。

template.yaml
# 該当箇所を抜粋
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Layers:
        - arn:aws:lambda:ap-northeast-1:0123456789012:layer:python-boto3:3 # Layerの指定

ローカル実行時、AWS SAM CLIは対象のLayerのパッケージをダウンロードし、ローカルにキャッシュします。
その後、docker-lambda のイメージをベース に対象Layerのキャッシュがオーバーレイされた
Dockerイメージが自動でビルドされ、そのイメージを使用して実行が行われます。

$ sam local invoke
Invoking app.lambda_handler (python3.8)
Downloading arn:aws:lambda:ap-northeast-1:0123456789012:layer:python-boto3  [####################################]  8076250/8076250
Image was not found.
Building image...
Requested to skip pulling images ...

Mounting C:\test\sam-app2\hello_world as /var/task:ro,delegated inside runtime container
START RequestId: bfee13e1-74be-1953-d236-aa7e0bc13d8e Version: $LATEST
END RequestId: bfee13e1-74be-1953-d236-aa7e0bc13d8e
REPORT RequestId: bfee13e1-74be-1953-d236-aa7e0bc13d8e     Init Duration: 310.57 ms        Duration: 4.80 ms      Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 23 MB

{"statusCode":200,"body":"{\"message\": \"hello world\"}"}

# samcli/lambda が Layer が展開された後のdocker-lambdaのイメージ
$ docker images
REPOSITORY             TAG                                   IMAGE ID            CREATED             SIZE
samcli/lambda          python3.8-a84bc9cd09799899970128014   c37be1104bea        2 hours ago         493MB
lambci/lambda          python3.8                             95403fd346c3        11 days ago         448MB

AWS Toolkit と AWS SAM CLIによるデバッグ

各種IDEに対応した AWS Toolkit は AWS SAMとの親和性が高く、
SAMアプリケーションの作成、デバッグ、デプロイを行うことができます。
以下は AWS Toolkit for Visual Studio Code 拡張でNode.jsランタイムの
Lambda関数のデバッグを行う例です。

デバッグの起動構成に以下のような構成を追加します。(.vscode/launch.json)

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to SAM CLI",
            "type": "node",
            "request": "attach",
            "address": "localhost",
            "port": 5858,
            "localRoot": "${workspaceRoot}/hello-world",
            "remoteRoot": "/var/task",
            "protocol": "inspector",
            "stopOnEntry": false
        }
    ]
}

-d, --debug-port オプションを指定し、sam local invoke します。

$ sam local invoke -e ./events/event.json -d 5858
Invoking app.lambdaHandler (nodejs10.x)

Fetching lambci/lambda:nodejs10.x Docker container image......
Mounting C:\vscode\sam-app\hello-world as /var/task:ro,delegated inside runtime container
Debugger listening on ws://0.0.0.0:5858/3e80f80b-bbaa-4a6c-b087-7f2f3fd46b6b
For help, see: https://nodejs.org/en/docs/inspector

Lambda関数がデバッグモードで起動しますので、Visual Studio Code側で設定した
起動設定からデバッグを開始することができます。
image.png

参考

AWS Serverless Application Model - Developer Guide
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html
GitHub - aws-sam-cli Releases
https://github.com/awslabs/aws-sam-cli/releases

以上です。
参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Aurora PostgreSQL 11.4 互換版がひっそりと出ていたので試してみた

こちらは PostgreSQL Advent Calendar 2019 17 日目のエントリです。

昨日(16 日目)は tom-sato さんでした。

実は、元々 Alibaba Cloud の POLARDB PostgreSQL 互換版で oss_fdw 機能を試そうとしたのですが、残念ながら実装が追い付いていなかったようなので、今回は AWS Aurora に戻ってきました。

そして、当アドベントカレンダーの中で一番内容の薄いエントリとなります(このアドベントカレンダー作ったの誰だよ?←私だ!)。

Aurora PostgreSQL 11.4 互換版

re:Invent 2019 開催直前の 2019/11 下旬にリリースされていました。

今回は東京リージョン(ap-northeast-1)も同時リリースだったようです。

PostgreSQL 11 の新機能については、こちらにあるページのリンク先を参照してください。

とはいえ、本家 PostgreSQL 11 は 2018/10 にリリース、次バージョンの PostgreSQL 12 も 2019/10 にリリースされているので、ほぼ 1 年遅れのタイミングとなります。

試す内容(1):10.5 互換版登場時に試したパラレルスキャンを R5 インスタンスで

こちらの記事と同じ内容を実行してみます。

ただし、Aurora は、2019/2 に R5 インスタンスをサポートしています。

※PostgreSQL 互換版の R5 インスタンスサポートが同時だったかどうは記憶にございません…T3 インスタンスは遅れてサポートされたようですが。

前回は R4 インスタンスで 10.5 互換版を試しましたが、今回は、R5 インスタンス(db.r5.large)で

  • 10.7 互換版
  • 11.4 互換版

を試します。

また、

の結果(POLARDB PostgreSQL 11.2 互換版)も引用します。

結果

インスタンスタイプ/バージョン シーケンシャル 1 回目 シーケンシャル 2 回目 インデックス 1 回目 インデックス 2 回目
db.r4.large/Aurora 10.5 ※昨年 5,707.766 2,921.991 42,457.903 493.760
4C16GB/POLARDB 11.2 5,388.365 968.011 6,091.151 264.254
db.r5.large/Aurora 10.7 4,405.818 2,462.651 29,931.351 439.518
db.r5.large/Aurora 11.4 4,048.994 2,314.962 32,709.778 426.766

それぞれの 1 回目が共有バッファにデータが載っていない状態、2 回目が載っている状態です。

  • R5 インスタンスは R4 と比べて高速(特にストレージアクセス)
  • PostgreSQL 10 と 11 の間では、大きな差は無し
  • Aurora と POLARDB の差は CPU コア数とストレージ構成の違いによるもの?

なお、Aurora 10.7 と 11.4 で 11.4 のほうが結果が悪くなっていますが、何度か実行していると「外れインスタンス」を掴むことがあり、これが影響している可能性があります(時間の都合で少ない試行回数の平均を取っています)。

Aurora R5 と POLARDB の違いですが、

  • CPU Core は Aurora が 2 に対し POLARDB が 4(AWS での m5.xlarge に近い)
  • ストレージは Aurora が 3AZ-6Copy、POLARDB が 1AZ-3Copy。
  • 当該インスタンスのストレージ I/O 帯域は Aurora が 4.75Gbps に対して POLARDB が 4Gbps
    • 2019/12/16 現在、AWS の日本語マニュアルの情報は古いです(× 3.5Gbps →〇 4.75Gbps)

なので、

  • メモリ上のデータを使った処理は CPU Core 数の分 POLARDB 有利(パラレルスキャンの負荷など)
  • ランダムで細かいストレージアクセスがある場合も POLARDB 有利(別 AZ からの ACK を待たなくても良いから?)
  • シーケンシャルなストレージアクセスの場合は Aurora のほうが多少有利?(遅延よりもI/O 帯域?)

ということでしょうか。

試す内容(2):パーティショニング

PostgreSQL 10 / 11 の新機能と言えば「宣言的パーティショニング」ですね(やや強引)。

というわけで、今回は、

  • 先のテストパターンのうち、インデックススキャンのほうのテーブル定義にパーティショニングを含める
  • 10 の宣言的パーティショニングでは PK が設定できないため number 列の PK を外す
  • 11 では親テーブルに PK を設定できるのでそちらの形で試す(PK の一部にパーティションキーを含める必要があるのでid列を単独のインデックスではなく PK の 2 列目に含める)

で試してみます。

なお、完全に手抜きですが、WHERE id < 10の条件に合わせてid09のパーティションと10以上のパーティションの 2 つに分けただけ、です。

※元記事に書いた通り、idというネーミングが完全に罠になっています(ごめんなさい)。

10用のテーブル定義
CREATE TABLE table_i (number SERIAL NOT NULL, id INT, value INT) PARTITION BY RANGE (id);
CREATE TABLE table_i_1 PARTITION OF table_i FOR VALUES FROM (0) TO (10);
CREATE TABLE table_i_2 PARTITION OF table_i FOR VALUES FROM (10) TO (100);
CREATE INDEX idx_id_1 ON table_i_1 (id);
CREATE INDEX idx_id_2 ON table_i_2 (id);
11用のテーブル定義
CREATE TABLE table_i (number SERIAL NOT NULL, id INT, value INT) PARTITION BY RANGE (id);
CREATE TABLE table_i_1 PARTITION OF table_i FOR VALUES FROM (0) TO (10);
CREATE TABLE table_i_2 PARTITION OF table_i FOR VALUES FROM (10) TO (100);
ALTER TABLE table_i ADD PRIMARY KEY(number, id);

ちなみに、パーティショニングで気になるINSERTの速度ですが、パーティショニング無しと比較して概ね 10 ~ 20% 程度余分に時間が掛かりました(ばらつきが大きいので値を掲示するのは控えます)。

結果

バージョン/パーティショニング有無 インデックス 1 回目 インデックス 2 回目
Aurora 10.7/無 29,931.351 439.518
Aurora 10.7/有 540.669 331.789
Aurora 11.4/無 32,709.778 426.766
Aurora 11.4/有 539.339 355.797

パーティショニング無しの結果は(1)からの引用です。

  • 共有バッファに載っていないと劇的に高速化
    • WHERE句の条件と完全にマッチする形で分割したため
  • パーティショニング有りでも 10 と 11 の差はほぼ無し

でした。

Aurora の場合、テーブル空間を別ストレージに分離する手法は取れないため、10 に対する 11 のアドバンテージは性能向上ではなくて

  • パーティション間のデータ移動ができる
  • デフォルトパーティションを作ることができる
  • 外部キーなどの制約を使うことができる

あたりでしょうか。

余談

Aurora PostgreSQL 互換版、この↓機能のおかげで便利にはなったものの、「共有バッファにデータが載っていない」状態でテストすることが難しくなってしまいました(設定で OFF にしても「あれ?載ってるのでは?」という挙動が何度も…)。


PostgreSQL Advent Calendar 2019 の明日(18 日目)は sira さんです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む