Vector とは
vector は timber とともに買収され datadog がメンテナンスしているオープンソースプロジェクトのようです。(Datadog acquires Timber Technologies)
A lightweight, ultra-fast tool for building observability pipelines
ということで Rust で書かれたメトリクスやログ収集のためのツールです。今回はこれを使って Kubernetes (EKS) のコンテナログを CloudWatch Logs に送信するために使ってみます。
Vector Components
Vector component を Source → Transform → Sink と繋げることでデータの収集、変換、ルーティングを簡単に行うことができます。
今回はシンプルな構成で次のものを使います。
- Source: kubernetes_logs
- Transform: remap (JSON ログの parse)
- Sink: aws_cloudwatch_logs
Helm を使って Kubernetes に deploy する
Agent を DaemonSet として deploy し、各 node から直接 CloudWatch Logs に送るという構成でセットアップします。
EKS ですので CloudWatch Logs へのアクセスには IRSA を使うこととします。(Pod Identity は試していない)
EKS は 1.30
node の OS は Amazon Linux 2023
Helm chart
Helm chart は次のリポジトリで管理されており、https://helm.vector.dev で公開されています。
helm repo add vector https://helm.vector.dev
$ helm search repo vector/vector
NAME CHART VERSION APP VERSION DESCRIPTION
vector/vector 0.37.0 0.42.0-distroless-libc A lightweight, ultra-fast tool for building obs...
vector/vector-agent 0.21.3 0.19.3 A Helm chart to collect Kubernetes logs with Ve...
vector/vector-aggregator 0.21.3 0.19.3 A Helm chart to aggregate data with Vector
vector/vector-shared 0.2.1 Shared dependencies of Vector Helm charts
今回試したのは helm chart の version が 0.37.0 で、vector の version は 0.42.0 です。
values
# DaemonSet として deploy する
role: Agent
# IRSA 用の IAM Role を指定
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: ${roleArn}
# 全ての node に deploy する
tolerations:
- operator: Exists
# node 内の優先度を上げる
podPriorityClassName: system-node-critical
# aggregator としての deploy ではないので Service は不要
service:
enabled: false
serviceHeadless:
enabled: false
logLevel: warn
livenessProbe:
httpGet:
path: /health
port: api
# config ファイルの定義用変数
customConfig:
# host の /var/lib/vector が /vector-data-dir に mount されるため、customConfig を使う場合はここを指定する必要がある
data_dir: /vector-data-dir
# livenessProbe のために api を有効化
api:
enabled: true
address: 0.0.0.0:8686
playground: false
sources:
# kubernetes_logs でログファイルから読み出す
container_logs:
type: kubernetes_logs
auto_partial_merge: true
delay_deletion_ms: 60000
glob_minimum_cooldown_ms: 60000
ignore_older_secs: 3600
max_line_bytes: 32768
read_from: beginning
rotate_wait_secs: 300
use_apiserver_cache: true
transforms:
# remap component を使ってログが JSON で出力されている場合に parse する
parsed_container_logs:
# inputs に sources で定義された名前を指定
inputs:
- container_logs
type: remap
# VRL で処理内容を定義
source: |
structured = parse_json(.message) ?? parse_nginx_log(.message, "combined") ?? parse_nginx_log(.message, "error") ?? null
if structured != null {
.data = structured
del(.message)
}
sinks:
# CloudWatch Log に送る
cloudwatch:
type: aws_cloudwatch_logs
# inputs に sources や transforms で定義した名前を指定
inputs:
- parsed_container_logs
group_name: ${cloudwatch_log_group_name}
create_missing_group: false
# LogStream の名前は複数の node で被らないように指定する
stream_name: "{{`{{`}}kubernetes.pod_namespace{{`}}`}}-{{`{{`}}kubernetes.pod_name{{`}}`}}-{{`{{`}}kubernetes.container_name{{`}}`}}"
create_missing_stream: true
region: ap-northeast-1
auth:
region: ap-northeast-1
# buffer には memory と disk が選択可能
buffer:
type: memory
max_events: 500
when_full: block
encoding:
codec: json
Vector Remap Language (VRL)
transform の remap component の source の中身は VRL という vector の DSL でよく使われる操作が function として定義されています。
今回は parse_json() を使って JSON で出力されたコンテナのログを parse して、CloudWatch Logs Insights で直接クエリできるようにしました。他にもいくつかよくあるログフォーマット用の parser も存在するので ??
で繋げてみました。こうすることで parse に失敗した場合に次の parser を試すといったことが可能です。
コンテナの出力は message という field に入っており、kubernetes_logs source は API Server から metadata を付与してくれます。JSON 内の項目がこの metadata を上書きしないようにここでは data という field に parse の結果を入れています。Google の Cloud Logging に倣って jsonPayload とするのも良いでしょう。del()
で元の message を削除しています。
structured = parse_json(.message) ?? parse_nginx_log(.message, "combined") ?? parse_nginx_log(.message, "error") ?? null
if structured != null {
.data = structured
del(.message)
}
VRL playground や vector CLI の vrl サブコマンドで VRL の動作確認もできます。
CloudWatch Logs に送られたログ
kubernetes_logs ではデフォルトで多くの metadata が付与されます。
これらは pod_annotation_fields や namespace_annotation_fields, node_annotation_fields で制御可能です。
{
"file": "/var/log/pods/kube-system_kube-proxy-fj9vw_23986ffa-791b-4c26-a8b5-e1ddf5bf4604/kube-proxy/0.log",
"kubernetes": {
"container_id": "containerd://c44b2ee3caca7594bd45f54203cb5b3887b3bc01b021c0a646378556d315e77c",
"container_image": "111222333444.dkr.ecr.ap-northeast-1.amazonaws.com/eks/kube-proxy:v1.31.1-minimal-eksbuild.2",
"container_image_id": "111222333444.dkr.ecr.ap-northeast-1.amazonaws.com/eks/kube-proxy@sha256:1f678e1d10559dec62ad6640a96e1deec17f3e1354e3002290a8bbc08da67c78",
"container_name": "kube-proxy",
"namespace_labels": {
"kubernetes.io/metadata.name": "kube-system"
},
"node_labels": {
"beta.kubernetes.io/arch": "amd64",
"beta.kubernetes.io/instance-type": "m7i.4xlarge",
"beta.kubernetes.io/os": "linux",
"eks.amazonaws.com/capacityType": "ON_DEMAND",
"eks.amazonaws.com/nodegroup": "system",
"eks.amazonaws.com/nodegroup-image": "ami-1234567890abcdef0",
"eks.amazonaws.com/sourceLaunchTemplateId": "lt-1234567890abcdef0",
"eks.amazonaws.com/sourceLaunchTemplateVersion": "8",
"failure-domain.beta.kubernetes.io/region": "ap-northeast-1",
"failure-domain.beta.kubernetes.io/zone": "ap-northeast-1c",
"k8s.io/cloud-provider-aws": "991feb0579d6e31b9de4b36d824a2fe8",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "ip-10-0-19-208.ap-northeast-1.compute.internal",
"kubernetes.io/os": "linux",
"node.kubernetes.io/instance-type": "m7i.4xlarge",
"topology.ebs.csi.aws.com/zone": "ap-northeast-1c",
"topology.k8s.aws/zone-id": "apne1-az1",
"topology.kubernetes.io/region": "ap-northeast-1",
"topology.kubernetes.io/zone": "ap-northeast-1c",
"workload/system": "true"
},
"pod_ip": "10.0.19.208",
"pod_ips": [
"10.0.19.208"
],
"pod_labels": {
"controller-revision-hash": "775c97d6d",
"k8s-app": "kube-proxy",
"pod-template-generation": "2"
},
"pod_name": "kube-proxy-fj9vw",
"pod_namespace": "kube-system",
"pod_node_name": "ip-10-0-19-208.ap-northeast-1.compute.internal",
"pod_owner": "DaemonSet/kube-proxy",
"pod_uid": "23986ffa-791b-4c26-a8b5-e1ddf5bf4604"
},
"message": "I1201 11:28:39.520230 1 proxier.go:822] \"SyncProxyRules complete\" ipFamily=\"IPv4\" elapsed=\"162.318057ms\"",
"source_type": "kubernetes_logs",
"stream": "stderr"
}
JSON 出力されていたログの場合
{
"data": {
"author": "アルフレッド・テニソン",
"length": "75",
"message": "過ぎ去りし麗しき日々は、再び我が元に返り来たらず。",
"method": "GET",
"severity": "info",
"time": "2024-11-29T08:30:34.613521319Z",
"uri": "/?n=100",
"uuid": "82d6e748-6449-484b-bf1e-65df7e7628b9"
},
"file": "/var/log/pods/meigen_meigen-6559557bfb-vs6jv_3336ef94-b0f5-413f-a79b-bfe1e7f4c67f/meigen/0.log",
"kubernetes": {省略},
"source_type": "kubernetes_logs",
"stream": "stderr"
}
終わりに
以上、とりあえず DaemonSet として vector を deploy して CloudWatch Logs にログを転送する例でした。
安定運用できるかは要検証ですが、Fluentd、Fluent Bit の代替として検討しようかという今日この頃です。