serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)

serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)

目次

  • serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)← 今ここ
  • serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)
  • serverless frameworkを使って本格的なAPIサーバーを構築(Express編)
  • serverless frameworkを使って本格的なAPIサーバーを構築(MVC編)

framework_repo.png

こんにちは!某会社のインフラエンジニアをしておりますが、最近は サーバーレス という言葉が流行ってますね。
今回は、serverless frameworkで 「 lambda + APIGateway + DynamoDB 」 の構成でいくつかサービスを作ったので、利点などをまとめていけたらと思います。

サーバーレスとserverless framework

まずサーバーレスとは

サーバーの管理が不要になる

  • サーバーレスアーキテクチャでは、管理するサーバーがないため、サーバー管理から開放され、その分管理コストがかからなくなります。物理的なサーバーの管理はクラウド事業者に一任できるので、故障やパッチ適用の心配をする必要はありません。

また、一般にコード実行サービスは高い可用性を持っているため、オンプレミスサーバーよりも障害耐性が高くなるでしょう。

アプリ開発において便利

  • APIサーバーとして、ハッカソン用に作ったアプリの運用など、ほとんどアクセスがないようなAPIサーバーをずっと放置しておくのは大変お金がかかります。

従来の冗長化構成に比べ安価

  • EC2 + RDS環境を構築したときに、ある程度の規模であれば安価に済ますことができます。

サーバーの脆弱性を突いた攻撃が効かない

  • セキュリティ被害の多くはサーバーOSやミドルウェアの脆弱性を突いた攻撃ですが、公開ネットワーク上にサーバーが無いので、一般的なサーバーの脆弱性を突く攻撃が効きません。

  • Amazonのプラットフォームに脆弱性が無いとは言えませんが、プラットフォーム仕様が公開されていないので脆弱性を探すことが難しく、一般的なサーバーOSより脆弱性被害の可能性は少ないと考えられます。

サーバーが落ちることがない

  • サーバーが無いのでサーバーダウンの概念がありません。

  • 厳密にはAmazonがダウンすれば共倒れしますが、どのプラットフォームもサービス本体がダウンすれば同じことが言えるので、世界的インフラのAWSと、自社サーバーや一般企業のレンタルサーバーのどちらを信用するか、という話になります。

放置できる

  • セキュリティパッチや、サーバー自体の管理が発生しないためある程度放置できます。

インフラエンジニアがいなくてもスケールする

  • lambdaは最大で3000も同時に実行できます。
  • dynamodbは、キャパシティユニットとオートスケールの設定がかんたんにできます。
  • S3で静的コンテンツを配信したとしても障害確率はかなり低いです。

とにかく安い!

  • サーバーは初期導入コストが高く、必要になるサーバースペックも事前に見積もっておかなければいけません。物理障害時のパーツ交換費用なども必要です。
    クラウドサーバーにしても、起動している時間は料金がかかるため、閑散時などは100%コストを活用できているとはいえません。

  • 一方、コード実行サービスであれば、実際にコードを実行した時間に応じて課金されるため、ムダなコストが発生しません。初期導入コストもかからないため、システムの規模に合わせて柔軟にスケールできます。

保守メンテが楽

  • セキュリテイパッチやサーバー管理などのメンテが不要になります。

APIGateway+DynamoDB+Lambda手動構築の苦悩

image.png

  • serverless framework を使わずに「lambda + APIGateway + DynamoDB」を構築しようとすると、ほぼボタン操作で設定しないといけないです。
  • どのステージをデプロイするのか、設定した項目・手順を忘れた。もとに戻したい、引き継ぎを行いたいなどいろいろなケースが考えられます

再現性がない

  • これに尽きると思います。
  • 基本的にAPIGatewayの設定などはボタン操作です。もちろんgit管理などできない。
    ステージの作成、レスポンスコード、methodの作成など操作を間違えても元に戻すことが難しくなり、実装コスト、人件費、考えるだけで大変なことが多すぎます。(大変だった。)

よって保守性・メンテ・引き継ぎが難しくなっていました。

そこでserverless frameworkの出番です!!

image.png

  • 基本的にAWSコンソールでのボタン操作は不要!!
  • serverless.ymlですべて管理・設定ができる => 設定をgit管理できる

    • aws cloudformationの形式と似ているため、最悪それを真似できます(cloudformationよりかんたん!)
  • コマンド1発でデプロイとロールバックが可能
  • 公式リポジトリにサンプルが沢山!!(こまりません)
  • プラグインもたくさん(ぶっちゃけ使わなくても高機能!!)
  • Expressと組み合わせると、超便利!!!!!(後日記事を書きます)

注意点など

  • iamのadmin権限が必要です。
  • Cloudfrontは設定できません。

次回

  • serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)(仮)

参考記事

続きを読む

AWS CloudWatch で、Amazon EC2 Windows インスタンスのパフォーマンスとログの監視をしてみる

0.はじめに

Amazon EC2 Windows インスタンスを利用していますが、
パフォーマンス監視は Zabbix を使って、
ログ監視は特に何も、
という感じでした。

CloudWatch のメトリクスの保存期間も長くなったみたいですし、
運用の手間やリスク、コスト削減も考慮して、
パフォーマンス監視を CloudWatch、
ログ監視を CloudWatch Logs、
にしようかと思います。

1.EC2Config の最新バージョンへのアップデート

  1. 以下のサイトの手順に従って、アップデートします。

2.Amazon EC2 Systems Manager (SSM) エージェントのインストール

  1. EC2 インスタンスに割り当てられている IAM Role に「AmazonEC2RoleforSSM」を付与します。

  2. 以下のサイトの手順に従って、アップデートします。

3.Amazon EC2 Systems Manager (SSM) エージェントを使っての CloudWatch の設定

  1. 以下のサイトを参考にしました。

  2. まず、以下の json ファイルを準備します。

    • 78 行目 : “NameSpace”: “System/Detail/Windows”

      • これが、カスタム名前空間になります。適当な名称に変更して下さい。
    • 44-69 行目 :
      • C ドラのメトリクスの設定になります。他のドライブを追加するのであれば、C を D に変えて、行を追加して下さい。
      • 85 行目にも追加するのを忘れずに。
    Sample-ConfigureCloudWatch-Windows.json
    {
        "EngineConfiguration": {
            "PollInterval": "00:00:15",
            "Components": [
                {
                    "Id": "CPULoadAverage",
                    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "CategoryName": "System",
                        "CounterName": "Processor Queue Length",
                        "InstanceName": "",
                        "MetricName": "CPULoadAverage",
                        "Unit": "Percent",
                        "DimensionName": "InstanceId",
                        "DimensionValue": "{instance_id}"
                    }
                },
                {
                    "Id": "MemoryAvailable",
                    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "CategoryName": "Memory",
                        "CounterName": "Available Bytes",
                        "InstanceName": "",
                        "MetricName": "MemoryAvailable",
                        "Unit": "Bytes",
                        "DimensionName": "InstanceId",
                        "DimensionValue": "{instance_id}"
                    }
                },
                {
                    "Id": "MemoryUtilization",
                    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "CategoryName": "Memory",
                        "CounterName": "% Committed Bytes in Use",
                        "InstanceName": "",
                        "MetricName": "MemoryUtilization",
                        "Unit": "Percent",
                        "DimensionName": "InstanceId",
                        "DimensionValue": "{instance_id}"
                    }
                },
                {
                    "Id": "DiskAvailableMegabytesC",
                    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "CategoryName": "LogicalDisk",
                        "CounterName": "Free Megabytes",
                        "InstanceName": "C:",
                        "MetricName": "DiskAvailableMegabytesC",
                        "Unit": "Megabytes",
                        "DimensionName": "InstanceId",
                        "DimensionValue": "{instance_id}"
                    }
                },
                {
                    "Id": "DiskFreeSpaceC",
                    "FullName": "AWS.EC2.Windows.CloudWatch.PerformanceCounterComponent.PerformanceCounterInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "CategoryName": "LogicalDisk",
                        "CounterName": "% Free Space",
                        "InstanceName": "C:",
                        "MetricName": "DiskFreeSpaceC",
                        "Unit": "Percent",
                        "DimensionName": "InstanceId",
                        "DimensionValue": "{instance_id}"
                    }
                },
                {
                    "Id": "CloudWatch",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatch.CloudWatchOutputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters":
                    {
                        "AccessKey": "",
                        "SecretKey": "",
                        "Region": "ap-northeast-1",
                        "NameSpace": "System/Detail/Windows"
                    }
                }
            ],
            "Flows": {
                "Flows":
                [
                    "(CPULoadAverage,MemoryAvailable,MemoryUtilization,DiskAvailableMegabytesC,DiskFreeSpaceC),CloudWatch"
                ]
            }
        }
    }
    

  3. EC2 のマネジメントコンソールの左側ペインから、「SYSTEMS MANAGER 共有リソース」→「マネージドインスタンス」を選択します。対象となる EC2 インスタンスが表示されるはずなので選択し(表示されなければ、これまでの手順に何か問題があるのかも?)、「関連付けの作成」ボタンを押下します。

    • FireShot Capture 152 - EC2 Management C_ - https___ap-northeast-1.console.aws.amazon.com_ec2_v2_home.png

  4. 「関連付けの作成」画面が表示されるので、以下の項目を入力し、「関連付けの作成」ボタンを押下します。

    • 関連付けの名前 : Sample-ConfigureCloudWatch-Windows ※ 任意
    • ドキュメントを選択
      • ドキュメント : AWS-ConfigureCloudWatch
      • バージョンの説明 : –
    • ターゲット ※ 対象となる EC2 インスタンスが設定されれば OK。
      • ターゲットの選択 : インスタンスの手動選択 ※ 任意
      • インスタンスの選択 : 対象となる EC2 インスタンス
    • スケジュール
      • 指定 : Cron スケジュールビルダー ※ デフォルト
      • 関連付けの実行 : 30 分ごと ※ デフォルト
    • パラメーター
      • Status : Enabled ※ デフォルト
      • Properties : ※ json ファイルの内容をコピペ
    • 詳細設定
      • S3 への書き込み : □ ※ デフォルト

    • FireShot Capture 153 - EC2 Management C_ - https___ap-northeast-1.console.aws.amazon.com_ec2_v2_home.png

  5. 「関連付けの作成」の結果画面が表示されるので、「閉じる」ボタンを押下します。

    • FireShot Capture 158 - EC2 Management C_ - https___ap-northeast-1.console.aws.amazon.com_ec2_v2_home.png

  6. 元の一覧画面が表示されるので、関連付けがちゃんと作成されているか確認します。

    • ※しばらくすると、ステータスが「保留中」から「成功」に変わるはずです。

    • FireShot Capture 159 - EC2 Management C_ - https___ap-northeast-1.console.aws.amazon.com_ec2_v2_home.png

    • FireShot Capture 160 - EC2 Management C_ - https___ap-northeast-1.console.aws.amazon.com_ec2_v2_home_.png

  7. CloudWatch のマネジメントコンソールの左側ペインから、「メトリクス」を選択します。設定したカスタム名前空間と EC2 インスタンスの ID でフィルタをかけて、設定したメトリクスのデータがあるか確認します。

    • FireShot Capture 161 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws_.png

