カテゴリー: AWS Page 1 of 2

AWS Chatbot(beta版)を用いたSlack通知

クラウド事業部 インフラエンジニアの吉岡です。

AWS Chatbot(beta版)を用いたSlack通知を試してみましたので、ご紹介したいと思います。

AWS Chatbotとはどういった機能なのか

Slack チャンネルや Amazon Chime チャットルームに対して通知を行う機能です。
サポートしているサービスの通知をSNSトピックに転送し、
SNSトピックからChatbotを経由して各チャットサービスに表示することができます。
現在対応しているサービスは下記の通りです。

AWS Billing and Cost Managemen
AWS CloudFormation
AWS CodeBuild等の開発者用ツール
Amazon CloudWatch Alarms
Amazon CloudWatch Events
AWS Config
AWS Health
AWS Security Hub
Amazon GuardDuty
AWS Systems Manager

今回はAWS System ManagerのRunCommandを実行し、Slackに通知します。設定する通知フローは下記の図の通りです。

Slackへの通知フロー

手順

・SNSトピックの作成
Amazon SNS→トピック→トピックの作成と進み、適当な名前を付けたトピックを作成します。(サブスクリプションはまだ設定しません。)

f:id:seeds-std:20191114181702p:plain

・CloudWatchEventの設定
CloudWatch→ルールの作成、と進みルールを作成します。
今回はchatbot-testという名前で作成しました。

サービス名:EC2 Simple Systems Manager(SSM)
f:id:seeds-std:20191114182349p:plain

イベントタイプ:すべてのイベント
f:id:seeds-std:20191114182353p:plain

ターゲット:SNSトピック(先ほど作成したトピック)
f:id:seeds-std:20191114182357p:plain

・Chatbotのインストール
AWS Chatbotを選択し、
Chatbotの設定を始めていきます。
通知を送る先となるChatツールを選択します。

f:id:seeds-std:20191114182950p:plain

SlackにChatbotをインストールします。

f:id:seeds-std:20191114183017p:plain

今回はpublicのslacktestというチャンネルに通知するように設定します。
IAMロールは自動作成、ロール名はchatbot-slackとしました。
SNS regionはSNSトピックの設定されているリージョン(今回は東京リージョン)
SNS topicsは先ほど作成したchatbot-testを指定します。

f:id:seeds-std:20191114183152p:plain
f:id:seeds-std:20191114183155p:plain
f:id:seeds-std:20191114183157p:plain

設定が完了すると、SNSトピックにサブスクリプションが追加されていることが確認できます。

動作確認

RunCommandを実行し、Slackに通知されることを確認します。
今回はAWS-RunShellScriptでechoコマンドを実行します。
インスタンス指定
SNS通知なし
の条件で下記の内容で実行します。

#!/bin/bash
echo "Hello chatbot"

実行後に下記の様な通知がSlackに届いていることが確認できました。

f:id:seeds-std:20191114183614p:plain

あとがき

今回はChatbotを用いてSlack通知を行うために必要な作業がどういったものか、勉強を兼ねて試してみましたが、非常に簡単に設定ができました。
現在はbeta版ですので、一部の環境のみですが、シーズではこの機能を利用してAWS Health Dashboardの情報をslack通知する設定を行っています。

CloudTrailが有効化されていることが前提ですが、セッションマネージャーを使ったログイン時に通知が届くような設定もできるようです。
正式なリリースが楽しみですね!

Kotlin/JSのAWS Lambda関数でJsonを返してみる

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 Promiseを利用して非同期に処理するようにしてみました
今回はこのLambda関数で kotlinx.serialization を用いてJsonを返すようにしてみたいと思います

目次

過去の記事

なぜ kotlinx.serialization?

まず kotlinx.serialization を用いる理由です
ここまでの記事を読んでくださっている方々であれば “わざわざないでしょ” と思っている方もいると思います
その通りで, json()JSON.stringifiy() 関数を用いればJsonを返すことができます
しかし, kotlinx.serialization を用いることにより, Client側でも同じクラスを利用することができ, 開発する際に便利であるといえます
またマルチプラットフォームであるため様々な環境で利用できるというのもメリットです

注意事項

この記事では kotlinx.serialization に関する詳しい解説は行いません
詳しく知りたい方は下記のリポジトリをご覧ください
github.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.promise
import kotlin.js.Json
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val tasks = (1..10).map {
async {
delay(1000)
it
}
}
val results = tasks.awaitAll()
return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

Jsonを返すようにする

目標

今回は下記のようなJsonを返すようにしてみます

{
"id": 0, // ID
"text": "", // 文字列
"random_numbers": [5, 38, 24] // 適当な数値
}

依存関係を追加する

kotlinx.serialization は外部ライブラリですので依存関係を追加する必要があります
また, 専用のプラグインも必要であるためプラグインの追加も行います

build.gradle.kts / pluginsブロック

plugins {
kotlin("js") version "1.3.50"
kotlin("plugin.serialization") version "1.3.50"
}

依存関係の追加

build.gradle.kts / dependenciesブロック

sourceSets["main"].dependencies {
implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.13.0") // 追加
}

リポジトリに jcenter を追加する必要があるので追加します

build.gradle.kts / repositories

repositories {
mavenCentral()
jcenter() // 追加
}

クライアント側に返すクラスを作成する

作成するクラスが今回のJsonに変換するクラスとなるので各種アノテーションも付与します
今回は Responseクラス の下に作成します(本当は別ファイルにするほうがいいです)

handler.kt

@Serializable
data class ResponseBody(
@SerialName("id")
val id: Int = 0,
@SerialName("text")
val text: String = "",
@SerialName("random_numbers")
val randomNumbers: List<Int> = emptyList()
)
  • @Serializable これがついているクラスがシリアライズ/デシリアライズすることができます
  • @SerialName(String) 引数で決められた文字列がkeyとなるようになります, ない場合は変数名が利用されるようになるのでなくても問題ないです

Jsonを返す準備をする

まずは前回までに書いていたコードの一部を削除しスッキリさせます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
return@promise Response(body = "")
}

続いて, 先ほど作ったクラスを文字列に変換するために kotlinxのJsonオブジェクト を作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable) // 追加
return@promise Response(body = "")
}

⚠注意1
今回の記事ではすでにkotlinが実装している Jsonクラス をインポートしているため同じ名前であるkotlinxの Jsonクラス をインポートできません
kotlinのJsonkotlinxのJson とを間違わないよう注意してください

