ECS上のアプリケーションのログをKibanaで表示してみた
1.はじめに
こんにちは、株式会社ネクストビートで開発をしている安岡と申します。
弊社のプロダクトでは、各種ログがKibanaから確認できます。
Kibana上ではログを視覚的に表示できたり、細かなフィルターを設定できます。これが、利用状況の確認や障害対応にとても役に立っています。
しかし、普段はログ周りの設定に触れる機会があまりありません。そこで、この機会にログ周りの設定をしてみました。具体的にはECS上のアプリケーションのログをKibanaで表示しました。この記事ではこの流れを解説します。
目次
- はじめに
- 概要
- アプリケーションの変更
- Fluent BitのDockerイメージの準備
- AWSリソースの変更
- 動作確認とKibanaの設定
- おわりに
2. 概要
最終的にKibanaに出力されるログは以下です。
また、構成図は以下のようになっています。
図を見てわかるようにFireLens, Amazon Kinesis Data Firehose, Amazon OpenSearch ServiceといったAmazonリソースを使用しています。
FireLensはさまざまなAWSサービスにログを送信できます[1]。今回はAmazon Kinesis Data Firehoseに送信しています。また、今回はFluent Bitイメージをベースとしています。
Amazon Kinesis Data Firehoseは指定した配信先にデータを自動配信できます[2]。今回はOpenSearch Serviceにログを配信しています。
Amazon OpenSearch ServiceはOpenSearchというログ分析エンジンをデプロイ、運用、スケールできます[3]。また、Kibanaも提供しています[4]。
また、このアーキテクチャについては[5][6][7][8]を参考にさせていただきました。
アプリケーションは以下の記事で作成したものを使用しています。本記事では既にアプリケーションが作成されているものとします。
また、完成後のコードは以下のGitHubリポジトリのapp-logブランチに格納しています。
アプリケーション: https://github.com/yasu307/sample-application-for-aws-deploy/tree/app-log
CDK for Terraform: https://github.com/yasu307/sample-cdktf/tree/app-log
※ 情報に間違いがあったり、推奨されないような実装をしているかもしれません。ご了承ください。
3. アプリケーションの変更
3.1 ログの変更
まずはアプリケーションログをJSON形式で出力するように変更します。後ほどKibana上で分析しやすくするためです。
ここではLogstash Logback Encoderを使用します。
まずはライブラリを追加します。build.sbtに以下を追加します。
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.15.2"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.3.7"
libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "7.4"
次に出力するログを変更します。logback.xmlの内容を以下に変更します。
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
ログの出力はLogstashEncoderをそのまま用います。設定をすれば出力内容の変更が可能なようです[9]。
また、Logbackの設定になりますが、テストのためinfoレベルからログを出力しています。
以上の設定をするとログは次のようになります。
{
"@timestamp": "2023-09-13T22:07:04.511+09:00",
"@version": "1",
"message": "Access to index",
"logger_name": "controllers.HomeController",
"thread_name": "application-akka.actor.default-dispatcher-6",
"level": "INFO",
"level_value": 20000,
"application.home": "/Users/yusuke.yasuoka/blog/app-log/sample-application-for-aws-deploy"
}
3.2 エンドポイントを作成
エンドポイントを3つ設定します。
まずHomeControllerを以下のように変更します。
@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
val logger: Logger = Logger(this.getClass())
def ping() = Action { implicit request: Request[AnyContent] =>
Ok
}
def index() = Action { implicit request: Request[AnyContent] =>
logger.info("Access to index")
Ok(views.html.index())
}
def error() = Action { implicit request: Request[AnyContent] =>
Try("hoge".toInt).recover(e => logger.error("parceInt error", e))
Ok(views.html.index())
}
}
次にroutesファイルを以下のように変更します。
GET / controllers.HomeController.index()
GET /error controllers.HomeController.error()
GET /ping controllers.HomeController.ping()
ルートは通常のログを出力します。
/errorはエラーのログを出力します。ログにはスタックトレースが含まれています。
/pingはエラーを出力しません。ヘルスチェックに使用します。
以上でアプリケーションの変更は終わりです。
4. Fluent BitのDockerイメージの準備
次はFireLens上で使用されるFluent BitのDockerイメージを作成します。
公式のFluent Bitのイメージを使用するとネストされたログが出力されてしまいます。
これだと分析しづらくなってしまうため、ネストを解消します。
ネストを解消するには、独自のイメージを作成し、Parserを設定する必要があるようです[10]。
4.1 Fluent Bitのファイルを作成
まず以下の構成でファイルを作成します。
sample-cdktf
├── fluent-bit
│ ├── Dockerfile
│ └── extra.conf
次にDockerFileを以下のように変更します。
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
COPY extra.conf /fluent-bit/etc/extra.conf
Fluent Bitのイメージに、設定ファイルを追加しているだけです。
次にFluent Bitの設定ファイルを作成します。extra.confを以下のように変更します。
[SERVICE]
Parsers_File parsers.conf
[FILTER]
Name parser
Match *
Key_Name log
Parser json
Preserve_Key false
Reserve_Data true
[OUTPUT]
Name stdout
Match *
Parserにはparsers.confに用意されているjsonを用います。Parseが終わったログは標準出力に出力します。
4.2 リポジトリをECR上に作成
Fluent Bitのイメージを格納するリポジトリをECR上に作成します。sample-cdktfファイルのmain.tsに次の内容を加えます。
const fluentbitEcr = new EcrRepository(this, 'sample-cdktf-fluentbit-ecr', {
name: 'project/fluentbit'
});
イメージプッシュ前にリポジトリが必要なので、一度デプロイします。
cdktf deploy
4.3 リポジトリにイメージをプッシュ
4.1で作成したfluentbitファイル上で以下コマンドを実行します。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/project/fluentbit:0.0.1 .
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/project/fluentbit:0.0.1
通常のイメージプッシュ方法に従いました[12]。
ECRのarnは使用しているものに変更してください。
以上でFluent BitのDockerイメージの準備は終わりです。
5.AWSリソースの変更
5.1 ECSのコンテナ定義を変更する
sample-cdktfファイルのmain.tsのcontainerDefinitionを以下に変更します。
const containerDefinition: string = `[
{
"essential": true,
"name": "sample-cdktf-container",
"image": "${ecrRepository.repositoryUrl}:latest",
"portMappings": [
{
"hostPort": 9000,
"protocol": "tcp",
"containerPort": 9000
}
],
"logConfiguration": {
"logDriver": "awsfirelens",
"options": {
"Name": "firehose",
"region": "ap-northeast-1",
"delivery_stream": "sample-cdktf-firehose"
}
}
},
{
"essential": true,
"image": "${fluentBitEcr.repositoryUrl}:0.0.1",
"name": "log_router",
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"config-file-type": "file",
"config-file-value": "/fluent-bit/etc/extra.conf"
}
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/aws/ecs/firelens-container",
"awslogs-region": "ap-northeast-1",
"awslogs-create-group": "true",
"awslogs-stream-prefix": "firelens"
}
}
}
]`
ここでは2つのコンテナが定義されています。
1つ目のコンテナはアプリケーションのコンテナです。これは以前からありましたが、今回はlogConfigurationを変更しています。この設定をすることでログがFirehoseに送信されます[14]。
2つ目のコンテナはFluent Bitのコンテナです。firelensConfigurationにて4.1で作成した設定ファイルを使うよう指定しています[15]。
次に、Fluent Bitのコンテナで使用するawslogs-groupを作成します。
new CloudwatchLogGroup(this, 'sample-cdktf-firelens-container-log-group', {
name: `/aws/ecs/firelens-container`
});
5.2 OpenSearch ServiceとFirehoseを作成する
まずはFirehoseで使用する各種リソースを作成します。IAMロールとS3バケットです。
const firehoseRole = new IamRole(this, 'firehoseRole', {
name: 'sample-cdktf-firehoseRole',
assumeRolePolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": [
"firehose.amazonaws.com"
]
},
"Effect": "Allow",
"Sid": ""
}
]
}`
});
const firehoseIamPolicy = new IamPolicy(this, 'sample-cdktf-firehose-iam-policy', {
name: 'sample-cdktf-firehose-iam-policy',
description: 'IAM policy for firehose',
policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"es:DescribeElasticsearchDomain",
"es:DescribeElasticsearchDomains",
"es:DescribeElasticsearchDomainConfig",
"es:ESHttpPost",
"es:ESHttpPut",
"es:ESHttpGet",
"logs:PutLogEvents",
"s3:AbortMultipartUpload",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:PutObject"
],
"Resource": [
"*"
],
"Effect": "Allow"
}
]
}`
});
new IamRolePolicyAttachment(this, 'attach-firehose-policy', {
role: firehoseRole.name,
policyArn: firehoseIamPolicy.arn
});
const firehoseS3Bucket = new S3Bucket(this, 'sample-cdktf-firehose-s3', {
bucket: 'sample-cdktf-firehose-s3'
});
IAMロールのポリシーではOpenSearch Service、Cloudwatch Logs、S3に関する権限を付与しています。
次にOpenSearch Serviceを作成します。
const elasticsearchDomain = new ElasticsearchDomain(this, 'sample-cdktf-es-domain', {
domainName: 'sample-cdktf-es-domain',
elasticsearchVersion: '7.10',
clusterConfig: {
instanceType: 't3.medium.elasticsearch'
},
ebsOptions: {
ebsEnabled: true,
volumeType: "gp3",
volumeSize: 10,
throughput: 125
},
accessPolicies: `{
"Version": "2012-10-17",
"Statement": [
{
"Action": "es:*",
"Principal": "*",
"Effect": "Allow",
"Resource": "*",
"Condition": {
"IpAddress": { "aws:SourceIp": ["xxx.xxx.xxx.xxx"] }
}
}
]
}`
});
テスト環境のため、データノードのインスタンスタイプやebsのボリュームは低めにしています。
また、アクセスポリシーは自分が使っているIPアドレスを許可しました。
次にFirehoseを作成します。
new KinesisFirehoseDeliveryStream(this, 'sample-cdktf-firehose', {
name: 'sample-cdktf-firehose',
destination: 'opensearch',
opensearchConfiguration: {
domainArn: elasticsearchDomain.arn,
roleArn: firehoseRole.arn,
indexName: 'sample-cdktf',
s3Configuration: {
roleArn: firehoseRole.arn,
bucketArn: firehoseS3Bucket.arn,
bufferingSize: 10,
bufferingInterval: 400
}
},
});
indexはsample-cdktfとしました。これを用いてKibana上でログを分類します。
5.3 その他のAWSリソースの変更
ALBのヘルスチェック先を/pingに変更します。albTargetGroupを以下に書き換えます。
healthCheck: {
interval: 30,
path: '/ping',
port: 'traffic-port',
protocol: 'HTTP',
timeout: 5,
unhealthyThreshold: 2,
},
次にECSのタスクロールに付与するIAMポリシーを変更します。ecsTaskIamPolicyを以下に変更します。
const ecsTaskIamPolicy = new IamPolicy(this, 'ecs-task-policy', {
name: 'ecs-task-policy',
description: 'Policy for ECS tasks',
policy: `{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"ecs:DescribeServices",
"ecs:CreateTaskSet",
"ecs:UpdateServicePrimaryTaskSet",
"ecs:DeleteTaskSet",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:ModifyRule",
"lambda:InvokeFunction",
"cloudwatch:DescribeAlarms",
"sns:Publish",
"s3:GetObject",
"s3:GetObjectVersion",
"firehose:PutRecordBatch"
],
"Resource": [
"*"
],
"Effect": "Allow"
}
]
}`
});
Actionに”firehose:PutRecordBatch”の1行を加えているだけです。
AWSリソースの変更は以上です。デプロイします。
cdktf deploy
6. 動作確認とKibanaの設定
6.1 アプリケーションログの生成
アプリケーションにアクセスします。次の流れでアプリケーションのURLを確認できます。
AWSコンソール → Amazon Elastic Container Service → sample-cdktf-cluster →
sample-cdktf-ecs-service → ネットワーキングタブ → DNS名
ルートと/errorに何度かアクセスします。
6.2 Kibanaの設定
まずKibanaにアクセスします。次の流れでKibanaのURLを確認できます。
AWSコンソール → Amazon OpenSearch Service → sample-cdktf-es-domain → Kibana URL
次にKibanaのindex patternを作成します。Firehoseで指定したindexが当てはまるようにします。
index pattern作成の流れは以下です。
1: index pattern作成画面に移動。次の流れで移動します。
ハンバーガーメニュー → Stack Management → Index Patterns
2: Create index patternボタンをクリック
3: Index pattern nameに”sample-cdktf-*”と入力する
4: Next stepボタンをクリック
5: Time fieldにて@timestampを選択する
6: Create index patternボタンをクリック
6.3 Kibanaのログの確認
ハンバーガーメニューを経由してDiscover画面に移動します。するとログが表示されています。
ルートへアクセスした際のログは以下。
/errorへアクセスした際のログは以下。スタックトレースが表示されているのがわかります。
以上で動作確認とKibanaの設定は終わりです。
7. おわりに
実際に自分で構築することでログ表示の流れがよくわかりました。
また、今回のような単純な実装だけでなく、要所要所でログを変換したり、送信先を制御できることもわかりました。エラーログのみSlackで通知をするといったものも実装してみたいです。
最後まで読んでいただきありがとうございました。
参考文献
[1]詳解 FireLens — Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る
[2]Amazon Kinesis Data Firehose とは何ですか?
[3]Amazon OpenSearch Service とは?
[4]Amazon OpenSearch Service マネージドサービス
[5]ECS ログ出力カスタマイズ FireLens の設定方法をわかりやすく整理してみた
[6]ECS ログ出力カスタマイズ FireLens でログの出力先を分岐させてみた
[7]詳解 FireLens — Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る
[8]Amazon Elasticsearch Service、Amazon Kinesis Data Firehose、Kibana を使用してユーザーの行動を分析する
[9]logstash-logback-encoder
[10]ECSでfirelensを利用したログ収集で、標準出力と標準エラー出力の転送先を分ける
[11]https://github.com/fluent/fluent-bit/blob/master/conf/parsers.conf
[12]Docker イメージをプッシュする
[13]ロギングオプションのタスク定義の例
[14]FireLens 設定を使用するタスク定義の作成
We are hiring!
本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?
・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい
など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!
🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008
また、ネクストビートについてはこちらもご覧ください。