Technote

by sizuhiko

PHP カンファレンス福岡2024に参加した

PHP カンファレンス福岡2024に参加しました。

2019年以来の PHP カンファレンス福岡。 そのときの記事がPHPカンファレンス福岡2019で、 標準インターフェースを使ったアプリケーション開発について発表してきましたです。

昨年はチケットを購入できず、先に予約していたフライトやホテルをキャンセルするという形になってしまったので、今年はちゃんとチケット購入してから、フライトとホテルを予約しました。昨年の反省が活かされている。 昨年の盛り上がりは X で確認していて、うらやましーと思ってました。そこから今年の月刊 PHP con が始まったと言っても過言ではないわけで、とても楽しみにしていました。

ちなみにプロポーザルはブログ記事にもしたGAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記APPSYNC_JS (AppSync JavaScript) で始める GraphQL API サーバーについて出したのですが、どちらも落選しました。 プロポーザルの倍率がやばすぎる(僕が普段の仕事 PHP じゃないので、旬な話題じゃないというのは否定できない事実ですがw)。

2019年が3年ぶり2回目だったので、今回は5年ぶり3回目ということになります。

前日入り

今回も前日入りしましたが、いきなり飛行機が遅延するというトラブルに。

この時点では 9:00 発が 9:40 になっただけだったけど、さらに 15分遅延となって 9:55 発に。 梅雨時期だし雨の影響もあるのでしかたがないですね。前日入りでよかったとポジティブに捉えましょう。

福岡に到着したら WeWork 大名へ。永和システムマネジメントでは東京支社がWeWork京橋内になったため、今回のように福岡に前日入りしてもWeWork拠点があれば作業できるのでとても便利。夕方からは袋詰めボランティアに申し込んでいたので、それまで作業します。

お昼はWeWork近くにあったウェストへ。

夕方ぐらいになったらWeWorkに知っている人たちが続々来て??ってなったんですが【非公式】PHPカンファレンス福岡2024・前日Meetup があったようです。気が付いてなかった….

そして袋詰めをやってきました。

そのあとは特に予定もなかったので、エールズへ。

野生のPHPerにも出会えたり、帰ろうかなーと思ったら店長から市川さん来るよ、って教えてもらったのでビールを追加して合流。 そのあとは Rummy 行って美味しいラムをいただいて、締めにやまちゃんでラーメンとビールを(3時ちょいに寝ました)。

当日

ちゃんと朝起きれて会場へ移動していたのですが、2日続けて朝にトラブルが…

100円均一で売ってるこの手の定期入れを使ってたんですが、初めて中身だけ落ちました。

祇園駅で駅員さんに天神駅へ連絡してもらって確認したら、それっぽいのがあるということでUターンして無事戻ってきました。 日本でよかった。改札通過してすぐポケットに入れた(いつもそう)ので、改札近くに落ちたのではないかと思います。 ギリギリ開始時間には間に合いました。

今回は午前中はメイン会場でセッションに参加してました。PHP養分がたくさん補充されました。

お昼は福岡の人と食べたいなーと思って、@nojimageさんに声をかけてご一緒させてもらいました。

午後はアンカンファレンス会場、スポンサーブース、廊下で交流してました。

16:00からはアンカンファレンス会場で、会社で今月2回Webアクセシビリティのワークショップをやったのを発表しました。

専門家じゃなくても良い教材があるので「みんなで勉強会てきにやってみると良いよ」といったメッセージが伝わったら良いなと思っています。

そのあとは懇親会(当日チケットあって嬉しかった)→ 非公式2次会 → やまちゃん の流れでした。 初めましての方、お久しぶりの方とたくさん話せました。楽しかった!

後日

帰京までの時間は6/23(日)「(非公式)PHP Conference Fukuoka After Hack!!」に参加して、このブログ記事を書いています。

2019年の記事の最後は

PHPカンファレンス福岡、来年も参加できると良いなー。

で終えていました。 しかし翌年それが当たり前の光景でないことがわかったのです。当時はそんなことになると思っていなかったですよね。 もちろんそういったことだけでなく、当たり前にカンファレンスがある訳でなく、多くの人の努力によって開催されているわけで感謝です。

あとはアンケートとフィードバックを入れてから帰ります。そしてまた参加できるように願っています。

APPSYNC_JS (AppSync JavaScript) で始める GraphQL API サーバー

AppSync は、AWS上で GraphQL API をサーバーレスに構築できるサービスです。

