The API with ID XXXXXXXXXX doesn’t include a resource with path /* having an integration arn:aws:lambda:ap-northeast-1:XXXXXXXXXX:function:product-send-mail on the ANY method.

課題

Lambdaの実行権限をAPI Gatewayに与えると、どういうわけかタイトルのエラーメッセージが、コンソール上で、下記のように赤字で表示された。

20221107225848

解決

表示の問題にすぎないので気にしなくていいらしい。自分の環境ではAPI GatewayからLambdaを実行することができていた。

ちなみに操作によって消せることもあるそうで、呼び出し元のARNの末尾の///*となっている部分を

--source-arn "arn:aws:execute-api:ap-northeast-1:[アカウントID]:[API Gateway ID]/*/*/*"

ちゃんと[Stage]/[Method]/[Resource]と指定すればよいそうである。

--source-arn "arn:aws:execute-api:ap-northeast-1:[アカウントID]:[API Gateway ID
]/v1/POST/send-mail"

試してみたところ、実際綺麗に表示されるようになった。

20221107225844

【日記】個人サイトをリニューアル

ここ数年くらい放置していた個人サイトを、技術検証も兼ねてリニューアルした。

といってもメインコンテンツはブログなので、個人サイトのほうはトップページとお問い合わせフォームくらいしかないのだが、

  • Visual Studio Codeの活用。
  • 同じくWSLとコンテナ(Docker)も利用してのローカル開発環境の整備
  • ヘッドレスかつサーバーレスなfront(Next.js)とback(Python)の分離
  • TerraformによるIaC
  • GitHub Actionsを使ったCI/CD

を行ったかなりの意欲作である。

ほとんど裏側の話のため、利用者にはあまり関係がないのがさみしいところだ。

やってみた感想

Visual Studio Code

Visual Studio Codeがさらにすばらしくなっている。元々Visual Studio大好きだったので使い勝手もよく似ている点も個人的にうれしいところ。

LinterやFormatter等のツールとも連携できるのはもちろん、コンテナ上でのリモート開発までできると来ており、文句なし。

WSLとコンテナ(Docker)も利用してのローカル開発環境の整備

ながらくVagrantを使っていたのだが、Docker desctopを使うために鞍替えした。コンテナを使うと設定や通信がやや複雑化するものの、気軽に立ち上げて気軽に捨てられる他、いつ立ち上げても同じ(おおむねは)環境が用意できるのは安心感がある。一人で開発している場合でも便利だが、チーム開発では威力を発揮してくれるに違いない。

ヘッドレスかつサーバーレス

安い、安い、実際安い。月200円いくかどうかというところじゃなかろうか。

しかも早いし堅牢である。FrontendがCloudFront+S3、BackendがAPI Gateway + Lambdaなので、ちょっとやそっとスパイクしても――してほしいものだが――ダウンどころか遅延さえしないであろう。もっともその場合はコストに反映されるのだが、それでも全てEC2で賄うことを考えれば全然安いほうであろう。

かねてから十全にS3へのオフロードをしたいと思っていて、Hugoを使いつつ頑張っていたのだが、やや使い勝手に欠ける面が無きにしも非ずだった。今回は割と満足いく使い勝手になった気がしている。ある程度の規模があるプロジェクトだと、おそらく素直にコンテナ+Fargateの構成が開発効率がよいと思うが(思いっきりマイクロサービス化するならともかくとして)、小規模なプロジェクトだとAPI Gateway+Lambdaは素晴らしい選択肢になると感じる。

ただ一方で、API GatewayのリソースをTerraformで構築するのは、業務用の本番環境にはお勧めしたくない選択肢という感があった。開発・運用で頻繁に更新をかける類のリソースはTerraformと相性がいまいち良くない。

TerraformによるIaC

コードとして書き上げるまでに本当に本当に本当に手間がかかるのだが、一方で、設定の統一が大変行いやすいというメリットは捨てがたい。雑にTerraform applyを欠けただけでも設定がすっきり統一されるのは爽快である。また、どのようなリソースをどの流れで作ったのか、確実にドキュメント化されているという安心感があるのも管理上見逃せない。コンソール上でポチポチ手作業で作るのは臨時の作業くらいなものであった。むしろこれまでに手作業で作ったリソースもTerraform化を進めたくらいである。

作業効率の不利さもコードの再利用が進めば軽減できるのではないかと期待している。今回、あとで流用できるようにコードはほぼ全部Publicリポジトリで公開するつもり。

GitHub Actionsを使ったCI/CD

CodePipelineも便利なのだが、GitHub Actionsはgitなのにディレクトリ単位でCI/CDの起動を管理できる点が見逃せない。LambdaについてはMonorepo構成を取ったので――さすがに関数ごとにリポジトリを切りたくなかった――これは嬉しい点。

おそらく個人サイトのほうはそこまで頻繁に更新しないのでメリットが目立たないが、頻繁に更新するのなら(すべきなのだが)CI/CDで自動化できているとやはり楽。しかも手作業がないのでオペミスの心配もない。構築するまでが大変だが、いったん構築できてしまえば大変幸せになれる印象。

本番環境へのデプロイはどう行うべきなのか?

ITサービスの開発・運用を行うにあたって避けて通れないのがデプロイである。プロジェクトのたびに一から考えると(そしてドキュメント化するのも)手間がかかるので、よくある方針や例を記しておく。

方針

  1. 手順をなるべく自動化して手作業は最小限にできる
  2. 効率化
  3. オペミスの防止
    • 手動の作業はミスがつきものなので
  4. 環境間で動作を合わせられる
  5. 違っていると不具合や事故の元なので。
  6. ロールバックに対応できる
  7. デプロイした内容にバグが見つかるのはザラ。とりあえず切り戻しができれば被害は最小限になる。
    • できなかったらHotfixとかで直すまでサービスダウンまであり得る。怖すぎ。
  8. 問題はDBのスキーマなど、別に依存がある場合。互換性があればいいが、ないとロールバックは無理。
    • マイグレーションすればいい、という考え方もあるが、本番のDBスキーマデグレードをかけるのは往々にして無理筋。
    • あと分散モノリシックな造りだと、API同士のバージョンに互換性が必要だったりもするのでさらにツライ。
  9. ダウンタイムを極力短くできる。できればゼロ。
  10. ダウンタイム=逸失利益なので、これは当然。
    • ただしゼロといっても厳密には0ミリ秒はありえなくて、ごくわずかな時間繋がらなくなっているはずだが、リトライするなどして認識されないため、ゼロダウンタイム(と思ってもらえる)。
  11. 事前にアプリケーションの動作確認、テストを行える
    • 本番環境で動作確認はなかなか無理がある。
  12. デプロイのログが残る。何かあったら通知が出る。
    • 不測の事態が起きた際、ログがなかったらお手上げ。通知もないと気づけない。

それぞれどう実現すればいいか?

  • 自動化
    • CI/CDツールを使う。CodePipelineやGitHub Actionsなど。
  • 環境間での動作の統一
    • できるだけ同じスクリプトで動作させればよい。環境間の違いはなるべくパラメータでカバー。
  • ロールバックへの対応
    • ECSを使えるならタスクを戻すだけ。
    • EC2に直でデプロイしている場合はシンボリックリンクなどで切り替える。
    • Lambdaを使っているならエイリアスに紐づいたバージョンを戻す。ただし呼び出す際にエイリアス付きで呼び出している必要あり。
    • CloudFront+S3の構成は難しい。どうしてもとあらばLambda@Edgeを使う方法のようにS3の前に切り替える仕掛けを仕込むことになるはず。
      • とはいえ大がかりすぎてコストやオーバーヘッドが気にかかる。前回のデプロイ時のファイル群をs3 syncで戻すくらいが落としどころか。運用でカバーである。
  • ダウンタイムの極小化
    • 切り替え式のデプロイを行っていれば切り替えにかかる時間だけとなる。
      • それが難しいCloudFront+S3だと上書きになってしまう。一瞬表示が崩れるなどのことは覚悟せざるを得ないかもしれない。
    • ただしAuroraのアップデートなどがあると専用の大がかりな仕掛けが必要になるはず。
  • 事前の動作確認
    • 環境を分ける。いわゆるstagingやpreと名のつく環境を本番と同じ設定で用意。
  • デプロイのログが残る。何かあったら通知が出る。
    • ECSのように元からログの記録とセットのものを使う。そういうものがなければ、デプロイスクリプトの中で自らログを残す。通知も同様、既製品の中にあればそれを使い、なければ自前でセット。

構成例

環境の分離

  • 本番環境(product)
  • テスト環境(staging)

デプロイフロー backend(ECSの場合)

  1. 複数の環境用設定(e.g. product.env, staging.env)を格納したコンテナイメージを作成。
  2. それをstaging環境で起動して確認。
  3. 同じソースコードからコンテナイメージを作り直してproduct環境にデプロイ。

※ 上記はCI/CDで実行する。Github Actionsとか。

デプロイフロー backend(API Gateway + Lambdaの場合)

  1. staging環境にデプロイして動作を確認
  2. 問題なければ同じソースコードからproduct環境にデプロイ。

余談:昔話

クラウドがまだ流行していないオンプレの時代は上書き式のデプロイがよく見られた。Dockerはまだ実用に耐える技術として普及しておらず、AWSのように切り替えるポイントがいくつもあるわけでなく、またサーバー自体も気軽に増減などさせられなかったから、ある意味最適でもあった。下手するとバージョン管理システムが使われていないプロジェクトもあったくいらいで、バージョン管理システムとデプロイツールの連携どころではなかった。今から思うと凄い時代である。その時代はstaging->productで同期させるのが堅いやり方だった。が、2022年現在、gitも普及している上、CI/CDのツールも豊富である。当時のノウハウはもう余程のことがなければお蔵入りさせてよさそうだ。

デプロイフロー frontend(Cloudfront + S3)

  1. staging環境にデプロイして動作を確認
  2. 問題なければ同じソースコードからproduct環境にデプロイ。

CORSで困ったときのリンク

先日CORSのエラーに散々悩んだので、解決のための参考になったリンクを記載。

ちなみに構成はFront(Next.js) + Back(AWS API Gateway + Lambda(Python)) 。

リンク

たとえば

Front(Next.js) -> AWS API Gateway -> Lambda(Python) という構成だと、どこか一つにミスがあるだけでCORSに失敗する。当たり前ではあるが。

そのため、OPTIONで投げるリクエストとレスポンス、そして本命のリクエストとレスポンス、と細分化して一つ一つ切り分けていくのが遠回りなようで確実な道。CORSとひとまとめにくくって調べるとトラブルシューティングには苦労する。というか、苦労した。

CloudFrontのCNAMEは重複不可

問題

新旧の個人用サイトを並行で動かそうとしたところ、CloudFrontの作成でエラー発生(CNAMEAlreadyExists)。

module.personal_website_frontend.aws_cloudfront_distribution.front_cdn: Creating...
╷
│ Error: error creating CloudFront Distribution: CNAMEAlreadyExists: One or more of the CNAMEs you provided are already associated with a different resource.
│       status code: 409, request id: d5f61e56-8789-4443-bd73-5928d7f0f2de
│
│   with module.personal_website_frontend.aws_cloudfront_distribution.front_cdn,
│   on ..\modules\personal-website-frontend\main.tf line 45, in resource "aws_cloudfront_distribution" "front_cdn":
│   45: resource "aws_cloudfront_distribution" "front_cdn" {
│
╵

CloudFrontは同じCNAMEを使ったDistributionを作成するのはダメな仕様らしい。

しかしそれではダウンタイムなしに切り替えができない。

解決

この辺ClassMethodさんにダウンタイムなしでの実行方法があった。

ただ、かなり複雑な作業なので、客先の本番環境で動いているサービスにこれを実行するのはかなりの慎重さがいりそう。

意外な弱点があるもの。

Terraformの*.tfstateファイルを保存するためのS3のバケットをCLIで作る

TerraformのtfstateファイルをS3に保存する場合、そのバケット自体はTerraformを使わずに作成することが推奨されている。

というわけで、CLIを使ってバケットを作成することにした。

コマンド

先にコマンドを記載する。結論、以下のようにした。

aws s3api create-bucket --region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1 --bucket [BUCKET_NAME]
aws s3api put-bucket-versioning --bucket [BUCKET_NAME] --versioning-configuration Status=Enabled
aws s3api get-bucket-versioning --bucket [BUCKET_NAME]
aws s3api put-public-access-block --bucket [BUCKET_NAME] --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api get-public-access-block --bucket [BUCKET_NAME]
aws s3api put-bucket-encryption --bucket [BUCKET_NAME] --server-side-encryption-configuration "{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"AES256\"}}]}"
aws s3api get-bucket-encryption --bucket [BUCKET_NAME]
aws s3api put-bucket-tagging --bucket [BUCKET_NAME] --tagging "TagSet=[{Key=Environment,Value=[ENVIRONMENT]}]"
aws s3api get-bucket-tagging --bucket [BUCKET_NAME]

[BUCKET_NAME]には実際のバケット名が入る。また、[ENVIRONMENT]には環境名が入るのだが、タグのつけ方は状況にあわされたし。

コマンドの説明

バケットは以下の方針で作っている。

  • バケット名は[環境名]-[用途]-[ユニークにするための文字列]とする。
    • e.g. product-log-abcdefgh
  • バージョニングを有効化する。
    • うっかり消してしまうと悲惨なので。
  • セキュリティのため、パブリックアクセスブロックは全部ブロック。またサーバーサイド暗号化も有効化する。
    • 個人で使うTerraformなのでAmazon S3 マネージドキーによる暗号化で済ませているが、より強固にしたいならKMSタイプを。
  • タグも付けておく。

S3のバケットをどのように分けるか?

S3を利用するにあたっては、通常、いくつかのバケットに分けて利用することになる。一つのバケットに全部おさめるとバケットの中がカオスになるからだ。

だが思いつき次第にバケットを作っていくのも不味い。今度はS3自体がカオスになってしまう。そもそも1アカウントで作れるバケットの数も上限がある。(緩和できるけど)

従って最適な分け方をしたいわけだが、どうすればいいだろうか。

基準は?

AWSの中の人が考え方を公開してくれている。

  1. 人が見て直感的に分かりやすい単位で分ける。
  2. バケット単位の設定を変えたい場合はバケットを分ける。
    • バージョニング、ライフサイクルポリシー、オブジェクトロックなど
  3. バケット単位のサービス上限に達しないよう分ける。

基本、これを元に考えていけばよいと思う。基本は1でまず分け、次に必要に応じて2の条件の違いで分ければよいのではないか。3は制約条件だから、抵触しそうなら気を付ける、で良いと思う。

具体的には?

私見ながら次のように考える。

  • まず環境、特に本番用途は必ず分けたい。オペミス防止のためのフールプルーフとして、保全の必要性が高いデータは分離しておくべきである。
  • 一般公開用も同じ理由でその他の非公開用バケットから分離したい。機密情報と公開用HTMLが一緒に入っている状況はあまり嬉しいものではない。おそらくバケットの設定も異なる。
  • ログも他と混ぜずに分離しておきたい。Athena等から別途使うことがありえるから、ということもあるし、ライフサイクルルールやバケットポリシーも設定したいからでもある。

それを踏まえ、環境が仮にproduct/staging/developの3点、単位を公開用(public)/ログ(log)/その他(general)の3種で分けるとすると、最低でも3*3で9つ必要となる。

  • product-public
  • product-log
  • product-general
  • staging-public
  • staging-log
  • staging-general
  • develop-public
  • develop-log
  • develop-general

さらにCloudTrailやCodePipeline、CloudFrontなど、AWS側で運用するバケットがあり、そちらが加わる。

S3は他のアカウントも含めてバケット名をユニークにしなければならず、従って上述の名前でバケットを作ることは困難だが、そこはそれ、後ろに乱数文字列をつけるなどしてなんとかかわすしかない。

参考