Unyablog.

のにれんのブログ

サポート外のリソースを含んだ Helm release を無理やり削除した

久々に helmfile sync を行った結果、PodSecurityPolicy リソースなんか知らないという理由で Upgrade に失敗した。

$ helmfile dep && helmfile sync
...
  Error: UPGRADE FAILED: resource mapping not found for name: "descheduler" namespace: "" from "": no matches for kind "PodSecurityPolicy" in version "policy/v1beta1"
  ensure CRDs are installed first

PSP? なにそれ CRD か何か?」と言われる有様。そういえば、k8s クラスタを最近アップグレードしたときに不要な PSP を消していた気がする。

一旦手動で helm uninstall をしようとするも、同じようなエラーが出て uninstall できない。

$ helm uninstall descheduler -n kube-system --debug
uninstall.go:97: [debug] uninstall: Deleting descheduler
uninstall.go:119: [debug] uninstall: Failed to delete release: [unable to build kubernetes objects for delete: resource mapping not found for name: "descheduler" namespace: "" from "": no matches for kind "PodSecurityPolicy" in version "policy/v1beta1"
ensure CRDs are installed first]
Error: failed to delete release: descheduler
helm.go:84: [debug] failed to delete release: descheduler
helm.sh/helm/v3/pkg/action.(*Uninstall).Run
        helm.sh/helm/v3/pkg/action/uninstall.go:120
main.newUninstallCmd.func2
        helm.sh/helm/v3/cmd/helm/uninstall.go:60
github.com/spf13/cobra.(*Command).execute
        github.com/spf13/cobra@v1.6.1/command.go:916
github.com/spf13/cobra.(*Command).ExecuteC
        github.com/spf13/cobra@v1.6.1/command.go:1044
github.com/spf13/cobra.(*Command).Execute
        github.com/spf13/cobra@v1.6.1/command.go:968
main.main
        helm.sh/helm/v3/cmd/helm/helm.go:83
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_arm64.s:1172

どうやら、以前この Chart を install したときには PSP があったので、今回 uninstall するにあったっても PSP を消そうとするが、API の対応がないので落ちているらしい。

ここで、helm はインストールしたときの情報を secrets に保存している。この内容をデコードすると、インストール時の YAML がそのまま格納されている。

$ kubectl describe secret -n kube-system sh.helm.release.v1.descheduler.v15
Name:         sh.helm.release.v1.descheduler.v15
Namespace:    kube-system
Labels:       modifiedAt=1687875686
              name=descheduler
              owner=helm
              status=uninstalling
              version=15
Annotations:  <none>

Type:  helm.sh/release.v1

Data
====
release:  13804 bytes

$ kubectl get secret -n kube-system sh.helm.release.v1.descheduler.v15 -o json | jq .data.release -r | base64 -d | base64 -d | gunzip | jq
{
  "name": "descheduler",
  "info": {
    "first_deployed": "2021-05-25T22:28:45.6228274+09:00",
    ...
  },
  ...
  "manifest": "---\n# Source: descheduler/templates/podsecuritypolicy.yaml\napiVersion: policy/v1beta1\nkind: PodSecurityPolicy\nmetadata:\n  name: descheduler\n  annotations:\n    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default'\n    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'runtime/de...(続く)"
...
}

この manifest を使って uninstall しているのでは?と考えて、manifest から PodSecurityPolicy の部分を削って secrets の値を変更してみたところ、ビンゴだった。

$ gzip new_release.json --stdout | base64 -w 0 | base64 -w 0 | pbcopy
$ kubectl edit secret -n kube-system sh.helm.release.v1.descheduler.v15
(コピーした内容を release に貼り付ける)

これで無事 PSP を除いた uninstall を行うことができた。