⚠注意2
JsonConfiguration には Stable 以外の設定もありますが, 実験的機能となっていますので利用はお勧めしません

Jsonを返す

まずは先ほど作成した ResponseBodyクラス のオブジェクトを作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) }) // 追加
return@promise Response(body = "")
}

最後に stringify関数 を用いてクラスを文字列に変換し レスポンスのbody に設定します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) })
return@promise Response(body = json.stringify(ResponseBody.serializer(), responseBody)) // 変更
}
  • ResponseBody.serializer() この関数は @Serializable アノテーションがついているクラスの場合コンパイラが自動的に作成してくれます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし {"id":1,"text":"Lambda Json!!!","random_numbers":[ランダムな数字が3つ]} 表示されれば成功です

最後に

いかがでしたでしょうか
LambdaでJsonを返すのはありがちです
これを利用することによりKotlinで作成するマルチプラットフォームなアプリケーションではかなり強力になりうると考えます
また, ここまで4つの記事に分け Kotlin/JS をもちいて AWS Lambda関数 を作って便利にしてみました
これがベストプラクティスかどうかはプロジェクトによると思いますが、1つの参考になるとうれしい限りです
ここまで読んでいただきありがとうございました


蛇足

JSON.stringify() を使ってJsonを返してみる

JavaScriptにもとから存在する, JSON.stringify を用いてもJsonを返すことができます

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = ResponseBody(1, "Lambda Json!!!", listOf(0, 1, 2))
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","randomNumbers":[0,1,2]}

しかしこれではJsonのKeyは指定できないので変数をすべて private に変更すると結果が変わるので注意が必要です

privateに変更したときの実行結果

{"id_0":1,"text_0":"Lambda Json!!!","randomNumbers_0":[0,1,2]}

こういうケースもあるため kotlinx.serialization を用いることをお勧めします

json() と JSON.stringify() を使ってJsonを返してみる

一番オーソドックスでわかりやすい返し方だと私は思います

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = json(
"id" to 0,
"text" to "Lambda Json!!!",
"random_numbers" to listOf(0, 1, 2)
)
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","random_numbers":[0,1,2]}

蛇足まで読んでいただきありがとうございました

AWSソリューションアーキテクトアソシエイト(SAA-C01)に合格しました

f:id:seeds-std:20191021181435p:plain

こんにちは。小國です。

社員の AWS 認定の取得が相次いでいるシーズです。私も 1ヶ月ほど前になりますが、AWSソリューションアーキテクトアソシエイト(SAA-C01)に無事に合格できました。

AWSソリューションアーキテクトアソシエイトを目指す方の参考になればと思い、その時の勉強方法などを紹介します。

まずは自己紹介

  • ソフトウェアエンジニア 12年(主に LAMP 環境で CakePHP、Laravel、他に Android もちょっとできます)
  • 応用情報技術者試験(AP)
  • LAMP や LEMP 程度なら構築できます(ネットワークは要勉強)
  • 過去に LPIC-1(LPIC-2 は前半だけ合格。。。)
  • とりあえず、なんでもやってみる人

勉強前の AWS の知識

AWS は EC2、VPC、RDS のチュートリアル*1を触ったことがある程度で、その他の各種サービスについては以下のような認識でした。

  • S3、聞いたことある
  • ACM、???
  • CloudFront、聞いたことある
  • ELB、聞いたことある
  • リージョン・AZ・エッジロケーション、微妙。。。
  • Lambda、ランブラ?(後にラムダと読むことを知る)
  • DynamoDB、???

勉強方法

勉強期間は約 2週間で、以下の 2冊の本をやりきりました。

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

  • 作者: NRIネットコム株式会社,佐々木拓郎,林晋一郎,金澤圭
  • 出版社/メーカー: SBクリエイティブ
  • 発売日: 2019/04/20
  • メディア: 単行本
  • この商品を含むブログを見る

AWS の主要なサービスについて要点をがしっかりとまとまっていて、わかりやすくとてもためになりました。章末や模擬試験の解答の解説も丁寧で良かったと思います。

勉強方法はというと、一通り目を通し、章末や模擬試験をすべて正解するまで(間違っている箇所はどこが間違っているか分かるまで)やりました。

おそらく 4、5回はやったかと思います。

徹底攻略 AWS認定 ソリューションアーキテクト – アソシエイト教科書

徹底攻略 AWS認定 ソリューションアーキテクト ? アソシエイト教科書

徹底攻略 AWS認定 ソリューションアーキテクト ? アソシエイト教科書

  • 作者: ??部昭寛,宮?光平,菖蒲淳司,株式会社ソキウス・ジャパン
  • 出版社/メーカー: インプレス
  • 発売日: 2019/01/18
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

1冊では不安だったので、こちらも購入しました。

こちらは、問題を解いて間違えた箇所について、本で調べました。こちらの本も同様に、すべて正解するまでやりました。

先に購入した対策本で理解ができていなかった箇所がカバーできたのでよかったと思いますが、解答の解説が少なかったように思います。

模擬試験

本試験の 2日前ほどに受けました。思っていたより簡単で、結果も 84%ほどの正答率でした。

いよいよ本試験!

本試験ですが、模擬試験に比べ文章も長く・問題も単純なものが少なく、難しかったです。

40分ほど残して一通り解答し、残りの時間いっぱいを使って見直しをしました。終わったあとはすごく頭が痛かったです。頭がしっかり働く午前中に受験したのが幸いでした。

問題文は、VPC や ELB のネットワークの複合的な問題や、DynanoDB、暗号化についての問題が多かった印象です。

感想

とりあえずは、AWSソリューションアーキテクトアソシエイト(SAA-C01)を取得できよかったです。

ただし、実際に AWS を使いこなせるかは甚だ疑問で、実際に手を動かしてみて知識・技術の定着をして、業務に役に立てるようやっていきたいです。

また、相当の難易度だと聞く、プロフェッショナル取得を目指せるよう勉強したいなと。。。

余談ではありますが、弊社 CTO の、原口さんこと cs_sonar さんが、AWSソリューションアーキテクトプロフェッショナル(SAP-C01)に合格されてます。

プロフェッショナルを目指す方は、ご参考にしてはいかがでしょうか。

blog.seeds-std.co.jp

Kotlin/JSのAWS Lambda関数でPromiseを使うようにする

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 では dyanmic の利用をなくすようにしました
そして今回は, Callbackの呼び出しをやめて, JavaScriptの非同期処理( Promise )になるよう変更したいと思います
また, Promiseスタンダード なものと Coroutines のものの2種類がありますので, この記事ではその両方を紹介したいと思います

