Litestream on App RunnerでS3にSQLiteをレプリケートしつつアプリをホスティングする
はじめに
今回は、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つのリクエストの処理を超えてファイルが持続することは保証されていない
1, 2は、コンテナ起動時にS3からLitestremでリストアすればよいですが、3に関してはどうしようもありません。ただ3は持続する場合もあり、キャッシュとして活用するケースが紹介されています。
以上より、ファイルが揮発する可能性を許容可能であれば活用できます。またLitestreamは、任意の間隔でスナップショット取得可能で、アプリでロジックを組んで対策もできるかもしれません(本記事では対象外)。
ストレージに関しては、記載の曖昧さが残っており、ロードマップでも改善が提案されています。本構成はEFSがVPC Connector経由で利用できるようになった場合はそちらを活用するのが良いと思います。
構成
前述したとおり、構成は以下の図の通りです。
コンテナの動作としては、以下のようになることが推測できます。
コンテナ | 挙動 |
---|---|
起動時 | S3からDBをローカルにリストア |
起動中 | S3へDBをレプリケート |
停止時 | リプレケート停止 |
再開時 | S3からDBをローカルにリストア |
構築
リポジトリ
GitHubにソースを公開していますので、cloneした上で作業して頂くのが良いと思います。README.mdにAWS環境作成手順を別途乗せていますので、合わせてご確認頂けたら幸いです。
ECR作成
CDKを利用して、ECR作成
yarn cdk deploy -c stageName=dev dev-go-api-sqlite-ecr
GoのAPIサンプル入りコンテナイメージをECRへ登録
コンテナイメージをビルドします。
FROM golang:alpine AS build-stage
RUN apk add alpine-sdk
ADD . /app
WORKDIR /app
RUN go build -o app .
FROM alpine:latest
COPY /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"]
#!/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を停止->再開して、自動でリストアされるか確認
停止を確認
再開後、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から処理の内容を確認出来ます。
補足
AutoScale設定
複数インスタンスが起動すると、整合性が取れなくなるため、AutoScale設定の最大インスタンス数は1にするのが良いと思います。(これが有効かどうか未確認)
過去のスナップショットを復元する
今回Litestreamの設定は、30s間隔でスナップショットを作成しています。
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)となります。プラスで以下の料金が発生します。
- リクエスト処理に使用されるコンピューティングリソース
- CloudWatch Logs転送
- 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言語Webアプリケーション開発
- budougumi0617/go_todo_appをベースにSQLiteに一部コードを差し替え
GoでAPI開発は経験がなかったので、非常に助かりました。。
Discussion