shibatch's journey

日々考えていることをつらつら書くだけです

きみもAWS LoadBalancer Controller芸人になろう

AWSKubernetesingressコントローラーとしてAWS LoadbBanacer Controllerがあります。 これ、使うとALBが自動で起動されて使うことができます。つまり、ALBの仕様に完全に依存する形のIngress Controllerです。

ALBは高機能な反面、ingressとして使うと少々クセがある感じがしたので覚え書きとしてまとめていきます。 仕様に制限があるのでその制限を回避する方法も載せていきます。

なお、現時点での最新版、v2.7での情報です。

1. リスナールールを「条件」「アクション」に分けてアノテーションで記載する

ALBにはリスナールールという仕組みがあり、AWS LoadBalancer Controllerではこの仕組みを制御できます。

Application Load Balancer のリスナールール - Elastic Load Balancing

リスナールールはアクセスを「条件」と「アクション」に分けて記述する機能です。 AWS LoadBalancer Controllerを使うとIngressでこのリスナールールを表現することができます。

条件

条件はalb.ingress.kubernetes.io/conditions.${conditions-name} で、ここにSourceIPやらリクエストメソッドはGETやらの条件をつけることができます。

アクション

アクションはalb.ingress.kubernetes.io/actions.${action-name}で、ここにリダイレクトするなり、特定のKuernetes のservice resourceに転送するなりのアクションをつけることができます。

これは実際におこなった設定なのですが「password変更のRESTful APIを塞ぎたい」場合、以下のようなアノテーションingressに設定することになります。

    alb.ingress.kubernetes.io/conditions.deny-password-login: >
      [{"Field":"http-request-method", "httpRequestMethodConfig":{"Values":["POST"]}}]
    alb.ingress.kubernetes.io/actions.deny-password-login: >
      {"Type": "fixed-response", "fixedResponseConfig":{"statusCode":"403"}}

conditionでPOST、actionで403のレスポンスを返す、としています。conditionとactionで名前を合わせることで一対の設定となります。 これをingressのruleに設定します。以下のようにすれば、example.com/api/session に対して上記のアノテーションを設定することができます。

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login  👈annotationで設定した名前を指定する
            port:
              name: use-annotation
        path: /api/session
        pathType: Exact

2.「条件」の個数制限に注意する

ALBの仕様として、「条件」は5つまでしか設定できません。

Application Load Balancer のクォータ - Elastic Load Balancing

この個数を超えるとingressを設定してもALBには設定は反映されません。AWS LoadbBanacer Controllerのログにはエラーが出力されますが、Ingress ControllerのPodやIngressは見かけ上異常なく見えるので注意が必要です。

3. pathTypeの仕様に注意する

IngressではpathTypeによって完全一致か前方一致かを指定できます。

Ingress | Kubernetes

  • ImplementationSpecific(実装に特有): このパスタイプでは、パスとの一致はIngressClassに依存します。Ingressの実装はこれを独立したpathTypeと扱うことも、PrefixExactと同一のパスタイプと扱うこともできます。
  • Exact: 大文字小文字を区別して完全に一致するURLパスと一致します。
  • Prefix: /で分割されたURLと前方一致で一致します。大文字小文字は区別され、パスの要素対要素で比較されます。パス要素は/で分割されたパスの中のラベルのリストを参照します。リクエストがパス p に一致するのは、Ingressのパス p がリクエストパス p と要素単位で前方一致する場合です。

簡単に言えばExactは完全一致、Prefixは前方一致で、AWS LoadbBanacer Controllerを使った場合でもその仕様は変わりません。

ただし、pathTypeはALBの「条件」に設定されるため、前述の個数制限は留意する必要があります。

たとえば、先ほど出した例は Exactを使いましたが、

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login 
            port:
              name: use-annotation
        path: /api/session
        pathType: Exact  👈これね

この場合は自動的に2つ「条件」を使うことになります。

  • HTTPホストヘッダーが example.com
  • パスパターンが/api/session

そのため、他の条件(リクエストメソッドやソースIPなど)は3つつけることができます。

さて、Prefixはどうなるのでしょう?

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login 
            port:
              name: use-annotation
        path: /api/session
        pathType: Prefix