目次

過去の記事

なぜ非同期に?

非同期処理することにより複数の処理を同時に実行することができます
例えば, 複数のファイルを受け取り, S3に保存する といったことを考えてみます
非同期処理ではない場合, 1ずつしかファイルのアップロードができません
非同期処理の場合, 複数のファイルを同時にアップロードできるようになるので時間の短縮が図れます

注意事項

この記事では Promise の詳しい解説は行いません
詳しく知りたい方は下記の記事を参考にしてください
qiita.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin
import kotlin.js.Json
import kotlin.js.Promise
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

スタンダードなPromise

まずはkotlin-stdlibに実装されている Promise を利用してみます

関数の書き換え

handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
executor(Response(body = "Hello from Kotlin Async Lambda!"))
}
  • executor は処理成功時に実行する関数で, 引数はジェネリクスで指定したクラス (この記事では Response ) となります
  • reject は処理失敗時に実行する関数で, 引数は Throwable クラスのオブジェクトとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Async Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(スタンダード)

今度はNode.jsの setTimeout を利用して本当に非同期に処理されているかを確認したいと思います
その前に, Kotlin/JSにはNode.jsの setTimeout は定義されていないので追加します
今回追加する場所はコードの最下部に追加します

handler.kt

external fun setTimeout(callback: dynamic, delay: Int, vararg args: Any?): Int
external fun clearTimeout(timeout: Int)
  • external : 外部の関数/変数を利用するために記述します。トランスパイルした場合にそのまま残るようになります
  • callback の部分が dynamic となっていますが, 関数の引数が固定値ではないためやむを得ず dynamic としています

続いて handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
val results = (1..10).map {
Promise<Int> { childExecutor, _ ->
setTimeout({childExecutor(it)}, 1000)
}
}
Promise.all(results.toTypedArray()).then {
executor(Response(body = "Result: ${it.sum()} - Standard"))
}
}

このコードでは 1秒待ってから数字を返す というコードを10回繰り返すものとなっています

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Standard と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

通常の処理であれば 1秒待って数字を返す を10回行えば10秒かかるはずですが, 非同期に実行されているため約1秒で完了します

CoroutinesのPromise

標準実装の Promise では見にくく感じませんでしたか?(私は少なからず見にくいなと感じました)
非同期処理を書くのであれば, Node.jsみたいに async で書きたいところです
そこで使うのが kotlinx.coroutines です

依存関係を追加する

kotlinx.coroutines は外部ライブラリなので build.gradle.kts に依存関係を追加する必要があります

build.gradle.kts

implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2") // 追加

コードを書き換える

まずはただの文字列を返すように handelr 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
return@promise Response(body = "Hello from Kotlin Coroutines Lambda!")
}

先ほどのコードと比較するとかなり単調なものになりました
※エラーを返す際は throw NullPointerException() のように throw するだけとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Coroutines Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(Coroutines)

先ほどと同じ関数を実装してみます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val tasks = (1..10).map {
async {
delay(1000)
it
}
}
val results = tasks.awaitAll()
return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}

先ほどとは異なり, CoroutineScope 内で実行できるため, async 関数を利用できます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Coroutines と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

最後に

いかがでしたでしょうか
非同期処理が行えるとよりできることの幅が増えるかと思います
また、Coroutinesの Promise と標準実装の Promise は互換性があるため外部ライブラリが非同期処理を行う場合にも使え,
よりKotlinらしい書き方ができるようになっていくと思います
次回は kotlinx.serialization を用いてJsonのレスポンスを行う記事を書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数でJsonを返してみる

AWSソリューションアーキテクトプロフェッショナル(SAP-C01) に合格しました

f:id:seeds-std:20191010165532p:plain

クラウド事業部の原口です。

AWSソリューションアーキテクトプロフェッショナル試験を受けてきて一度不合格となった末に合格しました。
合格点は750/1000点と結構お高め…
難しかったです。 勉強を初めてから1ヶ月半ほどかかりました。

一度不合格となってますし偉そうな事は言えないのですが、こんな勉強をしました、という事を書いてみたいと思います。

試験の概要

AWS 認定ソリューションアーキテクト – プロフェッショナル
AWS プラットフォームでの分散アプリケーションおよびシステムを設計するための
高度な技術的スキルと専門知識を認定します。
AWS でのクラウドアーキテクチャの設計およびデプロイに関する 2 年以上の実務経験を持つ方が対象です。
この試験で評価する能力は次のとおりです。
• AWS における、動的なスケーラビリティ、高可用性、フォールトトレランス、
信頼性を備えたアプリケーションの設計およびデプロイ
• 与えられた要件に基づいてアプリケーションを設計およびデプロイするための、適切な AWSサービスの選択
• AWS における複雑な多層アプリケーションの移行
• AWS におけるエンタープライズ全体のスケーラブルな運用の設計およびデプロイ
• コストコントロール戦略の導入

かなりのボリュームの試験です。
AWS 認定ソリューションアーキテクト – アソシエイト の上位版に位置する問題となります。

試験結果は 100~1000 点の範囲のスコアでレポートされます。最低合格スコアは 750 点です。スコ
アによって、試験での全体的な成績と合否がわかります。スケールドスコアモデルは、難易度にわず
かな違いのある複数の試験形式のスコアを平均化するために使用されます。

100点から?という単純な疑問とスケールドスコアモデルというものがわからなかったので調べてみましたが、
あまりよくわからなかったのですが「難易度が異なる試験問題をいい感じに調整」するモデルと解釈しました。
つまり問題によって配点が違ってくる、という事ですかね?

試験時間は170分。問題数は75問ですので1問あたり2~3分程度で解く必要があります。
試験合格の有効期限は3年みたいです。

その他詳細な試験ガイドは以下となります(公式)
試験ガイド

試験勉強

私はAWSを初めて触ってからもう5年くらいは経っていると思うのですが、
業務で触れるサービスが中心となってしまい、どうしてもよくわからないサービスというのは存在しました。
そのため、いったい自分は何が苦手なのか、を把握する事からはじめました

模擬試験

模擬試験を受けて再学習が必要な領域を特定と試験の感じの把握からはじめました
サンプル問題でもわかりますが、言い回しが独特でとにかく文章が長い、という印象です

公式ドキュメントの「よくある質問」

よくある質問を読み込むのを中心に行いました。
サービスの制限など実運用を検討する方へのドキュメントである為、
サービスのネックや落とし穴となりやす部分が集約されているものと感じました。

