Private SubnetにFargateを構築し、SQS・DynamoDBと連携する。

## はじめに
FargateをAPIバックエンド的な使い方をした構成を構築してみたかった。Webサービスは特にホストする予定はなく、APIのバックエンド処理をFargateのサービスタイプを利用した構成で試すことが目的。ちなみにこの構成を考える際に悩んだような部分は以下の点。

### 悩み
・Private SubnetにFargateを構築して色々なAWSサービスと統合するには、VPCエンドポイントが必要で、エンドポイント一つにつき月額10USDくらいかかってくる。
　⇒思ったよりも作成するエンドポイントが多そうで、重なるとだいぶコストになる。。。
・Fargateをサービスタイプで起動すると、こちらもコストがかかってくる。
・でも実際にFargateをPrivate Subnetでサービスとして構築してみたい。
・Lambdaを利用した既存構成はできているから、一時的にLambda部分をFargateに置き換えて検証しよう。
・検証が終わったらLambdaの構成に戻し、Fargate関連のサービスは削除しよう。(悲しいけど)
・削除漏れがあると嫌なので、Terraformで構築しよう。（一気にクリーンアップできるし、今後の資産にもなる）
　⇒この点は是非実践してみたかったが、Terraformを整備するのに時間がかかりそうで直ぐにはできなそうだったので、結局コンソールで作成することにした。代わりの他の構成部分をTerraformで整備していく方針。

### 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/fb64980a-8e22-c94b-ee45-dfe905371bf6.png)
Subscriber Lambdaの代わりとなるSubscriber Fargateを構築する。ただしある程度検証が終わったらLambdaの利用に戻す。

ちなみにこれまでの構成については以下の記事などで記録している。
↓

https://qiita.com/kennyQiita/items/acb8988d8ee2c64a4b71


## 実施内容
・Subscriber用のDockerイメージを格納するリポジトリの作成
・Subscriber用のDockerイメージの作成
・Subscriber用のECSクラスターの作成
・ECR用VPCエンドポイントの作成
・S3用VPCエンドポイントの作成
・CloudWatchLogs用のエンドポイント＋セキュリティグループの作成
・SQS用VPCエンドポイントの作成
・DynamoDB用VPCエンドポイントの作成
・IAMロールの設定確認
・ECS Task定義（Subscriber用）
・Fargateサービスのデプロイ
・動作確認

## ECRリポジトリ作成
`sub-repo`としてリポジトリを作成する。このリポジトリへ作成したDockerイメージをプッシュしていく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/adcf01a0-1e35-343e-c053-09145a6c0d68.png)

## Subscriber用のDockerイメージの作成 
`DockerFile`、`msg_subscriber.py`、`requirement.txt`をそれぞれ作成する
```Dockerfile:DockerFile
FROM python:3.8

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY msg_subscriber.py ./

ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["python", "./msg_subscriber.py"]

```
```python:msg_subscriber.py
import json
import boto3
from datetime import datetime
import os

queue_name = os.environ['QueueName']
sqs_endpoint_url = os.environ['SQSEndpointUrl']

dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
table = dynamodb.Table('sns_sqs_lambda_table')
sqs = boto3.resource('sqs', region_name='ap-northeast-1', endpoint_url=sqs_endpoint_url)
queue = sqs.get_queue_by_name(QueueName=queue_name)

def logging(errorLv, funcName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y%m%d %H:%M:%S')
    print(loggingDateStr + " " + funcName + " " + "[" + errorLv + "] " + errorMsg)
    return

def main():
  
  logging("info", "subscriber fargate", "service started")
  print("subscribe fargate started..")
  
  message = queue.receive_messages(MaxNumberOfMessages=1)
  result = ''
  
  if message:
    print("fetched message: " + message[0].body)
    try:
        result = table.put_item(Item={
            'message_id': message[0].message_id
            'subject': 'fixed subject',
            'message': message[0].body,
            'timestamp': datetime.now().isoformat(timespec='seconds')
        })
      
    except Exception as e:
        print(e)
        raise e

  else:
    print("there is no message.")
  
  print(result)
  return result

if __name__ == '__main__':
  main()
```
シンプルにSQSからメッセージを1件取得してくる内容。`QueueName`,`SQSEndpointUrl`はECSTask定義を作成する際に環境変数に設定する。（一度これを忘れてエラーとなった）
`boto3.resource('sqs')`の仕様などは以下の公式ガイドなどで確認できる。

https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html


```requirement.txt
boto3==1.9.183
```

### リポジトリへプッシュ
記述DockerイメージをビルドしてECRリポジトリへプッシュする。
ECRにログインし、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/4a73cef6-e916-d88b-14c1-bc149e38768f.png)
イメージをビルドし、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a17febe3-5fa0-5dbf-81a9-77800278d2b5.png)
イメージにタグ付けをし、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/4d2c4fba-94e2-b9ca-c198-711450d08f74.png)
プッシュ実行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ac14f4ab-4347-9cb5-0975-3773eae63367.png)

