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

0.はじめに

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

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

1.IAM Role へのポリシーのアタッチ

  1. 以下の IAM ポリシーを作成します。

    • ポリシー名 : GSCloudWatchWriteOnlyAccess ※ 任意
    • 説明 : ※ 任意
    • ポリシードキュメント :
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "Stmt1459146265000",
                "Effect": "Allow",
                "Action": [
                    "cloudwatch:PutMetricData"
                ],
                "Resource": [
                    "*"
                ]
            },
            {
                "Sid": "Stmt1459146665000",
                "Effect": "Allow",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ],
                "Resource": [
                    "arn:aws:logs:*:*:*"
                ]
            }
        ]
    }
    

  2. 作成したポリシーを EC2 インスタンスに割り当てられている IAM Role に付与します。

1.CloudWatch へのメトリクスデータの送信

  1. 色々調べたところ、collectd と その CloudWatch 用プラグインを利用するのが一般的みたいなので、今回はその手順で進めていきます。

  2. collectd をインストールします。

    $ sudo yum -y install collectd
    

  3. collectd の CloudWatch 用プラグインをインストールします。

    $ git clone https://github.com/awslabs/collectd-cloudwatch.git
    $ cd collectd-cloudwatch/src
    $ sudo ./setup.py
    
    Installing dependencies ... OK
    Installing python dependencies ... OK
    Downloading plugin ... OK
    Extracting plugin ... OK
    Moving to collectd plugins directory ... OK
    Copying CloudWatch plugin include file ... OK
    
    Choose AWS region for published metrics:
      1. Automatic [ap-northeast-1]
      2. Custom
    Enter choice [1]: 
    
    Choose hostname for published metrics:
      1. EC2 instance id [i-00484bb5ac67e244d]
      2. Custom
    Enter choice [1]: 
    
    Choose authentication method:
      1. IAM Role [testuekamawindowsserver]
      2. IAM User
    Enter choice [1]: 
    
    Enter proxy server name:
      1. None
      2. Custom
    Enter choice [1]: 
    
    Enter proxy server port:
      1. None
      2. Custom
    Enter choice [1]: 
    
    Include the Auto-Scaling Group name as a metric dimension:
      1. No
      2. Yes
    Enter choice [1]: 
    
    Include the FixedDimension as a metric dimension:
      1. No
      2. Yes
    Enter choice [1]: 
    
    Enable high resolution:
      1. Yes
      2. No
    Enter choice [2]: 
    
    Enter flush internal:
      1. Default 60s
      2. Custom
    Enter choice [1]: 
    
    Choose how to install CloudWatch plugin in collectd:
      1. Do not modify existing collectd configuration
      2. Add plugin to the existing configuration
      3. Use CloudWatch recommended configuration (4 metrics)
    Enter choice [3]: 
    Plugin configuration written successfully.
    Creating backup of the original configuration ... OK
    Replacing collectd configuration ... OK
    Replacing whitelist configuration ... OK
    Stopping collectd process ... NOT OK
    Starting collectd process ... NOT OK
    Installation cancelled due to an error.
    Executed command: '/usr/sbin/collectd'.
    Error output: 'Error: Reading the config file failed!
    Read the syslog for details.'.
    

  4. collectd の起動に失敗しています。collectd の python 用ライブラリが足りないみたいなので、インストールします。

    $ sudo yum -y install collectd-python
    

  5. collectd を起動します。

    $ sudo service collectd start
    

  6. collectd の自動起動の設定をします。

    $ sudo chkconfig collectd on
    $ sudo chkconfig --list | grep collectd
    
    collectd           0:off    1:off    2:on    3:on    4:on    5:on    6:off
    

  7. /etc/collectd.conf の設定を変更します。

    $ sudo cp -frp /etc/collectd.conf /etc/collectd.conf.ORG
    $ sudo vi /etc/collectd.conf
    
    • cpu :

      • LoadPlugin cpu をコメント解除し、以下の設定を行う。
      <Plugin cpu>
              ReportByCpu false
              ReportByState true
              ValuesPercentage true
      </Plugin>
      
    • df :

      • LoadPlugin df をコメント解除し、以下の設定を行う。
      <Plugin df>
      #       Device "/dev/hda1" 
      #       Device "192.168.0.2:/mnt/nfs" 
      #       MountPoint "/home" 
      #       FSType "ext3" 
      #       IgnoreSelected false
              ReportByDevice false
              ReportInodes false
              ValuesAbsolute true
              ValuesPercentage true
      </Plugin>
      
    • load :

      • LoadPlugin load をコメント解除し、以下の設定を行う。
      <Plugin load>
              ReportRelative true
      </Plugin>
      
    • memory :

      • LoadPlugin memory をコメント解除し、以下の設定を行う。
      <Plugin memory>
              ValuesAbsolute true
              ValuesPercentage true
      </Plugin>
      
    • swap :

      • LoadPlugin swap をコメント解除し、以下の設定を行う。
      <Plugin swap>
              ReportByDevice false
              ReportBytes false
              ValuesAbsolute false
              ValuesPercentage true
      </Plugin>
      

  8. /opt/collectd-plugins/cloudwatch/config/whitelist.conf の設定を変更します。以下のメトリクスの中で不要なものがあれば、適当に削除して下さい。

    $ cd /opt/collectd-plugins/cloudwatch/config/
    $ sudo cp -frp whitelist.conf whitelist.conf.ORG
    $ sudo vi whitelist.conf
    
    cpu-.*
    df-root-df_complex-free
    df-root-df_complex-reserved
    df-root-df_complex-used
    df-root-percent_bytes-free
    df-root-percent_bytes-reserved
    df-root-percent_bytes-used
    load--load-relative
    memory--percent-free
    memory--percent-used
    memory--memory-free
    memory--memory-used
    swap--percent-cached
    swap--percent-free
    swap--percent-used
    

  9. collectd を再起動します。

    $ sudo service collectd restart
    

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

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

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

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

