AWS EC2 のコスト計算 API を Rust で作成した

AWS EC2 のコスト計算をターミナルで行う

AWS EC2 のコスト計算を行う API を Rust で作成しました。フレームワークは Actix Web を使用しています。

github.com

以下の特徴とターミナルで JSON を加工できる点で、AWS 公式の オンデマンドインスタンス料金リザーブドインスタンス料金 または vantage といったサービスと比べて使いやすいかもしれません。

  • 結果がプログラマブルであること(レスポンスは JSON)
  • 単価だけでなく個数を指定して計算が可能であること
  • オンデマンドとリザーブド各種の料金を比較できること(各種: Upfront と Term の組み合わせ)
  • ドル/円の両方が表示できること

Rust 製 API をどこで使うべきか

Web サービスのバックエンドとして内部的に使用するマイクロサービスなどを作っていくのがよさそうです。

参考情報

Amazon Linux 2023 用の user_data.txt を作成する

AL2023 with Ruby, Python, Node.js, and Rust

EC2 環境を AL2023(Amazon Linux 2023) に切り替えるべく準備を進めています。AL2025 がリリースされる 2025年まで の 2 年間のベースとする予定です。

まずは作って壊せるスタンドアロンの開発環境(workstation)からはじめます。私の場合 1. VPC, Subnet, RouteTable, InternetGateway ... etc のレイヤは CloudFormation が担当し、2. EC2 や RDS のレイヤを ansible が担当します。個人的な開発環境の動作要件は以下になります。

  • mandatory:
    • Ruby, Python, Node.js, Rust の最新バージョンが利用可能であること
    • 必要なツール群が利用可能であること(jq, rg, fd, fzf)
  • optional:
    • ec2 instance connect で接続できること

僕が作った最強の user_data

こちらの user_data.txt を t4g.small で走らせると、インスタンスの起動から構築完了までの所要時間は約 20 分でした。

➜ pyenv exec ansible-playbook -i hosts site.yml --extra-vars "@workstation.json"

gist.github.com

➜ cat roles/spawn/tasks/main.yml
---
- name: Spawn an ec2 instance
  amazon.aws.ec2_instance:
    name: "{{ instance_name }}"
    key_name: "{{ key_name }}"
    vpc_subnet_id: "{{ vpc_subnet_id }}"
    instance_type: "{{ instance_type }}"
    security_groups: "{{ security_groups }}"
    network:
      assign_public_ip: true
      private_ip_address: "{{ private_ip_address }}"
    image_id: "{{ image_id }}"
    user_data: "{{ lookup('file', 'user_data.txt') }}"
    volumes:
      - device_name: /dev/xvda
        ebs:
          volume_size: 30
          volume_type: gp2
          delete_on_termination: true
    tags:
      Owner: "{{ owner }}"
      Stack: "{{ stack }}"
      Project: "{{ project }}"
      Application: "{{ application }}"
      Ec2InstanceConnectAccess: "{{ ec2_instance_connect_access }}"
➜ cat workstation/workstation.json
{
  "instance_name": "workstation",
  "key_name": "__key-name__",
  "vpc_subnet_id": "__subnet-id__",
  "instance_type": "t4g.small",
  "security_groups": ["__sg-id1__", "__sg-id2__"],
  "private_ip_address": "10.0.0.10",
  "image_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64",
  "owner": "kyagi",
  "stack": "deveplopment",
  "project": "workstation",
  "application": "hodgepodge of development tools",
  "ec2_instance_connect_access": "true"
}

シンボリックリンク的に指定できるイメージ ID 設定

上記の workstation.json では resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 のようにイメージ ID を指定しています。AL2023 から(?) image id がシンボリックリンク的に指定できるようになっています。リンク先の image id は自動的に最新のものが参照されるので ansible や Cfn で更新をする必要がなくなりました。

The following are dynamic kernel specifications. The default kernel version automatically changes with each major kernel version update.