4.Amazon EC2 Systems Manager (SSM) エージェントを使っての CloudWatch Logs の設定

  1. CloudWatch Logs の設定も、CloudWatch の手順と同じです。違うのは、json ファイルの設定内容だけです。

  2. 以下、EventLog の json ファイルです。

    • 36 行目 : “LogGroup”: “Windows/EventLog”

      • ロググループです。適当な名称に変更して下さい。
    • 10,18,26 行目 :
      • 出力レベルです。適当な値に変更して下さい。
    Sample-ConfigureCloudWatchLogs-Windows_EventLog.json
    {
        "EngineConfiguration": {
            "PollInterval": "00:00:15",
            "Components": [
                {
                    "Id": "ApplicationEventLog",
                    "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "LogName": "Application",
                        "Levels": "7"
                    }
                },
                {
                    "Id": "SystemEventLog",
                    "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "LogName": "System",
                        "Levels": "7"
                    }
                },
                {
                    "Id": "SecurityEventLog",
                    "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                    "LogName": "Security",
                    "Levels": "7"
                    }
                },
                {
                    "Id": "CloudWatchLogs",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatchLogsOutput,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "AccessKey": "",
                        "SecretKey": "",
                        "Region": "ap-northeast-1",
                        "LogGroup": "Windows/EventLog",
                        "LogStream": "{instance_id}"
                    }
                }
            ],
            "Flows": {
                "Flows":
                [
                    "(ApplicationEventLog,SystemEventLog,SecurityEventLog),CloudWatchLogs"
                ]
            }
        }
    }
    

  3. 以下、IIS ログの json ファイルです。

    • 22 行目 : “LogGroup”: “Windows/IisLog”

      • ロググループです。適当な名称に変更して下さい。
    • 5-14 行目 :
      • サイト毎の設定となります。W3SVC1 以外にも取得したのであれば、変更し行を追加して下さい。
      • 30 行目にも追加するのを忘れずに。
    Sample-ConfigureCloudWatchLogs-Windows_IisLog.json
    {
        "EngineConfiguration": {
            "PollInterval": "00:00:15",
            "Components": [
                {
                    "Id": "IISLogW3SVC1",
                    "FullName": "AWS.EC2.Windows.CloudWatch.IisLog.IisLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "LogName": "IISLogW3SVC1",
                        "LogDirectoryPath": "C:\inetpub\logs\LogFiles\W3SVC1",
                        "TimestampFormat": "yyyy-MM-dd HH:mm:ss",
                        "Encoding": "UTF-8"
                    }
                },
                {
                    "Id": "CloudWatchLogs",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatchLogsOutput,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "AccessKey": "",
                        "SecretKey": "",
                        "Region": "ap-northeast-1",
                        "LogGroup": "Windows/IisLog",
                        "LogStream": "{instance_id}"
                    }
                }
            ],
            "Flows": {
                "Flows":
                [
                    "(IISLogW3SVC1),CloudWatchLogs"
                ]
            }
         }
    }
    

  4. 以下、カスタムログの json ファイルです。

    • 24 行目 : “LogGroup”: “Windows/CustomLog”

      • ロググループです。適当な名称に変更して下さい。
    • 5-16 行目 :
      • 取得したいログファイル毎の設定となります。適宜変更して下さい。
    • ※すいません。こちら未確認です…。
    Sample-ConfigureCloudWatchLogs-Windows_CustomLog.json
    {
        "EngineConfiguration": {
            "PollInterval": "00:00:15",
            "Components": [
                {
                    "Id": "CustomLogs",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CustomLog.CustomLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "LogDirectoryPath": "C:\CustomLogs\",
                        "TimestampFormat": "MM/dd/yyyy HH:mm:ss",
                        "Encoding": "UTF-8",
                        "Filter": "",
                        "CultureName": "en-US",
                        "TimeZoneKind": "Local"
                    }
                },
                {
                    "Id": "CloudWatchLogs",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatchLogsOutput,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "AccessKey": "",
                        "SecretKey": "",
                        "Region": "ap-northeast-1",
                        "LogGroup": "Windows/CustomLog",
                        "LogStream": "{instance_id}"
                    }
                }
            ],
            "Flows": {
                "Flows":
                [
                    "(CustomLogs),CloudWatchLogs"
                ]
            }
        }
    }
    

  5. 以下、ETW の json ファイルです。

    • 24 行目 : “LogGroup”: “Windows/ETW”

      • ロググループです。適当な名称に変更して下さい。
    • 5-16 行目 :
      • 取得したいログファイル毎の設定となります。適宜変更して下さい。
    • ※すいません。こちら未確認です…。
    Sample-ConfigureCloudWatchLogs-Windows_ETW.json
    {
        "EngineConfiguration": {
            "PollInterval": "00:00:15",
            "Components": [
                {
                    "Id": "ETW",
                    "FullName": "AWS.EC2.Windows.CloudWatch.EventLog.EventLogInputComponent,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "LogName": "Microsoft-Windows-WinINet/Analytic",
                        "Levels": "4"
                    }
                },
                {
                    "Id": "CloudWatchLogs",
                    "FullName": "AWS.EC2.Windows.CloudWatch.CloudWatchLogsOutput,AWS.EC2.Windows.CloudWatch",
                    "Parameters": {
                        "AccessKey": "",
                        "SecretKey": "",
                        "Region": "ap-northeast-1",
                        "LogGroup": "Windows/ETW",
                        "LogStream": "{instance_id}"
                    }
                }
            ],
            "Flows": {
                "Flows":
                [
                    "(ETW),CloudWatchLogs"
                ]
            }
        }
    }
    

  6. CloudWatch のマネジメントコンソールの左側ペインから、「ログ」を選択します。ロググループの一覧が表示されるので、設定したロググループと EC2 インスタンスの ID でフィルタをかけて、設定したログのデータがあるか確認します。

    • FireShot Capture 162 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws.png

    • FireShot Capture 163 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws.png

    • FireShot Capture 164 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws_.png

99.ハマりポイント

  • 今回も結構ハマりました…。毎回、何やってんだ…。

  • 最初、今回の手順ではなく「SYSTEMS MANAGER 共有リソース」にある「ドキュメント」を作成して対応しようと思ったんですが、ドキュメントの作成がうまくいかず…、断念…。サポートセンターに問い合わせ中です。
  • その後、今回の手順で設定を進めたんですが、json ファイルの設定内容でハマりました。特にディスクの空容量のメトリクスの取得とかがよくわからず、難儀しました。こちらのサイトで何とか解決しました。ありがとうございます。

XX.まとめ

これで、
何とか運用監視も AWS だけで事足りる様になるのではと。

CloudWatch の 15ヶ月のデータ保存じゃ短すぎる!!
という場合は、
こちらのサイトのやり方などもあるみたいです。

続きを読む

EC2(Amazon Linux)のWebLogicでログを管理する

このシリーズの流れ。

EC2のEBSに出力されたログをCloudWatch Logsから見る。

管理するログを決める

以下を参考にさせてもらいました。
WebLogic Serverを基礎から学ぶシリーズ第3弾 入門!WebLogic Serverの運用監視

これらのログを管理することに決定。

  • サーバーログ
  • ドメインログ
  • コンソールログ
  • GCログ
  • HTTPアクセスログ
  • アプリケーションログ

CloudWatch Logsの設定

出力されたログをCloudWatch Logsに送信するため、まずはAWSのドキュメント1に従ってEC2インスタンスへCloudWatch Logsエージェントをインストールする。ログは管理サーバーにも管理対象サーバーにも出力されるので両方へインストールを行う。

1. IAMロールをEC2インスタンスにアタッチ

EC2インスタンスにCloudWatch Logsへのアクセスを委任。

  1. IAMロールを作成
  2. 作成したロールへ、CloudWatch Logsの操作を許可するカスタムポリシーを追加
  3. EC2インスタンスへロールを割り当て

2. CloudWatch Logs エージェントをインストール

$ sudo yum update -y
$ sudo yum install -y awslogs

$ sudo sed 's/^region = .*/region = ap-northeast-1/' /etc/awslogs/awscli.conf > ~/awscli.conf.tmp
$ sudo chown root:root ~/awscli.conf.tmp
$ sudo chmod 600 ~/awscli.conf.tmp
$ sudo mv ~/awscli.conf.tmp /etc/awslogs/awscli.conf

IAMロールをアタッチしているので認証情報の設定は不要。ただ東京リージョンのCloudWatch Logsへ送信したかったのでそこだけは設定。

3. awslogsサービスを開始

初期設定で/var/log/messagesを送信するようになっているので、まずはどんな感じか体験してみる。

$ sudo service awslogs start

開始後、CloudWatchコンソールを見ると…「CloudWatch > ロググループ > /var/log/messages > [インスタンスID]」にログ出てるー:v::v::v:

4. 追跡するログの設定

ポイントになるのは対象ログの場所、名前、タイムスタンプのフォーマット。
管理すると決めたログについて上記を確認し設定していく。

サーバーログ

WebLogicサーバーのイベントが記録される。起動とか停止とかアプリケーションのデプロイとか。管理サーバーと管理対象サーバーの両方に存在する。

まずはファイルパス、ファイル名、およびタイムスタンプのフォーマットを確認。

すべて管理コンソールから確認できる。ドメイン構造から「環境 → サーバー → [サーバー名]」と選択し「ロギング」タブの「一般」タブにて。(タイムスタンプのフォーマットは実際のログを見た方が早いかな)

確認できたのでCloudWatch Logs エージェントの設定ファイルへ追跡したいログの情報を記述する。以下のようになった。(管理サーバーでの例)

/etc/awslogs/awslogs.conf
[serverlog]
log_group_name = ServerLog
log_stream_name = leo1(管理サーバー)
datetime_format = ####<%b %d, %Y %I:%M:%S %p %Z>
file = /home/oracle/user_projects/domains/zodiac/servers/leo1/logs/leo1.log
multi_line_start_pattern = {datetime_format}

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

ドメインログ

ドメイン全体のステータスを確認できるログ。各サーバーログの特定ログレベル以上のメッセージが記録される。管理サーバーにのみ存在。

まずはファイルパス、ファイル名、およびタイムスタンプのフォーマットを確認。

すべて管理コンソールから確認できる。ドメイン構造からドメイン名を選択し、「構成」タブの「ロギング」にて。

確認できたのでCloudWatch Logs エージェントの設定ファイルへ追跡したいログの情報を記述する。以下のようになった。

