Ponz Dev Log

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

ブラウザからREST APIでAmazon S3にファイルをアップロードするときに気をつけること

書籍『AWSによるサーバーレスアーキテクチャ』(翔泳社)を読み進めているときにS3のファイルアップロード関連でつまづいたところが多々あったので、気をつけるべきところとして手順と一緒にまとめます。

AWSに関わる動的な処理を全てJavaScriptで実装することを選択した場合、AWS Lambda上ではAWS SDK for JavaScript一択ではありますが、ブラウザの場合は生のJavaScriptで記述することも可能です。

今回はブラウザ側で実行されるS3関連のコードをSDKを使わずREST APIで扱った時のメモです。 (書籍に従って生JSでゴリゴリ書きましたけど、潔くSDKを使えばこういったところで躓かなかったのではと思わずにはいられない。)

www.shoeisha.co.jp

やること

  • 署名付きURLを発行する。
  • バケットのアクセス権限を設定する。
  • REST API経由でリクエストを投げる。

署名付きURLを発行する

AWS S3 署名バージョン4でリクエストする

  • ファイル名(オブジェクトのキー)、バケット名、有効期限を使って署名付きのURLを発行します。このとき、署名バージョン4でリクエストします。
    • V4以外は受け付けません。明示的に指定した方がいいかも。
    • ここはLambda + API Gatewayで実装すると扱いやすい。

AWS regions created before January 30, 2014 will continue to support the previous protocol, Signature Version 2. Any new regions after January 30, 2014 will support only Signature Version 4 and therefore all requests to those regions must be made with Signature Version 4

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html

以下、コード例。

"use strict";

const AWS = require("aws-sdk");
const S3 = new AWS.S3({
    signatureVersion: 'v4'
});
const crypto = require("crypto");

function getSignedUrl(fileName) {
    const prefix = crypto.randomBytes(20).toString("hex");
    const params = {
        Bucket: process.env.UPLOAD_BUCKET,
        Key: prefix + "/" + fileName,
        Expires: 600
    };
    return S3.getSignedUrl("putObject", params);
}

module.exports = {
    getSignedUrl
};
  • オペレーション名を一緒に付与すること

    • オペレーションを指定するときは、putObjectとキャメルケースで指定すること。PutObjectと先頭大文字で指定するとエラーで弾かれる。
    • 公式ドキュメントには上記は明記されていないが、Sampleにはちゃっかりキャメルケースで指定されている。
  • ここで返却される署名付きURLは仮想ホスト形式で返されます。

    • http://{bucket-name}.s3.amazonaws.com
    • http://{bucket-name}.s3-aws-region.amazonaws.com

バケットのアクセス権限を設定する

バケットポリシー

まずはパブリックアクセス権限が付いていたら外します。 今回は署名付きURLでダウンロード/アップロードを行うため、リクエストの度にリクエスト者に権限が付与される仕組みにするためです。

バケットを選択 -> アクセス権限 -> バケットポリシーを開いてドキュメントを削除する。バケット一覧で該当のバケットに"非公開"と記載されていればOK。

常に最小限の権限を付与することが推奨されているため、不用意にパブリックアクセスを許可することは回避した方が良いです。

CORS設定

AWSとは別ドメインからアクセスすることになるので、どこから/どんなオペレーションが許可されるかバケットに設定する必要があります。 バケットを選択 -> アクセス権限 -> CORSの設定から以下のように設定を追加します。これだけ。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

あと地味なTipsとしては、1行目の<?xml...の記載は設定のエディタに書かなくても勝手に追加されます。

REST API経由でリクエストを投げる

PUTリクエストで投げる。

オブジェクトの新規作成をするんだからPOSTリクエストじゃね?と直感的に思いつきますが、PUTリクエスで投げます。POSTメソッドでリクエストを投げてもAccess Deniedと403が寂しく返ってくるだけです。 思い返してみれば、オペレーション名はPutObjectだもんね。

おそらく同一キーのオブジェクトに対しては上書きもできることから、POSTメソッドではなくPUTメソッドのリクエストにしているのではと推測します。 --> REST API Reference: PUT Object

Content-Typeはファイルのものを指定する

画像や動画をHTMLから送信する場合の定石だとContent-Type: multipart/form-dataでデータをバックエンドに送信しますが、S3へのアップロードに関しては単一ファイルの場合はmultipartリクエストを行うのは間違いです。 ファイル本体のContent-Typeを指定しましょう。

