Ponz Dev Log

ゆるくてマイペースな開発日記

Node.jsアプリでもAngularみたいにDIしながらアプリを作る

AngularではDependency Injection(DI)で自分で定義したServiceを各コンポーネントで使えるようにできます。Javaの世界でもSpring Frameworkを筆頭に、インターフェイスに対してどのような実装を紐づけるかをDIで実現しています。

ただ、、、サーバーサイドやCLIでNode.jsのアプリを作るとなると途端にDIをすることって少ないですよね。TypeScriptで型やインターフェイスを手に入れたのにClassをいちいちnewしたりimportするのは辛い...とても辛い!!

この記事ではInversifyJSを使ってサーバーサイドのNode.jsのアプリでDIを行います。

概要

実現したいこと

  • Node.jsアプリでDIを使ってInterfaceと実装クラスを疎結合にする。
  • 生のJSではなく、TypeScriptを使ってClassとInterfaceの恩恵を受ける。
  • 題材はCLIアプリケーションとする。
  • InterfaceへのDIだけでなく、実装クラスのDIについてもやってみる。

題材

最近お仕事で自分でやったことを例とします。題材とシチュエーションは以下の通りです。ちょっとした自動化アプリです。

  • Java(Spring Framework)のWebAPIを作っています。
  • 入力チェックのテストコードを全項目網羅して正しく入力チェックエラーを返すことを確認したい。
  • 手で書いて目Grepで過不足がないか確認するのは効率がよくない&何かしら見落とすリスクがあるので自動生成する。
  • この自動生成をCLIインターフェイスとしたアプリケーションとして実装する。

InversifyJS

今回のように、JavaScript/TypeScriptでDIを実現するのに不可欠なライブラリです。

github.com

正体はTypeScriptで作られたIoCコンテナ(=DIコンテナ)のライブラリ。TypeScriptで使うことが推奨されています。

また、最近のTypeScriptの流れなのか、アノテーションを使っています。@Inject@InjectableといったJavaでもお馴染みのやつを使えます。 詳細はGitHubのREADME.mdを読んでいただければと。

実践

完成形

最初に最終形をお見せします。 ファイル・ディレクトリは以下の通りになります。

CLIのエントリーポイントがsrc/generator.tsです。 各ディレクトリはクリーンアーキテクチャもどきで構成しています。各ディレクトリと格納クラスの役割は以下の通りです。

Interfaceを定義する

まずはController、UseCase、RepositoryのInterfaceを定義します。全部書いているとキリがないので一部だけ。

import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse } from "./CreateScriptResponse";

export interface ICreateScriptUseCase {
  generate(request: CreateScriptRequest): CreateScriptResponse;
}

実装は何も入れていません。どのようにこのgenerate関数が実現されるかは実装クラスに委ねられます。そして使用する側は実装クラスではなく、Interfaceにのみ依存します。

実装クラスを書く

Interfaceを実装します。前述した通り、JavaのInjectアノテーションが出てきますね。 Interfaceに実装クラスを注入する対象は @Inject() 、他のクラスで注入させたい対象は @Injectable() を使用すればあとはただのアプリです。

import { ICreateScriptUseCase } from "./UseCaseInterface";
import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse, CreateScriptResponseDetail } from "./CreateScriptResponse";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { injectable, inject } from "inversify";
import { TYPE } from "../common/Types";

/**
 * スクリプト生成ユースケースの実装クラス
 */
@injectable()
export class CreateScriptInteractor implements ICreateScriptUseCase {
  private readonly _listBlg: CaseScriptListBlg;
  private readonly _scriptBlg: CaseScriptBlg;

  constructor(
    @inject(TYPE.CaseScriptListBlg) listBlg: CaseScriptListBlg,
    @inject(TYPE.CaseScriptBlg) scriptBlg: CaseScriptBlg
  ) {
    this._listBlg = listBlg;
    this._scriptBlg = scriptBlg;
  }

  /**
   * スクリプト生成を実行する.
   */
  generate(request: CreateScriptRequest): CreateScriptResponse {
    // テストケースのリストを生成
    const caseList = this._listBlg.createCaseList(request);
    // ケース名を元にテストケーススクリプトを生成
    this._scriptBlg.generate(request, caseList);

    // レスポンス生成
    return this._createResponse(request.getOutputFilePath(), caseList.length);
  }

  private _createResponse(path: string, num: number): CreateScriptResponse {
    let detail = new CreateScriptResponseDetail();
    detail.setCaseName(path);
    detail.setCaseNum(num);
    return new CreateScriptResponse("OK.", [detail]);
  }
}

実装クラスと紐づける

最後に、inversify.config.ts にInterfaceと実装クラスの紐付けを定義します。ここで紐付けはこのクラスでしか行いません。 to()で実装クラスを紐づけるのですが、toSelf()なる関数があるらしい。(使い方は分からない...)

