AWS AppSyncとReactでToDoアプリを作ってみよう (2)DataSourceとResolverの設定

はじめに

前回の記事で定義したGraphQLスキーマはAPIのインターフェースの定義でした。
このインターフェースを使ってAPIにアクセスがあった際に、データを保存する先のリソース(DataSource)の作成と連携の部分を設定していきます。
AWS AppSyncでは、Resolverを作成することで、DataSourceとGraphQLスキーマとの紐付けを行います。

リソース(DataSource)の作成

今回の例では、GraphQLスキーマに定義したTodo型からDynamoDBのテーブルを作成します。
まずは、コンソール画面から、「AWS AppSync > 作成したプロジェクト > Schema」を開き、画面右上の「Create Recources」をクリックします。

CreateRecouces.png

次の画面では、定義済みのGraphQLスキーマから使用する型を選択します。
今回はTodoを選択しました。

スクリーンショット 2018-01-22 6.07.41.png

使用する型を選択すると、自動でテーブル名、テーブルの構成が入力されます。
今回は、idをプライマリキーとしました。

スクリーンショット 2018-01-22 7.05.49.png

ここまでの項目を入力すると、画面最下部にテーブル定義を元に、自動で追加されるスキーマ定義のプレビューが表示されます。
最後に、「Create」ボタンをクリックすると、DynamoDBテーブルが作成されます。

スクリーンショット 2018-01-22 7.05.59.png

Resolverの追加と修正

前の手順では、GraphQLスキーマから自動でDynamoDBとResolverのプロビジョニングを行いましたが、一部、定義元のスキーマと紐付いていない点があるので、追加・修正していきます。

getTodos

getTodosには、Resolverが紐付けられていないので、新規で追加します。

「Schema > Query > getTodos: [Todo]」の「Attach」をクリックします。
次の画面では、作成済みのDataSourceから紐付けるテーブルを選択します。
今回はTodoTabeを選択しました。

スクリーンショット 2018-01-22 6.42.08.png

次に、リクエストのマッピングを行います。
「Paginated scan」のテンプレートをベースに、次のように変更しました。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "Scan"
}

レスポンスのマッピングは次のようになります。

ResponseMappingTemplate
$utils.toJson($context.result.items)

最後に、画面右下の「Save」をクリックしてResolverの設定を保存します。

getTodo

これは、自動で追加されたQueryです。
今回のToDoアプリでは、item一件ごとに取得するGetItem操作は不要なので、削除します。

allTodo

これは、自動で追加されたQueryです。
こちらは、getTodosと処理が重複するため削除します。
(ページネーション処理も自動で設定済みのようなので、こちらを使うのがよさそうですが…)

addTodo

自動で追加されたMutation、putTodoの内容をこちらに移行すれば良さそうです。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key": {
        "id": { "S" : "${context.arguments.id}"}
    },
    "attributeValues" : {
        "id": {  "S": "${context.arguments.id}" },
        "title": {  "S": "${context.arguments.title}" },
        "description": {  "S": "${context.arguments.description}" },
        "completed": {  "B": "${context.arguments.completed}" }
    }
}
ResponseMappingTemplate
$utils.toJson($context.result)

updateTodo

updateTodoには、Resolverが紐付けられていないので、新規で追加します。
getTodosで行った新規追加手順と同様に操作を行い、次のテンプレートを設定します。

RequestMappingTemplate
{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key": {
        "id": { "S" : "${context.arguments.id}"}
    },
    "update" : {
        "expression" : "SET title = :title, description = :description, completed = :completed",
        "expressionValues" : {
            ":title" : { "S": "${context.arguments.title}" },
            ":description" : { "S": "${context.arguments.description}" },
            ":completed" : { "BOOL": ${context.arguments.completed} }
       }
    }
}
ResponseMappingTemplate
$utils.toJson($context.result)

更新操作は、DynamoDBの更新式の指定が必須となっているようでうです。

deleteTodo

こちらは、自動でしっかりとマッピングされているので、そのまま使用します。

putTodo

これは、自動で追加されたMutationです。
こちらは、addTodoと処理が重複するため削除します。

以上で、Resolverの設定が終わりました。

APIの動作確認

作成したAPIの動作確認を行ってみます。
コンソール画面から、「AWS AppSync > 作成したプロジェクト > Queries」を開き、画面左側のエディターエリアにクエリを入力し「▶」をクリックしてクエリを実行します。

スクリーンショット 2018-01-22 22.37.05.png

MutationとQueryそれぞれの項目を動作確認してみたいと思います。

addTodo

まずは、データを追加してみます。

Query
mutation addTodo {
  addTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
    title: "髪を切る"
    description: "来週までには"
    completed: false
  ) {
    id
    title
    description
    completed
  }
}

次のようなレスポンスが返ってくれば成功です。

Response
{
  "data": {
    "addTodo": {
      "id": "0651ed86-9314-4267-9bcf-7143b785f173",
      "title": "髪を切る",
      "description": "来週までには",
      "completed": false
    }
  }
}

getTodos

事前に何件かデータを追加した状態で以下のクエリを実行します。

Query
query {
  getTodos {
    id
    title
    description
    completed
  }
}
Response
{
  "data": {
    "getTodos": [
      {
        "id": "f163372a-8b54-4da4-9237-911a64067517",
        "title": "豆腐を食べる",
        "description": "腐りそう",
        "completed": false
      },
      {
        "id": "0cbab86a-ad72-41b4-a63d-9ce3f9a7d552",
        "title": "Qiita書く",
        "description": "あと2本",
        "completed": false
      },
      {
        "id": "0651ed86-9314-4267-9bcf-7143b785f173",
        "title": "髪を切る",
        "description": "来週までには",
        "completed": false
      }
    ]
  }
}

updateTodo

Query
mutation updateTodo {
  updateTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
    title: "部屋を掃除する"
    description: "さらっと済ます"
    completed: false
  ) {
    id
    title
    description
    completed
  }
}

※ Responseは省略

deleteTodo

Query
mutation deleteTodo {
  deleteTodo(
    id: "0651ed86-9314-4267-9bcf-7143b785f173"
  ) {
    id
    title
    description
    completed
  }
}

対象のitemのidを指定して、削除を行います。

※ Responseは省略

最後に

今回は、定義したGraphQLスキーマから自動でリソースを作成しましたが、AppSyncのコンソールからは、全て手動でリソースを用意することもできます。必要に応じて使い分けると良さそうです。
次回は、作成したGraphQL APIと連携するReactフロントエンドの実装を行っていく予定です。

参考

Attaching a Data Source -AWS AppSync
Provision from Schema -AWS AppSync
Resolver Mapping Template Reference for DynamoDB -AWS AppSync

続きを読む

Amazon Translateを使ってみた&自動翻訳付きチャットを作ってみた

Amazon Translateのプレビュー申請が先日通ったので、使ってみました。us-east-1(バージニア北部)で利用しています。

Amazon Translateとは?

公式ページへどうぞ。

成果物

とにかく成果物が早く見たい方はこちらへ。

https://github.com/kojiisd/amazon-translate-chat-demo

コンソールから使ってみる

とりあえずAWSコンソールから使ってみます。100万文字で15ドルの料金のようです。

スクリーンショット 2018-01-13 16.05.42.png

選択できる言語は7つとまだ少なめですが、いずれすぐに日本語も追加されるでしょう。

image.png

入力した文字がリアルタイムに翻訳されます。(日本語が選択できないので、文章を入れても自然な文章になっているのかは判断できませんがw)

スクリーンショット 2018-01-13 16.08.49.png

APIを使ってみる

ただこれだけだとなんの面白みもないので、自動翻訳をつけたようなチャットを作ってみたいと思います。

画面の準備

以前作成したiot-demo-deviceをベースに開発します。

https://github.com/kojiisd/aws-iot-demo-device/

とりあえずこんな感じの画面を組みます。

スクリーンショット 2018-01-20 13.26.33.png

項目名 意味
Name 名前
Source Language 受信するメッセージの言語
Target Language 翻訳対象言語
Start Chat チャット開始のためのボタン
Message メッセージ
Send Message メッセージ送付用ボタン