$ helm uninstall descheduler -n kube-system --debug
uninstall.go:97: [debug] uninstall: Deleting descheduler
client.go:478: [debug] Starting delete for "descheduler" CronJob
client.go:478: [debug] Starting delete for "descheduler" ClusterRoleBinding
client.go:478: [debug] Starting delete for "descheduler" ClusterRole
client.go:478: [debug] Starting delete for "descheduler" ConfigMap
client.go:478: [debug] Starting delete for "descheduler" ServiceAccount
uninstall.go:150: [debug] purge requested for descheduler
release "descheduler" uninstalled

全く同じ内容がドキュメントにあることにその後気づいた。事故らなくてよかったですね…。

helm.sh

今回の事象は Upgrade 時に何も考えずに非互換なリソースを消して済ましたのが原因であり、教訓としては以下になる。

  • 非互換なリソースの変更を伴う Cluster upgrade は、そのリソースを触る前に Helm 配下にないか確認する
    • 配下にあった場合は Helm を使ってそのリソースをなんとかするべき

curl の trace / trace-ascii が便利だった

curl で通信の内容を見ながら詳細なデバッグをしたいとき、 tcpdumpWireshark でパケットキャプチャを行うのが定番だと思う。ただ、通信が多い環境だと実際にデバッグしたい通信を絞り込むのが面倒だし、TLS だと暗号化されていてさらに面倒。

curl のオプションを見ると通信内容のトレースを記録できる --trace--trace-ascii があり、使ってみると便利だった。

everything.curl.dev

$ curl https://www.google.com/ [--trace-time] --trace[-ascii] trace-result.txt

これで curl が行った通信の内容が trace-result.txt に記録される。- にすると stdout に出力される。

また、--trace-time をつけると trace に時刻が付与され、--trace--trace-ascii にすると Hex は保存されず ascii の内容のみ確認することができる。

trace の内容は以下のような感じ(一部省略している)。