import "reflect-metadata";
import { Container } from "inversify";
import { TYPE } from "./Types";
import { IBaseController } from "../controller/IBaseController";
import { CreateScriptController } from "../controller/CreateScriptController";
import { ICreateScriptUseCase } from "../usecase/UseCaseInterface";
import { CreateScriptInteractor } from "../usecase/CreateScriptInteractor";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { ICaseListRepository, IScriptRepository } from "../repository/RepositoryInterface";
import { CaseListRepository } from "../repository/CaseListRepository";
import { ScriptRepository } from "../repository/ScriptRepository";

const container = new Container();

// Controller
container
  .bind<IBaseController>(TYPE.CreateScriptCtrl)
  .to(CreateScriptController);

// UseCase
container
  .bind<ICreateScriptUseCase>(TYPE.CreateScriptUseCase)
  .to(CreateScriptInteractor);

// Business Logic
container.bind<CaseScriptListBlg>(TYPE.CaseScriptListBlg).to(CaseScriptListBlg);
container.bind<CaseScriptBlg>(TYPE.CaseScriptBlg).to(CaseScriptBlg);

// Repository
container.bind<ICaseListRepository>(TYPE.CaseListRepo).to(CaseListRepository);
container.bind<IScriptRepository>(TYPE.ScriptRepo).to(ScriptRepository);

export { container };

そして使いたいクラスをDIコンテナから取り出せば完成!

import { container } from "./common/inversify.config";
import { IBaseController } from "./controller/IBaseController";
import { TYPE } from "./common/Types";
const controller = container.get<IBaseController>(TYPE.CreateScriptCtrl); // <-- Here!!

まとめ

InversifyJSを使うと、Node.jsのアプリでもDIやるのは結構簡単ですね。 今回はCLIアプリで一からコードを書きましたが、Webアプリでフレームワークを使うときも同じように書けるはず。

Serverless FrameworkでKotlin & LambdaのサーバーレスAPIを作る

朝活.Kotlinで作ったServerless Framework × Kotlin × LambdaのサーバーレスAPIまとめです。

justincase.connpass.com

serverless.com

概要

  • 題材

    • 松屋のメニューの写真をひたすら貼るSlack Bot. (ちょっとしたジョークアプリです)
  • 上記のSlackBotのバックエンドをLambda関数で実装する.

    • API管理: API Gateway
    • ロジック: AWS Lambda <--ここをKotlinで書いた
  • CI/CDファーストを目標に、なるべくも手作業を減らしてビルド&デプロイを簡単にする.

やったこと

まずアプリケーションを作成するときに悩むのが、ボイラープレートコード(フレームワーク等で必要になる典型的なコード)の扱い。 ビジネスロジックを書くことに集中できるのがサーバーレスアプリケーションの強みの1つなので、LambdaとAPI Gatewayの接続部分や、エントリーポイントのHandlerクラスはなるべく書きたくないところ。

実はServerless Frameworkのテンプレートから雛形は自動生成可能です(プラットフォームによってはない)。 sls create --helpを打って調べる限り, 使えるテンプレートは3つありますね。KotlinでJS使うテンプレートなんてあるのか...

それでは雛形をコマンドラインから作成。事前にnpm i -g serverlessでserverlessコマンドは入れておきましょう。今回は"aws-kotlin-jvm-maven"を使います。

serverless create \
    --template aws-kotlin-jvm-maven \
    --path matsuya-finder \
    --name matsuya-finder

5秒くらいでテンプレートは作成されます。指定した--path以下は以下のようなディレクトリ構成になっています。

.
 |- pom.xml
 |- serverless.yml
 |- src
    |- main
       |- kotlin
          |- com
             |- serverless
                |- ApiGatewayResponse.kt
                |- Handler.kt
                |- Response.kt
  • ApiGatewayResponse.kt ... Lambda --> API Gatewayにレスポンスを返す時のデータの格納クラス。弄らずにそのまま使う
  • Handler.kt ... アプリケーションのエントリーポイント。ビジネスロジックはここにInjectする。
  • Response.kt ... アプリケーションのレスポンス。data classになっている。
  • serverless.yml ... Serverless Frameworkに管理されたアプリケーションのデプロイ定義。裏ではCloudFormationを使っている。

あとは業務ロジックのコードを好きに書きます!

最終的なコードは以下の図のような構成になりました。 アプリのコードが重くなければ、KtorやJavaliなどのWeb Application Frameworkを使うよりもスッキリしてますね!

f:id:accelerk:20190323235930p:plain
ディレクトリ構造 最終形

また、serverless.ymlは以下の構成になっています。このファイルが置かれている場所で sls deploy を発行すればデプロイ完了です。

service: chom-matsuya-finder
  
# Base Definition
provider:
  name: aws
  runtime: java8
  stage: dev
  region: ap-northeast-1

# Packaging information
package:
  artifact: target/matsuya-finder-dev.jar