最初は名前やメッセージ受信時の言語を選択しないとチャットを
スタートできないようにしています。
→メッセージをSubscribeする際に自身と同じ名前だったら翻訳しないようにするため。

画面下部の「Original」と「Translated」にメッセージのやりとりが記録されるように実装します。

WebSocketにAWS IoTを利用する

チャットのやりとりをするために今回WebSocketを利用しますが、一から環境を用意するのが面倒なので、AWS IoTで手を抜きます。カスタマイズなどは特に気にせずデフォルトで設定してしまってOKです。

今回はこちらの記事と同じ設定をしました。

Three.jsとAWSを連携させてIoTっぽいことしてみた

AWS IoTは受信後のアクションが必要だったので、DynamoDBに履歴として登録する体で設定をしています。

aws-sdkのフルビルド

この記事を書いている時の「v2.184.0」ではフルビルドをしないとAWS.Translateクラスが使えなかったため、公式ページを参考にビルドを実施します。

https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/building-sdk-for-browsers.html

$ git clone git://github.com/aws/aws-sdk-js
$ cd aws-sdk-js
$ npm install
$ node dist-tools/browser-builder.js all > aws-sdk-full.js

翻訳部分を開発する

肝心の翻訳ですが、こんな感じのコードにします。事前にCredentialやEndpointなど必要な情報を設定しておく必要があります。なぜPromiseが必要かは後述します。

AWS.config.credentials = new AWS.Credentials(cred.awsAccessKeyId, cred.awsSecretAccessKey);
  :
  :
function translate(message) {

  var params = {
    Text: message,
    SourceLanguageCode: srcLang,
    TargetLanguageCode: targetLang
  };

  var syncProc = new Promise(
    function (resolve, reject) {
      window.translator.translateText(params, function onIncomingMessageTranslate(err, data) {
        if (err) {
          reject("Error calling Translate. " + err.message + err.stack);
      }
        if (data) {
          resolve(data.TranslatedText);
        }
      });
    }
  );

  return syncProc;
}

翻訳語の文章の追加部分の開発

上述のtranslateメソッドを呼び出す部分ですが、メッセージ受信後のコールバックメソッドとなります。