Amplify で利用されていることでも有名ですね。 Amplify から DynamoDB にアクセスするときや、通常の CRUD だけでなくクエリ条件を指定したい場合などのカスタマイズをするときは、自動生成されたリゾルバーを修正することになります。 このとき従来は VTL というテンプレート言語を学ぶ必要がありましたが、今日ではJavaScript(TypeScript)を利用できます。

最新の Amplify Gen2 でも TypeScript でリゾルバを指定できます。Amplify Gen2でNextJSのアプリケーション作成まで というクラメソの記事が参考になります。

公式ドキュメントで学ぶ

AWS のデベロッパーガイドにリゾルバーチュートリアル (JavaScript)があるので、これを読めば理解が進みます。 特にチュートリアル: DynamoDB JavaScript リゾルバーを読むと、DynamoDB にアクセスする GraphQL API の作り方が理解できるようになるでしょう。

TypeScript を使う

Amplify Gen2 を使っている場合は、すべて TypeScript で記述していて Amplify プロジェクトで管理されている Lint ルールなどを利用しているので、問題ないと思います。 一方で AppSync を直接使う(たとえば Terraform や CDK などを使って AppSync の API をデプロイする)場合は、JavaScript リゾルバーの概要の中にあるバンドル、TypeScript、ソースマップを読むと良いです。

ここを読むと、以下の理解が深まります。

  • esbuild を使って TypeScript ファイルをバンドルして 1 つの JavaScript ファイルにする方法
  • リゾルバや関数を作るときの TypeScript 設定や記述方法(主に型定義)
  • GraphQL スキーマ情報から TypeScript 型定義を生成する方法
  • Linter の設定

特に Linter の設定は重要になります。 詳しくはユーティリティeslint プラグインの設定 で確認できます。

APPSYNC_JS の制約

リゾルバや関数を JavaScript で記述できるようになったのは(VTLを書くことに比べ)嬉しいことなのですが、あくまでも JavaScript の文法が使える程度と思っていた方が良いです。もちろん単体テストコードが書きやすいなどのメリットはありつつも、制約が非常に多いことを理解しなくてはなりません。

  • コードサイズ: 32,000文字
  • 外部モジュール: npm でインストールされるようなものは、開発ツールを除いてほぼ使えないと思っていたほうが良い
  • ネットワークアクセス: できない。すべてのリソースへのアクセスはデータソースを使って行う
  • ファイルシステムアクセス: できない

どのようなものが向いているかは、データソースへの直接アクセスと Lambda データソース経由のプロキシのどちらかを選択するに書かれています。

ランタイム制約

次の制約を理解するとき読むべきはサポートされているランタイム機能です。

さきほど

あくまでも JavaScript の文法が使える程度と思っていた方が良い

と書きましたが、それがこのランタイム機能を理解した上での感想です。

サポートされていない機能は以下のとおりです。

ステートメント
  • try / catch / finally
  • throw
    • 代わりに util.error() 関数を利用する
  • continue
  • do-while
  • for
    • ただし for-in および for-of 式は例外でサポートされている。
  • while
  • ラベル付きステートメント
数学
  • 次の Math 関数はサポートされていません。
    • Math.random()
    • Math.min()
    • Math.max()
    • Math.round()
    • Math.floor()
    • Math.ceil()
関数
  • apply、bind call メソッドはサポートされていません。
  • 関数コンストラクタはサポートされていません。
  • 関数を引数として渡すことはサポートされていません。
  • 再帰的な関数呼び出しはサポートされていません。
promise

非同期プロセスはサポートされておらず、Promise もサポートされていません。

制約を理解した上で進める

と途中まででも読んだ段階で、これは JavaScript の文法が使える程度 になるということが理解できたでしょう。 外部ライブラリにこれらの記述を使っていないかというと、大抵どれか1つぐらいは該当してしまいます。 つまり実質的にテストコードを除いてバンドルするコードには、外部モジュールを使うという選択肢はなくなります。 ただ実際にはそれでは困ることが多いので、組み込むユーティリティや、ヘルパー関数などが揃っています。

リゾルバーおよび関数の JavaScript runtime 機能のページから、それらのユーティリティやヘルパーへのドキュメントページに遷移できるので、確認してください。

ただ一部 VTL のドキュメントには書いてあるユーティリティやヘルパーのうち、 JavaScript のリファレンスには記述されていないけど、利用可能なものもあります。 これらはドキュメントバグと思いますが、TypeScript で作っている場合はインポートで利用する型定義ファイルと、VTLのリファレンスを突き合わせてみると良いでしょう。