# Individual Function Definitions
functions:
  main:
    handler: net.ponzmild.Handler
    name: matsuya-finder
    description: Fetch today's Matsuya menu.
    events:
      - http:
          method: post
          path: matsuya

Tips

  • Serverless Frameworkのコマンドを打つときは, 権限のあるプロファイルを使おう

    • IAMロールをつけたのになぜかUnauthorizedとなってしまうことありますが、だいたいはIAMロールをつけたユーザーでCLIを叩いてないことが原因です。
    • (使用例) sls deploy --aws-profile your-serverless-agent
  • そのままデプロイすると統合リクエストのタイプが"Lambda Proxy"になる

    • このタイプはリクエストのコンテントタイプを細かく指定しづらいです。
    • また、雛形として自動生成されるApiGatewayResponse.ktはLambda Proxyタイプ用のレスポンス定義です!これ以外のタイプは別途レスポンスのクラスを用意しましょう。
    • serverless.ymlintegration:lambdaを指定するか、Handlerクラス内でパースのどちらかで解決しなければなりません。
    • 今回は後者にしています(以下のコード)
class Handler : RequestHandler<Map<String, Any>, ApiGatewayResponse> {
    override fun handleRequest(input: Map<String, Any>, context: Context): ApiGatewayResponse {
        // API GatewayのイベントのJSONからリクエストボディのみ取り出す
        val inputJsonString = input.get("body") as String

        // 全て文字列なので, Mapに詰め替える
        var inputBodyMap: HashMap<String, String> = hashMapOf()
        val kvStrings = inputJsonString.split("&")
        kvStrings.forEach {
            val kv = it.split("=")
            inputBodyMap.put(kv[0], kv[1]) // 詰め替え
        }

        // And more...
    }
}

Kotlinも結構簡単にサーバーレスなAPIを作れるんですね。他にもS3のイベントやCloudWatch Eventsにも対応できるのでできる幅も広そう。 また、Androidユーザーはフロントエンドと同じ言語を使えるからいいですね。iOSユーザーのためにSwiftでもできるのかな?

以上。

CloudWatch EventsからAWS Batchを叩くCfnテンプレートが書けないからCLI使う

CloudFormationテンプレートのYAMLファイルからAWS Batchを叩くCloudWatch Eventsのルール・トリガーを作ろうとしたらダメだったから代案としてCLIで作った話です。

CloudWatch Eventsのルールに対して、どのサービスをどのようなイベントに反応して/時間間隔で動かすのかを "ターゲット" として定義します。 折角なので他の環境でも使い回せるようにCloudFormationを使ってAWS Batchを起動するCloudWatch Eventsのターゲットを作れないかとドキュメントをみてる見ると、、、それらしい記述がない。

docs.aws.amazon.com

AWS APIとして定義はあるのかと見てみると、こっちにはTargetオブジェクトにBatchParametersというプロパティがあるから実は裏仕様としてCloudFormationを叩けるのでは?

docs.aws.amazon.com

以下のようにCfnテンプレートを作成して実行してみます。結論としては作成できませんでした。

AWSTemplateFormatVersion: "2010-09-09"
Description: AWS Batch Trigger template
Resources:
  PzBatchScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Triggers AWS Batch Job"
      Name: "pz-aws-batch-trigger"
      ScheduleExpression: "cron(0 0/1 * * ? *)"
      State: "ENABLED"
      Targets:
        - Arn: !Sub "arn:aws:batch:${AWS::Region}:${AWS::AccountId}:job-queue/logger-queue"
          Id: "Pz-Batch-Target-test"
          RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/AWS_Events_Invoke_Batch_Job_Queue_1960059509"
          BatchParameters:
            JobDefinition: !Sub arn:aws:batch:${AWS::Region}:${AWS::AccountId}:job-definition/scala-args-logger:2
            JobName: "test-batch-job"
          Input: "\"{\\\"Parameters\\\":{\\\"jobType\\\":\\\"test\\\",\\\"languageCode\\\",\\\"ja\\\",\\\"app\\\":\\\"APP\\\"]}}\""

以下結果のスクリーンショットです。

f:id:accelerk:20190224191323p:plain
BatchParametersはサポートされていない

Encountered unsupported property BatchParameters ...なるほど🤔 無理なのか。 ここに時間をかけてる場合じゃない。リリースと同時に作ればいいと割り切ってCLIから作ることにします。

docs.aws.amazon.com

# ルールの作成
aws events put-rule --name "test-batch-rule" --schedule-expression "cron(0 0/1 * * ? *)"

# ターゲットの作成
aws events put-targets \
    --rule "test-batch-rule" \
    --target "Id"="batch-executor-rule-id",\
"RoleArn"="arn:aws:iam::${AWS:Account}:role/AWS_Events_Invoke_Batch_Job_Queue_1960059509",\
"Arn"="arn:aws:batch:${AWS:Region}:${AWS:Account}:job-queue/logger-queue",\
"BatchParameters"="{"JobDefinition"="arn:aws:batch:ap-northeast-1:${AWS:Account}:job-definition/scala-args-logger:2","JobName"="test-batch-name"}",\
"Input"="\"{\\\"Parameters\\\":{\\\"jobType\\\":\\\"test\\\",\\\"languageCode\\\",\\\"ja\\\",\\\"app\\\":\\\"APP\\\"]}}\""

