非最新のboto3で最新のAWSサービスを利用する方法

いちいちLamdba Layerを準備せずにLambda & boto3からAmazon Bedrockにアクセスしたい人に捧ぐブログ
2023.09.30

CX事業本部@大阪の岩田です。

AWSの新サービスがローンチされた際にLambdaから新サービスにアクセスする処理を動かそうとしたらLambda実行環境にバンドルされているSDKのバージョンが古くなってエラーになる...というのは割とあるあるだと思います。最近だとAmazon BedrockのGAに伴い、LambdaからBedrockにアクセスする処理を試してみたいという方も多いのではないでしょうか?

上記のようなケースではLambda Layerを利用して最新版のSDKを利用するのが一般的な解決策です。普段からLambdaを利用している人であれば特に問題なく環境を構築できると思いますが、Lambda初心者の方にとってはLayerの作り方が分からなかったり、Layerのディレクトリ階層を1つ間違えてハマったり、若干ハードルが高いのではないでしょうか?Layerを利用せずにLambdaのデプロイパッケージ自体に最新版のboto3をバンドルする方式もありますが、この方式を利用するとマネコンからLambdaのコードを閲覧/編集できなくなるので、マネコンから直接Lambdaのコードを編集したいようなライトなユースケースには適しません。

この記事ではboto3を題材に

  • Lambda Layerを利用して最新版のboto3をデプロイする
  • Lambdaのデプロイパッケージに最新版のboto3をバンドルする

以外の選択肢として非最新のboto3から最新のAWSサービスを利用する方法をご紹介します。

結論

  • botocoreのGitHubリポジトリ等から利用したいAWSサービスに関する仕様を定義したJSONファイルをDLする
  • 上記のファイルをLambdaのデプロイパッケージに含める
  • 環境変数AWS_DATA_PATHを指定し、上記のJSONファイルを参照する

という手順を踏むことで非最新のboto3からでも最新のAWSサービスが利用可能です

boto3/botocoreはどうやってclientを生成しているのか?

boto3を使ってS3にアクセスする際は以下のようなコードを書きます

import boto3
s3 = boto3.client('s3')
...

このコードで生成されたのはbotocore.client.S3クラスのインスタンスです。しかし、botocoreの実装のどこを見てもS3クラスの実装は見当たりません。ではS3クラスはどこで定義されているのでしょうか?これはJSONFileLoaderというクラスがAWSサービスの仕様を定義したJSONファイルを読み込み、動的にクラスを定義することで実現されています。

少し長いですが、該当ファイルに記載されているコメントを抜粋します。

"""Module for loading various model files.

This module provides the classes that are used to load models used by botocore. This can include:

* Service models (e.g. the model for EC2, S3, DynamoDB, etc.)
* Service model extras which customize the service models
* Other models associated with a service (pagination, waiters)
* Non service-specific config (Endpoint data, retry config)

Loading a module is broken down into several steps:

* Determining the path to load
* Search the data_path for files to load
* The mechanics of loading the file
* Searching for extras and applying them to the loaded file

The last item is used so that other faster loading mechanism besides the default JSON loader can be used.

The Search Path

Similar to how the PATH environment variable is to finding executables and the PYTHONPATH environment variable is to finding python modules to import, the botocore loaders have the concept of a data path exposed through AWS_DATA_PATH.

This enables end users to provide additional search paths where we will attempt to load models outside of the models we ship with botocore. When you create a Loader, there are two paths automatically added to the model search path:

* <botocore root>/data/
* ~/.aws/models

The first value is the path where all the model files shipped with botocore are located.

The second path is so that users can just drop new model files in ~/.aws/models without having to mess around with the AWS_DATA_PATH.

The AWS_DATA_PATH using the platform specific path separator to separate entries (typically : on linux and ; on windows).

Directory Layout

The Loader expects a particular directory layout. In order for any directory specified in AWS_DATA_PATH to be considered, it must have this structure for service models::

<root>
  |
  |-- servicename1
  |   |-- 2012-10-25
  |       |-- service-2.json
  |-- ec2
  |   |-- 2014-01-01
  |   |   |-- paginators-1.json
  |   |   |-- service-2.json
  |   |   |-- waiters-2.json
  |   |-- 2015-03-01
  |       |-- paginators-1.json
  |       |-- service-2.json
  |       |-- waiters-2.json
  |       |-- service-2.sdk-extras.json

That is:

* The root directory contains sub directories that are the name
  of the services.
* Within each service directory, there's a sub directory for each
  available API version.
* Within each API version, there are model specific files, including
  (but not limited to): service-2.json, waiters-2.json, paginators-1.json

The -1 and -2 suffix at the end of the model files denote which version schema is used within the model. Even though this information is available in the version key within the model, this version is also part of the filename so that code does not need to load the JSON model in order to determine which version to use.

The sdk-extras and similar files represent extra data that needs to be applied to the model after it is loaded. Data in these files might represent information that doesn't quite fit in the original models, but is still needed for the sdk. For instance, additional operation parameters might be added here which don't represent the actual service api.

"""

