「AWS無料相談会」をオンラインで開催中

Docker で Step Functions Local の実行環境を構築する

最近、車を購入したので、色々なところに遠出をしています。エンジニアの内山です。

今回は、Step Functions をローカルPC内で実行する環境の構築について、ご紹介します。

概要

Step Functions は、複数の Lambda 関数を組み合わせて機能を実装できるようにするサービスです。
ループや分岐などのロジックを組むことができます。
ロジックが複雑になってくると、ローカルPCで動作確認したいと思うようになってきます。

ローカルPCで動作させるために Step Functions Local というツールが公式で用意されています。
今回は、Docker と Step Functions Local を用いて、動作チェック環境をローカルPC(Mac)上に構築する方法をご紹介します。

さっさと試したい方は以下のリポジトリをご参照ください。

https://github.com/memememomo/step-functions-local-test

構成

上記のような環境を構築します。設定するコンテナとしては以下の二種類があります。

  • SAM Local用のコンテナ
  • Step Functions用のコンテナ

SAM Local は、ローカルPC上で Lambda 関数を実行するツールです。Lambda 関数の実行時には、実行用のコンテナを作成します。注意点として、Docker Compose で作成したネットワーク(net)を設定する必要がある ということです。このことについては後述します。

また、図には含めていませんが、Goプログラムをビルドする用のコンテナも作成しています。

各 Dockerfile の設定

各コンテナ用の Dockerfile を設定していきます。

SAM Local 用の Dockerfile

SAM Local 用のコンテナでは、Python がインストールされているイメージを使用します。
Dockerfile では、SAM Local 用のコマンド(aws-sam-cli)をインストールするように設定しています。

# Dockerfile_sam
FROM python:3.5-alpine

ENV PYTHONUSERBASE=/usr/local

RUN apk add --no-cache py-pip git bash gcc libc-dev && 
    pip install --upgrade pip && 
    pip install --user awscli==1.16.76 aws-sam-cli==0.9.0

WORKDIR /var/opt
COPY . /var/opt/

EXPOSE 3001

Step Functions Local 用の Dockerfile

Step Functions Local 用のコンテナでは、公式で用意されているイメージを使用します。

# Dockerfile_stepfunctions
FROM amazon/aws-stepfunctions-local

EXPOSE 8083

Goプログラムビルド用の Dockerfile

Goプログラムビルド用のコンテナでは、Goがインストールされているイメージを使用します。
Dockerfile では、必要なツールをインストールするように設定しています。

# Dockerfile_go
FROM golang:1.11.5-alpine

RUN apk add --no-cache git bash make curl gcc libc-dev openssl && 
    go get -u github.com/golang/dep/cmd/dep

WORKDIR /go/src/step-functions-local-test
COPY . /go/src/step-functions-local-test

RUN dep ensure
RUN cd ./handlers/helloworld/ && CGO_ENABLED=0 GOOS=linux go build -v -installsuffix cgo -o main .

各種スクリプト

Docker コンテナ内で実行するスクリプトを作成していきます。

Goプログラムビルドスクリプト

go-build.sh という名前で、Goプログラムビルドスクリプトを作成します。
内容としては、依存モジュールのインストールとコンパイルコマンドの実行を行っています。

#!/usr/bin/env bash

dep ensure
cd ./handlers/helloworld/ && CGO_ENABLED=0 GOOS=linux go build -v -installsuffix cgo -o main .

SAM Local 起動スクリプト

start-lambda.sh という名前で、SAM Local を起動するスクリプトを作成します。
先述したとおり、Lambda関数実行用コンテナにDockerのネットワークを設定する ようにします。これが行われていないと、SAM LocalのLambdaから他のコンテナ(DynamoDB Localなど)にアクセスすることができなくなります。
--docker-networkオプションで、Dockerのネットワークを設定しています。

#!/bin/bash

sam local start-lambda 
  --docker-volume-basedir "${VOLUME}" 
  --docker-network step-functions-local-test_net 
  --host 0.0.0.0 
  --template template.yml

Lambda関数用のプログラム

handlers/helloworld/main.go という名前のファイルで、Lambda関数用のGoプログラムを作成します。
今回は、"Hello World!"という文字列を返すだけのLambda関数を実装しています。

package main

import "github.com/aws/aws-lambda-go/lambda"

func handler() (string, error) {
        return "Hello World!", nil
}

func main() {
        lambda.Start(handler)
}

Gopkg.toml という名前のファイルで、依存モジュールを記述します。

[prune]
  go-tests = true
  unused-packages = true

[[constraint]]
  name = "github.com/aws/aws-lambda-go"
  version = "1.2.0"

SAM テンプレート(template.yml)の設定

SAM Local で読み込ませるテンプレート(template.yml)を作成します。
"HelloWorld"という名前のLambda関数を作成し、先程作成したプログラムを実行するように設定しています。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: go1.x
    Timeout: 900

Resources:
  HelloWorld:
    Properties:
      CodeUri: ./handlers/helloworld
      FunctionName: HelloWorld
      Handler: main
    Type: AWS::Serverless::Function

Step Functions Local の設定

