これは、なにをしたくて書いたもの?
AWSのサービスをローカル環境で試す時に使うものといえば、LocalStackが著名なのではないかなと思います。
LocalStack - A fully functional local cloud stack
今回はMotoというものを試してみたいと思います。
Moto
Motoは、AWSのインフラを簡単にモックしてテストしやすくするためのライブラリーです。
A library that allows you to easily mock out tests based on AWS infrastructure.
Moto: Mock AWS Services — Moto 4.1.9.dev documentation
Pythonで実装されています。
使用するサービスを指定してインストールしたり、全部インストールしたりしてPythonから使います。
$ pip3 install 'moto[ec2,s3,..]' $ pip3 install 'moto[all]'
実装されているAWSサービスはこちらに一覧があり、
Implemented Services — Moto 4.1.9.dev documentation
各サービスのページを見ると実装されているAPI、実装されていないAPIが確認できます。
s3 — Moto 4.1.9.dev documentation
使い方は、こちら。
Getting Started with Moto — Moto 4.1.9.dev documentation
Pythonコード内でデコレーターやコンテキストマネージャーを使ってモックしたり、直接Motoを使ったり、テストコード内での
使い方が載っています。
Pythonコード以外からMotoを使う
こう書いていると、MotoはPythonのみで使えるライブラリーのようにも見えますが、Motoにはスタンドアロンのサーバーモードがあり、
こちらを使うことでPython以外からもMotoを使うことができます。
Non-Python SDK’s / Server Mode — Moto 4.1.9.dev documentation
インストール方法としては、パッケージとして導入する方法、Dockerイメージを使う方法があります。
$ pip3 install moto[server] $ moto_server $ docker container run motoserver/moto:latest
サンプルには、Python、Java、Scala、Terraformでの使用例が載っています。
Non-Python SDK’s / Server Mode / Example Usage
ただ、この使い方はAWS SDK for Python(Boto3)からアクセスする場合には推奨されていないことには注意しましょう。
However, this method isn’t encouraged if you’re using boto3, the best solution would be to use a decorator method.
Getting Started with Moto / Moto usage / Stand-alone server mode
MotoとLocalStack
Motoのサーバーモードの話を見ると、LocalStackと競合しそうなライブラリーに見えますが、MotoとLocalStackの関係については
双方に記載があったりします。
Motoでは、LocalStackはより高度な機能を付けた兄弟のような説明が書かれています。
LocalStack is Moto’s bigger brother with more advanced features, such as EC2 VM’s that you can SSH into and Dockerized RDS-installations that you can connect to.
FAQ for Developers / What … / Alternatives are there?
一方、LocalStackでは多くのサービスのバックエンドでMotoを拡張して使用していることが書かれています。
Many LocalStack service providers use moto as a backend. Moto is an open-source library that provides mocking for Python tests that use Boto, the Python AWS SDK. We re-use a lot of moto’s internal functionality, which provides mostly CRUD and some basic emulation for AWS services. We often extend services in Moto with additional functionality. Moto plays such a fundamental role for many LocalStack services, that we have introduced our own tooling around it, specifically to make requests directly to moto.
LocalStack Concepts / Service / call_moto
というわけで、MotoはLocalStackの基礎の役割にもなっている、という感じですね。
今回は、MotoのサーバーモードをTerraform、AWS SDK for JavaScriptから扱ってみたいと思います。
環境
今回の環境は、こちら。
$ python3 -V Python 3.10.6 $ pip3 -V pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)
$ aws --version aws-cli/2.11.18 Python/3.11.3 Linux/5.15.0-71-generic exe/x86_64.ubuntu.22 prompt/off
Terraform。
$ terraform version Terraform v1.4.6 on linux_amd64
Node.js。
$ node --version v18.16.0 $ npm --version 9.5.1
Moto(サーバーモード)をインストールする
まずはMoto(サーバーモード)をインストールします。
仮想環境の作成。
$ python3 -m venv venv $ . venv/bin/activate
Moto(サーバーモード)のインストール。
$ pip3 install moto[server]
今回は、4.1.8がインストールされました。
$ pip3 freeze | grep moto moto==4.1.8
なお、ダウンロードしたpipモジュールは300Mほどありました…。
起動。
$ moto_server
このまま本番環境では使わないこと、というのと、リッスンしているアドレスとポートが表示されます。
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit
ヘルプ。
$ moto_server -h usage: moto_server [-h] [-H HOST] [-p PORT] [-r] [-s] [-c SSL_CERT] [-k SSL_KEY] [service] positional arguments: service options: -h, --help show this help message and exit -H HOST, --host HOST Which host to bind -p PORT, --port PORT Port number to use for connection -r, --reload Reload server on a file change -s, --ssl Enable SSL encrypted connection with auto-generated certificate (use https://... URL) -c SSL_CERT, --ssl-cert SSL_CERT Path to SSL certificate -k SSL_KEY, --ssl-key SSL_KEY Path to SSL private key
リッスンするポートを変える場合。
$ moto_server -p 8000 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:8000 Press CTRL+C to quit
リッスンするアドレスを変える場合。
$ moto_server -H 0.0.0.0 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.0.6:5000 Press CTRL+C to quit
今回は、なにも指定せずに起動しておくことにします。
$ moto_server
TerraformでMotoを操作する
次は、TerraformからMotoを操作してみましょう。
今回はAmazon S3バケットの作成と、AWS Systems Manager Parameter Storeにエントリーを登録してみたいと思います。
作成したTerraform構成ファイルはこちら。
main.tf
terraform { required_version = "1.4.6" required_providers { aws = { source = "hashicorp/aws" version = "4.66.1" } } } provider "aws" { access_key = "mock_access_key" secret_key = "mock_secret_key" region = "us-east-1" skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true s3_use_path_style = true endpoints { s3 = "http://localhost:5000" ssm = "http://localhost:5000" } } resource "aws_s3_bucket" "my_bucket" { bucket = "my-bucket" } resource "aws_ssm_parameter" "ssm_parameter_word" { name = "word" type = "SecureString" value = "Parameter from Moto" } output "my_bucket_name" { value = aws_s3_bucket.my_bucket.bucket } output "parameter_name" { value = aws_ssm_parameter.ssm_parameter_word.name }
ポイントはこのあたりなのですが。
provider "aws" { access_key = "mock_access_key" secret_key = "mock_secret_key" region = "us-east-1" skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true s3_use_path_style = true endpoints { s3 = "http://localhost:5000" ssm = "http://localhost:5000" } }
これは以下の2つを参考にして書きました。
Non-Python SDK’s / Server Mode / Example Usage
Custom Service Endpoint Configuration / Connecting to Local AWS Compatible Solutions / LocalStack
クレデンシャルは今回は直接書いています。
あとはinit
してapply
。
$ terraform init $ terraform apply
$ export AWS_ACCESS_KEY_ID = test $ export AWS_SECRET_ACCESS_KEY = test $ export AWS_DEFAULT_REGION = us-east-1
Amazon S3バケットの作成確認。エンドポイントはMoto(サーバーモード)に向けています。
$ aws --endpoint http://localhost:5000 s3 ls 2023-05-06 23:18:36 my-bucket
AWS Systems Manager Parameter Storeの確認。
$ aws --endpoint http://localhost:5000 ssm get-parameter --name word --with-decryption { "Parameter": { "Name": "word", "Type": "SecureString", "Value": "Parameter from Moto", "Version": 1, "LastModifiedDate": "2023-05-06T23:18:36.459000+09:00", "ARN": "arn:aws:ssm:us-east-1:123456789012:parameter/word", "DataType": "text" } }
OKですね。
AWS SDK for JavaScript v3からMoto(サーバーモード)へアクセスする
最後は、Moto(サーバーモード)上に構築したリソースに、AWS SDK for JavaScript v3からアクセスしてみます。
Node.jsプロジェクトを作成。言語はTypeScriptを使うことにします。
$ npm init -y $ npm i -D typescript $ npm i -D @types/node@v18 $ npm i -D prettier $ npm i -D jest @types/jest $ npm i -D esbuild esbuild-jest $ mkdir src test
AWS SDK for JavaScript v3のうち、Amazon S3とAWS Systems Managerへのクライアントモジュールを追加。
$ npm i @aws-sdk/client-s3 @aws-sdk/client-ssm
依存関係。
"devDependencies": { "@types/jest": "^29.5.1", "@types/node": "^18.16.5", "esbuild": "^0.17.18", "esbuild-jest": "^0.5.0", "jest": "^29.5.0", "prettier": "^2.8.8", "typescript": "^5.0.4" }, "dependencies": { "@aws-sdk/client-s3": "^3.328.0", "@aws-sdk/client-ssm": "^3.328.0" }
scripts
。
"scripts": { "build": "tsc --project .", "build:watch": "tsc --project . --watch", "typecheck": "tsc --project ./tsconfig.typecheck.json", "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch", "test": "jest", "format": "prettier --write src test" },
各種設定ファイル。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "moduleResolution": "node", "lib": ["esnext"], "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true }, "include": [ "src" ] }
tsconfig.typecheck.json
{ "extends": "./tsconfig", "compilerOptions": { "baseUrl": "./", "noEmit": true }, "include": [ "src", "test" ] }
.prettierrc.json
{ "singleQuote": true, "printWidth": 120 }
jest.config.js
module.exports = { testEnvironment: 'node', transform: { "^.+\\.tsx?$": "esbuild-jest" } };
確認は、テストコードで行います。
作成したAmazon S3バケットへのオブジェクトのput/get。
test/moto-s3-access.test.ts
import { GetObjectCommand, GetObjectCommandInput, PutObjectCommand, PutObjectCommandInput, S3Client, } from '@aws-sdk/client-s3'; test('create bucket and put/get', async () => { const client = new S3Client({ credentials: { accessKeyId: 'mock_access_key', secretAccessKey: 'mock_secret_key', }, region: 'us-east-1', endpoint: 'http://localhost:5000', forcePathStyle: true, }); const bucketName = 'my-bucket'; const objectKey = 'test-object'; const putObjectCommandInput: PutObjectCommandInput = { Bucket: bucketName, Key: objectKey, Body: 'Hello Moto Server!!', }; try { await client.send(new PutObjectCommand(putObjectCommandInput)); expect('unreachable code').toBe('fail'); } catch (e) { expect((e as Error).message).toBe('AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end'); } const getObjectCommandInput: GetObjectCommandInput = { Bucket: bucketName, Key: objectKey, }; const getObjectCommandOutput = await client.send(new GetObjectCommand(getObjectCommandInput)); expect(await getObjectCommandOutput.Body?.transformToString('utf-8')).toBe('Hello Moto Server!!'); });
妙なコードが入っていますが…。
try { await client.send(new PutObjectCommand(putObjectCommandInput)); expect('unreachable code').toBe('fail'); } catch (e) { expect((e as Error).message).toBe('AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end'); }
これは、PutObjectCommand
を実行した時にこんなError
がスローされて失敗するからですね…。
Error: AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end at asSdkError (/path/to/node_modules/@aws-sdk/middleware-retry/dist-cjs/util.js:11:12) at /path/to/node_modules/@aws-sdk/middleware-retry/dist-cjs/retryMiddleware.js:35:51 at processTicksAndRejections (node:internal/process/task_queues:95:5) at /path/to/node_modules/@aws-sdk/middleware-flexible-checksums/dist-cjs/flexibleChecksumsMiddleware.js:58:20 at /path/to/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:7:26
Amazon S3互換のサービスを使うと、ハマることがあるようです。
S3 Upload API fails with ERR_STREAM_WRITE_AFTER_END · Issue #3404 · aws/aws-sdk-js · GitHub
ちなみに、LocalStackに置き換えて試すとこうはなりませんでした…。
AWS Systems Manager Parameter Store。こちらは特に問題なかったです。
test/moto-ssm-parameter-store-access.test.ts
import { GetParameterCommand, GetParameterCommandInput, SSMClient } from '@aws-sdk/client-ssm'; test('get parameter', async () => { const client = new SSMClient({ credentials: { accessKeyId: 'mock_access_key', secretAccessKey: 'mock_secret_key', }, region: 'us-east-1', endpoint: 'http://localhost:5000', }); const getParameterCommandInput: GetParameterCommandInput = { Name: 'word', WithDecryption: true, }; const getParameterCommandOutput = await client.send(new GetParameterCommand(getParameterCommandInput)); expect(getParameterCommandOutput.Parameter?.Value).toBe('Parameter from Moto'); });
Amazon S3でちょっと困ったことになりましたが、今回はこれで良しとしましょう。
まとめ
Moto(サーバーモード)を使って、TerraformやNode.jsからAWSのサービスの代替としてアクセスしてみました。
この使い方だとLocalStackと変わらない気もしますが、APIのカバー範囲などを見つつ使い分けていきたいなと思います。