⚙️

Shuttle を使って 10 分で Rust と Postgres を動かす

2024/12/01に公開

あ…ありのまま 今 起こった事を話すぜ!

おれは Shuttle のコマンドを打っていたと思ったらいつのまにかデプロイされていた…

な… 何を言ってるのか わからねーと思うがおれも何をされたのかわからなかった…

はじめに

Web の API を実装したい場合,まず Rust で書きたくなるのは自明である.しかしながら,Rust で API を実装したとしても,デプロイ先に困るのが常である.特に,何らかの DB を用いたい場合には途方に暮れることも多い.

実務であれば AWS などを使用するのが常套手段であるが,個人で簡単な API を作成して動かしたい場合は費用的に敬遠したくなるのが人間の性だ.

Shuttle というサービスは Rust に特化した PaaS であり,Rust の様々なフレームワークに対応している.非常に簡単にデプロイできるのでオススメなのだが,最近(2024 年 10 月?)リニューアル(shuttle.rs → shuttle.dev)されて以降の情報が少なかったのでまとめてみた.

作業時間は約 10 分程度でプロジェクト作成からデプロイまで完了した.

作るもの

書籍の情報を Postgres に保存し,保存された情報を取り出して JSON データを返す API を実装する.

今回はデプロイまでの流れを押さえることを重視しているため,適当な初期データを登録して全件もしくは id 指定の 1 件を取り出す機能を実装する.

↓ 作ったものはこちら ↓

Rust の準備

ドキュメント見ながらやればコマンド数発で終わる.インストール済の場合は最新バージョンにアップデートしておく.バージョンが古いコマンド実行時にエラーが発生する場合がある.

Shuttle の準備

まず,Shuttle の Web サイトからアカウントを作成しておく.GitHub アカウントと連携できるのですぐできる.

続いて,下記コマンドで shuttle コマンドをインストールする.

cargo install cargo-shuttle

適当なディレクトリに移動し,下記コマンドでプロジェクトを作成する.

コマンドを実行するといろいろ訊かれるので十字キーで選択する.今回は Postgres を用いたアプリケーションを実装したいので「Todo list with a Postgres database (postgres)」を選択した.

また,Shuttle 上でプロジェクトを作成するかどうか訊かれるので「yes」にしておくと連携も自動でやってくれて便利.

shuttle init

What type of project template would you like to start from?
A Hello World app in a supported framework
❯ Browse our full library of templates

✔ Select template · Postgres - Todo list with a Postgres database (postgres)

Creating project "shuttle-books-api" in "/Users/taro/development/studies/shuttle-books-api"

✔ Create a project on Shuttle with the name "shuttle-books-api"? · yes

Created project 'shuttle-books-api' with id proj_HOGEFUGAPIYO
Linking to project proj_HOGEFUGAPIYO
You can `cd` to the directory, then:
Run `shuttle run` to run the app locally.

プロジェクト作成が完了したらエディタで開いておく.src/main.rs に最初から todo リストの処理が書かれているので確認してみよう.

DB 接続やデータ登録・参照のコードが始めから用意されているのはなかなかありがたい.

書籍登録 API の実装

ここでは,以下 2 つのコードを実装する.

  1. マイグレーションファイル(migrations/0001_init.sql)の書き換え

  2. 書籍の情報を取得する処理(main.rs

マイグレーションファイルの書き換え

migrations/0001_init.sql に todo リストのテーブルを作成するコードが書いてあるので書籍用に書き換える.

今回は動作確認優先のため,同時に適当な初期データを作成している.

CREATE TABLE IF NOT EXISTS books (
  id serial PRIMARY KEY,
  title TEXT NOT NULL,
  isbn TEXT NOT NULL UNIQUE
);

INSERT INTO books (title, isbn) VALUES
('プログラミングRust 第2版', '978-4873119786'),
('Rustの練習帳', '978-4814400584'),
('ゼロから学ぶRust', '978-4065301951'),
('RustによるWebアプリケーション開発', '978-4065369579');

main.rs の書き換え

src/main.rs にはもともと todo リストを実行するためのコードが書かれているが,書籍情報を取得する処理に変更する.books テーブルのカラムに合わせて構造体を変更し,各関数の形と引数などを変更する.

また,書籍を一覧取得する list() 関数を追加した.登録処理は動かさなくて良いのでルーティングをコメントアウトしている.

大したことしていないので数分で終わる.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};

async fn list(State(state): State<MyState>) -> Result<impl IntoResponse, impl IntoResponse> {
    match sqlx::query_as::<_, Book>("SELECT * FROM books")
        .fetch_all(&state.pool)
        .await
    {
        Ok(books) => Ok((StatusCode::OK, Json(books))),
        Err(e) => Err((StatusCode::BAD_REQUEST, e.to_string())),
    }
}

async fn retrieve(
    Path(id): Path<i32>,
    State(state): State<MyState>,
) -> Result<impl IntoResponse, impl IntoResponse> {
    match sqlx::query_as::<_, Book>("SELECT * FROM books WHERE id = $1")
        .bind(id)
        .fetch_one(&state.pool)
        .await
    {
        Ok(book) => Ok((StatusCode::OK, Json(book))),
        Err(e) => Err((StatusCode::BAD_REQUEST, e.to_string())),
    }
}

