ようへいの日々精進XP

よかろうもん

AWS Step Functions の Activity Worker を "郷" で実装してみた系

どうも、郷です。

モチベーション

  • むかーし、むかーし、1 台の EC2 cron で動かしている複数のバッチ処理があったとさ
  • 村人(むらびと)たちはバッチ処理を動かす時だけ EC2 を起動したいと考えたとさ
  • 村人たちの思いを汲んだ禿兵衛は EC2 は Lambda で起動してから、バッチを実行しようと考えたとさ
  • ところが、禿兵衛は EC2 が正常に起動したことを正確に判断したり、バッチ処理の重複起動やハンドリングに頭を悩ましておりましたさ

そして、ちゃんと前後の処理をハンドリングしつつ、処理の流れをコード化(可視化)出来ればなあと黄昏れておりましたら…山の向こうから「AWS Step Functions というツールがあるばってん、こんツールはくさ、Activity を使えばくさ、EC2 やオンプレのタスクも制御出来るちゃんねー」という声を聞くのであった…

ということで、EC2 で動かしているバッチ処理を Activity で制御することを想定して、Golang で Worker を実装して Acitivity の挙動についてチュートリアルしてみたメモでござる。

AWS Step Functions とは

aws.amazon.com

Step Functions の詳しい説明については、以下の資料がとても参考になりました。

www.slideshare.net

有難うございます!!

郷で Activity Worker を実装

Activity

docs.aws.amazon.com

Activities are an AWS Step Functions concept that refers to a task to be performed by a worker that can be hosted on EC2, ECS, mobile devices—basically anywhere.

超ざっくりだけど…

  • Activity とは EC2 や ECS 等、オンプレミスのサーバーでも動く Worker によって実行されるタスクのこと
  • AWS SDKAWS CLI で Worker を実装出来る
  • Worker は Activity の ARN を指定して起動すると Activity に対してポーリングを行う

Activity は AWS CLI では以下のように作成します。

$ aws \
  --profile oreno-profile --region ap-northeast-1 \
    stepfunctions create-activity \
      --name=OrenoActivity

以下のように出力されます。

{
    "creationDate": 1500076193.843,
    "activityArn": "arn:aws:states:ap-northeast-1:012345678912:activity:OrenoActivity"
}

State Machine には以下のように指定します。

{
  "Comment": "Golang Demo",
  "StartAt": "Godesu1",
  "States": {
    "Godesu1": {
      "Type": "Task",
      "Resource": "arn:aws:states:ap-northeast-1:012345678912:activity:OrenoActivity",
      "Next": "wait_using_seconds"
    }
  }
}

参考

こちらこちらを目一杯参考にさせて頂きました。有難うございます!!

コード

gist.github.com

こうやって使うことを想定

./taskRunner -arn=${Activity ARN} -command=${実行したいコマンド}

Demo

Activity Worker を動かす環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1421

$ go version
go version go1.8.3 darwin/amd64

$ go build taskRunner.go

State Machine: ビジュアルワークフロー

State Machine とは State をつなげたフローのことで、以下のようなフローで動かしてみます。

f:id:inokara:20170715075537p:plain

State Name State Type State で実行したいコマンド
Godesu1 Task demo01.sh
wait_using_seconds Wait 10 秒待ってから State: Godesu2 を実行
Godesu2 Task demo02.sh

State Machine: コード

{
  "Comment": "Golang Demo",
  "StartAt": "Godesu1",
  "States": {
    "Godesu1": {
      "Type": "Task",
      "Resource": "arn:aws:states:ap-northeast-1:123456789012:activity:OrenoFirstActivity",
      "Next": "wait_using_seconds"
    },
    "wait_using_seconds": {
      "Type": "Wait",
      "Seconds": 10,
      "Next": "Godesu2"
    },
    "Godesu2": {
      "Type": "Task",
      "Resource": "arn:aws:states:ap-northeast-1:123456789012:activity:OrenoSecondActivity",
      "End": true
    }
  }
}

State1 : Godesu1 で実行するコマンド

$ cat demo01.sh
#!/usr/bin/env bash

for i in $(seq 1 2 10);
do
  echo $i
done

Activity Worker を以下のように実行して待機させておきます。

./taskRunner -arn=arn:aws:states:ap-northeast-1:123456789012:activity:OrenoFirstActivity -command="./demo01.sh"

以下のように出力されます。

$ ./taskRunner -arn=arn:aws:states:ap-northeast-1:123456789012:activity:OrenoFirstActivity -command="./demo01.sh"
2017/07/15 08:00:49 taskRunner Started.