== Info:   Trying 2404:6800:4004:80b::2004:443...
== Info: Connected to www.google.com (2404:6800:4004:80b::2004) port 443 (#0)
== Info: ALPN, offering h2
== Info: ALPN, offering http/1.1
== Info: successfully set certificate verify locations:
== Info:  CAfile: /etc/ssl/certs/ca-certificates.crt
== Info:  CApath: /etc/ssl/certs
=> Send SSL data, 5 bytes (0x5)
0000: 16 03 01 02 00                                  .....
== Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
=> Send SSL data, 512 bytes (0x200)
0000: 01 00 01 fc 03 03 ca 7f 00 66 6c 46 6f a9 2c 8f .......^?.flFo.,.
0010: 97 83 1a b8 c7 80 50 6d 0c c8 f8 73 bc 49 90 ab ......Pm...s.I..
0020: ec 8b bc be 4f 0d 20 a4 f1 17 e2 8c 0d 6f 61 25 ....O. ......oa%
0030: 54 d8 13 27 b6 5d e2 0d 92 de 42 9f 93 2c ce c5 T..'.]....B..,..
0040: 50 bd 3c 75 b9 bd 86 00 3e 13 02 13 03 13 01 c0 P.<u....>.......
0050: 2c c0 30 00 9f cc a9 cc a8 cc aa c0 2b c0 2f 00 ,.0.........+./.
0060: 9e c0 24 c0 28 00 6b c0 23 c0 27 00 67 c0 0a c0 ..$.(.k.#.'.g...
0070: 14 00 39 c0 09 c0 13 00 33 00 9d 00 9c 00 3d 00 ..9.....3.....=.
0080: 3c 00 35 00 2f 00 ff 01 00 01 75 00 00 00 13 00 <.5./.....u.....
0090: 11 00 00 0e 77 77 77 2e 67 6f 6f 67 6c 65 2e 63 ....www.google.c
00a0: 6f 6d 00 0b 00 04 03 00 01 02 00 0a 00 0c 00 0a om..............
...
<= Recv SSL data, 5 bytes (0x5)
0000: 16 03 03 00 7a                                  ....z
== Info: TLSv1.3 (IN), TLS handshake, Server hello (2):
<= Recv SSL data, 122 bytes (0x7a)
0000: 02 00 00 76 03 03 1d 3c 9b 52 99 6f e5 05 b7 9f ...v...<.R.o....
0010: 2b 35 94 0e 11 bd 4f 3a de e3 b8 1d d4 16 a4 45 +5....O:.......E
0020: c4 91 13 b0 18 ef 20 a4 f1 17 e2 8c 0d 6f 61 25 ...... ......oa%
...

TLS で暗号化された内容も、元の HTTP が平文で見れるので便利。

<= Recv header, 13 bytes (0xd)
0000: 48 54 54 50 2f 32 20 32 30 30 20 0d 0a          HTTP/2 200 ..
<= Recv header, 37 bytes (0x25)
0000: 64 61 74 65 3a 20 54 68 75 2c 20 31 35 20 4a 75 date: Thu, 15 Ju
0010: 6e 20 32 30 32 33 20 30 32 3a 33 37 3a 34 33 20 n 2023 02:37:43
0020: 47 4d 54 0d 0a                                  GMT..
<= Recv header, 13 bytes (0xd)
0000: 65 78 70 69 72 65 73 3a 20 2d 31 0d 0a          expires: -1..
<= Recv header, 35 bytes (0x23)
0000: 63 61 63 68 65 2d 63 6f 6e 74 72 6f 6c 3a 20 70 cache-control: p
0010: 72 69 76 61 74 65 2c 20 6d 61 78 2d 61 67 65 3d rivate, max-age=
0020: 30 0d 0a                                        0..
<= Recv header, 45 bytes (0x2d)
0000: 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 20 74 65 content-type: te
0010: 78 74 2f 68 74 6d 6c 3b 20 63 68 61 72 73 65 74 xt/html; charset
0020: 3d 49 53 4f 2d 38 38 35 39 2d 31 0d 0a          =ISO-8859-1..
<= Recv header, 245 bytes (0xf5)
0000: 63 6f 6e 74 65 6e 74 2d 73 65 63 75 72 69 74 79 content-security
0010: 2d 70 6f 6c 69 63 79 2d 72 65 70 6f 72 74 2d 6f -policy-report-o
...

eBPF の tc を使ってパケットを触ってみる開発入門資料

eBPF の tc (classifier) を使ってパケットを触ってみる話をしたので、資料を貼ります。

docs.google.com

eBPF は一度慣れるとかなり便利な仕組みで、見るべき場所が分かっていればそれなりに気軽に使うことができる。ただ、ドキュメントがだいぶ整備されてきたとはいえまだまだ最初の一歩の敷居は高いと思う。

この資料は、eBPF 開発の最初の一歩を踏み出したい人向けに作った。 eBPF を書いてデバッグし組み込むには、一体何を行う必要があってどこを見たらいいか?をハマりポイントとともに重点的に書いてみた。

Renovate で Flux 配下の Helm Chart をアップデートする

この記事は KMC アドベントカレンダー 14 日目の記事です。

adventar.org

前回の記事は id:crashrt さんでした。自分も百舌谷さん好きです!

crashrt.hatenablog.com

背景

現在、Kubernetes クラスタに入れる Manifest を Flux (Fluxcd) を使って管理している。

blog.kmc.gr.jp

Flux には Helm を管理できる機能があって、HelmRelease という Flux の CRD で Chart のバージョンや values を指定したらインストールしてくれる。

fluxcd.io

クラスタを運用するにあたっては積極的に Chart のバージョンを上げていきたいものの、いちいち Upstream repository を確認しにいくのはめんどくさい。

調べてみると依存パッケージを自動更新してくれることで有名な Renovate が Helm の更新に対応しているということで、導入してみた*1

Renovate と Flux

Renovate は Flux にしっかり対応していて、HelmRelease ファイルがあれば自動的にバージョン更新の面倒を見てくれる。

docs.renovatebot.com

ただ、更新を確認するためには HelmRelease とそのオブジェクトが参照している HelmRepository (Helm chart に対応するレポジトリの情報) の紐付けを正しく行う必要があるそうで、

  • HelmRelease / HelmRepository ともに namespace が明示的に設定されている必要がある
  • HelmRelease / HelmRepository ともに名前が一意じゃないと多分動かない

という制約がある。

ここで、現在クラスタでは Manifest の構成に Kustomize をいろいろ活用しており、

  • namepsace は上位の Kustomization で上書きするようにしている
  • HelmReelase / HelmRepository の共通パラメータは Component にまとめており、各アプリの Kustomzation でそれを上書きして namePrefix でリソース名を変更している

ため、 各 Kustomization 内の HelmRelease / HelmRepository には namespace の記述はなく、また(`namePrefix` が適用される前なので)リソース名も全て同じになっている。

そのため、 Renovate の Flux 機能をそのまま使うことはできなかった。

自前でパースする

Renovate は "Managers" でパースを行って、 パッケージ情報を "Datasource" として抽出し、その Datasource に対して更新のチェックを行う仕組みになっている。

これまでに書いていた話は Flux Manager がパースを行って Helm Datasource を HelmRelease から抽出するものだった。もし Flux Manager がうまく使えなかったとしても、他の Manager を使って Helm Datasource が抽出できれば Renovate はそれに対して更新チェックを行ってくれる。

そこで Regex Manager を使って正規表現で自前でパースするようにした。

具体的には、以下のような HelmRelease / HelmRepository が書かれたファイルに対して

---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
  name: repo
spec:
  url: https://helm.cilium.io/
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: release
spec:
  chart:
    spec:
      chart: cilium
      version: 1.12.3
  values:
    ipam:
      mode: kubernetes

以下のような正規表現を設定して Helm Datasource を抽出する*2

{
  ...
  "regexManagers": [
    {
      "fileMatch": ["^k8s/apps/.*/helm-overlay.yaml$"],
      "matchStringsStrategy": "combination",
      "matchStrings": [
        "  url: \"?(?<registryUrl>\\S+)\"?\\s*",
        "      chart: \"?(?<depName>[a-z0-9-]+)\"?\\s*",
        "      version: \"?(?<currentValue>\\S+)\"?\\s*"
      ],
      "datasourceTemplate": "helm"
    }
  ],
  ...
}

設定して数分待つと、ちゃんと Renovate が Helm を認識してくれてアップデートの PR を作ってくれた。

その他

これで Chart の更新が楽になったが、他にも Renovate を運用する上で細々としたポイントがあった。

StabilityDays

新しいバージョンが出てから一週間くらいは様子見したいので、StabilityDays を設定した。

確かに最新のバージョンが Pending になって Push されないようになった一方、1週間経過した(がすでに新しいバージョンが Publish されている)古いバージョンが Stable なものとして Push されるようになった。自分はリリースされてから1週間たっても新しいバージョンがない場合に Stable とみなしたいのであり、古いバージョンが Push されるのは意図した挙動ではない*3

軽減策として "internalChecksFilter": "none" を設定して StabilityDays に関わらず常に新しいバージョンを Push させることで古いバージョンが Suggest されることはなくなった。

StabilityDays を有効にすると GitHub PR に StabilityCheck という Status Check が追加されるようになる。最新バージョンが StabilityDays 分経過するまでこの Status が通らないので、 (Automerge を特に設定しない場合は)定期的に PR を見てステータスが緑色であれば確認してマージ、というフローを行うようになった。

StabilityDays が経過する前に新しいバージョンが出た場合は、その新しいバージョンが Push されるのでまたしばらく StabilityCheck が通らないことになる。Grafana のような高頻度で新バージョンがリリースされる Chart の場合、一生マージできなくなるという課題もある*4

Automerge

Bitnami の Chart などはアプリケーションのバージョンとは関係なく Base image が更新されるだけで新しい Chart がリリースされることがある。そのような些細な変更をいちいち確認するのはめんどくさいので、patch バージョンの変更であれば自動的にマージ(Automerge)するように設定した。

{
...
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "matchDatasources": ["helm"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    }
  ],
...
}

今のところ Renovate のマージを使っているので時間がかかるが、基本的にはうまくマージされている。 遅い問題で困ったら GitHub の Automerge を検討してもいいかもしれない。

うるさい

無限にアップデートがきてダルいという Renovate あるあるな問題もある。 Schedule を設定したら静かになると思いきや、既存の PR には Schedule に関わらず更新を送り続けるので Push 通知がうるさかった。

これに関しては、 "updateNotScheduled": false を設定することで既存ブランチの更新も Schedule 内におさめるようにすることができた。

ただ、そうすると

月曜に 1.2.1 が出る → 土曜日に更新される → (月曜に 1.2.2 が出るがブランチは更新されない) → 火曜に 8日間たって stabilityDays を通過する → 意図せず 1.2.1 がマージされる

といったロジックで古いバージョンがマージされてしまう可能性がある。その対策として、automergeScheduleschedule の直後に設定するようにして Automerge 発動を PR の更新直後に限定することにした。

Recap

最終的な Config は↓のようになった。将来的には minor も自動マージの対象とするかも。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base", ":disableDependencyDashboard"
  ],
  "timezone": "Asia/Tokyo",
  // バージョンが出てから8日間は Unstable とみなして Status check を pending にさせる
  "stabilityDays": 8,
  // Fluxcd は色々 flag をつけているので手動でアップデートしたい
  "flux": {
    "enabled": false
  },
  "regexManagers": [
    {
      "fileMatch": ["^k8s/apps/.*/helm-overlay.yaml$"],
      "matchStringsStrategy": "combination",
      "matchStrings": [
        "  url: \"?(?<registryUrl>\\S+)\"?\\s*",
        "      chart: \"?(?<depName>[a-z0-9-]+)\"?\\s*",
        "      version: \"?(?<currentValue>\\S+)\"?\\s*"
      ],
      "datasourceTemplate": "helm"
    }
  ],
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "matchDatasources": ["helm"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    }
  ],
  // StabilityDays に関わらず、最新の package 情報を PR にする。 Automerge で古いバージョンが StabilityDays を満たしてマージされるのを防ぐため
  "internalChecksFilter": "none",
  "schedule": ["before 8am on saturday"],
  "automergeSchedule": ["after 7am and before 11am on saturday"],
  // Rebase やバージョン更新も Schedule 内に行わせる
  "updateNotScheduled": false,
  "prHourlyLimit": 0,
}

