GCPでログを収集したり、インスタンスの使用状況などを監視する場合は、LoggingエージェントMonitoringエージェントを利用することが一般的でした。

筆者もこれらを利用してきましたし、Loggingエージェントのために数々のfluentdのconfigファイルを書いてきましたが、これらは新たなOpsエージェントに置き換えるのが良さそうに見えます。

Opsエージェントに関するGoogle Cloud Japanの公式ブログによれば、「従来のLoggingエージェントに存在していたOutOfMemoryエラーやデータの損失を防止できる」と書いてあるので、これらに頭を悩まされてきた身としては見過ごすわけにはいきません。

この記事では、Opsエージェントを使い始めるまでの設定と、任意のアプリケーションログをGCPのLoggingで閲覧するための設定内容を調査していきます。

Opsエージェントのインストール

Ubuntu20.04のVMインスタンスを起動し、オペレーションスイート(旧Stackdriver)のエージェントポリシーを利用してインストールと起動までをある程度自動化しましょう。

Ubuntuの起動

gcloud compute instances create ubuntu20 \
  --zone=asia-northeast1-a \
  --subnet=<YOUR_SUBNET> \
  --no-address \
  --image=ubuntu-2004-focal-v20210720 \
  --image-project=ubuntu-os-cloud \
  --boot-disk-size=100GB

OS Configエージェントのインストール

# インスタンス内で実行
sudo apt update
sudo apt -y install google-osconfig-agent

これで、エージェントポリシーをUbuntu OSに対して設定すれば、今後Ubuntuのインスタンスには自動でOpsエージェントがインストールされます。

エージェントポリシーの設定

次のコマンドは、Ubuntu 20.04 のインスタンスにOS Configエージェントが起動していれば ops-agentを自動アップグレードを有効にしてインストールするポリシーです。
これをCloud Consoleから閲覧する手段は執筆時点では無いようです。
紛らわしいですが、OS Policyとは別の機能です。

gcloud beta compute instances ops-agents policies create ops-agents-policy-safe-rollout \
  --agent-rules="type=ops-agent,version=current-major,package-state=installed,enable-autoupgrade=true" \
  --os-types=short-name=ubuntu,version=20.04

これで、インスタンスに ops-agentがインストールされます。

# インスタンス内で実行
systemctl status google-cloud-ops-agent
● google-cloud-ops-agent.service - Google Cloud Ops Agent
     Loaded: loaded (/lib/systemd/system/google-cloud-ops-agent.service; enabled; vendor preset: enabled)
     Active: active (exited) since Sat 2021-08-07 04:16:23 UTC; 1h 47min ago

ドキュメントによると、デフォルトでは、次のような設定が有効のようです。

logging:
  receivers:
    syslog:
      type: files
      include_paths:
      - /var/log/messages
      - /var/log/syslog
  service:
    pipelines:
      default_pipeline:
        receivers: [syslog]
metrics:
  receivers:
    hostmetrics:
      type: hostmetrics
      collection_interval: 60s
  processors:
    metrics_filter:
      type: exclude_metrics
      metrics_pattern: []
  service:
    pipelines:
      default_pipeline:
        receivers: [hostmetrics]
        processors: [metrics_filter]

ここからは、いくつかのパターンを想定して、設定を調査していきます。

コンテナの標準出力をLoggingに送る

nginxのコンテナを実行して、その標準出力をLoggingに送ってみます。
まずはdockerをインストールして、nginxコンテナを実行しましょう。
ここからはrootユーザで実行します。

# インスタンス内で実行
sudo su
apt install docker.io
docker run -d --name nginx -p 0.0.0.0:80:80 nginx:stable-alpine

docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED         STATUS         PORTS                NAMES
8f6af88ebe4a   nginx:stable-alpine   "/docker-entrypoint.…"   4 minutes ago   Up 4 minutes   0.0.0.0:80->80/tcp   nginx

この時、コンテナの標準出力は /var/lib/docker/containers/CONTAINER_IDディレクトリのCONTAINER_ID-json.logに吐き出されているので、
コンテナの標準出力をLoggingに送るには、次のようにconfigを書けば良さそうです。
このとき、LoggingにおけるlogNamecontainersとなっていました。

