日頃の行い

個人的な日頃の行いをつらつら書いてます\\\\ ٩( 'ω' )و ////

localstack上でSQS->Lambda(Scala製)の連携を動かしてみる

開発時にlocalstackをAmazon Resourceのモックとして使っていて、
前試してなんか動かなかったんだけど、ちゃんとやったら動いたのでそのメモです。

github.com

localstack上のSQSにメッセージを送ったら、localstack上でmappingされたScala製のLambdaがキックされる様子を観測するのがゴールです。
検証用に使ったコードはこちらです。

github.com

登場するコマンドたちのversion

$aws --version
aws-cli/1.16.260 Python/3.7.4 Darwin/18.7.0 botocore/1.12.250

$docker-compose --version
docker-compose version 1.24.1, build 4667896b

1. localstackの起動

docker-composeを利用しつつlocalstackのSQSとLambdaを起動します。
利用したlocalstackのversionは 0.10.6 です。
networksは内部で別のコンテナと通信したいときに指定するために使います。
(実は今回は使ってないですごめんね)

docker-compose.yml

version: "2"

services:
    localstack:
        image: localstack/localstack:0.10.6
        ports:
          - "4567-4597:4567-4597"
        environment:
            LAMBDA_EXECUTOR: docker
            DOCKER_HOST: unix:///var/run/docker.sock
            DATA_DIR: /tmp/localstack/data
            SERVICES: sqs,lambda
            DEBUG: 1
            # 内部APIを叩きたいときなどに使う
            LAMBDA_DOCKER_NETWORK: localstack-sqs-lambda_foo_network
        volumes:
          - "/var/run/docker.sock:/var/run/docker.sock"
        networks:
            foo_network:

networks:
    foo_network:

2. Lambda Handlerの作成

特に強い意図はないのですが仕事で触れていたのがScalaだったのでScalaで書いています。
HelloWorldとメッセージに含まれるBodyを表示する感じです。

scala/src/main/scala/com/ru/waka/FooHandler.scala

package com.ru.waka

import com.amazonaws.services.lambda.runtime.{Context, RequestHandler}
import com.amazonaws.services.lambda.runtime.events.SQSEvent
import scala.collection.JavaConverters._

class FooHandler extends RequestHandler[SQSEvent, Unit] {
  override def handleRequest(input: SQSEvent, context: Context): Unit = {
    context.getLogger.log("Hello World from Scala Code\n")
    context.getLogger.log("Message's body is...\n")
    input.getRecords.asScala.foreach(r => {
      context.getLogger.log(s"${r.getBody}")
    })
  }
}

Lambdaにわたす際のビルドにはsbt-assemblyを利用しています。

github.com

ビルドの様子

$make -C scala build
# Lambdaのコードビルド
make[1]: Entering directory '/Users/arata/.ghq/github.com/ara-ta3/localstack-sqs-lambda/scala'
./tools/sbt/bin/sbt assembly
[info] Loading settings for project scala-build from build.sbt ...
[info] Loading project definition from /Users/arata/.ghq/github.com/ara-ta3/localstack-sqs-lambda/scala/project
[info] Loading settings for project scala from build.sbt ...
[info] Set current project to scala (in build file:/Users/arata/.ghq/github.com/ara-ta3/localstack-sqs-lambda/scala/)
[info] Strategy 'discard' was applied to 32 files (Run the task at debug level to see details)
[info] Strategy 'filterDistinctLines' was applied to a file (Run the task at debug level to see details)
[info] Assembly up to date: /Users/arata/.ghq/github.com/ara-ta3/localstack-sqs-lambda/scala/target/scala-2.12/scala-assembly-0.1.0-SNAPSHOT.jar
[success] Total time: 1 s, completed 2019/12/20 22:50:41
make[1]: Leaving directory '/Users/arata/.ghq/github.com/ara-ta3/localstack-sqs-lambda/scala'

3. SQSとLambdaの作成

次にlocalstack上にSQSとLambda、そしてそのマッピングを作成します。
Lambda作成時には 2. でビルドした成果物のjarファイルを上げています。

# SQSの作成
$aws --endpoint-url http://localhost:4576 \
    sqs create-queue --queue-name 'foo-queue'
{
    "QueueUrl": "http://localhost:4576/queue/foo-queue"
}