Renovate と Helm の組み合わせ、なかなか便利なので使ってみてください!

*1:Dependabot は非対応だった https://github.com/dependabot/dependabot-core/issues/1744

*2:これができたのは HelmRepository と HelmRelease を同じファイルに記述するようにしていたため。別のファイルに書いて場合はどうすればいいのか知らない

*3:これはドキュメントに記載された挙動なので仕方ない

*4:実際になっている

dex と oauth2-proxy と ingress-nginx で OIDC ID token を取得して kubernetes-dashboad にログインする

Kubernetes では kubernetes-dashboard という各種リソースを見るためのダッシュボードが提供されている。

ブラウザ上で色々確認できるので便利なのだが、ログイン時は下のように自身の token か kubeconfig ファイルをアップロードする必要がある。

Kubernetes へのログインで Certificate や Token を使っている場合はそのままアップロードすればいいものの、 OIDC を設定したクラスタで OIDC 経由でログインしている場合は ID token を取得する必要があり、めんどくさい。

そこで、

  • oauth2-proxy で kubernetes 向けの OIDC ID token を dex から取得
  • ingress-nginx で ID token を Authorization header に Bearer token として付与し、 kubernetes-dashboard に送信

することでシームレスに見れるようにする。

dex

kubernetes が使っている client と同じ client id を使うので dex で新しく client を発行する必要はないが、 Callback URL を新たに登録する必要がある。