ホワイトペーパー

DR関連のものとストレージサービス関連のものはよく読みました。
AWS Well-Architectedなどはななめ読みしたくらいです。

blackbelt

よく知らないサービスはblackbeltを見ました。
スライドをぽちぽちーとしてたんですが、ちゃんと動画で見ると理解度が段違いだったのでできれば動画がいいと思います

AWS クラウドサービス活用資料集 | サービス別資料

心構え

膨大な文章量と170分

とにかく文章が長いので単純に疲弊してきます。
集中力を長持ちさせる自分なりの方法を見つける必要がありました
疲れてくると読んでるだけで頭に入ってない事が多くてそういう時に集中しなおせる方法を持ってるといいです
秘訣について「170分くじけない心が必要」と聞いて笑ってたんですが
本当に重要ではないかと思いました

正常性バイアスとの戦い

疲労してくると「正常正バイアス」がかかりやすくなる気がしました。
問題の選択肢はすべてその問題を解決できるものであったり、もっともらしい書き方をされていて
知識が不足していると簡単に迷います。
その状態で出てくるのが以下のような考え。

・実務ではこんなやり方はしないだろう
・こういう時は枯れた技術で望むべき
・問題文に定義されてないからこういう意味に違いない

これらが出てきた時は大体知識不足が原因で明確な説明ができない以上は知識不足であると自分に言い聞かせました。
わりとここで問題の深読みまでしだすと精神的にもハマるし時間も浪費します。

サービスや機能の特性を単語で関連付ける

ストリーミングデータの処理といえばKinesis。
といった形で、仮想的だけど実務的なRFPからAWSにおける提案例を連想できるようにしました。
実際実務として提案を心がけている事もありこの方法は自分に合った勉強法でした。

短期集中で行う

長いスパンで考えるよりも2週間くらいでがーーっとやった方がよいです

その他

試験にはPSIとピアソンVUEがありますが、可能な限りピアソンVUEでの受験をおすすめします!
PSIでは監督員と遠隔でチャットをしながら、カメラで常時監視されながらの試験となり、
「キーボードから手を離してはいけない」などのプレッシャーが半端なかったです笑

ピアソンVUEでも監督員はもちろん居ますが、口元に手をあてたり、頭を掻いたりしても問題はないので
この点は心理的負担がだいぶ減ります!

感想

正直合格の文字を見た時は喜びよりほっとしました。2,3回確認しました。(笑)
社内での人権ができてよかったです。

自信もつき、試験を通じた勉強を行った事で、より深く!AWSの理解が深まったという実感があります。
これからはプロフェッショナル原口としてよりお客様にピッタリのクラウド最適化した提案ができると思います!
お困りの事がありましたら、是非ご相談下さい!!

DevOpsとかの他の試験も受けていくぞー!って思ってたのですが
本当に疲れたので、とりあえずはアイスボーンをやります。
社内でもどんどんAWS認定の取得者を増やしたいですね。
これから試験を受ける方は頑張って下さい!

宣伝枠

AWSの無料相談会というものを行っています!
www.seeds-std.co.jp

弊社ではAWSが大好きなエンジニアを募集しています!じょいなす!
recruit.seeds-std.co.jp

Kotlin/JSのAWS Lambda関数を便利にしてみる

f:id:seeds-std:20191010160615p:plain

こんにちは, Web事業部の西村です
私の前回の記事 ではKotlin/JSを用いてLambda関数を書いてみました
しかし, dynamic を利用していたため使いにくい部分もあったと思います
今回はその点を改良しより使いやすくなるよう変更したいと思います

目次

過去の記事

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 で作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

Jsonを使って便利にしてみる

dynamicをなくす

Kotlin/JSには dynamic に似た Json があります
dynamic はJavaScriptのデータであったり, オブジェクトを格納することができますが, Json はKey-Value形式でのみ格納できるものとなっています
handler関数の引数, eventcontext はどちらもJavaScriptのJsonオブジェクトですのでKotlinの Json に変換できます
また, Callbackの第二引数もJsonオブジェクトを設定するためこちらも変更できます

handler.kt

fun handler(event: dynamic, context: dynamic, callback: (Error?, dynamic) -> Unit)

↑ が ↓ に変更できます

fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit)

このJsonオブジェクトへのアクセスはMapのように利用してアクセスすることができます

event["body"] // こちらか
event.get("body") // こちらでデータの取得ができます

Callbackに渡すのJsonを関数で作れるようにする

API Gatewayでは下記のJsonを処理するようになっています *1

{
"isBase64Encoded": true|false,
"statusCode": httpStatusCode,
"headers": { "headerName": "headerValue", ... },
"multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
"body": "..."
}

こちらをもとに引数が5つの関数を作成したいと思います
記述する場所はhandler関数の下になります

handler.kt

fun responseJson(statusCode: Int = 200, body: String = "{}", isBase64Encoded: Boolean = false, headers: Json = json(), multiValueHeaders: Json = json()) = json(
"statusCode" to statusCode,
"body" to body,
"isBase64Encoded" to isBase64Encoded,
"headers" to headers,
"multiValueHeaders" to multiValueHeaders
)
  • json() Jsonを作成する関数です/引数は vararg Pair<String, Any?> となっており json() と書くと空のJsonを作ることができます

Callbackに渡すよう変更する

現在handler関数は下記のようになっていると思います

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response: dynamic = object {}
response["statusCode"] = 200
response.body = "Hello from Kotlin Lambda!"
callback(null, response)
}

この関数を先ほど作成した関数を用いた形に変更します

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response = responseJson(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}

関数をデプロイする

前回と同様の手順を用いて関数をデプロイします
画面右側の Gradle タブを開き, kotlin_lambda -> Tasks -> other の順に開き, その中の compress をダブルクリックして実行します
ビルドに成功するとプロジェクトのディレクトリに compress.zip というファイルが更新されます
この生成されたファイルをコンソールからアップロードし, 保存します

動作確認

あらかじめ作成しておいたAPI GatewayのURLにアクセスし Hello from Kotlin Json Lambda! と表示されれば成功です

レスポンスをクラスに変更する

レスポンスはクラスを用いても影響が少ないためクラスに置き換えます
リクエストにクラスを利用しない理由については蛇足をご覧ください

まずは responseJson関数 を変更します

handler.kt