# Lambdaの作成
$aws --endpoint-url http://localhost:4574 \
    lambda create-function \
    --function-name 'foo-function' \
    --runtime=java8 \
    --role=dummyrole \
    --handler=com.ru.waka.FooHandler \
    --zip-file=fileb://./scala/target/scala-2.12/scala-assembly-0.1.0-SNAPSHOT.jar
{
    "FunctionName": "foo-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:foo-function",
    "Runtime": "java8",
    "Role": "dummyrole",
    "Handler": "com.ru.waka.FooHandler",
    "CodeSize": 10857096,
    "Description": "",
    "Timeout": 3,
    "LastModified": "2019-12-20T13:50:43.397+0000",
    "CodeSha256": "VJ4FGoNtdijTBO5acf/pu5WW/K/FQFytSH+x9WprcQc=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "fbaff8d2-6ac0-4498-879a-57b87cdd1466"
}

# SQSとLambdaのマッピング
$aws --endpoint-url http://localhost:4574 \
    lambda create-event-source-mapping \
    --event-source-arn arn:aws:sqs:us-east-1:000000000000:foo-queue  \
    --function-name "foo-function"
{
    "UUID": "b07dc12c-5a74-490b-82b4-1e43abdb6761",
    "BatchSize": 100,
    "EventSourceArn": "arn:aws:sqs:us-east-1:000000000000:foo-queue",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:foo-function",
    "LastModified": 1576849845.0,
    "LastProcessingResult": "OK",
    "State": "Enabled",
    "StateTransitionReason": "User action"
}

これでSQSにメッセージを送ればLambdaがキックされてHello Worldが表示されるはずです。

4. メッセージを送る

aws cliからメッセージを送ってみました。
そうするとdocker-composeのログにINFOログが流れてきたのがわかります。

$aws --region us-east-1 --endpoint-url http://localhost:4576 \
    sqs send-message \
    --queue-url 'http://localstack:4576/queue/foo-queue' \
    --message-body 'hogehoge'
{
    "MD5OfMessageBody": "329435e5e66be809a656af105f42401e",
    "MD5OfMessageAttributes": "d41d8cd98f00b204e9800998ecf8427e",
    "MessageId": "c3aa9a0c-9c3d-4b61-a426-9d45a2b2595f"
}

# docker-composeのログ
localstack_1  | 13:50:42.627 [elasticmq-akka.actor.default-dispatcher-4] INFO  o.elasticmq.actor.QueueManagerActor - Creating queue QueueData(foo-queue,MillisVisibilityTimeout(30000),PT0S,PT0S,2019-12-20T13:50:42.597Z,2019-12-20T13:50:42.597Z,None,false,false,None,None,Map())
localstack_1  | 2019-12-20T13:53:26:DEBUG:localstack.services.awslambda.lambda_api: Found 1 source mappings for event from SQS queue arn:aws:sqs:us-east-1:000000000000:foo-queue: ['arn:aws:lambda:us-east-1:000000000000:function:foo-function']
localstack_1  | 2019-12-20T13:53:26:DEBUG:localstack.services.awslambda.lambda_executors: Running lambda cmd: CONTAINER_ID="$(docker create -i  --entrypoint ""  -e DOCKER_LAMBDA_USE_STDIN="$DOCKER_LAMBDA_USE_STDIN" -e HOSTNAME="$HOSTNAME" -e LOCALSTACK_HOSTNAME="$LOCALSTACK_HOSTNAME" -e AWS_LAMBDA_FUNCTION_NAME="$AWS_LAMBDA_FUNCTION_NAME" -e AWS_LAMBDA_FUNCTION_VERSION="$AWS_LAMBDA_FUNCTION_VERSION" -e AWS_LAMBDA_FUNCTION_INVOKED_ARN="$AWS_LAMBDA_FUNCTION_INVOKED_ARN" --network="localstack-sqs-lambda_foo_network" --rm "lambci/lambda:java8" bash -c 'cd /var/task; java  -cp ".:localstack-utils-fat.jar" "cloud.localstack.LambdaExecutor" "com.ru.waka.FooHandler" "event_file.json"')";docker cp "/tmp/localstack/zipfile.2c9c80b9/." "$CONTAINER_ID:/var/task"; docker start -ai "$CONTAINER_ID";
localstack_1  | 2019-12-20T13:53:29:DEBUG:localstack.services.awslambda.lambda_executors: Lambda arn:aws:lambda:us-east-1:000000000000:function:foo-function result / log output:
localstack_1  | ()
localstack_1  | >Dec 20, 2019 1:53:28 PM cloud.localstack.LambdaContext$1 log
localstack_1  | > INFO: Hello World from Scala Code
localstack_1  | >
localstack_1  | > Dec 20, 2019 1:53:28 PM cloud.localstack.LambdaContext$1 log
localstack_1  | > INFO: Message's body is...
localstack_1  | >
localstack_1  | > Dec 20, 2019 1:53:28 PM cloud.localstack.LambdaContext$1 log
localstack_1  | > INFO: hogehoge

ハマったところ