## ECS Clusterの作成
次にコンテナを実行するクラスターを作成していく。といってもAWSコンソールで数クリックで完了する。
今回はNetworking onlyを選択し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/7c4e88ff-dbe1-44cf-3520-d0267ddebfa8.png)
クラスター名は任意に設定し`Create`。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/f3f4eefb-d830-d3c2-e949-a608b3f756a4.png)

## VPCエンドポイントの作成
今回はプライベートサブネットにFargateを作成し、各種のAWSサービスと連携させるため複数のVPCエンドポイントが必要になる。今回はFargateを利用するため、以下の公式ドキュメントにもあるようにECR用のエンドポイント（2種類）とS3エンドポイントがまずは必要。

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-vpc-endpoint-considerations

### ECR-dkr用エンドポイントの作成
`com.amazonaws.region.ecr.dkr`エンドポイントを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/7f15755e-51ba-aa88-bc57-90662fab77db.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ef8afb89-b381-7b6d-e24b-da90ec1a1c70.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/0c6ed80a-3c1e-e888-6e45-1b6f1136719e.png)

### ECR-api用エンドポイントの作成
`com.amazonaws.region.ecr.api`エンドポイントを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a7043a01-d90a-4c02-074f-b81fa07ecb98.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/caa1f93c-f894-e446-39d6-dc0dfe6f36f9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/c723cefe-5608-29c8-4170-b9d4e453c867.png)

### S3用のエンドポイントの作成
`com.amazonaws.region.s3`を作成。ゲートウェイ型。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/8c4768b1-0e1a-359a-3517-ef2856451e53.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ba58f1db-a6eb-439c-84b9-a3fb296181c7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/0a6c7952-4734-4844-f455-189228779291.png)

### CloudWatchLog用のエンドポイントの作成
FargateからCloudWatchLogsへログを配信したいので、`com.amazonaws.region.logs`エンドポイントを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/3cf3221e-3304-717d-1806-95da635e6d8a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/033b31ab-4b0e-22aa-416c-e8afcab77df3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/2ccce119-2105-8196-4f0c-583273a8a7db.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/2f540abe-3804-8159-140e-7010aa729bbf.png)

### CloudWatchLogs用エンドポイントに適用するセキュリティグループの設定
Fargateに適用したセキュリティグループを送信元として、443/tcpのインバウンド通信を許可する設定となればOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/da523209-647c-c72b-371c-1cc2bf28fcf9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/01eae09f-22bd-4ead-0e1c-fb010b1978ec.png)

### SQS用エンドポイントの作成
Private SubnetのFargateからSQSにアクセスするためにSQS用のVPCエンドポイントも作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/5742a6ce-6cf4-9c66-f5c6-075291735963.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/d5203881-6101-7d0a-0ab2-b7226d740082.png)

### DynamoDB用エンドポイントの作成
SQSから取得したメッセージの内容をDynamoDBにPutする処理が含まれているため、DynamoDBにアクセスさせるためにDynamoDB用のエンドポイントを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/f8bb8b96-df92-2eb9-b1f0-efcd81f612f6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/74873c02-9f89-805b-6fad-f5c1651f03f5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/78e6a6c2-7112-bd9a-e072-7afd0decc036.png)

ここまでの設定で合計6個のVPCエンドポイントが作成された。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/6d5720f8-a97e-cb94-bc37-1366b8147f51.png)


### IAMロール確認
ECSに付与しているIAMロールの権限を確認。DynamoDBのテーブルにPutItemするポリシーやSQSへのアクセスポリシーを追加している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/7c9b4baf-c96c-398c-742a-3ab8772ad7bb.png)

## Task定義の作成
SubscriberのFargateを実行する際のTask定義をしていく。Image URIには作成したリポジトリを指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/b661f34e-3350-1ffb-694a-0cc759fa4ae0.png)
pythonスクリプトで参照している環境変数を設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/aeb79cfa-2e02-e8f6-9b5b-8bdef513ba12.png)
Commandに`date`を記述しているがここは無くてもOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/c9fb5872-ba75-890e-bc8b-4b8c195f60bc.png)
Fargateを指定し、Taskのサイズも最小を指定。Task roleとTask execution roleはひとまず先ほど確認した同一のロールを指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/529c60b3-b22a-66d7-432b-3897c0194dee.png)
CloudWatchLogsへログが配信されるようにログ構成も設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/b055d56b-db80-dd52-3e57-3aa5b235f0da.png)

