aws-cliとdockerでサーバレスアプリケーションを作ってみる

## はじめに
サーバレスの仕組みを理解するためにこちらの本をベースに学習した内容を記述します。
<a href="https://www.amazon.co.jp/Amazon-Web-Services%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AC%E3%82%B9%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%96%8B%E7%99%BA%E3%82%AC%E3%82%A4%E3%83%89-%E8%A5%BF%E8%B0%B7-%E5%9C%AD%E4%BB%8B/dp/4839964564?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=1CP4WHXW7ZUK5&keywords=%E8%A5%BF%E8%B0%B7+aws&qid=1655732277&sprefix=%E8%A5%BF%E8%B0%B7+a%2Caps%2C238&sr=8-5&linkCode=li2&tag=cocolonifty00-22&linkId=ed77e6ac8300d6166b9846c25fc774b0&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=4839964564&Format=_SL160_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=cocolonifty00-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=cocolonifty00-22&language=ja_JP&l=li2&o=9&a=4839964564" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
> [サポートサイト（サンプルコードあり）](https://book.mynavi.jp/supportsite/detail/9784839964566.html)

2020年にaws cliのdockerイメージが公開されたことによりこちらを利用しておりますので、一部dockerに合わせた内容となっています。

## AWS CLIインストール
macに直接入れてもよかったのですが、docker imageがあったのでそちらを使ってみることにしました。
> [AWS CLIインストール ユーザガイド（Docker）](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-docker.html)

```bash:
$ docker run --rm -it amazon/aws-cli --version
// imageがない場合は、pullされます
aws-cli/2.2.2 Python/3.8.8 Linux/4.19.121-linuxkit docker/x86_64.amzn.2 prompt/off
```

## AWS認証情報の作成
この手順は、既に手元PCでaws-cliが入っていたりする方は不要です。
なお、これは一度ミスっており、記事にしています。
> [aws-cliとdockerを使う際、認証情報のマウントを飛ばしてしまった件](https://qiita.com/c3drive/items/6619676aeef48b6fea45)

#### 1.マウントする場所を作る
私は、普段開発で使っているworkディレクトリ配下に`dockeraws/.aws`を作りました。

```bash:
$ mkdir {任意の場所}/dockeraws
$ mkdir {任意の場所}/dockeraws/.aws
$ cd {任意の場所}/dockeraws/.aws
```
#### 2.~/.aws/credentialsにマウントするファイル作成
ファイルの構成はこちらを参考にしました。
[設定ファイルと認証情報ファイルの設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html)

```bash
$ vi credentials
```

```:credentials
[default]
aws_access_key_id=XXXXXXXXXXXX
aws_secret_access_key=XXXXXXXXXXXXXXXXXXXXXXXX
```
#### 3.~/.aws/configにマウントするファイル作成
ファイルの構成はこちらを参考にしました。
> [設定ファイルと認証情報ファイルの設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html)

```bash
$ vi config
```

```:config
[default]
region=ap-northeast-1
output=json
```

#### 4.動作確認

```bash
$ docker run --rm -it -v {任意の場所}/dockeraws/.aws:/root/.aws amazon/aws-cli configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************2DLO shared-credentials-file
secret_key     ****************Re2n shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config
```
問題なさそうです。

### aliasの設定
docker imageを使う場合、`docker run --rm -it amazon/aws-cli`が`aws`とイコールになります。
長いのでエイリアスを設定することでdockerを意識せずに使えるようになります。
また、認証情報のマウントを含めておかないと認証情報を要するコマンド実行時にエラーとなります。
マウントしない場合、[こちら](https://qiita.com/c3drive/items/6619676aeef48b6fea45)と同じ目に遭います。

```bash:
$ alias aws='docker run --rm -it -v {任意の場所}/dockeraws/.aws:/root/.aws amazon/aws-cli'

// エイリアス設定前
$ aws --version
-bash: aws: command not found

// エイリアス設定後
$ aws --version
aws-cli/2.2.2 Python/3.8.8 Linux/4.19.121-linuxkit docker/x86_64.amzn.2 prompt/off
```
なお、時が経った時、macにawscliをインストールしたと勘違いする可能性があるため、実際にはawsから少し変えています。以後記事内ではこちらを使っていきます。

```bash:実際の設定
$ alias dockeraws='docker run --rm -it -v /Users/yukokanai/work/aws/dockeraws/.aws:/root/.aws amazon/aws-cli'
```
## Amazon Cloud Watchをトリガーに自動処理をする
やっと本題です。今回試す構成は以下の通り。

|  No.  |  サービス  |  役割  |
| ---- | ---- | ---- |
|  1  |  [Amazon Kinesis DataStreams](https://docs.aws.amazon.com/ja_jp/streams/latest/dev/introduction.html)  |  ※無料枠なし  |
|  2  |  [Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html)  |  ・Kinesisの書き込み件数（putレコード量）をモニタリングし一定量のレコードの場合、SNSへアラーム通知  |
|  3  |  [Amazon SNS](https://docs.aws.amazon.com/ja_jp/sns/latest/dg/welcome.html) |  ・アラーム通知先（トピック）  |
|  4  |  [Amazon Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html)  |  ・SNSトピックをサブスクライブして監視（トピック更新がトリガーで実行）<br>・Kinesisへリシャーディング、つまりシャード数を増やす。 |

#### 1.Amazon Kinesis DataStreamsのストリームを作成する
ストリーム名は、`my-sample`、初期シャード数は`1`とします。

```bash
$ dockeraws kinesis create-stream --stream-name my-sample --shard-count 1
```
次にこのストリームに書き込むpythonスクリプトを作成します。
ホストを汚したくないので、ここでもdockerを使います。
なお、私はjupyter/notebookが使えるdockerfileを使っていますのでそこに`boto3`を追加します。
（通常は、単純なpythonイメージで実現できると思いますので、Dockerfileはあくまで参考として載せます）

```dockerfile:Dockerfile
FROM ubuntu:latest
RUN apt-get update && apt-get install -y \
	sudo \
	wget \
	vim
WORKDIR /opt
RUN wget https://repo.continuum.io/archive/Anaconda3-2019.10-Linux-x86_64.sh && \
	sh /opt/Anaconda3-2019.10-Linux-x86_64.sh -b -p /opt/anaconda3 && \
	rm -f /opt/Anaconda3-2019.10-Linux-x86_64.sh
ENV PATH /opt/anaconda3/bin:$PATH

# Pythonのpakage管理ツール
RUN pip install --upgrade pip
RUN pip install boto3 //今回追加
WORKDIR /
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]
```
```bash
//boto3を追加したのでビルド
$ docker build -f ../Dockerfile.jupyterlab . -t miharasatsuki/jupyter-notebook
//　起動
$ docker run -p 8888:8888 -v /Users/yukokanai/work/aws/dockeraws/.aws:/root/.aws --name my-lab miharasatsuki/jupyter-notebook
```
起動できたら以下にアクセスして、スクリプトを作っていきます。
http://localhost:8888/
![スクリーンショット 2021-05-05 17.59.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/681ab63c-9642-82ac-ff06-07b11e6906b5.png)

```python:put-records.py
import boto3
import datetime
import time
import uuid

kinesis = boto3.client('kinesis')
stream_name = 'my-sample'

partition_key = str(uuid.uuid4())
data = datetime.datetime.utcnow().strftime('%s')

for i in range(15):
    kinesis.put_record(
    StreamName=stream_name,
    Data=data,
    PartitionKey=partition_key)
```
実行してエラーがないことを確認しました。

#### 2.Amazon SNSでトピックを作成する
アラームの通知先であるSNSトピックを作成します。

```bash
$ dockeraws sns create-topic --name my-sample
{
    "TopicArn": "arn:aws:sns:ap-northeast-1:1234567890:my-sample"
}
```
#### 3.Amazon CloudWatchでアラームを設定する
こちらのリファレンスを参考にしつつ設定します。
> [AWS CLI バージョン 2 リファレンスガイド aws/cloudwatch/put-metric-alarm](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudwatch/put-metric-alarm.html)

```bash
$ dockeraws cloudwatch put-metric-alarm \
 --alarm-name my-kinesis-mon --metric-name IncomingRecords \
 --namespace AWS/Kinesis --alarm-description "Alarm when Request exceeds 10 per minute" \
 --statistic Sum --period 60 --threshold 10 --comparison-operator GreaterThanThreshold \
 --dimensions "Name=StreamName,Value=my-sample" --evaluation-periods 1 \
 --alarm-actions arn:aws:sns:ap-northeast-1:1234567890:my-sample
```
アラーム状態を変更してテストします。レスポンスはありません。

```bash
$ dockeraws cloudwatch set-alarm-state --alarm-name my-kinesis-mon \
 --state-reason 'initializing' --state-value ALARM
```
状態が変わったことをコンソールから確認。
![スクリーンショット 2021-05-05 18.47.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/fe9a7b1c-3569-e7a5-c668-1c3a7932a075.png)

#### 4.Lambda関数の作成
こちらもpythonスクリプトで実装していきますが、Lambdaを使うにあたりIAM権限設定が必要なのでまずそちらを行なっていきます。
> [チュートリアル: Python 3.8 での Lambda 関数の作成](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package-create.html)

まずは、実行ロールを作成します。
こちらのリファレンスを参考にしつつ設定します。
> [AWS CLI バージョン 2 リファレンスガイド aws/iam/create-role](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/create-role.html)

ポリシーの内容をJSONかYAMLのドキュメントファイルとして渡さないといけないようなので作成します。

```json:trustpolicy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
作成したファイルを使ってIAMロールを作成します。
ただし、コンテナから参照されるファイルなので、ここでファイルパスをホストにしてしまうとファイルがないと言われます。
これを避けるため、作成したファイルを一時的にマウントします。
なお、`file://`は指定したいディレクトリの前につけます。

```bash:自身のホストファイルを直接指定した場合
$ dockeraws iam create-role --role-name resharding_function_role \
 --assume-role-policy-document file:///Users/yukokanai/work/aws/iamPolicy/trustpolicy.json
Error parsing parameter '--assume-role-policy-document': Unable to load paramfile file:///Users/yukokanai/work/aws/iamPolicy/trustpolicy.json: [Errno 2] No such file or directory: '/Users/yukokanai/work/aws/iamPolicy/trustpolicy.json'
```
```bash:自身のホストファイルをdockerにマウントし、マウント先を指定させる
$ docker run --rm -it \
 -v /Users/yukokanai/work/aws/iamPolicy/trustpolicy.json:/tmp/trustpolicy.json \
 -v /Users/yukokanai/work/aws/dockeraws/.aws:/root/.aws amazon/aws-cli \
 iam create-role --role-name resharding_function_role \
 --assume-role-policy-document file:///tmp/trustpolicy.json

{
    "Role": {
        "Path": "/",
        "RoleName": "resharding_function_role",
        "RoleId": "AROAWHUBMU2PMEM4ANAZK",
        "Arn": "arn:aws:iam::1234567890:role/resharding_function_role",
        "CreateDate": "2021-05-05T11:50:02+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}
```
続いて、ポリシーの適用です。またjsonを描いていきます。

```json:permission.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "cloudwatch:*",
                "logs:*",
                "kinesis:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
```
```bash:
$ docker run --rm -it \
 -v /Users/yukokanai/work/aws/iamPolicy/permission.json:/tmp/permission.json \
 -v /Users/yukokanai/work/aws/dockeraws/.aws:/root/.aws amazon/aws-cli \
 iam put-role-policy --role-name resharding_function_role \
 --policy-name basic-permission \
 --policy-document file:///tmp/permission.json
```
Lambdaで使うスクリプトを作成します。

```python:{任意の場所}/resharding-function/resharding-function.py
import boto3
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

kinesis = boto3.client('kinesis')
cloudwatch = boto3.client('cloudwatch')

def lambda_handler(event, context):
    event_data = json.dumps(event,ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': '))
    logger.info("Event"  + event_data)
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))
    
    alarm_name = message['AlarmName']
    stream_name = message['Trigger']['Dimensions'][0]['value']  

    if alarm_name == 'my-kinesis-mon':

        #現在のオープンシャード数を取得
        stream_summary = kinesis.describe_stream_summary(
            StreamName=stream_name
        )
        current_open_shard_count = stream_summary['StreamDescriptionSummary']['OpenShardCount']

        #シャードを2倍に変更
        target_shard_count=current_open_shard_count * 2
        response = kinesis.update_shard_count(
            StreamName=stream_name,
            TargetShardCount=target_shard_count,
            ScalingType='UNIFORM_SCALING'
        )

        #現在のアラームの閾値をシャード数×1000の80%に設定
        new_threshold = target_shard_count*1000*0.8
        #不具合　logger.info("Set a threshold value to " + new_threshold)
        logger.info("Set a threshold value to " + str(new_threshold))
        response = cloudwatch.put_metric_alarm(
            AlarmName='my-kinesis-mon',
            MetricName='IncomingRecords',
            Namespace='AWS/Kinesis',
            Period=60,
            EvaluationPeriods=1,
            ComparisonOperator='GreaterThanThreshold',
            Threshold=new_threshold,
            Statistic='Sum'
        )
```
#### 余談
なお、上記は基本サンプルコードそのままでname変えていましたが、不具合があり動きませんでしたので、
44行目あたりをコメントアウトし、`str()`しています。
以下、Lambdaのログ

```bash
[ERROR] TypeError: can only concatenate str (not "float") to str
Traceback (most recent call last):
  File "/var/task/resharding-function.py", line 44
```

デプロイパッケージにします。

```bash
$ cd resharding-function/
$ zip -r9 ~/work/aws/resharding-function/resharding-function.zip *
  adding: resharding-function.py (deflated 52%)
```
Lambdaファンクションを作成します。
その前に、aws-cli、意外にファイル指定の実行が多いのでaliasのマウント先を増やして、必要なファイルを突っ込んでいく方式に切り替えます。

```bash
$ mkdir /Users/yukokanai/work/aws/mount_tmp
$ alias dockeraws='docker run --rm -it -v /Users/yukokanai/work/aws/mount_tmp/.:/tmp/. -v /Users/yukokanai/work/aws/dockeraws/.aws:/root/.aws amazon/aws-cli'
```

```bash
$ mv ~/work/aws/resharding-function/resharding-function.zip /Users/yukokanai/work/aws/mount_tmp/
$ dockeraws lambda create-function --function-name my-resharding-function \
 --zip-file fileb:///tmp/resharding-function.zip \
 --role arn:aws:iam::1234567890:role/resharding_function_role \
 --handler resharding-function.lambda_handler --runtime python3.8

{
    "FunctionName": "my-resharding-function",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:1234567890:function:my-resharding-function",
    "Runtime": "python3.8",
    "Role": "arn:aws:iam::1234567890:role/resharding_function_role",
    "Handler": "resharding-function.lambda_handler",
    "CodeSize": 1040,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-05-05T12:38:12.416+0000",
    "CodeSha256": "PVlBjGTCr8I0a8gPUXuNk3sPxY7ku7fWvfRBwrylwfY=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "287bfee2-4259-489c-82c6-d5186d6c9874",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}
```
作成したLambda関数をアラーム通知先のトピックに紐付けます。

```bash:アクセス権を追加
$ dockeraws lambda add-permission --function-name my-resharding-function \
 --statement-id 1 --action "lambda:InvokeFunction" \
 --principal sns.amazonaws.com \
 --source-arn arn:aws:sns:ap-northeast-1:1234567890:my-sample

{
    "Statement": "{\"Sid\":\"1\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sns.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:1234567890:function:my-resharding-function\",\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:sns:ap-northeast-1:1234567890:my-sample\"}}}"
}
```
```bash:サブスクライバとしてLambda関数を設定
$ dockeraws sns subscribe \
 --topic-arn arn:aws:sns:ap-northeast-1:1234567890:my-sample \
 --protocol lambda --notification-endpoint \
 arn:aws:lambda:ap-northeast-1:1234567890:function:my-resharding-function

{
    "SubscriptionArn": "arn:aws:sns:ap-northeast-1:1234567890:my-sample:dc04eb52-1c99-4ce7-aafe-a0481846d4e6"
}
```

#### 5.アラームのテスト
最初に作成したスクリプトを使ってテストしてみましょう。
スクリプト実行後、シャード数が増えていることを確認します。

```bash:実行前のシャード数が１であることを確認
$ dockeraws kinesis describe-stream-summary --stream-name my-sample
{
    "StreamDescriptionSummary": {
        "StreamName": "my-sample",
        "StreamARN": "arn:aws:kinesis:ap-northeast-1:1234567890:stream/my-sample",
        "StreamStatus": "ACTIVE",
        "RetentionPeriodHours": 24,
        "StreamCreationTimestamp": "2021-05-05T07:17:29+00:00",
        "EnhancedMonitoring": [
            {
                "ShardLevelMetrics": []
            }
        ],
        "EncryptionType": "NONE",
        "OpenShardCount": 1,
        "ConsumerCount": 0
    }
}
```
スクリプトを実行します。

```bash:実行後
$ dockeraws kinesis describe-stream-summary --stream-name my-sample
{
    "StreamDescriptionSummary": {
        "StreamName": "my-sample",
        "StreamARN": "arn:aws:kinesis:ap-northeast-1:1234567890:stream/my-sample",
        "StreamStatus": "ACTIVE",
        "RetentionPeriodHours": 24,
        "StreamCreationTimestamp": "2021-05-05T07:17:29+00:00",
        "EnhancedMonitoring": [
            {
                "ShardLevelMetrics": []
            }
        ],
        "EncryptionType": "NONE",
        "OpenShardCount": 64,
        "ConsumerCount": 0
    }
}
```

# おわりに
サーバレスちょっとやってみようという気分で手を出すと大変な感じでした。
ありがとうございました。
料金のかかるKinesisは削除しました。