aws cliからメッセージを送った際、Regionが $HOME/.aws/config に記述されているデフォルトのRegionを見ていて ap-northeast-1 になっていました。
しかし、アプリケーション(Java SDK)から叩いた際にRegionを ap-northeast-1 に指定しても利用されるSQSは us-east-1 になってしまいました。
そのため、上記では us-east-1 を利用するようにしています。
他の言語のSDKがどうなっているかはわからないですが、ローカルでの開発用途になると思いますし、localstackに対してaws cliで叩くときにregionのオプションをつければいいだけになるので us-east-1 でとりあえず逃げることはできそうですね。

Java SDKから叩いた際のコード

scala/src/main/scala/com/ru/waka/SQSMessageSender.scala

package com.ru.waka

import com.amazonaws.regions.Regions
import com.amazonaws.services.sqs.AmazonSQSClientBuilder
import com.amazonaws.services.sqs.model.SendMessageRequest

object SQSMessageSender {
  def main(args: Array[String]): Unit = {
    val sqs = AmazonSQSClientBuilder
      .standard()
      .withRegion(Regions.AP_NORTHEAST_1)
      .build()
    val req = new SendMessageRequest()
        .withQueueUrl("http://localhost:4576/queue/foo-queue")
        .withMessageBody("Message from Scala Code")
    sqs.sendMessage(req)
  }
}

terraform v0.8.8からv0.10.3に移行した時remote state周りでやったことメモ

気がついたらterraformのバージョンが上がってました。
使ってた時は0.8.xだった気がしたけど、
この前見たら0.10.xになってて、なるほど〜となりました。

細かいCHANGE LOGは terraform/CHANGELOG.md at master · hashicorp/terraform · GitHub をみるとして、
0.9.x から terraform remote hogehoge がなくなっていたので
0.10.x のversionでresourceのremote管理する方法を確認してみました。

確認は v0.10.3terraform plan したら出てきたURLを参考にしました。

$terraform --version
Terraform v0.10.3

$terraform plan
Deprecation warning: This environment is configured to use legacy remote state.
Remote state changed significantly in Terraform 0.9. Please update your remote
state configuration to use the new 'backend' settings. For now, Terraform
will continue to use your existing settings. Legacy remote state support
will be removed in Terraform 0.11.

You can find a guide for upgrading here:

https://www.terraform.io/docs/backends/legacy-0-8.html

www.terraform.io

↑のページの流れに沿って、移行の手順を書いてみました。

1. terraform v0.8.x で remote state を持ってくる

With the older Terraform version (version 0.8.x), run terraform remote pull. This will cache the latest legacy remote state data locally. We’ll use this for a backup in case things go wrong.

古いversionのterraformでローカルに最新の状態を持ってきて、
古いバージョンはここからダウンロードできました。

Terraform Versions | HashiCorp Releases

2. .terraform/terraform.tfstate のバックアップ

Backup your .terraform/terraform.tfstate file. This contains the cache we just pulled. Please copy this file to a location outside of your Terraform module.

何かあった時用にバックアップをとっておきましょう。
私はとりあえず /tmp に置きました。

3. backendの設定

Configure your backend in your Terraform configuration. The backend type is the same backend type as you used with your legacy remote state. The configuration should be setup to match the same configuration you used with remote state.

www.terraform.io

backendの設定を↑ページを参考に追加しましょう。
backendの設定がファイルになったんですね。
設定ファイルはこんな感じになりました。

backend.tf

terraform {
    backend "s3" {
        bucket = "terraform-sample-tfstate"
        key = "sample.tfstate"
        region = "ap-northeast-1"
    }
}

もしくはこのように最低限の設定ファイルを書いて、
initコマンド時にoptionで指定することもできます。

terraform {
    backend "s3" {}
}
$terraform init \
    -backend-config="bucket=terraform-sample-tfstate" \
    -backend-config="key=sample.tfstate" \
    -backend-config="region=ap-northeast-1" 
# backend configオプションにはファイルを指定するのかと思ったら違いました(ヘルプ読まないせい

