【初心者向け】AWS+TypeScriptで、AIを使って画像にラベル付けをするサーバレスアプリケーションを作ってみよう

本稿は[Serverless2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/serverless2)の14日目です。

## 作るもの
画像を**AWS S3**にアップロードすると、**AWS Lambda**が自動的に実行され、**AWS Rekognition**によって自動でラベル付けがされるというアプリケーションです。

<img src="https://user-images.githubusercontent.com/7953751/49696713-5fef2b80-fbf1-11e8-90cf-b8e82a0cf241.jpg" width="320">

このようなラーメンの画像をアップロードすると

```json
[
  { "Name": "Noodle",     "Confidence": 99.91551208496094 },
  { "Name": "Food",       "Confidence": 99.91551208496094 },
// 中略
]
```
このようなJSONファイルが出力されます。「Noodle」「Food」といったラベル情報が付いていることがわかります。

### 構成

![cloudcraft 2](https://user-images.githubusercontent.com/7953751/49697225-16eea580-fbf8-11e8-9b30-fe75f35db11e.png)

画像は[Cloudcraft](https://cloudcraft.co/)で作成

この図のどこを触っているのかイメージしながら本稿を読み進めると、理解しやすいと思います。

## 対象
- **TypeScript**に触れてみたい人
    - [TypeScript \- JavaScript that scales\.](https://www.typescriptlang.org/)
- AWSの下記サービスに触れてみたい人
    - **AWS Lambda**: サーバレスアーキテクチャでコードを実行
        - [AWS Lambda \(サーバーレスでコードを実行・自動管理\) \| AWS](https://aws.amazon.com/jp/lambda/)
    - **AWS S3**: クラウドストレージ
        - [Amazon S3（拡張性と耐久性を兼ね揃えたクラウドストレージ）｜AWS](https://aws.amazon.com/jp/s3/)
    - **AWS Rekognition**: AIによる画像認識
        - [【Amazon Rekognition】人工知能・機械学習に基づく画像認識・画像解析サービス \| AWS](https://aws.amazon.com/jp/rekognition/)
- これらを一括でデプロイする**Serverless Framework**に触れてみたい人
    - [Serverless \- The Serverless Application Framework powered by AWS Lambda, API Gateway, and more](https://serverless.com/)

### 必要なもの
- AWSアカウント
- AWS利用料金(たぶん5円ぐらい)
- パソコンとブラウザ

以下、2018年12月10日時点の情報です。

## AWS Cloud9で開発環境を作る
参考: [初めてのサーバーレスアプリケーション開発 ～Serverless Framework を使ってAWSリソースをデプロイする～ ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/serverless-first-serverlessframework/)	

まずは開発環境を作っていきます。
Node.jsが実行できる環境が必要なのですが、ここでは開発環境構築に**AWS Cloud9**を使っていきます。**AWS Cloud9**を作るだけで、これから開発するために必要な準備がほぼ全て揃います。

[AWS管理コンソール](https://console.aws.amazon.com/console/home)を開き、

1. 画面右上のドロップダウンから「アジアパシフィック(シンガポール)」を選択
1. 画面左上の「サービス」から「Cloud9」を選択
1. 「Create environment」
1. Nameに適当な名前、たとえば「my-serverless-dev」等を入力して「Next step」
1. 下記を設定して「Next step」※いずれもデフォルト設定のはずです
    - Environment type: 「Create a new instance for environment (EC2)」
        - 今回の開発環境のために、あたらしくサーバをひとつ立てるという設定です
    - Instance type: 「t2.micro (1 GiB RAM + 1 vCPU)」
        - サーバのスペックです。高性能になるほど高額になるので、ここでは最低スペックにしています
        - 動作が重かったり「out of memory」のようなエラーが散見される場合は、スペックを上げましょう。
    - Cost-saving setting: 「After 30 minutes(default)」
        - 操作せずに30分経ったら自動でサーバを休止状態にして費用を節約します
1. 設定を確認したら「Create Environment」

すこし時間かかりますので、その間に**AWS Cloud9**の特徴を説明します。

https://aws.amazon.com/jp/cloud9/

AWS Cloud9は、作成すると自動的に仮想マシン(AWS EC2)が立ち上がり、仮想マシンの中のファイルを編集し、コードを実行できるようになります。AWS Cloud9自体は無料ですが、この仮想マシンに利用料金がかかります。

AWS Cloud9には下記の特徴があります。

- 数ステップで簡単に開発環境が構築できる
- ブラウザ上でIDEが開き、サーバ上のファイルを編集できる
- AWSの各種サービスとの連携が容易

もちろん、開発に慣れている方はご自身のマシンのローカル環境下に開発環境を構築したほうがより生産性高く開発を進めることができます。その場合はNode.js等の必要なソフトウェアのインストールが必要になることと、AWSアカウントの接続情報のセットアップ(後述)が必要な事に注意しましょう。

### Serverless Frameworkのセットアップ
Cloud9が起動したら、画面下部の `ec2-user:~/environment $ ` と表示されているbashターミナル部分を使って操作していきます。

<img src="https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/images/ide-terminal.gif" alt=" AWS Cloud9 IDE のターミナルを使用する " style="max-width:100%">
画像は[AWS Cloud9 の IDE チュートリアル \- AWS Cloud9](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/tutorial.html#tutorial-terminal)より引用

下記コマンドを順番に実行していきます。

```bash
$ npm install --global serverless # Serverlessのインストール
$ serverless create --template aws-nodejs-typescript --path my-serverless-sample # Serverlessのプロジェクト作成
$ cd my-serverless-sample # 作成したプロジェクトに移動
$ npm install --save-dev # 必要なnpmパッケージのインストール
```

`serverless create`コマンドを実行すると、必要なパッケージ情報が書いてある`package.json`や、実行するプログラム本体となる`handler.js`等が自動で作成されます。今回は` --template aws-nodejs-typescript`と指定しているので、プログラムは`handler.ts`となりますし、TypeScriptでの実行やコンパイルに必要な設定ファイルやパッケージ情報が追記されます。

とりあえずデプロイしてみます。

```bash
$ serverless deploy
```

このコマンドを実行すると、必要なファイルがwebpackでコンパイルされ、AWS上にデプロイされます。今回であれば、`handler.ts`に記述したプログラムが**AWS Lambda**にデプロイされます。Cloud9の認証情報を使うので、AWSへの接続情報などを特に指定することなく、デプロイすることができます。

デプロイされたプログラムを手動実行するには、下記コマンドを実行します。

```bash
$ serverless invoke --function hello
```
下記のレスポンスが返ってきたら成功です。

```bash
{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!\",\"input\":{}}"
}
```

#### 参考: git管理する場合
プロジェクトをgit管理する場合、下記コマンドを実行しておきます。

```bash
$ git init
$ curl https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore > .gitignore # お好みで
$ echo 'package-lock.json binary' >> .gitattributes # お好みで
$ git remote set-url origin https://github.com/USERNAME/REPOSITORY.git # GitHubに上げるなら
```
必要に応じて所々`git diff` や `git commit` しながら進めると、より理解しやすいと思います。

#### 参考: Serverlessのテンプレートを使わない場合
TypeScriptのテンプレートを使わない場合や、既存のJavaScriptで書かれたプロジェクトをTypeScriptに書き換える場合、下記コマンドで必要なパッケージや設定ファイルをセットしていく事ができます。

```bash
$ npm install --save-dev typescript webpack serverless-webpack ts-loader @types/aws-lambda @types/aws-sdk @types/node
$ npx tsc --init
```
webpackの設定ファイルも自力で記述する必要があります。

```webpack.config.js
const path = require('path');

module.exports = {
  entry:   entry: './handler.ts',,
  mode: 'production',
  resolve: {
    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  target: 'node',
  module: {
    rules: [
      { test: /\.tsx?$/, loader: 'ts-loader' },
    ],
  },
};
```
また、もし「JavaScriptは書いたことがあるもののTypeScriptでの開発に慣れていない」という人は、tsconfigに`strict: false`の設定を入れると楽になると思います。

```diff:tsconfig.json
     /* Strict Type-Checking Options */
-    "strict": true,                           /* Enable all strict type-checking options. */
+    "strict": false,                           /* Enable all strict type-checking options. */
```

## 画像のラベル付けをするプログラムを書く
※以下、「S3のバケット名」となる`<myserverless20181214>`の部分は、ご自身のハンドルネームなど適当な名前に書き換えて下さい。重複しない名前をつける必要があるためです。

まず、今回必要になるAWSサービスをデプロイするための情報を記述します。デフォルトの状態で**AWS Lambda**はすでにデプロイできているので、**AWS S3**と**AWS Rekognition**の情報を記述していきます。


```serverless.yml
service:
  name: my-serverless-sample

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs8.10
  region: us-east-1
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:s3:::*"
      Action:
        - "s3:*"
    - Effect: "Allow"
      Resource: "*"
      Action:
        - "rekognition:*"

functions:
  hello:
    handler: handler.hello
    events:
      - s3:
          bucket: <myserverless20181214>-input-images # 書き換えて下さい
          event: s3:ObjectCreated:*
resources:
  Resources:
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: <myserverless20181214>-output-tags # 書き換えて下さい
```
`provider`に、AWS S3とAWS Rekognitionへのアクセス許可設定を、`functions`に、入力用S3オブジェクトが作成されたら実行する設定を、`resources`に、出力用S3バケットを作る設定を記述します。

TypeScriptで**AWS S3**と**AWS Rekognition**にアクセスするために、`aws-sdk`をインストールする下記コマンドを実行します。

```bash
$ npm install --save aws-sdk
```
**TypeScript**のプログラムを記述します。S3に配置されたファイルのバケット名とキー名を取得して、Rekognitionに渡して、JSONにしてS3に書き込む、というプログラムを書いていきます。

```handler.ts
import * as AWS from 'aws-sdk';
import * as AWSLambda from 'aws-lambda';

const rekognition = new AWS.Rekognition();
const s3 = new AWS.S3();

export const hello = function(event: AWSLambda.S3Event, context: AWSLambda.Context) {
  // AWS S3にアップロードされた画像の情報を取得
  const bucket = event.Records[0].s3.bucket.name;
  const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+g/, " "));
  const paramsToRekognition: AWS.Rekognition.DetectLabelsRequest = {
    Image: {
      S3Object: {
        Bucket: bucket,
        Name: key
      }
    },
    MaxLabels: 10,
    MinConfidence: 1
  }

  // AWS Rekognitionに渡す
  rekognition.detectLabels(paramsToRekognition, function (err, data) {
    const paramsToS3: AWS.S3.PutObjectRequest = {
      Body: JSON.stringify(data.Labels, null, 2),
      Bucket: '<myserverless20181214>-output-tags',
      Key: `${key}.json`
    };

    // AWS S3に出力する
    s3.putObject(paramsToS3, function (err, data) {
      console.log("成功！");
    });
  });
}
```

上記プログラムをコピペしてもいいですが、たとえば`const paramsToRekognition: AWS.Rekognition.DetectLabelsRequest = { ...`等は、実際に手で書いてみるとエディタの補完が効いて簡単に記述できるのが実感できると思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/838972f9-d52d-2c46-c808-77b4d14527e1.png)

`: AWS.Rekognition.DetectLabelsRequest`の部分はTypeScriptの**型情報**であり、大雑把に言うと「この変数にはこんなデータが入りますよ」という情報が定義されています。AWSとの連携は独自のメソッドやパラメータが多くて覚えづらいですが、TypeScriptの型情報があれば覚える必要はありません。エディタはその情報に沿って、補完を出してくれます。例えば「大文字か小文字かわかりづらい」ような時も、補完があれば間違うことはありません。

### デプロイ
完成したら、先程と同じく下記コマンドでデプロイします。

```bash
$ serverless deploy
```
最初にデプロイした物は上書きされて新しい物だけになるので、何回デプロイしても大丈夫です。

### 使ってみる
画像をアップロードしてみます。AWSのタブをもうひとつ開き

1. 画面左上「サービス」から「S3」
1. `<myserverless20181214>-input-image`というバケットを選択
1. 画面上に適当な画像をドラッグアンドドロップ
1. 「アップロード」を選択
1. 「Amazon S3」に戻り、`<myserverless20181214>-output-tags`というバケットを選択
1. JSONファイルがあるはずなので、開く

JSONファイルが作成され、ラベル情報が付いていれば成功です。

完成品はこちらにも置いてあります。
https://github.com/s2terminal/serverless-aws-sample

### うまくいかないばあい
**AWS CloudWatch**を使って、デバッグログを確認します。

1. 画面左上「サービス」から「Lambda」
1. 「my-serverless-sample」を選択
1. 「モニタリング」
1. 「CloudWatchのログを表示」
1. 時間帯が最も近いログを選択

プログラム中で`console.log()`を使った場合なども、ここに出力されます。
`成功！`が表示されていれば成功しているはずです。

### 片付け
S3にアップロードしたファイルは、手動で削除しておきます。

下記のコマンドで、`serverless deploy`した物は削除できます。

```bash
$ serverless remove
```
S3内のファイルを削除していないと、このコマンドでS3バケットを削除できないのでご注意下さい。

最後に、Cloud9の開発環境も、削除しておきます。

1. 画面左上「サービス」から「Cloud9」
1. my-serverless-dev(付けた名前)を選択し「Delete」

## 参考: AWSをはじめて使う人向け
AWSのアカウントを作成したばかりであれば12ヶ月間は**無料利用枠**があるため、今回紹介した利用範囲内の小規模・低スペック用途であれば、ほぼ無料で利用することができます。

画面右上の「自分のアカウント名」→「請求ダッシュボード」から、実際の利用料金がいくらなのか確認できます。

また、万が一「Cloud9環境を立ち上げっぱなしにしてしまった」などで意図しない請求が発生しないよう、課金金額が一定額を超えたらアラートが飛ぶように設定することもできます。
参考: [アラートと通知で請求額をモニタリング \- AWS 請求情報とコスト管理](https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/monitor-charges.html)

### 今回つかったサービスの料金
- AWS Cloud9
    - Cloud9自体は無料
    - EC2: 1時間あたり0.0116ドル
    - [AWS Cloud9 の料金 – アマゾン ウェブ サービス](https://aws.amazon.com/jp/cloud9/pricing/)
- AWS S3
    - 1GBあたり約0.023ドル
    - [料金 \- Amazon S3（クラウドストレージ）｜AWS](https://aws.amazon.com/jp/s3/pricing/)
- AWS Lambda
    - RAM1024MBで、1回1,000msecかかるとして、実行1,000回あたり0.01667ドル
    - [料金 \- AWS Lambda（サーバーレスでコードを実行）｜AWS](https://aws.amazon.com/jp/lambda/pricing/)
- AWS Rekognition
    - 画像10枚あたり0.01ドル
    - [【Amazon Rekognition】料金（人工知能・機械学習の画像認識・画像解析） \| AWS](https://aws.amazon.com/jp/rekognition/pricing/)

## 参考: もっと情報が欲しい人向け
以下をやってみましょう。

- たとえばServerlessを活用して、AWSのS3やRekognition以外の他サービス(API Gateway、Dynamo DB等)と連携してみたり
- たとえばTypeScriptではない言語をLambdaで動かしてみたり
- たとえばTypeScript+webpackを活用して、WebフロントエンドやCLIアプリケーションを作ってみたり
- たとえばMicrosoft AzureやGoogle等の他のAIサービスと連携してみたり

## 参考: ローカルで開発する人向け
今回つかったCloud9環境にインストールされていたNode.jsのバージョンはv6.15です。

```bash
$ node --version
v6.15.0
```
これよりも新しいバージョンであれば基本的には問題なく動作すると思います。

### AWS IAMの設定
AWS Cloud9では必要ありませんでしたが、Serverless DeployするためのIAMアカウントを作る必要があります。

https://serverless.com/framework/docs/providers/aws/guide/credentials/ を参考に

1. AWS consoleを開く
1. 「IAM」→「ユーザー」→「ユーザーを追加」
1. ユーザー名を、例えば「serverless-sample-user」等で設定
1. アクセスの種類「プログラムによるアクセス」
1. 「既存のポリシーを直接アタッチ」→「AdministratorAccess」
1. 下記を実行

```bash
$ export AWS_ACCESS_KEY_ID=XXXX # 画面に表示されたアクセスキー
$ export AWS_SECRET_ACCESS_KEY=XXXX # 画面に表示されたシークレットアクセスキー
```
これで`$ serverless deploy` が実行可能になります。

ご自身でアクセスキーを管理する場合は、AWSが開発している**git-secrets**を使うのがおすすめです。誤ってcommitにシークレットアクセスキーを含めてしまう、といった事故を予防できます。

https://github.com/awslabs/git-secrets

## まとめ
Serverless Frameworkを使うことで、簡単にサーバレスアプリケーションを開発することができました。またTypeScriptを使うことでAWSと連携するプログラムが記述しやすくなる事もわかりました。

ぜひServerlessやTypeScriptを活用してみて下さい。

### 参考
- [TypeScriptでCLIツールを作って、npmパッケージにする \- Qiita](https://qiita.com/suzuki_sh/items/f3349efbfe1bdfc0c634)
- [たのしいTypeScript \- Qiita](https://qiita.com/suzuki_sh/items/56cd103b713de9bae77c)