/etc/awslogs/awslogs.conf
[domainlog]
log_group_name = DomainLog
log_stream_name = zodiac
datetime_format = ####<%b %d, %Y %I:%M:%S %p %Z>
file = /home/oracle/user_projects/domains/zodiac/servers/leo1/logs/zodiac.log
multi_line_start_pattern = {datetime_format}

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

コンソールログ

標準出力、標準エラー出力のログ。管理サーバーと管理対象サーバーの両方で取得する。

まずはファイルパス、ファイル名を確認、というか設定する。WebLogicの起動スクリプトに変数名「WLS_REDIRECT_LOG」で出力ファイル名を記述できるが、今回は余すことなく取得できるようスクリプト実行時にリダイレクト先を指定する方向で設定。

管理サーバーの場合:

$ nohup /home/oracle/user_projects/domains/zodiac/startWebLogic.sh 1>/home/oracle/user_projects/domains/zodiac/servers/leo1/logs/console.log.`date +%Y%m%d%H%M%S`.$$ 2>&1 &

管理対象サーバーの場合:

$ nohup /home/oracle/user_projects/domains/zodiac/bin/startManagedWebLogic.sh leo2 t3://leo1.example.com:7001 1>/home/oracle/user_projects/domains/zodiac/servers/leo2/logs/console.log.`date +%Y%m%d%H%M%S`.$$ 2>&1 &

続いてタイムスタンプのフォーマット確認、だけど雑多な内容が出ると思われるので設定しようがない感。というわけでCloudWatch Logs エージェントの設定ファイルは以下のような記述になった。(管理サーバーでの例)

/etc/awslogs/awslogs.conf
[consolelog]
log_group_name = ConsoleLog
log_stream_name = leo1(管理サーバー)
file = /home/oracle/user_projects/domains/zodiac/servers/leo1/logs/console.log.*

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

ちなみにログローテーションについては、出力そんなにないだろうし内容の確認はCloudWatchコンソールから行うので今回はしなくていいんじゃないかなーって。:hamburger:

GCログ

ガベージコレクションのログ。アプリケーションがデプロイされる管理対象サーバーでのみ取得。

まずはファイルパス、ファイル名を確認、というか設定する。管理対象サーバーの起動スクリプトへGC関連のフラグを設定。

/home/oracle/user_projects/domains/zodiac/bin/startManagedWebLogic.sh
# JAVA_OPTIONS環境変数を定義する直前にGC関連のフラグを設定
JAVA_OPTIONS="-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/logdemo/gc.log.%t -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=20M ${JAVA_OPTIONS}"

export JAVA_OPTIONS

設定したフラグは以下。

フラグ 意味
-XX:+PrintGC GCの基本的なログを出力する
-XX:+PrintGCDetails GCに関する詳細なログを記録する
-XX:+PrintGCDateStamps GCのログの各エントリで実際の日付を出力する
-Xloggc:[パス] GCのログを標準出力から別のファイルへリダイレクトする
-XX:+UseGCLogFileRotation GCのログのローテーションを行う
-XX:NumberOfGCLogFiles=[N] ログローテーションで保持するログファイルの数を指定する
-XX:GCLogFileSize=[N] ログファイルのサイズを指定する

タイムスタンプのフォーマットは実際の出力を見て確認。