staticClients:
- id: kubernetes-client
  redirectURIs:
  - ...
  - 'https://{k8s dashboard domain}/oauth2/callback'
  ...

oauth2-proxy

oauth2-proxy は dex から id token を取得し、 ingress-nginx に Authorization Header として渡す役割を果たす。以下のような環境変数を設定すると良い。

      - name: OAUTH2_PROXY_PROVIDER
        value: oidc
      - name: OAUTH2_PROXY_OIDC_ISSUER_URL
        value: {OIDC issuer url}
      - name: OAUTH2_PROXY_COOKIE_DOMAINS
        value: {k8s dashboard domain}
      - name: OAUTH2_PROXY_WHITELIST_DOMAINS
        value:  {k8s dashboard domain}
      # Re-validate OAuth token per 1 hour
      - name: OAUTH2_PROXY_COOKIE_REFRESH
        value: 9m # id token の期限より短くする
      - name: OAUTH2_PROXY_SCOPE
        value: "openid profile groups offline_access email" # offline_access は refresh token の取得に使う。 email は oauth2-proxy の動作に必須
      - name: OAUTH2_PROXY_SET_AUTHORIZATION_HEADER
        value: "true"

ポイントは OAUTH2_PROXY_SET_AUTHORIZATION_HEADER で、これを true にすることで id token を Authorization Header として ingress-nginx に渡すことができる。