al2023-ami-kernel-default-arm64 for arm64 architecture
al2023-ami-minimal-kernel-default-arm64 for arm64 architecture (minimal AMI)
al2023-ami-kernel-default-x86_64 for x86_64 architecture
al2023-ami-minimal-kernel-default-x86_64 for x86_64 architecture (minimal AMI)

https://docs.aws.amazon.com/linux/al2023/ug/get-started.html

EC2 instance connect 用のタグ設定

タグ Ec2InstanceConnectAccess: true が指定された EC2 は AWS Management Console から EC2 Instance Connect ができるように設定します。別途 Ec2 Instance Connect にはリージョンごとの特定の IP 範囲からの接続をセキュリティグループで許可する必要がある で紹介した IAM 設定が必要です。

参考情報

docker buildx でマルチプラットフォームのイメージを作成する

いつのまにかビルド時にランタイム時のターゲットプラットフォームを選択できるようになっていた

昨今は ARM 系(graviton) の EC2 を使用することが多いので、これは助かります。:-)

➜ cat Dockerfile
# syntax=docker/dockerfile:1
FROM alpine:3.16
RUN apk add curl

➜ docker buildx build --platform linux/amd64 -t sample:amd64 .
(... snip ...)
➜ docker buildx build --platform linux/arm64 -t sample:arm64 .
(... snip ...)

➜ docker images | rg sample
sample                                                      arm64     92ff9bc76d35   6 minutes ago   9.88MB
sample                                                      amd64     66864ae4d60a   7 minutes ago   10.2MB

➜ docker inspect 66864ae4d60a --format='{{.Os}}/{{.Architecture}}'
linux/amd64

➜ docker inspect 92ff9bc76d35 --format='{{.Os}}/{{.Architecture}}'
linux/arm64
➜ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT STATUS  BUILDKIT PLATFORMS
default *       docker
  default       default         running 20.10.24 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux   running 20.10.24 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

macOS Ventura へ homebrew 経由で docker desktop for Mac をインストールする場合は順番に注意

macOS Ventura 13.3.1 での docker desktop for Mac は 1. cask 経由で GUI などのコンポーネントを先行してインストールしてから 2. コマンドツールの docker をインストールする必要があります。

➜ brew install --cask docker
➜ brew install docker
➜ brew install docker-buildx
➜ rm '/opt/homebrew/etc/bash_completion.d/docker'
➜ brew link --overwrite docker 
➜ docker --version
Docker version 20.10.24, build 297e128

cask を先行させずコマンドラインツールの docker を先にインストールすると 23.0.0 がインストールされてしまいます。その場合 docker image build 時に以下のようなメッセージで docker-buildx のインストールを要求されます。将来的には docker-buildx は必須のコンポーネントになってくるようです。

DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

参考情報

フォームの入力を一部記憶してユーザーの入力作業を省力化する

英語レッスンで知らない単語がひとつ以上出てきた場合

Rails で個人用の英単語帳アプリを開発しています。開発環境は Rails 7.0.4 + Ruby 3.2.2 です。

オンライン英会話レッスンではインターネットの記事を教材にすることが多いです。その場合、同記事中に出現する複数の英単語を連続で単語帳に登録することになります。復習時に使用されていた文脈を辿れるようにするため、登録時にその単語の「出自(source)」となる記事の URL を保存するようにしています。

1 つ目の単語の登録時のフォーム画面

2 つ目の単語の登録時のフォーム画面(URL は入力する必要がない)

ユーザーが単語に出会った場所 入力例
レアジョブの教材の場合 https://www.rarejob.com/dna/2023/04/14/paris-aims-to-keep-olympians-cool-without-air-conditioners/
オフラインの小説や書籍の場合 Harry Potter and the Philosopher's Stone p.68

記事 URL や書籍のページ情報はどこに保存するべきか

HTTP はステートレスなので、どこかに情報(この場合は Source フィールドに入力する URL) を保存する必要があります。Rails では ActionDispatch::Session Module 配下のいくつかのクラスを 利用して保存先を選択できます。CookieStore, CasheStore, ActiveRecordStore, MemCacheStore のどれかを選択可能ですが、ここではデフォルトの CookieStore クラスを利用して Cookie に保存します。

