ログ収集・分析(Fluent Bit / AWS OpenSearch)

| 13 min read
Author: noboru-kudo noboru-kudoの画像

今回はログの収集と分析に焦点を当てます。

いつの時代もアプリケーションが出力するログは、障害解析やアクセス/証跡管理に欠かせません。

マイクロサービスのような分散アーキテクチャを採用した場合、1つのアプリケーションは複数のサービスで構成されることになります。
このため、管理対象ログファイルの数は以前と比較すると飛躍的に多くなり、ファイルレベルでなく、全体のログを一元管理するためのバックエンドサービスは不可欠と言えます。
また、スケーラビリティや可用性を備えた構成にすると、サービスのインスタンス数は頻繁に増減し、それに伴って収集対象のログファイルも変わっていくことになります。
このような状況では、静的にログファイルを管理することは不可能に近く、動的に管理対象のログを検出・収集する仕組みも必要となります。

Kubernetesを実行基盤としたアプリケーションを考えてみましょう。
コンテナのローカルファイルシステムは一時的なもので、ここにログファイルを出力してもコンテナを再起動すると初期化されてしまいます。
これに対するシンプルな解決策は、標準出力・エラーを使うことです。
Kubernetes(具体的にはkubelet)では、コンテナの標準出力/エラーはコンテナログファイルとして各ノードで保管されます。
したがって、これらのログファイルを継続的に収集することで、ログを一元管理するバックエンドサービスに送信することが可能となります。
以下はKubernetesの公式ドキュメントから、ログ収集に関する方式の抜粋です。


引用元: https://kubernetes.io/docs/concepts/cluster-administration/logging/#using-a-node-logging-agent

標準出力・エラーのログファイル(log-file.log)をlogging-agentが収集し、ログ分析サービス(Logging Backend)に転送している様子が分かります[1]

これ以外にも、サイドカーコンテナを使った方式もありますが、今回は最も一般的なこちらの方式でログの収集と分析の仕組みを構築してみましょう。

ログ収集やその後の分析ツールは、OSSから有償サービスまで様々な選択肢がありますが、 ここではAWS環境での利用が容易な以下を選定します。

メトリクス同様に今回も2部構成に分けて実施します。
第1部はFluent Bit + AWS OpenSearchです。

Contents

事前準備

#

アプリケーションは以下で使用したものを使います。事前にEKS環境を準備し、アプリケーションのセットアップしておきましょう。

OpenSearchのセットアップ

#

まずは、ログ分析のバックエンドサービスとしてAWS OpenSearchをセットアップします。
AWS OpenSearchは、もともとはElastic社が所有するElasticsearchのマネージドサービス(Amazon ES)でした。
しかし、Elastic社のライセンス変更に伴い、AWSはElasticsearchの7.10.2からフォークしたOpenSearchを開発し、2021/9/8にこれをベースとしたAWS OpenSearchに名称変更しました[2]
このため、現状はAWS OpenSearchの内容はほぼElasticsearchと同じです。

OpenSearch(もちろんElasticSearchも)自体はログ専用のツールという訳ではなく、スケーラブルな全文検索エンジンですが、ここではログ検索の用途で使います。

OpenSearchもTerraformで作成します。/app/terraform/main.tfに以下を追記しましょう。

data "aws_region" "this" {}
data "aws_caller_identity" "this" {}

# OpenSearch
resource "aws_elasticsearch_domain" "this" {
domain_name = "task-tool-log"
elasticsearch_version = "OpenSearch_1.1"
ebs_options {
ebs_enabled = true
volume_type = "gp2"
volume_size = 30
}
node_to_node_encryption {
enabled = true
}
encrypt_at_rest {
enabled = true
}
domain_endpoint_options {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
advanced_security_options {
enabled = true
internal_user_database_enabled = true
master_user_options {
master_user_name = "admin"
master_user_password = "MZ-pass-123"
}
}
}

resource "aws_elasticsearch_domain_policy" "this" {
domain_name = aws_elasticsearch_domain.this.domain_name
access_policies = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow",
Principal = {
AWS = "*"
},
Action = "es:ESHttp*"
Resource = "arn:aws:es:${data.aws_region.this.name}:${data.aws_caller_identity.this.account_id}:domain/${aws_elasticsearch_domain.this.domain_name}/*"
}
]
})
}

