AWSとAzureとGCPを比較してみる – FaaS編

FaaSについてAWSとAzureとGCPを比較してみました。

注)

1. FaaS比較表

AWS Azure GCP
Lambda Functions Cloud Functions***
言語 Python,
node.js,
java,
C#,
go
ランタイムバージョン1.X
C#,
JavaScript,
F#,
Python*,
PHP*,
TypeScript*,
バッチ (.cmd、.bat)*,
Bash*,
PowerShell*

ランタイムバージョン2.X
C#**,
JavaScript**,
Java**
node.js***
最大実行時間 5分 10分 (従量課金プラン)
無制限 (App Serviceプラン)
9分
直接HTTPアクセスを受け付けるか 受け付けない(API Gatewayと連携必要) 受け付ける 受け付ける
トリガー Amazon S3,
Amazon DynamoDB,
Amazon Kinesis Data Streams,
Amazon Simple Notification Service,
Amazon Simple Email Service,
Amazon Cognito,
AWS CloudFormation,
Amazon CloudWatch Logs,
Amazon CloudWatch Events,
AWS CodeCommit,
スケジュールされたイベント (Amazon CloudWatch Events を使用),
AWS Config,
Amazon Alexa,
Amazon Lex,
Amazon API Gateway,
AWS IoT ボタン,
Amazon CloudFront,
Amazon Kinesis Data
Blob Storage,
Cosmos DB,
Event Hubs,
HTTP,
Microsoft Graph Events(2.Xのみ),
Queue storage,
Service Bus,
Timer,
Webhooks(1.Xのみ)
HTTP,
Cloud Storage,
Cloud Pub/Sub

*試験段階
**プレビュー
***ベータ

2. 対応言語の比較

言語の種類は試験段階とプレビューを含めればAzureが一番多いのですが、正式リリースされたものに限定すればAWSの方が種類が多いです。
一方GCPは機能自体がベータリリースなので、まだこれからといった感じでしょうか。

AzureはBashにも対応しているのが特徴です。運用系のシェルスクリプトをFaaS化すれば、スクリプト用のサーバが不要になりますね。

3. 最大実行時間

最大実行時間はAzureの10分(要host.jsonのfunctionTimeoutプロパティ変更)、GCPの9分に対しAWS Lamdbaは5分と約半分です。実際にAWS Lambdaを利用していると5分の壁を結構感じます。この点は他クラウドが羨ましいですね。
2017年のRe:InventでAWSはFargateというコンテナのサービスをリリースしましたが、このサービスがlambdaが5分以上実行できないことに対するAWSからの回答のように感じます。

4. 直接HTTPアクセスを受け付けるか

AWS lambdaだけ直接HTTPアクセスを受け付けることができません。HTTPアクセスを受け付けるには、API Gatewayと連携する必要がありますが、多機能な分やや設定が面倒な印象です。(但しAPI経由でLambdaを起動することは可能)

まとめ

AWS Lambdaのリリース後、Azure・GCP・Bluemix(現IBM Cloud)は超特急で追従しました。AWS LambdaがIT業界に与えたインパクトはとても大きかったと思います。
現在は「FaaS無ければばクラウドにあらず」といったところでしょうか。

また、AWS GreengrassやAzure IoT Edge**というエッジにデプロイするサービスも出てきています。
将来AWS LambdaがiPhoneやApple Watchにデプロイできるようにならないかなーと妄想中です。

**プレビュー

続きを読む

Dynamic DNS creation for core nodes in case of Autoscaling – AWS

パイソン & Linux Projects for $30 – $250. We wanted to implement the Dynamic creation/Deletion of DNS for nodes, when the AWS EMR cluster Autoscales up and down for Route53 using Autoscaling cloudwatch events that triggers a lambda function… 続きを読む

CloudWatchLogsAPIとfluent-plugin-s3を使ったLambdaログの保管と分析、可視化について

はじめに

これは、Sansan Advent Calendar 2017の24日目の記事です。

  • CloudWatchLogsはログを時系列で絞込検索がしにくいとか、見にくいし使いにくいツールである
  • 本番でLambdaの関数がxx 個以上動いており、障害発生時の調査にはCloudWatchLogsを頑張って使うしかない
  • apexでログを見る方法もないわけではないが、ずっとtailするのは難しい
  • CloudWatchLogsにずっと置いとくのもアレ
  • CloudWatchLogs早くいい感じになって欲しい

ということですでにfluentdなどで収集しているアプリケーションログやアクセスログと同様にS3に保管し、Elasticsearchに乗せてKibanaで検索、分析できるようにしたかったのです。CloudWatchLogsAPIとfluent-plugin-s3を使ってLambdaログの保管と分析、可視化をできるようにしました。

あきらめた案

fluent-plugin-cloudwatch-logsで収集しようともしていました。しかし、新たに生み出されるLogStreamの収集ができず、理由もよくわからず断念した。fluentdをpryで止めて結構デバッグしたものの、fluentd力もっとほしい…!となりました。
Lambdaのログは、CloudWatchLogsのLogGroup以下にLogStreamとしてつくられていくが、LogStreamに[$LATEST]というLambdaのバージョンプレフィックス的なやつが入るため、fluentdのbuffer_path設定と相性が悪かったです。(この仕様マジでやめてほしい。括弧とかドル記号とか入らないでほしい。)
また、50個以上のLogStreamsがある際に、このpluginではログを扱うArrayが予期せぬ入れ子構造になってしまっていたため修正しました。
どちらもマージされてはいるが、力不足によりこのプラグインでは収集を完遂できませんでした。

https://github.com/ryotarai/fluent-plugin-cloudwatch-logs/pull/80
https://github.com/ryotarai/fluent-plugin-cloudwatch-logs/pull/84

