[Ansible]CloudWatch Logs で Play Framework のログを見てみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

CloudWatch Logs で Play Framework のログを出力してみるまでの手順を ansible playbook と rake タスクとしてまとめてみました。今回はその事前設定と、タスクと ansible role の説明、及び play framework の logback の設定方法をざっくりと解説していきます。

今回のサンプルコードは以下に上がっています。

事前設定

今回のサンプルコードはEC2上にデプロイされることを想定したものですが、そのEC2に対してはCloudWatch Logsに対するアクセス権限が必要となります。CloudWatch Logs へのアクセス権限を持った IAM Role を作成し、EC2インスタンス作成時に付与してください。そのための資料としては以下の記事をご覧いただければとおもいます。

クイックスタート: 既存の EC2 インスタンスに CloudWatch Logs エージェントをインストールして設定する

デプロイコード

ansible playbook の role群とそれをラップした rake タスクのリポジトリは以下の様なディレクトリ構成になっています。

.
├── Gemfile
├── Rakefile
├── ansible
│   ├── inventory
│   │   └── hosts
│   ├── roles
│   │   ├── awslogs
│   │   ├── common
│   │   ├── iptables
│   │   ├── java
│   │   ├── play
│   │   └── yum
│   └── site.yml
└── tasks
    ├── play.rake
    └── provision.rake

事前に

$ bundle install --path vendor/bundle

した上で ansible を適宜インストールしてください。

rake タスク

アプリのデプロイに必要な rake のタスクは以下のようになっています。

$ bundle exec rake -vT
rake play:build     # build for release
rake play:checkout  # checkout from repository
rake play:prepare   # prepare for build
rake provision      # provision servers

rake play:build

このタスクはplay:checkoutとplay:prepareの双方をふくんでいます。やっていることは単純で先ほどの logback に必要な変更を施した play framework アプリを clone してきて本番用のビルドを行うというものになっています。

ビルド成果物のはのちの provision タスクで EC2 にアップロードされます。ビルドをEC2上で行うよりもローカルマシンで行うほうが早い場合にこの方法は有効です。

rake provision

このタスクは ansible playbook をラップしたものです。SSH_KEY_PATH に EC2 の sshキーを指定することで ansible 内で指定された host に対してプロビジョニングを実行します。

ansible

ansible/roles 配下ではプロビジョニングに必要な各roleが切り出されています。以下のsite.ymlで定義された各roleが実行されます。

site.yml

---
- hosts: '{{ hosts }}'
  sudo: yes
  roles:
    - { role: yum }
    - { role: common }
    - { role: iptables }
    - { role: java }
    - { role: awslogs }
    - { role: play, app_name: 'cloudwatchlogsample' }

各Roleの簡単な概要です。

  • yum : yum パッケージを最新にし、epelレポジトリをインストールします。
  • common : タイムゾーンに関する設定を行います。
  • iptables : HTTP80ポートのパケットをplay frameworkのデフォルトポート9000に転送する設定を行います。
  • java : java8をインストールします
  • awslogs : CloudWatch Logs にログ転送を行うエージェントをインストールし、必要な設定を行います。後で詳しく見ていきます。
  • play : 先ほどのビルド成果物を転送し、init.d配下にサービスとして登録します。サービス登録後に起動を行います。

このプロビジョニングによって CloudWatch Logs にログを流し込むデーモンが EC2 上に立ち上がり、サービスとして登録された play framework の実行スクリプトが走り、ワンライナーでアプリとログ取得に必要な環境設定が全て構築されることになります。

awslogs role

awslogs のロールについて見ていきます。

awslogs/tasks/main.yml

- name: install awslogs
  yum: name=awslogs state=latest disable_gpg_check=yes

- name: copy conf files
  template: src={{ item }}.j2 owner=root dest=/etc/awslogs/{{ item }}
  with_items:
    - awscli.conf
    - awslogs.conf

- name: create directory
  file: path=/home/ec2-user/awslogs/state owner=ec2-user state=directory mode=0775

- name: create state log file
  file: path=/home/ec2-user/awslogs/state/agent-state owner=ec2-user state=touch mode=0775

- name: start agent
  service: name=awslogs state=started
  • install awslogs : yum で CloudWatch Logs のエージェント awslogs パッケージをインストールします。
  • copy conf files : awslogs の起動に必要な設定ファイルを必要な箇所に上書きします。srcに示されているawscli.conf.j2, awslogs.conf.j2ファイルは以下のとおりです。