Step Functions Local に読み込ませるための設定ファイルを作成します。
aws-stepfunctions-local-credentials.txtというファイル名にします。
今回は Lambda のエンドポイントを指定しています(http://sam-local:3001/)。

AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
WAIT_TIME_SCALE=10
LAMBDA_ENDPOINT=http://sam-local:3001
BATCH_ENDPOINT=
DYNAMODB_ENDPOINT=
ECS_ENDPOINT=
GLUE_ENDPOINT=
SAGE_MAKER_ENDPOINT=
SQS_ENDPOINT=
SNS_ENDPOINT=
# https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/sfn-local-config-options.html#docker-credentials

Docker Composeの設定

Docker Composeで複数のDockerコンテナを連携させる設定を記述します。
docker-compose.yml というファイル名にします。

version: '3'
services:
  sam-local:
    build:
      context: ./
      dockerfile: ./Dockerfile_sam
    command: ./start-lambda.sh
    volumes:
      - .:/var/opt/
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - VOLUME=$PWD
    env_file:
      - .env
    networks:
      - net

  step-functions-local:
    build:
      context: ./
      dockerfile: ./Dockerfile_stepfunctions
    ports:
      - '8083:8083'
    env_file:
      - aws-stepfunctions-local-credentials.txt
    depends_on:
      - sam-local
    networks:
      - net

  # Goビルド用
  go-build:
    build:
      context: ./
      dockerfile: ./Dockerfile_go
    command: ./go-build.sh
    volumes:
      - .:/go/src/step-functions-local-test/:cached


networks:
  net:
    driver: bridge

以上で、設定が一通りできました。

コンテナの起動

以下のコマンドでコンテナを起動します。

# ビルド
$ COMPOSE_PROJECT_NAME=step-functions-local-test docker-compose build

# Lambdaハンドラーのビルド
$ COMPOSE_PROJECT_NAME=step-functions-local-test docker-compose run go-build ./go-build.sh

# サーバ立ち上げ
$ COMPOSE_PROJECT_NAME=step-functions-local-test docker-compose up

docker ps で確認すると、コンテナが起動していることを確認できると思います。

Step Functions の実行

起動している Step Functions コンテナ内に StateMachine を作成して、実行します。
コンテナを立ち上げたコンソールとは別のコンソールで、以下のコマンドを実行します。

# StateMachineを作成
$ aws stepfunctions --endpoint http://localhost:8083 create-state-machine --definition "{
    "Comment": "A Hello World example of the Amazon States Language using an AWS Lambda Local function",
    "StartAt": "HelloWorld",
    "States": {
      "HelloWorld": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld",
        "End": true
      }
    }
  }
  }}" --name "HelloWorld" --role-arn "arn:aws:iam::012345678901:role/DummyRole"


# StateMachineを実行
$ aws stepfunctions --endpoint http://localhost:8083 start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld

コンテナを立ち上げたコンソールで、以下のような出力が表示されたら成功です。

step-functions-local_1  | 2019-07-31 00:49:58.444: [200] StartExecution <= {"sdkResponseMetadata":null,"sdkHttpMetadata":null,"executionArn":"arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113","startDate":1564534198424}
step-functions-local_1  | 2019-07-31 00:49:58.483: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"ExecutionStarted","PreviousEventId":0,"ExecutionStartedEventDetails":{"Input":"{}","RoleArn":"arn:aws:iam::012345678901:role/DummyRole"}}
step-functions-local_1  | 2019-07-31 00:49:58.487: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"TaskStateEntered","PreviousEventId":0,"StateEnteredEventDetails":{"Name":"HelloWorld","Input":"{}"}}
step-functions-local_1  | 2019-07-31 00:49:58.507: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"LambdaFunctionScheduled","PreviousEventId":2,"LambdaFunctionScheduledEventDetails":{"Resource":"arn:aws:lambda:us-east-1:123456789012:function:HelloWorld","Input":"{}"}}
step-functions-local_1  | 2019-07-31 00:49:58.508: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"LambdaFunctionStarted","PreviousEventId":3}
sam-local_1             | 2019-07-31 00:49:58 Invoking main (go1.x)
sam-local_1             |
sam-local_1             | Fetching lambci/lambda:go1.x Docker container image......
sam-local_1             | 2019-07-31 00:50:01 Mounting /Users/uchiko/project/step-functions-local-test/handlers/helloworld as /var/task:ro inside runtime container
sam-local_1             | START RequestId: bf7b22bb-7cc7-1703-a07b-f94434457538 Version: $LATEST
sam-local_1             | END RequestId: bf7b22bb-7cc7-1703-a07b-f94434457538
sam-local_1             | REPORT RequestId: bf7b22bb-7cc7-1703-a07b-f94434457538        Duration: 1.21 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 5 MB
sam-local_1             | 2019-07-31 00:50:03 172.19.0.3 - - [31/Jul/2019 00:50:03] "POST /2015-03-31/functions/HelloWorld/invocations HTTP/1.1" 200 -
step-functions-local_1  | 2019-07-31 00:50:03.689: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"LambdaFunctionSucceeded","PreviousEventId":4,"LambdaFunctionSucceededEventDetails":{"Output":""Hello World!""}}
step-functions-local_1  | 2019-07-31 00:50:03.691: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"TaskStateExited","PreviousEventId":5,"StateExitedEventDetails":{"Name":"HelloWorld","Output":""Hello World!""}}
step-functions-local_1  | 2019-07-31 00:50:03.695: arn:aws:states:ap-northeast-1:123456789012:execution:HelloWorld:9f49a299-36f7-4b22-95b1-fb7ae816b113 : {"Type":"ExecutionSucceeded","PreviousEventId":0,"ExecutionSucceededEventDetails":{"Output":""Hello World!""}}

まとめ

ローカルPC上で Step Functions の動作チェックを行える環境の構築方法をご紹介しました。
Step Functions のロジックは複雑になりがちなので、ぜひ取り入れてみてください。

なお、MMMのDockerコンテナへの取り組みを以下で紹介しています。ぜひ合わせてご覧ください。

Dockerコンテナ基盤(AWS Fargate/Amazon ECS)