詳細な説明

ピタゴラ装置

Lambda

まずは、CloudWatchLogsのAPIを叩くためのLambdaをつくりました。Python3.6で実装しました。
このLambdaは、CloudWatchLogs.Client.create_export_taskを叩いています。
http://boto3.readthedocs.io/en/latest/reference/services/logs.html#CloudWatchLogs.Client.create_export_task

しかし、エクスポート対象にする期間の指定はコード上に置いておきたくありませんでした。(create_export_taskのfrom, toの指定)
そのため、前回実行時のタイムスタンプをS3上に置くようにし、これが無ければ現在からn時間分エクスポートし、あればfromにセットするという実装にしました。こうすることで、実行タイミングはCloudWatch EventsのRuleのみで与えられるようになります。

Lambdaの周辺

fluent-plugin-s3では、S3からログを取り込むことができます。S3イベント通知でSQSに流しています。あとはわりと普通です。

cloudwatchlogs-import-after.png

ちなみに

アレがこれであーなので、実はあと一歩のところで本番投入できていません。年内にはやっておきたいです。
現在の職場での特異的な話が絡むfluent-s3-plugin関連の設定についてなので、根底から覆るような話ではありません。もし同じような構成を考えている人がいたら安心してほしいです。

最後に

CloudWatchLogsが使いやすくなることや、Lambdaのログが自動的にS3へエクスポートし続ける設定がほしいです。LambdaのためにLambdaを作ることが減るといいなぁと思いますので、サンタさん(AWS)何卒よろしくお願いします。

続きを読む

OpsWorksのTime-based instancesをLambdaで無理やり日付ベースで稼働設定する

前置き

こんにちは。mediba advent calendar 2017 21日目担当のmoriです。

最近はめっきりコードを書くことが減り、コーディング技術が錆びついている状態で、さて何をやろうかと考えてはみたものの良い題材が思い浮かびませんでした。

なので、1年前にこの記事でやろうとしていた、
「OpsWorksのTime-based instancesを日付ベースで設定できるようにする」
という試みを、今の自分の知識でやってみようかと思います。

ものすごい成長したというわけではないですが、さすがに1年前の自分には勝てるでしょう。(というか記事を読み返すと、正直すごくイケてない)

OpsWorksでこんな事をやって建設的なのかどうかは余所へ置いておいて、
実際問題としてTime-based設定をするのに、マウスをクリックし続けるのは御免です。

やりたいこと

本記事のタイトルの通り、OpsworksのTime-basedを日付ベースで稼働設定できるようにすることです。要点は以下。

  • 特定のインスタンスを、毎月イベントがある3、13、23日だけ稼働させる。

    • 厳密にはイベント前日23時から、イベント翌日11時まで動かす。
    • それ以外は停止。

身も蓋もなく言えば、cronでその日にインスタンスをSTARTさせるシェルでも作れば良いかもしれませんが、肝心のその日にシェルが動かなかったりといった失敗した時も考えて、敢えてTime-basedに拘ってみます。

今回のポイント

  • Time-basedは曜日ベースでしか稼働設定できない。
  • Lambdaでやる。(時代はサーバーレス)
  • Pythonが自分的にアツイ気がするので採用。(今回初めて書く)
  • S3に設定ファイルを置く。

これに加え、1年前は時間と知識と技術が足りてなくて手が回ってなかったところも極力カバーできるように、ざっくりLambda関数を作って見ました。

実践

■Lambdaで使用するロールに、必要なポリシーを追加

        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::xxx/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "opsworks:*"
            ],
            "Resource": "*"
        }
  • s3のResourceは、設定ファイルを配置するバケットを指定。(バケットの作成はここでは省略)
  • opsworksの権限は緩めに設定しているが、本来は対象のスタックに限定するべき。

■s3バケットに設定ファイルをアップロード

今回設定ファイルとして使うのは以下の3つ。
これらを任意のバケットにアップロードします。

1.インスタンスを動かす対象日の設定

event_calendar.json
[3, 13, 23]

2.対象のインスタンスIDリスト

  • ここで指定するのは「EC2 instance ID」ではなく、「OpsWroks ID」なので注意
  • 一応環境の違いに対応するため、ファイル名の頭に「dev」をつけている。
dev_instance_list.json
[
 "abcdefgh-1234-43aa-9876-22222ce7733c",
 "stuvwxyz-5678-43bb-1234-33333f165c08"
]

3.Time-basedに設定する稼働スケジュール

  • 「イベント前日23時から、イベント翌日11時まで動かす。」

    • これを実現するために、イベントとその前後3日分のスケジュールを記載。
    • 「Before」がイベント前日、「Target」がイベント当日、「After」がイベント翌日
schedule.json
{
 "Before": {
  "0": "off",
  "1": "off",
  "2": "off",
  "3": "off",
  "4": "off",
  "5": "off",
  "6": "off",
  "7": "off",
  "8": "off",
  "9": "off",
  "10": "off",
  "11": "off",
  "12": "off",
  "13": "off",
  "14": "on",
  "15": "on",
  "16": "on",
  "17": "on",
  "18": "on",
  "19": "on",
  "20": "on",
  "21": "on",
  "22": "on",
  "23": "on"
 },
 "Target": {
  "0": "on",
  "1": "on",
  "2": "on",
  "3": "on",
  "4": "on",
  "5": "on",
  "6": "on",
  "7": "on",
  "8": "on",
  "9": "on",
  "10": "on",
  "11": "on",
  "12": "on",
  "13": "on",
  "14": "on",
  "15": "on",
  "16": "on",
  "17": "on",
  "18": "on",
  "19": "on",
  "20": "on",
  "21": "on",
  "22": "on",
  "23": "on"
 },
 "After": {
  "0": "on",
  "1": "on",
  "2": "on",
  "3": "off",
  "4": "off",
  "5": "off",
  "6": "off",
  "7": "off",
  "8": "off",
  "9": "off",
  "10": "off",
  "11": "off",
  "12": "off",
  "13": "off",
  "14": "off",
  "15": "off",
  "16": "off",
  "17": "off",
  "18": "off",
  "19": "off",
  "20": "off",
  "21": "off",
  "22": "off",
  "23": "off"
 }
}