2017-09-19T15:26:42.257+0900: 9.422: [Full GC (Metadata GC Threshold) 2017-09-19T15:26:42.257+0900: 9.422: [Tenured: 26081K->40434K(174784K), 0.1071922 secs] 94961K->40434K(253504K), [Metaspace: 58287K->58287K(1101824K)], 0.1072718 secs] [Times: user=0.10 sys=0.01, real=0.11 secs]
2017-09-19T15:26:43.770+0900: 10.934: [GC (Allocation Failure) 2017-09-19T15:26:43.770+0900: 10.934: [DefNew: 70016K->8704K(78720K), 0.0263706 secs] 110450K->50187K(253504K), 0.0264394 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
2017-09-19T15:26:45.177+0900: 12.342: [GC (Allocation Failure) 2017-09-19T15:26:45.177+0900: 12.342: [DefNew: 78720K->8704K(78720K), 0.0265826 secs] 120203K->58992K(253504K), 0.0266508 secs] [Times: user=0.02 sys=0.01, real=0.03 secs]
2017-09-19T15:26:45.673+0900: 12.837: [GC (Allocation Failure) 2017-09-19T15:26:45.673+0900: 12.837: [DefNew: 78720K->221K(78720K), 0.0097785 secs] 129008K->54214K(253504K), 0.0098368 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2017-09-19T15:26:47.298+0900: 14.462: [GC (Allocation Failure) 2017-09-19T15:26:47.298+0900: 14.463: [DefNew: 70237K->7929K(78720K), 0.0133497 secs] 124230K->61922K(253504K), 0.0134268 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

確認できたのでCloudWatch Logs エージェントの設定ファイルへ追跡したいログの情報を記述する。以下のようになった。

/etc/awslogs/awslogs.conf
[gclog]
log_group_name = GCLog
log_stream_name = leo2
datetime_format = %Y-%m-%dT%H:%M:%S.%f%z
file = /var/log/logdemo/gc.log.*
multi_line_start_pattern = {datetime_format}

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

GCログはCloudWatchコンソールからS3へ出力して別途ツールで眺める感じになるんじゃないかなー。

HTTPアクセスログ

その名の通りアクセスログ。アプリケーションがデプロイされる管理対象サーバーでのみ取得。

まずはファイルパス、ファイル名、およびタイムスタンプのフォーマットを確認。

すべて管理コンソールから確認できる。ドメイン構造から「環境 → サーバー → [サーバー名]」と選択し「ロギング」タブの「HTTP」タブにて。

確認できたのでCloudWatch Logs エージェントの設定ファイルへ追跡したいログの情報を記述する。以下のようになった。

/etc/awslogs/awslogs.conf
[accesslog]
log_group_name = AccessLog
log_stream_name = leo2
datetime_format = [%d/%b/%Y:%H:%M:%S %z]
file = /home/oracle/user_projects/domains/zodiac/servers/leo2/logs/access.log
multi_line_start_pattern = {datetime_format}

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

アプリケーションログ

log4j2を利用してログを出力。

ファイルパス、ファイル名、およびタイムスタンプのフォーマットは設定ファイルから確認。

src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="off">
    <Properties>
        <Property name="loglayout">[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5p %t %c %m%n</Property>
    </Properties>
    <Appenders>
        <Console name="stdout" target="SYSTEM_OUT">
            <PatternLayout pattern="${loglayout}" />
        </Console>
        <RollingFile name="app" fileName="/var/log/logdemo/app.log" filePattern="/var/log/logdemo/app-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${loglayout}" />
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="20 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
            <DefaultRolloverStrategy max="10" />
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="stdout" />
        </Root>
        <Logger name="com.mycompany" level="info" additivity="false">
            <AppenderRef ref="app" />
        </Logger>
    </Loggers>
</Configuration >

確認できたのでCloudWatch Logs エージェントの設定ファイルへ追跡したいログの情報を記述する。以下のようになった。

/etc/awslogs/awslogs.conf
[applicationlog]
log_group_name = ApplicationLog
log_stream_name = logdemo2
datetime_format = [%Y-%m-%d %H:%M:%S.%f]
file = /var/log/logdemo/app.log
multi_line_start_pattern = {datetime_format}

awslogsサービスを再起動して設定を反映。

$ sudo service awslogs restart

その他情報

CloudWatch Logs自体のログ

ログが追跡できていない時など、awslogsサービスでエラーがないか確認する際は/var/log/awslogs.logファイルを確認する。

$ cat /var/log/awslogs.log 

WebLogic + SLF4J + Log4j2

この組み合わせでログが出なさすぎたので解決方法をメモ。

ライブラリの設定:

pom.xml
<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.8.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.8.2</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.21</version>
    </dependency>
</dependencies>
  • log4j-core(version:2.8.2) // Log4j2本体、version:2.9.0にすると何故かデプロイできない…
  • log4j-slf4j-impl // Log4j2へのバインディング
  • slf4j-api // ログファサード

weblogic.xmlでSLF4Jのリソース定義を行う:

weblogic.xml
<container-descriptor>
    <prefer-application-packages>
        <package-name>org.slf4j.*</package-name>
    </prefer-application-packages>
    <prefer-application-resources>
        <resource-name>org/slf4j/impl/StaticLoggerBinder.class</resource-name>
    </prefer-application-resources>
</container-descriptor>

StaticLoggerBinder.classを常にアプリケーションのリソースからロードされるよう設定しないといけないらしい2。わっかんねー:innocent:


続きを読む

メールにパスワード付きzipを添付して「パスワードは別途お送りいたします」とする慣習がめんどくさいのでなんとかした

あの慣習

メールにパスワード付きzipを添付して「パスワードは別途お送りいたします」とする慣習、ありますよね。
自分からはやらないけど、相手に合わせてやらざるを得なかったりしてめんどくさい。

ここでは、このやり方の是非は問題にしません。
どんなに是非を説いても、この慣習があるという状況は変わらないので。

そして、この慣習を無くすことも考えません。
そういうのは巨大な力を持った何かにおまかせします。

昔のエラい人は言いました。「長いものには巻かれろ」と。
ただし、巻かれ方は考えたほうがいいと思うのです。

スマートな巻かれ方を考える

巻かれるにあたって、解決したいことはただ一つ。めんどくさくないこと。
このためにWebシステム作って、ブラウザ開いてどうのこうのなんてやってると本末転倒です。
可能な限り、普通のメール送信に近い形で実現したい。

というわけで、あれこれ考えた末、一部の制約を許容しつつ、AmazonSESを使ってサーバーレスな感じで解決してみました。

仕様

  1. 普通にメールを書く(新規・返信・転送問わず)
  2. ファイルをzipで固めずにそのまま放り込む
  3. SES宛のメールアドレスをToに、実際にファイルを送りたい相手をReply-Toに設定する。
  4. システムを信じて送信ボタンを押す
  5. 自分と相手に、パスワード付きzipが添付されたメールとパスワードのお知らせメールが届く

ただし、以下の制約があります。個人的には許容範囲です。

  • 結果的に相手方には全員Toで届く。Ccはできない(自分はBcc)
  • zipファイルの名前は日時(yymmddHHMMSS.zip)になる(中身のファイル名はそのまま)

システム構成

flow_01.png

  1. SESに宛ててメールを送る
  2. メールデータがS3に保存される
  3. それをトリガーにしてLambdaが起動する
  4. Lambdaがメールの内容を解析してパスワードとzipファイルを生成する
  5. いい感じにメールを送る(念のため自分にもBccで送る)

実装

Lambda

真面目にpython書いたの初めてだけどこんな感じでいいのかな?
大体メールと文字コードとファイルとの戦いです。

# -*- coding: utf-8 -*-

import os
import sys
import string
import random
import json
import urllib.parse
import boto3
import re
import smtplib
import email
import base64
from email                import encoders
from email.header         import decode_header
from email.mime.base      import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text      import MIMEText
from email.mime.image     import MIMEImage
from datetime             import datetime

sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'vendored'))
import pyminizip

s3 = boto3.client('s3')

class MailParser(object):
    """
    メール解析クラス
    (参考) http://qiita.com/sayamada/items/a42d344fa343cd80cf86
    """

    def __init__(self, email_string):
        """
        初期化
        """
        self.email_message    = email.message_from_string(email_string)
        self.subject          = None
        self.from_address     = None
        self.reply_to_address = None
        self.body             = ""
        self.attach_file_list = []

        # emlの解釈
        self._parse()

    def get_attr_data(self):
        """
        メールデータの取得
        """
        attr = {
                "from":         self.from_address,
                "reply_to":     self.reply_to_address,
                "subject":      self.subject,
                "body":         self.body,
                "attach_files": self.attach_file_list
                }
        return attr


    def _parse(self):
        """
        メールファイルの解析
        """

        # メッセージヘッダ部分の解析
        self.subject          = self._get_decoded_header("Subject")
        self.from_address     = self._get_decoded_header("From")
        self.reply_to_address = self._get_decoded_header("Reply-To")

        # メールアドレスの文字列だけ抽出する
        from_list =  re.findall(r"<(.*@.*)>", self.from_address)
        if from_list:
            self.from_address = from_list[0]
        reply_to_list =  re.findall(r"<(.*@.*)>", self.reply_to_address)
        if reply_to_list:
            self.reply_to_address = ','.join(reply_to_list)

        # メッセージ本文部分の解析
        for part in self.email_message.walk():
            # ContentTypeがmultipartの場合は実際のコンテンツはさらに
            # 中のpartにあるので読み飛ばす
            if part.get_content_maintype() == 'multipart':
                continue
            # ファイル名の取得
            attach_fname = part.get_filename()
            # ファイル名がない場合は本文のはず
            if not attach_fname:
                charset = str(part.get_content_charset())
                if charset != None:
                    if charset == 'utf-8':
                        self.body += part.get_payload()
                    else:
                        self.body += part.get_payload(decode=True).decode(charset, errors="replace")
                else:
                    self.body += part.get_payload(decode=True)
            else:
                # ファイル名があるならそれは添付ファイルなので
                # データを取得する
                self.attach_file_list.append({
                    "name": attach_fname,
                    "data": part.get_payload(decode=True)
                })

    def _get_decoded_header(self, key_name):
        """
        ヘッダーオブジェクトからデコード済の結果を取得する
        """
        ret = ""

        # 該当項目がないkeyは空文字を戻す
        raw_obj = self.email_message.get(key_name)
        if raw_obj is None:
            return ""
        # デコードした結果をunicodeにする
        for fragment, encoding in decode_header(raw_obj):
            if not hasattr(fragment, "decode"):
                ret += fragment
                continue
            # encodeがなければとりあえずUTF-8でデコードする
            if encoding:
                ret += fragment.decode(encoding)
            else:
                ret += fragment.decode("UTF-8")
        return ret

class MailForwarder(object):

    def __init__(self, email_attr):
        """
        初期化
        """
        self.email_attr = email_attr
        self.encode     = 'utf-8'

    def send(self):
        """
        添付ファイルにパスワード付き圧縮を行い転送、さらにパスワード通知メールを送信
        """

        # パスワード生成
        password = self._generate_password()

        # zipデータ生成
        zip_name = datetime.now().strftime('%Y%m%d%H%M%S')
        zip_data = self._generate_zip(zip_name, password)

        # zipデータを送信
        self._forward_with_zip(zip_name, zip_data)

        # パスワードを送信
        self._send_password(zip_name, password)

    def _generate_password(self):
        """
        パスワード生成
        記号、英字、数字からそれぞれ4文字ずつ取ってシャッフル
        """
        password_chars = ''.join(random.sample(string.punctuation, 4)) + 
                         ''.join(random.sample(string.ascii_letters, 4)) + 
                         ''.join(random.sample(string.digits, 4))

        return ''.join(random.sample(password_chars, len(password_chars)))

    def _generate_zip(self, zip_name, password):
        """
        パスワード付きZipファイルのデータを生成
        """
        tmp_dir  = "/tmp/" + zip_name
        os.mkdir(tmp_dir)

        # 一旦ローカルにファイルを保存
        for attach_file in self.email_attr['attach_files']:
            f = open(tmp_dir + "/" + attach_file['name'], 'wb')
            f.write(attach_file['data'])
            f.flush()
            f.close()

        # パスワード付きzipに
        dst_file_path = "/tmp/%s.zip" % zip_name
        src_file_names = ["%s/%s" % (tmp_dir, name) for name in os.listdir(tmp_dir)]

        pyminizip.compress_multiple(src_file_names, dst_file_path, password, 4)

        # # 生成したzipファイルを読み込み
        r = open(dst_file_path, 'rb')
        zip_data = r.read()
        r.close()

        return zip_data

    def _forward_with_zip(self, zip_name, zip_data):
        """
        パスワード付きZipファイルのデータを生成
        """
        self._send_message(
                self.email_attr['subject'],
                self.email_attr["body"].encode(self.encode),
                zip_name,
                zip_data
                )
        return

    def _send_password(self, zip_name, password):
        """
        zipファイルのパスワードを送信
        """

        subject = self.email_attr['subject']

        message = """
先ほどお送りしたファイルのパスワードのお知らせです。

[件名] {}
[ファイル名] {}.zip
[パスワード] {}
        """.format(subject, zip_name, password)

        self._send_message(
                '[password]%s' % subject,
                message,
                None,
                None
                )
        return

    def _send_message(self, subject, message, attach_name, attach_data):
        """
        メール送信
        """

        msg = MIMEMultipart()

        # ヘッダ
        msg['Subject'] = subject
        msg['From']    = self.email_attr['from']
        msg['To']      = self.email_attr['reply_to']
        msg['Bcc']     = self.email_attr['from']

        # 本文
        body = MIMEText(message, 'plain', self.encode)
        msg.attach(body)

        # 添付ファイル
        if attach_data:
            file_name = "%s.zip" % attach_name
            attachment = MIMEBase('application', 'zip')
            attachment.set_param('name', file_name)
            attachment.set_payload(attach_data)
            encoders.encode_base64(attachment)
            attachment.add_header("Content-Dispositon", "attachment", filename=file_name)
            msg.attach(attachment)

        # 送信
        smtp_server   = self._get_decrypted_environ("SMTP_SERVER")
        smtp_port     = self._get_decrypted_environ("SMTP_PORT")
        smtp_user     = self._get_decrypted_environ("SMTP_USER")
        smtp_password = self._get_decrypted_environ("SMTP_PASSWORD")
        smtp = smtplib.SMTP(smtp_server, smtp_port)
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()
        smtp.login(smtp_user, smtp_password)
        smtp.send_message(msg)
        smtp.quit()
        print("Successfully sent email")

        return

    def _get_decrypted_environ(self, key):
        """
        暗号化された環境変数を復号化
        """

        client = boto3.client('kms')
        encrypted_data = os.environ[key]
        return client.decrypt(CiphertextBlob=base64.b64decode(encrypted_data))['Plaintext'].decode('utf-8')

def lambda_handler(event, context):

    # イベントからバケット名、キー名を取得
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])

    try:
        # S3からファイルの中身を読み込む
        s3_object = s3.get_object(Bucket=bucket, Key=key)
        email_string = s3_object['Body'].read().decode('utf-8')

        # メールを解析
        parser = MailParser(email_string)

        # メール転送
        forwarder = MailForwarder(parser.get_attr_data())
        forwarder.send()
        return

    except Exception as e:
        print(e)
        raise e

pyminizip

パスワード付きzipは標準のライブラリじゃできないっぽい。
ということで、ここだけpyminizipという外部ライブラリに頼りました。
ただこれ、インストール時にビルドしてバイナリ作る系のライブラリだったので、Lambdaで動かすためにローカルでAmazonLinuxのDockerコンテナ立ててバイナリを作りました。何かほかにいい方法あるのかな。。

AWS SAM

ちなみに、これはAWS SAMを使ってローカルテストしてみました。
SMTPサーバーの情報を直書きして試してたところまでは良かったけど、それを環境変数に移すとうまく動かなくて挫折しました。修正はされてるけどリリースされてないっぽい。

導入方法

せっかくなので公開してみます。コードネームzaru
かなり設定方法が泥臭いままですがご容赦ください。。
https://github.com/Kta-M/zaru

自分の環境(Mac, Thunderbird)でしか試してないので、メーラーやその他環境によってはうまくいかないかも?自己責任でお願いします。

SES

SESはまだ東京リージョンで使えないので、オレゴンリージョン(us-west-2)で構築します。

ドメイン検証

まずはSESに向けてメールが送れるように、ドメインの検証を行います。
やり方はいろいろなので、このあたりは割愛。
たとえばこのあたりとか参考になるかも -> RailsでAmazon SES・Route53を用いてドメインメールを送信する

Rule作成

ドメインの検証ができたら、Ruleを作成します。

メニュー右側のRule Setsから、View Active Rule Setをクリック。
ses_rule_01.png

Create Ruleをクリック。
ses_rule_02.png

受信するメールアドレスを登録。検証を行なったドメインのメールアドレスを入力して、Add Recipientをクリック。
ses_rule_03.png

メール受信時のアクションを登録。
アクションタイプとしてS3を選択し、受信したメールデータを保存するバケットを指定します。このとき、Create S3 bucketでバケットを作成してあげると、必要なバケットポリシーが自動で登録されて便利。
SESからバケットへのファイルアップロードを許可するポリシーが設定されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSESPuts-XXXXXXXXXXXX",
            "Effect": "Allow",
            "Principal": {
                "Service": "ses.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<ses-bucket-name>/*",
            "Condition": {
                "StringEquals": {
                    "aws:Referer": "XXXXXXXXXXXX"
                }
            }
        }
    ]
}

また、バケットに保存されたメールデータは、貯めておいても仕方ないので、ライフサイクルを設定して一定期間経過後削除されるようにしておくといいかも。
ses_rule_04.png

ルールに名前を付けます。あとはデフォルトで。
ses_rule_05.png

登録内容を確認して、登録!
ses_rule_06.png

Lambda

デプロイ

SESと同じくオレゴンリージョンにデプロイします。
CloudFormationを利用するので、データをアップロードするS3バケットを作っておいてください。