たとえば多くの Math 関数がサポートされていない代わりに、mathヘルパーがあるのですが、これは JavaScript 側のドキュメントからは漏れています。もちろん利用可能なので、どのような機能があるかは VTL 側の$util.math の math ヘルパーを読むことになります。

Lambda データソースを使うという選択

もちろん、これは AppSync のリゾルバや関数で JavaScript を利用する場合の制約であって、AppSync のリゾルバから Lambda データソースを呼び出してそちらで処理する分には制約はありません。

AppSync のリゾルバや関数の実行に関する料金が AppSync に全て含まれているのに対し、Lambda データソースで実行される計算リソースは別途 Lambda の料金がかかるので、Lambda でないとできない場合だけとしておくとコストを抑えることができるでしょう。

困っていること

AppSync の JavaScript ランタイムには様々な制約があるので、AWS のコンソールからソースコードを書いているとリアルタイムで制約違反を教えてくれます。もちろん手元のエディタを使っていても ESLint のルールでチェックできるようになっています。 とくに TypeScript で記述すると、より厳密に ESLint のルールで確認できるようになっています。

多くのシンプルなリゾルバや関数では、ここで書く困りごとには遭遇しないかもしれません。 しかし運良く?その事象を引き当てたのです。

サンプルコード

例として(実際のケースとほぼ一緒ですが)、DynamoDB データソースに対して、複数のテーブルに跨った TransactWriteItem や BatchWriteItem を実行したいと仮定します。

サンプルコードとしてAmazon DynamoDB Item Taggingという、aws-samples オーガナイゼーションにあるリポジトリを利用します。

DynamoDBに保存するコード例 は Lambda の実装ですが、ここでは書き込みのパターンについて注目してください。 このコードは、タスクを保存するときにタグ付けされていた場合は、タグの WriteRequest を配列に追加しています(L88)。

特に何の変哲も無い、DynamoDB で TransactWriteItem や BatchWriteItem を使うときに書きそうなコードですが、ここに落とし穴があります。

このコードは TypeScript で、配列に追加するとき WriteRequest 型を指定しています。タグ付けするときも追加する型は WriteRequest なので、配列に追加される型が一致するためエラーにはなりません。

同じようなコードを AppSync のリゾルバや関数で書くと TS2322 になる

では同じようなコードを AppSync のリゾルバや関数で書いたとします。 TypeScript で書いていて、 AppSync が指定する ESLint のルールなどがあっても問題なく通過するとします。 では結果を esbuild でバンドルしてデプロイしてみましょう。

なんと、デプロイすると TS2322 エラーになります。

えっ、AppSync って JavaScript はサポートしているけど、 TypeScript はサポートしてないよね?

というのが、最初の驚きです。 またまたー、と思って先頭行に // @ts-nocheck を書くと、デプロイできるようになります。

ふぁぁっ!何だと、 tsc が実行されているのか?!という疑惑が生まれてきます。

いずれにせよ、このままではデプロイできないので、神様/AWSサポート様に問い合わせを行いました。 この問い合わせ、めっちゃ最終回答まで時間かかったのです…..

で、現時点これの回避策は、先頭行に // @ts-nocheck を書く、が正式回答になります。まじかー….

現時点 AppSync では JavaScript の制約をチェックするため、デプロイ時に JavaScript ファイルを TypeScript コードとしてチェックしているそうです。まじかー….

先ほどのコードを再度確認してみてください。TypeScript のコードをコンパイルして型情報を取り除くと、以下のようなコードになります。

    const taskDbItem = {
      PutRequest: {
        Item: {
          // we set the pk and sk to the item id. we prefix both with `task#` to allow filtering by task items
          pk: `task#${item.id}`,
          sk: `task#${item.id}`,
          // we are using a gsi to allow listing all items of a certain type, which in this case is task items
          // task: GSI key sharding
          siKey1: 'task',
          name: item.name,
          description: item.description,
          // tags are duplicated here to simplify retrieval
          tags: item.tags
        }
      }
    };
    params.RequestItems[this.tableName].push(taskDbItem);

    // next we write all the tags as separate DynamoDB items. We use the tag name as the partition key, and the tag value and the TaskItem id as a composite sort key.
    if (item.tags) {
      Object.entries(item.tags).forEach(([tagName, tagValue]) => {
        const tagDbItem = {
          PutRequest: {
            Item: {
              pk: `tag#${tagName}`,
              sk: `${tagValue}#task#${item.id}`,
            }
          }
        };
        params.RequestItems[this.tableName].push(tagDbItem);
      });
    }

