前回の記事で予告したように、今回は複数のEC2で採取したログを統合して確認できるようにする方法に関して書きたいと思います。
例えば、2台のEC2(1,2号機)で負荷分散を行なっている状態で一連の処理として4つのアクション(A,B,C,D)を実行したとします。
この時、それぞれのアクションがどちらのEC2で実行されるかはALB次第なので、A,Cは1号機、B,Dは2号機で実行されるというようなことが発生します。その際のログとしては1号機でA,Cに関するもの、2号機でB,Dに関するものが残されますが、確認の度に複数台のEC2でログを確認するのは面倒ですし、タイミングや順序が問題となるような挙動に関してはA,B,C,D全てのログがまとめて時系列で並んでいる状態で確認した方が確実です。
よって、複数EC2で採取したログを統合的に確認できる方法が重要になる訳です。
AWSではこのようなログの統合環境として「CloudWatch Logs」が用意されています。これを使用してLaravelで採取したログを確認できるようにしたいと思います。
IAMロール(ポリシー)設定
まずはEC2からCloudWatch Logsに情報を出力できるように権限設定しておきます。
上記操作に関連するポリシーとしては「CloudWatchLogsFullAccess」なるものが標準的に用意されていますのでこれを使用しますが、過去記事にあるようにS3へのアクセスなどもできるようにしていた場合、既にIAMロール自体はEC2に設定されていると思いますので、そのIAMロールに前述のポリシーを追加することになります。
CloudWatch Logs エージェントのインストール
CloudWatch Logsに対する情報送信はEC2上のCloudWatch Logs エージェント機能(以下、単に「エージェント」と表現します)が行います。
よって、本機能をインストールする必要があります。
EC2にログインし、/tmp辺りで以下のコマンドを実行します。実行ユーザーはrootです。
curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O
公式サイトでは上記完了後に以下のコマンドを実行するように書かれています。
python ./awslogs-agent-setup.py --region <リージョン>
しかし、AMIとして用意されている「Ubuntu20.04」から構築したEC2環境では「python」という名称は認識されていませんでした。
公式サイトによれば上記がダメなら改めて以下を実行するように書かれています。
python3 ./awslogs-agent-setup.py --region <リージョン>
こちらに関しては実行可能なのですが、残念ながらエラーになります。
ERROR: This script only supports python version 2.6 - 3.5
バージョンが合っていないようなので確認してみます。
python3 --version
結果は「Python 3.8.10」でした。EC2にインストールされているコマンドが新し過ぎてエラーになっているようです。ここはAWSにもう少し頑張ってもらいたいところ。
python2もインストールされているようなので、こちらのバージョンも確認してみると、こちらは「Python 2.7.18」とのことで使えそうです。
と言うことで、以下のように実行します(リージョンはEC2と同じく「東京」を指定します)。
python2 ./awslogs-agent-setup.py --region ap-northeast-1
以降、色々聞かれますので適宜入力して行きます。
なお、下記入力結果は最終的には「/var/awslogs/etc/awslogs.conf」なるファイルに反映されますが、その内容は後から更新可能なので、まずはサクサク進めてしまうのが良いです。
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
上記に関してはロール(ポリシー)で解決するので入力不要です(単にリターンキー押下)。
Default region name [ap-northeast-1]:
リージョンを聞かれます。コマンド実行時の引数で指定しているのに再度入力を求められるのは謎ですが、とりあえずデフォルトとして表示されている「東京」リージョンで良いので黙ってリターンキー押下。
Default output format [None]:
良く分かりませんが、入力しなくても良さそうなので黙ってリターンキー押下。
Path of log file to upload [/var/log/syslog]:
集計したいログファイルを指定するようです。Laravelの場合は「storage/logs」配下に採取されるので、それらが対象になるように指定します。
例えば「/home/user/laravel/storage/logs/*」のようにワイルドカードが使えます。
Destination Log Group name […]:
CloudWatch Logs側では採取したログを「ロググループ」と言う単位でまとめて管理するようになっていますので、分かりやすい名前を付けます。
Choose Log Stream name:
1. Use EC2 instance id.
2. Use hostname.
3. Custom.
Enter choice [1]:
ロググループ内で複数のEC2のいずれから採取されたものかを識別するための名前に何を使用するかを指定します。
とりあえずデフォルト(1)のままにしておきます。
Choose Log Event timestamp format:
1. %b %d %H:%M:%S (Dec 31 23:59:59)
2. %d/%b/%Y:%H:%M:%S (10/Oct/2000:13:55:36)
3. %Y-%m-%d %H:%M:%S (2008-09-08 11:52:54)
4. Custom
Enter choice [1]:
各レコード(行)に割り振られるタイムスタンプの書式を指定します。日本人であれば3の形式が一番馴染みやすいかと思います。
ただ、これはあくまでCloudWatch Logsとして割り振るタイムスタンプです。Laravelとしてのログ採取時に既にタイムスタンプを付加しているので実際にCloudWatch Logsで見た際には1行にタイムスタンプが2つ並んでいる状態になります。しかも、ログ採取してからCloudWatch Logsとして同情報を記録するまで若干のタイムラグがあるため、2つのタイムスタンプには差異があります。
CloudWatch Logsとしてのタイムスタンプは後述するレコードの絞り込み時に必要ですが、あくまで絞り込み条件として必要なだけで、特に表示したい動機はなく、その意味ではここで指定する書式はあまり重要ではありません。
という状況ですが、とりあえず3を選択しておきます。
Choose initial position of upload:
1. From start of file.
2. From end of file.
Enter choice [1]:
既存のファイルに関して、ファイルの最初からCloudWatch Logに送るか、今後追加された分から送るか、と言ったところでしょうか。
とりあえずデフォルト(1)のままにしておきます。
More log files to configure? [Y]:
別のログファイルに関して設定を続けるかを聞かれます。今回はLaravelのログを統合できれば良いので「N」を入力します。
これでエージェント機能がインストールされつつ、設定ファイル「/var/awslogs/etc/awslogs.conf」が作成されます。
エージェント機能の起動・停止等に関しては他のデーモンと同様に「service」コマンドで行います。
service awslogs start|stop|status|restart
Logs Insights
上記までの設定でCloudWatch Logsの所定のロググループに各EC2で採取したログが収集されるようになりますが、あくまで各EC2のログが個別に(つまりは複数)採取されているだけです。
CloudWatch Logsではロググループに属する複数のログから特定の条件に基づいて横断的に情報を収集・表示するための「Logs Insights」という機能が提供されています。
本機能はCloudWatchのコンパネでロググループを選択した画面上の「Logs Insightsで表示」をクリックして表示される画面上から利用できます。
同画面では情報を収集する条件を独自の書式で示した「クエリ」として設定できるようになっており、「クエリを実行」をクリックすることで当該クエリの実行結果が一覧で表示されるようになっています。
クエリの具体例は以下の通り。イメージとしてはSQLに近いですね。
fields @logStream, @message
| sort @message
| limit 10000
まずはどの情報を取得するかということを「fields」で指定します。「@message」が収集したログ情報自体を示しており、「@logStream」は当該レコードを収集したEC2のインスタンスを特定する情報(エージェントのインストール時に「Choose Log Stream name」として指定した情報)を意味します。
「sort」ではレコードの表示順を指定します。上記例ではログ情報自体がソート条件になっていますが、ログ上の各レコードの先頭に同情報を採取したタイムスタンプが設定されていれば、実質的に同タイムスタンプがソート条件になるので、これにより複数のEC2から採取したログを時系列にマージ&ソートして確認できるようになる訳です。
なお、その意味ではタイムスタンプの精度が重要になりますので、Laravelのログ採取機能を操作し、タイムスタンプの書式を変更しておくと良いです。その具体的な方法はGoogle先生に聞いてもらうとして、タイムスタンプの書式において「Y/m/d H:i:s.u」のように「s」(秒)の次に「u」(マイクロ秒)を指定しておくことでレコードに付加されるタイムスタンプがマイクロ秒の精度になります。
なお、根本的にEC2の時計の精度(異なるEC2の時刻情報の差分がどの程度の範囲内か)にも依存するため、あまり時刻情報にずれがあると異なるEC2で採取したログを正しく並べられないという問題も生じます。この点については、Ubuntuでは「systemd-timesyncd」なるもので時刻情報を適正に保つようになっているようなので、とりあえずは同機能を信じたいと思います。
最後に「limit」で表示するレコード数を指定します。
なお、Logs Insightsではデフォルトで直近1時間分しか検索対象になっていません。もし過去の特定の期間を対象としたければ、画面右上の「Custom」から対象期間を指定する必要があります。
特に「Absolute」を選択するとカレンダーから簡易に日付を選択できるインタフェースが提供されており、さらに細かく期間の開始/終了の時分秒が編集できるようになっているので、対象期間を細かく指定できます。
上記操作により、対象期間に関して複数EC2から採取したログ情報をマージ&時系列にソートした形で確認できるようになります。
総括
エージェントのインストール辺りが若干ごちゃごちゃしていますが、全般的に大きな問題もなく、ログの統合ができるようになりました。
今回は触れませんでしたが、Logs Insightsでは対象レコードの絞り込み条件を指定する「filter」句なども指定できます(SQLで言えば「where」のようなものかと)。この辺が使いこなせればログの調査もかなり楽になりそうです。
懸案事項としてはログの保管期間をどの程度にするかという問題があります。保管期間はロググループ単位に指定できますが、デフォルトは「失効しない」になっています。当然ながらこのままではログが蓄積され続けるので上限を設けるべきですが、できるだけログは残しておきたいので、蓄積のペースを見ながら随時判断して行きたいと思います。