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.まとめ

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

続きを読む

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添付!

続きを読む

【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

以上です.

続きを読む

OpsWorksでCloudWatch Logs Agentのインストールにコケる

結論

python-dev のパッケージを自分で指定して入れないと動かないとのこと。

現象

今年くらいにOpsWorksの画面から、自前でエージェントをインストールしなくても直接CloudWatch Logsを利用出来るようになった。だが、いざやってみるとSetupがFailureになる。

image.png

root@app1:/var/log# /opt/aws/cloudwatch/awslogs-agent-setup.py -n -r 'ap-northeast-1' -c '/opt/aws/cloudwatch/cwlogs.cfg'

Step 1 of 5: Installing pip ...DONE

Step 2 of 5: Downloading the latest CloudWatch Logs agent bits ... Traceback (most recent call last):
  File "/opt/aws/cloudwatch/awslogs-agent-setup.py", line 1272, in <module>
    main()
  File "/opt/aws/cloudwatch/awslogs-agent-setup.py", line 1268, in main
After this operation, 35.1 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main libexpat1-dev amd64 2.1.0-4ubuntu1.4 [115 kB]
Get:2 http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main libpython2.7-dev amd64 2.7.6-8ubuntu0.3 [22.0 MB]
Get:3 http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main libpython-dev amd64 2.7.5-5ubuntu3 [7,078 B]
Get:4 http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main python2.7-dev amd64 2.7.6-8ubuntu0.3 [269 kB]
Get:5 http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main python-dev amd64 2.7.5-5ubuntu3 [1,166 B]
Fetched 22.4 MB in 0s (40.3 MB/s)
Selecting previously unselected package libexpat1-dev:amd64.
(Reading database ... 97703 files and directories currently installed.)
Preparing to unpack .../libexpat1-dev_2.1.0-4ubuntu1.4_amd64.deb ...
Unpacking libexpat1-dev:amd64 (2.1.0-4ubuntu1.4) ...
Selecting previously unselected package libpython2.7-dev:amd64.
Preparing to unpack .../libpython2.7-dev_2.7.6-8ubuntu0.3_amd64.deb ...
Unpacking libpython2.7-dev:amd64 (2.7.6-8ubuntu0.3) ...
Selecting previously unselected package libpython-dev:amd64.
Preparing to unpack .../libpython-dev_2.7.5-5ubuntu3_amd64.deb ...
Unpacking libpython-dev:amd64 (2.7.5-5ubuntu3) ...
Selecting previously unselected package python2.7-dev.
Preparing to unpack .../python2.7-dev_2.7.6-8ubuntu0.3_amd64.deb ...
Unpacking python2.7-dev (2.7.6-8ubuntu0.3) ...

ドキュメント

よくドキュメントを読んでみると、 python-dev が必須とのこと。

If the installation of the agent fails, check to make sure that the python-dev package is installed. If it isn’t, use the following command, and then retry the agent installation:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/QuickStartChef.html

pythonのプロジェクトではない人には、デフォルトでfailさせるのはどうなんですかね。文句いうならpythonを使えという感じですかね 😨

image.png

LayerのOS Packagesに追加して、無事に online になりました。完。

続きを読む

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範囲をホワイトリストに登録 すれば、開発中資源もテストできそうです。

終わりに

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

続きを読む