これでなんとかCloudWatch EventsからAWS Batchを起動するルールを作成できました。今後CloudFormation経由で実行されることを期待します。。。

他にも罠があり、以下2つを見つけました。ご参考までに。

  • ジョブ定義のRefの個数とCWEのRefの数が一致しないとそもそもイベントがトリガーされなかった。。。
  • Refの後のキー名は先頭小文字にしないとトリガーされない。

以上。

JAWS DAYS 2019 参加レポート

日本のAWSユーザーグループであるJAWSが主催の "JAWS DAYS 2019" に行ってきました。お昼の時点で参加人数1,500人を超えていたこともあって、200人収容できるセッションでも立ち見が出るほどの盛況っぷりなのが印象的。

忘れないように会場で実際に聞いてきたセッションについて簡単にまとめておきます。


[Serverless] サーバレスで動かすトークン発行プラットフォーム

jawsdays2019.jaws-ug.jp

実際にトークン発行プラットフォームとして使っているEtheriumすごい的な話ではなく、如何に新しい技術を使って日々新しいものを作るかの過程の話だった。

新しいものを使うことはリスクになる上に、人・時間・知識がない中で最小限のプロダクトを作ってしまおう。サーバーレスは良いソリューションになるんだってのがこのセッションでの学び。

[Serverless] AWS Serverlessを活用したサービス監視

slide.seike460.com

サーバーレスを使いつつ、監視コンポーネントAWSのサービスの組み合わせをラップしたOSSを作った話。

最近本屋に平積みされているオライリーの "入門 監視" の内容を踏まえて、監視に必要な要素を明確にしながら自分でOSSを作ってしまおうというところがすごい。

AWSとのサービス統合でポイントになっていたのが、API Gatewayから直接Lambdaを叩かずSQSを間に挟んでいること。監視エージェントとストレージを疎結合にするための戦略なのは勉強になった。それにログやメトリクスとか膨大な量のデータを一気にAPI Gatewayで受けたらすぐにスロットリングしちゃうし、CloudWatch LogsにLambdaのサブスクリプション貼る時と同じことになるもんね。

API GatewayがLambda以外のサービスに直接リクエストできるのも初めて知った。。。

[EC2/DB] RDBリファクタリングと異種間DB移行の戦い – Amazon DMSを使った止めずにリファクタリングする手法

soudai.hatenablog.com

そーだいさん(id: Soudai)のRDBリファクタリングのセッション。

DBの寿命はアプリよりも長い。アプリが動いて数年経つと、謎テーブルの存在ややカラムに意図しない値、制約が貼ってない等々技術的な負債が貯まってくる。そんな状況でRDBリファクタリングされた時(現在進行形らしいですが)の挑戦記録を話されていました。

2ステップ大きく分けてされていたようで、どちらのステップも興味深い。

  1. 現状Aurora MySQL5.6で運用しているテーブルをリファクタリングするために、トリガー(SQLが発行されたイベントに反応して実行される処理)を使う。
  2. MySQL5.6だと1テーブル1イベントに対して1トリガーしか貼れないので、この制約がないPostgreSQL for RDSにマイグレーションする。

MySQLからPostgreSQLに移行するときに使用するのがAWS Database Migration Service (DMS)です。 なので、現行Aurora MySQL --(DMS)--> 旧スキーマPostgreSQL --(トリガー)--> 新スキーマPostgreSQL の流れでマイグレーションしたと。

参照はAPI経由でいきなり新しいスキーマPostgreSQLから実施することでModel単位に移行させるといったことは今後の業務で使えそうなTipsです。

さらに興味深かったのが、実運用のところ。平気で20~30秒(?)マイグレーションが遅延する、更新が激しいとDMSが単一障害点になってしまうといった使ってわかったデメリットなんだとか。なるほどな。

最後にそーだいさんが言っていたように、RDBリファクタリングは「覚悟」が必要とのこと。肝に命じておこう。

[Lunch Session] AWSからメール送るならSendGrid一択ですよね

jawsdays2019.jaws-ug.jp

自宅から歩ける範囲に平日にSendGridの勉強会をやっているのは知っていたけど、なかなか行けなかたので参加。

ランチタイムの15分セッション。SendGridそのもののアピールがほとんどでしたが、SESとSendGridの比較が乗ってて確実にメールを届けるなら良い選択肢だと認識。

[Lunch Session] クラウド時代のモニタリングといえばDatadogだよね

jawsdays2019.jaws-ug.jp

AWSだけでなく、一元的にモニタリング・メトリクス・ログ・APM収集できるのを再確認。2018のre:Inventで一番デカイブースを構えていたとのことだったので、ちょっと興味が湧いたな。