async fn add(
    State(state): State<MyState>,
    Json(data): Json<BookNew>,
) -> Result<impl IntoResponse, impl IntoResponse> {
    match sqlx::query_as::<_, Book>(
        "INSERT INTO books (title, isbn) VALUES ($1, $2) RETURNING id, title, isbn",
    )
    .bind(&data.title)
    .bind(&data.isbn)
    .fetch_one(&state.pool)
    .await
    {
        Ok(book) => Ok((StatusCode::CREATED, Json(book))),
        Err(e) => Err((StatusCode::BAD_REQUEST, e.to_string())),
    }
}

#[derive(Clone)]
struct MyState {
    pool: PgPool,
}

#[shuttle_runtime::main]
async fn main(#[shuttle_shared_db::Postgres] pool: PgPool) -> shuttle_axum::ShuttleAxum {
    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Failed to run migrations");

    let state = MyState { pool };
    let router = Router::new()
        .route("/books", get(list))
        // .route("/books", post(add))
        .route("/books/:id", get(retrieve))
        .with_state(state);

    Ok(router.into())
}

#[derive(Deserialize)]
struct BookNew {
    pub title: String,
    pub isbn: String,
}

#[derive(Serialize, FromRow)]
struct Book {
    pub id: i32,
    pub title: String,
    pub isbn: String,
}

ちなみにコードの #[shuttle_shared_db::Postgres] を書くだけで Postgres がプロビジョニングされる.意味がわからないよ(褒め言葉).

動作確認

shuttle run コマンドでローカルで動作確認を行う.コマンドを実行すると Postgres のコンテナが勝手に立ち上がる.DB 設定とか不要.マイグレーションも実行されるため初期データも入った状態となる.

下記のように,ローカルのアドレスと Postgres の情報が表示される.クライアントツールで接続する場合はこの情報を使う(試してはいない).

shuttle run

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 27.18s

    Starting shuttle-books-api on http://127.0.0.1:8000

These databases are linked to local service
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Type Connection string │
╞═════════════════════════════════════════════════════════════════════════════════════════════╡
│ database::shared::postgres postgres://postgres:postgres@localhost:21499/shuttle-books-api │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2024-11-29T13:48:28.422+09:00 [app] INFO shuttle_runtime::rt: Starting service

今回の実装では下記 2 つのエンドポイントが作成されているので,curl やブラウザで確認してみると JSON データが取得できる.

  • GET /books

  • GET /books/:id

ローカルの動作を停止する場合は Ctrl + C で停止する.また,DB 用に shuttle_<プロジェクト名>_shared_postgres のコンテナが立ち上がっているので適宜終了させる.

docker stop shuttle_shuttle-books-api_shared_postgres

デプロイ

ローカルで動作確認できたので Shuttle 上にデプロイする.下記コマンドを実行する.以上!

(少し時間がかかるので待つ)

shuttle deploy
INFO: Using NEW platform API (shuttle.dev)
Packing files...
Uploading code...
Creating deployment...
Deployment depl_HOGEFUGAPIYO - running
https://shuttle-books-api-hoge.shuttle.app

完了したら curl やブラウザでアクセスしてみる.

curl https://shuttle-books-api-hoge.shuttle.app/books | jq

[
  {
    "id": 1,
    "title": "プログラミング Rust 第 2 版",
    "isbn": "978-4873119786"
  },
  {
    "id": 2,
    "title": "Rust の練習帳",
    "isbn": "978-4814400584"
  },
  {
    "id": 3,
    "title": "ゼロから学ぶ Rust",
    "isbn": "978-4065301951"
  },
  {
    "id": 4,
    "title": "Rust による Web アプリケーション開発",
    "isbn": "978-4065369579"
  }
]

curl https://shuttle-books-api-lf5h.shuttle.app/books/1 | jq

{
  "id": 1,
  "title": "プログラミング Rust 第 2 版",
  "isbn": "978-4873119786"
}

補足

Shuttle コンソールの Resources タブから Postgres の接続情報を確認できる.画面上だと*表記になっているが,コピーボタンをクリックして貼り付けると接続情報が表示される.

例:

postgres://user_HOGEFUGA:HOGEFUGAPIYO@sharedpg-rds.shuttle.dev:5432/db_FOOBAR

まとめ

Rust のデプロイ先を求めて彷徨っていたが,Shuttle を使ったら 10 分でデプロイまで完了した.特にデータベースがほぼ設定不要で使用できるメリットは非常に大きく,個人レベルでの使用ならば無料で使えるので非常にオススメである.

Shuttle のサービスはまだまだ新しく情報も少ないが,今後の発展が楽しみである.現状 GitHub との連携ができていないので,できるようになるとさらに便利になりそう.

いずれにしてもコマンド一発でデプロイまでできてしまうのはなかなか画期的である.これを読んだ人は Rust の API を作りたくなることだろう.

以上だ( `・ω・)b

GitHubで編集を提案

Discussion