【Serverless】写真のアップロード速度を爆速にした話

この記事はうるる [Advent Calendar 2019](https://adventar.org/calendars/4548) 17日目の記事です。

# はじめに
この記事では、私が働いている会社、[株式会社うるる](https://www.uluru.biz/)で運営しているサービス幼稚園・保育園向け写真販売サービス [えんフォト](https://en-photo.net/)において、写真アップロードの速度を爆速にするためにAWSのServerlessアーキテクチャを導入したお話をします。

サービスの内容や、導入に至った経緯などもお話ししたいところですが
話がとても長くなってしまいそうなので、アーキテクチャ導入にあたって考えたことや、細かい処理の説明にのみフォーカスし、記事を書きたいと思います。

※社内のエンジニア向けにLTした際のスライドも活用していますのでご了承ください。

## 対象の読者
* AWS(特にサーバーレス)について興味がある人/これから学びたいと思っている人
* 画像のアップロード速度に困っている人
* 画像を扱うサービスの開発を担当している人

## 注意点
かなり長めの記事です。
読み切るのに根気がいるかと思いますが、**いいね**やストックをすることで数回に分けて読むことをお勧めします。

## システム構成(一部)
当アプリケーションは Laravel5系で構築されており、インフラ(IaaS)は主にAWSで構成されています。
webサーバーは[AWS Elastic Beanstalk](https://aws.amazon.com/jp/elasticbeanstalk/)で管理しているEC2インスタンスです。
写真の保存ストレージは[AWS S3](https://aws.amazon.com/jp/s3/)、写真のサムネイルの作成は開発時にはすでにLambdaを用いて処理させていました。
DBは[AWS RDS for MySQL](https://aws.amazon.com/jp/rds/mysql/)を使用しています。
また今回のアーキテクチャには深く関わりませんが、当サービスでは、顔写真の検索に[AWS Rekognition](https://aws.amazon.com/jp/rekognition/)を用いております。

# 移行前のアーキテクチャと構成
![スクリーンショット 2019-12-16 16.50.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/491e63ca-1317-70df-88d7-6b88b50e23a7.png)

処理の流れ

* クライアントサイドでアップロード処理を実行すると、サーバーへ写真や紐付けなどの情報がPOSTされます。
* Laravel上で受け取った写真の拡張子やサイズなどが、制限内に収まっているかどうかをチェックします。
* 写真が正しいものと判定されたら、写真に関するデータをRDSに保存します。(写真名とか、リレーションIDとかそんな奴らです)
* DBへの保存後、s3へ写真をPUTします。この際はLaravel上でS3のAPIを使用します。
* S3へ写真が上がると、次はLambdaが起動します
    * Lambdaは、**写真のトリミング**と**サムネイルの作成**のファンクションが順番に起動します。

※トリミング・・・4:3や16:9などの比率でアップロードされた写真を、現像写真サイズ用(89:127)にトリミングをします。
※サムネイル・・・ユーザーへの表示用に軽量化した写真です。

## 移行前の課題
移行前の構成における問題点は、画像のチェックやDBへのデータ保存、S３への写真転送など、一通りの処理が終わった後にクライアントへレスポンスが返るスタイルでした。
一枚の写真だけであれば対して問題は起きませんが、流石に1日に何千何万枚という写真がアップロードされ、サーバー上でその写真の処理をしていくと、いくつか問題が発生します。
そのうちの１つが、レイテンシーの発生でした。
![スクリーンショット 2019-10-25 16.21.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/7a326197-2c78-2f8b-7850-3fc0618fa739.png)

真ん中より左側が、サーバーレス移行前のレイテンシーです。
日常的にもそうですが、瞬間的に負荷が上がるタイミングなどでは特にめちゃくちゃ飛び出ます。

サーバー台数を増やしたり、EC2のインスタンスサイズをあげたりすればこの負荷はもう少し分散することができますが
サーバーレスであれば瞬間的なアップロードにも対応でき、なおかつそれぞれの処理も速い状態を維持できるというメリットがあったので、サーバーレスの導入を検討しました。 
何よりもサーバーレスを使いたかったのです。（笑）

サーバーレス化の後のレイテンシは見ての通りです。
効果は抜群でした。

# 新アーキテクチャ決定までにやったこと
## 当初、考案したアーキテクチャ
まず、やりたかったことは以下です。
<img width="987" alt="スクリーンショット 2019-12-16 20.36.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/c5d18c7a-5280-820c-99c2-82d959e5ac0e.png">
**EC2サーバーを一切介さない**というのが最大の目標として考えていました。
写真は一枚一枚が重いですし(数MB)、事業拡大とともに扱う写真枚数も右肩上がりになります。事業拡大に負けないくらいのシステムするというのも今回の目標でしたので、**スケーラブル**という魅力のあるサーバーレスに写真の処理を全任せすることを目指しました。

しかし、いくつかの障害にぶつかることとなります。

## API Gatewayの問題点

* 当初、API GatewayからLambdaを呼び出すことにより、サムネイルの処理を行うことを想定した。
* クライアントから直接API経由で画像を送信し、そのままLambdaで処理するスタイル
![スクリーンショット 2019-12-16 20.44.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/d51e3fd4-5014-8b84-c6ae-fe795980bcef.png)

しかし、Lambdaの呼び出しペイロード制限**6MB**に直面しました...
画像の送信サイズは20MBまであげたかったので、この方法は採用できませんでした。

## DynamoDBの断念

* Rekognition(顔認証)のデータ
    * 顔認証のデータが大量にあり、RDSのストレージを圧迫する原因となっている
    * オートスケールするDynamoに入れ、Lambda + Dynamoで顔認証を実装し、なんかイケてそうなアーキテクチャにしたかった
    * ~~Dynamo使いたい~~
* DynamoDB + Laravelがイマイチ？
    * ライブラリにより、ORMなどをDynamoでもサポートするようにはできる
    * 不可能ではない。というか多分できる。
    * ただ、Mysql + Dynamoは管理しづらいかもしれない
* 開発期間が足りない
    * Step FunctionとかAPIGatewayとかAuroraとか、不確定要素が多い中で**一人で**進めるプロジェクト
    * リリース時期の目標がある中で、Dynamoの検証を進める時間が足りなそうだった(開発期間3ヶ月くらい)
    * 何よりも優先度が低かった

DynamoDBについては、完全に**使いたい欲**が先行していました。
本来であれば、アプリケーション全体をサーバーレスへ対応させるため、MySQLを丸っとDynamoDBへ移行したかったですが
流石に検証期間が短すぎるため、一部だけ移行という方法を考えましたが
それでも今回は優先度が低かったため、見送りになりました。

## Aurora Serverlessの検討
DynamoDBは、RDS＋Lambdaの相性の悪さから考えた案でしたが
その次に[Aurora Serverless](https://aws.amazon.com/jp/rds/aurora/serverless/)を検討しました。

### DataAPI
<img width="984" alt="スクリーンショット 2019-12-16 20.58.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/b9e9b0b6-58f4-8bce-34ec-ae56eaa2c039.png">
このDataAPIという、サーバーレスにとって最高の機能を使えば大体の問題は解決すると考えました。
ただし、大問題が...

### ユースケース(公式ドキュメント抜粋)

* 使用頻度の低いアプリケーション
* 新規アプリケーション
* 可変ワークロード
* 予測不可能なワークロード
* 開発およびテスト用データベース
* マルチテナントアプリケーション

**開発およびテスト用データベース** だと...

本番環境向きではないと、公式が示しています。
これはあかんと思い、AWSのサポートに直接聞いてみました。
すると以下のような回答が返ってきました。

> 本番アプリケーションの要件に合致してたら使っていいよ。でもちゃんとテストしてね！

なるほど...テストしてみて使えそうだったら使ってもいいのか。
そう思い、早速検証に入りました。

### AuroraとAuroraServerlessの検証
AuroraとAuroraServerlessを対象に、サーバーレスに耐久できるかどうか検証をしました。
検証内容については本記事とテーマが少しずれてしまうため、また別の記事で紹介させてください。

結果は、**AuroraServerlessの圧勝**という結果でした。

Auroraってだけあり、パフォーマンスもそれなりに高く、安定して動いてくれました。
本番環境でも問題なく動かせるのではないかという結果を導くことにも成功しました。

しかし、ここでも大きな問題が発生しました...

### Aurora Serverlessの開発環境
開発環境でAuroraServerlessを使おうとする際の方法は以下２つでした。

* 実際のAWSリソースを準備しローカルからアクセスする
* Dockerコンテナで仮想的にAuroraServerlessを構築する

AuroraServerlessは割と新しいサービスで、コンテナはかなり難易度が高かったため、実際のリソースを使うことにしました。
しかし、AuroraServerlessの制限でAuroraにパブリックIPを割り当てることができませんでした...
開発環境からAuroraServerlessにアクセスするには、パブリックIPを割り当てる必要があったため、これは大打撃です。

時間をかけAuroraServerlessを検証し、やっと使える！というところまできたところでこれはかなりショッキングな制限でした。
ショックすぎて、AWSサポートに問い合わせてどうにかならないか聞きましたが、ドキュメント以上のことは返ってきませんでした...
そりゃそうだ、、、
AuroraServerless自体、新しいのでAWS側でも細かいところまでは回答ができないみたいだ。


### Aurora Serverlessの断念

DataAPIという強力な機能のためにいろんなものを犠牲にしようとしました。
AWS新サービス特有の情報の少なさ、ユースケース外の使用、問題が発生した時はマンパワーで頑張るという覚悟。

しかし、開発環境を整えることができないという問題により選択肢から除外せざるを得ませんでした。
結果的には、特にDBのようなアプリケーションの基盤となるようなものに関しては、ナレッジの少ない選択肢を選ぶのにはリスクが非常に高いので、採用しなくて正解だったとも言えます。

この当時、精神的に色々やられましたがなんとか諦めをつけることができました。
まさに、失恋した気分でした。

## RDS + Lambdaの検証
AuroraServerlessは諦めましたが、RDSは諦めることはできません。(というかこれで行くしかない)
Auroraへの移行も考えましたが、社内事情などもありながら、とりあえず現状通りRDSMySQLでトライしてみることにしました。

一般的に、RDS + Lambdaはアンチパターンと言われがちですが
Lambda自体の運用実績はあり、同時起動数がそこまで高くなっていないことは、モニタリングからも明白でした。
では実際にどのくらいの瞬間風速で同時起動数が跳ね上がるのかを検証してみました。

結果は以下でした。
<img width="985" alt="スクリーンショット 2019-12-16 21.12.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/5faf31a0-1c7b-9ebc-513e-5e3583568a1f.png">

現状のサービス規模であれば、コネクション問題を回避することが可能であるということが検証により明らかになりました。
また、ある程度事業拡大しても、耐えられることができるということまで示すことができたため、RDS+Lambdaを採用することに決まりました。

このように、紆余曲折を経てアーキテクチャの決定までこぎつけることができました。

# 新アーキテクチャと構成
以下の画像が新アーキテクチャです。
![スクリーンショット 2019-12-16 20.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/d857eedb-a692-9702-4472-0f2fa95691b9.png)
※お絵描きが下手なのは見逃してください...

どうでしょうか。大まかに説明します。
まずクライアントから**S3に直接**写真を突っ込み、それに反応し、[AWS StepFunctions](https://aws.amazon.com/jp/step-functions/)の**ステートマシンが起動**します。
ステートマシン内で、それぞれの処理をするLambdaファンクションが順々に実行されていき、DBへのデータ登録やサムネイルなどが作成され終了です。

アーキテクチャを以下の様に分解して、説明します。
<img width="875" alt="スクリーンショット 2019-12-16 21.21.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/8d0048fb-1162-a626-3936-44760312e413.png">

## ① 写真のアップロード
本記事のタイトルは「アップロード速度を爆速にした話」でしたね。
(長すぎて忘れてしまっていたらすみません...)

実はこの部分こそが、写真のアップロード速度を爆速にできた要因となっています。
写真のアップロード速度は、ブラウザ上でsubmitしてから、レスポンスが返ってくるまでの時間です。
移行前の構成では、レスポンスを返すまでに色々な処理が走ったため、待ち時間が発生してしまっていました。

ですので、これをシンプルに**S3に写真を保存したら即レスポンス**にするのが理論上、最も速いです。

写真を保存するだけなら、サーバー経由でもできますが、それは最速ではないです。
**S3に直接**
これが最速です。速くするからには最速にこだわりました。
速いは正義です。

ですが、S3に直接送信するためには、セキュリティの問題が存在しました。
当初[Cognito](https://aws.amazon.com/jp/cognito/)などの認証サービスを使用することでセキュリティを担保することを考えましたが、このためだけに認証サービスを入れるのはお金も時間もコストがかかりすぎました。

そこで見つけたのが **S3の署名付きURL**でした。
(参考：https://dev.classmethod.jp/cloud/aws/generate-s3-pre-signed-url-by-aws-cli/)

この署名付きURLを用いてアップロードを行うことで、セキュリティを担保しながらS3への直接アップロードを実現しました。
基本的にはSDKの使い方そのままなので非常に簡単でした。

### サンプルコード

```PHP
// PHP-SDK sample code
$s3Client = new Aws\S3\S3Client([
    'profile' => 'default',
    'region' => 'us-east-1',
    'version' => '2006-03-01',
]);
$cmd = $s3Client->getCommand('PutObject', [
    'Bucket' => 'my-bucket',
    'Key' => 'testKey'
]);
$request = $s3Client->createPresignedRequest($cmd, '+20 minutes'); // ここでURLの有効期限を設定する
```

出力すると以下のようなURLになります。

```PHP
echo $request;

https://my-bucket.s3.us-east-1.amazonaws.com/testKey?
X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=AKIAxxxxxxxxxxxxxxxxxxxxxx
X-Amz-Date=20191027T091851Z&
X-Amz-SignedHeaders=host&
X-Amz-Expires=1800&
X-Amz-Signature=33265d9ee979db5axxxxxxxxxxxxxxxxxxxxx

```

クライアントサイド(JavaScript)では、このエンドポイントへ向けて、写真をPUTすることでアップロードすることができます。

## ② ステートマシン（Step Functions）の起動
S3からステートマシンを起動させるにはCloudTrailとCloudWatchLogsを使用します。
詳しくは公式ドキュメントに記載してありますので割愛させていただきます。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-cloudwatch-events-s3.html

## ③ ステートマシンによる、写真の処理
今回のサーバーレス化を達成するためのコアとなる部分です。
StepFunctionsについては、色々な入門的な記事があるので、そちらを参考にしてみてください
(https://qiita.com/kooohei/items/150d03775536fe7ce273)

基本的には、LambdaやDynamoDB、SQSなどのAWS各種サービスを使い、ワークフローを構築できるというサービスです。
並行処理や条件分岐、失敗時のリトライや、エラーをCatchしての処理などの機能が備わっています。
移行前のアーキテクチャでは、LambdaをS3トリガーで単一的使っていました。
そこには、エラー時に捕捉できず、失敗したらそのままにするしかない、などというサーバーレス特有の問題がありました。
StepFunctionsを使うことで、自由にリトライさせることもできますし、並列処理を用いて、サムネイルを数パターン作らせるとか、Rekognitionも同時に実行することなど、色々な処理を構築することができることが魅力的です。

※StepFunctionsで構築したワークフローをステートマシンと言います。

### ステートマシンの定義
<img width="409" alt="スクリーンショット 2019-12-16 23.28.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/fb724653-2593-4e12-9250-5da8538a56b7.png">

今回は、割と単純な構成です。
各種データのDBへの保存、トリミング、サムネイル
失敗時には、レコードをエラーステータスに更新する。
といった処理を行っています。

### ステートマシン定義の一部
今回構築したワークフローは比較的単純なので、最初の部分のみステートマシンの定義方法を紹介します。
※ステートマシンの定義を記述したファイルをステートファイルと呼びます。

<img width="984" alt="スクリーンショット 2019-12-16 23.41.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/5e5d5bc8-af11-af32-8106-fe13f3d8371c.png">

ポイントは**Retry**の部分です。
待機時間に関しては、図中の説明の通りですが、今回のアーキテクチャ特有のエラーハンドリングが２つあります。

#### Lambda.TooManyRequestsException
これは、急激に大量のLambdaを起動した際に発生するエラーになります。
瞬間的な負荷にも耐えなければいけないですが、こちらはAWSが制限を設けているので、ある程度仕方ないエラーです。
この場合は、ちょっとだけ時間をあけてリトライさせて回避しています。

#### Lambda.ENILimitReachedException
こちらは、VPCの[ENI](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html)の制限です。(ENIの説明は省きます)
Lambda + RDSを使用した場合、RDSと同じVPC内に起動させますが、LambdaとRDSの接続をする際に、ENIが１つ生成されます。
しかも、このENIはLambdaコンテナが消えても一定時間残り続けるので、短い時間に大量のLambdaが起動してしまうとENIが爆増し、制限に達してしまいエラーとなります。
ENIの消滅に時間がかかるので、待機時間も長めにとっています。

ただこちらは、ちょうどリリースする直前にAWSが改善してくれました。
この発表には飛び跳ねました。
https://aws.amazon.com/jp/blogs/news/announcing-improved-vpc-networking-for-aws-lambda-functions/

このように、エラーに対して適切なハンドリングを設定できたのがStepFunctionsの魅力です。
失敗するとレコード中のエラーステータスを更新するようにしておりますが、エラーの種類によって実行Functionを切り替えて、ステータスコードを別のものにするなどもできた方がデバッグに役立ったかもしれません。
(エラー内容をfunctionへ受け渡すこともできたような記憶も...)

# CICD
さて、上記で写真を爆速でアップロードするための仕組みが整いました。
ここからはCICDの仕組みの構築です。
我々エンジニアは、作ればOKという世界でありませんよね。
作ったものを安全に正確にデプロイ(世の中へ供給)する必要があります。

## ServerlessFrameworkによるサーバーレスデプロイ
StepFunctionsやLambdaのデプロイには[Serverless Framework](https://serverless.com/?gclid=EAIaIQobChMIxInrxLa65gIVTHRgCh2O6gO_EAAYASAAEgLnvPD_BwE)を使用しています。

サーバーレスであるLambdaはもちろんGCP、AzureなどのサービスをCLIからデプロイできるツールです。
こちらも細かい使い方は省きますが、設定ファイルである`serverless.yml`の設定をご紹介します。
(参考：https://qiita.com/horike37/items/b295a91908fcfd4033a2)

### Lambdaの定義
ここでは1つのFunctionを紹介します。

```yaml
functions:
  storePhotoData:
    handler: storePhotoData.handler
    environment: ${self:custom.db}
    layers:
      - { Ref: LambdaLayer }
    tracing: Active
    vpc: ${self:custom.vpc}
    description: create storePhotoData for ${opt:env_name}-${opt:stage}
    memorySize: 2048
    timeout: 10
    reservedConcurrency: ${self:custom.reservedConcurrency.storePhotoData}
    role: storePhotoDataLambdaFunctionRole
```

設定自体は普通ですが、yamlの環境変数を用いて、ステージング、本番などの各環境別にそれぞれの設定値を入れています。
`environment: ${self:custom.db}` これとかです。

これは、ServerlessFrameworkの機能であるカスタム変数を利用し、各デプロイ環境ごとに値をセットしています。
(参考：https://www.queue-inc.tokyo/n/na08a319b5194)

実際に以下のように設定しています。
<img width="983" alt="スクリーンショット 2019-12-17 0.07.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/c8aeff65-11b1-8dcd-1fde-226ff9c3ae71.png">

* reservedConcurrency・・・Lambdaの同時実行数 -> 勝手に増えすぎないように制限
* db・・・RDSの接続情報
* VPC・・・セキュリティグループとサブネットの情報

実は、弊社ではえんフォトと同様、写真販売サービス[クラプリ](https://kurapuri.com/)というのも運営しており、基本的な構成はえんフォトと同じなので、同時にデプロイできるようにしています。

えんフォトとクラプリ、本番・ステージング・開発環境でそれぞれ別の値をセットしたいため、カスタム変数を使っています。
都度、OS上の環境を読み込むため、基本的にはCircleCIの環境変数に事前にセットしておくことでデプロイします。


また、`tracing: Active` にも注目です。
後述の監視で紹介しますが、[AWS X-Ray](https://aws.amazon.com/jp/xray/)を有効にするための設定です。

### StepFunctionsの定義
Serverless FrameworkはデフォルトでStepFunctionsが使えないので以下のプラグインを入れることで対応します。
https://serverless.com/plugins/serverless-step-functions/

```yaml
stepFunctions:
  stateMachines:
    enphotoSpica:
      events:
        - cloudwatchEvent:
            event:
              source:
                - aws.s3
              detail-type:
                - AWS API Call via CloudTrail
              detail:
                eventSource:
                  - s3.amazonaws.com
                eventName:
                  - PutObject
                requestParameters:
                  bucketName:
                    - ${opt:env_name}-${opt:stage}
      definition:
        StartAt: store
        States:
          store:
            Type: Task
            Resource:
              Fn::GetAtt: [storePhotoData, Arn]
            Retry:
              - ErrorEquals:
                - Lambda.TooManyRequestsException # 呼び出し頻度のエラー
                IntervalSeconds: 5
                MaxAttempts: 5
              - ErrorEquals:
                - Lambda.ENILimitReachedException # ENIの制限エラー
                IntervalSeconds: 30
                MaxAttempts: 2
              - ErrorEquals:
                - States.ALL
                IntervalSeconds: 1
                MaxAttempts: 3
            Catch:
              - ErrorEquals:
                - States.ALL
                ResultPath: $.error
                Next: updatePhotoDataToError
            Next: Trimming
```

基本的には、ステートファイルの定義をyaml形式に変えたものになります。

## CircleCIによる自動デプロイ
ServerlessFrameworkの準備ができたら、CircleCIで自動デプロイをするようにします。

```yaml
references:
  commands:
    install_npm_dependencies: &install_npm_dependencies
      working_directory: ~/app
      command: |
        sudo apt-get update
        sudo apt-get -y install graphicsmagick imagemagick
        sudo npm install -g serverless@1.49.0
        cd ./serverless && npm install
        cd ./layers/nodejs && npm install
        sudo ln -fns /node-v${NODE_VERSION}-linux-x64/bin/serverless /usr/local/bin/serverless
        sudo ln -fns /node-v${NODE_VERSION}-linux-x64/bin/sls /usr/local/bin/sls
        which serverless
        which sls
        serverless --version
        sls --version
```

大まかな流れは
serverlessの実行に必要なソフトウェアのインストール -> `serverless deploy`の実行です。

デプロイは以下のように行います。CircleCIの適切な場所でコマンドを実行してください。

```
sls deploy --force --env_name enphoto --stage prod
```

`--force`コマンドは、キャッシュなどで変更点がある場合でも反映してくれない事象が昔あったので、名残でつけています。

# 監視
さて、サーバーレスの実行、そして自動デプロイまで整いCIのための基盤は整いました。
ここからは、それを正しく運用するための監視基盤を紹介します。
(それほど大したものではありませんが)

## AWS X-Ray
LambdaにX-Rayを簡単に導入できるため、お試しの意味も込めて導入しました。
Lambdaのエラーなどを可視化することができます。
<img width="661" alt="スクリーンショット 2019-12-17 0.25.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/85cb2dbe-c89a-eb3e-7da0-9657d0d30f59.png">

単純な絵ですね...
(画像は全部緑ですが)失敗したりしていると色が変わってくれます。
また、実行時間のアベレージも表示してくれて便利です。

###エラーログのSlack通知
StepFunctionは基本的に大量実行に向いていないようで、ログがあまり充実していないです。
コンソールから見ることができるログも1000回までで、それ以前のログは遡れないです。
そのため、エラーが発生した時にはSlackに通知し、できるだけ早く調査対応を行えるようにしました。

クラスメソッドさんのこちらの記事を参考にさせていただきました。
https://dev.classmethod.jp/cloud/aws/step-functions-cloudwatch-event-slack/

Lambdaのソースコードは以下です。

```python
#coding: UTF-8
from __future__ import print_function

import json
import urllib
import urllib2
import base64
import zlib
import datetime
import os
import time

slack_api_token = "xoxp-11111111111-22222222222-33333333333-4444444444"
url = "https://slack.com/api/chat.postMessage"
color = '#FFCC33'
channel = os.environ['slackChannel']
username = os.environ['slackUsername']
icon = os.environ['slackIcon']

def lambda_handler(event, context):
    #ステートマシン実行ARN取得
    sf_exec_arn = event['detail']['executionArn']
    #ステートマシンのステータス取得 
    sf_status = event['detail']['status']
    
    #メッセージ整形
    message = "status : " + sf_status + "\n" + "result : " + sf_exec_arn
    
    params = {
        'token': slack_api_token,
        'channel': channel,
        'username': username,
        "icon_emoji": icon,
        "attachments": json.dumps([{
            "color": color,
            "text": message
        }])
    }
    params = urllib.urlencode(params)
    req = urllib2.Request(url)
    req.add_header('Content-Type', 'application/x-www-form-urlencoded')
    req.add_data(params)
    res = urllib2.urlopen(req)
    body = res.read()
```

こんな感じに通知が来ます。
<img width="754" alt="スクリーンショット 2019-12-17 0.34.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/e3146545-6f44-f07f-956d-954024946cee.png">

# 最後に
いかがでしたでしょうか。
割と長めの記事を書いたので、書くのも疲れましたが、最後まで読んでいただいた方々も疲れたと思います。
読んでいただきありがとうございました。

## アップロード速度
肝心のアップロード速度がどうなったか気になりますよね。
こんな感じです！どーん
<img width="986" alt="スクリーンショット 2019-12-17 0.38.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73081/1496f083-0eec-7a04-df07-7ab9f3f918e2.png">

特定の写真で実際に計測した結果ですが、4倍速という結果になりました！
元々が少し遅めだったということもありますが、お客様には非常に好評をいただいております。
嬉しいですね。

## AWS サポートの大切さ
プロジェクト開始時、最初は色々と自力でなんとかしようと踏ん張っていた時期がありました。
ですが、どうしても答えが見つからなかったり、ベストプラクティス的な問題にぶち当たったり、色々な困難がある中で、勇気を出してAWSサポートへ問い合わせをしてみた結果
とても参考になるアドバイスをくれることが結構ありました。
AWSの中の人が書いた記事などのリンクまで張ってきてアドバイスをくれたり、とても親切に対応してくださいました。
さすが天下のAWSさま。
これからも十分に活用させていただき、より良いサービスを作っていきたいと思います！

## RDSの改善(Proxy)
実はつい先日、RDSの改善の発表があり、RDS + Lambdaアンチパターンが解消されようとしています。
https://qiita.com/G-awa/items/b9138cc1c9e4867a905e

これはやばい、いち早く導入して、不安要素を取り除きたいと思います！

## まとめ
自己紹介が遅れましたが、私は新卒3年目の中堅(?一歩?二歩？手前の)エンジニアです。
サービス成長のために、自らサーバーレスによる改善を提案し、実行させていただきました。
AWSに関する書籍や、ネット記事、AWSカンファレンスなどにも参加し、サーバーレスについてほとんど何も知らない状態から色々な情報をインプットし、成し遂げることができました。
情報取集を含めると最初に動き始めてからは半年くらいの時間をこのプロジェクトに捧げました。
実際の稼働は３、４ヶ月ほどです。

その間には、特に社内のインフラエンジニアの先輩方には非常にお世話になりました。
リリース１ヶ月前くらいからは毎日終電前まで残業に付き合ってくださいました。
この場を借りてお礼申し上げます。

また、最後まで読んでいただいた皆さま、ありがとうございます。
ぜひ少しでもお役に立てた場合は**いいね**を押してください！！

# 終わり
Advent Calendar 17日目でした。
明日18日目は ryuichi fukudaさんによる記事を乞うご期待！
https://adventar.org/calendars/4548