Boto3でLambda@EdgeをCloudFrontにデプロイする

AWSから「AWS Lambda における Node.js 10 のサポート終了」のお知らせが届きました。

アカウントのLambdaがいくつかNode.js 10のランタイムを使っていて、Node.js 12以上への更新してねーとのこと。

まあこれ自体は単にランタイムを更新すればいいんですが、問題はLambda@Edgeです。

CloudFrontディストリビューション紐づけたLambda@Edge関数は、CloudFrontのコンソールから一つ一つBehaviorを選んで更新するか、Lambdaのコンソールからディストリビューションを選んで関数単位にデプロイするかでバージョンを上げれます。

ところが、複数のBehaviorに複数のNode.js 10のLambda@Edge関数がくっついていたためコンソールからだとと更新が中々しんどい事態になってました。

というわけでBoto3でスクリプトを組んで更新してみました。

まずは、Node.js 10の時の関数バージョンで紐づいちゃってる一覧をとります。

import boto3

cf = boto3.client('cloudfront')

distributions = cf.list_distributions()

if distributions['DistributionList']['Quantity'] > 0:
    headers = [
        "ID",
        "Domain Name",
        "Comment",
        "State",
        "Cache Behavior(Path Pattern)",
        "Lambda Function",
    ]
    print("\t".join(headers))

    while True:

        for distribution in distributions['DistributionList']['Items']:

            row = [
                distribution['Id'],
                distribution['DomainName'],
                distribution['Comment'],
                distribution['Status'],
            ]

            defaultCacheBehavior = distribution['DefaultCacheBehavior']
            row.append('Default')
            if defaultCacheBehavior['LambdaFunctionAssociations']['Quantity'] > 0:
                for function in defaultCacheBehavior['LambdaFunctionAssociations']['Items']:
                    row.append(function['LambdaFunctionARN'])

            print("\t".join(row))

            cacheBehavior = distribution['CacheBehaviors']
            if cacheBehavior['Quantity'] > 0:

                for behavior in cacheBehavior['Items']:
                    row = ['','','','']
                    """
                    row = [
                        distribution['Id'],
                        distribution['DomainName'],
                        distribution['Comment'],
                        distribution['Status'],
                    ]
                    """
                    row.append(behavior['PathPattern'])

                    if behavior['LambdaFunctionAssociations']['Quantity'] > 0:
                        for function in behavior['LambdaFunctionAssociations']['Items']:
                            row.append(function['LambdaFunctionARN'])

                    print("\t".join(row))

        # Max 100
        if 'NextMarker' in distributions['DistributionList']:
            nextMarker = distributions['DistributionList']['NextMarker']
            distributions = cf.list_distributions(Marker=nextMarker)
        else:
            break

else:
    print("No CloudFront Distributions.")
$ python list_distributions.py > list_distributions.tsv
$ cat list_distributions.tsv
ID              Domain Name                  Comment State     Cache Behavior(Path Pattern) Lambda Function
XXXXXXXXXXXXXX  xxxxxxxxxxxxx.cloudfront.net         Deployed  Default                      arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:foo-func:2
YYYYYYYYYYYYYY  yyyyyyyyyyyyy.cloudfront.net         Deployed  Default                      arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:foo-func:2
                                                               /login                       arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:bar-func:3
~~~~~
こんな感じで、TSVで出せるようにしてみました。

PHPでやった時も思いましたがCloudFrontの設定情報はたくさんあってネストしてるので処理がめんどくさいです…。

では、本題のディストリビューションに紐づくLambda@Edgeの更新です。

import boto3
import json
import copy
from dictdiffer import diff

cf = boto3.client('cloudfront')

# 対象の環境に合わせてBehaviorのパスに紐づくLambda@EdgeのARNを調整
path_map_function_arn = {
    'default': 'arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:foo-func:3',
    '/login': 'arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:bar-func:4',
}

def update_distribution(id, dryrun):

    try:
        distribution_config = cf.get_distribution_config(
            Id=id
        )
        distribution_config_old = copy.deepcopy(distribution_config)

        defaultCacheBehavior = distribution_config['DistributionConfig']['DefaultCacheBehavior']
        if defaultCacheBehavior['LambdaFunctionAssociations']['Quantity'] > 0:
            for idx, function in enumerate(defaultCacheBehavior['LambdaFunctionAssociations']['Items']):

                defaultCacheBehavior['LambdaFunctionAssociations']['Items'][idx]['LambdaFunctionARN'] \
                    = path_map_function_arn['default']

        distribution_config['DistributionConfig']['DefaultCacheBehavior'] = defaultCacheBehavior
        
        cacheBehavior = distribution_config['DistributionConfig']['CacheBehaviors']
        if cacheBehavior['Quantity'] > 0:

            for idx1, behavior in enumerate(cacheBehavior['Items']):

                if behavior['PathPattern'] in path_map_function_arn:

                    if behavior['LambdaFunctionAssociations']['Quantity'] > 0:
                        for idx2, function in enumerate(behavior['LambdaFunctionAssociations']['Items']):

                            cacheBehavior['Items'][idx1]['LambdaFunctionAssociations']['Items'][idx2]['LambdaFunctionARN'] \
                                = path_map_function_arn[behavior['PathPattern']]

        distribution_config['DistributionConfig']['CacheBehaviors'] = cacheBehavior

        # 差分を確認
        print(json.dumps(list(diff(distribution_config_old, distribution_config)), indent=2))

        if not dryrun:
            cf.update_distribution(
                DistributionConfig=distribution_config["DistributionConfig"],
                Id=id,
                IfMatch=distribution_config["ETag"],
            )
        else:
            print('Dry run update.')

    except cf.exceptions.NoSuchDistribution as e:
        print("This ID is not found.")


if __name__ == '__main__':
    id = input('Please input distribution id:')
    dryrun = input('Dry run y/n?:') == 'y'

    update_distribution(id, dryrun)
$ python update_distribution.py
Please input distribution id:YYYYYYYYYYYYYY
Dry run y/n?:y
[
  [
    "change",
    [
      "DistributionConfig",
      "DefaultCacheBehavior",
      "LambdaFunctionAssociations",
      "Items",
      0,
      "LambdaFunctionARN"
    ],
    [
      "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:foo-func:2",
      "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:foo-func:3"
    ]
  ],
  [
    "change",
    [
      "DistributionConfig",
      "CacheBehaviors",
      "Items",
      0,
      "LambdaFunctionAssociations",
      "Items",
      0,
      "LambdaFunctionARN"
    ],
    [
      "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:bar-func:3",
      "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:bar-func:4"
    ]
  ]
]
Dry run update.
ディストリビューションの設定を取得し、それを一部変更し再度セットするという処理の流れになります。
変えちゃいけないとこ変わったら嫌なので変更後の設定のDIFFをdictdifferで表示できるようにしています。

一覧取得と組み合わせれば一括更新もできるだろうけど確認しながら確実にやりたいので。

あんまりかっこいい感じに作れなかったけど、必要十分なことはできたのでヨシ!