CLOVER🍀

That was when it all began.

LocalStackを使って、AWSの機能をローカルで動かしてみる(S3)

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

  • LocalStackという、クラウドアプリケーション開発のためのモック/テスト用フレームワークがあるらしい
  • 現在はAWSのスタックを扱えるように開発しているらしく、これでAWSの機能をローカルで試してみよう
  • とりあえず、S3を触ってみよう

そんな動機で、LocalStackというものを試してみました。

LocalStack?

最初に書きましたが、クラウドアプリケーション開発のための、モック/テスト用フレームワークだそうです。

LocalStack

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud apps offline!

エラーインジェクションができることだったり、サービスがプラガブルであること、他のフレームワーク(moto)と比べて、
プロセスが独立していることを売りにしているようです。

Why LocalStack?

日本語情報も、いくつか。

LocalStackをつかってローカルでLambdaを実行してみた | Developers.IO

AtlassianのLocalStackを使ってみてなんとなく理解するまでのお話

LocalStack導入から実行まで

ドキュメントを読んでいると、JUnitと統合する機能もあるようです。

Integration with Java/JUnit

起動すると、各種APIが起動するようですが、それぞれが使用するポートのリストはこちら。

Overview

では、今回はS3のモックサービスとして使ってみましょう。

LocalStackを起動する

LocalStackはpipを使ってインストールするもののようですが、Dockerイメージが提供されているようなので、
こちらを使用することにします。

localstack/localstack

起動。

$ docker container run -it --rm --name localstack localstack/localstack:0.8.7

起動時に、各種APIのポートが出力されるので、見ておきましょう。

Starting mock API Gateway (http port 4567)...
Starting mock DynamoDB (http port 4569)...
Starting mock SES (http port 4579)...
Starting mock Kinesis (http port 4568)...
Starting mock Redshift (http port 4577)...
Starting mock S3 (http port 4572)...
Starting mock CloudWatch (http port 4582)...
Starting mock CloudFormation (http port 4581)...
Starting mock SSM (http port 4583)...
Starting mock SQS (http port 4576)...
Starting local Elasticsearch (http port 4571)...
Starting mock SNS (http port 4575)...
Starting mock DynamoDB Streams service (http port 4570)...
Starting mock Firehose service (http port 4573)...
Starting mock Route53 (http port 4580)...
2018-09-30T06:57:55:WARNING:infra.pyc: Service "elasticsearch" not yet available, retrying...
Starting mock ES service (http port 4578)...
Starting mock Lambda service (http port 4574)...

「Ready.」と表示されたら、起動完了です。

Ready.

あと、ダッシュボードが8080ポートで起動していたりします。

2018-09-30T06:57:43:INFO:werkzeug:  * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)

ホスト側のポートにはバインドしていません。コンテナのIPアドレスは、こちら。

$ docker container inspect localstack | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",

AWS CLIで接続してみる

まずは、AWS CLIから使ってみましょう。

AWS CLIは、こちらを見てインストールします。

AWS Command Line Interface のインストール - AWS Command Line Interface

クレデンシャルの設定。Access Key、Secret Access Keyを入力する必要があるのですが、設定さえしていれば値は
なんでも良いようです。

$ aws configure
AWS Access Key ID [None]: my-access-key-id
AWS Secret Access Key [None]: my-secret-access-key
Default region name [None]: 
Default output format [None]:

「--endpoint-url」を指定しつつ、s3のコマンドを使ってみます。

## Bucket作成
$ aws --endpoint-url=http://172.17.0.2:4572 s3 mb s3://test-bucket
make_bucket: test-bucket

## アップロード
$ echo 'Hello LocalStack' > file.txt
$ aws --endpoint-url=http://172.17.0.2:4572 s3 cp file.txt s3://test-bucket
upload: ./file.txt to s3://test-bucket/file.txt 

## 確認
$ aws --endpoint-url=http://172.17.0.2:4572 s3 ls test-bucket
2018-09-30 07:28:12         17 file.txt

## ダウンロード
$ aws --endpoint-url=http://172.17.0.2:4572 s3 cp s3://test-bucket/file.txt download.txt 
download: s3://test-bucket/file.txt to ./download.txt           
$ cat download.txt 
Hello LocalStack

「--endpoint-url」を指定する必要があるものの、ふつうに動きますね。

Node.jsから使ってみる

続いて、Node.jsのAWS SDKからアクセスしてみましょう。

Node.js 内の AWS SDK for JavaScript | AWS

What Is the AWS SDK for JavaScript? - AWS SDK for JavaScript

環境

今回の環境は、こちら。

$ node -v
v8.12.0


$ npm -v
6.4.1
準備

必要なライブラリをインストールします。テストコードで実行することにして、こちらはJestで。

$ npm i --save aws-sdk

$ npm i --save-dev jest