いうて DocumentClient.WriteRequest という型情報が消える程度ですが、これが落とし穴です。 型情報がなくなった params.RequestItems[this.tableName] の配列は、TypeScript の型推論が働くために、最初の taskDbItem 構造の型の要素を持つことが期待されます。

そこに item.tags があったときに構造が異なる taskDbItem を配列に追加しようとしたら、どうなるかわかりますね。 TS2322 です。

おそらく Amplify Gen2 でリゾルバを上書きして書いたときも、同じようなコードを書くとエラーになると推測されます(CDKがesbuildした結果をデプロイするので)。

回避策を入れる

回避策の、先頭行に // @ts-nocheck を入れる方法として、esbuild の banner オプションを利用します。 バンドルのリンティングで紹介されているサンプルコードに設定を追加します。

/* eslint-disable */
import { build } from 'esbuild'
import eslint from 'esbuild-plugin-eslint'
import glob from 'glob'
const files = await glob('src/**/*.ts')

await build({
  format: 'esm',
  target: 'esnext',
  platform: 'node',
  external: ['@aws-appsync/utils'],
  outdir: 'dist/',
  entryPoints: files,
  bundle: true,
  plugins: [eslint({ useEslintrc: true })],
  banner: {
    js: '// @ts-nocheck'
  }
})

この例のままだと、全部のファイルに // @ts-nocheck が入ってしまうので、特定のファイルだけにしたい場合は、esbuild を2つに分割して特定のリゾルバや関数だけに追加されるようにすると良いでしょう。

なお、AWSサポートへは TypeScript かつデベロッパーガイドに書いてあるとおり eslint プラグインの設定 をしてあれば、同じ効果を期待できるそうなので、全ファイルに入っていたとしても大きな問題はないのかな?とも思います。 // @ts-nocheck を入れたAPIについては念入りに動作確認をするようにコメントが入っていたこともお伝えしておきます。

まとめ

AppSync のリゾルバーや関数を JavaScript (TypeScript) で書けるようになって、VTL を書くよりも生産性があがったり、ユニットテストが書けるようになって品質を維持しやすくなるといった効果が期待できます。

一方で TS2322 に遭遇するといった落とし穴もあったりするので、注意は必要ですね。

APPSYNC_JS でそういったトラブルになった記事だったり StackOverflow の投稿だったりはまだ少ないので、問題や回避策がわかったら積極的に記事にしておくと良いかな?というのが今回ブログ記事にするきっかけにもなりました。

僕は JavaScript で書けるようになってすごく嬉しいので、今後も使っていきたいなと思っています。

CodeCommit で Renovate を使う

Renovateは依存関係の更新を自動化(PRを生成してくれたり、マージしてくれたり) するツールで、dependabot などと並んで有名なツールです。

CodeCommit で依存関係の更新を公式にサポートしているツールを探していたのですが、Renovate しか見つからなかったので、選択肢を検討することもなく導入してみることにしました。

導入方法

CodeCommit に導入するための公式ドキュメントがあります。

前回のブログCodeCommit と CodeBuild を GitHub と Actions の組み合わせのように使うで CodeCommit と CodeBuild の環境は設定済みで、かつ PR に対してビルド結果が連携できるようにしてあります。

Renovate を導入するときもこの手順は重要で、ライブラリをアップデートするときにビルドが通過するかわからないものはマージできないので、まず導入前に連携できるように設定しておきましょう。

AWS 環境の設定

ドキュメントどおり IAM や Role の設定を行います。 ここはプロジェクトによってやり方がいろいろあると思いますが、結果として CodeBuild から CodeCommit に PR が出せる様な設定になっていれば良いです。 必要なポリシーなども列挙されているので、ドキュメントをよく読んで進めれば大丈夫です。

AWS 環境ではできないこと

これもドキュメントに明記されています。

  • PR への担当アサイン
  • PRの自動マージ
  • rebaseLabel オプションの有効化

環境設定ファイルを記述する

ドキュメントでは config.js で書かれていますが、 renovate.json でも大丈夫です。 このあたりはお好きな設定ファイル形式を利用すると良いでしょう。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

スキーマ定義やパッケージルールなどを設定しておきます。 Renovate の対象リポジトリごとにリポジトリルートにファイルを設置します。

CodeBuild の設定

導入方法の選択

Renovate を導入するときの単位として、2つの選択肢があります。

  • Renovate を複数のリポジトリに対して実行する管理リポジトリを1つと、対になる CodeBuild プロジェクトを作成する
  • Renovate の対象リポジトリごとに CodeBuild のプロジェクトを作成する