## ECSサービスのデプロイ
作成したタスク定義からサービスを作成（デプロイ）していく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a1e3e2be-2923-06c8-6b3c-4f1f74dde57a.png)
Launch typeはFargateを指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/3f6461bb-f224-f351-aa0e-a7e0fcfb5d04.png)
Application TypeはServiceとする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/f5e57efb-3925-cbfa-9ca6-243651823387.png)
この辺はデフォルト値のまま
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/8aa7bfd6-792b-b2cd-389d-06a4f3f59bdf.png)
Private Subnetを選択し、Private Subnetのインスタンス用のSGを指定。
またPublic IPは不要しないように設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/3566bd46-5154-5826-8310-351449f96672.png)
以下は一旦デフォルト値。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/1c4cb7bb-a132-f62a-7cdb-58dd5368b2e8.png)

## 動作確認
PostmanからREST APIでリクエストを実施。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/c89f8ca2-6349-9b19-aadd-006fea8b93ad.png)

後続のLambdaによりSQS Queueにメッセージが追加された。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/44e3e881-e81e-368b-8b33-b76d59bc2064.png)

デプロイしたServiceによりTaskが起動。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/bac054f4-4db0-6767-5201-0549a80252bb.png)

PythonプログラムによるDynamoDBへの新規Item格納も無事に確認できた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/08f35ca5-0f60-3270-1fcf-1bfd537f3fdc.png)

CloudWatchLogsでもログ出力を確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/37914ff7-6dd3-6703-8fd3-e2f68ef99fec.png)

手元のメールボックスにもメール通知受信OK。

## 追記
これまでの実装だとメッセージを処理した後にメッセージがキューから消えずにデッドレターキューに残ってしまっていた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/e3d61ff2-d61d-1021-e362-64f45e48d0e6.png)

公式ドキュメントにもあるがメッセージは処理した後に削除しないと、設定しているデッドレターキューに移動されてしまう。以下のドキュメントなども参考にしながら、処理したメッセージを削除するコードを追加した。

https://boto3.amazonaws.com/v1/documentation/api/1.9.42/guide/sqs.html#sqs

```python:msg_subscriber.py
import json
import boto3
from datetime import datetime
import os

queue_name = os.environ['QueueName']
sqs_endpoint_url = os.environ['SQSEndpointUrl']

dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
table = dynamodb.Table('sns_sqs_lambda_table')
sqs = boto3.resource('sqs', region_name='ap-northeast-1', endpoint_url=sqs_endpoint_url)

queue = sqs.get_queue_by_name(QueueName=queue_name)

def logging(errorLv, funcName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y%m%d %H:%M:%S')
    print(loggingDateStr + " " + funcName + " " + "[" + errorLv + "] " + errorMsg)
    return

def main():
  while True:
  
    logging("info", "subscriber fargate", "service started")
    print('subscribe fargate started..')
    
    message = queue.receive_messages(MaxNumberOfMessages=1)
    result = ''
    
    if message:
      print("fetched message: ") 
      print(message[0])
      print("fetched message body: " + message[0].body)
      try:
          result = table.put_item(Item={
              'message_id':message[0].message_id,
              'subject': 'fixed subject',
              'message': message[0].body,
              'timestamp': datetime.now().isoformat(timespec='seconds')
          })
        
      except Exception as e:
          print(e)
          raise e
          
      message[0].delete()
      print("message has been deleted from sqs queue.")
  
    else:
      print("there is no message.")
    
    print(result)
    return result

if __name__ == '__main__':
  main()
```

## 終わりに
今回はPubSubバックエンドにAPIを処理する機構としてFargateのサービスタイプでの挙動を検証した。
検証期間は途中作業できない日もあったり、土日も挟んだので1週間ほどだったが、AWS料金が$15USDくらいいってしまった。やはりVPCエンドポイントは月額で固定料金がかかってくるので、この構成の維持は不要だし一旦Lambda利用に戻そうと思う。FargateはSpotを利用したりしてなるべくコスト軽減を試みたが、VPCエンドポイントの課金は避けられない。。Fargateのサービス起動という観点だけでなく、PrivateSubnetを利用して各AWSサービスに接続しにいくにはVPCエンドポイントも結構必要であることを再確認できたのはよかった。（NATを構築すればそれでも可能だと思うが、NATでも通信に応じた課金が発生する。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ff83be0a-1135-6638-2004-64ac4450113b.png)

## ハマったこと
SQSをpythonから操作する場合にboto3では`boto3.client('sqs')`でも操作できるし、`boto3.resource('sqs')`でも操作ができる。Lambdaでの実装は`client`を使っていたが、Fargateでは参考サイトなども参照にした際に`resource`の方を使っていたので、API仕様が異なっておりその部分を整理して取り組む必要があった。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html

## 参考サイト

https://awstut.com/2022/07/24/create-ecs-fargate-in-private-subnet/

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-vpc-endpoint-considerations

https://qiita.com/AWS11077/items/8332bd476124bc534c52