function onMessage(message) {
  var msgJson = JSON.parse(message.payloadString);
  var addingHtml = "<tr><td>" + msgJson.name + ": </td><td>" + msgJson.message + "</td>";
  if (msgJson.name == body.name) {
    addingHtml += "<td></td><td></td></tr>"
    $("#chatArea").prepend(addingHtml);
  }
  else {
    translate(msgJson.message).then(function (result) {
      addingHtml += "<td>" + msgJson.name + ": </td><td>" + result + "</td></tr>"
      $("#chatArea").prepend(addingHtml);
    }).catch(function(error){
      alert(error);
    });
  }

メッセージを受信した際、自身の名前と同じであれば翻訳は実施しないように処理をしています。

自分ではない人からのメッセージの場合translateメソッドを呼び出しますが、Translate APIにアクセスするタイミングで非同期処理となってしまうため、前述のPromiseでこの辺の処理の順番を制御しました。

実際の動き

画面を二つ用意して、それぞれ「kojiisd1」と「kojiisd2」でチャットを開始するようにします。設定はそれぞれ以下の通りとします。

kojiisd1 kojiisd2
Source Language French English
Target Language English French

kojiisd1からは英語で話すようにし、kojiisd2からはフランス語で話をしてみます。

こんな感じで操作できました(ちょっとGIFのサイズ大きい)。

amazon-translate-chat-demo3.gif

まとめ

Amazonが提供する翻訳サービスなので、Pollyなど他のサービスとの連携も容易にできるようになると期待が持てます。これは電話会議などもAmazonが提供する翻訳サービスを使いながらリアルタイムに全て自動翻訳できる日が来そうな感じがして、期待大のサービスですね。

続きを読む

「Microservices Meetup vol.6」に行ってきた

2017/1/19のデイリーストックランキングにランクインしました。

【毎日自動更新】Qiitaのデイリーストックランキング!ウィークリーもあるよ_-_Qiita.png


いままでの。
「Microservices Meetup vol.2」行ってきたメモ。
「Microservices Meetup vol.4」に行ってきた

図らずして1回おきに参加してますね。

Connpassのイベントページ

少し間が空いてしまったがまた定期的に開催していきたいです。
登壇したいよーって人は直接リプ投げてください
by @qsona さん

Microservices on AWS by @Keisuke69 さん

Keisuke Nishitani @ AWS

Specialist Solutions Architect

サーバーレスアプリケーション開発ガイド 2月発売

マイクロサービスのポイント

  • 管理運用まで含めて分散型
  • 各コンポーネントが独立している
    • 単独で実行できないのは適切に設計されていない

独立して分散していると負荷の高い機能の単位でスケールさせられる=コスト効率が良い

  • 一つのことをうまくやる

    • 複雑化したら分割する
  • 多言語

    • チーム(機能)にはそれぞれの問題に対して適切なツールを選択する自由がある
    • OS、言語、各種ツールに至るまで最適なアプローチを目指せる
  • 検索:Elasticsearch / Solr

  • ソーシャル:グラフDB

  • ログデータ:Cassandra

  • セッション:Redis

†AWSでは:AWSには100ちょいのサービスがあって、その中で更にチームが分かれている。
各チームに社内標準の開発プロセスは存在しない。

  • ブラックボックス

    • 詳細は外部のコンポーネントに公開されない
  • DevOps
    • マイクロサービスにおける組織原理

†AWSでは:運用のみのチームはない。オンコールも開発チームが請け負う

  • 俊敏性

    • 狭い範囲のコンテキストで活動=サイクルタイムが短い
    • システムもシンプル
    • パラレルな開発とデプロイが可能
  • イノベーション

    • 選択に対する権限と責任を持つのでイノベーションを起こしやすい
    • DevとOpsの対立がないため、効率化やイノベーションが起こしやすい
  • 拡張性

    • 適切に非干渉化されてていることで水平方向に単独にスケールできる
  • 可用性

    • ヘルスチェック、キャッシング、隔壁、サーキットブレーカーと言った仕組みは全体の可用性を向上させる

課題

  • 分散型であることは難しい

    • システム数が増える
    • 協調動作の難しさ
    • コンポーネント間のコミュニケーションメッセージ増によるレイテンシ低下
    • ネットワークの信頼性は無限ではない、帯域も無限ではない
  • 移行が大変
    • モノリシックなシステムの分割は難しい
  • 組織
    • 組織体制の変更が必要になるが、それは大変なこと
  • アーキテクチャの難易度
    • 非同期通信
    • データ整合性
    • やっぱりココが一番の課題
    • サービスディスカバリ
    • 認証
  • 運用の複雑さ

アーキテクチャ

一番シンプルなパターン

CloudFront – ALB – ECS – datastore(ElastiCache, Dynamo, )
|
S3

  • バックエンドをRESTfulなAPIにしたSPA
  • 静的なコンテンツはS3とCloudFront
    • CDN通すことでレイテンシが上がることもある
    • キャッシュとの併用を検討
  • ECSとAutoScalingをALBとともに使うことが多い
    • ALB(L7LB)でアプリレベルの情報をルーティング
    • コンテナインスタンスにリクエストを分散
    • ECSのコンテナを負荷に応じてスケールアウト/インする

コンテナのメリット

  • Portable
  • Flexible
  • Fast
    • 軽量で早い
    • ポータビリティと関連して開発サイクルも早く
  • Efficient

  • 一貫性のある環境

  • バージョン管理出来る

Dockerの特徴

  • Package
  • Ship
  • Run

ECS

  • 複数のコンテナをEC2のクラスタ上で一元管理

    • まだTokyoにきてないけど、FargateでEC2の管理もいらなくなるよ
    • EKS(Kubernetes)も発表されています

データストア

  • インメモリ

    • Memcached, Redis
    • ElastiCache
    • DB負荷の軽減
  • RDBMS
    • 無限のスケーリングには向いていない
    • Amazon RDS
  • NoSQL
    • 水平スケーリングをサポートするものが多い
    • テーブル結合ができないのでロジックの実装が必要になる場合も
    • Amazon DynamoDb, KynamoDB Accelarator(DAX)

APIの実装

  • APIの設計、改善、デプロイ、モニタリング、保守派手間がかかる
  • 異なるバージョンが混在すると大変

Amazon API Gateway

  • 複数バージョンとステージ
  • Cognite User Poolsと連携して認証を追加
  • スロットリング、モニタリング
  • バックエンドとしてLamdbaが使える

サーバーレス

  • サーバーはないに越したことはない
  • スケーリングと高可用性の担保が大変
CloudFront - API Gateway - Lamdba - datastore
   |                            (ElastiCache, Dynamo)
   |
  S3

課題をAWSでどうするか

サービスディスカバリ

  • お互いの死活確認や発見
  • ハードコードするとスケールできない

    • メタデータをどこに置くか
  • ALBを利用したサービスディスカバリ

  • DNS(Route53)のサービスディスカバリ

    • VPC単位の振り分け
  • ECSイベントストリームを使用したサービスディスカバリ

    • CloudWatchイベントで拾ってキック
    • LamdbaでRoute53に登録
    • これが一番多いかも
  • DynamoDBを使用したサービスディスカバリ

    • DNSキャッシュの問題がない
    • 自由度が高い
    • 実装が大変
    • DynamoDBストリームを活用して他のサービスへステータス変更を反映
  • イベントベースのアーキテクチャ

    • イベントで処理する
    • いわゆる結果整合性とも関連
    • Dual Write Problem
    • Event Sourcing
      • 状態の記録ではなく状態の変更「イベント」を記録
      • Amazon Kinesis
      • kinesisにpublishして他サービスはsubscribe
      • S3にログを残す
      • トランザクションログの仕組み

モニタリング

  • CloudWatch

    • ログファイルの一元化
    • CloudWatch LogsかS3に保存可能
    • ECSはCloudWatch Logsに一元化できる

分散トレース

  • AWS X-Ray
  • 複数サービスに分散したリクエスト処理を透過的に追える

LogWatchの先に Kinesis FireHorse – ほにゃほにゃ

スロットリング

  • 大事だよ

リトライ

  • 多くのエラーはリトライでカバーできるものが多い
  • リトライ多発で過負荷になることがある
    • Exponential back offもしくはフィボナッチ数列を利用したリトライ感覚の調整
    • 更に乱数でゆらぎを付けて重ならないように

LT:クラウド型医療系業務システムと Microservices への歩み by @hashedhyphen さん

https://speakerdeck.com/hashedhyphen/kuraudoxing-dian-zi-karutesisutemuto-microservices-hefalsebu-mi

クラウド型電子カルテシステムの話

  • メインロジックをRails
  • 常時接続をNode.js
  • バックエンドをScala
  • 基盤はAWS

ここに至る道のり

ファーストリリース前

  • レコメンドは大量のデータが必要

  • メインロジックで実現させようとすると厳しかった

    • Threadとか試したけど……
    • 別アプリケーションとして分離
    • JVM上でScala + Skinnyでスレッドアプリケーションンを実装
    • Microservicesちっくな構成へ
  • 障害検知時

    • メインロジック内のプロトタイプ版が動く!

会計機能リリース前

  • お金を扱う機能は安定性が欲しい
  • Scala + Cats による実装を別エンドポイントとして実装
  • マイクロサービスっぽい作りになっていたから自由度のある技術選択が出来た

まとめ

  • ちょっとマイクロサービス化したことで自由度が上がった
  • 原理主義的にならなくても恩恵がある
  • エンジニアの伸びしろ!

FrontEndからみるmicroserviceとBackendからみるmicroservice by @taka_ft さん

Takahiro Fujii @ Rakuten Travel

楽天内でも採用アーキテクチャはサービスによって異なる。

サービスとしては

  • コンシューマ向け
  • Extranet
  • In-house
  • other
    • ここまではWEBとモバイルがあって、100以上のAPIを利用する
  • API(内部APIを直接利用)

Phase 1

  • 大きなモノリシックなアプリだった
  • 機能がどんどん増えていく
    • 機能別で複数のモノリシックなアプリに分割
    • その後フロントエンドとバックエンドに分割

Phase 2

  • ドメインモデルを整理
  • なるべくRESTfulで作るようになっていった
  • 大きなAPIから小さなAPIに分離
  • I/Fの決定に時間がかかるようになった
  • API呼び出しが大変になった
  • Microservicesっぽくなってきた 2014年くらい

  • 国内予約、海外予約で多言語化だけではない商習慣に根ざしたドメインの差異による重複ロジックの増殖が起きた

  • Microservicesっぽくなってきたが、どんどん品質が下がっていった

Phase 3

(ちょうど前日にプレスリリース)サイト前面刷新に向けての取り組み

  • FrontendsはJavaScriptに刷新
  • API GatewayにKong

組織

  • フロントエンドチームが2人から始まって半年でReactエンジニアを集めて20人以上に

    • 日本人は2, 3人

UI Component

  • SpringからJavaScriptでSPAに変更
  • zeplinのデザインモックからUI Componentを実装するようになった
  • Storyboardを使ってUI Coponentを管理
  • ドメインを含むUI Componentはドメインと結び付きが強いことが多い=専用のオーケストレーションAPIを用意してUI Componentないで処理を閉じることが出来る
  • レスポンシビリティを明示的に定義

    • どこまでフロントエンドでやるか、どこからAPIからもらうか
  • フロントエンドの「使いやすいレスポンス」の要求

  • バックエンドの「汎用的でシンプルなレスポンス」の希望

    • これらのバランスを取る
  • API Gatewayはフロントエンド(UI)チームの管轄

    • ただし状況によってはバックエンド側に持っていくことも検討するつもり

LT: “マイクロサービスはもう十分”か? by @qsona さん

https://speakerdeck.com/qsona/enough-with-the-microservices

POSTDに投稿していた翻訳記事。

スタートアップ企業のほとんどはマイクロサービスをさいようすべきではない

銀の弾丸はない。

「チーム間の依存性」の解決にマイクロサービスというアプローチは違う。疎結合と分散は別。

組織が大きくなると、複数チームが1つのコードベースを触るようになる。そしてマイクロサービス化したくなる。

しかし、モノリスの分割で十分。

チームとは何か

  • 自律的に動けるべき

    • チームが増えるとコミュニケーションコストや、しがらみ
  • チームの分割は目的にそった分割を行う

  • 悪い分割の例: Dev / Ops

  • 依存度が低いほど自律的に動ける

  • High Output という本

  • 使命型組織/技術型組織

    • Micorservicesは使命型組織
    • 組織間が密ならサービス間も密になる

話戻して

  • スタートアップは組織をキレイに分割することが難しい
  • 分割しても密になってしまう

  • 大きなモノリスになるとやはり分割は難しい

    • ドメインの意識は必要
  • マイクロサービス設計しなくても「マイクロサービス精神」で開発すると効果的なのではないか

FiNCが初期からマイクロサービスでやってた理由

  • 単独の事業にできるような一つ一つのサービスを組み合わせて提供してきたから

あとで読み返して修正します。
資料パスの追加とかも。

続きを読む

AWSとAzureとGCPを比較してみる – FaaS編

FaaSについてAWSとAzureとGCPを比較してみました。

注)

1. FaaS比較表

AWS Azure GCP
Lambda Functions Cloud Functions***
言語 Python,
node.js,
java,
C#,
go
ランタイムバージョン1.X
C#,
JavaScript,
F#,
Python*,
PHP*,
TypeScript*,
バッチ (.cmd、.bat)*,
Bash*,
PowerShell*

