EC2 から CloudWatch にログを送る

先日、Deno のコミュニティでログ関連のモジュールのファイル出力に関して質問していた。
質問自体は解決したが、今時はリモートにログ溜める方が一般的かもという話があって、確かにそうだなと感じた。

が、今作ってるものはEC2で動作させてお安く運用したいというのもあり、Fargate使うときみたいにあまり気にせず出力をCloudWatchに吐いてくれる感じにならない。

というわけで、EC2上で動作させたアプリケーションのログをCloudWatchで取得するまでのメモしておきたい。

参考

前提

  • EC2(AmazonLinux2) のインスタンスは作ってある

設定

awslogs をインストール。

1
sudo yum install -y awslogs

IAM ポリシーの作成

  • IAM > ポリシー > ポリシーの作成
  • ポリシーの作成
    • JSON
    • 次のものを貼り付け
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html(参照)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": [
"*"
]
}
]
}
  • タグの追加:今回はスキップ
  • ポリシーの作成
    • 名前:任意(今回はAwsLogsPolicy)
    • 説明:任意

ポリシーを作成。

IAM ロールの作成

  • IAM > ロール > ロールの作成の順に開く
  • 信頼されたエンティティを選択
    • 信頼されたエンティティタイプ:AWSのサービス
    • ユースケース:EC2
  • 許可を追加
    • 作成したポリシーにチェックを入れる
  • 名前、確認、および作成
    • ロール名:任意(今回はAwsLogsRole)
    • 説明:任意

ロールを作成。

ロールをEC2インスタンスに関連付け

  • 作ってあるログを収集したいいインスタンスを選択
  • アクション > セキュリティ > IAM ロールを変更
  • IAM ロールを変更
    • 作成したロールを割り当て

ここまでやってから気が付いたが、インスタンスに今回作ったロールを割り当てるのは、悪手な気がする。

  • インスタンスにインスタンス用のロールを割り当て
  • インスタンス用のロールに使いまわすポリシーを充てる

の方が良さそう。

aws logs の設定ファイルの編集

デフォルトは次の状態だった。

/etc/awslogs/awscli.conf
1
2
3
4
[plugins]
cwlogs = cwlogs
[default]
region = us-east-1

以下のように書き換えた。

