🚥

ポストモーテム: AWS Lambda内のリクエストからHTTPヘッダが消えた日

2024/04/18に公開

AWS Lambdaで突如としてHTTPヘッダが消失し、それにより悩まされることとなった日の経験を共有します。この問題がどのように生じ解決に至ったのか、また、私たちが学んだ教訓について述べていきます。

対象のLambda関数

今回問題が起きたLambda関数では、ランタイムにNode.jsを利用していました。Lambda関数の中には、外部のAPIサーバに対するリクエスト処理が含まれます。
環境情報は以下の通りです。

  • ランタイム: Node.js 18 (18.18.2)
  • リクエストライブラリ: ky v1.0.1

エラーの発生

ある時、APIサーバからのレスポンスが"415 Unsupported Media Type"というエラーで返ってくるようになりました。エラーメッセージは以下のようなものです。

Request failed with status code 415 Unsupported Media Type

問題が起きる前は一度も発生していないエラーでしたが、一度発生した後は、全てのリクエストが同じ状態に陥りました。
困ったことに、エラーが発生し始めた時期の前後では、 Lambda関数のデプロイや設定変更を一切行っていなかった のです。また同様に、APIサーバ側でも何も変更が発生されていないことも確認しました。

(APIサーバが嘘を返していなければ)415ステータスコードはサーバ側でサポートしていないメディアタイプの時のエラーなため、何が起きているのか、仮説はある程度は立てられます。実際に、ログからHTTPリクエストのダンプ結果を見てみると、なぜか HTTPヘッダが送信されていない(HTTPヘッダが空である) ことが明らかになりました。

原因

kyライブラリの使い方を見直して明示的に Content-Type ヘッダを付与するようにしてみたり、別の環境からAPIを呼び出してみたり、切り分けのために色々と試しました。

直接原因:ライブラリのissue

結論として、直接の原因は、kyライブラリの下記のissueでした。

https://github.com/sindresorhus/ky/issues/535

上記の通りですが、Node.js 18.18.2のkyでは、HTTPヘッダを指定しても空になる不具合があります(解決済み)。これにより、リクエスト時に Content-Type ヘッダが指定されず、APIサーバ側が415エラーを返すようになっていました。

間接原因:コンテナランタイムの自動更新

ライブラリのバグを踏むこと自体は珍しいことではありませんので、本記事の本題でもありません。
ポイントは、Lambdaコンテナランタイムの自動更新が関わっていた点にあります。ある時に自動でランタイムが更新され、それがバグ発生の固有条件と組み合わさる形で、HTTPヘッダの消失という問題が引き起こされたのです。

AWS Lambdaのランタイム管理制御機能は、2023年1月に発表され、比較的新しい機能です。
なお、この機能で管理されているランタイムの更新とは、Node.js 18→20のようなバージョンアップではなく、マイナーなランタイムパッチとセキュリティパッチを指しています。

https://aws.amazon.com/jp/about-aws/whats-new/2023/01/runtime-management-controls-aws-lambda/

コンテナランタイム管理制御では、下記の3つの選択肢の中から、ランタイム更新タイミングを選ぶことができます。

更新モード 説明 デフォルト
[Auto] (自動) AWSが自動でランタイムを更新する
[Function update] (関数更新) ユーザーが関数を更新した時だけランタイムを更新する
[Manual] (手動) 更新を行わず、利用バージョンを明示的に固定する

デフォルトが「自動」であり、私たちのLambda関数もデフォルト設定で運用していました。これによって、パッチやセキュリティ更新を開発者である私たちは手間をかける必要なく、AWS側の責任範囲において実施してくれることになります。
今回はこの動作が悪い方向に作用してしまった形ですね…(注:「自動」にするのは避けるべき、という主張をするつもりは一切ありません)

対処

上記の通りの原因がわかったため、回復手段としては、ランタイムをNode.js 20に更新することで対応しました。更新後はエラーも解消され、今も問題なく動作しています。