ランタイムバージョン2.X
C#**,
JavaScript**,
Java**
node.js***
最大実行時間 5分 10分 (従量課金プラン)
無制限 (App Serviceプラン)
9分
直接HTTPアクセスを受け付けるか 受け付けない(API Gatewayと連携必要) 受け付ける 受け付ける
トリガー Amazon S3,
Amazon DynamoDB,
Amazon Kinesis Data Streams,
Amazon Simple Notification Service,
Amazon Simple Email Service,
Amazon Cognito,
AWS CloudFormation,
Amazon CloudWatch Logs,
Amazon CloudWatch Events,
AWS CodeCommit,
スケジュールされたイベント (Amazon CloudWatch Events を使用),
AWS Config,
Amazon Alexa,
Amazon Lex,
Amazon API Gateway,
AWS IoT ボタン,
Amazon CloudFront,
Amazon Kinesis Data
Blob Storage,
Cosmos DB,
Event Hubs,
HTTP,
Microsoft Graph Events(2.Xのみ),
Queue storage,
Service Bus,
Timer,
Webhooks(1.Xのみ)
HTTP,
Cloud Storage,
Cloud Pub/Sub

*試験段階
**プレビュー
***ベータ

2. 対応言語の比較

言語の種類は試験段階とプレビューを含めればAzureが一番多いのですが、正式リリースされたものに限定すればAWSの方が種類が多いです。
一方GCPは機能自体がベータリリースなので、まだこれからといった感じでしょうか。

AzureはBashにも対応しているのが特徴です。運用系のシェルスクリプトをFaaS化すれば、スクリプト用のサーバが不要になりますね。

3. 最大実行時間

最大実行時間はAzureの10分(要host.jsonのfunctionTimeoutプロパティ変更)、GCPの9分に対しAWS Lamdbaは5分と約半分です。実際にAWS Lambdaを利用していると5分の壁を結構感じます。この点は他クラウドが羨ましいですね。
2017年のRe:InventでAWSはFargateというコンテナのサービスをリリースしましたが、このサービスがlambdaが5分以上実行できないことに対するAWSからの回答のように感じます。

4. 直接HTTPアクセスを受け付けるか

AWS lambdaだけ直接HTTPアクセスを受け付けることができません。HTTPアクセスを受け付けるには、API Gatewayと連携する必要がありますが、多機能な分やや設定が面倒な印象です。(但しAPI経由でLambdaを起動することは可能)

まとめ

AWS Lambdaのリリース後、Azure・GCP・Bluemix(現IBM Cloud)は超特急で追従しました。AWS LambdaがIT業界に与えたインパクトはとても大きかったと思います。
現在は「FaaS無ければばクラウドにあらず」といったところでしょうか。

また、AWS GreengrassやAzure IoT Edge**というエッジにデプロイするサービスも出てきています。
将来AWS LambdaがiPhoneやApple Watchにデプロイできるようにならないかなーと妄想中です。

**プレビュー

続きを読む

AWSLambdaがGo言語に対応したのでDynamoDBと絡ませながらデモってみた

はじめに

去年のRe:Inventで「来年の早い時期にLambdaがGoに対応するよ!」なんてアナウンスされた時は、来たかっ!という気持ちになりました。
首を長くして待っていたら、本日サポートしたと発表があったので早速試してみました。

デモdemo

普段Node.jsを使ってLambda+DynamoDBの構成を組んでいるので、今回もDynamoDBを絡めてデモってみました。

公式が出している記事を参考にしていただければいいのですが、
現時点でサポートしているバージョンは 1.x です。
基本的なデプロイの流れは
1. Go書く
2. Linux用にビルドする
3. zipでアップロード
という流れになると思います。

Go書く

基本形は下記のような感じです。

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
    "fmt"
)

type Response struct {
    Message string `json:"message"`
    Ok      bool   `json:"ok"`
}

func Handler() (Response, error) {
    // 処理部分
}

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

lambda.Start(Handler) でLambdaの処理を実行します。
なんか少し違和感があるのは私だけでしょうか…
んで、 Handler の中に実際の処理を書いていく感じになります。

DynamoDB(GetItem, PutItem, Query)

aws-sdk for goを使ってDynamoDBにリクエストを送っていきます。
上記のソースコードに追記したのが下記になります。

package main

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-lambda-go/lambda"
    "fmt"
)

type Response struct {
    Message string `json:"message"`
    Ok      bool   `json:"ok"`
}

func Handler() (Response, error) {
    // session
    sess, err := session.NewSession()
    if err != nil {
        panic(err)
    }

    svc := dynamodb.New(sess)

    // GetItem
    getParams := &dynamodb.GetItemInput{
        TableName: aws.String("go-demo"),
        Key: map[string]*dynamodb.AttributeValue{
            "id": {
                S: aws.String("1"),
            },
        },
    }

    getItem, getErr := svc.GetItem(getParams)
    if getErr != nil {
        panic(getErr)
    }
    fmt.Println(getItem)

    // PutItem
    putParams := &dynamodb.PutItemInput{
        TableName: aws.String("go-demo"),
        Item: map[string]*dynamodb.AttributeValue{
            "id": {
                S: aws.String("2"),
            },
            "name": {
                S: aws.String("hoge"),
            },
        },
    }

    putItem, putErr := svc.PutItem(putParams)
    if putErr != nil {
        panic(putErr)
    }
    fmt.Println(putItem)

    // Query
    queryParams := &dynamodb.QueryInput{
        TableName: aws.String("go-demo"),
        KeyConditionExpression: aws.String("#ID=:id"),
        ExpressionAttributeNames: map[string]*string{
            "#ID": aws.String("id"),
        },
        ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
            ":id": {
                S: aws.String("1"),
            },
        },
    }

    queryItem, queryErr := svc.Query(queryParams)
    if queryErr != nil {
        panic(queryErr)
    }
    fmt.Println(queryItem)

    return Response{
        Message: fmt.Sprintln(getItem.Item),
        Ok:      true,
    }, nil
}

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

GetItemで返ってくるレスポンスはこんな感じ

{
  Item: {
    id: {
      S: "1"
    },
    name: {
      S: "test"
    }
  }
}

Queryで返ってくるレスポンスはこんな感じ

{
  Count: 1,
  Items: [{
      id: {
        S: "1"
      },
      name: {
        S: "test"
      }
    }],
  ScannedCount: 1
}

ビルドして、zipに固める

$ go build -o main.go
$ zip deployment.zip main

linux用にビルドしないといけないので、macの方は下記を実行してからzipに固めてください。

$ GOOS=linux go build -o main

Lambdaにデプロイする

Lambdaのコンソールから新しく関数を作成して、ランタイムを Go 1.x を選択します。
アタッチするロールには今回はDynamoDB関係の権限も忘れずに。
スクリーンショット 2018-01-16 15.05.24.png

関数が作成されたら、先ほど作成したZipファイルをアップロードすれば完了です。
スクリーンショット 2018-01-16 15.06.08.png

これでGoを使ってDynamoDBのデータを操作できるぞ!

あとがき

正直Go言語はほとんど触ったことなかったので、ソースコードはきったねぇと思いますがご了承ください。
Node.jsの感覚で実装したら動いたって感じです…(汗)

Node.jsで実装していると「シングルスレッドじゃあしんどいな…」と感じている時もあったりします。
Go言語はNode.jsと違って並列処理に長けているので、ケースバイケースで「Node.jsよりもGoの方がよいんでは?」となりそうなので、今のうちにGo言語に慣れておかなければと実感しました。

ではまた!

続きを読む

Glueの使い方的な⑦(Step Functionsでジョブフロー)

Step FunctionsでGlueのジョブフローを作る

Glueの使い方的な③(CLIでジョブ作成)“(以後③と書きます)で書いたように、現在Glueのジョブスケジュール機能は簡易的なものなので、複雑なジョブフロー形成には別のスケジューラーが必要になる場合もあります。
例えばGlueのクローラーとGlueジョブもそれぞれにスケジュール機能があり統合したジョブフローを作ることがGlueだけでは出来ません(例えばクローラーを実行し終わったらジョブを実行するとか)。今回はサーバーレスなジョブフローのサービスであるStep Functionsを使って、クローラーを実行し正常終了したら後続のジョブを実行するというフローを作ってみます。

全体の流れ

  • Glue処理内容
  • StepFunctionsの処理内容
  • 前準備
  • Step FunctionsでStateMachine作成
  • 実行

処理内容

Glueの使い方的な①(GUIでジョブ実行)“(以後①と書きます)で実行したものと同じクローラーとジョブを使います。入力データも出力結果も①と同じです。
今回行うのはGlueクローラー処理が終わったら次のGlueジョブ処理開始というジョブフロー形成です。

