RNIアドベントカレンダー7日目 Lambda のアップデートをした話

これは RNI 開発部 (SDD) Advent Calendar 2023 最終日の記事です。

本来は 2 日目に入る予定だったのですが、諸事情で最終日になってしまいました。 来年は早めに書いておこうと思います。

Lambda のアップデートをした話

SDD では 3 年ほど前に主要なインフラを AWS に移行しました。 その後、徐々に Lambda 上のスクリプトが増え、現在 RnI で利用しているのは以下言語になっています。

RnI では今年、特に利用している Lambda ランタイムのサポート終了が多く、 3 年前から使っていた多くのスクリプトがアップデートの対象になりました。

参考: Lambda ランタイム - AWS

実際には、以下の通りアップデートを行っています。

  • Ruby 2.7 -> 3.2
  • Go 1.17 -> 1.20
  • Node.js 14.x -> 18.x
  • Python 3.7 -> 3.11

テストコードがない Lambda をアップデートする

RnI では基本的に TDD で開発を進めているため、コアである RailsiOS, Android のコードにはテストコードを書いているのですが、Lambda のコードは AWS への移行期にコードを書いていたこともあり、多くのコードがテストコードを書いていない状態で放置されていました。

今回は時間の制約があり、新たにテストを書く時間が取れなかったことから、以下のように方針を定めました。

  • Linter & Formatter を(VSCode 上でのみ)導入する。
  • 実行時にエラーが出ないように、アップデートで起きる問題を(特に Linter で)事前に解決する。
  • 依存するライブラリはリリースログを確認しながら破壊的な変更がないことを確認し、アップデート後に問題が起きる可能性を低くしておく。

上記の方針を定めた理由として、Lambda 上で動くコードが外部ライブラリをほぼ利用していなかったこと、個別のスクリプトが大きくなかった(100 行未満がほとんど)ため、ソースコードを読むだけで挙動を確認しやすかったことが挙げられます。

Linter の適用で防げた問題

Linter の適用により最も効果があったのは言語レベルでの変更点でした。 わかりやすい例として、以下のようなものがあります。

Python 3.9 で削除された dateutil の可視化

dateutilPython 3.9 以降、 python-dateutil に名称変更されています。 RnI では今回、 Python 3.7 から 3.11 へのアップデートだったため、この問題に対処する必要がありました。 こういった点に対しては、VSCode 上で依存ライブラリの名前解決ができない、といった表示がされるため非常にスムーズに対処していくことができました。

なお、 dateutil についてはタイムゾーンを扱うために導入していたことから、今回は名称変更ではなく、 zoneinfo を使う形で修正を加えています。

3.7 時点でのコード:

from dateutil import tz, zoneinfo
datetime
    .fromtimestamp(int(j['timestamp']) / 1000)
    .replace(tzinfo=tz.tzutc())
    .astimezone(zoneinfo.gettz("Asia/Tokyo"))
    .strftime('%Y-%m-%d %H:%M:%S')

3.11 用に修正したコード:

from zoneinfo import ZoneInfo
datetime
    .fromtimestamp(int(j['timestamp']) / 1000, tz=ZoneInfo('UTC'))
    .astimezone(ZoneInfo("Asia/Tokyo"))
    .strftime('%Y-%m-%d %H:%M:%S')

requirements.txt や Gemfile , package.json などの設置

Lambda デプロイ後に特に問題が発生せずに運用されたスクリプトの中には、ライブラリのバージョン指定が明確にされていない(requirements.txt などが存在しない)ものが少数ながらも存在しました。

これらについては、正確には Linter で防げたわけではないのですが、上記 dateutil の問題を修正する際に併せて発覚したため、標準ライブラリ以外を利用している Lambda スクリプトに対してはバージョン指定を明確に行う修正を行いました。

Linter の導入で防げなかった問題

ここは計画時点での盲点でもあったのですが、RnI では CodeBuild を利用して Lambda スクリプトのビルドを行っています。 そのため、各言語のバージョンアップに伴い、 buildspec ファイルの runtime-versions セクションを書き換えるだけでなく、使用可能なランタイムを確認してアップデートする必要がありました。

参考: 使用可能なランタイム - AWS

複数の言語やバージョンを扱っている buildspec ファイルの分離

RnI では上述の通り複数の言語を使用しているのですが、これら複数の Lambda スクリプトを 1 つの buildspec ファイルで扱っていたため、 CodeBuild 側の Linux イメージのバージョンを合わせることができず、複数の buildspec ファイルに分離する必要が生じました。

x86_64 から arm64 への移行

これは Go でのみ発生した問題ですが、RnI では Mac を使って開発をしているため、ここ数年でローカル開発環境が x86_64 から arm64 になっています。 ローカル環境でビルドのテストを実施する際には arm64 になるため、併せて Lambda 側のランタイム設定でも同様にアーキテクチャx86_64 から arm64 へ変更しました。

しかしながら、これまで Lambda スクリプトは全て x86_64 で動作させていたため、この項目は除外(デフォルト値の通りに設定)しており、 CFn の設定を変更しておらず、デプロイ時に一時的に障害が発生する原因となりました。

