ECS上のアプリケーションのログをKibanaで表示してみた

1.はじめに

Yusuke Yasuoka
nextbeat-engineering
23 min readSep 15, 2023

--

こんにちは、株式会社ネクストビートで開発をしている安岡と申します。

弊社のプロダクトでは、各種ログがKibanaから確認できます。
Kibana上ではログを視覚的に表示できたり、細かなフィルターを設定できます。これが、利用状況の確認や障害対応にとても役に立っています。

しかし、普段はログ周りの設定に触れる機会があまりありません。そこで、この機会にログ周りの設定をしてみました。具体的にはECS上のアプリケーションのログをKibanaで表示しました。この記事ではこの流れを解説します。

目次

  1. はじめに
  2. 概要
  3. アプリケーションの変更
  4. Fluent BitのDockerイメージの準備
  5. AWSリソースの変更
  6. 動作確認とKibanaの設定
  7. おわりに

2. 概要

最終的にKibanaに出力されるログは以下です。

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へアクセスした際のログは以下。スタックトレースが表示されているのがわかります。

/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

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--