この場合は3つ「条件」を使います。

  • HTTPホストヘッダーが example.com
  • パスパターンが/api/session
  • パスパターンが/api/session/*

そのため、他の条件(リクエストメソッドやソースIPなど)は2つになります。

いちばんわかりにくいImplementationSpecificの場合はどうなるのでしょうか? これは指定したそのままがALBに設定されます。

        path: /api/session
        pathType: ImplementationSpecific

↑たとえばこの場合はExactと同じように、2つ「条件」を使うことになります。example.com/api/session には適用されますが example.com/api/session/hoge には適用されない設定です。

        path: /api/session/*
        pathType: ImplementationSpecific

これはExactでもPrefixでもない指定になって、2つ「条件」を使うことになります。 example.com/api/session には適用されず、example.com/api/session/hoge には適用されます。

こういう感じなので、AWS LoadbBanacer Controller を使う場合は設定が思った通りに反映されているかALBを確認しながらやると良いです。

4. 制限は回避できる

ここまで条件の指定は5つまでしかない、ということを口すっぱく言ってきましたが、この仕様は回避することができます。 たとえばソースIPをたくさん条件に指定したい場合は以下のように複数アノテーションを作ればよいです。

    alb.ingress.kubernetes.io/actions.allow-from-certain-ips1: >
      {"Type": "forward", "ForwardConfig": {"TargetGroups": [{"ServiceName": "exampleservice", "ServicePort": "80"}]}}
    alb.ingress.kubernetes.io/conditions.allow-from-certain-ips1: >
      [{"Field":"source-ip", "sourceIpConfig": {"values":["8.8.4.0/24", "8.8.8.0/24", "8.34.208.0/20"]}}]
    alb.ingress.kubernetes.io/actions.allow-from-certain-ips2: >
      {"Type": "forward", "ForwardConfig": {"TargetGroups": [{"ServiceName": "exampleservice", "ServicePort": "80"}]}}
    alb.ingress.kubernetes.io/conditions.allow-from-certain-ips2: >
      [{"Field":"source-ip", "sourceIpConfig": {"values":["8.35.192.0/20", "23.236.48.0/20", "23.251.128.0/19"]}}]
  - host: example.jp
    http:
      paths:
      - backend:
          service:
            name: allow-from-certain-ips1
            port: 
              name: use-annotation
        path: /example/*
        pathType: ImplementationSpecific
  - host: example.jp
    http:
      paths:
      - backend:
          service:
            name: allow-from-certain-ips2
            port: 
              name: use-annotation
        path: /example/*
        pathType: ImplementationSpecific

これは特定のソースIPからexample.jp/example/*にアクセスした場合はそのままexampleserviceに通すことを意味しています。


どうでしょう?AWS LoadBalancer Controllerの深淵を少しは覗くことができたでしょうか??

なんだか難しそうには見えますが、慣れてしまうと割と高機能なALBの機能を活用できるので便利です。

誰か困っている人に届きますように……

2023年ふりかえり

今年を雑に振り返りたい

今年、業務面を振り返ると自律して動けるようになって信頼を積み上げることができたという感じ。去年まではなんだかんだ「人の歩いてきた道」をなぞるところもあったと思うが今年は自社内の課題を見出しそれに対して解決していく動きが強くできたと思う。反面ちょっと技術力でなんとかする、みたいな動きにはちょっと乏しかったかもしれない。

組織やチームがダイナミックに変わった年でもあった。退職した人もいるし新しく入った人もいるし、今まであまり話さなかった人とも話すようになった。社内にいながら変化を感じられるのは刺激になるしとても良い。あと6年経つとチーム内でもだいぶ古株になってきているところにちょっと戸惑いがある。でもそれも信頼を積み上げてこられた証拠なのかもしれない。

あまり年の初めに抱負を立てることはないのだけれど、来年はもうちょっと技術力で〜した、という場面が増えると良い。登壇する機会も増やせたらな

今年も良い方々に恵まれました。来年もよろしくお願いいたします。

Herokuのデータベースを(Kubernetes Podを経由して)Amazon Auroraにデータ移行してみた

この記事はGMOペパボエンジニア Advent Calendar 2023の10日目になります🎄

最近PaaSであるHerokuにあるサービスをAWS(Amazon EKS on Fargate)で動かそうとしています。まだ検証段階ではあるものの、動作するところまではこぎつけました。HerokuとAWSを以下の組み合わせで移設しようとしています。

どれもいざやってみるとさほど困難さはなかったのですが(最初にEKSを構築してIngressを動かすほうがよほど手間がかかった)、Herokuで採用しているデータベースであるPostgreSQLに関しては知識ゼロから始めたもので学びが多かったのでやったことを書いていきます。雰囲気だけでも感じていただけたら嬉しいです!

なお、移行して動いた、という段階なのでパフォーマンスがどうなったといった視点は今回はないです…ご了承ください。

Auroraの構築については省略します😃他にも解説記事はたくさんあるかと思いますので

下準備

※私は作業PCがMacなのでMacの場合です

Herokuでデータベースのバックアップを取得するためにはHerokuのコマンドを実行することになるのですが、内部的にPostgreSQLCLIコマンド─psql─をKickするため、最初にpsqlコマンドを利用できるようにする必要があります。psqlコマンドを使うためには以下からHerokuに構築しているPostgreSQLのバージョンに合ったものをインストールします。

postgresapp.com

インストールが終わったら、psqlコマンドを使うためにパスを通してあげます。自分は雑にzshrcに以下を入れました。

% tail -3 ~/.zshrc

# psql PATH
export PATH=${PATH}:/Applications/Postgres.app/Contents/Versions/latest/bin

psqlコマンドが打てるようになったら準備完了です😎

% which psql
/Applications/Postgres.app/Contents/Versions/latest/bin/psql

Heroku PostgreSQLのデータベースを覗いてみる

ではHerokuのPostgreSQLをちょこっと覗いてみましょう。ログインはheroku pg:psql です。オプションを指定しない場合はHerokuでサービスが参照しているデータベースの所有者ユーザーになります。

% heroku pg:psql
--> Connecting to postgresql-example-98765
psql (13.13, server 13.12 (Ubuntu 13.12-1.pgdg20.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

heroku-database::DATABASE=>

PostgreSQLではメタコマンドと言って短いコマンドでデータベースの内容をわかりやすく出力してくれる仕組みがあります。データベース一覧とその所有者を表示する\lを打ってみます。

heroku-database::DATABASE=> \l
                                                     List of databases
      Name      |     Owner      | Encoding |   Collate   |    Ctype    |                Access privileges
----------------+----------------+----------+-------------+-------------+--------------------------------------------------
 ma72h7wq4Z4uhlk | bkreikhp6iwaly | UTF8     | en_US.UTF-8 | en_US.UTF-8 | bkreikhp6iwaly=CTc/bkreikhp6iwaly               +
                |                |          |             |             | internal_utility_u1jk56dcsvf7q8=c/bkreikhp6iwaly
 postgres       | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | postgres=CTc/postgres
 template0      | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres                                     +
                |                |          |             |             | postgres=CTc/postgres
 template1      | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | postgres=CTc/postgres
(4 rows)

※データベース名、ユーザー名は架空のものです

いちばん上に表示されているma72h7wq4Z4uhlkが実際に使用しているデータベース、つまり今回データ移行するものです。HerokuのPostgreSQLのデータベース名はランダム文字列なんですね。右隣に表示されているOwnerの名前もランダム文字列です。 このデータベース名は後ほど使うので控えておきます。

バックアップ(スナップショット)をとる

それではリストア元のデータベースのバックアップを取得します。一般的な話になるのですが、データベースのバックアップをバックアップは2種類に大別できまして、コールドバックアップとホットバックアップがあります。コールドバックアップはデータベースのソフトウェア(今回の場合はPostgreSQL)を停止させてデータのバックアップを取得するものですが、PaaSであるHerokuはコールドバックアップは想定していないようです。

そこでホットバックアップを取得するのですが、これはとても簡単で以下のコマンドを実行するだけです。

% heroku pg:backups:capture

これは内部的にはPostgreSQLpg_dump​コマンドを実行し、先ほど確認したma72h7wq4Z4uhlkデータベースの論理バックアップを取得しています。バックアップファイルはHeroku内に保管されます。

コマンド実行後Webコンソールでみても取得されていることがわかります。

Heroku backup

※今回の場合は2MBと極小のデータですが、20GB以上のデータだとタイムアウトする可能性があるからデータベースのブランチ(耳慣れない表現ですがデータベースの書き込みがないレプリカ)を作成することを推奨しています。

バックアップファイルを取得する

Herokuに格納されたバックアップデータをローカルPCにダウンロードします。出力ファイル名を指定しない場合はlatest.dumpという名前のファイルがダウンロードされます。

% heroku pg:backups:download
Getting backup from ⬢... done, #1
Downloading latest.dump... ████████████████████████▏  100% 00:00 1.94MB
% ls -l latest.dump
-rw-r--r--  1 shibatch  staff  2030927 11 29 19:04 latest.dump
shibatch@PM-GMXJX5060K sandbox-eks % ls -lh latest.dump

リストア用のPodを立てる

今回、EKSとAuroraはサブネットを分けている構成にしています。

構成図
今回はEKSにHerokuのWeb Dynosに相当するPodを構築するため、EKSのあるサブネットからAuroraのあるサブネットに疎通する必要があります。

そこで疎通のテストも兼ねて、EKS上にAurora(PostgreSQL)をCLI操作するためのPodを立てて、そのPodにバックアップデータを転送してリストアすることにしました。 EKSに以下の通り、PostgreSQLが使えるPodを立てます。このコンテナイメージは内部でPostgreSQLを起動するためパスワードの環境変数指定が必要ですが、今回psqlコマンドを打ちたいだけなのでパスワードはなんでもよいです。

% kubectl run postgres-cli --image=postgres:13-alpine --env="POSTGRES_PASSWORD=test" 
% kubectl get pod
NAME            READY   STATUS    RESTARTS   AGE
postgres-cli   1/1     Running   0          2m49s

Podにリストアしたデータを転送する

KubernetesにはPodに対してファイルを転送するコマンドがあるので今回はそのコマンドで転送します(こんなコマンドあるの知らなかった)。 転送完了したらPodにログインしてファイルの存在確認します。

% kubectl cp ./latest.dump postgres-cli:/tmp/sandbox-pg.dump
% k exec -it postgres-cli -- /bin/bash
postgres-cli:/# ls -l /tmp
total 1984
-rw-r--r--    1 502      dialout    2030927 Nov 29 10:12 sandbox-pg.dump

カンのいい読者なら察したでしょうが、このコピーは検証用データベースの2MBのバックアップデータだからシュッとできたものの、本番用のGB単位のデータだったら転送に時間がかかる&Podのtmp領域の空き容量の関係でこんなうまくいかない可能性があります。この問題はこれから考えます😃

Auroraにデータベースのあれこれを作成する

あとはリストアしていくだけ…と言いたいところですが、Auroraにあらかじめリストアするデータベースを作成しておかないといけないようでした。 まずはログインしたPodを踏み台にしてAuroraのプライマリインスタンスにログインします。初回ログインではスーパーユーザーになるでしょう。下記ではデフォルトのpostgresユーザーとしています。

postgres-cli:/# psql --host=aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com --port=5432 --username=postgres --password -d postgres
Password:
psql (13.13, server 13.12)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

とにもかくにもHeroku PostgreSQLにあったデータベース名でデータベースを作ってみます。

postgres=> CREATE DATABASE ma72h7wq4Z4uhlk;
CREATE DATABASE
postgres=> \l
                                                List of databases
      Name      |      Owner       | Encoding |   Collate   |    Ctype    |           Access privileges
----------------+------------------+----------+-------------+-------------+---------------------------------------
 ma72h7wq4Z4uhlk | bkreikhp6iwaly | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
<snip>

スーパーユーザーで運用するのはセキュリティ上望ましくないので、運用のためのロールとユーザーを作成します。 まずはroleから。今回はappというロール名にしました。

postgres=> CREATE ROLE app;
CREATE ROLE

作成したapproleにデータベースへのアクセス権を付与していきます。

postgres=> GRANT CONNECT ON DATABASE ma72h7wq4Z4uhlk TO app;
GRANT
postgres=> GRANT ALL PRIVILEGES ON DATABASE ma72h7wq4Z4uhlk TO app;
GRANT

ログインユーザーを作成します。今回はapp_userとしました。

postgres=> CREATE ROLE app_user LOGIN PASSWORD 'なにがし';
CREATE ROLE

app_userapproleの権限を付与します。

postgres=> GRANT app TO app_user;
GRANT ROLE

一旦postgresのコンソールを抜けて、ma72h7wq4Z4uhlk databaseへログインしなおします。

postgres-cli:/# psql --host=aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com --port=5432 --username=postgres --password -d ma72h7wq4Z4uhlk

app roleに対して、publicスキーマのテーブル、シーケンスに対する権限を付与します。

ma72h7wq4Z4uhlk=> GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app;
GRANT
ma72h7wq4Z4uhlk=> GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app;
GRANT
ma72h7wq4Z4uhlk=> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO app;
GRANT

リストアを実施する

Podログインした状態からバックアップファイルを指定してリストアします!リストア先は先ほど作成したデータベース名です。

postgres-test:/# pg_restore --verbose --clean --no-acl --no-owner -h aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com -U postgres -d ma72h7wq4Z4uhlk /tmp/sandbox-pg.dump
<snip>

これでリストアは完了ですね!

この状態でPodのコンテナに設定するプライマリデータベース接続用の環境変数(Herokuからそのまま使う場合はDATABASE_URL)をAuroraのものに変更するとちゃんと接続してPodがRunningになってくれました。

DATABASE_URL:  postgres://app_user:password@aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com:5432/ma72h7wq4Z4uhlk primary

今後の課題

というわけで、これでHeroku PostgreSQL→Auroraにデータ移行できたよ〜というものを紹介してみました。ここにある手順ですがまだ荒削りなところがあります。具体的には先ほど言及したもっと大きなサイズのデータ移行の場合どうするかということと、あと移行後のデータの完全性をちゃんと知りたいなぁと感じており、今後の課題です。

意外にすんなりできた、というのが感想です。本気でデータ移行する場合は書き込みできない状態にして実施することになるなどもっと細かい点を詰めなければなりませんが、何かしらの参考になれば幸いです😃

メンテナンスしやすいTerraformコードを書くために気をつけていること

TerraformというIaCのためのツールがあります。割とディレクトリ構成から管理の仕方までいろいろと自由がきくツールであります。

最近これでAWSVPCやEKS、RDS、ElastiCacheをコードにしたのだけれど、個人的に今まで他の方が書いたコードを読み解いたりもしてきた経験から、なんとなく「こう書いた方が他人には優しいだろう」「将来的なメンテナンスコストが抑えられるだろう」と思いつつ書いていることに気づいたので、自分がどういうことに気を付けているかを書き留めてみます。   …といってもそんなに奇をてらったようなテクニックを使っているわけではないので(むしろそういうことをするとメンテナンスしにくくなるので🙅‍♀️)そんなの常識じゃん、みたいなこともありそうだけれどとりあえず書いてみます。

1. module化は避ける

いきなり好みが分かれそうですが、自分がTerraformでコードを書くときはmodule化しません。module化は便利ですがTerraformのコードの階層が深くなりがちで、その分何が生成されるのかが他人から見たときに直感的に把握しにくくなると感じています。共通化はworkspaceの使用、for_eachやforを使った利用に極力とどめておくべきです。

2.tfstateはなるべく小さく

tfstateを小さくすることは何個かメリットがあります。ひとつはterraform plan / applyの実行時間が短くなること。もうひとつは複数人で作業していても更新した箇所とは無関係な箇所でterraformの差分が現れてこれ何だろう〜で時間を食うようなこと(これが本当によくある…)が防げます。 どの程度の範囲で単位にするのが良さそうかというと例えばRDSだとそのRDSを使うためのIAMやSGまで、VPCだとSubnetとInternetGatewayとNatGatewayくらいまでくらいが妥当じゃないでしょうか。

3.workspace、for_eachの積極活用

環境を分けるときはworkspaceを使う、複数台作成するときはfor_eachを使う。これで大体の場合は対応できるというか、逆にこの方法でコードにできないような構成を避ける、環境の方をTerraformで管理しやすい方に寄せるまであります。

RDS(Aurora)を例にとると、

resource "aws_rds_cluster" "main_db" {
  cluster_identifier              = "main_db-${terraform.workspace}"
  engine                          = "aurora-postgresql"
  engine_version                  = var.main_db_engine_version
  db_subnet_group_name            = aws_db_subnet_group.main.name
  vpc_security_group_ids          = [aws_security_group.main_db.id]
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main_db.name
}

resource "aws_rds_cluster_instance" "main_db" {
  for_each           = var.main_db_instances
  identifier         = "${each.key}-${each.value.az}"
  cluster_identifier = aws_rds_cluster.main_db.id
  instance_class     = each.value.instance_class
  engine             = "aurora-postgresql"
  availability_zone  = each.value.az
  promotion_tier     = each.value.promotion_tier
}

このようにworkspaceでクラスタ名を分けつつfor_eachを使ってクラスタインスタンスをそれぞれ設定できるようにします。 workspace(環境)によって分けるtfvarsは以下のようにします。

# staging
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1c"
    instance_class = "db.t4g.medium"
    promotion_tier  = 0
  }
}
# production
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user02" = {
    az = "ap-northeast-1c"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user03" = {
    az = "ap-northeast-1d"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "internal01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.large"
    promotion_tier  = 100 //writerの対象にしない
  }
}

こうするとstagingはインスタンス1台だけ、productionは4台作って1台はフェイルオーバ対象から除外する… といったことが柔軟にできますね!1台だけインスタンスクラスを変える、といったこともできます。

4. 作成後にterraform importよりは構築時からTerraformで作成する

めんどくさくてWebコンソールから作成してからコードにしがちですが、構築時からTerraform使ったほうがよいと考えてます。Terraform ImportだとImport漏れが起こりがちで、コードから同じ環境がきちんと再現できるかが微妙に怪しいからです。せっかくコード化するならちゃんと運用時に使えるコードにしたいものです。

こんなところでしょうか?ほかにも無意識にやってることはありそうだけれども思いつく限りでこのあたりは気を付けて使ってます。TerraformのコツはやりたいことをTerraformに落とし込むよりTerraformコードにしやすい構成にすることだと思っております……ではでは

Datadogのログ管理機能のコストを削減する

Datadogにはログ管理という機能があり、ログを一挙に取得してそれを良い感じに判別する機能を持っています。 ただDatadogはAPMが優秀でよく使うのですが、その分ログ管理機能は(自分は)あまり使っていなく、その割に全ログ取得するとなるとかなりコストがかかっている状態でした。

そこでDatadogのログ監視は基本的には使わず、何か障害があったときに参考にする程度でよいかな、と思い、Agentが取得するログをerr / warn / crit とつくものだけに限定することにしました。 ドキュメントにも方法は載っているのですがAgent側でDD_LOGS_CONFIG_PROCESSING_RULESを使うとできます。

こんな感じ。

- name: DD_LOGS_CONFIG_PROCESSING_RULES
  value: '[{"type":"include_at_match","name":"include_error_warn_critical_logs","pattern":"(?i)(err|warn|crit)"}]'

これで大文字小文字関係なくerr / warn / critとつくログのみを収集するようになります。 実際やってみると取得されるログが激減して良い感じ。

Datadog

もともとのDatadogの思想とはそぐわないかもしれませんが、そんなこと言ったって無駄なログまで取得されて課金されていくのをみすみす見逃していくわけにはいかなかったのです。次の支払い明細が楽しみ。ではでは。

KubernetesでRedisを構築してわかったこと/検証試験をしてわかったこと

KubernetesでRedisを構築した。

構成決定まで紆余曲折があったのだけど、それは置いておくとして、最終的には以下の素朴な構成に落ち着いた。

Redis構成

StatefulSetで4Replicasで構成する。それぞれのRedisはStandaloneで動くので、メモリ情報はPod間で共有しない。今回の要件では特に共有は不要だったのでこのようなシンプルな構成にした。

それぞれのPodがPersistentVolumeClaim(PVC)をマウントする。ApplicationはRedisへはService Resourceを介してアクセスする。なお、メモリの容量は1GiBにした。

StatefulSetを構築してわかったこと

StatefulSetの構築は初めてだったのだけれど、ひとつ新しく知ったのはStatefulSetごとに別のPVCをマウントする場合、StatefulSetの中にvolumeClaimTemplatesというパラメータを設定すると1対1でマウントするPVCを自動で作ってくれる。しかもPVCのResourceを別途作成する必要もない。こんな感じ。

  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
      storageClassName: volume-storage-class

初めて知ったのだけれどめちゃくちゃ便利だ。 ref: StatefulSet | Kubernetes

これで以下のような構成になる。

% k get statefulset -n redis
NAME    READY   AGE
redis   4/4     2d18h
% k get pod -n redis
NAME      READY   STATUS    RESTARTS   AGE
redis-0   1/1     Running   0          19h
redis-1   1/1     Running   0          19h
redis-2   1/1     Running   0          19h
redis-3   1/1     Running   0          19h
% k get pvc -n redis
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
data-redis-0   Bound    pvc-8a2080c6-edb5-4c25-8366-7ea8d4f958f2   1Gi        RWO            volume-storage-class   2d18h
data-redis-1   Bound    pvc-5af5bad2-2b84-49bc-9b63-9368aaea9648   1Gi        RWO            volume-storage-class   2d18h
data-redis-2   Bound    pvc-17ad4657-bd7d-4f4a-a7d9-c2da90a01423   1Gi        RWO            volume-storage-class   2d18h
data-redis-3   Bound    pvc-7b797afe-570f-4e19-9ebc-da30372aedee   1Gi        RWO            volume-storage-class   2d18h

Redisのパラメータ調整

StatefulSetでRedisが起動した後、設定をながめているとデフォルト値ではまずいな、と思ったことがひとつあった。

redis-svc.redis.svc.cluster.local:6379> INFO Memory
<snip>
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
<snip>

これはつまりメモリの格納量が飽和するとそれ以上格納できなくなることを示している。ref: Key eviction | Redis

なのでメモリのサイズを使い切ったら使用頻度の低いキーを削除するポリシーに変えた。

apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
  namespace: redis
data:
  redis.conf: |
    maxmemory 1gb
    maxmemory-policy allkeys-lfu

メモリ限界検証の試験をした

  • StatefulSetのリソース設定でメモリのLimitを1Giにした
  • Redisの設定で使えるメモリも1GBとした
  • 使われないキーから削除する

これらの設定でちゃんと動作するのかが不安だったのでメモリ限界検証の試験をした。

RedisのPodに向けて、キーを格納しまくる。(DEBUGコマンドがあるようだがkeyをsetするほうがしっくりくるのでシェルで回した)

%  k exec -it redis-pod -- /bin/bash
root@redis-pod:/data# for i in {1..1000000}; do redis-cli -h 10.233.134.136 set test$i "$(for l in {1..10000}; do echo 1234567890;done)" > /dev/null; done

これを実施してしばらくしたら、Redisのメモリが1024Mで頭打ちになった。

root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human
used_memory_human:1023.81M
root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human
used_memory_human:1023.83M
root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human
used_memory_human:1023.86M

キーの数は減っていくような動作に見えた

root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace
# Keyspace
db0:keys=123803,expires=0,avg_ttl=0
root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace
# Keyspace
db0:keys=123541,expires=0,avg_ttl=0
root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace
# Keyspace
db0:keys=123280,expires=0,avg_ttl=0

evicted_keysはガンガン増えていったので、消されていっていることがわかる

root@redis-pod:/data# redis-cli -h 10.233.134.136 info stats | grep evicted_keys
evicted_keys:83580

Podをtopでみるとこちらも1024Miで頭打ちになっていた。

2023年 4月21日 金曜日 15時55分40秒 JST
NAME      CPU(cores)   MEMORY(bytes)
redis-2   57m          1023Mi
2023年 4月21日 金曜日 15時55分45秒 JST
NAME      CPU(cores)   MEMORY(bytes)
redis-2   57m          1023Mi

この状態で新規にキーバリューを格納したら正常に動作することを確認できた。

root@redis-pod:/data# redis-cli -h 10.233.134.136 set check_testdata1 "check_testdata_put1"
OK
root@redis-pod:/data# redis-cli -h 10.233.134.136 get check_testdata1
"check_testdata_put1"

検証してわかったこと

Redisはオンメモリデータベースなので、Podのメモリ使用量もRedisで設定するmaxmemoryの値は使うことを意識しないといけない。 つまり、

  • RedisでStatefulSetを構築するときはメモリのリソース設定が必須。1Gi使いたい場合はrequest / limitともに1Gi以上の値を入れる必要がある。
    • 自分の場合はRedisのmaxmemory、StatefulSetのRequest、StatefulSetのLimitをすべて1Giに合わせた。これでちゃんと動いてそうだったので。
  • maxmemory-policy はちゃんと設定しましょう

ということがわかった。

なお、CPUリソースの設定値がまだよくわからないのでLimitだけ多めに設定して、あとはVPAを入れて運用しながら調整することにした。

参考までに、自分がつくったyamlファイルを置いておく。

redis yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
  namespace: redis
data:
  redis.conf: |
    maxmemory 1gb
    maxmemory-policy allkeys-lfu
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
  namespace: redis
spec:
  serviceName: redis
  replicas: 4
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      nodeSelector:
        storage: redis-storage
      tolerations:
      - key: "type"
        operator: "Equal"
        value: "redis-storage"
        effect: "NoExecute"
      containers:
      - name: redis
        image: redis:7.0.11
        command: ["redis-server", "/redis-standalone/redis.conf"]
        resources:
          requests:
            memory: "1Gi"
          limits:
            memory: "1Gi"
            cpu: "1"
        env:
        - name: ALLOW_EMPTY_PASSWORD
          value: "yes"
        volumeMounts:
        - name: data
          mountPath: /data
        - name: config
          mountPath: /redis-standalone
      volumes:
      - name: config
        configMap:
          name: redis-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
      storageClassName: volume-storage-class
---
apiVersion: v1
kind: Service
metadata:
  name: redis-svc
  namespace: redis
spec:
  ports:
  - port: 6379
    targetPort: 6379
    name: redis
  clusterIP: None
  selector:
    app: redis
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: redis
  namespace: redis
spec:
  targetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: redis
  updatePolicy:
    updateMode: "Off"

CloudFrontでTLS終端「だけ」の設定をする

あまりないケースだとは思うのですが、TLS終端を自社の管理内でやって、TLS終端したアクセスをSaaSにリクエストを転送する、ということが必要になったので実施したことを書きます。

※いやそんな場合普通ないでしょ、と思うでしょうが具体的な例として、SaaSでメルマガのURLのクリックカウントサービスを使っていて、その相手先のサーバでのTLS終端のために認証鍵を受け渡しするという煩雑な作業があり、そんなことはやめて自社でTLS終端して完結したい〜という場合があったのです…

TLS終端のやり方は複数あります。

  • 自社のサーバでロードバランサを構築する
  • AWSなどでNLBやALBといったLBaaSを使う
  • AWSなどでCDNを使う

今回はCDN(CloudFront)でTLS終端することにしました。自社管理サーバ増やしたくない(メンテナンスフリーにしたい)な…という気持ちがあったのと、NLBやALBは原則転送先も同じVPCである必要があるため今回の用途には合わないからです。

CloudFront使うとコストかかるんじゃないの、という懸念はあると思いますが、CloudFrontってリクエスト数とアウトプットのデータでの従量課金なので、今回の用途では低コストで導入できるはずです。(めっちゃくちゃクリックカウントが増えない限り)

やったことはかんたんなので箇条書きで書きます。

(転送元の自社ドメインex.mydomain.comとします)

これでOKでした。ノーメンテで障害にも強いTLS終端するだけのディストリビューション爆誕しました。

このような対応をすることはレアだとは思いますが、こういったレアなものほど文献はないので、誰かの参考になりますように。