可愛いDatadogのステッカー貰えたし、使ってみようかしら。Mackerel、ElasticSearch/Kibanaと比較して要検討。

[DevEnv] 至高の CI/CD パイプラインを実現する5つの約束

speakerdeck.com

内容は上のスライドにまとまっているけど、肝心だと思ったとこだけ抜粋 + 当日のメモ。

  • パイプライン ファースト
    • アプリよりも先にまずはCI/CDパイプラインを作ることを強調。
    • 一発目のデプロイからアプリは雛形でいいからパイプラインでデプロイを書けることが、ROIの高い投資だと表現していた。
    • パイプラインも最初から整えなくても、手元のデプロイスクリプトをまとめるだけでひとまずOK

自動化されたパイプラインを維持

  • 自動化できない変更は避ける
  • パイプラインにアプリ都合の複雑なオペレーションを押し込まない、アプリで吸収しちゃう

柔軟なパイプラインの維持

  • 常にシンプルにキープ
  • パイプラインをコード化する

パイプラインのUXの継続的改善

  • CI/CDパイプラインを開発メンバーに提供するサービスとして考える
  • 何が実行されて、なぜデプロイが失敗したのか分かるよう
  • 時間短縮を図ろう
  • 作り込みすぎて安定性を失うことを避ける

パイプラインを唯一のデプロイ方法にする

  • "とりあえず手作業" は楽だが禁忌 (ビジネスが危機的な場合のみ許す)
  • パイプラインが有名無実の代物になるから

[Supporter Session] 三題噺「F-Secure 基幹システムは Serverless !あと IoTセキュリティとAWSセキュリティ」

jawsdays2019.jaws-ug.jp

F-Secureというセキュリティソフト・コンサルティング会社の営業さんがスピーカーのセッション。なんとLT3本立てという豪華な構成に加えて、話がめちゃくちゃうまかった。

会社の事業紹介しつつAWSを使いながらセキュリティサービスをどう提供しているかというお堅い感じのサービスにも関わらず新鮮なトークでした。

ユーザーがアンチウイルスソフトの利用登録とアクティベーションを可能な限り早く行うため、かつユーザーを逃さないようにサーバーレスにしたとのこと。AWSユーザー企業ケーススタディの1つにも乗っていたのでアーキテクチャ図を見てみましたが、Kinesis とLambdaの組み合わせで実現しているのが面白い。

aws.amazon.com

[Others] Infrastructure as Codeに疲れたので、僕たちが本来やりたかったことを整理する

speakerdeck.com

個人的に一番登壇者の苦労に共感できたセッション。(会場の人の反応を見る限り大多数が同じような悩み・つらみを抱えていそう)

リソース管理のためにTerraFormテンプレートを書いてたけど、本当はそこにばかり時間をかけている場合じゃなくて価値あるタスクをするべきなのに辛さばかり目立つという話。

ちょうど前日・前々日とCloudFormationからCloudWatch Eventsのルールを作れないとこでハマってたので、ただただ共感でした。

ここでのセッションの要点としては、ROIを考えてInfrastructure as Codeで管理すべきこととすべきではない(割りに合わない)ことを考えましょうという点でしょう。

ROIが低いならWebコンソールやCLIからでええやんとのことでしたが、私が考えている答えとしてはほぼ同じです。ただ、本番環境でも使うサービスなら失敗してもロールバックできるように出来る限りCLIがベターかなとは思います。

[CLI] AWS CLIではじめるコマンドラインライフ 〜 正しい「運用自動化」への第一歩

jawsdays2019.jaws-ug.jp

えーまじ〜GUI使うのは中学生までだよね〜をかなり本気で考えている(ような) JAWS CLI支部の方のセッション。

AWSを真に理解するとはAWS APIの働きを理解することである。AWS CLIならAWS APIのほとんどを操作できるのだから、CLIで操作することこそAWSを真に理解するための道であると説いていたのが印象的。

確かに本番環境でGUIをいじるのは流石にないけど、CloudFormationで時間かけて無駄に長いスタックのYAML書くよりもCLI書いて直接操作するのが直感的だ。

  1. 何はともあれ、公式リファレンスを読もう。
  2. CLI補完機能を使おう! aws_completer をセットするだけ。
  3. --queryオプションを使おう! 出力制御がCLIだけで完結できるから、jqよりもいいぞ! docs.aws.amazon.com

JAWS CLI支部の勉強会開催場所は会社のすぐそばだし行こうかしら。。。


上記以外にもEKSやDevSecOps/テスト自動化のセッションと懇親会の時のLTも聞きたかったけど、時間が被って断念。。。SlideShareやSpeakersDeck、Twitterに資料が上がることを期待します。

セッション以外も嬉しかったポイントがあって、広い会場でも確実にスピーカーの声が聞こえるように一人一人スピーカーホンが配られてたのはGJだった。隣のセッションとはパーティション一枚でしか区切られてなかったから、結構声が隣から漏れてきてたし。

