PHPを書いていると生き生きするPHPer

稲葉太一氏:「CDKでデプロイ先を量産したり環境ごとの差をどうにか埋めたりした話」というタイトルでお話しします。ネット上では「ちゃちい」という名前で活動している、稲葉といいます。よろしくお願いします。

軽く自己紹介をします。PHPを書くPHPerです。PHPを書いていると生き生きします。基本的に受託の会社に勤めていたり、フリーランスとして活動していたり、自分の会社で仕事を請けたりしています。完全に受託のエンジニアです。いわゆるLAMP構成からエンジニアを始めているので、“古スタックエンジニア”です。

登壇活動はあまりしたことありませんが、「PHPerKaigi 2021」の時に、どうしてもPHPで「Lambda」を使いたいという話をしました。今日から始まる「PHPerKaigi 2022」では、PHPについてパンフレット掲載原稿を書きました。ライブでの登壇は初めてなので、どうぞ生暖かい目で見守ってください。

新プロジェクト開始でAWS CDKの利用を決意

本編に入る前に、ここから先はフィクションです。お客さんがいる受託の話なのでフィクションにしています。

一応フィクションの中の実際の問題として、インフラの担当がいません。勤め先にも僕の会社にもインフラ担当というエンジニアがいないので、バックエンドエンジニアである僕がどうにかインフラを構築したという話です。

「ああ、そういうつらいことあるなあ」と思ったらぜひツイートしてください。救われます。「そこはインフラ的にはいまいちかもな」という視点があれば、こっそり教えてもらえるとありがたいです。

受託案件ごとにいろいろなメンバーから「CloudFormation」や「Terraform」をやってみたいと言われることがありました。特に断る理由はないので……というよりインフラに対する知見がなにもないので、ぜひチャレンジしてほしいと思った結果、生まれるのはカオスです。

案件のアプリケーションの開発に注力しなければいけない、リソースが足りなすぎるというタイミングだったので、どうにか統一したい、習得と運用の両方をできる限り低負荷にしたいという思いがありました。

そんな中で新しいプロジェクトが始まりました。まっさらな状態から始められる仕事が降ってきたんですね。せっかくだからきちんとIaCしたいなと思いました。

これは僕の個人的な思いですが、Terraformの記法が苦手です。YAMLを書くのも苦手です。案件はAWSが大前提だったので、どこで知ったのかは憶えていませんがCDKというものに出会いました。

AWSを使い倒したいと思った理由

AWSを使いまくって、アプリケーションコードで楽できるところは楽したいと思いました。Webフレームワークが持っているような認証の機能は使いたくない、もしかしたらフレームワークを変えるかもしれないという考えから「Cognito」を使ってみようと決意しました。

これまでの案件は、EC2のインスタンスがボコボコ立っていて、「CodeDeploy」で、オートスケールで、時々イベントがあって、スパイクしてぜんぜんアクセスできなくなったりするみたいなことがあって、インスタンスの管理をしたくなかったので、すごく単純な思いでLambdaを使いたいと思いました。

また、RDSのインスタンスも気にしたくないし、今回始める仕事はそれほどアクセス数が多くなるものではない前提だったので、「Aurora Serverless」を試しに入れてみようと思いました。

スライドは括弧書きにしましたが、v2はずっと待っています。CORSで引っかかりたくないので、「CloudFront」ですべてのリクエストを受けて、その背後で静的リソースはS3で、PHPで書かれたアプリケーションは「API Gateway」に投げて、という構成にしたいと考えた結果、ごちゃごちゃするんですね。さらに本番もステージングも、その構成を作りたいわけです。基本的にどの現場もそうだと思うのですが、本番とステージングは同じ構成にするのが大前提だと思います。

アプリケーションを動かすには設定する項目が多いのでは?を解決した閃き

(※スライドの「6.AWSを使い倒したい(当社比)」)について軽く4つ抜き出しましたが、これ以外にも、これを動かすための証明書はどうするんだとか、「Route 53」は? とか、諸々引っ張られて、それを×2やるのはなかなかしんどいと思いました。

CDKは、コマンドCLIで、--contextというオプションで、外から値を渡せることを知りました。本番用の値とステージング用の値を渡すことで、その変化を吸収すればいいかなと思ったのですが、このアプリケーションを動かすには設定項目(コンテキストで渡す値の量)が多くなるのではないかなと。これはさすがにしんどいなと思いました。

そうしたら、cdk.jsonにcontextというキーがありました。これを調べると、CDKの中でapp.node.tryGetContextなんとかとした時に、スライドのような優先順序で値が取得できることがわかりました。1番目に、「現在のAWSアカウントから自動的に」。2番目が「CDKコマンドで設定した--contextオプションの値」。今話した内容です。

その上で、「プロジェクト内のcdk.jsonファイルのcontextキーの値」。それでもなければ「実行ユーザーのホームディレクトリ直下にある.cdk.jsonファイルのcontextキーの値」。最後に、動的にと言ったらあれですが、「CDKのコードの中でsetContextメソッドを呼んでセットした値」という順番で読み込まれます。

ここで閃きました。cdk.jsonはJSONなんだからオブジェクトを突っ込めばいけるのでは? そうすればこのアプリケーションを動かすためのたくさんの設定項目を収められるんじゃない? と考えて、試したらいけました。