fun responseJson(statusCode: Int = 200, body: String = "{}", isBase64Encoded: Boolean = false, headers: Json = json(), multiValueHeaders: Json = json()) = json(
"statusCode" to statusCode,
"body" to body,
"isBase64Encoded" to isBase64Encoded,
"headers" to headers,
"multiValueHeaders" to multiValueHeaders
)

data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

続いて, Callbackの引数を変更します

fun handler(event: dynamic, context: dynamic, callback: (Error?, Json?) -> Unit)

fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit)

最後にレスポンスを変更します

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response = responseJson(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}

この変更ができたらデプロイし動作を確認します
Hello from Kotlin Class Lambda! と表示されれば成功です

最後に

いかがでしたでしょうか
Kotlinを用いていると dynamic では少し取り扱いにくく感じてしまいます
しかし Json にしてしまうことで,Mapのように利用でき, より使いやすいのではないかと思います
次回は非同期処理にする記事を書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数でPromiseを使うようにする


蛇足

リクエストのほうもクラス使ったらだめなの?

注意事項はありますが、クラスを利用しても問題はないです

handler.kt

package jp.co.seeds_std.lambda.kotlin
import kotlin.js.Json
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Event, context: Json, callback: (Error?, Response?) -> Unit) {
println("body: ${event.body} isBase64Encoded: ${event.isBase64Encoded}")
// event.copy() // Error!
val response = Response(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}
data class Event(val body: String?, val isBase64Encoded: Boolean)
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

この時ログに bodyisBase64Encoded の出力が行われます
その後 copy() を実行しようとした場合, 変数 eventEventクラスではなく Json となっていますので, 関数が見つからず実行時にエラーが出てしまいます
また, 注意点として private な変数はトランスパイルした際に変数名が変更されてしまうためうまく利用できません
そのため, リクエストの eventcontext はJsonで, レスポンスはKotlinのクラスを利用していくことがいいのかなと私は思います
蛇足も読んでいただきありございました

AWS Lambda関数をKotlin/JSで書いてみる

f:id:seeds-std:20190925123639p:plain

WEB事業部の西村です。

唐突ですが皆さんはLambda関数を作成する際にどのような言語を用いていますでしょうか?
用途に合った言語や自分で書きやすいと思う言語などの要因で決めていると思われます
私はKotlinという言語が好きでKotlinで書こうと思ったのですが、Kotlinで書かれている記事はそのほとんどがJavaで実行することを前提とした元なっています
しかし、Javaではコールドスタートした際に実行時間がかかってしまうというデメリットがあります
KotlinはJavaだけでなくJavaScriptでも動作させることができるので今回はNode.jsで実行させる関数の作成を行いたいと思います

目次

この記事の目標

Kotlin/JSを用いたLambda関数の作成とそのレスポンス確認

この記事で取り扱わないこと

この記事では AWS LambdaAmazon API Gateway については詳しく解説いたしません
あらかじめLambda関数とその関数を呼び出すためのAPIの作成を行っておいてください

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクトの作成

プロジェクトの作成を行います
任意のディレクトリにプロジェクト用のフォルダを作成します(この記事では KotlinLambda としています)
その後そのディレクトリ内に build.gradle.ktssettings.gradle.kts の2つのファイルを作成します

KotlinLambda
├─build.gradle.kts
└─settings.gradle.kts

作成したらIntelliJ IDEAでプロジェクトのフォルダを開きます
続いて、 build.gradle.ktssettings.gradle.kts を開き下記の内容を入力します

build.gradle.kts

plugins {
kotlin("js") version "1.3.50"
}
repositories {
mavenCentral()
}
kotlin {
target {
nodejs()
useCommonJs()
}
sourceSets["main"].dependencies {
implementation(kotlin("stdlib-js"))
}
}

settings.gradle.kts

rootProject.name = "kotlin_lambda"

その後画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら準備完了です

ハンドラソースコードの追加

次にソースディレクトリを作成します
プロジェクトのディレクトリに src/main/kotlin でディレクトリを作成します
またパッケージ名を追加する場合、パッケージを作成します(この記事では jp.co.seeds_std.lambda.kotlin とします)
作成したディレクトリに handler.kt というファイルを作成し下記の内容を記述します

handler.kt

package jp.co.seeds_std.lambda.kotlin
@JsExport
@JsName("handler")
fun handler(event: dynamic, context: dynamic, callback: (Error?, dynamic) -> Unit) {
val response: dynamic = object {}
response["statusCode"] = 200
response.body = "Hello from Kotlin Lambda!"
callback(null, response)
}
  • @JsExport JavaScriptのexportを行うアノテーションです
  • @JsName(name: String) 記述したクラスや関数をトランスパイルした際 name に指定した名前となるよう変換してくれるようになります
  • dynamic JavaScriptのオブジェクトをそのまま取り扱います(. [] でその中の関数や変数にアクセスできます)

デプロイ

デプロイパッケージの作成を行います
圧縮を自動化するためのタスクを追加します
build.gradle.kts を再度開き下記の内容を追加します

build.gradle.kts

import com.google.gson.JsonParser
import java.io.OutputStream
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
plugins { ... }
repositories { ... }
kotlin { ... }
open class CompressTask : DefaultTask() {
lateinit var buildDirectory: File
lateinit var projectName: String
var rootProjectName: String? = null
private val blackLists = setOf("kotlin-test-nodejs-runner", "kotlin-test")
@TaskAction
fun compress() {
val projectPrefix = if(rootProjectName != null && rootProjectName != projectName) "$rootProjectName-" else ""
val zipFile = File(buildDirectory, "../compress.zip")
val outputStream = ZipOutputStream(zipFile.outputStream(), Charsets.UTF_8).apply {
setLevel(9)
}
val jsDir = File(buildDirectory, "js")
val projectDir = File(jsDir, "packages/$projectPrefix$projectName")
val nodeModuleDir = File(jsDir, "node_modules")
addDependencies(outputStream, File(projectDir, "package.json"), nodeModuleDir, mutableSetOf(*blackLists.toTypedArray()))
addZipEntry(outputStream, File(projectDir, "kotlin/$projectPrefix$projectName.js"))
outputStream.close()
}
private fun addZipEntry(zipOutputStream: ZipOutputStream, file: File, addDirectory: String = "") {
val name = if(addDirectory.isEmpty()) file.name else "$addDirectory/${file.name}"
if(file.isDirectory) {
file.listFiles()?.forEach {
addZipEntry(zipOutputStream, it, name)
}
} else {
val zipEntry = ZipEntry(name)
zipOutputStream.addZipEntry(zipEntry) {
it.writeFile(file)
}
}
}
private fun addDependencies(zipOutputStream: ZipOutputStream, packageJsonFile: File, nodeModuleDir: File, addedDependencies: MutableSet<String> = mutableSetOf()) {
val packageJson = JsonParser().parse(packageJsonFile.reader()).asJsonObject
if(!packageJson.has("dependencies")) return
val dependencies = packageJson.getAsJsonObject("dependencies")
dependencies.keySet().forEach {
if(!addedDependencies.contains(it)) {
addedDependencies.add(it)
val dependenciesDir = File(nodeModuleDir, it)
addZipEntry(zipOutputStream, dependenciesDir, "node_modules")
addDependencies(zipOutputStream, File(dependenciesDir, "package.json"), nodeModuleDir, addedDependencies)
}
}
}
private inline fun ZipOutputStream.addZipEntry(entry: ZipEntry, crossinline block: (OutputStream) -> Unit) {
try {
putNextEntry(entry)
block(this)
} catch (e: Exception) {
e.printStackTrace()
} finally {
closeEntry()
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun OutputStream.writeFile(file: File) {
file.inputStream().use {
it.copyTo(this)
}
}
}
tasks.register("noMainCall") {
doFirst {
kotlin.target.compilations.all { compileKotlinTask.kotlinOptions.main = "noCall" }
}
}
tasks.register<CompressTask>("compress") {
dependsOn("compileKotlinJs", "noMainCall")
this.buildDirectory = rootProject.buildDir
this.projectName = project.name
this.rootProjectName = rootProject.name
}
tasks["compileKotlinJs"].mustRunAfter("noMainCall")

追加後、画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら画面右側の Gradle タブを開きます
kotlin_lambda -> Tasks -> other の順に開き、その中の compress をダブルクリックして実行します
するとプロジェクトのディレクトリに compress.zip というファイルが生成されます
この生成されたファイルをコンソールからアップロードします
その後の ハンドラkotlin_lambda.{パッケージ名}.handler に変更します (この記事では kotlin_lambda.jp.co.seeds_std.lambda.kotlin.handler となります)
アップロードと変更が完了したら保存を行います

動作確認

あらかじめ作成しておいたAPI GatewayのURLにアクセスし Hello from Kotlin Lambda! と表示されれば成功です

最後に

いかがでしたでしょうか
Javaの時より動作も軽くなったと感じるのではないかと思います
次回はもう少し使いやすくなるような記事も書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数を便利にしてみる

Systems Manager セッションマネージャーを利用したEC2へのリモート接続

クラウド事業部の上野です。

AWSのプライベートなネットワーク(インターネット上から直接アクセスできないネットワーク)上に立てたEC2インスタンスへのsshアクセスはどのように行われていますでしょうか?よくある構成としてはパブリックなネットワーク(インターネット上からアクセスできるネットワーク)上にあるEC2インスタンスを経由してアクセスといったものがあります。俗に踏み台サーバやBastionサーバと呼ばれるものです。弊社でも踏み台サーバを用意することが多いですね。

ところがAWSにはEC2インスタンスへのアクセスをサポートするSystems Manager セッションマネージャーという機能があります。
これはEC2にインストールされているSSM エージェントを利用してリモート接続を行います。SSMエージェントを利用すればネットワーク的に接続できないEC2インスタンスへ接続することも可能ですし、sshを使わないのでsshdのサービスを停止してもアクセスすることができるという優れものです。長年サーバ管理者をやっていますが、sshdを止めれる日が来るなんて思いもしなかったです。

それでは試してみましょう。
プライベートネットワーク上にEC2インスタンスを起動し、セッションマネージャーを利用してアクセスするということをやってみたいと思います。

まず、AWSアカウント作成後に標準で準備されているVPCにプライベートネットワークを用意します。

f:id:seeds-std:20190917124214p:plain
ssm01

このプライベートネットワーク上にEC2インスタンスを立てます。このインスタンスはパブリックIPを持たないため外部から直接ssh接続することはできません。

f:id:seeds-std:20190917124322p:plain
ssm02

次にSystems Manager をEC2が利用できるようにIAMロールを作成してEC2インスタンスに設定します。
ロールに設定するポリシーは AmazonEC2RoleforSSM になります。

これで準備が整いました。では早速インスタンスに接続してみましょう。
AWSマネジメントコンソールより、Systems Manager -> セッションマネージャーを開きます。
セッションの開始というボタンを押すとインスタンスの一覧が表示されます。インスタンスを選択し、セッションの開始ボタンを押します。

f:id:seeds-std:20190917124836p:plain
ssm04

するとWebブラウザでコンソールの画面が開きます。
ユーザはssm-userというユーザで接続されているようですね。sudoコマンドを使ってroot権限得ることもできます。

f:id:seeds-std:20190917125109p:plain
ssm05

それでは試しにsshdのサービスを止めてみましょう。
リモートからsshdを止めるなんていう暴挙は初めてです。sshdを停止してもアクセスできていることが確認できます。

f:id:seeds-std:20190917125237p:plain
ssm09

セッションの履歴からどのIAMユーザで接続されたかを確認することができます。

f:id:seeds-std:20190917140113p:plain
ssm06

セッションの詳細な情報をログとして出力することも可能です。
CloudWatch Logsに出力するように設定するとこのようになります。実行されたコマンド一つ一つまで記録されていますね。

f:id:seeds-std:20190917135950p:plain
ssm007

S3にファイルとして保管させることも可能です。

f:id:seeds-std:20190917140412p:plain
ssm08

いかがでしたか。

比較的容易にセッションマネージャーを使ってアクセスすることができました。
セッションマネージャーのいいところは踏み台サーバが不要であるということだけではなく、EC2インスタンスへのアクセスをIAMユーザで管理できるということにあります。
ログもコマンドレベルで出力されていますので、いざという時の監査ログとしても利用できそうですね。
今後は踏み台サーバではなく、セッションマネージャーを使うことも考慮に入れていきたいと思います。

AWS サーバレスでlibreofficeを使ってExcelをPDF変換

クラウド事業部の川勝です。

弊社のプロジェクトのいくつかでは、帳票のPDF出力にExcelで作成したものをPDF変換かけて使用するというものがあります。
Excel->PDF変換にはlibreoffceのheadlessモードを使用しているのですが、今回その仕組みを汎用的に使えるようにサーバレスで構築してみたのでご紹介いたします。

要件

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

構成

f:id:seeds-std:20190827191927p:plain

Serverless libreoffice

まずAWS Lambdaでlibreofficeが使えるのか?(外部コマンドあるか)というところからはじめました。
外部コマンドでsofficeがあるかどうか。
ちょっと古い2014年の記事ですがどうもなさそうです…

AWS Lambdaをいろいろ暴く – Qiita

(ちなみにamazon linuxベースとのことなので大体の外部コマンドは使用可能ですね。)

自力でインストールするしかない、、できるのか、、?と調べていたところ以下を発見いたしました。

serverless-libreoffice/STEP_BY_STEP.md at master · vladgolubev/serverless-libreoffice · GitHub

こちらのStep:1ではlibreofficeをコンパイルするところから始まっていますが、リポジトリ内にコンパイル済みのファイルがありますのでそちらを使用します。(Step:2)

S3にserverless-libreofficeをアップロード

$ aws s3api create-bucket --bucket lambda-libreoffice-demo
$ curl -L https://github.com/vladgolubev/serverless-libreoffice/releases/download/v6.1.0.0.alpha0/lo.tar.gz -o lo.tar.gz
$ aws s3 cp lo.tar.gz s3://lambda-libreoffice-demo/lo.tar.gz --acl public-read-write

これで前準備は完了。 

Lambda実行時にアップロードしたファイルを展開して使用できるようにします。
今回はgoで実装していきます。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
}

main.goのinitでs3から取得したファイルを /tmp に展開します。
initで呼び出すことでlambdaコンテナの起動時のみ実行されます。
注意点はLambdaでは /tmp 以下にしか書き込み権限がないのと容量制限(500MB)があります。

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数のためにディスクにスクラッチスペースが必要な場合はどうすればよいですか?

各 Lambda 関数では /tmp ディレクトリに 500 MB の容量 (非永続型) を割り当てることができます。

ひとまずこれで /tmp/instdir/program/soffice として実行可能になります。
受け取ったExelファイルは /tmp/sample.xlsx にファイルとして保存しているとして、以下の様な感じで実行できました。

const convertCommand = "/tmp/instdir/program/soffice --headless --invisible --nodefault --nofirststartwizard --nolockcheck --nologo --norestore --convert-to pdf --outdir /tmp %s"
command := fmt.Sprintf(convertCommand, "/tmp/sample.xlsx")
exec.Command("sh", "-c", command).Run()

/tmp/sample.pdf が生成されるので、これをbase64エンコードしてjson形式で返却します。

…がしかし、実行はできましたが、このままだと日本語が文字化けてしまいます。
日本語フォントに対応が必要そうです。

日本語フォント

「lambda 日本語フォント」とかで調べていると以下の記事にあたりました。

AWS LambdaでPhantomJS日本語フォント対応 | RCO Ad-Tech Lab Blog

Lambdaの実行環境にフォントを追加する – Qiita

ようはfont-cache生成できればよさそうです。
上記記事では生成したfont-cacheをデプロイパッケージに含めていましたが、initでlibreofficeの展開もしているのでそこでfont-cache生成したらいいかな、と思ったのでその方向で実装しました。

initに追加します。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
os.Setenv("HOME", os.Getenv("LAMBDA_TASK_ROOT"))
cmd := "mkdir -p /tmp/cache/fontconfig && fc-cache " + path.Join(os.Getenv("HOME"), ".fonts")
exec.Command("sh", "-c", cmd).Run()
}

.fonts ディレクトリに使用するfontファイルをデプロイパッケージに含めてlambdaにアップロードします。

以下ディレクトリ構成でzipに固める

.
├── .fonts
│   ├── hoge.ttc
│   └── fuga.ttc
├── .fonts.conf
└── main

main goのbuild済みファイルです。
.fonts.confは参考URLのまま以下の設定にしています。

<fontconfig>
<cachedir>/tmp/cache/fontconfig</cachedir>
</fontconfig>

これで実行するとPDFに日本語が含まれていてもOKになりました!

Timezone

構築後に社内で試してもらっていると日付関係が変?という報告がありました。
LambdaのデフォルトtimezoneはUTCなのでExceleで =Now() とかするとUTCの時刻になるようです。
これは対応が簡単で、lambdaの環境変数設定で解決。
TZ をkeyにして設定可能でした。

key: TZ
value: Asia/Tokyo

ちなみに日付の並び順は変わらなかったので、Excel側でフォーマット指定してもらうことにしました…

=TEXT(NOW(),"yyyy/MM/dd hh:mm:ss")

これにて無事日本語が含まれるExcelデータもPDF変換可能です!

その他ハマったところ

バイナリメディアタイプ

要件のところで書いていた話

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

API Gatewayの設定でバイナリメディアタイプを設定することで、ファイルアップロードしたらPDFがそのままダウンロードできる…と思ってやっていたのですが、どうにもうまくいかず断念しました。(これは自分のgo言語の力量不足かもしれませんが..)

あと、ブラウザからではリクエストヘッダーに Accept: application/pdf が付与されないためCloudFrontをかまさないといけない、、、ということなので今回はjsonでやり取りでいいかな、となったのも理由です。

送信するときにExcelデータをbase64エンコードして、変換したPDFもbase64エンコードして返却させています。
送信側でプログラム書く場合はこの方がシンプルに構築できるのでいいかな、と感じています。

API Gatewayのタイムアウト

API GatewayのタイムアウトはLambdaより短いです。
Lambda ・・・最長15分

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数はどれくらいの時間実行できますか?

AWS Lambda 関数は、1 回あたりの実行時間を最長 15 分に設定することができます。タイム> アウトは 1 秒から 15 分までの間で任意に設定できます。

API Gateway ・・・最長29秒

Amazon API Gateway の制限事項と重要な注意点 – Amazon API Gateway

統合のタイムアウト・・・Lambda、Lambda プロキシ、HTTP、HTTP プロキシ、AWS 統合など、すべての統合タイプで 50 ミリ秒~29 秒。

また制限の解除も対象外です。

Lambdaのデフォルトのメモリ(128MB)だとタイムアウトすることが多かったのでしたので256MBで実行するようにしました。

最後に

サーバレス開発で今回のように外部コマンドもLambda上で展開してしまえば結構なんでもできそうだなと思いました。
API gatewayのバイナリメディアタイプに関しては引き続き調べていきたいところです。

あと今回はサーバーレスアプリ自体にはあまり触れませんでしたが SAM CLI を使用して開発、デプロイしています。

GitHub – awslabs/serverless-application-model: AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications

このあたりの話はまたの機会に…

以上です!

Amazon Elastic File System (Amazon EFS) におけるバーストとプロビジョニングの変更を自動化する

クラウド事業部の原口です。

AWSのEFSが発表されてから「これを待っていた!!」というエンジニアの方も多かったと思いますが、割と落とし穴的な制限が多く、
その中の一つとしてEFSの「バースト」と「プロビジョニング」の2つのスループットモードがあります。

バーストスループットモードとは何か。

誤解を恐れず簡単に説明しますと、スマフォの帯域制限みたいな機能となります。
EFSではバーストクレジットと呼ばれる値があり、このクレジットがある間は非常に高速なスループットでデータのやり取りが可能なのですが
クレジットがなくなると制限がかかりめちゃくちゃ低速なファイルストレージになってしまいます。
これはプロダクション環境ではサーバ障害とも言えるレベルでの低速さになります。

バーストクレジットを回復するには?

基本的にファイルストレージを使っていなければ回復していきます。
そのため、EFSの利用頻度がクレジットの回復力を下回る場合はクレジットは減る事がありません。

また、使っているEFS内のサイズによってもクレジットのそもそもの量が異なります。
大きいファイルが多数あるなど、EFS内のデータ量が多ければクレジットのそのものの絶対量が多く付与されます。

これを理解してない場合に起こる問題

基本的には運用時、EFSはバーストスループットモードとして動作する事がほとんどですが、
よくある事例としては「テスト段階では高速で快適に動いていたのに、本番運用していたら急激に低速になり障害があった」という事例です。

CloudWatchのメトリクスに「BurstCreditBalance」というメトリクスがありますが、こちらがバーストクレジットで、
ゼロになると低速なストレージになってしまいます。

f:id:cs_sonar:20190826124939p:plain

上記のようなグラフの場合は運用で徐々にクレジットが減っていってるのでずっと運用しているといつかは障害となりそうです。

プロビジョニングスループット

さて、バーストクレジットを使い果たして低速となってしまった場合はどうすればいいのでしょうか?

もちろん「大きなファイルを作ってバーストクレジットを増やす」というのも一つの解決策ですが、
一般的にはバーストモードからプロビジョニングスループットというモードへ変更する事で対策できます。

プロビジョニングスループットは帯域保証のような機能で50(MB/秒)といった値を設定する形でスループット値を保証させるモードです。
このモードではクレジットなどに関係なく設定した速度は必ず保証されます。

しかし・・・このプロビジョニングスループットは非常に高額です。
1MB/秒 の保証にあたり月額7.2USDもかかります。
件のように50MB/秒を保証させると月額360USD ≒ 38,000円。
ただでさえEFSは高額なのにこの価格は無視できる額ではありませんよね。

プロビジョニングとバーストクレジットを交互に設定をする事でコスト削減を図る

実はバーストクレジットからプロビジョニングスループットに変更すると
プロビジョニングスループット中はバーストクレジットが回復していきます。

f:id:cs_sonar:20190826125825p:plain

8/23の途中からプロビジョニングスループットとしていますが、3日ほどでクレジット最大値まで回復していますね。

つまり、最もコスト効率がいいのは

クレジットを限界まで使い直前でプロビジョニングスループットに変更 。
クレジットが回復したらバーストスループットに変更。

という形となります。

CloudWatch + SNS + lambda を使ったバーストとプロビジョニングの変更を自動化

ここからが本題。
上記の形がコスト効率が最もよいのですがこんなの人の手でぽちぽちやってれないので自動化します。

まずは、バーストとプロビジョニングの変更を行うlambda関数を作成します
バーストモードにするものとプロビジョニングにするものの2つ

EFS-to-bursting

console.log('Loading function');
var https = require('https');
var http = require('http');
const aws = require('aws-sdk');
var efs = new aws.EFS();
exports.handler = (event, context) => {
(event.Records || []).forEach(function (rec) {
if (rec.Sns) {
var message = JSON.parse(rec.Sns.Message);
var params = {
FileSystemId: message.AlarmDescription, /* required */
ThroughputMode: 'bursting'
};
efs.updateFileSystem(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else     console.log(data);           // successful response
});
// ここでslack等に通知するといいと思う
}
});
};