https://github.com/boto/botocore/blob/fcad88429eebfb3024be80e2700648c4a357ca16/botocore/loaders.py

ということで、botocoreはデフォルトでは

  • <botocoreのルートディレクトリ>/data
  • ~/.aws/models

というディレクトリに加え、環境変数AWS_DATA_PATHが定義されている場合はAWS_DATA_PATHからもAWSサービスの仕様を定義したモデルファイルを検索します。

Lambdaに関して考えると、我々ユーザーからはLambda実行ユーザーのホームディレクトリは操作できないため、環境変数AWS_DATA_PATHを定義して最新のモデルファイルを読み込ませることで、非最新のboto3からでも最新のAWSサービスが利用できそうです。

やってみる

ここから具体的な手順を紹介していきます。全てLambdaのマネコン上で完結しても良いのですが、LambdaのマネコンはCloud9のマネコンと違ってローカル環境のファイルを所定の場所にアップロードするような機能がないため、ローカル環境でデプロイ用のZIPファイルを生成してデプロイする手順で進めていきます。多少手間は増えますが、Lambdaのマネコンから1つ1つファイル/ディレクトリの作成やファイルの中身のコピペを行う手順でも問題はありません。

以後の手順は事前にPython3.11のLambda関数が作成されており、そのLambda関数の諸々の設定を更新していくという前提で進めます。

botocoreのリポジトリから最新のモデルファイルをDL

まずbotocoreのGitHubリポジトリから最新のモデルファイルをDLします。事前にローカル環境に一時的な作業用ディレクトリを作成しておきます。後の手順で権限の問題が発生しないようにこのディレクトリは/tmp配管作成します。

mkdir -p /tmp/lambda/.aws/models
cd /tmp/lambda/.aws/models

ブラウザでGitHubのリポジトリを開いたら、.(ドット)をタイプし、Web版のVS Codeでリポジトリを開き直しましょう。

VSCodeで開き直したら、画面左のエクスプローラーからbotocore/data/<利用したいAWSサービス>のディレクトリを選択し、右クリックからダウンロードを選択します。

ダウンロード先は先程作成した/tmp/lambda/.aws/modelsを指定します。このブログでは例としてbedrock-runtimeを利用します。

関連するディレクトリ&ファイルがダウンロードできたらパーミッションを付与します。今回はちょっとした検証なので雑に777で設定します。

chmod  -R 777 /tmp/lambda/.aws/models

Lambdaのコードを準備

モデルファイルの準備ができたのでLambdaのコードを準備します。今回は以下のようなコードにしました。

import boto3
import botocore

def lambda_handler(event, context):

    print(botocore.__version__)
    print(boto3.__version__)
    bedrock = boto3.client(service_name='bedrock-runtime', region_name="us-east-1")
    return

このコードを/tmp/lambda/lambda_function.pyに保存し、先程のモデルファイルと合わせてZIPファイルにパッケージングします。

cd /tmp/lambda
zip -r lambda.zip .aws
zip lambda.zip lambda_function.py