aws_elasticsearch_domainリソースでOpenSearchドメイン(task-tool-log)を作成しています。
ここでは検証用のため、デフォルトのシングルノード構成としています。
また、内部ユーザーデータベース(internal_user_database_enabled)を有効にして、マスターユーザー・パスワードを個別指定しています。
AWS OpenSearchではSAMLやCognitoを使用した認証が用意されていますので、実運用で使う際は、こちらを利用するのが良いでしょう。
認証についての詳細は、以下のAWS OpenSearchの公式ドキュメントを参照してください。

aws_elasticsearch_domain_policyリソースではIAMレベルのアクセスポリシーを作成しています。
ここではIAMレベルでのアクセス制限は実施せず(AWS = "*")、OpenSearch内での認証のみとしています。

Caution

現状TerraformのAWS Providerのaws_elasticsearch_domainは、OpenSearchに関して不具合があり、インスタンスタイプの指定ができません。
AWS Providerの4.9.0バージョンではOpenSearch用のリソースが新しく追加され、この不具合が解消される予定です。
https://github.com/hashicorp/terraform-provider-aws/pull/23902

Terraform実行前に、マネジメントコンソールからTerraformの実行ユーザーのIAMポリシーにOpenSearchリソース作成の許可を与えるために、AmazonOpenSearchServiceFullAccessを追加してください。

ポリシーを追加したら、TerraformでAWS環境に反映(terraform apply)しましょう。 具体的な方法は以下を参照しくてださい。

反映が終わったら、実際にOpenSearchにアクセスしてみましょう。
OpenSearchは、Elasticsearch同様に可視化ツールのKibanaからフォークしたOpenSearch Dashboardsがあります。

マネジメントコンソールからOpenSearchを選択します。ダッシュボードに作成したドメインが表示されています。
opensearch dashboard

ドメインをクリックするとドメインの詳細が表示されます。
OpenSearch DashboardsのところにURLがありますので、こちらのリンクをクリックします。

domain detail

OpenSearch Dashboardsの認証ページが表示されるはずです。ユーザー・パスワードはTerraformで設定したマスターユーザー(上記はadmin/MZ-pass-123を指定)を入力します。

opensearch login

ログインに成功すると以下のような表示になります。Explore on my ownをクリックします。

opensearch select

テナントの選択ではPrivateを指定し、Confirmをクリックします。

opensearch private tenant

以下のようなトップページが表示されれば、初期セットアップは完了です。

opensearch top

Fluent Bitのアクセス許可設定

#

次に、コンテナログを収集し、OpenSearchへ転送するFluent Bitをセットアップします。
その前に、Fluent BitがOpenSearchにアクセスできるようにIAM関連リソースとKubernetesのServiceAccountを準備しておきましょう。
/app/terraform/main.tfに以下を追記します。

data "aws_iam_policy_document" "opensearch" {
version = "2012-10-17"
statement {
actions = ["es:ESHttp*"]
effect = "Allow"
resources = ["arn:aws:es:${data.aws_region.this.name}:${data.aws_caller_identity.this.account_id}:domain/${aws_elasticsearch_domain.this.domain_name}/*"]
}
}

resource "aws_iam_policy" "opensearch" {
name = "TaskToolOpenSearchAccess"
policy = data.aws_iam_policy_document.opensearch.json
}

