CLOVER🍀

That was when it all began.

Moto(Server Mode)を使って、AWSのサービスをローカルで試してみる

これは、なにをしたくて書いたもの?

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で実装されています。

GitHub - getmoto/moto: A library that allows you to easily mock out tests based on AWS infrastructure.

使用するサービスを指定してインストールしたり、全部インストールしたりして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から扱ってみたいと思います。

環境

今回の環境は、こちら。

Python。

$ python3 -V
Python 3.10.6


$ pip3 -V
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

AWS CLI。

$ 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

AWS CLIで確認してみましょう。環境変数を設定。

$ 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

@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のカバー範囲などを見つつ使い分けていきたいなと思います。