logging:
  receivers:
    containers: # これがlogNameになる
      type: files
      include_paths:
      - /var/lib/docker/containers/*/*.log
  service:
    pipelines:
      default_pipeline:
        receivers: []
      nginx_pipeline:
        recievers: [containers]
        processors: [nginx]
  processors:
    nginx:
      type: parse_json
      field: log
      time_key: time
      time_format: '%Y-%m-%dT%H:%M:%S.%NZ'

fluentdであれば、下記のようなログの中から logプロパティを取り出したりできるのですが、
Opsエージェントでの方法がわかりませんでした。
筆者の理解の範囲では、fluentdで柔軟な設定ができるLoggingエージェントの方が自分たちの要件にあったparseができそうです。

{"log":"172.17.0.1 - - [07/Aug/2021:05:35:21 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.68.0\" \"-\"\n","stream":"stdout","time":"2021-08-07T05:35:21.547137658Z"}

ログファイルをLoggingに送る

続いてはコンテナの標準出力ではなく、インスタンスの任意のpathに吐き出されるログをLoggingに送ります。
色々なログパターンが欲しいので、GitLabを起動することにします。
次のdocker-composeファイルを用意しました。
34.146.28.37はubuntuインスタンスに外部から到達するために起動したTCPロードバランサーのIPアドレスです。

version: "3.8"
services:
  gitlab:
    image: 'gitlab/gitlab-ee:latest'
    hostname: 'gitlab'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://34.146.28.37'
        nginx['listen_port'] = 80
        nginx['listen_https'] = false
        gitlab_exporter['enable'] = true
    ports:
      - '80:80'
    volumes:
      - '/srv/gitlab/config:/etc/gitlab'
      - '/srv/gitlab/logs:/var/log/gitlab'
      - '/srv/gitlab/data:/var/opt/gitlab'

これをdocker composeで実行すると、 /srv/gitlab/logs/gitlab-rails/application_json.logに次のようなログが出力されるので、
これをLogginに送ることを目的にします。

{"severity":"DEBUG","time":"2021-08-07T09:31:13.303Z","correlation_id":null,"message":"ActiveRecord connection established"}

これは、次のようにconfigを書けば実現できます。

logging:
  receivers:
    gitlab-application:
      type: files
      include_paths:
      - /srv/gitlab/logs/gitlab-rails/application_json.log
  service:
    pipelines:
      default_pipeline:
        receivers: []
      gitlab_application:
        receivers: [gitlab-application]
        processors: [gitlab-application]
  processors:
    gitlab-application:
      type: parse_json
      field: message
      time_key: time
      time_format: '%Y-%m-%dT%H:%M:%S.%LZ'

Cloud Consoleでは次のように構造化されたログを確認することができます。

さらに別のログを見ていきましょう。
/srv/gitlab/logs/nginx/gitlab_access.logには、nginxのアクセスログが出力されています。
これを正規表現を使ってparseしてみましょう。

サンプルとして、ログを1つ取り出します。

36.13.16.123 - - [07/Aug/2021:09:39:46 +0000] "GET /search/opensearch.xml HTTP/1.1" 200 321 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" 1.74

これをparseするためには次のような正規表現を書けます。

/^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)"(?:\s+(?<http_x_forwarded_for>[^ ]+))?)?$/

先程のconfigと合わせると、最終的に次のようなファイルが出来上がりました。

logging:
  receivers:
    gitlab-application:
      type: files
      include_paths:
      - /srv/gitlab/logs/gitlab-rails/application_json.log
    gitlab-access:
      type: files
      include_paths:
      - /srv/gitlab/logs/nginx/gitlab_access.log
  service:
    pipelines:
      default_pipeline:
        receivers: []
      gitlab_application:
        receivers: [gitlab-application]
        processors: [gitlab-application]
      gitlab_access:
        receivers: [gitlab-access]
        processors: [gitlab-access]
  processors:
    gitlab-application:
      type: parse_json
      field: message
      time_key: time
      time_format: '%Y-%m-%dT%H:%M:%S.%LZ'
    gitlab-access:
      type: parse_regex
      fielad: message
      regex: '/^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)"(?:\s+(?<http_x_forwarded_for>[^ ]+))?)?$/'
      time_key: time
      time_format: '%d/%b/%Y:%H:%M:%S %z'

Loggingの画面でもしっかり構造化されていることを確認できます。

まだまだ研究が足りていませんが、ops-agentを使って任意のログをLoggingに送ることができました。
新しい発見があればまた記事にします。