# git clone git@github.com:Kta-M/zaru.git
# cd zaru
# aws cloudformation package --template-file template.yaml --s3-bucket <cfn-bucket-name> --output-template-file packaged.yaml
# aws cloudformation deploy --template-file packaged.yaml --stack-name zaru-stack --capabilities CAPABILITY_IAM --region us-west-2

Lambdaのコンソールに行くと、関数が作成されています。
また、この関数の実行に必要なIAMロールも作成されています。
lambda_01.png

トリガー設定

バケットにメールデータが入るのをトリガーにして、Lambdaが動くように設定します。

関数の詳細画面のトリガータブに移動します。
lambda_02.png

トリガーを追加をクリックし、S3のイベントを作成します。
SESからデータが来るバケット、イベントタイプはPutです。それ以外はデフォルト。
バケットはlambda_03.png

暗号化キーを作成

このLambda関数内では、暗号化された環境変数からSMTP関連の情報を取得しています。
その暗号化に使用するキーを作成します。

IAMコンソールから、左下にある暗号化キーをクリックします。
リージョンをオレゴンに変更し、キーを作成してください。
lambda_04.png

設定内容は、任意のエイリアスを設定するだけで、残りはデフォルトでOKです。
lambda_05.png

環境変数数設定

Lambdaに戻って、関数内で使用する環境変数を設定します。
コードタブの下のほうに、環境変数を設定するフォームがあります。
暗号化ヘルパーを有効にするにチェックを入れ、先ほど作成した暗号化キーを指定します。
環境変数は、変数名と値(平文)を入力し、暗号化ボタンを押します。すると、指定した暗号化キーで暗号化してくれます。
設定する環境変数は以下の4つです。

変数名 説明
SMTP_SERVER smtpサーバー smtp.example.com
SMTP_PORT smtpポート 587
SMTP_USER smtpサーバーにログインするユーザー名 test@example.com
SMTP_PASSWORD SMTP_USERのパスワード

lambda_06.png

ロール設定

最後に、このLambda関数を実行するロールに必要な権限を付けます。
– メールデータを保存するS3バケットからデータを取得する権限
– 暗号化キーを使って環境変数を復号する権限

まず、IAMコンソールのポリシーに行き、ポリシーの作成->独自のポリシーを作成で以下の2つのポリシーを作成します。
lambda_07.png

ポリシー:s3-get-object-zaru
<ses-bucket-name>には、SESからメールデータを受け取るバケット名を指定してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1505586008000",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::<ses-bucket-name>/*"
            ]
        }
    ]
}

ポリシー;kms-decrypt-zaru
<kms-arn>には、暗号化キーのARNを指定してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1448696327000",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "<kms-arn>"
            ]
        }
    ]
}

最後に、この2つのポリシーを、Lambda関数実行ロールにアタッチします。
まず、IAMコンソールのロールに行き、ロールを選択し、ポリシーのアタッチからアタッチします。
lambda_08.png

動作確認

これで動くようになったはずです。
ToにSES向けに設定したメールアドレス、Reply-Toに相手のメールアドレスを設定し、適当なファイルを添付して送ってみてください。どうでしょう?

まとめ

どんとこいzip添付!

続きを読む

業務でawsを使っていたらCloudTrailを設定しよう

awsを使っていたらCloudTrailを設定しよう

CloudTrailとは

  • AWS アカウントのガバナンス、コンプライアンス、運用監査、リスク監査を可能にするサービスです。(公式)

CloudTrail-01-HeaderPic-v2-small.png

できること

  • アカウントアクティビティをログに記録し、継続的に監視し、保持できます。
  • SDK やコマンドラインツール、その他の AWS のサービスを使用して実行されるアクションなど、AWS アカウントアクティビティのイベント履歴を把握できます。
  • このイベント履歴により、セキュリティ分析、リソース変更の追跡、トラブルシューティングをより簡単に実行できるようになります。

メリット

コンプライアンスの簡素化

  • アカウント内で行われた操作のイベントログが自動的に記録および保存されるため、コンプライアンス監査を簡素化できます。
  • Amazon CloudWatch Logs との統合により、ログデータを検索したり、コンプライアンス違反のイベントを特定したり、インシデントの調査と監査者の要求に対する応答を迅速化したりできます。

ユーザーとリソースのアクティビティの可視化

  • AWS マネジメントコンソールでの操作と AWS API コールを記録することにより、ユーザーおよびリソースのアクティビティを把握しやすくなります。
  • AWS を呼び出したユーザーとアカウント、呼び出し元 IP アドレス、および呼び出し日時を特定できます。

セキュリティのオートメーション

  • セキュリティの脆弱性を引き起こす可能性のあるイベントが検出されたときに実行されるワークフローを定義できます。
  • 例えば、 Amazon S3 バケットを公開する API コールが CloudTrail によってログに記録されたときに特定のポリシーをそのバケットに追加する、というワークフローを作成できます。

利用してみる

  • CloudTrailにアクセスするとすでにログが記録されているのがわかります
  • 誰が何時に何の操作をしたのかがわかります

Screen Shot 2017-09-16 at 21.28.41.png

  • すべてのイベントを表示してみると

Screen Shot 2017-09-16 at 21.28.56.png

  • 7日以上のログを保存する場合や、アラートを飛ばしたいときは「証跡情報を作成」します

Screen Shot 2017-09-16 at 21.29.17.png

  • s3バケットなどを細かく設定できます!!!!!!

料金

  • WS CloudTrail を使用すると、サポートされたサービスの作成、変更、削除のオペレーションに関する過去 7 日間分のアカウントアクティビティの表示とダウンロードを無料で実行できます。

  • 証跡情報を作成する際に AWS CloudTrail からの課金は発生しません。CloudTrail の証跡を作成すると、Amazon S3 バケットに 2 種類のイベントを送信できるようになります。

  • 詳しくは公式の料金表

サポートされているサービス

ほとんどサポートされていますね

  • AWS Marketplace
  • Amazon Athena
  • Amazon CloudSearch
  • Amazon EMR
  • AWS Data Pipeline
  • Amazon Kinesis Firehose
  • Amazon Kinesis Streams
  • Amazon QuickSight
  • Amazon API Gateway
  • Amazon Elastic Transcoder
  • Amazon Elasticsearch Service
  • Amazon Simple Workflow Service
  • AWS Step Functions
  • Amazon Machine Learning
  • Amazon Polly
  • Amazon WorkDocs
  • アプリケーションの Auto Scaling
  • Auto Scaling
  • Amazon EC2 Container Registry
  • Amazon EC2 Container Service
  • AWS Elastic Beanstalk
  • Amazon Elastic Compute Cloud
  • Elastic Load Balancing
  • AWS Lambda
  • Amazon Lightsail
  • Amazon DynamoDB
  • Amazon ElastiCache
  • Amazon Redshift
  • Amazon Relational Database Service
  • Amazon WorkSpaces
  • AWS CodeBuild
  • AWS CodeCommit
  • AWS CodeDeploy
  • AWS CodePipeline
  • AWS CodeStar
  • Amazon GameLift
  • AWS IoT
  • AWS Application Discovery Service
  • AWS CloudFormation
  • AWS CloudTrail
  • Amazon CloudWatch
  • Amazon CloudWatch Events
  • Amazon CloudWatch Logs
  • AWS Config
  • AWS マネージドサービス
  • AWS OpsWorks
  • AWS OpsWorks for Chef Automate
  • AWS Organizations
  • AWS Service Catalog
  • Amazon Simple Email Service
  • Amazon Simple Notification Service
  • Amazon Simple Queue Service
  • AWS Database Migration Service
  • AWS Server Migration Service
  • Amazon Cognito
  • AWS Device Farm
  • Amazon CloudFront
  • AWS Direct Connect
  • Amazon Route 53
  • Amazon Virtual Private Cloud
  • AWS Certificate Manager
  • Amazon Cloud Directory
  • AWS CloudHSM
  • AWS Directory Service
  • AWS Identity and Access Management
  • Amazon Inspector
  • AWS Key Management Service
  • AWS Security Token Service
  • AWS WAF
  • Amazon Elastic Block Store
  • Amazon Elastic File System
  • Amazon Glacier
  • Amazon Simple Storage Service
  • AWS Storage Gateway
  • AWS Personal Health Dashboard
  • AWS サポート

サポートされていないもの

サポートされていないものもありますので注意が必要です。

Amazon AppStream
Amazon AppStream 2.0
Amazon Connect
Amazon Lex
Amazon Mobile Analytics
Amazon Pinpoint
Amazon Rekognition
Amazon WorkMail
AWS Batch
AWS Greengrass
AWS Shield
AWS Snowball
AWS X-Ray
Amazon Chime
Amazon WorkSpaces Application Manager
AWS Artifact
AWS Mobile Hub
AWS Snowball Edge
AWS Snowmobile
AWS Trusted Advisor

便利なリンク先

こちらいろいろやっていらっしゃる方がいます

まとめ

  • これでアカウントのガバナンス、コンプライアンス、運用監査、リスク監査を可能になりました。
  • 案外かんたんなので、設定触ってみることをおすすめします!

続きを読む

[AWS]EC2シャットダウン時にログをS3に逃がす

AWS上でAutoScaleを利用していると、ヘルスチェックでミスったりの予期せぬEC2停止でEC2ごと削除されることがままあります。
いざ原因をログから解析しようと思っても、EC2ごとログが消えているため、どうにもならないことがありました。

そんなときのためにEC2停止時に指定したログをS3上に逃がすスクリプトを作成しました。
注:この方法は、EC2がshutdownコマンドなどで正常停止する場合に取得可能です。(AutoScaleなどから切り離された時など)
EC2自体がカーネルパニックやハードウェア障害で正常に停止していない場合はこの方法では救えません(経験上ほぼないですが)

[必要なもの(※Amazon Linuxに設定する前提)]

-EC2停止時にS3にログを退避するスクリプト
-退避する対象のログリスト
-ログ退避スクリプトをEC2停止時に実行するサービス起動・停止(init.d)スクリプト

[EC2停止時にS3にログを退避するスクリプト]

日付とインスタンスIDをログに付与してS3にアップロード
※1 以下はawsの情報取得にcredentialsにアクセスキーを記載して利用しているが、IamRoleを利用できるならばそのほうがよい
※2 パスも/usr/local/bin/と/usr/local/etc/を利用しているが任意

/usr/local/bin/logmvs3_shutdown.sh
#!/bin/sh

DATESTR=`date +%Y%m%d_%H%M%S`

# コンフィグ情報読み込み
INSTANCEID=`wget -q -O - http://169.254.169.254/latest/meta-data/instance-id`
AWSSETTING=/root/.aws/credentials
SRCFILELIST=/usr/local/etc/shutdown_targetfile.txt
TMPCPPATH=/tmp/logtmp
BUCKET=hogehoge-log
FOLDER=hoge/shutdown

export AWS_CONFIG_FILE=$AWSSETTING