バージョンとJestの設定。

  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "aws-sdk": "^2.325.0"
  },
  "devDependencies": {
    "jest": "^23.6.0"
  },
  "jest": {
    "testEnvironment": "node"
  }
テストを書く

作成したコードは、こんな感じです。
test/localstack-s3.test.js

const AWS = require('aws-sdk');

AWS.config.update({
    accessKeyId: "access-key-id",
    secretAccessKey: "secret-access-key",
    s3: { endpoint: "http://172.17.0.2:4572" },
    s3ForcePathStyle: true
});

const s3 = new AWS.S3();

test('using s3', done => {
    const bucketName = 'my-bucket';
    const objectKey = 'my-object';
    const message = 'Hello Node.js!!';

    s3.createBucket({ Bucket: bucketName }, (err, bucketCreated) => {
        s3.putObject({ Bucket: bucketName, Key: objectKey, Body: message }, (err, objectPutted) => {

            s3.getObject({ Bucket: bucketName, Key: objectKey }, (err, data) => {
                expect(data.Body.toString('utf-8')).toEqual(message);
                done();
            });

        });
    });
});

test('using s3 with async', async () => {
    const bucketName = 'my-bucket-async';
    const objectKey = 'my-object';
    const message = 'Hello Node.js!! with Async';
    
    await s3.createBucket({ Bucket: bucketName }).promise();
    await s3.putObject({ Bucket: bucketName, Key: objectKey, Body: message }).promise();
    const data = await s3.getObject({ Bucket: bucketName, Key: objectKey }).promise();
    expect(data.Body.toString('utf-8')).toEqual(message);

    return Promise.resolve();
});

Access Key、Secret Access Keyやエンドポイントの設定をする必要があります。

AWS.config.update({
    accessKeyId: "access-key-id",
    secretAccessKey: "secret-access-key",
    s3: { endpoint: "http://172.17.0.2:4572" },
    s3ForcePathStyle: true
});

また、S3へのアクセスはPathStyleとなります。

あとは、ふつうに使えばOKです。

test('using s3', done => {
    const bucketName = 'my-bucket';
    const objectKey = 'my-object';
    const message = 'Hello Node.js!!';

    s3.createBucket({ Bucket: bucketName }, (err, bucketCreated) => {
        s3.putObject({ Bucket: bucketName, Key: objectKey, Body: message }, (err, objectPutted) => {

            s3.getObject({ Bucket: bucketName, Key: objectKey }, (err, data) => {
                expect(data.Body.toString('utf-8')).toEqual(message);
                done();
            });

        });
    });
});

Promiseを使って書くこともできるんですね、といろいろ調べていて気づきました。

test('using s3 with async', async () => {
    const bucketName = 'my-bucket-async';
    const objectKey = 'my-object';
    const message = 'Hello Node.js!! with Async';
    
    await s3.createBucket({ Bucket: bucketName }).promise();
    await s3.putObject({ Bucket: bucketName, Key: objectKey, Body: message }).promise();
    const data = await s3.getObject({ Bucket: bucketName, Key: objectKey }).promise();
    expect(data.Body.toString('utf-8')).toEqual(message);

    return Promise.resolve();
});

Support for Promises in the SDK | AWS Developer Blog

Using JavaScript Promises - AWS SDK for JavaScript

実行。

$ npm test

結果は、AWS CLIでも確認してみましょう。

$ aws --endpoint-url=http://172.17.0.2:4572 s3 ls                                       
2006-02-03 16:45:09 test-bucket
2006-02-03 16:45:09 my-bucket-async
2006-02-03 16:45:09 my-bucket


$ aws --endpoint-url=http://172.17.0.2:4572 s3 ls my-bucket
2018-09-30 07:40:22         15 my-object
$ aws --endpoint-url=http://172.17.0.2:4572 s3 cp s3://my-bucket/my-object a.txt
download: s3://my-bucket/my-object to ./a.txt                   
$ cat a.txt 
Hello Node.js!!

$ aws --endpoint-url=http://172.17.0.2:4572 s3 ls my-bucket-async
2018-09-30 07:40:22         26 my-object
$ aws --endpoint-url=http://172.17.0.2:4572 s3 cp s3://my-bucket-async/my-object b.txt
$ cat b.txt 
Hello Node.js!! with Async

OKそうですね。

ダッシュボード

最初に8080ポートの紹介をしましたが、LocalStackが稼働しているサーバーの8080ポートにHTTPでアクセスすると、
ダッシュボードを見ることができます。

http://[LocalStackが稼働しているサーバー]:8080/ f:id:Kazuhira:20180930164738p:plain

LocalStackを使うと、どのサービスを使っているのかが確認できるようですね。

まとめ

LocalStackを使って、AWSのサービスのうち、S3のモックを使って動かしてみました。

こういう互換のサービスが、テスト目的でローカルで動かせるのは便利ですね。