State2 : Godesu2 で実行するコマンド

$ cat demo02.sh
#!/usr/bin/env bash

for i in $(seq 2 2 10);
do
  echo $i
done

Activity Worker を以下のように実行して待機させておきます。

./taskRunner -arn=arn:aws:states:ap-northeast-1:123456789012:activity:OrenoSecondActivity -command="./demo02.sh"

以下のように出力されます。

$ ./taskRunner -arn=arn:aws:states:ap-northeast-1:123456789012:activity:OrenoSecondActivity -command="./demo02.sh"
2017/07/15 08:00:55 taskRunner Started.

State Machine の実行

以下のように AWS CLI で実行します。

$ aws \
  --profile oreno-profile --region ap-northeast-1 \
    stepfunctions start-execution \
      --state-machine-arn=arn:aws:states:ap-northeast-1:012345678912:stateMachine:DemoStateMachine-Go-3

以下のようにレスポンスが返ってきます。

{
    "startDate": 1500075053.349,
    "executionArn": "arn:aws:states:ap-northeast-1:012345678912:execution:DemoStateMachine-Go-3:859949c4-b3a4-4b7c-84ed-60f555e859e6"
}

そして、それぞれの Activity Worker では以下のように出力されています。

f:id:inokara:20170715083348p:plain

それぞれ別の Activity を利用しているので、順番も制御出来ている(Godesu1 → Godesu2 の順番)ことが判ります。

f:id:inokara:20170715092126p:plain

マネジメントコンソールでも上図のように State Machine の処理が正常に終了していることが判ります。

このフロー図を見ているだけでウキウキしますね。

ということで

Step Functions = Lambda という印象がありましたが、Activity を使うことで従来のバッチ処理等について変更を最小限に処理フロー(State Machine)に組み込めるのはとても嬉しいです。しかし、こちらでも言及されている通り、Activity は Worker を実装する必要があったり、Worker の運用管理が必要になる点は注意が必要です。

今回、禿兵衛の目の前にあるシチュエーションの場合、Activity Worker は EC2 が起動したタイミングで必ず起動することを保証する必要があったりしますが、それ自体も State の一つとして Lambda ファンクションに組み込んでしまうのもありかなと思ったりしています。

Step Functions とても面白いサービスで好きになっちゃいました。

2017 年 07 月 14 日(金)

ジョギング

  • 香椎浜 x 2 周
  • 8 時過ぎから走っているけど暑さでフラフラ

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

右手薬指の根本

が痛い。おそらく肘から来ている。

夕飯

買い物に行けなかったので外食。

担々麺が美味しいお店で担々麺と水餃子、ビール。

2017 年 07 月 13 日(木)

今日は

ジョギングも日課は無し。

久しぶりの JAWS-UG 福岡

jaws-ug-kyushu.doorkeeper.jp

乾杯から始まるゆるっとした勉強会。これがイイ。

香椎に付いたのが 23 時過ぎだったけど、森田さんとコンテナ街の十蔵で一杯。

2017 年 07 月 12 日(水)

寝苦しい夜だった

  • 何度も目覚めてエアコン付けたり

ジョギング

  • 香椎浜 x 2 周
  • 夜中に右足親指を捻ったみたいで痛いけど走った

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

夕飯

2017 年 07 月 09 日(日)

ジョギング

  • 香椎浜 x 2.5 周
  • 朝早い時間は雨が降っていたりで、結局は 12 時過ぎから走りはじめて暑さに敗北
  • 右太ももが辛い

日課

  • (腕立て x 30 + 腹筋 x 30) x 3

奥さん

  • 体調イマイチ
  • きっとストレスなんだろうなあ

夕飯

  • 楽しくご飯食べたいのでたこ焼きパーリー
  • 今回は上手く焼けたんではないかな

俺のチュートリアル 2017 夏 〜 go-datadog-api をちょっと使ってみた 〜

go-datadog-api

Golang で実装された Datadog API のラッパーです。

github.com

使ってみた

準備

$GOPATH 等は設定済みの状態です。

go get gopkg.in/zorkian/go-datadog-api.v2

monitors を取得するやつ

GetMonitors() 関数を利用すれば良いようです。

package main

import (
    "log"
    "gopkg.in/zorkian/go-datadog-api.v2"
)

func main() {
    client := datadog.NewClient("api key", "application key")

    mons, err := client.GetMonitors()
    if err != nil {
        log.Fatalf("fatal: %s\n", err)
    }

    for _, mon := range mons {
        log.Printf("Monitor %d: %s\n", mon.GetId(), mon.GetName())
    }
}