RailsGuides > Action Controller Overview > 5. Session

5 Session

Your application has a session for each user in which you can store small amounts of data that will be persisted between requests. The session is only available in the controller and the view and can use one of several of different storage mechanisms:

  • ActionDispatch::Session::CookieStore - Stores everything on the client.
  • ActionDispatch::Session::CacheStore - Stores the data in the Rails cache.
  • ActionDispatch::Session::ActiveRecordStore - Stores the data in a database using Active Record (requires the activerecord-session_store gem).
  • ActionDispatch::Session::MemCacheStore - Stores the data in a memcached cluster (this is a legacy implementation; consider using CacheStore instead).

All session stores use a cookie to store a unique ID for each session (you must use a cookie, Rails will not allow you to pass the session ID in the URL as this is less secure).

ただ、今回の記事の URL や書籍のページ情報といった情報であれば CookieStore ではなく CacheStore を使うという選択肢もあるかもしれません。変更は config/initializers/assets.rb で Rails.application.config.session_store でクラスを指定するだけで、コード上の session (ハッシュのようにも見えるがインスタンスメソッド) は変わらず内部で切り替わるようです。このへんはフレームワーク様々でしょうか。

If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using ActionDispatch::Session::CacheStore.

なお、JavaScript を使う場合は localStorage への保存も選択肢にあがりそうです。localStorage は セキュリティやパフォーマンス上からは使用を控えるべき ですが、今回のようなセンシティブではない情報では検討してもよさそうです。

よりよい実装を求めて