あとはお弁当豪華でしたね。さすが "満願全席" がテーマなだけありました。(写真は取り忘れた。。。無念)


以上。

AWS Batchに渡したCommandがプログラムの引数に渡らない時はDockerfileを見よう

お仕事でも使ってるAWS BatchでCommandに渡した値がプログラム(Javaだとmain関数のargs部分)に渡ってこなくてハマった時の対象方法メモ。 AWSの公式ドキュメントが分かりずらいので、Dockerのドキュメントも参照しながら解決しました。

Commandとコマンドライン引数の関係

そもそもの話。AWS Batchのコンテナプロパティ"Command"はDockerfileに記述されたどの部分にマッピングされるのだろうか? AWS Batchのジョブ定義のパラメータのセクションを見ると以下のように書いてあります。

このパラメータは、Docker Remote API の コンテナを作成する セクションの Cmd にマッピングし、 COMMAND パラメータを docker run にマッピングします。Docker CMD パラメーターの詳細については、https://docs.docker.com/engine/reference/builder/#cmd を参照してください。

🤔???

分かりづらいので噛み砕いて説明します。。。AWS Batchに渡されたジョブ定義のCommandはdocker runの後に続くパラメータとして渡されます。Dockerfile内での記述でどうも引数の渡され方の挙動が変わる らしく、以下のような感じになります。

  • CMDでコマンドを指定した --> ジョブ定義で指定したCommandの1番目が実行可能バイナリ、2番目以降が引数となる
  • ENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる
  • CMDとENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる

3番目のパターンに関してはDockerのドキュメントの方に以下のようなコメントがありました。

If the image also specifies an ENTRYPOINT then the CMD or COMMAND get appended as arguments to the ENTRYPOINT

例えば下のようなDockerfileに対してジョブ定義のコマンドで "hoge fuga"と渡した場合は java -jar /app/application.jar hoge fuga となります。 混乱しないようにするためには、Dockerfile内はENTRYPOINTのみ指定して、ジョブ定義は引数扱いにした方が良いですね。

FROM openjdk:8-alpine
COPY target/scala-2.12/args-logger-1.0.0.jar /app/application.jar
ENTRYPOINT ["java", "-jar", "/app/application.jar"]

今回引っかかったところはDockerfileでCMDを指定したつもりでコマンドを書いていたら実はENTRYPOINTだったから引数が欠けてしまったというオチでした。。。

CommandとParameterの関係

ついでに、ジョブ定義にはCommandとParameterというパラメータがあります。関係性としては、Command内に変数を埋め込んだ場合に実際の値としてParameterの値を入れることになります。値を外から(CloudWatch Eventsで指定した値とか)Parameterとして注入することでアプリ内で動きを変えるといった使い方ができるようです。

DockerfileとCommandとParameterを設定して動かす

実際に動かして確かめます。渡された引数をログ出力するだけのScalaのアプリを使います。 commandパラメータはジョブ定義で以下のように ["Ref::jobType", "Ref::language"] として変数化しました。

f:id:accelerk:20190218002513p:plain

実際に渡したパラメータは下図の通り。

f:id:accelerk:20190218002550p:plain

実際に動かしてみると...

f:id:accelerk:20190218002637p:plain

確かにcommandパラメータにParameterを埋め込んだ値で引数が全て渡っていますね。

GCP Professional Data Engineer認定取得しました

GCP Professional Data Engineer認定試験に合格してきました! 認定取得にあたり、取得するまでのポイントや試験のことを書いておきます。

cloud.google.com

事前に準備したこと

  • Coursera   - 各サービスのコンセプトやらユースケースを細かく詳しくレクチャーしてくれる。
    • 分量が多いので、概要を掴むくらいを期待するなら重すぎるかも。   - 間に手を動かすパートと小テストがあったから少しは自信がつく

www.coursera.org

  • qwiklabs   - 手を動かす用. アカウントは制限時間ありで用意してくれる

押さえるべきサービス

データに関するサービスだけでなく、セキュリティやログ出力設定まで聞かれます(システムを作る/使う上では基本だもんね)。

ざっと列挙するだけでも以下のサービスを一通り抑える必要がありました。 もちろん、現時点で使っているサービスがあるならそれを掘り下げると良い。自分みたいにほとんど知らないなら、チュートリアルの一つ目だけやって触るだけでもしたほうがいいです。