あらためて①のクローラーとジョブの処理内容は以下の通りです

クローラーの内容

入力のCSVファイルからスキーマを作成します

ジョブの内容

“S3の指定した場所に配置したcsvデータを指定した場所にparquetとして出力する”

Step Functionsを使ったジョブフローの内容

図の四角をStep Functionsでは”State”と呼びます。処理の1単位と思ってください。

ジョブフローは以下のような形です。

Stateごとに流れを説明します

  • “Submit Crawler Job”でLambdaを使いGlueクローラーを実行
  • “Wait X Seconds”で指定時間待つ
  • “Get Crawler Job Status”でLambdaを使いGlueクローラーの状態をポーリングして確認
  • “Job Complete?”で状態を判定して結果によって3つに処理が分岐
    • 失敗なら”Job Failed”エラー処理
    • 終了なら”Run Final Glue Job”でLambdaを使い後続のGlueジョブを実行
    • 処理中なら”Add Count”でLambdaを使いカウンタをインクリメント。
      • “Add Count”の後”Chk Count”でカウンタをチェックし3回以上になっていたら”Job Failed Timeout”でタイムアウト処理、3未満なら”Wait X Seconds”に戻りループ処理

スクリーンショット 0030-01-13 21.47.05.png

前準備

①と同じです

今回使うサンプルログファイル(19件)

csvlog.csv
deviceid,uuid,appid,country,year,month,day,hour
iphone,11111,1,JP,2017,12,14,12
android,11112,1,FR,2017,12,14,14
iphone,11113,9,FR,2017,12,16,21
iphone,11114,007,AUS,2017,12,17,18
other,11115,005,JP,2017,12,29,15
iphone,11116,001,JP,2017,12,15,11
pc,11118,001,FR,2017,12,01,01
pc,11117,009,FR,2017,12,02,18
iphone,11119,007,AUS,2017,11,21,14
other,11110,005,JP,2017,11,29,15
iphone,11121,001,JP,2017,11,11,12
android,11122,001,FR,2017,11,30,20
iphone,11123,009,FR,2017,11,14,14
iphone,11124,007,AUS,2017,12,17,14
iphone,11125,005,JP,2017,11,29,15
iphone,11126,001,JP,2017,12,19,08
android,11127,001,FR,2017,12,19,14
iphone,11128,009,FR,2017,12,09,04
iphone,11129,007,AUS,2017,11,30,14

入力ファイルをS3に配置

$ aws s3 ls s3://test-glue00/se2/in0/
2018-01-02 15:13:27          0 
2018-01-02 15:13:44        691 cvlog.csv

ディレクトリ構成

in0に入力ファイル、out0に出力ファイル

$ aws s3 ls s3://test-glue00/se2/
                           PRE in0/
                           PRE out0/
                           PRE script/
                           PRE tmp/

ジョブのPySparkスクリプト