同じことを JavaScript やフレームワークの機能を使ってもっとスマートに実装できる方法がありそうですが... フロントエンドのスキルが低いためこのような実装をしています (^_^;

参考情報

Ec2 Instance Connect にはリージョンごとの特定の IP 範囲からの接続をセキュリティグループで許可する必要がある

AWS Management Console からブラウザベースのターミナルで EC2 に接続する

AWS Management Console 内から任意の EC2 インスタンスに SSH する機能が EC2 Instance Connect です。CloudShell 同様、ブラウザベースのターミナルで接続することになります。EC2 および IAM の設定以外にも Security Group の Inbound Rules を設定する必要があるのですが、公開されているドキュメントからはやや辿りにくいので、紹介します。

ドキュメント によると 4 つのタスクがありますが、すでに公開鍵/秘密鍵のペアで EC2 に接続できる状態であれば Task1 のネットワークアクセスは問題ないはずで、Task 2 の ec2-instance-connnect パッケージと sshd 設定も Amazon Linux 2023 であればデフォルトで設定済みなので必要ありません。Task 3 の pip install ec2instanceconnectcli は Optional なので必要ありません。

問題は Task 4 でインスタンス ID ごともしくはインスタンス全体に "Action": "ec2-instance-connect:SendSSHPublicKey" を許可する必要があります。ここでは AWS CloudShell を使用して awscli を実行しました(なお CloudShell 上の vi でコピー&ペーストはインデントがおかしくなるので json はローカルマシンで編集してアップロードしています (^_^; )

[cloudshell-user@ip-10-12-34-56 ~]$ aws iam create-policy --policy-name Ec2InstanceConnectAccess --policy-document file://./Ec2InstanceConnectAccess.json

インスタンスIDごとにポリシーを設定している場合、別のインスタンスへの接続許可を与えるには配列を修正した後にポリシーを更新する必要があります(初回の新規作成は create-policy で 2 回目の更新からは create-policy-version)

[cloudshell-user@ip-10-12-34-56 ~]$ aws iam create-policy-version --policy-arn arn:aws:iam::__your_account_id__:policy/Ec2InstanceConnectAccess --policy-document file://./Ec2InstanceConnectAccess.json

ドキュメントには上記 4 つのタスクしか掲載されていませんが、これだけではまだ接続できず、Security Group の Inbound Rules にリージョンごとの特定の IP 範囲を指定してあげる必要があります。これは トラブルシューティング から確認できます。

参考情報

ゲストユーザ用のデータを実データから用意する

SignUp 以前のユーザーに、実際に「動いている様子」を体感してもらう

Rails で個人用の英単語帳アプリを開発しています。開発環境は Rails 7.0.4 + Ruby 3.2.2 です。

ごくごく限られた知り合いに実際にアプリを触ってもらってフィードバックをもらうために、ゲストユーザを作成することにしました。アカウントは guest@example.com です。

アプリの性質上、一定量のデータをストックした時に効果が実感できる機能があります。このため、ゲストユーザ用のアカウントにある程度の実データを入れておくことで潜在的なユーザにアプリをよりよく知ってもらうことができると考えています。一定量のデータがあってはじめて体感できる要素を以下に挙げます。

  • 機能
    • 「今日学んだ単語」「最近1週間で学んだ英単語」「最近1ヶ月で学んだ英単語」
    • ページネーションの UI
    • 検索(対象、範囲、パフォーマンス)
  • パフォーマンス

ゲストユーザでのサービス体験のもう一つの目的は登録に関連した心理的抵抗を減らすことです。メールアドレスや OAuth アカウントを必要としないので、セキュリティ上の不安を払拭できます。まずはサービスを体験してもらって、気に入ったら登録すればいい「登録の自由」を確保することで、本当にサービスを必要とするユーザーのみに登録してもらうことができるのではないかと考えています。

sqlite3 で自分の英単語データをゲストユーザ用にコピーする

自分自身がサービスの最初のユーザであるので、実データは自分の英単語帳を使用します (^_^; 自分の user_id が 1 でゲストユーザの user_id が 6 である場合、CREATE TEMPORARY TABLE で作成した一時的なテーブルを経由して user_id:1 が持つレコードを user_id:6 にコピーしています。

実データのコピー元を user_id:1, コピー先を user_id: 6 とする

irb(main):001:0> User.all
  User Load (0.6ms)  SELECT "users".* FROM "users"
=>                                                              
[#<User:0x000000010d2aa630 id: 1, firebase_local_id: "ABC....", created_at: Fri, 28 Oct 2022 15:33:11.535747000 UTC +00:00, updated_at: Sun, 09 Apr 2023 14:51:21.497815000 UTC +00:00>,
 #<User:0x000000010d8ad168 id: 6, firebase_local_id: "XYZ...", created_at: Sun, 05 Feb 2023 15:15:26.484686000 UTC +00:00, updated_at: Sun, 05 Feb 2023 15:15:26.484686000 UTC +00:00>]
irb(main):002:0> 

CREATE TEMPORARY TABLE で作成した一時的なテーブルを経由して user_id:1 が持つレコードを user_id:6 にコピーする

➜ sqlite3 development.sqlite3
SQLite version 3.39.5 2022-10-14 20:58:05
Enter ".help" for usage hints.

sqlite> .tables
ar_internal_metadata  schema_migrations
flashcards            users

sqlite> CREATE TEMPORARY TABLE ephemeral AS SELECT * FROM flashcards;
sqlite> .tables
ar_internal_metadata  schema_migrations     users
flashcards            temp.ephemeral

sqlite> UPDATE temp.ephemeral set user_id = 6;

sqlite> SELECT user_id, COUNT(*) FROM flashcards GROUP BY user_id;
1|273

sqlite> SELECT user_id, COUNT(*) FROM temp.ephemeral GROUP BY user_id;
6|273

sqlite> INSERT INTO flashcards (item, example, source, created_at, updated_at, user_id) SELECT item, example, source, created_at, updated_at, user_id FROM temp.ephemeral;

sqlite> SELECT user_id, COUNT(*) FROM flashcards GROUP BY user_id;
1|273
6|273

sqlite> SELECT user_id, COUNT(*) FROM temp.ephemeral GROUP BY user_id;
6|273

docker buildx build を使用して Rails の master.key をコンテナイメージから除外する

master.key とは

Rails の master.key とは認証情報を暗号化する時に使用されるキーファイルです。rails credentials:edit で認証情報を credentials.yml.enc に暗号化(ENCrypt) して保存する時に使用されます。認証機能に Firebase authentication を利用している場合、Firebase の API キーを格納している方が多いのではないでしょうか。

10.1 Custom Credentials

Rails stores secrets in config/credentials.yml.enc, which is encrypted and hence cannot be edited directly. Rails uses config/master.key or alternatively looks for the environment variable ENV["RAILS_MASTER_KEY"] to encrypt the credentials file. Because the credentials file is encrypted, it can be stored in version control, as long as the master key is kept safe.

master.key は .dockerignore でコンテナイメージに入れないようにしておく

私の場合 fly.io のデプロイ手順で Dockerfile を生成したので、master.key はデフォルトで除外する設定になっていました。スクラッチから作る時は Github などから 参考となる .dockerignore をいくつか見て回るのがよいかもしれません。

➜ grep master.key .dockerignore 
config/master.key

master.key がない場合に Rails を失敗させる config.require_master_key 設定

config.require_master_key を true に設定することで、キーファイル master.key もしくは環境変数 RAILS_MASTER_KEY が存在せず Rails が認証情報を取得できない場合に、Rails コマンドを異常終了させることができます。

config/environments/development.rb

  # Causes the app to not boot if a master key hasn't been made available through
  # ENV["RAILS_MASTER_KEY"] or the config/master.key file.
  config.require_master_key = true

コンテイメージビルド失敗: 抱える矛盾

この設定を有効にした場合、認証情報を取得できない場合は rails db:migrate も失敗するようになります。コンテナイメージを作成する場合に必要となる rails db:migrate が失敗するのでこのままではコンテナイメージを作成することができません。

Dockerfile

RUN bundle install
RUN bundle exec rails db:migrate
➜ docker buildx build -t camelot:latest .
[+] Building 27.8s (13/14)
 => [internal] load build definition from Dockerfile                                                                                                 0.0s
 => => transferring dockerfile: 626B                                                                                                                 0.0s

(... snip ...)

 => [ 8/10] RUN bundle install                                                                                                                      23.8s
 => ERROR [ 9/10] RUN bundle exec rails db:migrate                                                                                                   2.8s
------
 > [ 9/10] RUN bundle exec rails db:migrate:
#0 2.753 Missing encryption key to decrypt file with. Ask your team for your master key and write it to /camelot/config/master.key or put it in the ENV['RAILS_MASTER_KEY'].
------
ERROR: failed to solve: executor failed running [/bin/sh -c bundle exec rails db:migrate]: exit code: 1

➜ 

コンテナイメージビルド成功: docker buildx build の Secret to expose the build (--secret) を利用して矛盾を解決する

コンテナイメージに master.key は含めたくないがビルドには必要、という矛盾を解決するために docker buildx build が提供している機能が Secret to expose the build (--secret) です。この機能を利用することで、master.key をビルド時に「一時的に」渡すことが可能になります。

Dockerfile 側で「一時的に」渡すがイメージには含めたくないファイルを RUN --mount ... で指定し、コマンド側で docker buildx build --secret ... で指定して受け渡しをそれぞれ設定します。

Dockerfile

RUN bundle install
RUN --mount=type=secret,id=master_key,target=config/master.key,required=true bundle exec rails db:migrate
➜ docker buildx build --secret id=master_key,src=./config/master.key -t camelot:latest .
[+] Building 29.4s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                 0.0s
 => => transferring dockerfile: 626B                                                                                                                 0.0s

(... snip ...)

 => [ 8/10] RUN bundle install                                                                                                                      23.1s
 => [ 9/10] RUN --mount=type=secret,id=master_key,target=config/master.key,required=true bundle exec rails db:migrate                                3.6s
 => [10/10] COPY ./db/development.sqlite3 /camelot/db/                                                                                               0.0s
 => exporting to image                                                                                                                               1.7s
 => => exporting layers                                                                                                                              1.6s
 => => writing image sha256:900504dca3fe2761332f919e0526baa016c053722466844412efdacf0a371d37                                                         0.0s
 => => naming to docker.io/library/camelot:latest                                                                                                    0.0s

➜ 

Secret to expose the build (--secret) 機能を使用することで、先ほどはコンテナイメージビルド時に失敗した rails db:migrate が「一時的に」ファイルを渡すことで無事成功するようになりました。ビルドしたイメージの中にファイルは含まれていません。

➜ docker run -it camelot:latest ls -l config/master.key
ls: cannot access 'config/master.key': No such file or directory

ランタイム時に master.key を環境変数でコンテナに渡す: ローカルホストの場合

ビルドタイム時には「一時的に」渡すことができましたが、master.key がコンテナイメージに含まれていないため、ランタイム時は環境変数を使用して渡すことが必要になります。ローカルマシンであれば -e オプションが使えます。

➜ docker run -p 3000:3000 --name camelot-on-docker -e RAILS_MASTER_KEY='__THIS_IS_SECRET__' -it camelot:latest

ランタイム時に master.key を環境変数でコンテナに渡す: AWS AppRunner の場合

AWS AppRunner の場合は AWS Management Console から App Runner > Service > [Service name] > Configuration > Configure Service > Service Settings > Environment variables から設定可能です。master.key の内容を Plain text として貼り付けてもいいですが Secrets Manager や SSM Parameter Store 経由で設定するとより安全かもしれません。

参考情報

https://guides.rubyonrails.org から特定の Rails のバージョンに絞って検索する場合、site 検索が便利です。

AWS AppRunner で Rails アプリを動かす場合のベストプラクティス

AppRunner から割り当てられるドメインと DNS rebinding atttacks

AppRunner にアプリをデプロイした場合、アプリに割り当てられるドメインは ランダム文字列.リージョン.awsapprunner.com になります。このドメインは Rails の DNS rebinding attacks を防ぐためのガードレールにひっかかってしまうため、設定で解除する必要があります。

config/environments/your_environment.rb

AppRunner はお手軽さとコストの面で優秀

以下の記事の比較で「AppRunner はスケールをゼロにできる(Scaling down to 0)」が評価されているのは Action: Pause がメニューにあるからです。Pause 中は課金はされないので、動かす場面が限られている場合、この点で ECS よりも費用を抑えられます。

cloudonaut.io

コンテナベースにしておいたほうがいろいろと楽

元となるレポジトリに ECR(コンテナレジストリ) ではなく Github(ソースコードレポジトリ) も指定できますが、その場合は AppRunner がサポートしているランタイムのバージョンに合わせる必要があります。Ruby だと現在サポートされているのは 3.1.2 です。コンテナベースで運用するほうが、ランタイムのバージョンを気にする必要がなく fly.io や ECS/EKS にも移行しやすいです。

他の SaaS とどう使い分けていくべきか

個人開発アプリを完全無料で公開したいのであれば fly.io ですが、社内用にアクセスをパブリックではなく VPC 内に制限して 公開する場合などは AppRunner のほうがよさそうです。また RDS など既存の AWS のデータストア/リソースを活用できる点も利点となります。CloudWatch を使えるところも嬉しいかも (^_^;

参考情報

Fly.io のファーストインプレッション

Heroku の料金体系改定

Heroku's Next Chapter | Heroku でアナウンスされている通り 11/28 から heroku の料金体系が改定されます。無料枠の FreeHobby プランは 11/28 で廃止されます。そのため、多くのエンジニアが無料枠を求めて fly.io, railway, render などの PaaS を候補に引っ越し準備をはじめているようです。

個人的には heroku の 新プランEcoBasic もかなり良心的だと思います。無料で使えなくなるのが残念な反面、価値のあるものにはきちんと対価を払いたいとも考えています。また、数ドルを惜しんで各 PaaS のコマンドや手順で自分のツールボックスを書き換えるのも少し手間だな... といったようにベンダーロックイン脳になっているところもあります。(^_^;

今まで他の PaaS を使ったことがなかったため、Heroku への「刷り込み」も少なからずあります。このバイアスを是正するためには比較対象となる PaaS を使ってみるのがよいと考えました。今回は CDN とは別軸での Edge Computing の考え方と LiteFS に積極的な姿勢で、 Fly.io を使ってみます。

Fly.io の特徴

Fly.io を使って個人の Rails アプリをデプロイしてみました。まだ不明な部分も多いですが概要と気になった特徴を以下にまとめておきます。

  • デプロイコマンド(fly launch + fly deploy)すると Dockerfile の作成とイメージビルドが行われ、app 用と db 用(postgresql) のコンテナが 2 つ作成される。
  • db 用のコンテナの内部アドレスとパスワードは提供されるが外部アドレスは提供されない。ただし fly proxyで db 用コンテナまで ssh tunnel を開通することができる。
  • drop database や drop table ができない ため sequel での流し込みはできない? db はその都度作り直す流儀らしい。
  • 最初から grafana で各コンテナ用に詳細なメトリクスが用意されているのはよい。

作成されるファイル

how can I drop and re-create a production database attached to my app? - Questions / Help - Fly.io

$ git bn deploy/add-flyio
.dockerignore
Dockerfile
fly.toml
journal/20221104165758_add_flyio.md
journal/commands
lib/tasks/fly.rake

$ grep bn ~/.gitconfig
  bn = "!f() { top=$(git log --oneline --walk-reflogs ${1} | head -1 | awk '{print $1}'); bottom=$(git log --oneline --walk-reflogs ${1} | tail -1 | awk '{print $1}'); git diff ${bottom}^ ${top} --name-only; }; f"
  • grafana

開発環境の sqlite3@mac とステージング環境の postgresql@heroku をデータベースツールキット sequel とカスタム rake タスクで同期する

sqlite3 DB(on Mac) と postgresql DB(on Heroku) を同期する

私の場合 Rails の開発環境とステージング環境をそれぞれ以下のように構築しています。開発に伴う動作確認や実際の英単語の入力は開発環境(Mac mini)で行うことがほとんどなので、自然と両環境での DB に差分が発生します。

環境 ホスト DB
開発環境 Mac mini sqlite3
ステージング環境 Heroku postgresql

ここではデータベースツールキットの sequel とカスタム rake タスクを使用して両環境の DB を同期する方法を紹介します。同様の環境で開発している方の参考になれば幸いです。

実装内容

rails コマンド(rake タスク) に独自のネームスペースとして custom を用意し、その配下に db:sync タスクを定義します。タスク自体は heroku コマンドと sequel を組み合わせた単純なものです。

$ bundle exec rails -T | grep custom
rails custom:db:sync                     # Synchronize db between development and staging

lib/tasks/custom.rake

namespace :custom do
  namespace :db do
    desc "Synchronize db between development and staging"
    task :sync do
      app = `heroku apps | sed '/^$/d' | tail -1`.chomp
      postgresql_credentials_url = `heroku pg:credentials:url | tail -1`

      sh "heroku pg:reset -a #{app} --confirm #{app}"
      sh "bundle exec sequel -C sqlite://db/development.sqlite3 #{postgresql_credentials_url}"
      sh "heroku run rake db:migrate"
    end
  end
end

Gemfile

group :development do
(... snip ...)

  # Use the database toolkit for ruby [https://github.com/jeremyevans/sequel]
  gem "sequel"

(... snip ...)
end

group :staging do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "pg", platforms: %i[ mri mingw x64_mingw ]
end

カスタム rake タスクから heroku コマンドと sequel を呼び出した様子

LiteFS

SQLite 推しなので LiteFS の今後に注目しています (^_^)

参考情報

github.com

Connecting to Heroku Postgres | Heroku Dev Center

SQLite3からPostgreSQLへのデータ移行

fly.io