特にBigQuery, IAM, ストレージ/データベースサービスの使い分け, 機械学習の方法(特徴量エンジニアリングとか)は押さえておく必要がありました。

  • IAM (Identity and Access Management)
    • 権限, 職掌分離を行うサービス。他のクラウドベンダーのIAMとほぼ同じ機能。
  • BigQuery
    • いわゆるデータウェアハウス, 引くレベルで高速にデータをSQLで探索できる。
    • テーブルやビューの作成を作成してデータを保存できるけど、データの検索に重きを置いている印象。
  • Google Cloud Storage
    • ストレージサービス。アクセス頻度やオブジェクトを置くリージョンによって最適なストレージタイプが異なる。(ここはAmazon S3と同じかも)
    • ストレージタイプはリージョンマルチリージョン/ 単一リージョン / 低頻度アクセス用 / アーカイブ用の4タイプ。
  • Cloud Pub/Sub
    • Publish-Subscribe型のメッセージングサービス。
    • プッシュ通知的なpush型, キュー的なpull型の2タイプあり。
  • Cloud Dataflow
    • データ処理のパイプライン(複数のデータ処理を順序づけて/ 並列にして実行する1セット)を構築するサービス。
    • ストリーミングもできるし、バッチ処理もできる。
    • GCPのサービス間(Pub/Sub -> BQ, GCS(Avroファイル) -> BigTable)の転送するだけならば事前に用意されたテンプレートを使えばOK!
    • カスタムの処理を組み込む場合は Apache Beam SDK(Java or Python)で書いてデプロイする。
  • Cloud Datalab
    • Jupyter Notebookのホスティングサービス。
    • Git連携(ungit)だけでなくBigQuery, ML EngineといったGCPのサービスとの連携もできる。
  • Cloud Dataproc
    • Spark / Hadoopホスティングサービス。
    • 一度クラスターを立てたらずっと起動したままではなく、一定時間未使用のまま経過したらクラスターをシャットダウンできるのでお財布に優しい!
    • 既存のSpark / Hadoopで組んだサービスをクラウドに移行するならDataprocに移すのが良いとされる。
  • Cloud Dataprep
    • GUIベースのデータ変換、データクリーニングサービス。
    • BigQueryのテーブルやGCSにおいたCSV等のデータの中身の欠損値を除去したり、テーブルJOINしたりできる。
    • 操作はレシピという形で実行されるけど、実態はDataflowのジョブのようです。
  • Cloud ML Engine
  • Vision API
    • 学習済みの画像認識機械学習モデルをAPI経由で呼び出せるサービス。
    • Amazon RekognitionやWatson Visual Recognitionに相当
    • 認定試験も問題を見る限り、自分でTensorflowでモデル作るよりも既に学習済みのモデルあるならそっち使った方が早いよという意図で扱っているようです。
  • Stackdriver Logging / Monitoring
    • 名前の通り、ログやモニタリングのサービス。
    • ほとんどのサービスでは組み込みでStackdriver Loggingでログを見れるようにはなってる。
  • Cloud Bigtable
    • ワイドカラム型のスキーマを持ったNoSQLデータベース。
    • 読み込み、書き込みのレイテンシが早い。
    • IoT機器からのデータの保存先で使ったり、時系列データをストリーミングして書き込んだ後に分析用にBQへデータを流し込むといったユースケースが考えられる。
  • Cloud SQL / Cloud Spanner
  • Cloud Composer
    • Apache Airflowベースの構成管理, ワークロード管理ツール
    • AWSのCloudFormation + Step Functionを合わせたような印象だけど、複数のサービスを組み合わせたジョブスケジューラーとして最適かも。
  • Cloud Storage Transfer Service
    • 特定のデータソースに格納されたデータをGCSに転送するサービス。
    • データソースには、HTTP/HTTPSのエンドポイントをもつソース / Amazon S3(!?) / GCSが使える!

試験受験したとき

大きく分けて以下2種類。出題数的には前者8割、後者2割でした。ケーススタディは試験の公式サイトに載っているやつそのままです。

  • 特定のユースケースに沿って、どのサービスをどのように使うか
  • ケーススタディに沿って各課題に対するソリューションとしてどのサービスを使うか

時間は2時間ありましたが、1.5時間で一通り解き終わりました。全部見直しするには少し時間が足りなかったです。。。 認定試験のサイトだとケーススタディを猛烈プッシュしていましたが、実際には各サービスについての一問一答形式が多くて焦ります。AWSの試験を受けたことがある方には同じ形式の出題だったと言えば分かりますかね笑

また、ケーススタディはビジネス要件(履歴データを集約して予測分析したい、本番環境を柔軟にスケールさせたい)、技術要件(出来るだけマネージドサービスを使う、Hadoopワークロードはそのまま移行したい etc...)が出題として問われたり、時にはヒントになっていました。 ただ、受験した会場特有なのか分かりませんが、左に問題、右にケーススタディの構成や要件が表示という2窓構成でした。めっちゃ見づらかった。。。

認定試験を通しての学び

Kubernatesを触るために最初に使い始めたGCPでしたが、今回の認定試験の学習を通してデータ分析や分析基盤の構築について学べました。 特に以下のことについては今まで触ったことのあるクラウド(IBM Cloud, AWS)以上に理解できたと自信を持てます。

  • 分析用途のデータ処理パイプライン構築にはどのようなサービスを組み合わせれば良いか。
  • 機械学習サービスは他のクラウドベンダーのAIとは何が違うのか。どのように使うのか。事前に使えるモデルは何か。
  • 分析に使うデータウェアハウスのBigQueryの特徴。
  • ストレージの使い分け。