再発防止

では、このような事態を防ぐことは可能だったのでしょうか?

ユニットテスト

不具合を抑える第一の防波堤はユニットテストですが、今回はユニットテストで気づくことは難しそうです。

当たり前ですが私たちはユニットテストがパスしたものをデプロイして稼動させています。
今回の事象はデプロイ後しばらく安定稼働していて、ある瞬間から時限式に発生したものでした。

また、ユニットテストコードでは、HTTPリクエスト部分はモックを使うことが多いと思います。まさかHTTPヘッダが消えることがあるとは想像していなかったため、「リクエストHTTPヘッダに Content-Type が含まれていること」をテストコードではアサーションしていません。原因がわかった今でも、そこまでをやるのは「過剰」な気はしています。

ライブラリのバグをウォッチ

kyの当issueを把握していれば、事前に身構えることもできたかもしれませんが、これも現実的には難しそうです。
使用している全てのライブラリについて、既知のissueを把握するのは大変です。依存ライブラリは間接的に利用されているものも含めれば、かなりの数になるためです。

コンテナランタイムの更新タイミングに備える

ランタイム更新が自動でも、更新時期が前もってわかっていれば、確認や対処をすることができたかもしれませんが、この方法は実現できないようでした。
AWS公式のドキュメントからは、ランタイムバージョンの更新を「自動」で運用した場合に、実際に更新される時期がいつになるかについて言及したドキュメントはなく、公開された仕様にはないようです
(その意味で、今回の事象に関しても、本当にエラー発生時期の直前にランタイムが更新されたかどうか?については特定できていません)
ただ更新に関しては2フェーズのランタイムバージョンロールアウトという方式で行われることは説明されています。今回のLambda関数は頻繁にデプロイするものでなかったために、更新が後回しになっていて「早く失敗する」ことができていませんでした。故に想定外のタイミングで問題を引き起こすこととなっていたようです。

一方で、ランタイム更新が起きたことを事後に知ることはできそうです。
各ランタイムバージョンには、バージョン番号とARNが関連付けられており、Lambdaの実行ログにもそれらが出力されているためです。
ただ、せっかくAWSがマネージドな仕組みで面倒を見てくれている範囲を監視するのも、自動モードのメリットを潰してしまっているように思います。

教訓

今回学んだことは3つあります。

1つ目は、改めて、AWSアーキテクチャの設計原理「 故障に備えた設計(Design for Failure) 」の大切さです。
今回の事象も、エラーの発生自体はAWS CloudWatchのアラームですぐに気づくことができました。
事前に備えておけるものについては備えておくべきですが、それでも全てを完璧にコントロールすることはできません。今回のLambda関数では、エラーが起きた後のリカバリ処理に手動対応で時間がかかるものであったため、エラーが起きても後から自己修復可能な形にする、というのがふりかえりの中で出てきたアクションアイテムの1つです。

2つ目は、「 障害は最も起きてほしくないタイミングで起きる 」という前提で、日々の運用をしていく気持ちが大切だという点です。今回の事象では、デプロイや設定変更を行っていないタイミングで「なぜ急に?」という疑問が、対応の足を引っ張っていたと感じました。「我々がどうにもできない何かに原因があるはずだ」という思い込みは捨てて、落ち着いて事実・状況を把握して対応にあたっていく気持ちが大切だと感じました。

最後に、「 デフォルト設定であったとしても、AWSの設定1つ1つの意味をきちんと理解し運用する 」ことの大切さです。今回ではLambdaのランタイム更新の動作の部分ですね。こういった設定をきちんと把握することで、AWSに対する理解も深まりますし、運用保守の観点では原因に対して筋の良い仮説を立てることができるようになると感じました。逆に、ランタイム更新があることを認識すらしていなければ、原因の特定にはかなり遠回りをすることになるかなと思いました。

Cariot開発チーム(フレクト)

Discussion