OpenApi bake theme pluginをswagger-phpのアトリビュート形式で吐き出すように修正しました
最近は、アプリの開発・実行基盤の構築でインフラエンジニアっぽいことばかりやっていて、プロダクションのコードを書いていなくてストレスが溜まりつつある渡辺です
今構築している環境では、PHPでAPIを書いていて、swagger-phpでAPIドキュメントを作成して、フロントアプリとの連携をしています。以前書いたブログで OpenApi bake theme plugin や swagger-phpを紹介しているので参考にしてみてください。
当初、OpenApi bake theme pluginを使ってアノテーション形式で記載しようと思っていたのですが、構築するにあたりswagger-phpの開発状況を確認したところ、かなりアップデートされていて、より便利に使えそうなのでほとんどメンテナンスしていなかった、 OpenApi bake theme plugin
を修正することにしました。
OpenApiTheme plugin for CakePHP
swagger-php
swagger-phpで利用可能な形式は元々はアノテーション形式のみだったのですが、アトリビュート形式がいつの間にか追加されていました。以下がそれぞれのサンプルです。
- アノテーション形式のサンプル
/** * Article Entity * * @OA\Schema( * schema="Article", * title="", * description="Article entity", * @OA\Property( * property="id", * type="integer", * format="int32", * description="", * ), * @OA\Property( * property="user_id", * type="integer", * format="int32", * description="", * ), * @OA\Property( * property="title", * type="string", * description="", * ), * @OA\Property( * property="slug", * type="string", * description="", * ), * @OA\Property( * property="body", * type="string", * description="", * ), * @OA\Property( * property="published", * type="boolean", * description="", * ), * @OA\Property( * property="created", * type="string", * format="datetime", * description="", * ), * @OA\Property( * property="modified", * type="string", * format="datetime", * description="", * ), * )
- アトリビュート形式のサンプル
#[OA\Schema( schema: 'Article', title: 'Article', description: 'Article Entity', required: ['id', 'user_id', 'title', 'slug', 'published', 'user', 'tags'], properties: [ new OA\Property( property: 'id', type: 'integer', format: 'int32', description: '', ), new OA\Property( property: 'user_id', type: 'integer', format: 'int32', description: '', ), new OA\Property( property: 'title', type: 'string', description: '', ), new OA\Property( property: 'slug', type: 'string', description: '', ), new OA\Property( property: 'body', type: 'string', description: '', ), new OA\Property( property: 'published', type: 'boolean', description: '', ), new OA\Property( property: 'created', type: 'string', format: 'datetime', description: '', ), new OA\Property( property: 'modified', type: 'string', format: 'datetime', description: '', ), ] )]
アノテーションでの記述でも便利なのですが、所詮コメントの中に記載する形なので、書き心地はあまり良いとはいえない状況でした。 アトリビュート形式に変更になったことで、IDEの補完などの恩恵を受けられるようになり、かなり改善された印象です。
実際の開発現場では、このプラグインで自動生成したものをカスタマイズして利用することになると思うのでこの改善はかなり有益だと考え、OpenApi bake theme plugin
でもアトリビュート形式を採用することにしました。
アトリビュート形式のサンプル
CakePHPのCMS Tutorialを題材にいくつか記述例を紹介します
一覧取得API
以下が一覧取得処理をAPI化した場合の記述です。Queryは paramaters に OA\Parameterを利用して記述します
#[OA\Get( path: '/api/articles.json', summary: 'Articles index', parameters: [ new OA\Parameter( name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'number'), description: 'Page number to be get', ), new OA\Parameter( name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'number'), description: 'Number of elements per page', ), ], responses: [ new OA\Response( response: 201, description: 'OK', content: new OA\JsonContent(ref: '#components/schemas/Article'), ), new OA\Response(response: 401, description: 'Unauthorized'), new OA\Response(response: 403, description: 'Forbidden'), ] )] public function index()
新規作成API
新規作成APIは、JSON形式のデータをbodyに入れる形になるので、 OA\RequestBody / OA\JsonContent を使用して記述しています
#[OA\Post( path: '/api/articles/add.json', summary: 'Add article', description: 'Add article', requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['user_id', 'title', 'slug', 'published', 'user', 'tags'], properties: [ new OA\Property( property: 'user_id', type: 'integer', format: 'int32', description: '', ), new OA\Property( property: 'title', type: 'string', description: '', ), new OA\Property( property: 'slug', type: 'string', description: '', ), new OA\Property( property: 'body', type: 'string', description: '', ), new OA\Property( property: 'published', type: 'boolean', description: '', ), ] ) ), responses: [ new OA\Response( response: 201, description: 'OK', content: new OA\JsonContent(ref: '#components/schemas/Article'), ), new OA\Response(response: 401, description: 'Unauthorized'), new OA\Response(response: 403, description: 'Forbidden'), new OA\Response( response: 422, description: 'Validation Error', content: new OA\JsonContent(ref: '#components/schemas/Application'), ), ] )] public function add()
詳細取得API
OpenApi bake theme plugin では詳細取得APIはアソシエーションがあるModelの場合、アソシエーション内容も含むレスポンスのドキュメントを生成するように実装しています。
また、 /api/articles/{id}.json
のようにidをpathに含む形式なので OA\Parameter の in を path に設定しています。
#[OA\Get( path: "/api/articles/{id}.json", summary: "Get Article", description: "Get Article", parameters: [ new OA\Parameter( name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer', format: 'int32'), description: 'Article id', ), ], responses: [ new OA\Response( response: 200, description: 'OK', content: new OA\JsonContent( type: 'object', allOf: [ new OA\Schema(ref: '#components/schemas/Article'), new OA\Schema( properties: [ new OA\Property( property: "user", ref: "#/components/schemas/User", description: "User Entity", ), new OA\Property( property: "tags", type: "array", items: new OA\Items(ref: "#/components/schemas/Tag"), description: "Tag Entities", ), ], ), ], ), ), new OA\Response(response: 401, description: 'Unauthorized'), new OA\Response(response: 403, description: 'Forbidden'), new OA\Response(response: 404, description: 'Not Found'), ] )] public function view($id = null)
edit / delete はこれらの応用で記述できるので省略します。
まとめ
ということで、簡単にswagger-phpのアトリビュート形式での記載方法を紹介しました。 このプラグインを使わないにしても、詳細取得APIでアソシエーションを含むレスポンス形式は、定義済みのOA\Schemaを利用して別形式のレスポンスを生成する記述は参考になるのではないかなと思います。
参考リンク
Azure Container Appsの従量課金ワークロード プロファイルをbicepでプロビジョニング
最近は今後の開発作業を効率的に進めるために、CI/CD/IaC用の基盤(サンプル?)的なものを作ったしてます。今までやりたいなと思っていても、様々な理由で取り組めていなかったことも色々盛り込んでいるので、諸々知見が溜まってきました。今回は、その中からContainer Appsを取り上げようと思います。
Container Appsの実行環境
Container Appsは元々従量課金プランのみでの提供でしたが、2023年8月にdedicated plan(専用プラン)がGAしました。
従来の従量課金プランでは、CPU/Memoryの組み合わせで最小 0.25 / 0.5Gi から 最大 2vCPU / 4.0Giのサイズが用意されていましたが、dedicated planでは、従来の従量課金プランとよく似た、"従量課金ワークロード プロファイル"が用意されておりこちらでは 最大 4vCPU / 8Gi まで拡張されていいます。
また、必要に応じて専用ワークロードプロファイルを追加することで、専用のハードウェア上でContainer Appを実行することができるようになりました。参考
今回は、この新しいdedicated planに対応したContainer App Environmentをbicepでプロビジョニングする方法を調査した結果をまとめようと思います。
dedicated plan環境の作成
dedicated plan版のContainer Apps Environmentを作成するには以下のようなbicepファイルを用意します
param logAnalyticsWorkspaceName string = 'example-log' param appInsightsName string = 'example-app-insights' param environmentName string = 'example-container-apps-env-test' param location string = resourceGroup().location // Create a Log Analytics workspace resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: logAnalyticsWorkspaceName location: location properties: any({ retentionInDays: 30 features: { searchVersion: 1 legacy: 0 enableLogAccessUsingOnlyResourcePermissions: true } sku: { name: 'PerGB2018' } }) } // Create an Application Insights resource resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName location: location kind: 'web' properties: { Application_Type: 'web' WorkspaceResourceId:logAnalyticsWorkspace.id } } // Create a managed environment resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = { name: environmentName location: location properties: { appLogsConfiguration: { destination: 'log-analytics' logAnalyticsConfiguration: { customerId: logAnalyticsWorkspace.properties.customerId sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey } } daprAIInstrumentationKey: appInsights.properties.InstrumentationKey zoneRedundant: false peerAuthentication: { mtls: { enabled: false } } workloadProfiles: [{ name: 'Consumption' workloadProfileType: 'Consumption' }] } }
今回の肝になるのは、以下の部分です
... workloadProfiles: [{ name: 'Consumption' workloadProfileType: 'Consumption' }] ...
追加された workloadProfiles
プロパティを指定することで、"従量課金ワークロード プロファイル"を作成することができます。また、ここに以下のように独自のワークロードを定義することで、専用サーバー上にアプリを配置することができるようになります。
{ name: 'myworkload' maximumCount: 10 minimumCount: 3 workloadProfileType: 'D4' }
現時点で指定可能なworkloadProfileTypeは以下に記載があります
Azure Container Apps のワークロード プロファイル - プロファイルの種類
(minimumCount は可用性を保証するために3以上を指定することが推奨されています)
プロビジョニング
以下のコマンドでプロビジョンングすることができます
$ export RESOURCE_GROUP=[リソースグループ名] $ az deployment group create \ --resource-group $RESOURCE_GROUP \ --template-file bicep/container-apps-env.bicep
正常に処理が完了すると、以下のようにportalでも設定を確認することができます
dedicated plan環境にアプリをデプロイ
dedicated plan版のContainer Apps Environmentにアプリをデプロイするには以下のようなbicepファイルを用意します
param location string = resourceGroup().location param environmentName string = 'example-container-apps-env-test' param containerAppName string = 'example-app' @allowed([ 'multiple' 'single' ]) param revisionMode string = 'single' resource environment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { name: environmentName } resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { name: containerAppName location: location identity: { type: 'SystemAssigned' } properties: { workloadProfileName: 'Consumption' managedEnvironmentId: environment.id configuration: { activeRevisionsMode: revisionMode dapr:{ enabled:false } ingress: { external: true targetPort: 80 transport: 'auto' allowInsecure: false traffic: [ { weight: 100 latestRevision: true } ] } } template: { containers: [ { image: 'nginx:latest' name: containerAppName resources: { cpu: any('4.0') memory: '8Gi' } } ] scale: { minReplicas: 0 maxReplicas: 10 rules: [ { name: 'http-scaling-rule' http: { metadata: { concurrentRequests: '10' } } } ] } } } } output fqdn string = containerApp.properties.configuration.ingress.fqdn
従来の従量課金プランと異なるのは、プロパティの以下の部分です
... properties: { workloadProfileName: 'Consumption' ...
workloadProfileName
にワークロードプロファイル名を指定することで、実行環境を指定することができます。
デプロイ
以下のコマンでデプロイできます
$ az deployment group create \ --resource-group $RESOURCE_GROUP \ --template-file bicep/app.bicep
無事作成が完了すると、portalでも以下のように設定内容を確認することができます
おまけ
先日まで存在を知らなかったのですが、az deployment group
には what-if
というコマンドが存在します。このコマンドを利用することで、bicepファイルと実際のリソースの差分を表示することができます
例えば、上記アプリのCPU / メモリを以下のように修正します
... resources: { cpu: any('2.0') memory: '4Gi' } ...
この状態で下記のコマンドを実行すると、以下のように差分が表示されます
$ az deployment group what-if \ --resource-group $RESOURCE_GROUP \ --template-file bicep/app.bicep Note: The result may contain false positive predictions (noise). You can help us improve the accuracy of the result by opening an issue here: https://aka.ms/WhatIfIssues Resource and property changes are indicated with these symbols: - Delete ~ Modify * Ignore The deployment will update the following scope: Scope: /subscriptions/ac25fc44-3b4d-4b2f-917e-672509e414ca/resourceGroups/kaz29-key-vault-test-rg ~ Microsoft.App/containerApps/example-app [2023-05-01] - properties.configuration.ingress.exposedPort: 0 - properties.runningStatus: "Running" ~ properties.template.containers: [ ~ 0: ~ resources.cpu: 4.0 => 2.0 ~ resources.memory: "8Gi" => "4Gi" ] * Microsoft.App/managedEnvironments/example-container-apps-env-test * Microsoft.Insights/components/example-app-insights * Microsoft.OperationalInsights/workspaces/example-log * microsoft.alertsmanagement/smartDetectorAlertRules/Failure Anomalies - example-app-insights
この機能を使えば、stateを持たないbicepでもIaC的なことを比較的安心に組めそうです。 例えば、PR作成時にdiffを取って自動で差分をPRコメントに投稿するようにすれば、bicepファイルの変更と実際の差分を確認して安心してレビューができそうです
まとめ
いかがでしたか、dedicated planプランを利用すると従量課金ワークロード プロファイルでも、以前より豊富なリソースサイズの中からアプリの特性に合わせたものを選択できるようになりますし、専用ワークロードプロファイルを作ればさらに自由度が高い環境を構築することもできます。
Container Appsはとても便利なので、ぜひ試してみてください。
参考リンク
Azure Database for PostgreSQLでCDCを試してみる
最近開発しているサービスがだんだん成長してきて、先々を考えるといくつかのサービスに分離したいなーと思いChange Data Capture (CDC)について色々と調べていました。
MySQLでの構築については、この記事DebeziumでCDCを構築してみたがとても丁寧に解説されているのでお薦めです。この記事の解説を参考にしてMySQL+Kafka+Debeziumで動作してお試しできる環境ができたので、色々と挙動を確認できました。
PostgreSQLでCDC
MySQLでの実験環境は簡単に構築できたのですが、今回導入を検討しているサービスではPostgreSQLを使用しています。 ということで、まずは手元でPostgreSQL + Kafka + DebeziumでCDC環境を構築してみます。
Kafkaの構築
こちらは前出のブログの記載とほぼ同じで、Docker hubにある公式イメージから構築します。
version: "3.8" services: pg-debezium-zookeeper: container_name: pg-debezium-zookeeper image: "bitnami/zookeeper:latest" ports: - "2181:2181" environment: - ALLOW_ANONYMOUS_LOGIN=yes pg-debezium-kafka: container_name: pg-debezium-kafka image: "bitnami/kafka:latest" ports: - "9092:9092" - "29092:29092" environment: - KAFKA_CFG_BROKER_ID=1 - ALLOW_PLAINTEXT_LISTENER=yes - KAFKA_CFG_ZOOKEEPER_CONNECT=pg-debezium-zookeeper:2181 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:29092 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://pg-debezium-kafka:9092,PLAINTEXT_HOST://127.0.0.1:29092 depends_on: - pg-debezium-zookeeper
PostgreSQLの構築
MySQLでの構築と同様に、PostgreSQLを利用する場合にも設定を変更する必要があります。
デフォルトでは、wal_level = REPLICATION
になっているのですが、これを wal_level = LOGICAL
に変更しより詳細なログを出力する必要があります。LOGICALに設定変更するとログのサイズが増えるようなので注意が必要かもしれません。
今回は設定ファイルを変更せずにコンテナ起動時のコマンドで設定を変更することにします。 docker-compose.ymlはこんな感じ
version: "3.8" services: ... 省略 pg-debezium-postgres: container_name: pg-debezium-postgres image: postgres:14.7-alpine command: [ "postgres", "-c", "wal_level=logical" ] volumes: - pg-debezium-postgres-data:/var/lib/postgresql/data:cached environment: POSTGRES_PASSWORD: "Passw0rd" POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8" POSTGRES_USER: "test" POSTGRES_DB: test healthcheck: test: [ "CMD-SHELL", "pg_isready" ]
テーブルの作成
テストに使用するテーブルを作っておきます
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "CREATE TABLE test (id SERIAL PRIMARY KEY, subject text NOT NULL, created timestamptz NOT NULL);"
Debeziumの構築
Debeziumも、公式のイメージがあるのでそれを利用して、上記のKafkaと接続するように設定します。
version: "3.8" services: ... 省略 pg-debezium: container_name: pg-debezium image: "debezium/connect:2.0" ports: - "8083:8083" environment: - BOOTSTRAP_SERVERS=pg-debezium-kafka:9092 - GROUP_ID=1 - CONFIG_STORAGE_TOPIC=_kafka_connect_configs - OFFSET_STORAGE_TOPIC=_kafka_connect_offsets - STATUS_STORAGE_TOPIC=_kafka_connect_statuses depends_on: - pg-debezium-zookeeper - pg-debezium-kafka - pg-debezium-postgres
最終的なdocker-compose.yml
これまでの解説分を全て含んだdocker-compose.ymlはこんな感じです
version: "3.8" services: pg-debezium-zookeeper: container_name: pg-debezium-zookeeper image: "bitnami/zookeeper:latest" ports: - "2181:2181" environment: - ALLOW_ANONYMOUS_LOGIN=yes pg-debezium-kafka: container_name: pg-debezium-kafka image: "bitnami/kafka:latest" ports: - "9092:9092" - "29092:29092" environment: - KAFKA_CFG_BROKER_ID=1 - ALLOW_PLAINTEXT_LISTENER=yes - KAFKA_CFG_ZOOKEEPER_CONNECT=pg-debezium-zookeeper:2181 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:29092 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://pg-debezium-kafka:9092,PLAINTEXT_HOST://127.0.0.1:29092 depends_on: - pg-debezium-zookeeper pg-debezium-postgres: container_name: pg-debezium-postgres image: postgres:14.7-alpine command: [ "postgres", "-c", "wal_level=logical" ] volumes: - pg-debezium-postgres-data:/var/lib/postgresql/data:cached environment: POSTGRES_PASSWORD: "Passw0rd" POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8" POSTGRES_USER: "test" POSTGRES_DB: test healthcheck: test: [ "CMD-SHELL", "pg_isready" ] pg-debezium: container_name: pg-debezium image: "debezium/connect:2.0" ports: - "8083:8083" environment: - BOOTSTRAP_SERVERS=pg-debezium-kafka:9092 - GROUP_ID=1 - CONFIG_STORAGE_TOPIC=_kafka_connect_configs - OFFSET_STORAGE_TOPIC=_kafka_connect_offsets - STATUS_STORAGE_TOPIC=_kafka_connect_statuses depends_on: - pg-debezium-zookeeper - pg-debezium-kafka - pg-debezium-postgres networks: internal: driver: bridge internal: true external: driver: bridge internal: false name: pg_debezium_external_network volumes: pg-debezium-postgres-data:
こんな感じで起動してみます
$ docker compose up -d [+] Running 4/4 ⠿ Container pg-debezium-postgres Started 0.4s ⠿ Container pg-debezium-zookeeper Started 0.4s ⠿ Container pg-debezium-kafka Started 0.6s ⠿ Container pg-debezium Started 0.9s
これで、PostgreSQL + Kafka + Debezium でのCDCの基盤が動作している状態になります。
CDCの構築
基盤の構築が終わったので、Debeziumコンテナ内にコネクタを作成します。
以下が今回の調査で作成した設定内容です。 sample.json
という名称で保存しておきます。
{ "name": "postgres-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "pg-debezium-postgres", "database.port": "5432", "database.user": "test", "database.password": "Passw0rd", "database.dbname" : "test", "database.server.name": "pg-debezium-postgres", "table.whitelist": "public.test", "plugin.name": "pgoutput", "topic.prefix": "test_topic", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": false, "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": false, "database.connectionTimeZone": "UTC", "include.schema.changes": "false" } }
PostgreSQLコネクタの設定内容は、公式ドキュメントを参照してください。
設定ファイルができたので以下のコマンドでコネクタを作成します。(出力されているJSON文字列は整形しています)
$ curl -i -X POST -H "Accept:application/json" -H \ "Content-Type:application/json" \ http://localhost:8083/connectors/ \ -d @./sample.json HTTP/1.1 201 Created Date: Sat, 06 May 2023 05:36:54 GMT Location: http://localhost:8083/connectors/postgres-connector Content-Type: application/json Content-Length: 733 Server: Jetty(9.4.48.v20220622) { "name": "postgres-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "pg-debezium-postgres", "database.port": "5432", "database.user": "test", "database.password": "Passw0rd", "database.dbname": "test", "database.server.name": "pg-debezium-postgres", "table.whitelist": "public.test", "plugin.name": "pgoutput", "topic.prefix": "test_topic", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": "false", "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": "false", "database.connectionTimeZone": "UTC", "include.schema.changes": "false", "name": "postgres-connector" }, "tasks": [], "type": "source" }
トピックの確認
以下のコマンドで、トピックの一覧を表示することができます。まだレコードが存在しないので、先ほど作成したコネクタで定義したトピックはまだ存在しません。
$ docker-compose exec pg-debezium-kafka kafka-topics.sh --list --bootstrap-server pg-debezium-kafka:9092 __consumer_offsets _kafka_connect_configs _kafka_connect_offsets _kafka_connect_statuses
レコードを追加してみます。レコードを追加するとトピックが作成されていることが確認できます。
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "INSERT INTO test (subject, created) values ( 'Test 1', NOW());" INSERT 0 1 $ docker-compose exec pg-debezium-kafka kafka-topics.sh --list --bootstrap-server pg-debezium-kafka:9092 __consumer_offsets _kafka_connect_configs _kafka_connect_offsets _kafka_connect_statuses test_topic.public.test
では、kafkaコンテナに用意されている、kafka-console-consumer.sh
を使用して購読してみます。
$ docker-compose exec pg-debezium-kafka kafka-console-consumer.sh \ --bootstrap-server 127.0.0.1:9092 \ --from-beginning --topic test_topic.public.test {"before":null,"after":{"id":1,"subject":"Test 1","created":"2023-05-06T05:44:15.096521Z"},"source":{"version":"2.0.0.Final","connector":"postgresql","name":"test_topic","ts_ms":1683351855101,"snapshot":"false","db":"test","sequence":"[null,\"24280176\"]","schema":"public","table":"test","txId":738,"lsn":24280176,"xmin":null},"op":"c","ts_ms":1683351855591,"transaction":null}
無事、先ほど追加したレコードの内容は取得できています。では、別のターミナルを開いてデータベースを更新してみます。
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "INSERT INTO test (subject, created) values ( 'Test 2', NOW());" $ docker exec -it pg-debezium-postgres psql -U test test \ -c "UPDATE test SET subject = 'Test 2 updated' WHERE id = 2;"
コンシューマには以下のように表示されました。
{ "before": null, "after": { "id": 2, "subject": "Test 2", "created": "2023-05-06T05:46:17.579336Z" }, "source": { "version": "2.0.0.Final", "connector": "postgresql", "name": "test_topic", "ts_ms": 1683351977580, "snapshot": "false", "db": "test", "sequence": "[\"24280464\",\"24280856\"]", "schema": "public", "table": "test", "txId": 739, "lsn": 24280856, "xmin": null }, "op": "c", "ts_ms": 1683351977937, "transaction": null } { "before": null, "after": { "id": 2, "subject": "Test 2 updated", "created": "2023-05-06T05:46:17.579336Z" }, "source": { "version": "2.0.0.Final", "connector": "postgresql", "name": "test_topic", "ts_ms": 1683352381043, "snapshot": "false", "db": "test", "sequence": "[\"24282144\",\"24282200\"]", "schema": "public", "table": "test", "txId": 741, "lsn": 24282200, "xmin": null }, "op": "u", "ts_ms": 1683352381077, "transaction": null }
データを更新したのになぜか before
が null
になってしまっています。
色々調べた結果、Debeziumのユーザガイド内に記載がありました。 7.3.2. Debezium PostgreSQL 変更イベントの値 こちらの解説を参考に以下のように設定を変更します。
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "ALTER TABLE public.test REPLICA IDENTITY FULL;" ALTER TABLE
設定の変更ができたので、再度データを更新してみます。
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "UPDATE test SET subject = 'Test 2 updated 2' WHERE id = 2;" UPDATE 1
無事、before
に変更前のデータが入るようになりました。
{ "before": { "id": 2, "subject": "Test 2 updated", "created": "2023-05-06T05:46:17.579336Z" }, "after": { "id": 2, "subject": "Test 2 updated 2", "created": "2023-05-06T05:46:17.579336Z" }, "source": { "version": "2.0.0.Final", "connector": "postgresql", "name": "test_topic", "ts_ms": 1683352906329, "snapshot": "false", "db": "test", "sequence": "[\"24284944\",\"24285000\"]", "schema": "public", "table": "test", "txId": 743, "lsn": 24285000, "xmin": null }, "op": "u", "ts_ms": 1683352906769, "transaction": null }
では最後に削除を試してみます。
$ docker exec -it pg-debezium-postgres psql -U test test \ -c "DELETE FROM test WHERE id = 2;"
無事、after
が null
になりました
{ "before": { "id": 2, "subject": "Test 2 updated 2", "created": "2023-05-06T05:46:17.579336Z" }, "after": null, "source": { "version": "2.0.0.Final", "connector": "postgresql", "name": "test_topic", "ts_ms": 1683353081106, "snapshot": "false", "db": "test", "sequence": "[\"24285496\",\"24285552\"]", "schema": "public", "table": "test", "txId": 744, "lsn": 24285552, "xmin": null }, "op": "d", "ts_ms": 1683353081505, "transaction": null }
これで、コンシューマを作成すれば、変更データをもとにコピーを作成したりデータを集計したりなど色々と処理ができますね!前出のブログでも紹介されている、KafkaJSを使ってコンシューマを作るのが良さそうです。
Azure Database for PostgreSQLでの設定
ここまで、手元環境のdoker上のPostgreSQLでDebeziumを試してみましたが、Azure Database for PostgreSQLで動作するか検証することにしてみます。
Azureの公式ドキュメントには、変更データ キャプチャ用に Azure Event Hubs の Apache Kafka Connect のサポートを Debezium と統合するという記事があり、Azure Event Hubsを利用した構築方法が解説されています。
このページにも 警告
として記載がありますが、Event Hubを利用する場合イベントの保存期間が制限されるという制約があるようです。これが実運用時に問題になるかはまだ把握できていないのですが、まずは自前でCDCを構築してみようと思います。
Azure Database for PostgreSQLの作成
今回は試験用なので、以下のように小さめ(お安い)で作成しています。 (フレキシブル サーバーでないと14などの新しいバージョンが利用できないので、フレキシブル サーバーを使っています)
設定の変更
まずは、 wal_level
を変更します。サーバーパラメータ編集ページで以下のようにwal_level
を LOGICAL
に変更して、設定を保存します。
次に、今回は手元環境から直接PostgreSQLに接続をする必要があるので、ネットワーク編集ページで 現在のクライアントIPを追加する
を選択して、接続元IPアドレスを追加して、設定を保存します。
今回は実験用なのでこのように外部からの接続を許可する設定をしましたが、実運用時の設定は各環境に合わせて適切に設定してください
これで、手元環境から接続できるようになったので、以下のコマンドで接続確認をします。
$ docker exec -it pg-debezium-postgres psql -h ホスト名.postgres.database.azure.com -U test test Password for user test: psql (14.7) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. test=>
無事接続できましたので、先ほどと同様にテーブルを作成します。また、先ほど追加で設定したレプリケーションの設定なども更新しておきます。
$ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "CREATE TABLE test (id SERIAL PRIMARY KEY, subject text NOT NULL, created timestamptz NOT NULL"); $ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "ALTER TABLE public.test REPLICA IDENTITY FULL;" $ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "ALTER ROLE test WITH REPLICATION;"
CDCの構築
SSL接続設定の準備
Azure Database for PostgreSQLへの接続は、TLSを使用して接続する必要があるため、Debeziumからの接続に少し準備が必要です。 公式ドキュメントのAzure Database for PostgreSQL - フレキシブル サーバーでのトランスポート層セキュリティを使用した暗号化された接続の解説を参考にして、ルート証明書をダウンロードします。
docker exec -it pg-debezium curl -O https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem
コネクタの追加
準備ができたので、以下のような設定でコネクタを追加します。SSLでの接続が必要なため、ローカル用の設定に database.sslmode
/ database.sslrootcert
の2つを追加しています。
今回は、azure~sample.jsonという名前で保存します。
{ "name": "azure-postgres-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "データベースのホスト名", "database.port": "5432", "database.user": "test", "database.password": "データベースのパスワード", "database.dbname" : "test", "database.server.name": "データベースのホスト名", "database.sslmode": "verify-full", "database.sslrootcert": "/kafka/DigiCertGlobalRootCA.crt.pem", "table.whitelist": "public.test", "plugin.name": "pgoutput", "topic.prefix": "azure_pgsql_topic", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": false, "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": false, "database.connectionTimeZone": "UTC", "include.schema.changes": "false" } }
先ほどと同様に以下のコマンドでコネクタを追加します。
$ curl -i -X POST -H "Accept:application/json" -H \ "Content-Type:application/json" \ http://localhost:8083/connectors/ \ -d @./azure-sample.json HTTP/1.1 201 Created Date: Sat, 06 May 2023 06:49:08 GMT Location: http://localhost:8083/connectors/azure-postgres-connector Content-Type: application/json Content-Length: 924 Server: Jetty(9.4.48.v20220622) { "name": "azure-postgres-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "ホスト名", "database.port": "5432", "database.user": "test", "database.password": "データベースのパスワード", "database.dbname": "webapp", "database.server.name": "ホスト名", "database.sslmode": "verify-full", "database.sslrootcert": "/kafka/DigiCertGlobalRootCA.crt.pem", "table.whitelist": "public.test", "plugin.name": "pgoutput", "topic.prefix": "azure_pgsql_topic", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": "false", "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": "false", "database.connectionTimeZone": "UTC", "include.schema.changes": "false", "name": "azure-postgres-connector" }, "tasks": [], "type": "source" }
以下のコマンドで、コネクタが追加されているか確認します
$ curl -i -X GET -H "Accept:application/json" \ -H "Content-Type:application/json" \ http://localhost:8083/connectors/ HTTP/1.1 200 OK Date: Sat, 06 May 2023 06:51:04 GMT Content-Type: application/json Content-Length: 49 Server: Jetty(9.4.48.v20220622) ["postgres-connector","azure-postgres-connector"]
問題なく追加されているようです。
トピックの確認
準備ができたので、先ほどと同様に kafka-console-consumer.sh
を使用して購読してみます。
$ curl -i -X POST -H "Accept:application/json" -H \ "Content-Type:application/json" \ http://localhost:8083/connectors/ \ -d @./azure-sample.json HTTP/1.1 201 Created Date: Sat, 06 May 2023 06:49:08 GMT Location: http://localhost:8083/connectors/azure-postgres-connector Content-Type: application/json Content-Length: 924 Server: Jetty(9.4.48.v20220622) { "name": "azure-postgres-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "ホスト名", "database.port": "5432", "database.user": "test", "database.password": "パスワード", "database.dbname": "webapp", "database.server.name": "ホスト名", "database.sslmode": "verify-full", "database.sslrootcert": "/kafka/DigiCertGlobalRootCA.crt.pem", "table.whitelist": "public.test", "plugin.name": "pgoutput", "topic.prefix": "azure_pgsql_topic", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": "false", "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": "false", "database.connectionTimeZone": "UTC", "include.schema.changes": "false", "name": "azure-postgres-connector" }, "tasks": [], "type": "source" }
準備ができたので、別ターミナルからデータを追加・更新・削除してみます。
$ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "INSERT INTO test (subject, created) values ( 'Test 1', NOW());" $ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "INSERT INTO test (subject, created) values ( 'Test 2', NOW());" $ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "UPDATE test SET subject = 'Test 2 updated' WHERE id = 2;" $ docker exec -it pg-debezium-postgres psql -h ホスト名 -U test test \ -c "DELETE FROM test WHERE id = 2;"
コンシューマでは、問題なく以下のように変更を取得できました。
{"before":null,"after":{"id":1,"subject":"Test 1","created":"2023-05-06T02:28:46.075454Z"},"source":{"version":"2.0.0.Final","connector":"postgresql","name":"azure_pgsql_topic","ts_ms":1683355748922,"snapshot":"first","db":"webapp","sequence":"[null,\"20568873528\"]","schema":"public","table":"test","txId":48642,"lsn":20568873528,"xmin":null},"op":"r","ts_ms":1683355749255,"transaction":null} {"before":null,"after":{"id":2,"subject":"Test 2","created":"2023-05-06T02:28:46.084254Z"},"source":{"version":"2.0.0.Final","connector":"postgresql","name":"azure_pgsql_topic","ts_ms":1683355748922,"snapshot":"true","db":"webapp","sequence":"[null,\"20568873528\"]","schema":"public","table":"test","txId":48642,"lsn":20568873528,"xmin":null},"op":"r","ts_ms":1683355749257,"transaction":null} {"before":{"id":2,"subject":"Test 2","created":"2023-05-06T02:28:46.084254Z"},"after":{"id":2,"subject":"Test 2 updated","created":"2023-05-06T02:28:46.084254Z"},"source":{"version":"2.0.0.Final","connector":"postgresql","name":"azure_pgsql_topic","ts_ms":1683356320172,"snapshot":"false","db":"webapp","sequence":"[\"20602423752\",\"20602427936\"]","schema":"public","table":"test","txId":48718,"lsn":20602427936,"xmin":null},"op":"u","ts_ms":1683356320309,"transaction":null} {"before":{"id":2,"subject":"Test 2 updated","created":"2023-05-06T02:28:46.084254Z"},"after":null,"source":{"version":"2.0.0.Final","connector":"postgresql","name":"azure_pgsql_topic","ts_ms":1683356329235,"snapshot":"false","db":"webapp","sequence":"[\"20602428392\",\"20602428648\"]","schema":"public","table":"test","txId":48720,"lsn":20602428648,"xmin":null},"op":"d","ts_ms":1683356329507,"transaction":null}
これで、Azure Database for PostgreSQLでもDebeziumを使用したCDCが構築できることが確認できました。
まとめ
今回は、Azure Database for PostgreSQL + DebeziumでCDCを構築するための準備として、ローカル環境 + Azure Database for PostgreSQLでCDCを構築するところまでを解説しました。 実際に利用するためには、まだまだ調査しないといけないことは多いですが、ひとまず以降の実験・調査をする土台までは検証できました。
さらっと書いていますが、概念を把握するのに色々実験したり、PostgreSQL・Azure Database for PostgreSQLで動作させるために何度も構築し直したり、色々と試行錯誤が必要でした。
この後は、以下のような残った課題を順次調査を進めていこうと思います
- Kafka + zookeeperをどこに構築するか検討して構築
- コネクタのパラメータの調整
- ログの管理や監視
- Azure Event Hubsを利用する形での実験と検討
また知見が溜まったら、ブログにまとめようと思います。
関連リンク
Github Actions で Azure Container Apps の B/G Deployを設定する
先日、現在開発をしているサービスのQueue workerの一部をAzure Container Appsに移行しました。とても使いやすいのでメインのAPIの移行準備として、Github Actions使用したB/Gデプロイの実験をしてみました。
サンプルコードの準備
今回実験用にデプロイするのは、クイック スタート: Azure Container Apps にコードをデプロイする で使用している、 Azure Container Apps Album APIを使いたいと思います。
このAPIはnodejsで実装されていて、静的に保持したデータを返すAPIが一つだけ定義されています。まずは手元の環境で実際に実行してみます。
自身の環境でGithub Actionsを実行したいので、 Azure Container Apps Album APIを fork
してpullします。
Dockerfileが用意されているので、以下のようにビルド・起動します。
$ cd containerapps-albumapi-javascript/src $ docker build -t kaz29/containerapps-albumapi-javascript . $ docker run -it --rm -d --name containerapps-albumapi-javascript -p 80:3500 kaz29/containerapps-albumapi-javascript
実際にAPIを叩くとこんな感じです
$ curl http://localhost/albums | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 751 100 751 0 0 35753 0 --:--:-- --:--:-- --:--:-- 57769 [ { "id": 1, "title": "You, Me and an App ID", "artist": "Daprize", "price": 56.99, "image_url": "https://aka.ms/albums-daprlogo" }, { "id": 2, "title": "Seven Revision Army", "artist": "The Blue-Green Stripes", "price": 17.99, "image_url": "https://aka.ms/albums-containerappslogo" }, { "id": 3, "title": "Scale It Up", "artist": "KEDA Club", "price": 39.99, "image_url": "https://aka.ms/albums-kedalogo" }, { "id": 4, "title": "Lost in Translation", "artist": "MegaDNS", "price": 39.99, "image_url": "https://aka.ms/albums-envoylogo" }, { "id": 5, "title": "Lock Down your Love", "artist": "V is for VNET", "price": 39.99, "image_url": "https://aka.ms/albums-vnetlogo" }, { "id": 6, "title": "Sweet Container O' Mine", "artist": "Guns N Probeses", "price": 39.99, "image_url": "https://aka.ms/albums-containerappslogo" } ]
src/models/Album.js
に配列で持っているデータを返すだけのシンプルなAPIです。
リソースの準備
基本的にはここの手順通りなのですが、まずはリソースグループとContainer Registryを作成します。
環境変数を設定
export GITHUB_USERNAME="<YOUR_GITHUB_USERNAME>" export RESOURCE_GROUP="album-containerapps" export LOCATION="japaneast" export ACR_NAME="acaalbums"$GITHUB_USERNAME export API_NAME="album-api"
リソースグループを作成
az group create \ --name $RESOURCE_GROUP \ --location "$LOCATION"
Container Registryを作成
az acr create \ --resource-group $RESOURCE_GROUP \ --name $ACR_NAME \ --sku Basic \ --admin-enabled true
Github actionの設定
下記で取得したユーザ名・パスワードをGithub Actions Secretに設定
az acr credential show --name $ACR_NAME -g $RESOURCE_GROUP --query "username" --out tsv az acr credential show --name $ACR_NAME -g $RESOURCE_GROUP --query "passwords[0].value" --out tsv
ユーザ名: CONTAINER_REGISTRY_USERNAME
パスワード: CONTAINER_REGISTRY_PASSWORD
今回は、github container registry ではなく、Azure Container Registryを使うのでGHAの設定を以下のように書き換えます。 また、tagのpushのみデプロイをしたいので main の pushトリガーを削除しています。
環境変数からtag名を取得して、ACRにpush時のtagとして使用しています。
name: Build and Push on: push: # Publish semver tags as releases. tags: ["v*.*.*"] workflow_dispatch: env: ACR_NAME: acaalbumskaz29 API_NAME: album-api jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Log in to container registry uses: docker/login-action@v1 with: registry: ${{ env.ACR_NAME }}.azurecr.io username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Build and push container image to registry uses: docker/build-push-action@v2 with: push: true tags: ${{ env.ACR_NAME }}.azurecr.io/${{ env.ACR_NAME }}:${{ env.TAG }} context: ./src - name: Upload artifact uses: actions/upload-artifact@v2 with: name: deploy-artifact path: bicep/*
修正をcommit/pushした上で、以下の様にタグをつけてpushするとACRにデプロイ用のイメージがpushされます
$ git tag v0.0.1 $ git push origin --tags
Container Apps環境を作成
以下のコマンドを実行し、Container Apps環境を作成します。
export CONTAINERAPPS_ENVIRONMENT="my-containerapps-env" az containerapp env create \ --name $CONTAINERAPPS_ENVIRONMENT \ --resource-group $RESOURCE_GROUP \ --location $LOCATION
アプリをデプロイ
デプロイの環境ができたので、アプリをデプロイする準備・設定を進めていきます。
サービスプリンシパルを作成
デプロイに使用するサービスプリンシパルを作成します。以下では、1年有効な設定をしていますがこの辺りは適宜修正してください。
$ export RESOURCE_GROUP_ID=$(az group show \ --name "$RESOURCE_GROUP" \ --query id --output tsv) $ az ad sp create-for-rbac \ --display-name "$RESOURCE_GROUP GHA deploy" \ --scope $RESOURCE_GROUP_ID \ --role Contributor \ --sdk-auth \ --years 1
上記azコマンドで出力されたjson文字列を、GHA Action Secretに保存します。
初回デプロイ
まずは、B/Gデプロイではなく先ほどビルドしたコンテナを単純にデプロイします。
azコマンドでもデプロイはできるのですが、細かな設定ができないので今回はbicepを使用してデプロイします。
この辺りの詳細は、トニー (@TonyTonyKun) / Twitter さんの ブログ - Azure Container Apps で Blue-Green Deployments を試してみたがとても参考になります。
bicep/api.bicep を作成
# bicep/api.bicep param containerAppName string param location string = resourceGroup().location param environmentId string param imageName string param tagName string param revisionSuffix string param oldRevisionSuffix string param isExternalIngress bool param acrUserName string @secure() param acrSecret string @allowed([ 'multiple' 'single' ]) param revisionMode string = 'single' resource containerApp 'Microsoft.App/containerApps@2022-03-01' = { name: containerAppName location: location properties: { managedEnvironmentId: environmentId configuration: { activeRevisionsMode: revisionMode ingress: { external: isExternalIngress targetPort: 3500 transport: 'auto' allowInsecure: false traffic: ((contains(revisionSuffix, oldRevisionSuffix)) ? [ { weight: 100 latestRevision: true } ] : [ { weight: 0 latestRevision: true } { weight: 100 revisionName: '${containerAppName}--${oldRevisionSuffix}' } ]) } dapr:{ enabled:false } secrets: [ { name: 'acr-secret' value: acrSecret } ] registries: [ { server: '${acrUserName}.azurecr.io' username: acrUserName passwordSecretRef: 'acr-secret' } ] } template: { revisionSuffix: revisionSuffix containers: [ { image: '${acrUserName}.azurecr.io/${imageName}:${tagName}' name: containerAppName resources: { cpu: any('0.25') memory: '0.5Gi' } } ] scale: { minReplicas: 1 maxReplicas: 10 rules: [ { name: 'http-scaling-rule' http: { metadata: { concurrentRequests: '10' } } } ] } } } } output fqdn string = containerApp.properties.configuration.ingress.fqdn
bicep/deploy.bicep を作成
#bicep/deploy.bicep param location string = resourceGroup().location param isExternalIngress bool = true param revisionMode string = 'multiple' param environmentName string param containerAppName string param imageName string param tagName string param revisionSuffix string param oldRevisionSuffix string param acrUserName string @secure() param acrSecret string resource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { name: environmentName } module apps 'api.bicep' = { name: 'container-apps' params: { containerAppName: containerAppName location: location environmentId: environment.id imageName: imageName tagName: tagName revisionSuffix: revisionSuffix oldRevisionSuffix: oldRevisionSuffix revisionMode: revisionMode isExternalIngress: isExternalIngress acrUserName: acrUserName acrSecret: acrSecret } }
後ほど、B/Gデプロイを実現するために、以下のパラメータを定義しています。
- revisionSuffix: 新たにデプロイされるリビジョン
- oldRevisionSuffix: 現在デプロイされているリビジョン
また、初回デプロイ時にはまだ実行中のリビジョンが存在しないため、以下の様にrevisionSuffix / oldRevisionSuffixが同じ場合には最新版のリビジョンに100%トラフィックを流すように設定しています。
traffic: ((contains(revisionSuffix, oldRevisionSuffix)) ? [ { weight: 100 latestRevision: true } ] : [ { weight: 0 latestRevision: true } { weight: 100 revisionName: '${containerAppName}--${oldRevisionSuffix}' } ]) }
workflowを更新
.github//workflows/build-and-push.yaml に以下のjobを追加します。
deploy: runs-on: ubuntu-latest needs: build environment: name: build url: https://${{ steps.fqdn.outputs.fqdn }} outputs: revision_suffix: ${{ steps.revision_suffix.outputs.revision_suffix }} previous_revision_suffix: ${{ steps.previous_revision_suffix.outputs.previous_revision_suffix }} fqdn: ${{ steps.fqdn.outputs.fqdn }} steps: - name: Download artifact uses: actions/download-artifact@v2 with: name: deploy-artifact - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV # タグ名から.(ドット)を除去する - name: Set revision suffix name to env id: revision_suffix run: | echo "REVISION_SUFFIX=${TAG//./}" >> $GITHUB_ENV echo "::set-output name=revision_suffix::${TAG//./}" - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Deploy to containerapp uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az deployment group create \ -f ./deploy.bicep \ -g ${{ env.RESOURCE_GROUP_NAME }} \ --parameters \ environmentName=${{ env.CONTAINER_APPS_ENVIRONMENT }} \ containerAppName=${{ env.API_NAME }} \ imageName=${{ env.API_NAME }} \ tagName=${{ env.TAG }} \ revisionSuffix=${{ env.REVISION_SUFFIX }} \ oldRevisionSuffix=${{ env.REVISION_SUFFIX }} \ acrUserName=${{ secrets.CONTAINER_REGISTRY_USERNAME }} \ acrSecret=${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
リビジョン名には . (ドット)
は含められないため、 Set revision suffix name to env
stepでタグ名のドットを除去しています。
修正をcommit/pushした上で、以下の様にタグをつけてpushするとコンテナアプリが追加され、APIがデプロイされます。
$ git tag v0.0.2 $ git push origin --tags
デプロイが完了したら、Auzre Portalのコンテナアプリのページに表示されているURLをブラウザで開くとAPIの動作を確認できます。
B/Gデプロイの設定を追加
では今回の主目的の、B/Gデプロイを実現するための設定を追加していきます。まずは、workflowのdeploy stepを以下の様に修正します
deploy: runs-on: ubuntu-latest needs: build environment: name: build url: https://${{ steps.fqdn.outputs.fqdn }} outputs: revision_suffix: ${{ steps.revision_suffix.outputs.revision_suffix }} previous_revision_suffix: ${{ steps.previous_revision_suffix.outputs.previous_revision_suffix }} fqdn: ${{ steps.fqdn.outputs.fqdn }} steps: - name: Download artifact uses: actions/download-artifact@v2 with: name: deploy-artifact - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV # タグ名から.(ドット)を除去する - name: Set revision suffix name to env id: revision_suffix run: | echo "REVISION_SUFFIX=${TAG//./}" >> $GITHUB_ENV echo "::set-output name=revision_suffix::${TAG//./}" - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Get Previous revision name id: previous_revision_suffix uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp export REVISIONS=`az containerapp revision list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --name ${{ env.API_NAME }} --query '[].name' --out tsv` echo "REVISION_NUM=`az containerapp revision list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --name ${{ env.API_NAME }} --query '[] | length(@)' --out tsv`" >> $GITHUB_ENV echo "PREVIOUS_REVISION_NAME=${REVISIONS##*--}" >> $GITHUB_ENV echo "::set-output name=previous_revision_suffix::${REVISIONS##*--}" - name: Active revision count check if: ${{ env.REVISION_NUM != 1 }} uses: actions/github-script@v3 with: script: | core.setFailed('Multiple revisions are active!') - name: Deploy to containerapp uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az deployment group create \ -f ./deploy.bicep \ -g ${{ env.RESOURCE_GROUP_NAME }} \ --parameters \ environmentName=${{ env.CONTAINER_APPS_ENVIRONMENT }} \ containerAppName=${{ env.API_NAME }} \ imageName=${{ env.API_NAME }} \ tagName=${{ env.TAG }} \ revisionSuffix=${{ env.REVISION_SUFFIX }} \ oldRevisionSuffix=${{ env.PREVIOUS_REVISION_NAME }} \ acrUserName=${{ secrets.CONTAINER_REGISTRY_USERNAME }} \ acrSecret=${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - name: Get new revision's fqdn id: fqdn uses: azure/CLI@v1 with: inlineScript: | export FQDN=`az deployment group show \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.DEPLOYMENT_NAME }} \ --query properties.outputs.fqdn.value \ --out tsv` export BASE_NAME=${FQDN#*.} echo "::set-output name=fqdn::${{ env.API_NAME }}--${{ env.REVISION_SUFFIX }}.$BASE_NAME"
各ステップの概要
- Get Previous revision name
B/Gデプロイを実現する為に、現在実行中のリビジョン名を取得しています。
- Active revision count check
B/Gデプロイ時に、実行中のリビジョンが複数ある場合はどのようにトラフィックを割り当てるか判断できないので、複数リビジョンが稼働している場合は、エラーになるようにチェックしています。
- Get new revision's fqdn
次に定義するジョブで、新しいリビジョンのURLを表示するために新しいリビジョンのFQDNを生成しています。
このjobを実行すると、新しいリビジョン(green)がデプロイされますが、トラフィックの割り当ては 0%
に設定されているため実際には新しいリビジョンのAPIは呼び出されません。
リビジョン毎にURLが発行されるので、新しいリビジョンのURLを使用して、動作確認を実施しします。
今回は、確認が完了後に、新しいリビジョンに 100%
、古いリビジョンに 0%
のトラフィックの割り当て、Blue / Greenを入れ替えて最新版を反映します。
B / Gを入れ替える
今回は、B/Gの入れ替え時に、承認処理を挟むために Environments
機能 の Environment protection rules
を利用します。
残念ながら、現状、Environment protection rulesは publicリポジトリか、Github Enterpriseでのみ利用可能です。
Environmentを追加
リポジトリの Settings
- Environments
で、リビジョン入れ替え用のEnvironment flip
を作成します。
以下の様に Required reviewers
をチェックし、レビュアーのアカウントを選択します。
B / Gを入れ替えるJobを追加
flip: runs-on: ubuntu-latest needs: deploy environment: name: flip steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Flip revisions uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az containerapp ingress traffic set \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.API_NAME }} \ --revision-weight \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.revision_suffix }}=100 \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.previous_revision_suffix }}=0
古いリビジョンを非アクティブにする
Environmentを追加
リポジトリの Settings
- Environments
で、非アクティブ用のEnvironment deactivate
を作成します。
以下の様に Required reviewers
をチェックし、レビュアーのアカウントを選択します。
古いリビジョンを非アクティブにするJobを追加
deactivate: runs-on: ubuntu-latest needs: [flip, deploy] environment: name: deactivate steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Deactivate previous revision uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az containerapp revision deactivate \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.API_NAME }} \ --revision \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.previous_revision_suffix }}
デプロイを実行
修正をcommit/pushした上で、以下の様にタグをつけてpushすると新しいリビジョンがデプロイされます。
$ git tag v0.0.3 $ git push origin --tags
flip
デプロイ完了後、flipの実行待ち状態でworkflowが止まります。 deploy
jobのボックスにはデプロイされた新しいリビジョンのAPIのURLが表示されています。
Review deployments
をクリックして、B / Gを入れ替え(flip)します。
flipが完了すると、トラフィック割り当てが変更され新しいリビジョンを利用する状態になります。
deactivate
この状態で問題がなければ、古いリビジョンを非アクティブにする必要があります。リビジョンがアクティブな状態だと課金対象になってしまいますので、早めに非アクティブ化したほうが良いでしょう。
workflowは deactivate
Jobの実行待ち状態で停止しています。
Review deployments
をクリックして、古いリビジョンを非アクティブ化(deactivate)します。
deactivate jobが完了すると古いリビジョンが非アクティブ化されます
無事、Github actionsを使用して、Container AppsのB/Gデプロイが実現できました。
まとめ
いかがでしたでしょうか?Container AppsでBlue / Green デプロイを実現するには、現在実行中のリビジョン名が必要なため若干複雑な流れになっています。やっていることは特に難しいことではないですが、調査に少し時間がかかりました。「もっといい方法があるよ!」とかあれば是非教えてほしいです。
現状、Environment protection rulesは publicリポジトリか、Github Enterprise以外では利用できないので、privateリポジトリで開発をしている現場で使うには一部見直しが必要かもしれません。
今回はBlue / Green デプロイを採用しましたが、ちょっと修正すればカナリーリリースとかも実現できると思うので、参考にしてもらえると嬉しいです。参考までに、私が試したforkしたAPIのリポジトリを残しておきます。
私が現在担当している現場でも、今後Container Appsに移行する予定があるので、いい感じで実現できて捗りそうです。
参考資料
CakePHPのschemaからtypescriptのinterfaceを吐きだすプラグインをかいた
小ネタです。
最近は相変わらずCakePHPでAPIを書いて、nextjsでフロントのアプリを書くサイトばかり作っているのですが、API側で定義したAPIレスポンスデータをフロント側用にinterfaceを書くのがだるいのでプラグインを書いた話です。
TsExport plugin for CakePHP
TsExport plugin for CakePHPは以下のようにインストールしてください。
composer require --dev kaz29/cakephp-ts-export-plugin
実行は以下のような感じ。
bin/cake export_entity --all または bin/cake export_entity モデル名
実際に実行すると、以下のようにinterface定義が標準出力に出力されます。
bin/cake export_entity Users /** * User entity interface */ export interface User { id: number name: string email: string password: string created?: string modified?: string }
フロント側では、src/types/exported_interfaces.ts のようなファイル名でこのプラグインの出力をそのまま使って、フロント用に変更する場合は、別のファイルでextend して項目を追加したり不要なものをOmitしたりしたものを使ってます。
Azure Web PubSubのnegotiateをPHPで実装してみる
最近書いているとあるサービスでリアルタイム更新をしたいと思い、Azure SignalR ServiceとAzure Web PubSubを試してます。
クイックスタートを参考にすれば、Azure Functionsで割と簡単に動作を試せます。
今回のサービスのバックエンドAPIはPHPで書かれているため、 negotiate
の処理をPHPのAPIで実施したいと考えていたのですが、残念ながらAzure PubSubのPHP SDKは現時点で提供されていません。(多分この先も提供はされなそう... (;_; )
ということで、Azure Web PubSub service client library for JavaScript を参考に、negotiate
が何をしているか調べてみました。
調べた結果、negotiate
のレスポンスは以下のような内容になっていました。
{ baseUrl: 'wss://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]', token: 'JWT token', url: 'wss://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]?access_token=[JWT Token]' }
ふむふむ、JWTで認証しているよう...。生成されるJWTの中身は以下の様な内容でした。
{ "header": { "typ": "JWT", "alg": "HS256" }, "claims": { "iat": 1623618349, "exp": 1623621949, "aud": "https://[PubSubName].webpubsub.azure.com/client/hubs/[hubname]" }, "signature": "sigunature...", "raw": "eyJ0eXAiOiJ..." }
要は、接続文字列からこのJWTを生成できれば良さそうです。ということで、gree/joseを使ってざくっと書いてみたのが以下。
<?php declare(strict_types=1); class PubSubToken { protected $endpoint; protected $wssEndpoint; protected $accesskey; protected $version; protected $alg = 'HS256'; public function __construct($connectionString) { $params = explode(';', $connectionString); foreach ($params as $param) { list($k, $v) = explode('=', $param, 2); $this->{strtolower($k)} = $v; } $this->wssEndpoint = preg_replace('/(http)(s?:\/\/)/i', 'ws$2', $this->endpoint); if ($this->endpoint === null || $this->accesskey === null || $this->version === null || $this->wssEndpoint === null) { throw new \Exception('Parameter error'); } } public function getAuthenticationToken(string $hub, string $userId = null, int $ttl = 3600): array { $now = time(); $payload = [ 'iat' => $now, 'exp' => $now + $ttl, 'aud' => "{$this->endpoint}/client/hubs/{$hub}", ]; if ($userId !== null) { $payload['sub'] = $userId; } $jwt = new \JOSE_JWT($payload); $jwt->header['alg'] = $this->alg; $jwt->header['typ'] = 'JWT'; $jwt->sign($this->accesskey, $this->alg); $jws = new \JOSE_JWS($jwt); $jws = $jws->sign($this->accesskey, $this->alg); $token = $jws->toString(); return [ 'baseUrl' => "{$this->wssEndpoint}/client/hubs/{$hub}", 'token' => $token, 'url' => "{$this->wssEndpoint}/client/hubs/{$hub}?access_token={$token}", ]; } } $pubsub = new PubSubToken('Azure WebPubSubの接続文字列'); $token = $pubsub->getAuthenticationToken('test');
このtoken
を使って無事subscribeできました。
ということで、このtokenをAPIで返してあげれば、クライアント側でsubscribeできそうです。
コードは、gistにも上げておきました。
CakePHP4用のOpenApi bake theme pluginを公開しました
最近は久々にガッツリPHPのコードを書いているわたなべです。
このところ、仕事でもプライベートでもPHPでAPIを書いて、Next.jsでフロントのWebアプリを書くことがほとんどです。
この場合API仕様は以前ブログにも書きましたが、swagger-phpのアノテーションで記述して、Swagger-UIで参照できる様にしています。
Swagger-UI と swagger-php
最近は使われている方も多いと思いますが、簡単に説明すると、EntityとControllerに以下の様なアノテーションを記述します。
Entity/Article.php
/** * Article Entity * * @OA\Schema( * schema="Article", * title="", * description="Article entity", * @OA\Property( * property="id", * type="integer", * format="int32", * description="", * ), * @OA\Property( * property="user_id", * type="integer", * format="int32", * description="", * ), * @OA\Property( * property="title", * type="string", * description="", * ), * @OA\Property( * property="slug", * type="string", * description="", * ), * @OA\Property( * property="body", * type="string", * description="", * ), * @OA\Property( * property="published", * type="boolean", * description="", * ), * @OA\Property( * property="created", * type="string", * format="datetime", * description="", * ), * @OA\Property( * property="modified", * type="string", * format="datetime", * description="", * ), * )
Controller/Api/ArticlesController.php
/**
* Index method
*
* @OA\Get(
* path="/api/articles.json",
* summary="Articles index",
* description="Articles index",
* @OA\Parameter(
* name="page",
* in="query",
* required=false,
* @OA\Schema(
* type="number",
* ),
* description=""
* ),
* @OA\Parameter(
* name="limit",
* in="query",
* required=false,
* @OA\Schema(
* type="number",
* ),
* description=""
* ),
* @OA\Response(
* response=200,
* description="successful operation",
* @OA\JsonContent(
* @OA\Property(
* property="success",
* type="boolean",
* default=true,
* ),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* allOf={
* @OA\Schema(ref="#/components/schemas/Article"),
* @OA\Schema(
* @OA\Property(
* property="user",
* ref="#/components/schemas/User",
* description="User Entity",
* ),
* ),
* },
* ),
* ),
* @OA\Property(
* property="pagination",
* ref="#/components/schemas/Pagination",
* ),
* ),
* ),
* )
* @return \Psr\Http\Message\ResponseInterface
*/
これらのコードを、以下の様なコマンドでswagger-phpを使用してビルドします。
swagger.jsonをビルドするコマンド
#!/usr/local/bin/php -q <?php include_once __DIR__.'/../autoload.php'; $app_path = '.'; $openapi = \OpenApi\scan( $app_path, [ 'exclude' => [ 'vendor', 'tmp', 'logs', 'tests', 'webroot', ] ] ); file_put_contents(dirname($app_path).'/docs/swagger.json', $openapi->toJson());
ビルドが成功すると、swagger.jsonが作成されるのでこれをSwagger-UIで読み込むと、以下の様にドキュメントを見ることはもちろん、Swagger-UI上からAPIを呼び出すこともできます。
これすごい便利なのでおすすめなのですが、記述するのが結構面倒なのと、記述方法にいろいろ癖があるので書くたびに毎回試行錯誤することになったりします。
前からなんとかしたいなぁと思っていたのですが、現在とあるリプレース案件で大量にAPIを作成する予定で、この作業を少しでも効率化したいと思いCakePHPのbakeテンプレートを書きました。
bakeテンプレートを自作すると、CakePHPを使っている方であればご存知のbakeコマンドで生成される雛形のソースコードをカスタマイズすることができます。
OpenApiTheme plugin
OpenApiTheme pluginでは、APIを作成する際には定番のfriends od cake CRUD Pluginを使うことを前提で作成しました。
今回は、以下の2つのbakeコマンドを追加しています。
- open_api_model - モデルのbake時にEntityにOpenApiのSchema定義を自動生成する
- open_api_controller - コントローラのbake時にCRUDのAPI定義を自動生成する
実際には以下のような感じでbakeすることができます。
// モデルのbake $ bin/cake bake open_api_model Articles // コントローラのbake $ bin/cake bake open_api_controller Articles --prefix Api
現在のバージョンでは、EntityのSchameにはアソシエーション先のプロパティはあえて含めないようになっています。 定義すると便利は便利なのですが、実際の利用シーンではどのアソシエーションをContainさせるかはAPIによって変わるケースが多いのでEntity側で定義してしまうと使いにくいことが多いです。 この為、OpenApiTheme pluginではEntityのSchameにはアソシエーションを含めずにControllerのAPI定義の方で複数のSchemaを合成(?)するようにしています。
公式のbakeでは、index actionではBelongsToのみcontainし、view acrionでは全てのアソシエーションをcontainするコードが生成されるので、それに倣って以下のようなレスポンスを定義しています。
index action のレスポンス定義サンプル
* @OA\Response( * response=200, * description="successful operation", * @OA\JsonContent( * @OA\Property( * property="success", * type="boolean", * default=true, * ), * @OA\Property( * property="data", * type="array", * @OA\Items( * allOf={ * @OA\Schema(ref="#/components/schemas/Article"), * @OA\Schema( * @OA\Property( * property="user", * ref="#/components/schemas/User", * description="User Entity", * ), * ), * }, * ), * ), * @OA\Property( * property="pagination", * ref="#/components/schemas/Pagination", * ), * ), * ),
view action のレスポンス定義サンプル
* @OA\Response( * response=200, * description="successful operation", * @OA\JsonContent( * @OA\Property( * property="success", * type="boolean", * default=true, * ), * @OA\Property( * property="data", * allOf={ * @OA\Schema(ref="#/components/schemas/Article"), * @OA\Schema( * @OA\Property( * property="user", * ref="#/components/schemas/User", * description="User Entity", * ), * @OA\Property( * property="tags", * type="array", * @OA\Items(ref="#/components/schemas/Tag"), * description="Tag Entities", * ), * ), * }, * ), * ), * ),
bakeしたままでは実際に作成したいAPIにマッチしないケースも多々あるとは思いますが、これを元に実際のAPI定義を作成することで、記述の手間をだいぶ軽減できると思います。
以下で実際にOpenApiTheme pluginで生成したAPI仕様を確認できますので、ぜひ一度見てみてください。
開発環境でのSwagger-UIの利用
普段利用している開発環境では、開発中のAPIをSwagger-UIから直接叩けるように、開発環境用のdocker-compose.ymlにSwagger-UIのコンテナも含めるようにしています。
docker hub に上がっている、公式のDockerコンテナを利用しています。
まとめ
現在進行中の実案件にもOpenApiTheme pluginを導入して使い始めていますが、仕様書作成がだいぶ捗ります。
随時フィードバックして改善していくつもりですが、ぜひ使っていただいて、要望などあればIssueなりPRなりいただければと思います。