個人的にはデータをSQLで分析するBigQueryやDataflow, Dataprepと他のクラウドで処理したデータを連携させたりできないかとアイデアを膨らませるいいきかっけになったかと。 例えばAWSGCPの2つを使ってアプリのログのデータ分析をマルチクラウドできるんじゃないか? (冗長かもしれないけど)

  • AWS ECSで動くアプリケーションのログをCloudWatch Logsに集約
  • CloudWatch Logsのログ生成をAmazon SNSのトピックに流し込む
  • SNSをサブスクライブするLambda経由Kinesis Firehoseにデータを入れる
  • Kinesis FirehoseからAWS S3にログをストリーミングで直接保存
  • Cloud Storage Transfer ServiceでS3からGCSにログデータを転送
  • DataflowでGCSからBigQueryに流し込む
  • BigQueryで分析

後日談

認定通ったら社内のPrimary Job Role Specialtyが Application Developer: Google Cloud Microservicesになってました⸜(* ॑꒳ ॑*  )⸝笑 今後はGCP Associate Engineer取ってみたいですね。あとはAWS SAAも!

BuildKitを使って docker build のビルド時間を半分以下にする

元ネタはAWS CodeBuildでBuildKitがサポートされたという話。

dev.classmethod.jp

BuildKitとは、Docker v18.06から搭載された "次世代 docker build" のようです。BuildKitを使わないよりも圧倒的にビルドパフォーマンスが高くなる&セキュアにビルドできるようなので試してみました。

BuildKit自体の説明は上の記事でも紹介されていたSlideShareが一番簡潔で分かりやすいので是非見ていただければと。

www.slideshare.net

実践

今回は以下のようなJavaのコードをGradleでJarに固めることを試します。 やっていることはSlackのWebhook URLにUUIDをメッセージとして投げつけているだけです。

package net.ponzmild;

import java.io.IOException;
import java.util.UUID;
import net.ponzmild.client.SlackClient;

/**
 * 処理のエントリーポイント
 * @author ponzmild
 */
public class Handler {

  public static void main(String[] args) {

    String uuid = UUID.randomUUID().toString();
    System.out.println("Message UUID is " + uuid);

    String webhookUrl = System.getenv("SLACK_URL");
    if (webhookUrl == null || webhookUrl.isEmpty()) {
      System.out.println("Environment variable 'SLACK_URL' doesn't set or is empty.");
      System.exit(1);
    }

    SlackClient slackClient = new SlackClient(webhookUrl);
    try {
      String responseStr = slackClient.postMsg(uuid);
      System.out.println("Response is " + responseStr);
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

}

また、Dockerfileは以下のようにマルチステージビルドで何もしなくても比較的高速でビルドされるようにします。

FROM openjdk:8-jdk-alpine as build
COPY . /usr
WORKDIR /usr
RUN ./gradlew build

FROM openjdk:8-jre-alpine
COPY --from=build /usr/build/libs/dck-slack-messenger-1.0.jar /app/
ENV SLACK_URL https://hooks.slack.com/services/xxxxxx/yyyyyyyyy/zzzzzzzzzzzz
CMD java -jar /app/dck-slack-messenger-1.0.jar

BuildKitを使わない場合

まずは BuildKitを使わないdocker buildから。以下のように見たことあるようなログが出ますね。(ログが長かったのでGradleのビルド部分しか見えませんが...) ビルド所要時間は 約90秒 でした。

f:id:accelerk:20190109015058p:plain
BuildKitを使わない場合の docker build

BuildKitを使った場合

お次は BuildKitを使った場合です。 BuildKitの使い方は簡単で、ターミナル上で export DOCKER_BUILDKIT=1 をセットするだけ!ビルドしてみましょう。

f:id:accelerk:20190109015322p:plain
BuildKitを使った場合の docker build

おお!全然ログが違いますね。。。ビルド時間は 41秒 !! BuildKitを使わない場合の半分以下じゃん! それと自分の設定によるものかもしれませんが、ログ出力の観点では以下のような違いもありました。

  • 各ステップの所要時間が出力されるようになった。上の画像の右端に1/10秒単位で吐かれていますね。
  • Dockerfileで指定した EVN がログに出力されなくなった。
  • Gradleのビルド途中経過のログが一切出力されなくなった。
  • マルチステージビルドの場合は、ステージ名も出力されるようになった。

環境変数がログに出なくなったのは個人的には嬉しいです。デバッグしづらいかもしれませんが、秘密鍵とかトークンがそのままログに出るとセキュアではないですしね。

AWS以外でもDockerのバージョンさえ満たしていれば、他のクラウドやCIサービスで大いに活用できそうです。CI時間が長いとお困りの方は使ってみてはいかかでしょうか?