■Lambda関数を作成

1.Labmda関数を新規作成

  • ランタイムは今回は「Python 3.6」を指定。
  • ロールは最初にポリシーを設定したものを使用。

スクリーンショット 2017-12-22 10.55.53.png

2.環境変数を定義。

今回、環境変数として設定するのは以下の4つです。

  • S3_BUCKET ・・・ 設定jsonファイルをアップロードしているバケット名
  • EVENT_SCHEDULE_JSON ・・・ 上記作った「Time-basedに設定する稼働スケジュール」のファイル名
  • ENV ・・・ 実行される環境。今回は上記の「対象のインスタンスIDリスト」作成の際で触れたように「dev」環境とする。
  • EVENT_CALENDAR ・・・ 対象となるイベント日リストのjsonファイル

スクリーンショット 2017-12-22 13.56.15.png

3.コード本体

初Pythonなので、ツッコミどころ満載だとは思いますが、少なくとも動くはずです。

time-based-ctl.py
import os
import boto3
import json
import datetime

def lambda_handler(event, context):
    # Time-basedは曜日指定で設定なので、それ用の配列を用意
    weekday_list = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

    # s3とopsworksのclient
    s3 = boto3.resource('s3')
    s3_client = s3.meta.client
    ops_works_client = boto3.client('opsworks', region_name='us-east-1')

    # s3のbucket名を環境変数から取得
    bucket_name = os.environ['S3_BUCKET']

    # 設定jsonファイルのオブジェクトキーを環境変数から取得    
    obj_key_event = os.environ['EVENT_SCHEDULE_JSON']
    obj_key_event_calendar = os.environ['EVENT_CALENDAR']
    obj_key_instance_list = os.environ['ENV'] + "_instance_list.json"

    # 設定json取り込み(tryでやってみる)
    try:
        # 稼働スケジュールの設定を取得
        response_event = s3_client.get_object(Bucket = bucket_name, Key = obj_key_event)
        schedule_json_event = json.loads(response_event['Body'].read())

        # 対象のインスタンスを取得
        response_instance_list = s3_client.get_object(Bucket = bucket_name, Key = obj_key_instance_list)
        instance_list = json.loads(response_instance_list['Body'].read())

        # 対象日リストを取得        
        event_calendar = s3_client.get_object(Bucket = bucket_name, Key = obj_key_event_calendar)
        event_calendar_list = json.loads(event_calendar['Body'].read())

    except Exception as e:
        print(e)


    # 今日の日付を取得。UTCなので、日本時間にする。(他に方法あるかも)
    today = datetime.datetime.now() + datetime.timedelta(hours = 9)
    # この変数の意味は後述
    skip = False

    # ここからは個別に解説
    # [A]forループ
    for target_idx in range(2, 6): 
        # 対象日の曜日を特定しセット
        target_day = today + datetime.timedelta(days = target_idx)
        tareget_weekday = weekday_list[target_day.weekday()]

        # [B] 1つ前のループで対象日の稼働スケジュール設定をしていたらスキップ
        if skip and target_day.day not in event_calendar_list:
            skip = False
            continue
        elif target_day.day in event_calendar_list:
            # 対象日の前日と翌日を特定
            before_day = target_day - datetime.timedelta(days = 1)
            after_day = target_day + datetime.timedelta(days = 1)

            if skip:
                scuedule_setting = {
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }
            else:
                scuedule_setting = {
                    weekday_list[before_day.weekday()]: schedule_json_event['Before'],
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }

            # インスタンスの分だけ、Time-baseのセットを実施
            for target_instance in instance_list:
                response = ops_works_client.set_time_based_auto_scaling(
                                                         InstanceId = target_instance,
                                                         AutoScalingSchedule = scuedule_setting,
                                                         )
            # 対象日前後の稼働スケジュールをいじっているので、次ループはスキップさせる。
            skip = True
        else:
            # 対象日以外は停止させて置く必要があるので、そのように設定。
            for target_instance in instance_list:
                response = ops_works_client.set_time_based_auto_scaling(
                                                                         InstanceId = target_instance,
                                                                         AutoScalingSchedule = { weekday_list[target_day.weekday()]: {}}
            skip = False

[A]の補足

    for target_idx in range(2, 6): 
        # 対象日の曜日を特定しセット
        target_day = today + datetime.timedelta(days = target_idx)
        tareget_weekday = weekday_list[target_day.weekday()]

rangeを「2,6」で回している理由
Time-basedの設定ベースが曜日ベースである都合上、1週間以上先は設定できないこと、今回は対象日だけではなくその前後日の稼働スケジュールも操作することを踏まえ、2日後〜6日後としています。
何かの拍子で、この処理が1日や2日動かなかったとしても、リカバリはできるようになっています。
※7日後は実行当日に当たる曜日の稼働スケジュールをいじることになる為、含めない。

[B]の補足

        # [B] 1つ前のループで対象日の稼働スケジュール設定をしていたらスキップ
        if skip and target_day.day not in event_calendar_list:
            skip = False
            continue
        elif target_day.day in event_calendar_list:
            # 対象日の前日と翌日を特定
            before_day = target_day - datetime.timedelta(days = 1)
            after_day = target_day + datetime.timedelta(days = 1)

            # 前ループが対象日だった場合、稼働スケジュール設定の対象から外す
            if skip:
                scuedule_setting = {
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }
            else:
                scuedule_setting = {
                    weekday_list[before_day.weekday()]: schedule_json_event['Before'],
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }

スキップの意味
対象日の設定が2日連続だった場合の対応です。スキップしないと、稼働スケジュールがイベントの日ように設定したものが、完全停止状態に上書きされてしまいます。

elifでもスキップ判定をみているのも同様の理由です。(この場合は、イベント当日設定の24時間稼働が、イベント前日の設定に上書きされてしまう)

■lambda関数を動かす

あとはトリガーを[CloudWatch Events]で、cron設定にて1日1回動くように設定するだけです。

まとめ

とまぁ、駆け足で実装をしてみました。
これでOpsworksのTime-basedで、イベント日にほぼ確実に自動(?)スケールアウトができるようになります。

Python自体は初心者どころか、ほぼ初見のド素人な訳ですが、印象的には書きやすかったので、もう少し綺麗なコードが書けるように勉強してみようかと思います。

次はニッチな用途ではなく、普通に役立つ何かを紹介したいです・・・。

続きを読む

Amazon LightsailでもAWS Lambdaで運用して(◜◡◝)できた話

はじめに

ある案件で、社内のユーザーが使用する数台のサーバを提供しているものがありまして
その案件で先ごろ特殊な要件が出てきました。(以降、G案件と記載します)

一定期間経ったらサーバのGlobal IPアドレスを変更したい…

G案件はAmazon Lightsail(2016年末から日本リージョンに提供されたVPSサービス)で構築されています。

EC2であればLambdaで色々手を入れられるので、こうした細工は問題なくできそうに思いましたが、
AWS SDKがLightsailに対応しているか分からなかったので調査するところから始めました。

結果としては、SDKもLightsailに必要十分に対応していたので、必要なことはすべて実装できて
:smiley:な結末となりました。

ここでは、ハマった点も含めハンズオン形式で紹介してみようと思います。

どうやって実装する?

Lightsailでは静的なGlobal IPアドレスがアカウントあたり5つしか確保できない仕様があるので、
エフェメラルで設計するのが通常です。

この場合、サーバを再起動してもGlobal IPアドレスは変わりませんが、サーバを一度停止させると
IPアドレスが解放され次回の起動時に別のGlobal IPアドレスが割当てされることになります。

つまり、変更時にサーバが短時間ダウンすることを許容できれば、


  1. 影響のない時間に各サーバを停止させる
  2. 数分後に各サーバを起動させる
    (必要なサービス群などは自動で起動するよう、あらかじめ仕込んでおく)

というだけの簡単な処理で実装することができます。
本件ではユーザーもサーバログインが必要なので、変更されたGlobal IPアドレスを確認できるように、
Lightsailの画面で情報が確認できるだけの制限IAMアカウントも用意しておくことにします。

対象とシナリオ

  • 対象
    G案件のテストサーバということで下記の名前のサーバ5台をLightsailインスタンスで用意します。
test-g01
test-g02
test-g03
test-g04
test-g05
  • シナリオ
    各サーバは週1回、下記の日時にて停止/起動を行うことで運用することとします。
停止曜時 : 日曜日 22:55
起動曜時 : 日曜日 23:00

Lambdaファンクション設定①(インスタンス起動)

AWSのアカウントにサインインし、[サービス] > [Lamda] > [関数] と開きます。

01a.png

[関数の作成] をクリックして今回のファンクションを作成開始します。

02a.png

まず、ファンクションが使用するロールを定義します。
[ロール] > [カスタムロールの作成] をクリックします。

03a.png

自動的にIAMロールを作成する画面に飛ぶので、ロール名をals-instances-opと入力したうえで
[ポリシードキュメントを表示] > [編集] とクリックして、下記のjsonを記述します。

als-instances-op.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "lightsail:*"
      ],
      "Resource" : "*"
    }
  ]
}