se2_job0.py
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
## @type: DataSource
## @args: [database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0"]
## @return: datasource0
## @inputs: []
datasource0 = glueContext.create_dynamic_frame.from_catalog(database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0")
## @type: ApplyMapping
## @args: [mapping = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1"]
## @return: applymapping1
## @inputs: [frame = datasource0]
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1")
## @type: ResolveChoice
## @args: [choice = "make_struct", transformation_ctx = "resolvechoice2"]
## @return: resolvechoice2
## @inputs: [frame = applymapping1]
resolvechoice2 = ResolveChoice.apply(frame = applymapping1, choice = "make_struct", transformation_ctx = "resolvechoice2")
## @type: DropNullFields
## @args: [transformation_ctx = "dropnullfields3"]
## @return: dropnullfields3
## @inputs: [frame = resolvechoice2]
dropnullfields3 = DropNullFields.apply(frame = resolvechoice2, transformation_ctx = "dropnullfields3")
## @type: DataSink
## @args: [connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4"]
## @return: datasink4
## @inputs: [frame = dropnullfields3]
datasink4 = glueContext.write_dynamic_frame.from_options(frame = dropnullfields3, connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4")
job.commit()

入力のCSVデータのスキーマ

クローラーによって作成されるスキーマ

スクリーンショット 0030-01-13 22.01.43.png

StepFunctionsでStateMachine作成

StepFunctionsは一連のジョブフローをJSONで定義しこれを”StateMachine”と呼びます。
StateMachine内の処理の1つ1つの四角をStateと呼びます。処理の1単位です。
このJSONの記述はASL(AmazonStatesLanguages)と呼ばれStateTypeとしてChoice(分岐処理)やWait(待ち)やParallel(並列実行)などがJSONだけで表現出来ます。またTaskというStateTypeからはLambdaやアクティビティ(EC2からStepFunctionsをポーリングする)を定義できます。前述の通り今回はLmabdaを使います。

マネージメントコンソールからいくつかあるテンプレートを元に作ることも出来ますが、カスタムでJSONを一から作ることもできます。

新規StateMachine作成画面
“Author from scrach”で一からJSON作成

スクリーンショット 0030-01-14 10.24.59.png

“Template”を選ぶとASLのStateパターンのいくつかのテンプレが選べます

スクリーンショット 0030-01-14 10.28.14.png

左側の”コード”部分にJSONを書き、右側の”ビジュアルワークフロー”の部分にJSONコードで書いたフローがビジュアライズされます

スクリーンショット 0030-01-14 10.29.50.png

StateMachine

今回のStateMachieのJSONは以下です。
内容は前述の通りです。
※[AWSID]のところは自身のAWSIDと置き換えてください

{
  "Comment": "A state machine that submits a Job to Glue Batch and monitors the Job until it completes.",
  "StartAt": "Submit Crawler Job",
  "States": {
    "Submit Crawler Job": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-cr1",
      "ResultPath": "$.chkcount",
      "Next": "Wait X Seconds",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 120,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
    "Wait X Seconds": {
      "Type": "Wait",
      "SecondsPath": "$.wait_time",
      "Next": "Get Crawler Job Status"
    },
    "Get Crawler Job Status": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-crcheck",
      "Next": "Job Complete?",
      "InputPath": "$",
      "ResultPath": "$.response",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
      "Job Complete?": {
      "Type": "Choice",
      "Choices": [{
          "Variable": "$.response",
          "StringEquals": "FAILED",
          "Next": "Job Failed"
        },
        {
          "Variable": "$.response",
          "StringEquals": "READY",
          "Next": "Run Final Glue Job"
        }
      ],
      "Default": "Add Count"
        },
    "Add Count": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-addcount",
      "Next": "Chk Count",
      "InputPath": "$",
      "ResultPath": "$.chkcount",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
      "Chk Count": {
      "Type": "Choice",
      "Choices": [{
          "Variable": "$.chkcount",
          "NumericGreaterThan": 3,
          "Next": "Job Failed Timeout"
        }],
      "Default": "Wait X Seconds"
    },
    "Job Failed": {
      "Type": "Fail",
      "Cause": "Glue Crawler Job Failed",
      "Error": "DescribeJob returned FAILED"
    },
        "Job Failed Timeout": {
      "Type": "Fail",
      "Cause": "Glue Crawler Job Failed",
      "Error": "DescribeJob returned FAILED Because of Timeout"
    },
    "Run Final Glue Job": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-job1",
      "End": true,
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    }
  }
}

Lambda

今回使うLambdaは4つです。流れも振り返りながら見ていきます
書き方はいろいろあるし今回はエラーハンドリングも甘いのであくまでも動きのイメージをつかむための参考程度にしてください。最後のGlueジョブの実行についてはジョブの終了判定とかはしてないです。

“Submit Crawler Job”

GlueのAPIを使ってクローラーのStartを行う

glue-test1-cr1
# coding: UTF-8

import sys
import boto3
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.start_crawler(Name='se2_in0')
    return 1

“Wait X Seconds”

Waitで指定秒数待つ

“Get Crawler Job Status”

GlueのAPIを使ってクローラーのステータスを取得します

glue-test1-crcheck
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.get_crawler(Name='se2_in0')
    response = response['Crawler']['State']
    return response

“Job Complete?”

Choiceで取得したステータスが、”READY”なら正常終了、”FAILED”なら失敗、それ以外は実行中の分岐処理

“Job Failed”

ステータスが失敗なら
FailでStepFunctionsをエラーさせます

“Run Final Glue Job”

ステータスが正常終了なら
GlueのAPIを使ってジョブをStartします

glue-test1-job1
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.start_job_run(
    JobName='se2_job0')
    return response['JobRunId']

“Add Count”

クローラーがまだ実行中なら
カウンタにインクリメントします

glue-test1-addcount
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    chkcount = event["chkcount"]
    chkcount = chkcount + 1

    return chkcount

“Chk Count”

choiceでカウンタが3未満か3以上かをチェックします

“Job Failed Timeout”

Failでカウンタが3以上だった時のエラー処理

“Wait X Seconds”

3未満の場合はここに戻りループ処理

実行

Step Functionsを実行

作成したStateMachineを選び”新しい実行”をクリック

スクリーンショット 0030-01-14 10.54.54.png

JSONに引数を入れて”実行の開始”をクリック
今回はJSON内で使う変数で”wait_time”を60秒で待ちの時間として入力しています

スクリーンショット 0030-01-14 10.55.52.png

実行状況

スクリーンショット 0030-01-14 10.59.44.png

CloudWatchイベントでスケジュール

あとは上記で作成したStateMachineをCloudWatchイベントでCRON指定すれば定期的実行されるジョブフローの完成です。This is Serverless!

スクリーンショット 0030-01-13 22.34.00.png

その他

今回はクローラー実行後にジョブ実行というシンプルなフローでしたが、Step Functionsは並列度を替えたり引数の受け渡しをしたり、さらにLambdaでロジックを書くことができるので自由度高く複雑なフローの作成が行えます。Glueとの相性はいいのではないでしょうか?

JSON部分も30分もあれば学習完了というカジュアルさがありLambdaを使ってAPI操作で様々なAWSの処理を繋げるのにはとてもいい印象です。

かなりシンプルな処理だったのですがコードがやや多い印象で、より複雑な処理になると結構大きいJSONになりそうで、JSONなのでコメント書けないとか少し大変な部分が出て来るのかもしれません。

バージョン管理を考えるとCliでの処理で運用したほうが良さそうですが、こういったサービスはGUIでの良さもあるのでどちらに比重を置いた運用がいいかは考慮が必要かもです

本文中で使ったカウンタのステート情報はDynamoDBなどに入れた方が良いかもです。

マイクロサービス化しやすいので、極力本来の処理のロジックをLambda側にやらせてそれ以外のフロー処理(分岐とかカウンタインクリメントとか)をJSONで書くのがいいと思います。今回カウンタはLambdaでやってしまいましたが。

ログはCloudWatchLogsに出ます

To Be Continue

TODO

参考

StepFunctions BlackBelt資料
https://www.slideshare.net/AmazonWebServicesJapan/20170726-black-beltstepfunctions-78267693

続きを読む

TerraformをアップグレードしたらVPC周りで怒られた件

Terraformのバージョンをv0.10.7からv0.11.2にアップグレードしたら、planで以下のようなエラーが発生しました。

% terraform plan
 ------------------------------------------------------------------------

Warning: output "database_subnet_group": must use splat syntax to access aws_db_subnet_group.database attribute "id", because it has "count" set; use aws_db_subnet_group.database.*.id to obtain a list of the attributes across all instances



Warning: output "elasticache_subnet_group": must use splat syntax to access aws_elasticache_subnet_group.elasticache attribute "id", because it has "count" set; use aws_elasticache_subnet_group.elasticache.*.id to obtain a list of the attributes across all instances



Warning: output "igw_id": must use splat syntax to access aws_internet_gateway.mod attribute "id", because it has "count" set; use aws_internet_gateway.mod.*.id to obtain a list of the attributes across all instances



Warning: output "vpc_endpoint_s3_id": must use splat syntax to access aws_vpc_endpoint.s3 attribute "id", because it has "count" set; use aws_vpc_endpoint.s3.*.id to obtain a list of the attributes across all instances



Warning: output "vpc_endpoint_dynamodb_id": must use splat syntax to access aws_vpc_endpoint.dynamodb attribute "id", because it has "count" set; use aws_vpc_endpoint.dynamodb.*.id to obtain a list of the attributes across all instances



Error: Error running plan: 4 error(s) occurred:

* module.vpc.output.vpc_endpoint_dynamodb_id: Resource 'aws_vpc_endpoint.dynamodb' not found for variable 'aws_vpc_endpoint.dynamodb.id'
* module.vpc.output.vpc_endpoint_s3_id: Resource 'aws_vpc_endpoint.s3' not found for variable 'aws_vpc_endpoint.s3.id'
* module.vpc.output.elasticache_subnet_group: Resource 'aws_elasticache_subnet_group.elasticache' not found for variable 'aws_elasticache_subnet_group.elasticache.id'
* module.vpc.output.database_subnet_group: Resource 'aws_db_subnet_group.database' not found for variable 'aws_db_subnet_group.database.id'

でも今回エラーとなった構文は、 .tfstate の中にのみ存在する記述だったりして、しばらく悩んでしまいました(dynamodbとか使ってないしw。

原因はどうやら利用していたVPCのモジュールが [DEPRECATED] になった事でした。

利用していたvpcモジュール : terraform-community-modules/tf_aws_vpc

[DEPRECATED] Use https://github.com/terraform-aws-modules/terraform-aws-vpc

なのでvpcモジュール利用箇所を

 module "vpc" {
-  source = "github.com/terraform-community-modules/tf_aws_vpc"
+  source = "terraform-aws-modules/vpc/aws"

   name = "vega_vpc_${var.env}"

のように修正し terraform plan し直したら上述のWarningとErrorが全部消えました。

めでたしめでたし。

ただ、VPC系モジュールが軒並み~ update in-place または -/+ destroy and then create replacement になってしまったのでaplyする際は注意が必要です。

私の場合、たまたまv0.10からv0.11にアップグレードしたタイミングで発覚しただけですが同様のエラーでお困りの人向けにまとめてみました。

続きを読む

大きいところだとそこに穴開けるとかすごいハードル高そう。完全クローズドにしないと。

こちらの記事に対するDy66さんのコメントです → 「おっ、と思ったけどDynamoDBってAWSよね?大きいところだとそこに穴開けるとかすごいハードル高そう。完全クローズドにしないと。」 続きを読む

MonacaからPhoneGap Builderへ移行

なぜ切り替えるに至ったか

Monacaを使っていたものの、公開版を作成するためには有料プログラムにしなくてはならない。
使い始めてた時からわかっていたもの、便利だったのでずーっと問題を放置してきた。
しかし、いよいよ自分が作成したアプリが完成に近づいてきてビルド回数に制約があることも踏まえ、切り替えるのも自分のスキルアップにつながるかなと思ってやってみた。

まとめると

・Monacaありがとう
・riot,AWS,phinaのコンビネーションでもMonacaもPhoneGap Buildもできました
・でも、はまるところもあります

以下、自分の備忘録に近いところもあるけど、吐き出しておく。

Monacaはアプリ開発を加速します

そもそも切り替えるかどうか悩んだのは、やっぱり便利だから。
開発で悩みを減らす、そのことをビジネスにしているのだからそれはそれでいいのだと思う。
ホントありがたいサービスです。
これがなかったら、私はここまでこれませんでした。
費用面がクリアできるなら、Monacaで開発したほうがいいです。
私のように3連休まるまるこの問題解決に使うことなく開発に専念できます。

では切り替えますか…何に?

Monacaはcordovaをベースにしてることは知ってます。
でも、みなさんphonegapに行きつくわけですね。
そこからして無知な私は調べるしかなくて。
調べてた途中でtelerik platformなるサービスも見つけたが、2018年5月でサービスやめるっていうことで、マイグレーションガイドがあってこれが非常にありがたかった。これみれば、イマドキこういうサービスがあるんだと理解できた。やっぱりcordovaかphonegapの2択なんだとわかったので。

クリップボード01.jpg

その2択を調べてみた1ところ…やっぱりよくわからないけど、PhoneGap Buildを使わないと最終的なビルドが面倒そうに見えたので、phonegapにしてみることにした。

Phonegap Builderに切り替えようとしたけど

というわけで、また調べると過去にアプローチした方のを参考にすることにした2

参考にしたいものの、ギャップが大きすぎた。
ここで、今回のアプリと自分がこれまでどうやって開発してきたかを簡単に説明すると…
・riot.js+phina.js+aws(DynamoDB,Coginto Userpool)で作られたゲーム
 ・一部をのぞきcdn参照。
・開発は自分の自宅PC(windows10)
・IDE?そんなもん知らんなぁ(テキストエディタで編集してIISサーバ経由で動作確認)
 ※IISサーバ経由なのはriotが求めるから(XSSが…というのでそれはそれで仕方ない)。
 ※ほんとはpercel出たあたりに期待したのだけど、phinaとの相性3で断念。

そもそもMonacaで動くのか?

つまり、そもそもMonacaですらまともに動くかどうかわからない状態。
というわけで、まずはMonacaに移植することにした。

案の定動かないわけです。
エラーログもはかずに、背景色のみ。
CDNなところはすべて取っ払い、必要なコンポーネントを全部追加して、パスも通しても動かず。

理屈的にはコンポーネントを追加すれば動くはずなんだけど…
今回は必要なライブラリはダウンロードして取り込むことにしました。

回り道したようにも思えましたが、結果として「Androidアプリ設定」などその後の作業がイメージしやすかったです。

動いちゃうと案外満足しちゃうもので、やっぱこのまま…とも一瞬思いました。

いよいよMonacaから旅立つ

でも、ビルドに制約があるので…改めて手順にしたがってphonegapから。
あらかじめnode.js,npmは使える環境にはしていたので、そこは割愛。

1.Phonegapのインストール

npm install -g phonegap

まぁ普通。npm install -g phonegapしたかったので、管理者権限でやったことぐらい?

2.スマホにデバッグツールをインストール

インストール自体は悩まないのだけど…後述しますが、ネットワーク連係するアプリの場合あまり使えないのです…

3.プロジェクトを作成

phonegap create <AppName> --template blank

ここも特にひねりはない。参考例もblankにしているが、下手にhelloworld入っているよりわかりやすいんじゃないかなと思いました。

4.開発

今回はあらかじめ開発してあるので、www以下に開発した成果物を放り込む。
その際に以下を残せというから残した。

<script type="text/javascript" src="cordova.js"></script>

5.モバイルでの確認

$ phonegap serve android

できるんだ!と思ってやってみたところ…できた!
ここまできたら、2.で導入したデバッカーで動作させようとしたところ…

ちーっとも接続しない。
そもそもPC→スマホって接続できてないのでは?
と思って、昔でいうLANボード2枚刺しみたいなことをしようと調べたら…
いまさら SoftAP を使用してWindowsノートマシンを無線親機にする

これこれ。ありました。この手順にしたがって、PCを無線親機化して、スマホ側で認証することでようやく接続できました。
で、画面が表示されてボタンも反応して一安心、と思ってログオン機能を試したところ動かない。
サーバ側のログをみても無反応。

何がまずいのかわからなかったので、またネットでググると
Cordova、IonicでHTTP通信できない・画像とかが読み込めないときの対処法3選
これのホワイトリストの設定ミスの項をみて、そもそもwhitelistのプラグイン入れてない!と気がづき、入れました。

今度は…と思ったら「認証できません」というあらかじめアプリで仕込んだメッセージが。
なんでなん?
と思ってサーバ側のエラーログを見たら

NetworkingError: Network Failure

このキーワードで調べるとCORSConfigurationにぶち当たるのだが、これが曲者。
S3の話はそれで解決するのかもしれないが、実際のところはサーバ側(ここでいえばphonegap serve)の問題とのこと。
(この裏どりだけが再検索しても出てこない…英語のページでnot client sideというようなキーワードがあったはず。)

つまりは

$ cordova serve android

した後、自PCで

http://localhost:3000/

して確認したら、あとは信じるしかない…と割り切って先に進んだ。

6.プラグイン

ここもまぁ普通。あまり悩むことはない。

7.設定の修正

ここはMonacaを経由したおかげで何をすべきかが分かった。
むしろここを見たほうがいい。

8.アイコンの変更

Favicon Generator
である程度作ってくれるのだが、最新のiosに対応してないので自力で作らないところがあるのがつらい。

9.PhoneGap Buildでアプリを作成する

ようやくここまできました。
PhoneGap Build

まずはadobeのIDがなかったので作りました。まぁ悩むところはないが、パスワードの制約が面倒。
そして、やっぱりgithubが必要だというので、これまで避けてきたけどgithubもアカウント作成。

githubを使うのも初めてだったのでプログラムソースのアップロードに四苦八苦。
結局webから登録可能だと知ったのが、調べ始めてから3時間後。
無題.jpg

ネットを見ると、gitから登録する手順はたくさん載っているが、こんなシンプルな方法がなかなか出てない。当たり前だからか?

そこに至るまでにgithubのデスクトップ版入れたりしたが、どうにも使いづらいうえに案外ネットにも情報がない。
結局git for windowsを入れたほうがシンプルで分かりやすい。

10.PhoneGap Buildに鍵を登録する

これも書いてあるとおりなのだが、javaも必要なのか…てかkeytoolだけが必要なんだけど

keytool -genkey -v -keystore [keystore_name].keystore -alias [alias_name] -keyalg RSA -keysize 2048 -validity 10000

このときのエイリアス名、のちに使うので慎重に。

無題.jpg

「アカウントを編集」-「署名キー」-「キーを追加」
このときにタイトルはどうでもいいけど、エイリアスはkeytoolで作成したときに -alias [alias_name]で指定した値と一致しないとエラーになる。(←エラーとなったからわかる。)

果たして動くのか?

そうなんです、ここまで作業したものの最終的に実機確認ができてないので不安でしたが…
動きましたー
・aws cognito userpoolを使った認証
・aws DynamoDBを使ったデータ取得、更新
・riot.js+routerを使ったフレームワーク
・phina.jsの動作
すべて問題なし。

ただ反応が鈍いかな…PC(corei7)とスマホ(arrowsM02)を比較しちゃダメなのかな。

続きを読む

AWS Mobile Hub による react-native アプリ開発(Cloud API REST呼び出し)

AWS mobile-hub が react-native に対応したとのことなので、マニュアル (Developer Guide) を元にサンプルアプリを作成・試してみる。

今回は Dynamodb にてテーブルを作成し、CRUDアプリケーションをサンプルに沿って作成してみた。

はじめに

前回の記事から Dynamodb に登録・参照する簡単なサンプルプログラムを実行してみる。

Backend (Mobile Hub) の設定

テーブルの作成

コマンドから下記を実行

awsmobile database enable --prompt

マニュアル通りの Notes テーブルを作成

Welcome to NoSQL database wizard
You will be asked a series of questions to help determine how to best construct your NoSQL database table.

? Should the data of this table be open or restricted by user? Open
? Table name Notes

You can now add columns to the table.

? What would you like to name this column NoteId
? Choose the data type string
? Would you like to add another column Yes
? What would you like to name this column NoteTitle
? Choose the data type string
? Would you like to add another column Yes
? What would you like to name this column NoteContent
? Choose the data type string
? Would you like to add another column No


Before you create the database, you must specify how items in your table are uniquely organized. This is done by specifying a Primary key. The primary key uniquely identifies each item in the table, so that no two items can have the same key.
This could be and individual column or a combination that has "primary key" and a "sort key".
To learn more about primary key:
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey


? Select primary key NoteId
? Select sort key (No Sort Key)

You can optionally add global secondary indexes for this table. These are useful when running queries defined by a different column than the primary key.
To learn more about indexes:
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes

? Add index No
Table Notes saved

CRUD API の作成

下記コマンドにて cloud-api を作成

awsmobile cloud-api enable --prompt

先ほど作成した Notes テーブルから生成を選択。ログイン必須を選択した。

This feature will create an API using Amazon API Gateway and AWS Lambda. You can optionally have the lambda function perform CRUD operations against your Amazon DynamoDB table.


? Select from one of the choices below. Create CRUD API for an existing Amazon DynamoDB table
? Select Amazon DynamoDB table to connect to a CRUD API Notes
? Restrict API access to signed-in users Yes
Adding lambda function code on: 
/Users/masanao/dev/awsmobile/awsmobilejs/backend/cloud-api/Notes/
...
Path to be used on API for get and remove an object should be like:
/Notes/object/:NoteId

Path to be used on API for list objects on get method should be like:
/Notes/:NoteId

JSON to be used as data on put request should be like:
{
  "NoteTitle": "INSERT VALUE HERE",
  "NoteContent": "INSERT VALUE HERE",
  "NoteId": "INSERT VALUE HERE"
}
To test the api from the command line (after awsmobile push) use this commands
awsmobile cloud-api invoke NotesCRUD <method> <path> [init]
Api NotesCRUD saved

設定の反映

下記コマンドにて AWS のモジュールを作成

awsmobile push

作成に少し時間がかかるので、完了するのを待つ。

building backend
   building cloud-api
      zipping Notes
   generating backend project content
   backend project content generation successful
done, build artifacts are saved at: 
/Users/masanao/dev/awsmobile/awsmobilejs/.awsmobile/backend-build

preparing for backend project update: awsmobile
   uploading Notes-20180108140947.zip
   upload Successful  Notes-20180108140947.zip
done

updating backend project: awsmobile
awsmobile update api call returned with no error
waiting for the formation of cloud-api to complete
cloud-api update finished with status code: CREATE_COMPLETE

Successfully updated the backend awsmobile project: awsmobile

retrieving the latest backend awsmobile project information
awsmobile project's details logged at: awsmobilejs/#current-backend-info/backend-details.json
awsmobile project's access information logged at: awsmobilejs/#current-backend-info/aws-exports.js
awsmobile project's access information copied to: aws-exports.js
awsmobile project's specifications logged at: awsmobilejs/#current-backend-info/mobile-hub-project.yml
contents in #current-backend-info/ is synchronized with the latest in the aws cloud

フロントエンドの構築

App.js を編集・CRUDのコードを書く。

モジュール import

import { API } from 'aws-amplify-react-native';

CRUDコードの記述

サンプルコードをそのまま貼り付けすることとした。

state の追加

state = {
  apiResponse: null,
  noteId: ''
     };

  handleChangeNoteId = (event) => {
        this.setState({noteId: event});
}

saveNote 関数の追加

  // Create a new Note according to the columns we defined earlier
    async saveNote() {
      let newNote = {
        body: {
          "NoteTitle": "My first note!",
          "NoteContent": "This is so cool!",
          "NoteId": this.state.noteId
        }
      }
      const path = "/Notes";

      // Use the API module to save the note to the database
      try {
        const apiResponse = await API.put("NotesCRUD", path, newNote)
        console.log("response from saving note: " + apiResponse);
        this.setState({apiResponse});
      } catch (e) {
        console.log(e);
      }
    }

getNote 関数の追加

  // noteId is the primary key of the particular record you want to fetch
      async getNote() {
        const path = "/Notes/object/" + this.state.noteId;
        try {
          const apiResponse = await API.get("NotesCRUD", path);
          console.log("response from getting note: " + apiResponse);
          this.setState({apiResponse});
        } catch (e) {
          console.log(e);
        }
      }

deleteNote 関数の追加

  // noteId is the NoteId of the particular record you want to delete
      async deleteNote() {
        const path = "/Notes/object/" + this.state.noteId;
        try {
          const apiResponse = await API.del("NotesCRUD", path);
          console.log("response from deleteing note: " + apiResponse);
          this.setState({apiResponse});
        } catch (e) {
          console.log(e);
        }
      }

rennder 関数の変更(Viewの変更)

<View style={styles.container}>
        <Text>Response: {this.state.apiResponse && JSON.stringify(this.state.apiResponse)}</Text>
        <Button title="Save Note" onPress={this.saveNote.bind(this)} />
        <Button title="Get Note" onPress={this.getNote.bind(this)} />
        <Button title="Delete Note" onPress={this.deleteNote.bind(this)} />
        <TextInput style={styles.textInput} autoCapitalize='none' onChangeText={this.handleChangeNoteId}/>
</View>

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  textInput: {
      margin: 15,
      height: 30,
      width: 200,
      borderWidth: 1,
      color: 'green',
      fontSize: 20,
      backgroundColor: 'black'
   }
});

作成したApp.js ソース

react-native のサンプルソースを元にしたため、一部不要なソースコードがありますがそのままにしています。

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import Amplify from 'aws-amplify-react-native';
import { API } from 'aws-amplify-react-native';
import { withAuthenticator } from 'aws-amplify-react-native';
import aws_exports from './aws-exports';

Amplify.configure(aws_exports);

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  Button,
  TextInput,
  View
} from 'react-native';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,n' +
    'Cmd+D or shake for dev menu',
  android: 'Double tap R on your keyboard to reload,n' +
    'Shake or press menu button for dev menu',
});