ということで、コンテキストにオブジェクトを入れます。(スライドを指して)これはcdk.jsonの一部です。contextがあって、その中にparam1で、String文字列。param2で、オブジェクト。オブジェクトの中にまたキーと値がある状態で、CDKの中で値を取ってみると、app.node.tryGetContext('param1')だと、文字列のfugafugaが返ってきます。param2は、きちんとオブジェクトのかたちで返ってきます。それなら環境によって取り出すコンテキストを切り替えればいいのではないかと思いました。

ちなみに、CDKを使っているとcdk.context.jsonというファイルができますが、これはCDKが管理するファイルらしいので触らないようにします。

すごく短い例を作ってみました。これもcdk.jsonですね。contextの中にproductionというキーと、stagingというキーのオブジェクトを作って、オブジェクトの中身にfqdnというキーで文字列を入れています。見ればわかると思いますが、本番環境のドメイン名はこれだよ、ステージング環境のドメイン名はこれだよ、としたいですね。

CDKのCLI、deployを叩く時に--context stage=stagingと渡して実行すると、CDKのコードの中でapp.node.tryGetContext('stage')。stageを渡しているので、stageを取ってくるとstagingが返ってきます。

stagingという文字列がこのstageに入っているので、これをまたtryGetContextに渡します。すると、目当てのオブジェクトが返ってきます。stageというコンテキストは、あえてcdk.jsonには書きません。このcontextの中にstageという名前のキーが存在しないようにすることで、CLIでの指定を必須にしています。

stageというキーがない状態で実行してもTypeScriptがエラーになってくれます。たぶんtryGetContextの結果がnullになって、tryGetContext(null)とやったら、「型がおかしい」というエラーになるので、ここで安全性が保たれます。

環境ごとに切り替えたい値の例

環境ごとに切り替えたい値とは、実際にどんなものがあるか。まず、別々のAWSアカウントが用意できる場合。例えば、本番環境とステージング環境のAWSアカウントが別、あるいは作業をする開発者ごとにアカウント発行してますよという時のアカウントIDを収める。ちなみにAWSアカウントが複数用意できない場合、例えば開発者用はひとまとめでこのアカウント、という場合についてはこのあと話します。

次に、CloudFrontなどのAレコードを設定するためのホストゾーンID。Route 53のホストゾーンをCDKで作って、例えば本番環境でIaCで管理させると、消えちゃうんじゃないかという怖さがあります。なので、消えると怖いものについては、あらかじめAWSのコンソール上で手動でポチポチ作って、それをCDKで扱えるようにするために必要な値、例えばARNや、この場合はホストゾーンIDをcdk.jsonのcontextに入れるようにしました。

ほかに、LambdaのメモリサイズやAurora Serverlessのキャパシティ、自動停止までの時間など、要はスペックに関するところですね。本番だとメモリサイズは1GB欲しいけれど、開発環境なら128でいいなとか、費用を抑えるための調整がcdk.jsonの中で設定できます。

また、これは僕が実際に案件でやった話ですが、「ECR」や「Docker」のリポジトリのアドレスも、ECRは別途リポジトリとしてCDKで管理させずに固定で用意して、そのアドレスをcontextに埋め込みました。

あと、Webアプリケーションを開発する方ならわかると思いますが、アプリケーションを実行環境に渡す環境変数の値によって挙動を変えることはよくあると思います。envファイルという便利なソリューションがありますが、これは非常によろしくないので、強い意思で、環境変数の値はすべてアプリケーションリポジトリの中ではなく、CDKのインフラ側の知識として持たせるようにしました。

環境ごとにAWSアカウントを用意できない時の対処法

先ほどAWSアカウントの話をしましたが、基本的にAWSアカウントは環境ごとに用意したほうがいいです。本番とステージングはまったく別にしたほうがいいでしょう。これも先ほど話しましたが、開発者が使うアカウントを個別に用意する、1人ずつ発行するのはなかなかしんどい。僕はそういう環境にはいませんが、軽率にアカウントを発行できない会社もあるのではないかなと思います。せめて、この案件の開発用のアカウントは出してほしいと思います。

では、個別に用意できない場合。つまり「開発者はみんなこのAWSアカウントを使え」という場合、素直にCDKをイチから始めるとスタック名の重複が起きてしまいます。

例えば、AさんがデプロイしたInfraStackという名前のものと、BさんがデプロイしたInfraStackは、後書きで上書きし合うので、CDKでプログラムを書くことで回避しましょう。

スタック名を重複させない技

スタック名を重複させない技。これはCDKをある程度使っているほとんどの方がやっていることだと思うので、今更の話かもしれませんが、ステージ名は先ほどと同じようにtryGetContextで取ってきます。InfraStackに渡すIDのところをInfraStack、ハイフン、stage名(※スライドでは「InfraStack-${stage}」)とします。例えばproductionとstagingならInfraStack-productionとなり、スタックが重複しなくなります。開発者の名前をステージ名にして運用するのはどうでしょうか。

先ほどお見せしたものを、Aさん用とBさん用に拡張しました。productionとstagingのほかに、a-sanやb-sanというキーでオブジェクトを用意しています。そうすると、同じAWSアカウント内でもInfraStack-a-sanとb-sanになって競合しなくなります。スタック名が重複しない前提でCDKを組んだほうが、あとからつらい目に遭わずに済みます。

最初は動的にスタック名を生成しない実装でデプロイして、あとからやはり重複しないようにしようとすると、名前の規則性が変わってしまいます。例えば、本番リリース前なら作り直してもいいですが、一度公開しちゃうとつらいことになると思います。

(次回へつづく)