EFS-to-provisioned-iops

console.log('Loading function');
var https = require('https');
var http = require('http');
const aws = require('aws-sdk');
var efs = new aws.EFS();
exports.handler = (event, context) => {
(event.Records || []).forEach(function (rec) {
if (rec.Sns) {
var message = JSON.parse(rec.Sns.Message);
var params = {
FileSystemId: message.AlarmDescription, /* required */
ProvisionedThroughputInMibps: '10', // プロビジョニング10MB
ThroughputMode: 'provisioned'
};
efs.updateFileSystem(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else     console.log(data);           // successful response
});
// ここでslack等に通知するといいと思う
}
});
};

FileSystemId: message.AlarmDescriptionの部分ですが
受け取ったCloudWatchアラームにはどのEFSがアラームを出したか、というものが取得しずらかったので
CloudWatchアラームのDescriptionにEFSのIDを載せる形にしました

SNS(Simple Notification Service)のトピックを作成してlambdaに連携します

SNSは通知先にメールやhttpなどだけでなくlambdaに通知できます。
ですので、バーストモードにするlambdaに連携するトピックと、
プロビジョンドにするlambdaに連携するトピックの2つを作成します。

f:id:cs_sonar:20190826131112p:plain

cloudwatchにてアラームを作成

BurstCreditBalanceを監視し、上記作成のSNSに通知するアラームを作成します。
こちらも、
・バーストクレジットが一定数以上の時 -> プロビジョンモードにするSNSに発火
・バーストクレジットが一定数以下の時 -> バーストモードにするSNSに発火
という形で2つ作成します。

f:id:cs_sonar:20190826131630p:plain

lambdaの所でも書いた通り、DescriptionにはEFSのIDを入力します

f:id:cs_sonar:20190826131750p:plain

バーストとプロビジョニングの変更を自動化できました

このように複数EFSあると便利さがわかりますね!

f:id:cs_sonar:20190826132003p:plain

クレジットが減るとプロビジョニングになり、クレジットが充足するとバーストモードになる形です

注意点

EFSの制限で、スループットモードの変更は1日に1回までしかできません!
そのため、1日でバーストクレジットを使い果たしてしまうような環境ではずっとプロビジョニングモードで動かす必要があります。

本日は以上で!

Page 1 of 2

© SEEDS Co.,Ltd.