AWS Lambda 関数のランタイムを一気に更新する

こんにちは
沢山の AWS Lambda 関数のランタイムを更新する必要が出てきたので、今回はその内容についてお伝えします。

:::note warn
**大前提**
基本的にマイナーバージョンアップであれば後方互換性があるので、問題は出にくいかもしれません。
しかし、問題が出て困るような環境（例：本番環境、利用者が多いなど）では、本仕組みを使わずに、動作確認してから個別に更新することをお勧めします。
:::

## ことの発端
わたしが管理している AWS ルートユーザーのメールアドレスに、以下のようなメールが届きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/9c0590f9-6f21-4e4d-e7be-3eb4f823ba74.png)

2024 年 10 月 14 日で Python3.8 のサポートが終了になるので、それまでに更新対応してくださいね！というものです。

このアカウントは、 Python3.8 時代にたくさんの AWS Lambda 関数を開発し運用しているのでポチポチやるのも一苦労です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/a4421dd6-d831-d990-7ce2-68ea8e4604c6.png)
※Python3.8 な Lambda 関数の一部

定期的にこういった更新対応が必要になるのはわかっているので、極力省力化できるようにAWS Lambda と AWS StepFunctions を用いて実装しました。

## AWS Lambda の実装
### IAM Role に必要なアクション許可
最低限、以下のアクションを許可すればよいです。
サンプルコードから改造する場合は、それに見合ったアクション許可を付与してください。

* lambda:ListFunctions
* lambda:UpdateFunctionConfiguration
* logs:CreateLogGroup	
* logs:CreateLogStream	
* logs:PutLogEvents


### サンプルコード
後述の AWS StepFunctions から入力パラメータを送信してもらうので、```event``` コンテキストの ```current``` キーと ```target``` キーを受けられるようにしています。
また、AWS StepFunctions に向けて、どの AWS Lambda 関数を更新したのかといった情報やエラー時のトレースを送れるようにもしてあります。

:::note normal
エラーハンドリングは最低限なので、必要に応じて適宜追加してください。
:::
```python:lambdaRuntimeUpdate.py
import traceback
import logging
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    if event:
        if 'current' in event:
            current=event['current'].lower()
        else:
            return {"Message": "current が存在しないか、現在のランタイムが指定されていません"}
        if 'target' in event:
            target=event['target'].lower()
        else:
            return {"Message": "target が存在しないか、新しいランタイムが指定されていません"}
    else:
        return {"Message": "StepFunctionsから次のように入力してください。{\"current\": <現在のランタイム>, \"target\": <新しいランタイム>}"}
    
    targetFunctions=[]
    
    client=boto3.client('lambda')
    paginator = client.get_paginator('list_functions')

    for lambdafunctions in paginator.paginate():
        for lambdafunction in lambdafunctions['Functions']:
            if current == lambdafunction['Runtime']:
                try:
                    targetFunctions.append(lambdafunction['FunctionName'])
                    client.update_function_configuration(
                        FunctionName=lambdafunction['FunctionName'],
                        Runtime=target
                    )
                except:
                    logger.error(traceback.format_exc())
                    return {"Message": traceback.format_exc()}
    return {
        "CurrentRuntime": current,
        "TargetRuntime": target,
        "TargetFunctions": targetFunctions
    }
```

## AWS StepFunctions のステートマシンの実装
今回は手動実行なので至極簡単に ```Lambda:Invoke``` 1つのみです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/30f09934-ae9b-cf6b-4a32-aef2e2c6239d.png)

なぜ、わざわざ AWS StepFunctions 用いるのかの答えは、パラメータを入れやすくするのと今後、AWS Health からの通知を契機に自動化したいとなったときにも対応しやすくするためです。

### ステートマシンのサンプルコード 
```json:LambdaRuntimeUpdateStateMachine.json
{
  "Comment": "A description of my state machine",
  "StartAt": "LambdaRuntimeUpdate",
  "States": {
    "LambdaRuntimeUpdate": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "OutputPath": "$.Payload",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": "arn:aws:lambda:ap-northeast-1:<AWS AccountId>:function:lambdaRuntimeUpdate:$LATEST"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException"
          ],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "End": true
    }
  }
}
```

IAM Role について、このステートマシンを初回保存する際に自動的に作成されるもので問題ありません。

## 動かしてみよう
AWS Lambda 関数と AWS StepFunctions のステートマシンが作成できたら、ステートマシンから実際に更新します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/ac26394c-e9b6-bc4d-ba3a-7172fd1fe121.png)
画像の様に```json```形式のパラメータを入力値として設定します。

```json:入力例
{
    "current": "現在のランタイム",
    "target": "更新先のランタイム"
}
```

実行し、特に問題がなければ、以下の様にステートマシンの```実行の入力と出力```に表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/e0c3670e-86db-f270-497b-dd8fa73865b5.png)

AWS Lambda の関数の画面でも以下の通り更新されていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/43fc500f-1ad7-0c3d-88c6-3599eeae58dd.png)

## まとめ
[退屈なことは Python にやらせよう](https://www.amazon.co.jp/dp/4873119278/) というタイトルの書籍をご存知でしょうか？
自作 RPA 本の草分け的存在として大ヒットしました。
冒頭お伝えした通り、ランタイムを変えてしまうことで影響が出て困るような場合は、本記事でお伝えした内容は使いづらいのです。
しかし、全部が全部そういった関数ではないかもしれません。Lambda 関数にはタグを付与することができます。そのタグが付いている場合は処理をスキップし、対象外にした旨を出力してあげて、それ以外は更新してしまう。そうすることで作業工数の低減につながるのではと考えます。
よき、Cloud Operation Life を～

ーーー
記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。