awslogs/templates/awscli.conf.j2

[plugins]
cwlogs = cwlogs
[default]
region = ap-northeast-1

regionではCloudWatchLogsのリージョンを指定します。

awslogs/templates/awslogs.conf.j2

#
# ... 省略
#
[general]
# Path to the CloudWatch Logs agent's state file. The agent uses this file to maintain
# client side state across its executions.
state_file = /home/ec2-user/awslogs/state/agent-state
#
# ... 省略
#

[home/ec2-user/logs/]
log_group_name = playsample
log_stream_name = stream-sample-log
file = /home/ec2-user/logs/application.log
datetime_format = %Y-%m-%d %H:%M:%S

log_group_nameでロググループ名、log_stream_nameでログストリーム名、fileで監視対象ログファイルを記述し、datetime_format でログのフォーマットを指定します。

  • create directory, create state log file : awslogs エージェントのstateを記録するためのログファイルを作成します。
  • start agent : awslogs サービスを開始します。

アプリコード

logback.xmlのカスタマイズを先ほどのawslogsの設定ファイルと適合する形で行います。

conf/logback.xml

<configuration>

  <conversionRule conversionWord="coloredLevel" converterClass="play.api.Logger$ColoredLevel" />

  <appender name="FILE_ERROR" class="ch.qos.logback.core.FileAppender">
    <file>/home/ec2-user/logs/error.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
      <fileNamePattern>/home/ec2-user/logs/error%i.log</fileNamePattern>
      <minIndex>1</minIndex>
      <maxIndex>20</maxIndex>
    </rollingPolicy>
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>50000</maxFileSize>
    </triggeringPolicy>
    <encoder>
      <pattern>[%date{yyyy-MM-dd HH:mm:ss}] [%level] from %logger in %thread - %replace(%message){`\n`, ` `} %ex %n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
  </appender>

  <appender name="FILE_INFO" class="ch.qos.logback.core.FileAppender">
    <file>/home/ec2-user/logs/application.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
      <fileNamePattern>/home/ec2-user/logs/application%i.log</fileNamePattern>
      <minIndex>1</minIndex>
      <maxIndex>20</maxIndex>
    </rollingPolicy>
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>50000</maxFileSize>
    </triggeringPolicy>
    <encoder>
      <pattern>[%date{yyyy-MM-dd HH:mm:ss}] [%level] from %logger in %thread - %replace(%message){`\n`, ` `} %n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
  </appender>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%coloredLevel - %logger - %message %exception</pattern>
    </encoder>
  </appender>

  <!--
    The logger name is typically the Java/Scala package name.
    This configures the log level to log at for a package and its children packages.
  -->
  <logger name="play" level="INFO" />
  <logger name="application" level="DEBUG" />

  <root level="WARN">
    <appender-ref ref="FILE_ERROR" />
    <appender-ref ref="FILE_INFO" />
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

logback.xml作成時にはlogbackでレベルごとに出力するファイルを分けるを参考にしました。

ログレベルに応じて追記するログファイルを振り分け、rollingPolicyとtriggeringPolicyでログローテーションの設定をしています。ログローテーションを実行している場合でも CloudWatch Logs は以下の条件であればサポートされます。

  1. ファイルがリネームされ、同名のファイルが新規作成される
  2. ファイルがコピーされ、中身がトランケートされる
  3. ファイルが同じような条件で新規作成される

(参考サイト: CloudWatch Logs Agent の挙動について調べたことのまとめ)

デプロイと確認

必要な前設定をした上でビルドスクリプトを実行

$ bundle exec rake play:build

IAM Roleの設定されたEC2を立ち上げた上でIPを ansible/inventory/hosts の下に追記して

ansible/inventory/hosts

[web]
xxx.xxx.xxx.xxx

sshキーを指定してデプロイスクリプトを実行します。

$ bundle exec rake provision SSH_KEY_PATH=YOUR_SSH_KEY_PATH

デプロイに成功したらコンソールをひらき、ログが追記されていることを確認します。

console-1

作成時刻等のカラムの表示設定は右上の歯車ボタンで行えます。

console-2

デプロイとログ取得をワンライナーで行えると気持ちいいですね!

参考サイト