lightsailのActionはもう少し細かく制御してもいい気もしますが、Lambdaファンクション以外では
使用しないロールなので、セキュリティも特に問題ないでしょう。

入力ができたら [許可] を押してロールを作成します。

元の画面に戻ったら、以下のように設定して [関数の作成] をクリックします。

04a.png

Factor Value
名前 als-instances-start
ランタイム Python 2.7
ロール 既存のロールを選択
既存のロール als-instances-op

作成された関数の画面に遷移したら、【関数コード】の項目を探して早速コードを書いていきましょう。

Lambdaでは現在さまざまな言語がサポートされるようになりましたが、
実装したいことに応じて特性に合わせたものを選択すると良いと思います。
今回は純粋なインフラ運用なので、計算処理などに向いたPythonが良いのではないでしょうか。

Python自体シンプルに書けるのと、やりたいことも単純なのでとても短いコードでできます:relaxed:

05a.png

まず、ファイル名はals-instances-start.pyとして、
ハンドラ名をals-instances-start.lambda_handlerに、そして以下のようなコードを書きます。

als-instances-start.py
import boto3

lightsail = boto3.client('lightsail')
instances = ['test-g01', 'test-g02', 'test-g03', 'test-g04', 'test-g05']

### lambda_handler.
def lambda_handler(event, context):
    start_instance(instances)

### definition of 'start_instance' function.
def start_instance(lists):
    for name in lists:
        log = lightsail.start_instance(
            instanceName=name
        )
        print 'started instance: ' + name
        print log

続けて、万一台数が増えた場合に実行時間が延びるといけないので「タイムアウト」は少し延ばしておきます。
とりあえず8秒くらいならまず大丈夫でしょうか。

そのまま [保存] して作成したファンクションを一旦保存します。