module "fluentbit" {
source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"
version = "~> 4.0"
create_role = true
role_name = "FluentBit"
provider_url = var.oidc_provider_url
role_policy_arns = [aws_iam_policy.opensearch.arn]
oidc_fully_qualified_subjects = ["system:serviceaccount:fluent-bit:fluent-bit"]
}

resource "kubernetes_namespace" "fluentbit" {
metadata {
name = "fluent-bit"
}
}

resource "kubernetes_service_account" "fluentbit" {
metadata {
namespace = "fluent-bit"
name = "fluent-bit"
annotations = {
"eks.amazonaws.com/role-arn" = module.fluentbit.iam_role_arn
}
}
}

ここでは以下を定義しています。

  • aws_iam_policy.opensearch: OpenSearchアクセスためのIAM Policy
  • module.fluentbit: Fluent BitのServiceAccountに紐付けるIAM Role
  • kubernetes_namespace.fluentbit: Fluent Bitを配置するKubernetesのNamespace
  • kubernetes_service_account.fluentbit: Fluent Bit Podが利用するServiceAccount

先程同様に、これもAWS/EKS環境に反映(terraform apply)しておきましょう。

AWS OpenSearchへのアクセスはこれだけでは不十分です。OpenSearch側でも、このIAMロールからの操作を許可する必要があります[3]

再度OpenSearch DashboardsのUIにアクセスしてこれを実施していきましょう。
OpenSearch DashboardsのサイドバーからSecurityを選択します。
opensearch sidebar security

サイドバーよりRolesを選択し、ロール一覧の中からall_accessをクリックします[4]

opensearch security role

Mapped usersタブを選択し、Manage mappingをクリックします。

opensearch mapped users

Backend rolesに先程Terraformで作成したIAM Role(FluentBit)のARNを入力し、Mapをクリックしてください(ARNについてはマネジメントコンソールのIAMメニューより確認できます)。

opensearch add backend roles

これで、Fluent BitがOpenSearchに対して、ログ(インデックス)を保存可能になります。
なお、ここで実施しているOpenSearchのアクセスコントールの詳細については、公式ドキュメントを参照してください。

Fluent Bitのインストール

#

次はFluent Bit本体をセットアップしましょう。
今回は素のFluent Bitではなく、AWS環境向けに用意されているイメージを使います。

これについてもEKS向けのHelmチャートが公開されていますので、こちらを使いましょう。
以下でHelmチャートのリポジトリを追加します。

helm repo add eks https://aws.github.io/eks-charts
helm repo update

続いてFluent Bitをインストールします。現時点で最新の0.1.14を指定しました。

# アクセスするOpenSearchのホスト名をAWS CLIで取得。マネジメントコンソールから取得しても構いません。
export DOMAIN_NAME=task-tool-log
export OPEN_SEARCH_HOST=$(aws es describe-elasticsearch-domain --domain-name ${DOMAIN_NAME} --output text --query "DomainStatus.Endpoint")

helm upgrade --install aws-for-fluent-bit eks/aws-for-fluent-bit \
--version 0.1.14 \
--namespace fluent-bit \
--set serviceAccount.create=false \
--set serviceAccount.name=fluent-bit \
--set firehose.enabled=false \
--set cloudWatch.enabled=false \
--set kinesis.enabled=false \
--set elasticsearch.enabled=true \
--set elasticsearch.host=${OPEN_SEARCH_HOST} \
--set elasticsearch.awsRegion=ap-northeast-1

--namespaceserviceAccount.nameには、先程Terraformで作成したものを指定しています。
また、出力先としてOpenSearch(Elasticsearch)以外のサービスは無効化しています。

インストールが終わったら、内容を確認してみましょう。

まずはPodの状況です。

kubectl get pod -l app.kubernetes.io/name=aws-for-fluent-bit \
-n fluent-bit
NAME                       READY   STATUS    RESTARTS   AGE
aws-for-fluent-bit-cxc4d   1/1     Running   0          96s
aws-for-fluent-bit-kzd77   1/1     Running   0          96s