# ログ取得の対象を作業フォルダにコピー
mkdir -p $TMPCPPATH 2>/dev/null

cat ${SRCFILELIST} | while read FILEPATH
do
cp -f ${FILEPATH} $TMPCPPATH/
done

# 指定パス対象にgzip化
for i in `find $TMPCPPATH -maxdepth 1 -type f | egrep -v gz`
do
        gzip -c $i > $i"_shutdown_"$INSTANCEID"_"${DATESTR}.gz
        rm -f $i
done

# S3のフォルダへ.gzファイルをアップロード 合わせて、取得したログ一覧も
aws s3 cp $TMPCPPATH s3://$BUCKET/$FOLDER/$DATESTR/ --exclude "*" --include "*.gz" --recursive
aws s3 cp $SRCFILELIST s3://$BUCKET/$FOLDER/$DATESTR/

# 作業フォルダを削除する
rm -rf $TMPCPPATH
[EC2停止時にS3にログを退避するスクリプト]

ファイル単位で指定
ここではシステム関連の/var/log/のみ退避させている。tomcatやnginxなどのアプリケーションログも指定可能
各システムに必要なものを指定する

/usr/local/etc/shutdown_targetfile.txt
/var/log/messages
/var/log/cron
/var/log/secure
[ログ退避スクリプトをEC2停止時に実行するサービス起動・停止(init.d)スクリプト]

サービス起動時に2重起動防止をしておき、サービス停止時(マシン停止時)にスクリプトを実行
chkconfig: runlevel 起動順番 終了順番 のため、起動を一番遅く、停止時は一番早く実行する

/etc/rc.d/init.d/logmvs3_shutdown
#!/bin/bash
#
# Version: 0.1
#
# chkconfig: 345 99 01
# description: logmvs3_shutdown shell
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin

# Source function library.
. /etc/init.d/functions

start() {
        touch /var/lock/subsys/logmvs3_shutdown
}

stop() {
        rm -f /var/lock/subsys/logmvs3_shutdown
        /usr/local/bin/logmvs3_shutdown.sh

restart() {
        stop
        start
}


case "$1" in
        start)
                start
                ;;
        stop)
                stop
                ;;
        restart)
                restart
                ;;
        *)
                echo "Usage: $0 {start|stop|restart}"
                exit 1
esac
exit 0
[サービス登録と確認]

あとは以下のコマンドを実行し、サービスに登録および開始しておく
chkconfig --add logmvs3_shutdown
service logmvs3_shutdown start

再起動してみて、指定のS3バケットのフォルダにログがアップロードされていればOK

続きを読む

AWSでガッチリログ解析

はじめに

現在EC2上で動いているサービスのログを解析することになり、方法を調べた結果AWS上のサービスを使用してする方法がベストだと考え実装した。これらのサービスは比較的に最近できた(たぶん)のであまりこの方法で紹介している日本語の記事がなかった。今でもログ解析といえば、fluentd + Elasticsearch + Kibanaが圧倒的に王道みたいだがわざわざAWSが色々と提供してくれているのでそちらを使う。以下、EC2でサーバーが動きログファイルがあることを前提にしています。インフラの人でもなんでもないので間違いがあれば、優しくご指導ください。

参考資料

この記事で詰まった際は下記を参照ください。また私は下記の資料を通して実装しました。
AWS公式 ログ解析のチュートリアル
AWS Kinesis Firehose
AWS Kinesis Analytics
AWS Elastic Search
Amazon Kinesis Agent

全体図

Screenshot 2017-09-06 02.55.05.png

全体的な図は上記のようになります。画像ではインスタンスで走っているサーバーがApacheとなっていますが、Nginxや他のサーバーを使用でも設定次第で問題にはなりません。

全体の流れ

システムを構築している時、記述通りしているつもりでも簡単な間違いで動かなくなることが多々あります。そんなときに全体の流れ、繋がりをしっかりと理解することによりデバックが容易になります。

  1. EC2インスタンスから、サービスとサービスをつなげる役割を持つAmazon Kinesis Firehose(以Firehose)にログを流す。
  2. このFirehoseはS3 Bucketにデータを流す先としている。
  3. 次に、Amazon Kinesis AnalyticsはFirehoseからS3 Bucketに流れているログデータを解析し、解析後のデータを他のFirehoseに渡す。
  4. 解析後のログを渡されたFirehoseはAmazon Elasticsearch Service(以下Elasticsearch)にデータを受け流す。
  5. Elasticsearchに保存されたデータはkibinaを通じデータを可視化し、ユーザーにたどり着く。

注意: ヘッダーの右上からRegionをN.Virginiaにしてから以下を進めてください。(N.Virginia を含む特定のRegionでしかここで使用するAmazon Elasticsearch Serviceがないためです。)

ステップ1:一つ目のFirehoseを作成

EC2インスタンスとS3へログを流すFirehoseを作成します。
1. Amazon Kinesis へアクセスします。

2. Firehoseへ行き、 Create Deliver Stream(デリバリーストリームを作成) ボタンを押し作成します。

3. Delivery stream nameを入力しますが、ここはこのFirehoseの名前なのでなんでも構いません。何を入力すればよいかわからない方は 「log-ingestion-stream」としましょう。

4. 次に情報元をDirect PUT or other sources(直接のPUTまたは他の情報源)に選択します。

先にお話したとおりFirehoseは情報を一つの場所からもう一つの場所へ移行させる役割を持ちます。ここではKinesis StreamまたはDirect PUT or other sourcesの選択肢があり、Kinesis Streamを情報元にしている場合はKinesis Streamからそれ以外はDirect PUT or other sourcesを選択します。今回の情報源はEC2なのでDirect PUT or other sourcesとなります。

5. 一番下まで行き、Nextボタンを押します。押した後、次の選択肢はデフォルトのDisabledを選択します。

Firehoseではただ情報を受け渡すだけではなく、AWS Lambdaを使用して情報に変更を加えてから渡すことができるようです。ここではそれの選択肢として、Disable(しない)とEnable(する)があります。私は使用したことがないのでこれ以上の解説はできません。

6. Nextボタンを押し、次は送り先(Destination)をAmazon S3に選択します。

一つ目はEC2からS3へ情報(データ)を送ります。

7. S3のバケットとして、ログの保管する場所を指定します。ここではCreate Newボタンを押してバケットの名前をなんでも構いませんのでつけてください。何にすればいいかわからない方は「log-bucket」としてください。

8. Nextボタンを押し、一番したまで行けば、IAM roleをしていするフォームがありますので、Create new or Chooseボタンを押しましょう。

9. タブが開けば、Create new IAM roleを選択したまま他はデフォルトで右下のAllowを押します。

10. 完了すればNextを押し確認に問題がないようでしたらCreate delivery streamボタンを押し、作成します。

ステップ2:Amazon Kinesis AgentをEC2にインストール

先程作成した、firehoseにEC2からログデータを送るために、AWSが公式に開発しているJAVA製のEC2からFirehoseにリアルタイムでファイル情報を送ってくれるAmazon Kinesis Agentをインストールしましょう。ここが最も間違いが起きやすいところかと思います。

1. ダウンロード方法を参考にAmazon Kinesis Agentインストールしましょうしてください。

yumが入っていれば以下のコマンドでインストールできます。

sudo yum install –y aws-kinesis-agent

私の場合は、 Java JDEなかったのでtools.jar が見つからないというようなエラーが出ました。そのようなエラーが出た方は

sudo apt-get install openjdk-7-jdk

JDKをインストールしましょう。(参考)

2. agentの設定をする

amazon kinesis agentに「どのファイルログのデータをどこに送るのか」または「どのような形で送るのか」という事などを設定していきます。
Amazon Kinesis Agentの設定ファイルは /etc/aws-kinesis/agent.jsonにあるかと思います。そのファイルを以下のように設定してください。
なお、
filePattern: "full-path-to-log-file"full-path-to-log-file解析したいログファイルへのフルパスに(nginxをご使用の方は etc/nginx/access.log かと思います)してください。

deliveryStream: "name-of-delivery-stream"name-of-delivery-streamを送りたいfirehoseの名前に(ステップ1で作成したfirehoseの名前)してください。

/etc/aws-kinesis/agent.json
{
  "cloudwatch.endpoint": "monitoring.us-east-1.amazonaws.com",
  "cloudwatch.emitMetrics": true,
  "firehose.endpoint": "firehose.us-east-1.amazonaws.com",
  "flows": [
  {
    "filePattern": "full-path-to-log-file",
    "deliveryStream": "name-of-delivery-stream",
    "dataProcessingOptions": [
    {
      "initialPostion": "START_OF_FILE",
      "maxBufferAgeMillis":"2000",
      "optionName": "LOGTOJSON",
      "logFormat": "COMBINEDAPACHELOG"
    }]
 }
 ]
}

長くなってしまいますが、親切にするためにはここで気にしなくてはならないことが幾つかあります。

エンドポイント(firehose.endpoint)

一番初めの注意通り、N.VirginiaにFirehoseを作成している方はなんの問題もありませんが、そのようにしていない方はエンドポイント一覧を参考にして, cloudwatch.endpointfirehose.endpointを変更してください。なお、はじめの注意でも述べたようにElasticsearchでは数少ないリージョンにしか対応していないため、他のリージョンにしている方は最後になってやり直さなければならない可能性もあります。

情報処理の設定(dataProcessingOptions)

Apacheを使用している方はなんの問題もありませんが、nginxを使用している方はここに少し変更が必要です。こちらの設定でログデータの処理方法を変更できます。
設定のオプションとしてmatchPatternがあり、こちらと正規表現を使用してどのようなログフォーマットでも処理が可能になります。下のものは処理をしたいログとそのmatchPatternの一つの例ですので参考にしてご自身のものを変更してください。(参照)

111.111.111.111 - - [02/Dec/2016:13:58:47 +0000] "POST /graphql HTTP/1.1" 200 1161 "https://www.myurl.com/endpoint/12345" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36" 0.172 "query user userMessages hasPermissions F0 F1" 11111;222222
{
    "dataProcessingOptions": [
    {
      "optionName": "LOGTOJSON",
      "logFormat": "COMMONAPACHELOG",
      "matchPattern": "^([\d.]+) - - \[([\w:/]+).*\] "(\w+) /(\w+).*(\d.+)" (\d{3}) (\d+) "(\S+)" "(.*?)" ([\d.]+) "(.*?)" (\d+);(\d+)",
      "customFieldNames": ["client_ip", "time_stamp", "verb", "uri_path", "http_ver", "http_status", "bytes", "referrer", "agent", "response_time", "graphql", "company_id", "user_id"]
    }]
}

3. Amazon Kinesis Agent始動

以下のコマンドでagentを動かします。

sudo service aws-kinesis-agent start

これで終わってもいいのですが、Agentのログファイルでしっかり動いていることを確認しましょう。