06a.png

保存ができたら、[テスト] をして実際にインスタンスが起動できることを確認しましょう。
※ 予め全てのインスタンスは落としておいてください。

08a.png

テストをするためには、テストイベントが必要なので自動で作成画面が立ち上がります。

09.PNG

ここではイベント名をtest01として(何でもいいです)、下部の [保存] をクリックします。
作成した関数の画面に戻ったら、[テスト] をクリックして停止インスタンスが起動することを確認して下さい。

次にこのファンクションを実行するトリガーを設定します。
今回は定時起動なので、cronのスケジュール形式で設定します。

[CloudWatch Events]をクリックします。

10a.png

【トリガーの設定】から以下のような設定をして、[追加] > [保存] と進めて設定を上書きします。

11a.png

Factor Value
ルール 新規ルールの作成
ルール名 rule-als-instances-start
ルールタイプ スケジュール式
スケジュール式 cron(00 14 ? * SUN *)
トリガーの有効化 ✔︎

保存された後に以下のようなトリガーが追加されていることを確認します。

12a.png

これで、サーバ起動の設定は完了です。

なお、スケジュール式では時刻をUTCで入力しますので注意が必要です。
詳しい仕様については下記で紹介されています。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html

Lambdaファンクション設定②(インスタンス停止)

基本的には起動と同様の設定なので、詳しい設定は割愛してコードのみを記載します。

関数の名前をals-instances-stopとして、各種の名前もそれに合わせて変えますが、
IAMロールは新しく作成せずals-instances-opを共用します。

als-instances-stop.py
import boto3

lightsail = boto3.client('lightsail')
instances = ['test-g01', 'test-g02', 'test-g03', 'test-g04', 'test-g05']

### lambda_handler.
def lambda_handler(event, context):
    stop_instance(instances)

### definition of 'stop_instance' function.
def stop_instance(lists):
    for name in lists:
        log = lightsail.stop_instance(
            instanceName=name, 
            force=False
        )
        print 'stopped instance: ' + name
        print log

起動とほぼ同様の内容ですが、強制停止をするかどうかで以下の定義を1行追加しています。

force=False

あと当然ですが、CloudWatch Events のcronスケジュール式の設定値も以下のようになります。

cron(55 13 ? * SUN *)

ユーザーが変更後のGlobal IPアドレスを確認するためのIAM

今回は「lightsail_general」というIAMユーザーを作成して、Policy Generatorで
Lightsailの読み取り権限のみを持つポリシーを生成しました。

本筋からずれるので、ここではユーザー設定の画面イメージと生成されたjsonだけ紹介します。

13.PNG

policygen-lightsail_general-201712201925.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1513765439000",
            "Effect": "Allow",
            "Action": [
                "lightsail:GetActiveNames",
                "lightsail:GetBlueprints",
                "lightsail:GetBundles",
                "lightsail:GetDomain",
                "lightsail:GetDomains",
                "lightsail:GetInstance",
                "lightsail:GetInstanceAccessDetails",
                "lightsail:GetInstanceMetricData",
                "lightsail:GetInstancePortStates",
                "lightsail:GetInstanceSnapshot",
                "lightsail:GetInstanceSnapshots",
                "lightsail:GetInstanceState",
                "lightsail:GetInstances",
                "lightsail:GetKeyPair",
                "lightsail:GetKeyPairs",
                "lightsail:GetOperation",
                "lightsail:GetOperations",
                "lightsail:GetOperationsForResource",
                "lightsail:GetRegions",
                "lightsail:GetStaticIp",
                "lightsail:GetStaticIps"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

ハマったところ

1. ドキュメント探しに時間がかかった

Lambda経験が浅かったので、SDKのドキュメントが見つけられず地味に苦労しました。
他の方が同じ轍を踏まないよう、URLを書いておきます。(JSで書くことも検討したので併せて)

2. LightsailのIAMの記載方法が特殊

IAMの権限が足りず、Lambdaファンクションのテスト実行がなかなか通らず苦労しました。

IAMでは、”Resource”で許可するリソースのカテゴリやリージョンまで絞るのがセオリーだと思います。
しかし、Lightsailコントロールの全権限を与えるユーザー定義として下記のような制御では権限が足りず
Lightsailの画面が403エラーで表示できませんでした。

lightsail_all_NG.json
{
  "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lightsail:*"
            ],
            "Resource": [
                "arn:aws:lightsail:::*"
            ]
        }
    ]
}

原因までは探りませんでしたが、下記のようにすると大丈夫でした。

lightsail_all_OK.json
{
  "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lightsail:*"
            ],
            "Resource" : "*"
        }
    ]
}

おそらくLightsailは、何らか他のAmazonリソースに依存しているのだと思います。

おわりに

いかがでしたでしょうか。

Lightsail運用については、現時点(2017年末)ではさほど多くの情報を見つけることができませんでしたが、
EC2に近いようなことはだいたい実装できるのではないかという所感でした。

簡便に使用を始められることと、安さや課金体系の分かりやすさはLightsailの大きな魅力だと思います。
ほどほどの運用も敷けそうなので、スモールサービスなどに検討してみてはいかがでしょうか。

続きを読む

Fargate の監視についての考察

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

概要

AWS Fargate の登場により、EC2インスタンスやそのクラスタを意識せずともコンテナが実行できるようになりました。AWSさん曰く、”コンテナをデプロイする最も簡単な方法”ということです。すごいですね。一方で、コンテナを本番環境に導入すると”9ヶ月でコンテナ数が5倍に増加する”や”仮想マシンより9倍早く縮退するようなライフサイクルで利用される”といったレポート1 もありますので、より流動的になるインフラに対して運用時の可視性を確保するためにFargateのモニタリングについて考えてみました。