class App extends Component<{}> {
  constructor(props) {
        super(props);
    this.state = {
      apiResponse: null,
      noteId: ''
    };
  }

  handleChangeNoteId = (event) => {
        this.setState({noteId: event});
  }

  // Create a new Note according to the columns we defined earlier
    async saveNote() {
      let newNote = {
        body: {
          "NoteTitle": "My first note!",
          "NoteContent": "This is so cool!",
          "NoteId": this.state.noteId
        }
      }
      const path = "/Notes";

      // Use the API module to save the note to the database
      try {
        const apiResponse = await API.put("NotesCRUD", path, newNote)
        console.log("response from saving note: " + apiResponse);
        this.setState({apiResponse});
      } catch (e) {
        console.log(e);
      }
    }
     // noteId is the primary key of the particular record you want to fetch
      async getNote() {
        const path = "/Notes/object/" + this.state.noteId;
        try {
          const apiResponse = await API.get("NotesCRUD", path);
          console.log("response from getting note: " + apiResponse);
          this.setState({apiResponse});
        } catch (e) {
          console.log(e);
        }
      }

     // noteId is the NoteId of the particular record you want to delete
      async deleteNote() {
        const path = "/Notes/object/" + this.state.noteId;
        try {
          const apiResponse = await API.del("NotesCRUD", path);
          console.log("response from deleteing note: " + apiResponse);
          this.setState({apiResponse});
        } catch (e) {
          console.log(e);
        }
      }