Fluent BitはDaemonSetとして作成されますので、ノード数と同じレプリカが正常に実行されていることを確認します。

コンテナログも確認してみましょう。

# 1つ目のPod名取得
export FLUENTBIT_POD=$(kubectl get pod -n fluent-bit -l app.kubernetes.io/name=aws-for-fluent-bit -o jsonpath='{.items[0].metadata.name}')
kubectl logs ${FLUENTBIT_POD} -n fluent-bit

特にエラーが発生していなければOKです。エラーが発生している場合はOpenSearchのエンドポイントやアクセス許可が正しく設定できているかを見直してください。

最後にFluent Bitの設定ファイルを確認してみましょう。

kubectl get cm aws-for-fluent-bit -n fluent-bit -o yaml
# fluent-bit.confのみ抜粋
[SERVICE]
    Parsers_File /fluent-bit/parsers/parsers.conf

[INPUT]
    Name              tail
    Tag               kube.*
    Path              /var/log/containers/*.log
    DB                /var/log/flb_kube.db
    Parser            docker
    Docker_Mode       On
    Mem_Buf_Limit     5MB
    Skip_Long_Lines   On
    Refresh_Interval  10

[FILTER]
    Name                kubernetes
    Match               kube.*
    Kube_URL            https://kubernetes.default.svc.cluster.local:443
    Merge_Log           On
    Merge_Log_Key       data
    Keep_Log            On
    K8S-Logging.Parser  On
    K8S-Logging.Exclude On
[OUTPUT]
    Name            es
    Match           *
    AWS_Region      ap-northeast-1
    AWS_Auth        On
    Host            search-task-tool-log-xxxxxxxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com
    Port            443
    TLS             On
    Retry_Limit     6
    Replace_Dots    On

Helmチャートのデフォルトそのままで明示的に設定ファイルを作成しませんでしたが、以下の内容で動作していることが確認できます。

  • [INPUT]: Nodeのコンテナログ(標準出力・エラー)を収集
  • [FILTER]: ログエントリーにKubernetesのメタ情報付加[5]
  • [OUTPUT]: 作成済みのOpenSearchへログ転送

なお、Helmチャートのパラメータで、自身の環境に合うようにカスタマイズも可能です。詳細はHelmチャートのリポジトリを参照してください。

ログ収集結果の確認

#

これでログ収集のためのプラットフォームが完成しました。
後はアプリケーションをデプロイして、そのログをOpenSearch Dashboardsで確認しましょう。

今回task-serviceのアプリケーションを改修して、リクエスト時にJSON形式のログを標準出力へ出力するようにしました。

こちらでコンテナイメージを作成して、デプロイします。
ここではtask-serviceのみを記載していますが、アプリケーションを動作させるためには、他のサービスのビルドも必要です。詳細はアプリケーション開発編を参照してください。

# PROJECT_ROOTにはリポジトリルートを設定してください
cd ${PROJECT_ROOT}/app/apis/task-service
docker build -t <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/mamezou-tech/task-service:1.0.0 .
docker push <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/mamezou-tech/task-service:1.0.0
# デプロイ
kubectl apply -f ${PROJECT_ROOT}/k8s/v3/overlays/prod
# アプリケーション状態確認
kubectl get pod -n prod
Information

既にアプリケーションをデプロイ済みの場合は、タグを変更するかimagePullPolicyAlwaysに変更してください。

デプロイが終わったら、アプリケーションのUIを操作して、いくつかログを出力させておきましょう。

それでは、OpenSearch Dashboardsを使ってログを可視化してみましょう。
サイドバーよりStack Managementを選択します。

opensearch stack management

メニューよりIndex Patternsを選択し、Create index patternをクリックします。

opensearch index pattern step1

index pattern namefluent-bitと入力すると、下部にマッチするインデックスが表示されます。
これを確認できたらNext stepをクリックします。

opensearch index pattern step2

Time fieldには@timestampを選択します。これはFluent Bitで全ログインデックスに対して付与されています。

opensearch index pattern step3

下図のように、Elasticsearchで登録されている属性が確認できます。
これでインデックスパターンの登録は完了です。

opensearch index pattern step4

では、ログを確認しましょう。サイドバーよりDiscoverを選択します。
opensearch discovery sidebar

以下のように、収集した全てのログが表示されていることが分かります。

opensearch discovery

このままでは分かりにくいので、task-serviceコンテナに絞ったり不要なログをフィルタリングすると以下のようになります。

opensearch task-service

レプリカ数に係わらず、task-serviceに関するログが集約して表示できていることが分かります。

ここでOpenSearch Dashboardsの具体的な使い方に言及するよりも、実際に自分で試してみたほうが実感できると思います。
左側の属性からフィルタリングや表示項目を調整したり、直接フィルタリング条件を指定したりしてみましょう。
直接入力する場合は、以下のOpenSearch Dashboardsの公式ドキュメントを参照してください(+ Add Filterリンクから選択方式で指定もできます)。

ログだけにとどまらず、Fluent Bitによって収集されたKubernetesのメタ情報や、JSON形式のログの属性から多角的な視点でログを分析できることが分かるはずです。

Information

現状OpenSearch Dashboardsのドキュメントはまだ整備されているとは言えない状況です。
場合によっては、フォーク元になっているKibana(v7.10)のドキュメントを参照した方が良いかもしれません。

クリーンアップ

#

HelmでインストールしたFluent Bitは以下で削除します。

helm uninstall aws-for-fluent-bit -n fluent-bit 

それ以外のリリース削除については、以下を参照してください。

ここではOpenSearchも既存のTerraformの設定ファイルに含めていますので、ここで同時に削除されます。

まとめ

#

今回はログ収集にFluent Bit、ログの一元管理と分析にAWS OpenSearchを使用し、アプリケーションのログ分析基盤を構築しました。

OpenSearchのElasticsearch由来の全文検索機能やKibana由来のリッチなUIにより、ログ分析がかなり楽になる印象を持った方も多いでしょう。

ただし、一般的にログのバックエンドサービスは非常に高いスループットでデータを処理する必要があります。
これに見合うようにAWS OpenSearchのスペックアップやマルチノード構成とした場合は、かなりのコストを覚悟する必要があります。
このため、運用コストとサービス利用料との兼ね合いによっては、マネージドサービスではなくELK/EFKスタック(Elasticsearch/LogStash(Fluentd)/Kibana)を導入するケースもあるでしょう。

また、OpenSearchのような高機能サービスではなく、それよりは安価なCloudWatchで十分まかなえるケースも多いかと思います。
次回はログのバックエンドサービスとして、AWS OpenSearchをAmazon CloudWatchに置き換えてみたいと思います。


参照資料


  1. ログローテート(logrotate)はkubeletの責務になります。 ↩︎

  2. AWSとElastic社の間での軋轢があるようですが、ここでは触れません。気になる方はこちらのブログを参照してください。 ↩︎

  3. AWS OpenSearchのきめ細やかなアクセスコントール(Fine-Grained Access Control)の詳細については公式ドキュメントを参照してください。 ↩︎

  4. 今回は事前定義されたものを使っていますが、実運用では専用のロールを作成し、最低限のポリシーとなるようにするべきです。 ↩︎

  5. https://docs.fluentbit.io/manual/pipeline/filters/kubernetes ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  2. Tauri でデスクトップアプリ開発を始める (2022-07-08)
  3. Deno による Slack プラットフォーム(オープンベータ) (2022-09-27)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  7. 直感が理性に大反抗!「モンティ・ホール問題」 (2022-07-04)
  8. Rust によるデスクトップアプリケーションフレームワーク Tauri (2022-03-06)
  9. 箱ひげ図で外れ値を確認する (2022-05-18)
  10. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)