ingress-nginx

ingress-nginx は oauth2-proxy にリダイレクト(& reverse proxy)して Authorization Header を取得し、それを付けた上で kubernetes-dashboard に reverse proxy する。

構成としては、oauth2-proxy に reverse proxy するための認証がない Ingress と、kubernetes-dashboard に reverse proxy するための auth_request 付き Ingress を用意することになる。

oauth2-proxy 用 Ingress

oauth2-proxy の認証まわりの endpoint である /oauth2 配下を oauth2-proxy の service にわたす。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  rules:
  - host: {k8s dashboard host}
    http:
      paths:
      - path: /oauth2/
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard-oauth2-proxy
            port:
              number: 80

kubernetes-dashboardIngress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/auth-response-headers: "Authorization"
    nginx.ingress.kubernetes.io/auth-url: "https://{k8s dashboard host}/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://{k8s dashboard host}/oauth2/start?rd=$scheme://$host$escaped_request_uri"
spec:
  rules:
  - host: kubernetes-dashboard.kmc.gr.jp
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard
            port:
              number: 443

ポイントは nginx.ingress.kubernetes.io/auth-response-headers: "Authorization" を設定することで、これで oauth2-proxy から来た Authorization Header を Request Header に設定して kubernetes-dashboard にリクエストを送ってくれる。

これで、 kuberentes-dashboard へのリクエストに OIDC で認証された kubernetes において valid な Authorization Header が乗るようになり、ユーザーの権限で Kubenetes dashboard が閲覧・編集できるようになった*1

*1:最初の Token を求める画面は出なくなる

Filebeat の Nginx module でアクセス先パスを正規化する

今回の ISUCON では Filebeat を使って Nginx のログを Elasticsearch に放り込み、 Kibana ダッシュボードで表示していた。

nonylene.hatenablog.jp

なかなか役に立ってよかったものの、 /user/:userID/home のようなパスだと userID に様々な値が入ってしまい、総計では時間がかかっていても上位としてダッシュボードに出てこないという課題があった。

/user/4372904890324/home のようなパスに対しては /user/:userID/home といった正規化したフィールドを作ってから Elasticsearch に保存するようにしたい。

Ingest pipeline をいじる

Filebeat の nginx module ではパース処理は基本的に Elasticsearch の Ingest pipeline で行っており、 URL の情報は Ingest pipeline 側で初めてパースして見れるようになる。そのため、 URL に関した処理を行いたい場合は Ingest pipeline 内の URL パース処理の後に追加する必要がある(ずっと Filebeat 側に Processor を追加しようとして本番中も感想戦でもハマっていた…)。