本来、 x86_64 から arm64 へのアーキテクチャ変更時には、 CFn ファイルへ以下を追加する必要があります。

参考: AWS::Lambda::Function - AWS CloudFormation User Guide

Resources:
  LambdaResourceName:
    Type: "AWS::Lambda::Function"
    Properties:
      Architectures:
        - arm64

今後 RnI では arm64 で動作させるリソースが増えていくことが想定されるため、必要に応じて追記・修正していく必要があります。

今後対応していくこと

今回のアップデートでは、個々の Lambda スクリプトで利用している外部ライブラリがほとんどなかったことと、スクリプト自体の行数もさほど長くなかったため力業で進めてしまいましたが、今後 Lambda への依存度が高くなってくるとこの方法では難しい事が容易に想像できます。

幸いながら、今回は 1 スクリプトのみ問題が発生する比較的軽微なダメージでアップデートが完了していますが、今後もこの方法で進めていくのは憚られます。 今回のアップデートでは、今後の対応方針を見極めるためにも作業ログを残し、どのような点で躓いたかなどを併記しておくことで、以下の課題及び今後対応していくべきことが改めて確認できました。

Lambda スクリプトもきちんとテストを書いておく

個々のスクリプト自体はさほど難しくない内容であっても、同時に多くの Lambda がアップデート対象になるとそれなりに確認に時間がかかります。 AWS に移行した当初は Lambda スクリプトの数もさほど多くなかったため、手作業で 1 つ 1 つ見ていく方法でも運用上大きな問題はありませんでした。ですが、この 1 年で Lambda スクリプトの数はかなり増えており、今後も増えることが容易に想像できます。

また、外部ライブラリ(pipRubyGems など)を利用しているコードは、ライブラリ側のアップデートも併せて確認していく必要がある他、アップデートのタイミングで言語レベルで名称が変更されているモジュールなどを確認する必要があることから、Rails 同様に、それぞれの言語に適したテストコードを記載し、CI の段階で問題がないことを担保することが必要です。

Lambda にもローカル環境を用意する

本番環境上で動作させている Lambda を一発で修正するのはそれなりに難しい作業になります。しかし、現時点では Lambda スクリプトが layered になっている部分があったり、依存する AWS 上の別サービスがあることから、ローカル環境でスクリプトを実行してテストを行うことが今回はできませんでした。 また、本番環境上でテストを行う際にも、他のシステムに影響が出るスクリプトもあり、実際にスクリプトを動かしてテストできる Lambda スクリプトは限定的になっていました。

従って、今後は LocalStack などを併用し、ローカル環境でインフラ側もある程度テストできる環境を整えるとともに、個々の Lambda スクリプトをより管理しやすい AWS SAM や Serverless Framework の導入を行うことで、実際の AWS 上でも本番同様に準備された複数の環境を利用できるようにしていくことが必要です。

SAM や Serverless Framework には、モックパラメータを使ってテストを回す仕組みも備わっており、モックパラメータがわかっている状態でテスト実行ができるだけでも、アップデート対応時にはかなり助かります。

Lambda のデプロイは自前でコードを書かず、ツールに任せる

AWS に移行した段階で用意した Lambda のデプロイ用コードは、 CodeBuild でビルドしたコードを S3 にコピーし、一括で全 Lambda スクリプトをデプロイする形式になっていました。そのため、1 ファイルの修正であっても、都度全ての Lambda スクリプトがビルド & デプロイされてしまいます。

Lambda スクリプトの数が少ない時にはこの方法でも充分だったのですが、今回のように複数のファイルを随時にアップデートしていきたい場合などには、ビルド & デプロイにかかる時間が積もっていきます。

自前のスクリプトを修正することでも対応は可能ですが、既にあるツールを使わないのも DRY ではないため、 Lambda のデプロイは自前でコードを書かずに Serverless Framework, AWS Serverless Application Model (AWS SAM) のようなツールに任せて行くのが良いでしょう。

特に今回、 CFn ファイルが 1 つにまとまっているメリットがデメリットを上回ってしまう形になったため、AWS SAM は CFn ファイルが増えてしまう問題はあるものの、個別に更新可能なメリットも享受できる形式であると感じました。

まとめ

Lambda スクリプトは気軽に作成することができ非常に便利ですが、アップデートやテストなど、下回りが疎かになっていました。

RnI ではコアの部分で利用している RailsiOS, Androidスクリプトはきちんとテストコードが書かれ、CI/CD が確立されています。 今回 Lambda のアップデートで躓いた部分も、「テストを書きましょう」「CI/CD をきちんと回しましょう」「DRY に作りましょう」といった、よく言われるものの蔑ろにしてしまいがちな部分をおざなりにしてしまったために起きた事故と捉えることもできます。

継続的な開発をしていく上で重要なこれらの要素を、 Lambda アップデートの中で再確認できたのは非常に有意義だったと感じています。また、2024 年にこれらの問題を一緒に解決していけるエンジニアを RnI では引き続き採用しています。興味がある方は是非一度、RnI の採用ページをご覧いただけると幸いです。

Happy holiday!