前者の良いところは、Renovate の対象リポジトリが多数あるとき、管理リポジトリに設定している CodeBuild の buildspec ファイルでリポジトリ名を列挙していき、対象リポジトリ側は、リポジトリルートに設定ファイルを入れておくだけで良いことです。 ただしリポジトリごとに実行スケジュールを変更するといったことはできないので、すべての対象リポジトリに対して順番に PR が作成されていきます。

後者の良いところは、リポジトリごとに実行スケジュールを柔軟に変更できることです。ただしリポジトリ数ぶんの CodeBuild プロジェクトが必要になるので対象リポジトリ数が多くなると面倒に感じるかもしれません(IaC で構築していればそうでもないかも?)。

buildspec ファイルの設置

今回は導入リポジトリ数も少なかったのと、実行スケジュールをリポジトリごとに変更したいという要望があったので、後者の方式で導入してみました。

どちらの方法でも CodeCommit / CodeBuild では実現可能です。

前回の記事で build.yml や deploy.yml を作っているので、そこに renovate.yml といった名前で buildspec ファイルを記述します。

ドキュメントでは Docker を使う方法と CLI を使う方法が紹介されていますが、私たちは CLI の方式を採用しました。

version: 0.2
env:
  shell: bash
  git-credential-helper: yes
  variables:
    RENOVATE_PLATFORM: 'codecommit'
    RENOVATE_REPOSITORIES: '["repoName1"]'
    RENOVATE_CONFIG: '{"extends":["config:recommended"]}'
    LOG_LEVEL: 'debug'
    AWS_REGION: 'ap-northeast-1'
phases:
  install:
    runtime-version:
      nodejs: 20
  build:
    on-failure: CONTINUE
    commands:
      - npm install -g renovate
      - renovate

RENOVATE_REPOSITORIES が違うだけで、各リポジトリに入れておきます。

CodeBuild プロジェクトの作成して実行スケジュールを設定する

CodeBuild プロジェクトを新規作成してリポジトリを紐づけておきます。 あとは実行スケジュールを EventBridge に設定して、作成した CodeBuild プロジェクトを実行 ( StartBuild ) するように設定します。

ここまでで実行スケジュールに応じて PR が生成できるようになります。

セキュリティアップデートに対応する

Renovate は標準では CVE などに対応するパッチの PR は生成されません。 この機能を有効にするためのオプションは2つあります。

前者は GitHub のみサポートしており CodeCommit では利用できません。 後者は実験的な機能という位置付けのようですが、すべてのプラットフォームで動作する様になっています。

osvVulnerabilityAlerts を有効にする

環境設定ファイル(この記事では renovate.json)に設定を追加します。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "osvVulnerabilityAlerts": true
}

実は GitHub が必要なんです

ドキュメントを読むとこれだけで良いのかと思っていたのですが、実際に Renovate を実行しても CVE の PR は生成されません。

おや?っと思ってログを確認したりしたのですが、原因がわからず Renovate のソースコード自体を確認してみました。

すると、なんと OSV のデータソースが GitHub に固定してハードコードされてるじゃないですか。

該当のコード

  private async initialize(): Promise<void> {
    // hard-coded logic to use authentication for github.com based on the githubToken for api.github.com
    const token = findGithubToken(
      find({
        hostType: 'github',
        url: 'https://api.github.com/',
      }),
    );

    this.osvOffline = await OsvOffline.create(token);
  }

ドキュメントからリンクされているディスカッションには、違うデータソースを使う場合の書き方があって、OSVの任意のデータソースを利用する場合は独自にzipファイルをダウンロードしてやるのも良いでしょう。

で、そのまま GitHub のデータソースを利用する場合は、GitHub の PAT を生成して(権限は public_repo ぐらいでok) renovate.yml に設定を追加します。

env:
  secrets-manager:
    GITHUB_COM_TOKEN: "[シークレットの名前]:[シークレットのキー]"

CodeBuild の buildspec では環境変数に設定する値を SecretsManager から取得できるので、GitHub で生成した PAT はそこに格納しておいて、CodeBuild から参照できるようにしておきましょう。

プロジェクト都合で GitHub 使えないから CodeCommit 使っているのに!という声はごもっともだと思うので、そういう場合は zip ファイルをダウンロードして実行する手順を試してみることをお勧めします。

さいごに

できれば CodeCommit も GitHub と Dependabot の関係のように標準で脆弱性の PR 対応して欲しいですよね。 ビルド結果の通知とか脆弱性の対応とか、Code兄弟の機能として不足しているなーという部分の強化に期待しましょう。