AWS Fargate で利用可能なモニタリング

AWS FargateはこれまでのAmazon ECSと同様にCloudWatch Logs と CloudWatch Eventsをサポートしているので、awslogsを通じてアプリケーションやミドルウェアのログはCloudWatch Logsに転送することが可能で、Fargate タスクの状態変化はCloudWatch EventsのトリガとしてSNSトピックやLambda関数を実行できます。
CloudWatch メトリクスは非常にシンプルで、ECSではクラスタ単位のメトリクスをモニタしていましたが、それがサービス単位のCPUとメモリのメトリクスのみになります。

Fargate 運用の可視性を確保するための要素

システム上で起こりうるあらゆる事象に対して可視性を確保するうえで、必要なデータを 5W1H的な分類で整理してみると以下のようになります。WHATはグラフ化できるような数値データ、WHYはログや状態を示すイベント、HOWはアプリケーションのトランザクショントレース(APM, Application Performance Monitoring)として、WHENとWHEREについては基本的にモニタリングのデータは時系列データでありタグ等のメタデータが付属するため共通するものなので割愛してます。( 例えば、この分類で車の運転のモニタリングを考えると、WHATは速度などの計器類のデータ、WHYはドライバーの操作ログ、HOWはドライブレコーダー、という感じです。どれが欠けても事故の調査は難しくなるし、全部無い=モニタリングしない、を想像すると恐ろしいですねw )

Fargateの
モニタリング
WHAT WHY HOW
データのタイプ メトリクス ログ トレース
対応サービス CloudWatch
(メトリクス)
CloudWatch
Logs, Events
(なし)
データの種類 CPU, mem アプリ, ミドル (なし)
データの粒度 サービス単位 サービス単位,
タスク単位
(なし)

さてこうしてみると、Fargateがコンテナ環境のモニタリングにおいてトレースをサポートできていないのは痛いなぁと思うのと同時に、AWSのことだから割とすぐにX-RayがFargateをサポートするんだろなー…とか想像できます。2
また、ログのサポートが整っている一方で、メトリクスはCPUとメモリだけでディスクI/OやネットワークI/Oが無い、サービス単位だけでタスク単位では見られない、などまだ手薄感がありますが、こちらは今時点はECSのモニタリングの仕組みを流用してるからかなー、Docker Stats API のFargate版みたいなのすぐ出ますよねぇ?とか思うわけです。3

まとめ:Fargateはどうモニタリングするか

モニタリングのプロセスを分けて考えると、データ収集と保存–>可視化–>アラート–>分析–>ナレッジの蓄積、という感じになると思いますが、各プロセスについて以下のように対応を進めるのが良いかと思っています。

Fargateの
モニタリングプロセス
対応サービス メモ
データ収集と保存 CloudWatch,
Events, Logs
ログも忘れず収集する
可視化 CloudWatch
Dashboard
意外と使われていない気がするけど
便利なので使ったほうがいいと思う
アラート CloudWatch Eventsも併用する
分析 CloudWatch
Dashboard
作り込めば問題分析に使えるので
やはり使ったほうがいいと思う
ノウハウの蓄積 (なし) ポストモーテム用にDashboardの時間が
固定できればいいのになぁ

BIツールやELKスタックを利用することがかなり普及していて重要なデータを可視化することが良いとは良く知られていると思いきや、モニタリングデータの可視化はそれほど重要視されていない気がしてやった人のみぞ知る、、、となっている気がするので、手近なところでCloudWatch Dashboardは取り組んで見る価値が十分あるんじゃないかなと思います。4



  1. 「Docker採用の驚くべき8つの事実」 Datadogが毎年実施しているユーザー調査レポートです。コンテナのライフサイクルの短縮は毎年進んでいます。 

  2. これについては、本当に何も知らず想像で言っていますが、、多分そうなるでしょう。 

  3. Datadogは Fargateのローンチパートナーで3rdパーティのモニタリングツールとしてFargateのリリース時に紹介されていますが、実際このAPIが無いとdocker-dd-agentがうまく仕事出来ないので困るのです。このAPIが出た際は記事を更新します。 

  4. 可視化含めてモニタリングプロセスの劇的な改善をするのがDatadogの強みなのでご興味ある方は“無償トライアル”へGo!!,,,ご清聴ありがとうございました。 

続きを読む

Lightsailインスタンスのスナップショット管理

概要

Lightsailインスタンスのスナップショット管理の自動化について検討してみる。検討する機能は2つ。

  1. Lightsailインスタンスのスナップショットを取得する
  2. Lightsailインスタンスのスナップショットを削除する

いずれも設定したスケジュールに従って自動実行する。これにより、何世代かのスナップショットを残しつつ、無尽蔵にたまらないよう古いものはガーベージされる。

構成

CloudWatch EventsとLambdaの組み合わせで実現する。スナップショット作成用、スナップショット削除用でそれぞれCloudWatch EventのルールとLambdaファンクションを作成する。

実装

まずはLambdaファンクションを2つ作成する。

スナップショット作成

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import os
import datetime

client = boto3.client('lightsail')

#Lightsailのインスタンス名。Lambdaファンクションの環境変数から受け取る
name = os.environ['instancename']

def lambda_handler(event, context):

    #スナップショットファイル名に付加するタイムスタンプ
    timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')

    try:
        #スナップショットの作成
        response = client.create_instance_snapshot(
            instanceSnapshotName = name + '-' + timestamp,
            instanceName = name
        )
    except Exception as e:
        print(e)
        raise e

Lightsailのインスタンス名はLambdaファンクションの環境変数としてセットする。

スナップショット削除

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import os
import datetime

client = boto3.client('lightsail')