Filebeat 関係の Ingest Pipeline は Filebeat が直接 Elasticsearch に登録するようになっており(filebeat setup --pipelines コマンドで可能)、その設定は /usr/share/filebeat/module/nginx/access/ingest/pipeline.yml にある。

このファイルには適用する Ingest pipeline の Processor (Filebeat の Processor とはまた別!)の設定が並んでおり、その末尾に正規化関係の処理を追加すれば良い。

...
processors:
...
- set:
    copy_from: url.path
    field: url_path_normalized
- gsub:
    field: url_path_normalized
    pattern: "^/user/[a-zA-Z0-9-_]+/gacha/index$"
    replacement: "/user/:userID/gacha/index"
... (以降正規化したいパスを記載していく)

編集したら filebeat setup --pipelines コマンドで Elasticsearch に Ingest pipeline を再登録する。登録された様子は Kibana の Management でも確認できる。

これで正規化後のパスが url_path_normalized フィールドに追加され、 /user/ 配下の API に時間がかかっていることが一目で分かるようになった。

めでたし。

ISUCON12 本選に出場した (:old_noto_innocent: Team)

ISUCON 12 本選に出場した。結果は 167492 点で 7 位(結果修正後)。そこそこかな?

isucon.net

アプリケーション周りは utgw の記事を見ると分かりやすいです

blog.utgw.net

予選の記事は↓。ブログ賞受賞とのことで、個人スポンサーグッズをもらえるらしい。ありがとうございます!ありがとうございます!

nonylene.hatenablog.jp

やったこと

  • いつものサーバー周り
  • Elastic Stack 関係いろいろ

    • nginx のパスをいい感じに aggreate して表示しようとしたが、競技中は無理だったので諦めた
      • (nginx のログをパースする) ingest pipeline が Elasticseach で処理されるのに気づいておらず、ひたすら Filebeat 側の処理を追加していた
    • Filebeat が Elasticsearch に送らなくなったので全てを再構築した
      • 結局 Filebeat 周りの設定ミスだったりファイルをうまくつかんでいなかったりが原因だった
    • 感想戦では MySQL 周りをブラッシュアップ nonylene.hatenablog.jp
  • バグ探しやレビュー

    • nginx のパス aggregate を諦めた後はサーバー側でやることがなくなったのでアプリを見ていた
    • utgw の記事で言及されている通り、確率的に落ちる不具合が終盤で発生していた
      • はじめは uid が int32 の範囲を超えたときに起きるベンチマーカー or アプリケーションのバグかと疑っていたが、結論としては DB を分割したことに起因して invalid な session が指定された時の挙動が 403 -> 401 に 2/3 の確率で変化したことが原因だった
      • Cloud Trace を眺めることで、エラーとなっているリクエストがだいたいどこまで動いているのかを把握できたことで、競技中のバグ発見につながったと思う。入れててよかったトレーシング
      • そして爆速修正してくれたチームメイトに感謝

感想

今回は5台あったのでどうなることやらと思ったが、結果的にはいつもの構成とそこまで変わらなかった。考慮する変数が多いのはサーバー担当としてはやりがいがあった。

予選でうまくいった Elastic Stack は、新しく nginx のパスの aggregate を行おうとして失敗したり、 MySQL のクエリが大きすぎてうまくハンドリングできないなど、いくつか課題があった。試行時間も割りと使ってしまったし、その時間があれば上位に行けたかもしれない。次回に向けてブラッシュアップしていきたい。

最近はほとんどアプリを見れていなかったけど、今回は割りとコードを読んでレビューしたり挙動確認したりできた。チームの最終スコアにもつながったのでよかった。

ただ様々な賞に一つも引っかからなかったのはくやしい!次回も頑張りましょう。

今年も ISUCON 楽しかったです。やはり本選は予選で得た反省や知見をすぐ試せるといった点でもいいですね。個人的にも興味のある新しい技術を触るいい機会になりました。チームメイトや運営のみなさん、ありがとうございました!