CodeCommit と CodeBuild を GitHub と Actions の組み合わせのように使う

この記事は AWS の CodeCommit と CodeBuild を使って「 GitHub と Actions だったら簡単にできる PR のビルドが成功したときだけマージ可能になる」仕組みを作ってみたので、その手順をまとめました。

それって標準でできないの?

はい、 AWS の Code 兄弟だけでソースコードを管理している場合、CodeCommit には PR の機能と承認フローの制御はあるけど、ビルド結果と連動した仕組みは用意されていません。 僕も担当するプロジェクトで使うまで、それぐらいあるやろーと思っていたのですが、無いと聞いて衝撃を受けました。

前提

CodeCommit 上のリポジトリのブランチ戦略は git-flow で運用しています。 なので、 以下のようなブランチやタグが存在します。

  • master
  • develop
  • release/v9.9.9
  • hotfix/v9.9.9
  • feature/xxxx-xxx

CodeCommit と CodeBuild で git-flow を取り扱う

この時点でかなり面倒なのですが、以下のQiita記事が参考になります。

なんで StepFunctions を使うかというと、Code兄弟を組み合わせて使ったとき、 feature/* のような動的ブランチの PR に対してビルドを実行できないといった問題があるためです。 feature/* に PUSH されたときに CodeBuild が動くとかはできるんですけど、そういうことじゃないんだよ Code兄弟よ。わかってないな、というとこ。

EventBridge -> StepFunctions への連携

CodeCommit への PR や PUSH のイベントを EventBridge で受け取って StepFunctions へ受け流します。

こちらの記事が参考になります。

CodeCommit の PR や PUSH のイベントが発生したら、StepFunctions を呼び出すように設定します。

基本的には事前定義パターンでも問題ないと思いますが、EventBridgeの段階でイベントを絞り込みたい場合は、カスタムで絞り込んでも良いと思います。 対象イベントをどうするかは EventBridge でも StepFunctions で判定してもどちらでも良いでしょう。実行コストを機にするなら EventBridge でフィルタしたほうが良いですね。

たとえば PR の作成とソースの更新時にだけ動かしたい場合は、以下のようなJSONになります。

{
  "detail": {
    "event": ["pullRequestCreated", "pullRequestSourceBranchUpdated"]
  },
  "detail-type": ["CodeCommit Pull Request State Change"],
  "resources": ["CodeCommitのARN"],
  "source": ["aws:codecommit"]
}

GitHub Actions に置き換えると、以下のような部分を EventBridge や StepFunctions で設定すると思っていれば間違いないです。

on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - 'feature/**'
      - 'release/**'
      - develop
      - master
  push:
    branches:
      - 'release/**'
      - develop
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

StepFunctions のフロー

参考記事に StepFunctions から CodeBuild を実行する例が出ているので、そちらを参考にしてください。

フローとしては以下の様な流れを設定しています。

  1. PR かどうか判定する $.detail-typeChoise で判定して分岐
    1. PR だったら CodeBuild の build を実行して、 ResultPath: $.BuildResult に結果を格納
    2. ビルド結果を CodeCommit に通知する Lambda を実行
  2. 1 の ELSE
    1. PUSH になるので、ブランチ $.detail.sourceReferencerefs/heads/developrefs/heads/master , refs/heads/release にマッチしていたら CodeBuild の deploy を実行して、 ResultPath: $.BuildResult に結果を格納

実際はチャットにもビルド結果を通知したりしているので、フローはもう少し複雑ですが、大まかには上記のとおりです。

StepFunctions から CodeBuild を呼び出す時の設定可能なパラメータなど。AWSの公式ドキュメントを見るとわかりやすいです(翻訳のタイトル変だけどw)

Step AWS CodeBuild Functions による呼び出し

git-flow の CodeBuild プロジェクトを設定するときは、ソースの設定を指定せずに PR のソースブランチに対してビルドするようにします。

新機能 – Step Functions と AWS CodeBuild を使用した継続的インテグレーションワークフローの構築の記事で CodeBuild を実行するときの StepFunctions 定義は以下のようになっています。

    "Trigger CodeBuild Build With Tests": {
        "Type": "Task",
        "Resource": "arn:${AWS::Partition}:states:::codebuild:startBuild.sync",
        "Parameters": {
          "ProjectName": "${projectName}"
        },
        "Next": "Get Test Results"
    },

Parameters に以下のパラメータを追加して、特定のソースブランチに対してビルドできるように設定します。

  • SouceTypeOverride: “CODECOMMIT”
  • SourceLocationOverride: CodeCommit のURL
  • BuildspecOverride: 実行するビルドスペックのパス
  • SourceVersion: ブランチ名(event.detail.sourceReference)

CodeBuild でやること

CodeBuild では分岐処理を書くとしてもシェルの if 文を使うことになるので、そういったケース分けは StepFunctions で分岐して、buildspecfile を分けておいて BuildspecOverride で切り替える様にしておいた方が良いです。

GitHub Actions でいうと、単純なジョブが1つ書けるだけと思っていたほうが良いです。

CodeCommit にビルド結果を通知する

では CodeBuild の結果を CodeCommit に通知してみましょう。 ここは連携の仕組みはないので、 CodeCommit のコメント欄と承認機能を利用します。

まずビルド結果を CodeCommit にコメントして、ビルドが成功していたら承認する Lambda を実装します。

import { CodeCommitClient, PostCommentForPullRequestCommand, UpdatePullRequestApprovalStateCommand } from "@aws-sdk/client-codecommit":

export const handler = async (event) => {
    const client = new CodeCommitClient();
    const buildResult = event.BuildResult.Error ? JSON.parse(event.BuildResult.Cause) : event.BuildResult;
    const buildUrl = `https://リージョン.console.aws.amazon.com/codesuite/codebuild/コードビルドID/projects/${buildResult.Build.ProjectName}/build/${buildResult.Build.Id}`;
    const icon = event.BuildResult.Error ? '' : '';
    const content = `${icon} ビルド [${buildResult.Build.Id}](${buildUrl})`;
    const input = {
        pullRequestId: event.detail.pullRequestId,
        repositoryName: event.detail.repositoryNames[0],
        beforeCommitId: event.detail.sourceCommit,
        afterCommitId: event.detail.destinationCommit,
        content,
    };
    const command = new PostCommentForPullRequestCommandI(input);
    await client.send(command);

    if (!event.BuildResult || event.BuildResult.Error) {
        return;
    }
    const approvalInput = {
        pullRequestId: event.detail.pullRequestId,
        revisionId: event.detail.revisionId,
        approvalState: "APPROVE",
    };
    const approvalCommand = new UpdatePullRequestApprovalStateCommand(approvalInput);
    await client.send(approvalCommand);
}

本当は絵文字じゃなくて、バッジを使いたかったのですけど、CodeCommit では利用できる Markdown 記法に制限があって、外部画像は利用できないようです。 CodeBuild にもバッジ機能はありますが、 git-flow のように feature/* のワイルドカードブランチで、 StepFunctions からビルド対象ブランチを指定して実行する場合にはバッジが作れないので、それも使えません。 ここは絵文字一択でしょう。 あとは、ビルドが成功していても失敗していても、ビルド結果に飛べるリンクを付けることで確認しやすくなります。

この Lambda からは以下の2つのコマンドを利用するためのポリシーが必要になるので、忘れずにアタッチしましょう。

  • PostCommentForPullRequest
  • UpdatePullRequestApprovalState

ビルドが成功したときだけマージ可能にする

上記の Lambda でビルドに成功したときだけ承認を実行するようにしました。 CodeCommit では「承認ルールテンプレート」を設定できます。

以下のルールを前提とします。

  • CI でのビルドが成功している
  • PR 作成者以外の人が承認している

まず前者の承認ルールテンプレートを設定してみましょう。

  • 必要な承認の数: 1
  • 承認プールのメンバー
    • IAMユーザー名または引き受けたロール
    • 値: ビルドが成功していたら承認する Lambda のロール名
  • ブランチ名: develop

git-flow では feature/* から develop に向けて PR を出すので、ブランチフィルターを設定しておくと良いです。 また、Lambda からの承認であることを判定するには、Lambda に割り当てているロール名で判定するのが楽だと思うので、ロール名で判定すると良いでしょう。

続いて後者の承認ルールテンプレートを設定してみましょう。

  • 必要な承認の数: 2
  • ブランチ名: develop

ここでは承認者の絞り込みはしておらず、2つの承認があったらというルールにしています。 プロジェクトによっては特定の人の承認を必要とする場合もあるでしょうから、そのあ場合は承認プールのメンバーを設定して必要な承認数を指定してください。 ここでは誰かしら1人以上が承認してくれたら、という前提になっています。

ではなぜ必要な承認の数が 2 なのか?というと、CIでの承認数1と人の承認数1を合わせて2以上という形にしています。 人が2人以上承認していてもCIが失敗していた場合は、前者の承認ルールを満たしていないのでマージすることはできません。

ビルド成功で1、人の承認が1つ以上でルールが満たされます。

さいごに

これらの手順で CodeCommit / CodeBuild を使ったときに GitHub と Actions で簡単にできていたワークフローを実現できます。

面倒だ、面倒すぎる、と思いますよね。

できれば Code兄弟を避けていきたいのですが、プロジェクト事情でそれ以外の選択肢が選べないこともあるでしょう。 そのようなときに、この記事が参考になればと思います。 ここまで全体の手順を解説してくれる記事が見当たらなかったので、細かい設定は置いておいて大まかなら流れをベースに解説しました。 それぞれの部分(たとえばStepFunctionsとCodeBuildの連携)といった記事は検索すると見つかるので、具体的な実装方法はそれぞれの最新情報を確認ください。

Code兄弟さん、もっと便利になってくれないかな、と願う日々です。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 5

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 4 の続編となります。

GAEにデプロイして動作確認しながら修正

前回で Slim4 へ移行でき、少し動く様になってきたので動作確認しながら微修正していきます。

GAE 2nd gen 用の設定変更

app.yml ファイルに定義していたデプロイ対象外ファイルの一覧 skip_files は別のファイル .gcloudignore に記述するように変更になったので、定義を移行しました。そのコミット

Carbon のバージョンアップ

8.2 環境で動くように Carbon を 1.21 から 2.72 にアプデしました。そのコミット

composer ファイルをデプロイ対象に追加

GAE 2nd gen からは composer での依存関係はGAEデプロイ時に解決されるので、デプロイ対象ファイルに composer.json と composer.lock ファイルを追加しました。そのコミット

ミドルウェアの定義を変更

ミドルウェアがうまく動作していないことがわかったので、調べていると書き方が変わっていることに気づきました。すでにパラメータは変更していたのですが、実行メソッド名も変更になっていました。

そこでt PHP5.5 のときは interface を実装するコードになっていなかったので、ちゃんと MiddlewareInterface を実装するようなコードに修正。

use Psr\Http\Server\MiddlewareInterface;

class AllowedProvidersMiddleware
class AllowedProvidersMiddleware implements MiddlewareInterface
{

そうすると、以前は __invoke メソッドで定義していたところを process に変更する必要があることがわかりました。 その修正コミット

セッションの処理も修正

さきほどのコミットでは、セッション保持の Memcache がうまく動作していなかったので、いったん session_set_save_handler はコメントアウトして、デフォルトの tmp 管理にしています。ただ複数インスタンス起動するとうまく動作しなくなるので、いったん動作確認を進める上での暫定対応です。

またセッションミドルウェアも全体に対して有効にするのでなく、セッションが必要なAPI(ここでいうと管理画面のログイン周辺)についてだけ有効にするようにルーティングを変更しています。

その後、Google Cloud の Cloud Datastore に DatastoreSessionHandler というものがあるのがわかり、以下のように設定を変更しました。そのコミット

use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Datastore\DatastoreSessionHandler;

    $datastore = new DatastoreClient();
    $handler = new DatastoreSessionHandler($datastore);
    session_set_save_handler($handler, true);
    session_save_path('sessions');

POST のボディに JSON を渡す場合の対応

Slim4 では addBodyParsingMiddleware を利用する必要があったので、修正しました。そのコミット

併せて BASIC 認証時の before 処理も不要であることがわかったので削除しています。

GAE へのデプロイ方法を README に追記

GAE 2nd gen からはビルド結果を以下のコマンドでデプロイします。

gcloud app deploy --project toiletevolution --version 2 --no-promote --appyaml=app.yml

version を指定して新しいバージョンで動作確認を可能にして、 --no-promote をつけることで従来のURLからのアクセスは、新しいバージョンに振り分けられない様にします。

そしてどうなったか

この記事は PHP のバージョンアップをメインにしているのですが、実際には WebComponents 側も修正しています。

そしてシュミレータで一ヶ月ぐらいの稼働テストを実施して、問題なさそうだったので某日に正式リリースを実施しました。 その後、新しいバージョンでの問題も発生せず順調に動いています。

これでしばらくは落ち着くのと、バージョンアップ追従もどんどんできるようになっていくので安心です。

ブログの記事にまとめてみると、そんなに大変な修正でもなかったな?という感じですが、当時は久々に PHP 触ったりするのもあり、結構大変でした。

今後は phpstan とか入れたりして、コードの品質も上げていければなと思っています。