tail -f /var/log/aws-kinesis-agent/aws-kinesis-agent.log

tailコマンドを利用してAgentの活動ログを監視します。無事動いているようなら

2017-09-08 08:47:29.734+0000 ip-10-0-0-226 (Agent.MetricsEmitter RUNNING) com.amazon.kinesis.streaming.agent.Agent [INFO] Agent: Progress: 9135 records parsed (1257812 bytes), and 9132 records sent successfully to destinations. Uptime: 88380043ms

このようなログが定期的に流れてきます。

考えられるエラーと解決方法を紹介します

agentがログファイルを読めていない

考えれる原因の考えられる原因は、
1. ログファイルへのフルパスが間違えている
2. ログファイルを読む権限がない

1.のフルパスが間違えている場合は、先程のagentの設定ファイルで変更した。”filePattern”のパスに問題があることになるので、そのパスが実際存在するか確認しましょう。

tail ログファイルへのパス

file does not exists 的なメッセージがでたらファイルが存在しないということなので正しいログファイルへのフルパスに変えてあげましょう。

2.のログファイルを読む権限がない場合は、agentがログファイルを読み込めれるように

sudo chmod o+r ログファイルへのパス

で誰でもログファイルを読めるようにしましょう。(インフラエンジニア的にこれはありなのか疑問)

ログデータの送り先が不在

これが該当する場合は、確か404のエラーコードを大量にログファイルに出力されます。
この場合は、agentのログファイルのdeliveryStreamのプロパティの名前と一番初めに作成したAmazon Kinesis Firehoseの名前があっていることを確認しましょう。
それがタイポもない場合はagentを再インストールしたらなおるかもしれません。

アクセスキーとシークレットキーがみつからない

credentialsなのでAWSのアクセスキーとシークレットキーが見つからない方は、awsのcredentialsに登録してもいいですが、確実なのはagentの設定ファイルで設定することです。

/etc/aws-kinesis/agent.json
{
  "awsAccessKeyId": "あなたのアクセスキー",
  "awsSecretAccessKey": あなたのシークレットキー
}

ステップ3:Amazon Elasticsearch Serviceドメインの作成

  1. Elasticsearchのページに行き、新たなドメインを作成するためにGet StartもしくはCreate a new domainのボタンをクリックします。
  2. はじめにドメインに名前をつけます。他と同じくなんでも構いませんが、何をつければいいのかがわからない方は「log-summary」にでもしましょう。
  3. 次の選択肢はElasticsearchのバージョンですが、特にこだわりがないかたは最新でものでいいと思います。なのでそのままにして、Nextボタンをおしましょう。
  4. 次の設定も同じく特にこだわりがない方はそのままにしておいて、Nextボタンをおして次にいってください。
  5. その次では、このElasticsearchへのアクセスを制限をします。Templateから簡単に設定ができますのでそちらから各自設定してください。難しいことはわからないしめんどくさいのも嫌な方はTemplateからAllow open access to the domainを選択してください。こちらは特に制限なくアクセスが可能になるのでAWS側ではおすすめしていません。重要なデータをお使う場合は必ずきちんと設定しましょう。
  6. 次の画面でもろもろの設定を確認してConfirmボタンをおしましょう。そうすればElasticsearchドメインが作成されます。(起動までしばらく時間がかかります)

ステップ3:二つ目のFirehoseの作成

次に先程作成したAmazon Elasticsearch Serviceへデータを送るためにfirehoseを作成します。これは、EC2から一つ目のFirehoseを通じ、(まだ作ってない)Amazon Kinesis Analyticsにより処理された情報をElasticsearchに送るためのFirehoseです。

  1. Amazon Kinesis へアクセスします。
  2. Firehoseへ行き、 Create Deliver Stream(デリバリーストリームを作成) ボタンを押し作成します。
  3. Delivery stream nameを入力しますが、ここはこのFirehoseの名前なのでなんでも構いません。何を入力すればよいかわからない方は 「log-summary-stream」としましょう。
  4. 次に情報元をDirect PUT or other sources(直接のPUTまたは他の情報源)に選択します。
  5. 一番下まで行き、Nextボタンを押します。押した後、次の選択肢はデフォルトのDisabledを選択します。
  6. Nextボタンを押し、次は送り先(Destination)をAmazon Elasticsearch Serviceに選択します。
  7. 送り先のElasticsearchとして、先ほど作成したドメインを選択します
  8. Indexは、request_dataにして、typesはrequestsにします。その他のIndex RotationとRetry durationはそのままにしておいてください。
  9. Elasticsearchに送るのを失敗した場合にS3にバックアップとしてデータを送ります。Backup S3 bucketとして新しく、バケットを作成しましょう。名前はなんでも構いませんが、思いつかない方は「log-summary-failed」とでもしておいてください。
  10. Nextボタンを押し、一番したまで行けば、IAM roleをしていするフォームがありますので、Create new or Chooseボタンを押しましょう。
  11. タブが開けば、Create new IAM roleを選択したまま他はデフォルトで右下のAllowを押します。
  12. 完了すればNextを押し確認に問題がないようでしたらCreate delivery streamボタンを押し、作成します。

ステップ4: Amazon Kinesis Analytics を作成

Amazon Kinesis Analyticsでははじめに作成したEC2からログデータを取ってくる一つ目のFirehoseと先ほど作成したElasticsearchを目的地にしている二つ目のFirehoseの中間にあるものになります。つまりは一つ目のFirehoseからログ情報を所得し、それを処理した後、二つ目のFirehoseに渡しそれがElasticsearchへ流れ着きます。では実際に作っていきましょう。

  1. Amazon Kinesis Analyticsへアクセスする。
  2. Create Applicationに行き、Applicationの名前をつけます。何をつければいいかわからない方は、log-aggregationとでもしておいてください。なんでも構いません。
  3. Descriptionの方も何か書きておいた方は書いていただき、特にわからない方は空欄のままCreate Applicationボタンをおしましょう。

情報元(Source)を選択する

  1. 作成した後、作成されたアプリケーションのホームページに行くと思います。 そのままConnect to a sourceボタンをおして情報元を選択します。
  2. 情報元を選択するページに行くと2つのFirehoseの名前が出てくると思いますが、ここのステップのはじめにも紹介したように一つ目のFirehose(EC2から情報を取ってきている方)を選択してください。
  3. 選択した後しばらく待つと、流れてきているログをAnalyticsアプリケーションが所得するので、それが確認出来しだいSave and continueボタンをおしましょう。

SQLを使用して処理をする

今まではクリックするだけだっだのですが、ここがおそらく最も大変なところです。SQLをもともとちゃんと書けるエンジニアは簡単に実装できていいじゃんという感じですが、私みたいなエセエンジニアは困ったものです。

Go to SQL editorでFirehoseにホーム画面からSQLおエディターに移動できます。
ここで好きなようにデータを処理変更してくださいと言っても何をどうすればいいかわかんない方がいると思います。
このSQLではtableの代わりにstreamを変更します。SOURCE_SQL_STREAM_001が情報が流れているstreamでこちらからログ情報を取ってきて、処理をしDESTINATION_SQL_STEAMという名前のストリームに変換します。そうしたらそのストリームを次のFirehoseに送ってくれます。
以下に一つの処理方法の例を載せておきます。これはresponseというカラムの数の合計をstatusCountに入れています。つまりresponse(200, 404とか)の数をまとめて数を数えているということです。
ここからわかることは、”(ダブルクオーテーションマーク)で囲むことで、SOURCE_SQL_STREAM__001のカラムの値が手に入ることです。

CREATE OR REPLACE STREAM "DESTINATION_SQL_STREAM" (
 datetime VARCHAR(30),
 status INTEGER,
 statusCount INTEGER);
CREATE OR REPLACE PUMP "STREAM_PUMP" AS
 INSERT INTO "DESTINATION_SQL_STREAM"
 SELECT
 STREAM TIMESTAMP_TO_CHAR('yyyy-MM-dd''T''HH:mm:ss.SSS',
LOCALTIMESTAMP) as datetime,
 "response" as status,
 COUNT(*) AS statusCount
 FROM "SOURCE_SQL_STREAM_001"
 GROUP BY
 "response",
 FLOOR(("SOURCE_SQL_STREAM_001".ROWTIME - TIMESTAMP '1970-01-01 00:00:00') minute / 1 TO MINUTE);

こちらが僕が実際に使っているコードです。 SOURCE_SQL_STREAM_001のdatetimeのフォーマットyyyy-MM-dd''T''HH:mm:ss.SSSのように変換してElasticsearchへ保存しています。

CREATE OR REPLACE STREAM "DESTINATION_SQL_STREAM" (
 datetime VARCHAR(30),
 host VARCHAR(16),
 request VARCHAR(32),
 response INTEGER,
 bytes INTEGER,
 agent VARCHAR(32));
CREATE OR REPLACE PUMP "STREAM_PUMP" AS
 INSERT INTO "DESTINATION_SQL_STREAM"
 SELECT
 STREAM 
 TIMESTAMP_TO_CHAR('yyyy-MM-dd''T''HH:mm:ss.SSS', CHAR_TO_TIMESTAMP('dd/MMM/yyyy:HH:mm:ss Z', "datetime")) as datetime,
 "host" as host,
 "request" as request,
 "response" as response,
 "bytes" as bytes,
 "agent" as agent
 FROM "SOURCE_SQL_STREAM_001"

より詳しくは公式ページドキュメントを参照してください。
コメントで質問していただければ頑張って答えます。

目的地を選択する

そのままDestinationタブを選択して、処理された情報の送り先を選択します。Select a streamから二つ目のFirehose(Elasticsearchにつながっている方)を選択し、他のものはそのままでSave and continueをクリックしましょう。

これで全てのパイプラインは繋がり作業はほとんど完成です。

ステップ5:Kibanaで可視化されたデータをみる

Amazon Elsacticsearch serviceにはデータを可視化してくれるKibanaが含まれています。

Elasticsearchの作成したドメインのホーム画面に行きkibanaという文字の横にリンクが有ると思います。そこをクリックするとKibinaのページに飛びます。はじめて訪れた場合はindex-patternを入力しなければなりません。そこにはrequest_dataと入力してください。

仮に、request_dataと入力しても続けれない方はしばらく時間をおいてみてもう一度試してください。データがElasticsearchに貯まるまで時間がかかるためだと思われます。
KibanaはDatetimeのカラムを自動で認識します。ただし、フォーマットに制限があるらしくyyyy-MM-dd''T''HH:mm:ss.SSSのような形で情報を保存すればKibanaは必ずDatetimeだと認識します。

そのまま続行を押すとKibanaのサイトで自由に可視化されたデータをみれます。

長い作業、お疲れ様でした。他にはAmazon Kinesis Analyticsでデータを複雑に処理をしたりも可能なので挑戦してみてください。
編集リクエストお待ちしております。m(_ _)m