2.CloudWatch Logs へのログデータの送信

  1. awslogs をインストールします。

    $ sudo yum -y install awslogs
    

  2. /etc/awslogs/awscli.conf の設定を変更します。

    $ sudo cp -frp /etc/awslogs/awscli.conf /etc/awslogs/awscli.conf.ORG
    $ sudo vi /etc/awslogs/awscli.conf
    
    [plugins]
    cwlogs = cwlogs
    [default]
    region = ap-northeast-1
    

  3. /etc/awslogs/awslogs.conf の設定を変更します。

    • apache や nginx の設定もしています。不要であれば、削除して下さい。
    $ sudo cp -frp /etc/awslogs/awslogs.conf /etc/awslogs/awslogs.conf.ORG
    $ sudo vi /etc/awslogs/awslogs.conf
    
    [/var/log/messages]
    datetime_format = %b %d %H:%M:%S
    file = /var/log/messages
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/messages
    
    [/var/log/cron]
    datetime_format = %b %d %H:%M:%S
    file = /var/log/cron
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/cron
    
    [/etc/httpd/logs/access_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/access_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/access_log
    
    [/etc/httpd/logs/error_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/error_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/error_log
    
    [/etc/httpd/logs/ssl_access_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_access_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_access_log
    
    [/etc/httpd/logs/ssl_error_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_error_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_error_log
    
    [/etc/httpd/logs/ssl_request_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_request_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_request_log
    
    [/var/log/nginx/access.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/access.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/access.log
    
    [/var/log/nginx/backend.access.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/access.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/backend.access.log
    
    [/var/log/nginx/badactor.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/badactor.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/badactor.log
    
    [/var/log/nginx/error.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/error.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/error.log
    

  4. awslogs を起動します。

    $ sudo service awslogs start
    

  5. awslogs の自動起動の設定をします。

    $ sudo chkconfig awslogs on
    $ sudo chkconfig --list | grep awslogs
    
    awslogs            0:off    1:off    2:on    3:on    4:on    5:on    6:off
    

99.ハマりポイント

  • 今回は、凡ミスばかりで本当に自分が嫌になりました…。もう、毎回、何やってんだ…。

  • CloudWatch へのメトリクスデータの送信では、 CloudWatch のカスタム名前空間の「collectd」ではなく、AWS の EC2 のフィルタに表示されると勘違いして、全然ログが出てこないと悩んでいました…。もう、本当馬鹿…。
  • 後、/etc/collectd.conf の設定も結構悩みました。
  • CloudWatch Logs へのログデータの送信では、/etc/awslogs/awscli.conf の設定を /etc/awslogs/awslogs.conf にすると勘違いして、本当に無駄な時間を浪費しました…。

XX.まとめ

以下、参考にさせて頂いたサイトです。
ありがとうございました。

続きを読む

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でガッチリログ解析

はじめに

現在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

続きを読む

AWS SDK for php でRoute53を操作する

やりたいこと

AWS Route53 を aws sdk を用いて php で操作したい。

前提条件

・管理ポリシー
 作成したユーザーの管理ポリシーは以下のようなポリシーをアタッチしておく
– AmazonRoute53DomainsFullAccess
– AmazonRoute53FullAccess

1. phpで AWS CLIのアクセスする準備

セットアップ手順はいくつかありますが、ここではComposerを利用した手順でサクッと準備します。

1-1. Composer のインストール

# curl -sS https://getcomposer.org/installer | php

1-2. 最新版のSDK導入

# php composer.phar require aws/aws-sdk-php

1-3. Composer’s autoloaderの読み込み

実際にphpでSDKを参照するにはautoload.phpを読み込む。

<?php
require ‘vendor/autoload.php’;

2. スクリプトの準備

以下の構成でスクリプトを準備します。

+config.php 設定ファイル(環境によってkey,secret等を設定する。)
+DNSUtil.php Route53制御用のクラス
+test.php テスト呼び出しファイル

2-1. config.php

<?php

class AWSConfig {

// AWS - credentials
  public static $Credential_Key    = "xxxxxxxxxxxxxxxxxxxx";
  public static $Credential_Secret = "****************************************";
  public static $Region = "ap-northeast-1";

// AWS - Route53
  public static $Route53_HostedZoneId = "...";

}

2-2. DNSUtil.php

<?php
/**
 * Route53 を制御してDNSの管理を行います。
 *
 */
require_once (dirname(__FILE__) . '/config.php');
require '/<path to aws-sdk>/vendor/autoload.php';
use AwsRoute53Route53Client;

class DNSUtil {
   /**
    * コンストラクタ
    *
    */
    function __construct() {
        $this->client = Route53Client::factory(array(
            'version'     => 'latest',
            'credentials' => array(
                'key'     => AWSConfig::$Credential_Key,
                'secret'  => AWSConfig::$Credential_Secret,
            ),
            'region' => AWSConfig::$Region,
        ));
    }

   /**
    * レコード一覧取得
    * ここではレコードタイプを指定して一覧取得しています。
    *
    */
    public function getList($type="A") {
        $ret = array();
        try {
            // http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_listResourceRecordSets
            $res = $this->client->listResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
            ));
            foreach ($res['ResourceRecordSets'] as $record) {
                $res_type = $record['Type'];
                if ($res_type != $type) {
                    continue;
                }
                $name = $record['Name'];
                $val  = $record['ResourceRecords'][0]['Value'];
                $ret[$name] = $val;
            }
        } catch (Exception $e) {
            // http://docs.aws.amazon.com/aws-sdk-php/v2/api/namespace-Aws.Route53.Exception.html
            echo $e->getMessage();
        }
        return $ret;
    }

   // Aレコードの更新 なければ作成、あれば更新
    public function update_ARecord($domain, $address) {
        // http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_changeResourceRecordSets
        $arrays = array(
            'Action' => 'UPSERT',  // string: CREATE | DELETE | UPSERT
            'ResourceRecordSet' => array(
                'Name' => $domain,
                'Type' => 'A',
                'TTL'  => 86400,
                'ResourceRecords' => array(
                    array(
                        'Value' => $address,
                    ),
                ),
            ),
        );
        try {
            $this->client->changeResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
                'ChangeBatch'  => array(
                    'Comment' => 'from my PHP script',
                    'Changes' => array($arrays),
                ),
            ));
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

    // Aレコード削除
    public function delete_ARecord($domain, $address) {
        $arrays = array(
            'Action' => 'DELETE',  // string: CREATE | DELETE | UPSERT
            'ResourceRecordSet' => array(
                'Name' => $domain,
                'Type' => 'A',
                'TTL'  => 86400,
                'ResourceRecords' => array(
                    array(
                        'Value' => $address,
                    ),
                ),
            ),
        );
        try {
            $this->client->changeResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
                'ChangeBatch'  => array(
                    'Comment' => 'from my PHP script',
                    'Changes' => array($arrays),
                ),
            ));
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

}

Route53Clientのインスタンスを生成します。
レコードの更新APIでは、レコードの作成、削除、更新が「CREATE」「DELETE」「UPSERT」
で指定できます。

2-3. test.php

<?php
require_once (dirname(__FILE__) . '/DNSUtil.php');

$obj = new DNSUtil();
// Aレコードの更新・設定
$obj->update_ARecord('hoge1.test.net', '11.22.33.44');
//$obj->delete_ARecord('hoge1.test.net', '11.22.33.44');

// Aレコードの一覧取得
$list = $obj->getList("A");
foreach ($list as $domain => $address) {
    print("domain : $domain -> address:$address n");
}

動作確認

以下のように呼び出してエラーなく、情報が更新、取得できることを確認できる。

# php test.php

参考文献

AWS SDK for PHP ; Installing using Composer
http://docs.aws.amazon.com/aws-sdk-php/v2/guide/installation.html#installing-using-composer

listResourceRecordSets:一覧取得
http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_listResourceRecordSets

changeResourceRecordSets:レコード値の更新
http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_changeResourceRecordSets

続きを読む

[AWS]EC2初期設定でとりあえず設定したほうがよいもの

AWSのEC2を利用するときに基本として設定するものをまとめる。
WebサーバでもBatchサーバでも以下をとりあえず以下を設定しておくとよいのではないでしょうか。

[設定する内容]

  • マシン起動時スクリプト(per-boot)  

    • SWAPファイル設定
    • HOST名設定
  • ファイルディスクプリタ設定、スレッド設定
    • limit.conf
    • sysctl.conf
[マシン起動時スクリプト(per-boot)]

SWAPファイル設定を起動時に実施。EC2はデフォルトでSWAP設定がない。
(ほぼ参照:http://dev.classmethod.jp/cloud/ec2linux-swap-bestpractice/)

create_swap.sh
#!/bin/sh

SWAPFILENAME=/swap.img
MEMSIZE=`cat /proc/meminfo | grep MemTotal | awk '{print $2}'`
MEMSIZE_D=`expr $MEMSIZE * 2`
MEMSIZE_H=`expr $MEMSIZE / 2`

if [ $MEMSIZE -lt 2097152 ]; then
  SIZE=${MEMSIZE_D}k
elif [ $MEMSIZE -lt 8388608 ]; then
  SIZE=${MEMSIZE}k
elif [ $MEMSIZE -lt 67108864 ]; then
  SIZE=${MEMSIZE_H}k
else
  SIZE=4194304k
fi
fallocate -l $SIZE $SWAPFILENAME && chmod 600 $SWAPFILENAME && mkswap $SWAPFILENAME && swapon $SWAPFILENAME

HOST名設定を起動時に実施 EC2のタグ:NameがそのままHost名になるため、SSHログイン後の判別が簡単。

set_hostname.sh
#!/bin/sh

REGION=`/usr/bin/curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/ | sed 's/.$//'`

INSTANCE_ID=`/usr/bin/curl -s http://169.254.169.254/latest/meta-data/instance-id`

NAME_TAG=`/usr/bin/aws ec2 describe-instances --region ${REGION} --instance-ids ${INSTANCE_ID=} --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value' --output text`

INSTANCE_NAME="${NAME_TAG}"

hostname ${INSTANCE_NAME}
sed -i -e "s/^HOSTNAME=.*$/HOSTNAME=${INSTANCE_NAME}/g" /etc/sysconfig/network
[ファイルディスクプリタ設定、スレッド設定]

FD設定。大体やり忘れてアプリケーション実行時に足りなくなる。
また、スレッド数も同じことになるので、kernel.threads-maxで設定しておく。

/etc/security/limits.conf
#以下を末尾に追加
* soft nofile 65536
* hard nofile 65536
/etc/sysctl.conf

fs.file-max = 100000
kernel.threads-max = 100000

続きを読む

Webアプリのスマホテストを自動化する

Webブラウザアプリケーションのスマホテストを外部サービスを使い自動化します。
AWS Device FarmRemote TestKit等のサービスが対象になりますが、今回はAWS Device Farmを利用します。
なお、CIツールはプラグインが存在するJenkinsを用います。

必要なもの

Jenkinsのインストール

terraformを用いてEC2上に、Jenkinsをセットアップします。
スクリプトでは、ユーザデータを用いて、Jenkinsのセットアップを実施しています。

参考レポジトリ

bash
$ git clone https://github.com/Thirosue/devicefarm-sample.git
$ cd devicefarm-sample/provisioning/
$ terraform apply -var-file=~/.aws/terraform.tfvars -var 'key_name=[keypair]'
ec2_jenkins.tf
variable "key_name" {}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
}

resource "aws_instance" "jenkins" {
  ami           = "${data.aws_ami.amazon_linux.id}"
  instance_type = "t2.micro"
  key_name      = "${var.key_name}"
  user_data = <<EOF
IyEvYmluL2Jhc2gKCndnZXQgLU8gL2V0Yy95dW0ucmVwb3MuZC9qZW5raW5zLnJl
cG8gaHR0cDovL3BrZy5qZW5raW5zLWNpLm9yZy9yZWRoYXQvamVua2lucy5yZXBv
CnJwbSAtLWltcG9ydCBodHRwOi8vcGtnLmplbmtpbnMtY2kub3JnL3JlZGhhdC9q
ZW5raW5zLWNpLm9yZy5rZXkKCnl1bSBpbnN0YWxsIC15IGdpdCBqZW5raW5zIGph
dmEtMS44LjAtb3BlbmpkawphbHRlcm5hdGl2ZXMgLS1zZXQgamF2YSAvdXNyL2xp
Yi9qdm0vanJlLTEuOC4wLW9wZW5qZGsueDg2XzY0L2Jpbi9qYXZhCgpjaGtjb25m
aWcgamVua2lucyBvbgovZXRjL2luaXQuZC9qZW5raW5zIHN0YXJ0CgpleGl0IDA=
EOF
}
userdata.sh
#!/bin/bash

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key

yum install -y git jenkins java-1.8.0-openjdk
alternatives --set java /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

chkconfig jenkins on
/etc/init.d/jenkins start

exit 0

Jenkinsの管理コンソールセットアップ

EC2のPublicIPを確認し、以下にアクセス

http://[IPv4 Public IP]:8080

アクセス後、Jenkinsがロックされているので、指示通り/var/lib/jenkins/secrets/initialAdminPasswordを確認し入力します。

Unlock Jenkins

Device Farm Plugin インストール

Jenkins-プラグインマネージャよりaws-device-farm-pluginをインストール。

DeviceFarmPlugin

DeviceFarm AccessKey/SecretKey設定

Jenkinsの管理画面に用意したIAMユーザのAccessKey/SecretKeyを設定

DeviceFarmIAMSetting

テストコード作成

以下を参考にテストコードを作成する。
参考レポジトリ はAppium+JUnitをgradleでbuildしている。

SampleTest.java
import com.codeborne.selenide.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Platform;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SampleTest {

    private RemoteWebDriver driver;

    @Before
    public void setUp() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setPlatform(Platform.IOS);
        capabilities.setBrowserName("safari");
        driver = new RemoteWebDriver(new URL("http://127.0.0.1:4723/wd/hub"),
                capabilities);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
    }

    @After
    public void tearDown() throws Exception {
        driver.quit();
    }

    public boolean takeScreenshot(final String name) {
        String screenshotDirectory = System.getProperty("appium.screenshots.dir", System.getProperty("java.io.tmpdir", ""));
        System.out.println(screenshotDirectory);
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        return screenshot.renameTo(new File(screenshotDirectory, String.format("%s.png", name)));
    }

    @Test
    public void runTest() throws Exception {
        driver.get("https://www.google.co.jp/");
        Thread.sleep(1000);

        System.out.println(driver.getTitle());
        System.out.println(driver.getPageSource());

        driver.findElement(By.id("lst-ib")).sendKeys("AWS DeviceFarm");
        driver.findElement(By.id("tsbb")).click();
        assertTrue(takeScreenshot("index"));

        assertEquals("AWS DeviceFarm", driver.findElement(By.id("lst-ib")).getAttribute("value"));
        assertTrue(takeScreenshot("result"));
    }
}

build設定

参考レポジトリ はカスタムタスクのinstallZipでDeviceFarmへのuploadファイルを生成する。

スクリーンショット 2017-09-14 時刻 20.28.51.png

task installZip(dependsOn: ["clean", "packageTests", "installDist"]) << {
    new File("build/work").mkdir()
    new File('build/zip.sh') << 'cd build/work; zip -r zip-with-dependencies.zip .'
    copy{
        from "build/install/test/lib/"
        into "build/work/dependency-jars/"
    }
    copy{
        from "build/libs/test-1.0-SNAPSHOT-tests.jar"
        into "build/work/"
    }
    "chmod 755 build/zip.sh".execute().waitFor()
    "build/zip.sh".execute().waitFor()
    copy{
        from "build/work/zip-with-dependencies.zip"
        into "build/"
    }
}

DeviceFarmテスト設定

作成済みのDeviceFarm Project及びDeviceFarm Device Poolを選択して、buildで固めたzipファイルを指定する。

スクリーンショット 2017-09-14 時刻 20.34.19.png

テスト実行

Jenkinsでテスト実行後、DeviceFarmマネジメントコンソールへのレポートリンク等がJenkinsのテスト結果画面に表示されます。

スクリーンショット 2017-09-14 時刻 20.39.01.png

その他(開発中のテストについて)

54.244.50.32~54.244.50.63. の IP範囲をホワイトリストに登録 すれば、開発中資源もテストできそうです。

終わりに

利用事例が少なそうですが、かなり使えそうなサービスなので、今後積極的に利用していきたいと思います。

続きを読む

boto3(AWS SDK for python)を使って、ローカルファイルをS3にアップロードする方法

はじめに

boto2を使って、ローカルストレージにあるファイルをアップロードするQiita記事はよく見かけるが、
boto3を使ったものがなかなか見つからなかったので、公式サイトを参考に実装したものを貼っておく。

開発環境

Python 3.6.1
pip 9.0.1

手順

  • パッケージインストール
  • Configファイル設定
  • 実装 (pythonソースコード)

パッケージインストール

boto3(AWS SDK for python)

$ pip install boto3

コマンドラインよりAWSのサービスを操作するためのパッケージも併せてインストールしておく。

$ pip install awscli

Configファイル生成

アクセスキー、シークレットキー、リージョン等をConfigファイルに書き込む。
下記コマンドを実行し、後は対話型にデータを入力していけばホームディレクトリ配下にファイルが生成される。

※ boto2では、ソース上よりAWSアクセスキー、シークレットキーを読み込んでいたが、
boto3では、Configファイルから上記2つキーを取得する。

$ aws configure
AWS Access Key ID [None]: xxxxxxxxxxxxxxxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxxxxxxxxxxx
Default region name [None]: xxxxxxxxxxxxxxxxxxxxx
Default output format [None]: xxxxxxxxxxxxxxxxxxx

生成されるファイル(2つ)

~/.aws/credentials
----------------------------------------------
[default]
aws_access_key_id = ACCESS_KEY_ID
aws_secret_access_key = SECRET_ACCESS_KEY
----------------------------------------------
~/.aws/config
----------------------------------------------
[default]
region = [xxxxxxxxxxxxxxxxx]
output = [xxxxxxxxxxxxxxxxx]
----------------------------------------------

ホームディレクトリに生成され、boto3実行時もホームディレクトリの上記ファイルを読みに行っているので、
.awsのディレクトリを移動させるとエラーが発生するので注意。

botocore.exceptions.NoCredentialsError: Unable to locate credentials

もし移動させると、上記のようなエラーが発生する。
まぁ、普通の人は移動させないんだろうけど。。。

実装 (Pythonソースコード)


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

import sys
import threading

import boto3

# boto3 (.aws/config)
BUCKET_NAME = 'YOUR_BUCKET_NAME'

class ProgressCheck(object):
    def __init__(self, filename):
        self._filename = filename
        self._size = int(os.path.getsize(filename))
        self._seen_so_far = 0
        self._lock = threading.Lock()
    def __call__(self, bytes_amount):
        with self._lock:
            self._seen_so_far += bytes_amount
            percentage = (self._seen_so_far / self._size) * 100
            sys.stdout.write(
                    "r%s / %s (%.2f%%)" % (
                        self._seen_so_far, self._size,
                        percentage))
            sys.stdout.flush()


def UploadToS3():
    # S3Connection
    s3 = boto3.resource('s3')
    s3.Object(BUCKET_NAME, 'OBJECT_KEY (S3)').upload_file('UPLOAD_FILE_PATH (lOCAL)')

UploadToS3()  

OBJECT_KEY (S3) : S3でのオブジェクトキーを設定する。
UPLOAD_FILE_PATH (lOCAL) : アップロードするローカルファイルのパスを設定する。

終わりに

気が向いたら、boto2でのアップロード方法との比較も書きたいと思います。
最後までお読みいただき、ありがとうございました。

何か誤り、アップデート事項ございましたらご指摘お願いいたします。

続きを読む