ZIPファイルができたらLambdaのマネコンからZIPファイルをデプロイしておきましょう。デプロイが完了するとLambdaのマネコンは以下のようになります。やはりマネコンから直接コードを閲覧/編集できる状態には安心感があります。

最新AWSサービスを利用してみる

コードがデプロイできたら実際に試してみましょう。まずはそのままテストを実行します。実行に失敗し、以下のようなログが出力されるはずです。bedrock-runtimeなんてサービスは知らないぞとのことです。botocoreのバージョンが1.30.1なので納得です。

1.30.1
1.27.1
[ERROR] UnknownServiceError: Unknown service: 'bedrock-runtime'. Valid service names are: accessanalyzer, account, acm, acm-pca, alexaforbusiness, amp, amplify, amplifybackend, amplifyuibuilder, apigateway, apigatewaymanagementapi, apigatewayv2, appconfig, appconfigdata, appfabric, appflow, appintegrations, application-autoscaling, application-insights, applicationcostprofiler, appmesh, apprunner, appstream, appsync, arc-zonal-shift, athena, auditmanager, autoscaling, autoscaling-plans, backup, backup-gateway, backupstorage, batch, billingconductor, braket, budgets, ce, chime, chime-sdk-identity, chime-sdk-media-pipelines, chime-sdk-meetings, chime-sdk-messaging, chime-sdk-voice, cleanrooms, cloud9, cloudcontrol, clouddirectory, cloudformation, cloudfront, cloudhsm, cloudhsmv2, cloudsearch, cloudsearchdomain, cloudtrail, cloudtrail-data, cloudwatch, codeartifact, codebuild, codecatalyst, codecommit, codedeploy, codeguru-reviewer, codeguru-security, codeguruprofiler, codepipeline, codestar, codestar-connections, codestar-notifications, cognito-identity, cognito-idp, cognito-sync, comprehend, comprehendmedical, compute-optimizer, config, connect, connect-contact-lens, connectcampaigns, connectcases, connectparticipant, controltower, cur, customer-profiles, databrew, dataexchange, datapipeline, datasync, dax, detective, devicefarm, devops-guru, directconnect, discovery, dlm, dms, docdb, docdb-elastic, drs, ds, dynamodb, dynamodbstreams, ebs, ec2, ec2-instance-connect, ecr, ecr-public, ecs, efs, eks, elastic-inference, elasticache, elasticbeanstalk, elastictranscoder, elb, elbv2, emr, emr-containers, emr-serverless, es, events, evidently, finspace, finspace-data, firehose, fis, fms, forecast, forecastquery, frauddetector, fsx, gamelift, gamesparks, glacier, globalaccelerator, glue, grafana, greengrass, greengrassv2, groundstation, guardduty, health, healthlake, honeycode, iam, identitystore, imagebuilder, importexport, inspector, inspector2, internetmonitor, iot, iot-data, iot-jobs-data, iot-roborunner, iot1click-devices, iot1click-projects, iotanalytics, iotdeviceadvisor, iotevents, iotevents-data, iotfleethub, iotfleetwise, iotsecuretunneling, iotsitewise, iotthingsgraph, iottwinmaker, iotwireless, ivs, ivs-realtime, ivschat, kafka, kafkaconnect, kendra, kendra-ranking, keyspaces, kinesis, kinesis-video-archived-media, kinesis-video-media, kinesis-video-signaling, kinesis-video-webrtc-storage, kinesisanalytics, kinesisanalyticsv2, kinesisvideo, kms, lakeformation, lambda, lex-models, lex-runtime, lexv2-models, lexv2-runtime, license-manager, license-manager-linux-subscriptions, license-manager-user-subscriptions, lightsail, location, logs, lookoutequipment, lookoutmetrics, lookoutvision, m2, machinelearning, macie, macie2, managedblockchain, marketplace-catalog, marketplace-entitlement, marketplacecommerceanalytics, mediaconnect, mediaconvert, medialive, mediapackage, mediapackage-vod, mediapackagev2, mediastore, mediastore-data, mediatailor, memorydb, meteringmarketplace, mgh, mgn, migration-hub-refactor-spaces, migrationhub-config, migrationhuborchestrator, migrationhubstrategy, mobile, mq, mturk, mwaa, neptune, network-firewall, networkmanager, nimble, oam, omics, opensearch, opensearchserverless, opsworks, opsworkscm, organizations, osis, outposts, panorama, payment-cryptography, payment-cryptography-data, personalize, personalize-events, personalize-runtime, pi, pinpoint, pinpoint-email, pinpoint-sms-voice, pinpoint-sms-voice-v2, pipes, polly, pricing, privatenetworks, proton, qldb, qldb-session, quicksight, ram, rbin, rds, rds-data, redshift, redshift-data, redshift-serverless, rekognition, resiliencehub, resource-explorer-2, resource-groups, resourcegroupstaggingapi, robomaker, rolesanywhere, route53, route53-recovery-cluster, route53-recovery-control-config, route53-recovery-readiness, route53domains, route53resolver, rum, s3, s3control, s3outposts, sagemaker, sagemaker-a2i-runtime, sagemaker-edge, sagemaker-featurestore-runtime, sagemaker-geospatial, sagemaker-metrics, sagemaker-runtime, savingsplans, scheduler, schemas, sdb, secretsmanager, securityhub, securitylake, serverlessrepo, service-quotas, servicecatalog, servicecatalog-appregistry, servicediscovery, ses, sesv2, shield, signer, simspaceweaver, sms, sms-voice, snow-device-management, snowball, sns, sqs, ssm, ssm-contacts, ssm-incidents, ssm-sap, sso, sso-admin, sso-oidc, stepfunctions, storagegateway, sts, support, support-app, swf, synthetics, textract, timestream-query, timestream-write, tnb, transcribe, transfer, translate, verifiedpermissions, voice-id, vpc-lattice, waf, waf-regional, wafv2, wellarchitected, wisdom, workdocs, worklink, workmail, workmailmessageflow, workspaces, workspaces-web, xray
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 6, in lambda_handler
    bedrock = boto3.client(service_name='bedrock-runtime', region_name="us-east-1")
  File "/var/lang/lib/python3.11/site-packages/boto3/__init__.py", line 92, in client
    return _get_default_session().client(*args, **kwargs)
  File "/var/lang/lib/python3.11/site-packages/boto3/session.py", line 299, in client
    return self._session.create_client(
  File "/var/lang/lib/python3.11/site-packages/botocore/session.py", line 991, in create_client
    client = client_creator.create_client(
  File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 129, in create_client
    service_model = self._load_service_model(service_name, api_version)
  File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 231, in _load_service_model
    json_model = self._loader.load_service_model(
  File "/var/lang/lib/python3.11/site-packages/botocore/loaders.py", line 142, in _wrapper
    data = func(self, *args, **kwargs)
  File "/var/lang/lib/python3.11/site-packages/botocore/loaders.py", line 408, in load_service_model
    raise UnknownServiceError(

独自モデルを読み込ませて最新AWSサービスを利用してみる

普通に最新AWSサービスを利用できないことが確認できたので、今度は環境変数AWS_DATA_PATHを設定し、独自にパッケージングしたbedrock-runtimeのモデル定義を読み込ませてテストします。Lambdaの設定から以下のように環境変数を設定します。

環境変数設定後に再度テスト実行すると...

botocoreとboto3のバージョンは古いままですが無事にテスト成功しました。これはつまりbedrock-runtimeのclientクラスの生成に成功したということであり、最新AWSサービスの定義が読み込めていることが分かります。

あとはマネコンからLambdaのコードを編集しながら遊んでいきましょう

まとめ

非最新のboto3から最新AWSサービスを利用する方法の紹介でした。

実際に業務で最新AWSサービスを利用したい場合はLambda Layerを利用すべきですが、個人の検証目的やちょっとした遊びでLambda & boto3から最新AWSサービスを利用したい時のライトな解決策として覚えておいて損は無いと思います。