続きを読む

【Rails】ブラウザからS3へ画像を直接アップロード

こちらを参考に, 画像をブラウザからS3へ直接アップロードできるようにしました.

1. AWSの準備 (時間:5分)

IAMでS3アクセス用のユーザを作成します. まずは以下のようにコンソール画面からIAMへアクセス.

ss 2017-09-18 12.44.57.png

次に, 以下のUsersをクリック
ss 2017-09-18 12.48.08.png

Add userをクリック
ss 2017-09-18 12.48.16.png

任意の名前を入力. プログラマティックアクセスにチェックして次へ.
ss 2017-09-18 12.48.37.png

既存ポリシーの中から AmazonS3FullAccessを見つけてチェック.
eee

以上でユーザ作成は完了. 詳細画面でCreate Access keyをクリックして必要なキーを取得する.
aaa

このようにkeyIDとシークレットkeyが表示されるので, メモっておく.
ss 2017-09-18 12.50.27.png

次にS3の設定を行う.

適当なバケットを作成する. 各種設定はデフォルトのままで良い. 作成し終えたら, そのバケットのプロパティを開き, Permissions -> CORS configurationに以下のように記述する. これで終わり.

ssd

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

2. バックエンドでS3ポリシーを作成

自身のRailsアプリに, POSTメソッドで叩ける適当なエンドポイントを作成する.

routes.rbに例えば以下のように記述.

post 'image_upload', to: 'images#upload'

そして, images#uploadを次のように定義する.

class ImagesController < ApplicationController
  # Deviceを使って認証処理をしている場合, 次の1行によって認証を要求できる.
  before_action :authenticate_user!, only: [:upload]

  # 先ほど作ったバケットの名前とアクセスKeyIdとシークレットkey
  S3_BUCKET = 'your-bucket-name'
  AWS_ACCESS_KEY_ID = 'XXX'
  AWS_SECRET_KEY = 'XXX'

  def upload
    # アップロード後のファイル名
    key = 'hogehoge'

    acl = 'public-read'
    ctype = params[:content_type]

    # ポリシー作成
    policy_document = {
      # 1分間のみ有効
      expiration: (Time.now + 1.minute).utc,
      conditions: [
        # アップロード先のS3バケット
        { bucket: S3_BUCKET },
        # ファイルの権限
        { acl: acl },
        # ファイル名
        { key: key },
        # ファイルの形式
        { 'Content-Type' => ctype },
        # アップロード可能なファイルのサイズ
        ['content-length-range', params[:size], params[:size]]
      ]
    }.to_json
    policy = Base64.encode64(policy_document).gsub("\n", '')

    # signatureの作成
    signature = Base64.encode64(
        OpenSSL::HMAC.digest(
            OpenSSL::Digest::Digest.new('sha1'),
            AWS_SECRET_KEY, policy)).gsub('\n', '')

    # アップロードに必要な情報をJSON形式でクライアントに返す
    render json: {
      url: "https://#{S3_BUCKET}.s3.amazonaws.com/",
      form: {
        AWSAccessKeyId: AWS_ACCESS_KEY_ID,
        signature: signature,
        policy: policy,
        key: key,
        acl: acl,
        'Content-Type' => ctype
      }
    }
  end
end

3. フロントエンドでポリシー取得しアップロード実行

画像をアップロードしたいViewに, 以下のようなjavascriptを仕込みます.


画像を投稿します

<div style="width: 500px">
  <form enctype="multipart/form-data" method="post">
    <input type="file" name="userfile" accept="image/*">
  </form>
</div>

<div id="thumbnail" style="max-width: 100px;">
  <img src="/plus.png" id="image_to_upload">
</div>

<button class="btn btn-primary" id="upload">投稿</button>

<script type="text/javascript">
$(function() {
  var file = null;

  // アップロードするファイルを選択
  $('input[type=file]').change(function() {
    file = $(this).prop('files')[0];
    // 画像以外は処理を停止させるためファイル形式をチェック
    if (file.type != 'image/jpeg' && file.type != 'image/png') {
      // 画像でない場合は消す
      var img_src = $('<img>').attr('src', '/plus.png')
      $('#thumbnail').html(img_src);
      file = null
      return;
    }

    // サムネ表示
    var reader = new FileReader();
    reader.onload = function() {
      var img_src = $('<img>').attr('src', reader.result)
      img_src.css('width', '500px');
      $('#thumbnail').html(img_src);
    }
    reader.readAsDataURL(file);
  });

  // アップロードボタンクリック
  $('#upload').click(function(){
    // ファイルが指定されていなければ何も起こらない
    if(!file) {
      return;
    }

    // ポリシーを発行する
    $.ajax({
      url: 'http://localhost:3000/image_upload',
      type: 'POST',
      data: {
        content_type: file.type,
        size: file.size
      }
    })
    .done(function( data, textStatus, jqXHR ) {
      // 取得したポリシーをフォームデータの形に整形する
      var name, fd = new FormData();
      for (name in data.form) if (data.form.hasOwnProperty(name)) {
        fd.append(name, data.form[name]);
      }
      fd.append('file', file); // ファイルを添付

      $.ajax({
        url: data.url,
        type: 'POST',
        dataType: 'json',
        data: fd,
        processData: false,
        contentType: false
      })
      .done(function( data, textStatus, jqXHR ) {
        console.log('success!')
      })
      .fail(function( jqXHR, textStatus, errorThrown ) {
        // アップロード時のエラー
        console.log('error: 2'); 
      });  

    })
    .fail(function( jqXHR, textStatus, errorThrown ) {
      // ポリシー取得時のエラー
      console.log('error: 1');
    });

  });

});
</script>

次のようなUIが出来上がります. 「ファイルを選択」をクリックしてファイルを適当に選択すると, サムネイルが表示されます. この状態で投稿ボタンを押すと画像がS3へ直接アップロードされます.
ss 2017-09-18 13.16.07.png

以上です.

続きを読む

オンプレMySQLからAuroraへの移行で躓いたポイント(その1・移行準備段階)

中小規模のオンプレWebシステム(DBとしてMySQLを使用)をAWS環境(Auroraを使用)に移行する際に躓いたポイントをいくつか記録しておきます。

0. 前提

移行前、移行後の環境として、以下のようなものを想定してください。

0-1. 移行前

  • VMwareによる仮想環境
  • ストレージはエントリレベル(10G-iSCSI)
  • MySQLは5.5/バイナリログあり/sync_binlog=1(遅い!)、複数あるがメインは1台
  • データ量は1TiB未満
  • DR:DBなどでのDBサーバ冗長化は行っていない
  • Webサーバは数台(Java8(G1GC)/Tomcat8)
  • Web~DB間のコネクションプーリングあり(DBCP2)

0-2. 移行後

  • WebサーバはほぼそのままEC2へ(東京リージョンAZ-a/cに等分して配置)、ALBを使用
  • Auroraはr3.xlarge~2xlarge程度、Multi-AZ、バイナリログあり(sync_binlog=1固定)、KMSで暗号化

1. 事前検証時

小規模なインスタンスを使って動作検証および軽い負荷テストを行った時点では、特に問題はありませんでした。
しかしその後、Auroraを冗長化してフェイルオーバー試験を行い始めたときに、問題が発生。

1つ目は、テスト用のAuroraクラスタ/インスタンスを削除する際のミスによる「あれ?フェイルオーバーしない」問題。

2つ目は、JavaのDB Connectorの選択問題。
Java環境向けにはAurora Driverを含むMariaDB Connector/J(1.2.0以降)があり、これを使えばAuroraのフェイルオーバーに対応できますが、

  • (使用中の)MySQL Connector/Jと細部の挙動が違う(パラメータで調整可能なものもあるが全て合わせるのは難しい)
  • Connector/Jから返される値の型や評価結果が違うことがある(これはアプリケーション側のコードの品質の問題ですが…)

ということで、断念。
結局、MySQL Connector/Jのまま、以下の対応を行うことに。

但し、プールしたコネクションの有効性確認にValidation Queryを使う「古い方法」でありパフォーマンス面ではあまりよろしくないです(対症療法です…)。

また、負荷を上げてフェイルオーバーを行うと「切れたコネクションプールの回収処理が詰まる」という3つ目の問題が発生。
当初はコネクションプーリングを外したり(接続負荷が低いMySQLがベースとはいえ処理が遅くなってしまったので断念)、プーリングのパラメータ調整で何とかしようと試行錯誤しましたが、「JavaのGCが追いついていない」のを見て、G1GCの目標時間(-XX:MaxGCPauseMillis)を調整して改善しました(オンプレ時は処理時間が短く済んでいたので、デフォルト値よりも短くしていました)。
※GCとして、G1GCではなくCMSを使っている場合も同様に「GCが追い付かなくなる」問題が出る可能性があります。

なお、速度改善のため、TomcatのコネクションプーリングをDBCP2からTomcat JDBC Connection Poolに変えてみましたが、一部のクエリの実行順序・タイミングに問題が見られたので、こちらも断念しました(調査・検証期間が足りず…)。

また、Auroraとは直接関係ありませんが、ALBでDraining時のリクエストが一部受け付けられずタイムアウトする問題が発生。
こちらはサポートから「現象は確認しており改善方法を検討しましたが困難なため、現時点では『仕様』ということになります」という主旨の回答を受けました。

2. データ移行時

AWSにはDMSもあるのですが、MySQLレプリケーションのほうが慣れているので、

  • ある定点のバックアップを初期データとしてmysqldump形式でS3バケットへ(バケットは暗号化しておきましょう)
  • ↑のS3バケットをEC2インスタンスからgoofysで読み取りマウントして、Auroraへリストア
  • 定点からの差分はMySQLレプリケーションを使って反映
  • 移行作業当日は、レプリケーションを切ってオンプレ側からAWS側にDNSのレコードを切り替える
  • 万一のために、オンプレ側に切り戻し用の環境を作っておく(オンプレ側マスタDBがMySQL 5.5のため、切り戻し用にはMySQL 5.6を用意)

という形での移行を進めました。

まず、オンプレMySQL⇒Auroraのレプリケーションを行うときに、「レプリケーション元とレプリケーション先で時間がずれる」問題が発生。

続いて、オンプレ側切り戻し環境へのレプリケーションを行う際に、「スレーブでマスタの指定ができない(エラーになる)」問題と「レプリケーションが止まる」問題が相次いで発生。

慣れていても、油断は禁物ですね。

特に前者は、そもそもオリジナルのDBの初期設定時の設定漏れが原因ですので…。

なお、切り戻し環境としてオンプレ環境をAuroraのスレーブにするときには、オンプレMySQL側に以下の記事の「3. rds_heartbeat2の作成」の作業も必要です。
これをしていなくても、Aurora⇒MySQL(RDS以外)のレプリケーションが止まります。

※その2に続きます。

続きを読む