monitor の各要素は GetId()GetName() 等のメソッドを利用すれば取得出来るようです。

ところが

以下のようなエラーが出ちゃいます。

$ go run dd-monitors.go
2017/07/09 17:22:03 fatal: json: cannot unmarshal string into Go struct field Options.evaluation_delay of type int
exit status 1

取得した monitor の evaluation_delay というキーの値について、意図した値(型は int)が入っていない為、JSON の解析に失敗しているようです。この事象については、以下の issue でやりとりされていました。

github.com

evaluation_delay に値が入っていない場合の処理については、こちらのプルリクエストで Merge はされているので、近日中には Merge された内容でリリースされるんぢゃないかなーと期待しております。

ということで

monitor の取得については

もう少々待ちたいと思います。当然、evaluation_delay に値が設定されている場合や evaluation_delay がそもそも設定されていない monitor の場合には、以下のように monitor の各要素を取得することは出来ています。

$ cat dd-monitors.go
package main

import (
    "log"
    "gopkg.in/zorkian/go-datadog-api.v2"
)

func main() {
    client := datadog.NewClient("api key", "application key")

    mon, err := client.GetMonitor(12345678)
    if err != nil {
        log.Fatalf("fatal: %s\n", err)
    }
    log.Printf("Monitor %d: %s\n", mon.GetId(), mon.GetName())
}

$ go run dd-monitors.go
2017/07/09 17:51:12 Monitor 12345678: Apache Log Monitor 40x Error Test

そもそも evaluation_delay とは

Datadog API ドキュメントには以下のように書かれています。

evaluation_delay Time (in seconds) to delay evaluation, as a non-negative integer. For example, if the value is set to 300 (5min), the timeframe is set to last_5m and the time is 7:00, the monitor will evaluate data from 6:50 to 6:55. This is useful for AWS CloudWatch and other backfilled metrics to ensure the monitor will always have data during evaluation.

ざっくりと意訳すると…

  • メトリクスを遅延評価するオプション
  • 秒で設定する
  • CloudWatch 等のメトリクスで評価するモニタにおいてデータを常に確保したい場合に設定すると良い

CloudWatch のメトリクスを取得する際に NO DATA になってしまうことを防げるのかもしれないですが、使ったことが無いのでなんとも言えません。すいません。

GolangJSON パースのちょっと入門

encoding/json を使ったチュートリアル

GolangJSON パースについてはちゃんと書こうとすると 1 つの記事になってしまうんだろうけど、ひとまず encoding/json パッケージを使ったチュートリアルをしてみます。

参考

シンプルに纏まっていて解りやすかったです。有難うございましたmm

基本

  • JSON のデータ構造に合わせて構造体を定義する
  • Unmarshal 関数を使用してパースする
  • JSON を生成する場合には marshal 関数を利用する

そもそも構造体って?

1つもしくは複数の値をまとめて格納できる型。それぞれのメンバ(フィールド)は型が異なっていてもよい点が配列と異なる。

なるほど。(今更感)

以下のサンプルで言うと、

type SelfIntroduction struct {
    Name string              `json:"name"`
    Options []Options        `json:"options"`
}

これが構造体。

サンプル

参考サイトのサンプルスクリプトを拝借させて頂いて自己紹介スクリプトを self-introduction.go として保存しておきます。

package main

import (
    "encoding/json"
    "fmt"
)

type SelfIntroduction struct {
    Name string              `json:"name"`
    Options []Options        `json:"options"`
}

type Options struct {
    Name string              `json:"name"`
    Value json.Number        `json:"value"`
}

func main() {
    json := `
{
  "name": "ハゲ",
  "options": [
    {
      "name": "髪",
      "value": "つるっぱげ"
    },
    {
      "name": "age",
      "value": 40
    }
  ]
}
`
    jsonBytes := ([]byte)(json)
    data := new(SelfIntroduction)

    if err := json.Unmarshal(jsonBytes, data); err != nil {
        fmt.Println("JSON Unmarshal error:", err)
        return
    }

    fmt.Println("Name: " + data.Name)
    for _, option := range data.Options {
        fmt.Printf("%s: %s\n", option.Name, option.Value)
    }
}

実行してみます。

$ go run self-introduction.go
Name: ハゲ
髪: つるっぱげ
age: 40

おお、ちゃんと解析出来ています。

まだまだ GolangJSON を扱う上で色々と学ぶ必要が有りそうなので、追々チュートリアルしていきます。

ということで

go-datadog-api から始まって、evaluation_delay を経由して GolangJSON パースまで薄く触れてみました。引き続き、チュートリアルな夏は続きます。