/etc/awslogs/awscli.conf[編集後]
1
2
3
4
[plugins]
cwlogs = cwlogs
[default]
region = ap-northeast-1
/etc/awslogs/awslogs.conf
1
2
3
4
5
6
7
8
# 上の方に、パラメータの説明が書いてある
[/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 = /var/log/messages

以下を追記する。

/etc/awslogs/awslogs.conf
1
2
3
4
5
6
[/aws/ec2/application.log]
log_group_name = /aws/ec2/application.log
log_stream_name = {instance_id}
datetime_format = %Y-%m-%dT%H:%M:%S%z
time_zone = LOCAL
file = /home/ec2-user/app/application.log

awslogs の起動

1
2
3
4
# 起動
sudo systemctl start awslogsd
# 自動実行も入れておく
sudo systemctl enable awslogsd.service

動作確認

動作確認したいので、適当に CloudWatch にログ転送させるアプリを実行してみる

app.ts
1
2
3
4
5
6
7
8
import { delay } from "https://deno.land/std@0.142.0/async/mod.ts";

console.log("LOGSTART")
for(let i=0;i<100;i++){
await delay(1000)
console.log(`OUTPUT${i} ${new Date}`)
}
console.log("LOGEND")

実行。標準出力をすべて application.log へリダイレクト。

1
2
3
4
$ pwd 
/home/ec2-user/app

$ deno run app.ts >> application.log

ここでCloudWatch のロググループを見に行くと、/aws/ec2/application.log が作成されているので、開いてみる。
ログの書き出しがされているのが確認できる。

エラーログは標準ログと別

以下のように console.error を使うように書き換える。

app.ts
1
2
3
4
5
6
7
8
9
import { delay } from "https://deno.land/std@0.142.0/async/mod.ts";

console.log("LOGSTART")
for(let i=0;i<100;i++){
await delay(1000)
console.log(`OUTPUT${i} ${new Date}`)
console.error(`ERROR${i} ${new Date}`) // <= 書き足し
}
console.log("LOGEND")

この状態で、実行すると次のようになる。

1
2
3
4
5
$ deno run app.ts >> application.log
ERROR0 Sat Jun 04 2022 08:40:02 GMT+0000 (Coordinated Universal Time)
ERROR1 Sat Jun 04 2022 08:40:03 GMT+0000 (Coordinated Universal Time)
ERROR2 Sat Jun 04 2022 08:40:04 GMT+0000 (Coordinated Universal Time)
# 以下に続く

標準出力とエラー出力が、分かれていたなと思い出し実行コマンドを以下のように修正。

1
$ deno run app.ts >> application.log 2>&1

これで、console.error() もログ出力がリダイレクトされるので、すべてCloudwatchで参照できるようになる。

この状態だと、ログファイルが永遠に肥大化を続ける気がする

というわけで、アプリケーションの外側で logrotate を使って適当にログローテーションを設定してみる。

やり方は、以前書いていた。
ログローテーションを考える
(昔書いた記事が役に立つのは実に有意義である。)

/etc/logrotate.d/app1 を作成。
ログローテーションもするのでディレクトリを掘る前提にしておく。

/etc/logrotate.d/app1
1
2
3
4
5
6
7
8
/home/ec2-user/app/log/*.log{
su ec2-user ec2-user
hourly
dateext
rotate 1
missingok
truncate
}

hourly で、logrotate が動くようにしておく。

1
cp /etc/cron.daily/logrotate /etc/cron.hourly/

実行コマンドも変更

1
2
$ mkdir log
$ deno run app.ts >> log/application.log 2>&1

別コンソールで、ログローテーションを強制実行するといい具合にローテーションが起きてるのが確認できる。
これで単一のファイルのログが永遠と大きくなるのは回避できるはず。

サービス化する

サービス化したいので、system unit ファイルを作成します。

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=deno app
After=network.target

[Service]
Type=simple
ExecStart=/home/ec2-user/.deno/bin/deno run /home/ec2-user/app/app.ts >> /home/ec2-user/app/log/application.log 2>&1
Restart=always

[Install]
WantedBy=default.target

サービス化するにあたって終了してほしくないので、アプリケーション側は無限ループにしておく。
(本来はサーバーアプリの起動をすることになるだろうけれども。)

app.ts[修正後]
1
2
3
4
5
6
7
8
9
import { delay } from "https://deno.land/std@0.142.0/async/mod.ts";

console.log("LOGSTART")
while(true){
await delay(1000)
console.log(`OUTPUT$ ${new Date}`)
console.error(`ERROR$ ${new Date}`)
}
console.log("LOGEND")

と、こんな具合で用意して、次のように起動する。

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl start deno-app

ログが書き込まれない。

調べてみてこの記事にたどり着く。

こちらに倣い、/etc/systemd/system/deno-app.service を書き換える。

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=deno app
After=network.target

[Service]
Type=simple
ExecStart=/bin/sh -c '/home/ec2-user/.deno/bin/deno run /home/ec2-user/app/app.ts >> /home/ec2-user/app/log/application.log 2>&1'
Restart=always

[Install]
WantedBy=default.target

もしくは、

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=deno app
After=network.target

[Service]
Type=simple
ExecStart=/home/ec2-user/.deno/bin/deno run /home/ec2-user/app/app.ts
Restart=always
StandardOutput=append:/home/ec2-user/app/log/application.log
StandardError=append:/home/ec2-user/app/log/application.log

[Install]
WantedBy=default.target

こちらもいけそうだが、AmazonLinux2 に入っていた systemd のバージョンは、219だったので現状は、修正プラン1しか使えない。

1
2
3
$ systemctl --version
systemd 219
+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN

というわけで、/etc/systemd/system/deno-app.service をプラン1に書き換える。
改めてサービスを起動しなおすと、また欲しい先にログを吐き出してくれるようになる。

ここまでやれば、まぁ一旦いいだろう。


今回は、EC2で動かしているサービスのログをCloudWatchに吐き出すのを試みてみた。
よくやる構築なのだろうけど、イチからやらないと身には付かないものです。

これができちゃうと、複雑なログの機構をアプリケーション内で抱えるのって確かに悪手にも思える。
今回はDenoでやってみたが、deno deploy で動作させることを考えると、基本は console.hogehoge でログ出しておく方がいいだろうし。
逆に(複雑な)ログの機構を抱えるなら、cliツールとかなら検討してもいいのかもしれない。

ではでは。