#Lightsailのインスタンス名。Lambdaファンクションの環境変数から受け取る
name = os.environ['instancename']

def lambda_handler(event, context):

    #Lightsailのスナップショットのリストを取得
    response = client.get_instance_snapshots()

    #インスタンス名でフィルタ
    filterdList = filter(lambda x: x['fromInstanceName'] == name, response['instanceSnapshots'])

    #作成時刻でソート
    sortedList = sorted(filterdList, key=lambda x:x['createdAt'])

    #削除対象が存在する場合
    if len(sortedList) > 0:
        try:
            #最も古いスナップショットを削除
            response = client.delete_instance_snapshot(
                instanceSnapshotName = sortedList[0]['name']
            )
        except Exception as e:
            print(e)
            raise e

この例では一番古いスナップショットを問答無用で削除する。「n世代は確実に残す」という考慮をしたい場合は、作成時刻の新しい順でソートしてn+1番目以降を削除する、といったロジックにする必要がありそう。

CloudWatch Events

CloudWatch Eventsのルールを2つ作成する。「イベントソース」は「スケジュール」を選択し、任意のスケジュールを設定。ターゲットとしてさきほど作成したLambdaファンクションを指定。その他のオプションはデフォルトのままでOK。

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(後編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。後編のこちらでは、主にAWS Lambdaでのデプロイ・運用にフォーカスします
    • Pythonプログラムのパッケージングとアップロードに際しハマった知見を共有します
    • AWS CloudWatchと連携し、指定時間での自動ツイートを実現します
  • 前編はこちらです

AWS Lambda

AWS Lambdaとは

AWS Lambda はサーバーをプロビジョニングしたり管理しなくてもコードを実行できるコンピューティングサービスです。…コードが実行中でなければ料金はかかりません。

「一定の時刻に起動してツイート」さえしてくれればいいようなbotを動かすには、常時起動のサーバは必須ではありません。Lambdaのようなプラットフォームは今回のユースケースにうってつけと言えます。

関数をアップロードしておき、任意のイベントによって関数をトリガーするのがLambdaの基本的な使い方です。実際にやってみます。

ハマる、ハマる

が、実際には色々困りました…。順番に知見を共有します。

パッケージング

デプロイパッケージの作成 (Python)

Lambda 関数を作成するには、最初に Lambda 関数デプロイパッケージ (コードと依存関係で構成される .zip ファイル) を作成します。

ということで、プロジェクトのディレクトリに依存関係をインストールし、ZIP化する必要があります。pipのオプションでライブラリのインストール先は指定できるので、プロジェクトディレクトリに移動して…

$ pip3 install -r requirements.txt -t ./lib

ところが、エラーで失敗。

DistutilsOptionError: must supply either home or prefix/exec-prefix — not both

HomeBrewでPython導入しているとpip3 install -tが失敗する

らしいです。StackOverflowに同様の問題がありました。

ただ、回避の手段も回答されてます。ホームディレクトリに.pydistutils.cfgという名前のファイルを作って、以下の設定(というか、空のprefixを指定するハック)を書けば通るようになります。

pydistutils.cfg
[install]
prefix=

パッケージングの自動化について

pip installができるようになったのはいいとしても、増えた依存モジュールを再インストールして、もう一回.zipの中にモジュールを入れて…とか毎回手作業でやるのは辛いです。パッケージ管理ツールを導入して自動化できないかなあ、と調べたのですが色々動きが激しいようで。

ライブラリの配布について | Python Snippets

この辺りはベスプラを知りたいところではあります。追うのが大変そうだったのと、今回は大した規模でもないのでシェルスクリプトでなんとかすることにしました。

package_lambda.sh
#!/usr/bin/env bash

rm lambda.zip

cd src
pip3 install -r requirements.txt -t ./lib
zip ../lambda.zip *.py
cd lib
zip -r ../../lambda.zip *

src配下には自分で書いたPythonスクリプト群を配置しています。src/lib配下に依存モジュールをインストールし、それぞれ順番にZIPの直下に詰めてます。結論としてこれで大丈夫だったので先に進みます。

ハンドラ関数の定義

AWS Lambdaのコンソールに移ります。関数を作成、ランタイムには”Python3.6″を選びます。コードエントリで「.ZIPファイルをアップロード」を選び、作ったZipをアップしましょう。

スクリーンショット 2017-12-10 22.55.08.png

さて、次は画面右側の「ハンドラ」で処理の起点となる関数の名前を指定します。公式の説明では、

関数の filename.handler-method 値。たとえば、「main.handler」は、main.py で定義されたハンドラーメソッドを呼び出します。

すなわち[ファイル名].[関数名]とすれば良いので、前回作った感じだとmain.mainかなー、とか指定してテスト実行すると…

スクリーンショット 2017-12-10 23.08.36.png

落ちます。

ハンドラ関数の引数の数が間違っていると落ちる

"errorMessage": "main() takes 0 positional arguments but 2 were given"

Lambda 関数ハンドラー (Python)

まあドキュメントを読めという話でお恥ずかしいのですが、引数が合ってないわけですね。イベントハンドラであるところのLambda関数はeventとcontextを受けるのが基本ですから、その形に沿ったハンドラ定義が必要です。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)
+   return chosen

+def lambda_handler(event, context):
+   result = main()
+   return { 'tweetedURL': result }

if __name__ == '__main__':
    main()

あとは、コンソールでmain.lambda_handlerをハンドラに指定すればOKです。せっかくなので実際にツイートされた動画のURLを呼び出し元に返す仕様にしてみました。

定期実行

デプロイはできたので、次は定時実行の仕組みを作ります。

スクリーンショット 2017-12-11 22.05.53.png

コンソール画面の「トリガーの追加」から「CloudWatch Events」を選びます。トリガーのルールにcron式を選べば、今回やりたいことは実現できますね。

