💡

Litestream on App RunnerでS3にSQLiteをレプリケートしつつアプリをホスティングする

2022/07/31に公開約10,000字

はじめに

今回は、SQLiteを使ったWebアプリケーションをAWS App Runner上にホスティングします。同時に少し前話題になったSQLiteをAmazon S3にレプリケーションできるLitestreamを用いてS3にデータをレプケーションしてみます。
プロトタイピングや個人開発で、マネージドRDBMSを利用せず、料金抑えたい場合に使えるかなーと考え、App Runnerの素振りも兼ねて試してみました。

本構成における注意点

App Runnerのコンテナインスタンス内のファイルシステムの特性は、AWS App Runner Developer Guideから、引用します

Storage – App Runner implements the file system in your container instance as ephemeral storage. Files are transient. For example, they don't persist when you pause and resume your App Runner service. More generally, files aren't guaranteed to persist beyond the processing of a single request, as part of the stateless nature of your application. Stored files do, however, take up part of the storage allocation of your App Runner service for the duration of their lifespan.
Note
Although ephemeral storage files might not persist across requests, they sometimes do persist. This can be useful in certain situations. For example, when handling a request, you can cache files that your application downloads if future requests might need them. This might speed up future request handling, but can't guarantee the speed gains. Your code shouldn't assume that a file that has been downloaded in a previous request still exists.

以上を私の言葉でまとめてみます。

  1. ファイルシステムは揮発性ディスクで、ファイルは一時的
  2. サービスを一時停止、再開してもファイルは持続しない
  3. 1つのリクエストの処理を超えてファイルが持続することは保証されていない

1, 2は、コンテナ起動時にS3からLitestremでリストアすればよいですが、3に関してはどうしようもありません。ただ3は持続する場合もあり、キャッシュとして活用するケースが紹介されています。
以上より、ファイルが揮発する可能性を許容可能であれば活用できます。またLitestreamは、任意の間隔でスナップショット取得可能で、アプリでロジックを組んで対策もできるかもしれません(本記事では対象外)。
ストレージに関しては、記載の曖昧さが残っており、ロードマップでも改善が提案されています。本構成はEFSがVPC Connector経由で利用できるようになった場合はそちらを活用するのが良いと思います。

構成

前述したとおり、構成は以下の図の通りです。

image

コンテナの動作としては、以下のようになることが推測できます。

コンテナ 挙動
起動時 S3からDBをローカルにリストア
起動中 S3へDBをレプリケート
停止時 リプレケート停止
再開時 S3からDBをローカルにリストア

構築

リポジトリ

GitHubにソースを公開していますので、cloneした上で作業して頂くのが良いと思います。README.mdにAWS環境作成手順を別途乗せていますので、合わせてご確認頂けたら幸いです。

https://github.com/shuntaka9576/go_api_sqlite

ECR作成

CDKを利用して、ECR作成

ECRの作成
yarn cdk deploy -c stageName=dev dev-go-api-sqlite-ecr

GoのAPIサンプル入りコンテナイメージをECRへ登録

コンテナイメージをビルドします。

Dockerfile
FROM golang:alpine AS build-stage
RUN apk add alpine-sdk
ADD . /app
WORKDIR /app
RUN go build -o app .

FROM alpine:latest
COPY --from=build-stage /app/app /usr/local/bin/app
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.8/litestream-v0.3.8-linux-amd64-static.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz
COPY entrypoint.sh /usr/local/bin
COPY litestream.yml /etc/litestream.yml
RUN chmod +x /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
entrypoint.sh
#!/bin/sh
echo "start container entry point"
set -e

if [ -f ./todo.db ]; then
  echo "delete $DB_PATH"
  rm $DB_PATH
fi

# アプリ起動前にSQLiteをリストア
litestream restore -v -if-replica-exists -o $DB_PATH s3://$REPLICATE_BUCKET_NAME/replica

# Litestreamのレプリケートを実行しつつ、APIアプリを起動
exec litestream replicate -exec "/usr/local/bin/app"
コンテナビルド
make build
# or docker build -t go-api-sqlite:latest

作成されたECRのURL(<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com)を持ちいて、

# ECRへログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
# latestタグうち
docker tag go-api-sqlite:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/go-api-sqlite:latest
# コンテナをECRへpush
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/go-api-sqlite:latest

App Runnerの作成

詳しい手順はこちら

$ yarn cdk deploy -c stageName=dev dev-go-api-sqlite-app-runner 
...
Do you wish to deploy these changes (y/n)? y
dev-go-api-sqlite-app-runner: deploying...
[0%] start: Publishing 9f890b3b26c642c19b4bb1b96e6fc2a31bfa0dde3be73a897363b9b4c5614414:current_account-current_region
[100%] success: Published 9f890b3b26c642c19b4bb1b96e6fc2a31bfa0dde3be73a897363b9b4c5614414:current_account-current_region
dev-go-api-sqlite-app-runner: creating CloudFormation changeset...

 ✅  dev-go-api-sqlite-app-runner

✨  Deployment time: 341.31s

Outputs:
dev-go-api-sqlite-app-runner.AppRunnerOutput = https://<個人>.ap-northeast-1.awsapprunner.com
curl -XGET https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks
curl -XPOST https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks -d '{"title": "test1"}'
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:<アカウントID>:stack/dev-go-api-sqlite-app-runner/f**

✨  Total time: 348.17s

Outputsで出力されたコマンドを実行し、結果が返却されたらApp Runnerで正常にアプリが動作しています。