  render() {
    return (
<View style={styles.container}>
        <Text>Response: {this.state.apiResponse && JSON.stringify(this.state.apiResponse)}</Text>
        <Button title="Save Note" onPress={this.saveNote.bind(this)} />
        <Button title="Get Note" onPress={this.getNote.bind(this)} />
        <Button title="Delete Note" onPress={this.deleteNote.bind(this)} />
        <TextInput style={styles.textInput} autoCapitalize='none' onChangeText={this.handleChangeNoteId}/>
</View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
  textInput: {
      margin: 15,
      height: 30,
      width: 200,
      borderWidth: 1,
      color: 'green',
      fontSize: 20,
      backgroundColor: 'black'
   }
});
export default withAuthenticator(App);

実行

react-native run-ios

スクリーンショット 2018-01-08 18.51.18.png

テキストボックス内に NoteID を入力して “Save Note” を押すと、保存されます。

内容はソースコードに書いてあるように固定文字が登録されます。
あくまでサンプルソースということで・・・。

AWS 確認

Mobile Hub の Cloud Logic には下記のように登録されています。

スクリーンショット 2018-01-08 19.01.38.png

Lambda も確認すると、NotesCRUD-xxxx という名前で登録されているのが確認できます。

スクリーンショット 2018-01-08 19.02.59.png

DynamoDB も同様にテーブルが作成され、データも登録されているのがわかります。

スクリーンショット 2018-01-08 19.24.53.png

通常のCRUD であればほぼノンプログラミングでバックエンドロジックが生成できますね。

続きを読む