例えば、MP4の動画をアップロードする場合は、Content-Type: video/mp4といった具合です。

"use strict";

/**
 * ファイルをS3バケットにアップロードする。
 * @param {string} url アップロード先S3バケットの署名付きURL
 */
function upload(url) {
    // MP4の動画を添付したと想定。
    const uploadBtn = document.getElementById("uploadButton");
    const file = uploadButton.files[0];

    // S3バケットにアップロード実行。
    $.ajax({
        url: url,
        type: "PUT",
        data: file,
        processData: false,
        contentType: "video/mp4"
    })
    .done(response => {
        console.log(response);
        alert("Upload Finished!");
    })
    .fail(err => {
        console.log(err);
        alert("Failed to upload...");
    });
}

実はこれもちゃっかり公式ドキュメントに記載されています。日本語版にはないですけどね。 以下のExample2にJPEG画像の例が参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/RESTObjectPUT.html?shortFooter=true#RESTObjectPUT-responses-examples


ブラウザに含めるJSファイルのサイズを気にしてSDKを入れられない場合などに参考にしていただければと思います。

以上。

技術書典6 Knative本 読了後感想

実はタイトルの技術書展6には実は行けなかったのですが、 気になっていた本があったので思わずBoothでポチって読んで、手を動かした感想です。

感想を書く対象の本はこちらになります。 『Knativeの歩き方 KubernetesからServerlessを訪ねて』

toshi0607.booth.pm

感想

まず50ページ弱の文字通り薄い本なので、かなり読みやすいです。 構成は最も理解しやすいであろう順序で書かれているので、頭から読んで苦になりません。技術書として最高です。

特に、Serving --> Build --> Build + Serving --> Eventing + Servingの流れは素晴らしい。Serving + Buildのあたりなんか、もう明日には使えそう。

また、プラットフォーム依存の説明が少ないので、他のプラットフォームでも応用させやすい本ですね。

今やk8sはどのプラットフォームもサービスとして提供してるから当然ではあるのだが、GKE特有の説明もさらっと終わらせるので良い。公式にインストール方法が書かれているので、IBM CloudのIKS、AWSのEKS、AzureのAKS、Openshiftでも同様に動かせるはずです。

さらに言うと、説明が丁寧!読者を置いてけぼりにせずスラスラ読めます。

KnativeはCRD(Custom Resource Definition)を使って独自のKubernetesオブジェクトを作成することで様々な機能を拡張してますが、各CRDごとの役割を省かず丁寧に説明されています。独自の用語ばかりだとついていけなくて積み本行きになりがちですが、全くそんなことなかった。

Knativeについて思うこと

正直な話、コンテナ周りって扱うのつらさを感じるんですよね。便利なのは使ってて分かりますけど。 コンテナ単品だとランタイムやアプリをラップしているだけだし、Kubernetesでコンテナオーケストレーションすると今度はクラスタの管理やYAMLの記述に圧殺される。ネットワークやサービスメッシュは別プロダクトのIstioやLinkerdを使わないといけない。

こんなもどかしさを抱えているときに、「開発に集中する」ことを実現できるKnativeはかなり魅力的なプロダクトですね。 コンテナをビルドできる、そのままデプロイしてサービスを公開できる、イベントドリブンな構成も1つのプラットフォームでできてしまうのは驚き。 HTTPベースだけじゃなく、イベントを扱えるところも今までにない。

ただ、build, deploy, and manage modern serverless workloads と、サーバーレスを自称しているのどういうことだろうと考えこんでしまったのも事実。 なぜなら、今までServerless = FaaSという認識を持っていたから。CNCF Serverless Whitepaper v1.0でもFaaSを重点的に取り上げるくらいですしね。 思うに、文字通りプラットフォーム(インフラ構成やら、クラウドプロバイダーがどれやら、どの言語を使っているやら) = サーバーを意識させないところなんだろうな。もしくはプラットフォームの制約を受けないところ。

「開発に集中する」ことを実現している点では、開発者側から見ると既存のPaaS~FaaSと変わらないです。 CloudFoundryやHerokuで体験したコードを書いてコマンド一発でアプリができるという素晴らしい体験を、コンテナ・Kubernetesでオープンに実現しようというのがKnativeなんじゃないかなとこの本を読みながら考えさせられました。


以上。

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を埋め込んだ値で引数が全て渡っていますね。