(backend configの使い方はここで気が付きました。
Terraform backend init: settings in -backend-config file are ignored · Issue #13552 · hashicorp/terraform · GitHub

4. initコマンドの実行

Run the init command. This is an interactive process that will guide you through migrating your existing remote state to the new backend system. During this step, Terraform may ask if you want to copy your old remote state into the newly configured backend. If you configured the identical backend location, you may say no since it should already be there.

あとはinitコマンドを打つだけです。

$terraform init \
        -backend=true \
        -force-copy \
        -get=true \
        -input=false

Initializing the backend...
New backend configuration detected with legacy remote state!

Terraform has detected that you're attempting to configure a new backend.
At the same time, legacy remote state configuration was found. Terraform will
first configure the new backend, and then ask if you'd like to migrate
your remote state to the new backend.



Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (0.1.4)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 0.1"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

# terraform {
#    backend "s3" {}
# }
# backend.tfが↑のときは↓のようなコマンドになります

$terraform init \
        -backend-config="bucket=terraform-sample-tfstate" \
        -backend-config="key=sample.tfstate" \
        -backend-config="region=ap-northeast-1" \
        -backend=true \
        -force-copy \
        -get=true \
        -input=false

5. terraform planを実行してstateが正しいか確認

Verify your state looks good by running terraform plan and seeing if it detects your infrastructure. Advanced users may run terraform state pull which will output the raw contents of your state file to your console. You can compare this with the file you saved. There may be slight differences in the serial number and version data, but the raw data should be almost identical.

terraform planを実行してみて、問題なさそうか確認します。

$terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aws_s3_bucket.terraform-sample-tfstate: Refreshing state... (ID: terraform-sample-tfstate)
No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, Terraform
doesn't need to do anything.

up-to-dateみたいなので問題なさそうです。
init時にbackendの設定がうまくいっていない場合、
はじめにplanしたときみたいに Deprecation warning みたいなのが出ます。
その時はbackendの設定がうまく言ってないはずなので、
initを頑張るといい感じになるかなと思います・・・

最終的にできたレポジトリはこんな感じでした。
S3は確か名前の重複ができない気がしたのでそのままでは試せないかもしれないですが、
参考になれば幸いです。
terraformガンガン進んでいって追いつくの大変だけど、とても便利なので使っていきたいと思います。

github.com

ScalaでDynamoDBに接続するためにAWS SDKを利用してみた時の話

ScalaJavaAWS SDKを使ってDynamoDBを利用した時のメモ
本番ではDynamoDBの本物を利用してましたが、ここではDynamoDB Localを利用します。
Java製のAWS SDKScalaから利用しています。

作業したgithubレポジトリはこちらgithub.com

1. DynamoDBローカルのインストール

下記のページからダウンロードして使います。

docs.aws.amazon.com

DynamoDBローカルの準備や起動についてはREADMEを参考にしてください。

https://github.com/tarata/dynamodb-local-for-blog/blob/master/README.md

2. 適当なテーブルの作成

HashキーやらRangeキーなど設定がありますが、それらについては下記のページを参考にしてください。
今回はHashキーをidにしたテーブル「users」を作成します。

qiita.com

Credentialは $HOME/.aws/credential のkeyを利用しています。

テーブル作成に利用するクラス

これで設定おkです
この後UserモデルだけJavaで書きました
なぜなら、Scala力が足りないからです。

Userモデル

3. テーブルのデータを突っ込む部分と取得する部分

データを突っ込む部分

データを取得する部分

たったこれだけでDynamoDBへのORマッパー的なことが出来ます。
アノテーションで出来て便利ですね!!11
4. ハマったところに続く...


ちなみに、jsonを保存したりしたい場合は以下のサイトが参考になります。
Using Custom Marshallers to Store Complex Objects in Amazon DynamoDB - AWS Developer Blog - Java

4. ハマったところ

com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: Failed to instantiate new instance of class
	at com.amazonaws.services.dynamodbv2.datamodeling.ConversionSchemas$StandardItemConverter.createObject(ConversionSchemas.java:629)
	at com.amazonaws.services.dynamodbv2.datamodeling.ConversionSchemas$StandardItemConverter.unconvert(ConversionSchemas.java:420)
	at com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.privateMarshallIntoObject(DynamoDBMapper.java:690)

Failed to instantiate new instance of class は主に Class.newInstance() を実行した時に出るエラーメッセージみたいですね。
Java力が足りなくてわかりませんでした。
Scala力も足りないしDynamoDB力も足りないので、切り分けが出来なかったのですが、普通にググったら普通にわかってあれがあれでした。

このOR Mapperはデータを取得した際に、今回で言えば、 **User.newInstance()** を実行し、
その後setterでデータを入力しているようです。
なので、引数なしコンストラクタがないと死にます←

IntelliJが「これ使ってないよ!使ってないのに消さないの??」
って言ってきたので消したら動きませんでした。
怖いですね、リフレクションメソッド

5. 最終的にどうしたか

正直Mapperは必要がないので、findAllとsaveといメソッドを持ったRepositoryを定義しました。

UserRepository.scala

UserConversions.scala

こうすればアノテーション地獄にもならずかけます。
そんな複雑なことしないので、ORマッパーを使うよりリポジトリを定義して使ってあげるほうが楽そうです。