Rate または Cron を使用したスケジュール式

  • 日または週日の値は疑問符である必要がある
  • UTCしか使用できない(ので、日本時間に合わせてずらす)

あたりが注意点でしょうか。今回は平日の定時につぶやいて欲しいので、こんな式にしてみます。

cron(15 10 ? * MON-FRI *)

運用してみて

運用して1週間ほど様子を見てみましたが、無料利用の範囲内でおおむね問題なく動いております。
今度はTedみたいな勉強系の動画やらブログ記事やら拾わせても面白いかもしれませんね。

よかったこと

一気通貫して人が使えるサービスの形まで持って行くと、否応無しに広く技術をさらうことになります。インフラよりに苦手意識があったので半ば無理やりにでも触るのはいい経験になりました。

反省

  • YouTubeAPIとか使えばもっと楽に実装できたんじゃないか疑惑
  • パッケージングとAWS周りでハマりすぎた。アップロードが絡むあたりからはTry&Errorより前にドキュメントを読もう

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

リンク

続きを読む

LambdaとCloudWatchでサーバレスなTwitterBot作ってみた

こんにちは。駆け出しインフラエンジニアの(@k4ri474)です。
この記事はVASILY Advent Calendar 2017 11日目の記事です。

Twitterでbotを作ろうと思った時、ざっくり二つ選択肢が頭に浮かびますよね?
巷に溢れているbot作成サービスに登録するか、
はたまた自作するか。

エンジニアの方は、とりあえずプログラミングしてみっか〜となることが多いような気がします。
Twitter APIに関してはもちろん公式ドキュメントがありますし、プログラムもググれば山ほど先人たちのものが見れるので、
なんちゃらキーやらなんちゃらトークンさえ取得できれば、肝心のツイートはチョチョイのジョイかと思います。

ただ、継続的に動かすbotとして考えるとベースのサーバはどうしても必要になるのでちと悩みますよね。
自分のサイトをホスティングしてメールサーバ動かしてついでにbotを回す、などといったような状況ならクラウドでサーバを立てとけばいいんですが、botだけでいい時には少々大げさです。

そこでAWS Lambda + Amazon CloudWatch Eventsを使ってサーバレスに、かつ継続的に実現します。

概要

1postするためのプログラムをpythonで書き、それをLambdaにセットします。
そしてLambdaをCloudWatch Eventsで定期的に発火させ、botの体をなすようにします。

実装(Lambda)

まず、こちらが普通のつぶやきプログラムです。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

tweet = "Hello World"
session = OAuth1Session(CK, CS, AT, AS)

params = {"status": tweet }
session.post(URL, params = params)

Twitterに接続するにはOAuth認証が必要なのでrequests_oauthlib(https://github.com/requests/requests-oauthlib)
というPython用のOAuth認証ライブラリを利用しました。
Lambdaではライブラリとプログラムコードを一まとめにzip圧縮する必要があるので、プログラムと同じディレクトリにインストールしておきます。

% pip install requests requests_oauthlib -t ./

各種キーは苦労して取得したら、それらを各々セットしていただければあとはつぶやきをいじって実行するだけです。

さて、上のプログラムをLambda用に書き換えます。
Lambdaから関数を呼び出してもらうためには、ハンドラー関数を作成する必要があります。
パラメータとして eventcontext を取る関数で、Lambdaを呼び出したイベントデータやランタイム情報を内部で使えますが、今回はシンプルに使わない方向でいきます。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

def my_handler(event, context):
    tweet = "Hello World"
    session = OAuth1Session(CK, CS, AT, AS)

    params = {"status": tweet }
    session.post(URL, params = params)

これで準備は完了です。
こちらのファイル群をzip化し、コマンドでアップロードしてみます。

% aws lambda create-function \
--region ap-northeast-1 \
--function-name "sample" \
--runtime "python3.6" \
--role "MY_ROLE" \
--handler "sample.my_handler" \
--zip-file "fileb://./sample.zip" \
--profile PROFILE

注意したいのはhandlerオプションです。構文は実行ファイル名.ハンドラー関数名ですので、作成したプログラム名とハンドラー関数名に合わせて適宜編集してください。

また、セットするroleにCloudWatchLogsの権限を与えておくと実行結果をログ出力できるようになるのでオススメです。
今回は定義済みのポリシー、arn:aws:iam::aws:policy/CloudWatchLogsFullAccessをアタッチしたロールを事前に作成していたので、そちらをセットしてみました。
ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

以上でLambdaへのアップロードは完了です。

実装(CloudWatch Events)

さて、あとはこのLambda関数を定期実行するだけです。
まずはCloudWatch Eventsをトリガーにセットします。

スクリーンショット 2017-12-10 23.09.08.png

Lambdaのダッシュボードから関数を選択し、設定画面でトリガーを設定します。上図のようにCloudWatch Eventsをトリガーにセットしたら、下にスクロールしてトリガーの詳細を設定します。

スクリーンショット 2017-12-10 23.09.56.png

例としてはこんな感じでしょうか。
ルールタイプをスケジュール式に選択すると、おなじみのcron記述で定期実行の間隔をコントロールできます。
僕は試しにもう一方のrate式を使ってみることにしました。

このような感じで設定して保存すると無事トリガーとして機能します。
お疲れ様です。

成果物

このままのプログラムでは全く同じ内容をつぶやくばかりで面白くないので、
プログラムをちょちょっといじってimgurから猫画像のリンクを拾ってきてpostする感じのbotを作ってみました。

https://twitter.com/k4ri474/status/937562270476812290

自分の猫postで心が洗われます。
皆さんもサーバレスTwitter botでぜひ遊んでみてくださいb

続きを読む