$ curl -XPOST https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks -d '{"title": "test1"}'
{"id":1}
$ curl -XPOST https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks -d '{"title": "test1"}'
{"id":2}
$ curl -XGET https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks
[{"id":1,"title":"test1","status":"todo","created":"2022-07-31 05:45:20"},{"id":2,"title":"test1","status":"todo","created":"2022-07-31 05:45:43"}]

動作確認

ローカル環境からリストア

LitestreamでSQLiteがレプリケートされているS3を指定し、ローカルPCにDBをリストアしてみます。

$ litestream restore -v -if-replica-exists -o todo.db s3://dev-go-api-sqlite-replica-<アカウントID>/replica
2022/07/31 15:01:08.151456 s3: restoring snapshot 86de7c7a0d711718/00000003 to todo.db.tmp
2022/07/31 15:01:08.212286 s3: restoring wal files: generation=86de7c7a0d711718 index=[00000003,00000003]
2022/07/31 15:01:08.247536 s3: downloaded wal 86de7c7a0d711718/00000003 elapsed=35.202577ms
2022/07/31 15:01:08.269046 s3: applied wal 86de7c7a0d711718/00000003 elapsed=21.478191ms
2022/07/31 15:01:08.269072 s3: renaming database from temporary location

$ sqlite3 todo.db
SQLite version 3.32.3 2020-06-18 14:16:19
Enter ".help" for usage hints.
sqlite> select * from task;
1|test1|todo|2022-07-31 05:45:20|2022-07-31 05:45:20
2|test1|todo|2022-07-31 05:45:43|2022-07-31 05:45:43

App Runnerを停止->再開して、自動でリストアされるか確認

停止を確認
スクリーンショット 2022-07-31 15 03 37
スクリーンショット 2022-07-31 15 08 51

再開後、APIでリスト取得します

$ curl -XGET https://<リソース毎>.ap-northeast-1.awsapprunner.com/tasks
[{"id":1,"title":"test1","status":"todo","created":"2022-07-31 05:45:20"},{"id":2,"title":"test1","status":"todo","created":"2022-07-31 05:45:43"}]

データが返却されていることから、リストアされていることが分かります。

CloudWatch Logsから処理の内容を確認出来ます。
スクリーンショット 2022-07-31 15 17 03

補足

AutoScale設定

複数インスタンスが起動すると、整合性が取れなくなるため、AutoScale設定の最大インスタンス数は1にするのが良いと思います。(これが有効かどうか未確認)

スクリーンショット 2022-07-31 15 22 48

過去のスナップショットを復元する

今回Litestreamの設定は、30s間隔でスナップショットを作成しています。

litestream.yml
dbs:
 - path: $DB_PATH
   replicas:
     - type: s3
       bucket: $REPLICATE_BUCKET_NAME
       path: replica
       region: ap-northeast-1
       retention: 120h # WALファイルを保持期間
       snapshot-interval: 30s # スナップショット取得インターバル

litestream snapshotsでスナップショット一覧を確認できます。litestream restoreでスナップショット指定リストアが可能です。
万が一データが揮発した場合、一度アプリを停止して、新しい世代を削除するのが良さそうです。もっとよい復旧手順が見つかりましたら、記事にしようと思います。

$ litestream snapshots s3://dev-go-api-sqlite-replica-<アカウントID>/replica
replica  generation        index  size  created
s3       3041d4387f582d1c  0      534   2022-07-31T00:41:49Z
s3       3041d4387f582d1c  1      534   2022-07-31T00:47:19Z
s3       3041d4387f582d1c  2      627   2022-07-31T00:47:49Z
s3       6e1bf4d6faafb5b8  0      534   2022-07-31T00:40:24Z

$ litestream restore -o test.db -generation 6e1bf4d6faafb5b8 s3://dev-go-api-sqlite-replica-<アカウントID>/replica

料金

App Runnerの料金は公式を引用します。

アプリケーションをテスト中で、App Runner サービスでは毎日 2 時間ずつ、1 秒あたり 2 リクエスト分のトラフィックが発生します。App Runner は、受信したリクエストを処理するためにサービスをアクティブなコンテナインスタンス 1 つ分のみスケールアップし、毎日 2 時間、コンテナインスタンスのメモリをプロビジョニングします。コスト削減のために、1 日のうち残りの 22 時間はサービスを一時停止します。

4.80 USD/月

毎日 8 時間、毎秒約 80 回のリクエストが散発的に発生します。App Runner は、受信したリクエストを処理するためにサービスをコンテナ 1 つ分のみスケールアップし、毎日 24 時間、コンテナインスタンスのメモリをプロビジョニングします。

25.50 USD/月

1 vCPU、2 GBのコンテナサイズを利用し、リクエストを処理せず、1日動かした料金が約$0.34(※1)なので、1月で約$10程度(※2)となります。プラスで以下の料金が発生します。

  1. リクエスト処理に使用されるコンピューティングリソース
  2. CloudWatch Logs転送
  3. S3下り

Litestreamが項1にどれくらい影響を与えるか未知なので、しばらく動作させてみて検証しようと思います。

HerokuとCloudflare R2で同じ構成方が安そうな印象です(ただしHerokuは24時間でストレージが揮発しますので、App Runnerでどの程度揮発しないのかというところが観点になりそうです、、)

※1 0.007($)*2(GB)*24(h)
※2 0.007($)*2(GB)*24(h)*30(day) = 10.08

さいごに

App Runnerは初めて試してみましたが、手軽にECRからコンテナのホスティング公開が出来てよかったです。CDKでIaCが出来るところもよかったです。少し運用してみて、どれくらい揮発するかなど、分かったことがあった場合追記しようと思います。

参考資料

今回ホスティングしたGoアプリケーション

GoでAPI開発は経験がなかったので、非常に助かりました。。

Litestream関連

Discussion

ログインするとコメントできます