8
5

Rust+Locoでサービス開発してShuttleにデプロイまで試してみる

Last updated at Posted at 2024-09-16

Locoとは

RailsにインスパイアされたRust製のウェブアプリケーションフレームワークです。

主な機能はこんな感じです。Railsに慣れていればとても親しみやすい構成です。

image.png

※MVCではありますが、ViewはJSONがデフォルトなので、RailsのAPIモードのような感じです。

Shuttleとは

Rust用のHerokuのような感じです。Rustアプリケーションを簡単にデプロイできます。
Postgresqlなどのサービスもある程度無料で使えます。
Heroku同様、コマンド1つでデプロイできてSSLのURLが付与されて即アクセスできて便利です。

作ったもの

haikunator でランダムな命名を生成するサービスです。
調べれば他にいくらでもありそうではありますが、今回はRustの練習がてら作ってみました。

screenshot_top.png

作っていく

環境構築

今回はGitHub Codespeces上にDevContainerで構築していきます。
Ctrl+P → Add Dev Container Configuration Files と選択していき、Rustのコンテナを構成します。

image.png

Dev Containerの構成ファイルができたので、Locoを使えるように少し修正していきます。

.devcontainer/devcontainer.json
{
    "name": "Loco",
    "dockerComposeFile": "compose.yaml",
    "postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
    "service": "app",
    "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
    "forwardPorts": [
        5150, 5153
    ],
    "features": {
        "ghcr.io/devcontainers/features/node:1": {}
    }
}
  • imageを削除し、Dockerfileとcompose.ymlを使うように変更する
  • フロントエンドはJSにしたいのでfeaturesでnodeを入れる
  • フロントエンドのホットリロード用に5153ポートを空ける(5150はLoco用)
.devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/rust:1

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
     && cargo install loco-cli cargo-insta cargo-cache cargo-shuttle \
     && chown -R vscode /usr/local/cargo

COPY .env /.env
  • LocoのCLIやShuttleのCLIなどを入れる

ここまで変更したら、Dev Containerをビルドします。

image.png

※すごく時間がかかるので、気長に待ちます。(15分くらい)

環境確認

ビルドが終わったら各種コマンドを確認してみます。

$ rustc --version
rustc 1.80.1 (3f5fd8dd4 2024-08-06)
$ cargo --version
cargo 1.80.1 (376290515 2024-07-16)
$ loco --version
loco-cli 0.2.8
$ cargo shuttle --version
cargo-shuttle 0.47.0
$ npm --version
10.8.2
$ pnpm --version
9.10.0

Loco new

loco new コマンドで雛形を作ります。
lightweight-serviceはあまりにもminimalするぎるので、一旦SaaS Appを作成します。不要なものはあとで全部消します。

$ loco new

image.png

ディレクトリ構成

このような構成で生成されます。

image.png

フロントエンド

frontend/ ディレクトリにReactアプリが生成されているので、一旦そのままビルドします。

$ cd frontend
$ pnpm install
$ pnpm build

image.png

フロントエンドファイル配信設定

Rust側にあるstaticフォルダを配信するかフロント側のdist/フォルダを配信するか選べるので、今回はフロント側を有効にします。

config/development.yml
    #
    # (1) Server-side static assets config
    # ====================================
    #
    # for use with the view_engine in initializers/view_engine.rs
    # static:
    #   enable: true
    #   must_exist: true
    #   precompressed: false
    #   folder:
    #     uri: "/static"
    #     path: "assets/static"
    #   fallback: "assets/static/404.html"
    #

    # (2) Client side app static config
    # =================================
    #
    # Note that you need to go in `frontend` and run your frontend build first,
    # e.g.: $ npm install & npm build
    #
    # (client-block-start)
    static:
      enable: true
      must_exist: true
      precompressed: false
      folder:
        uri: "/"
        path: "frontend/dist"
      fallback: "frontend/dist/index.html"
    # (client-block-end)
    #

Loco start

この状態で試しに起動してみます。
環境に合わせて以下どちらかで起動すると思います。

$ cargo loco start
$ cargo loco start --binding 0.0.0.0

無事表示できました。 :raised_hands:

image.png

GitHub CodeSpacesの場合は、アクセスしても表示されない場合があるので、ポート設定で「公開設定」を一瞬publicにしてまたすぐprivateにしたりしてみると何故かアクセスできるようになったりします。

バックエンドもちゃんと動いていそうです。 :raised_hands:

$ curl localhost:5150/_health
{"ok":true}

image.png

不要な機能の削除

現状、雛形で生成されたフロントエンドは何故かバックエンドとはなんの関係もないただのペライチでした。バックエンドにはauthやuserのAPIが実装されていますが、そこと連動した動作までは試せないようでした。

今回はその辺の機能は必要ないため、削除していきます。
また、database周りやworker、mailerなども不要なので削除していきます。

量が多いので省略しますが、差分はこんな感じです。

Haikunator Generator

環境整備が済んだのでアプリケーションを実装していきます。
ざっくり以下のような感じです。

API

src/controllers/haikunator.rs
use axum::debug_handler;
use loco_rs::prelude::*;

use crate::views::haikunator::GeneratorResponse;

#[debug_handler]
async fn generate() -> Result<Response> {
    let generated = haikunator::Haikunator::default().haikunate();
    format::json(GeneratorResponse::generate(&generated))
}

async fn generate_txt() -> Result<Response> {
    let generated = haikunator::Haikunator::default().haikunate();
    format::text(&generated)
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api")
        .add("/gen", get(generate))
        .add("/gen.txt", get(generate_txt))
}

フロントエンド

frontend/src/pages/HaikunatorGenerator.tsx
'use client'

import { useState } from 'react'

export const HaikunatorGenerator = () => {
  const [generatedString, setGeneratedString] = useState<string>('')

  const generateString = async () => {
    try {
      const response = await fetch('/api/gen')
      if (response.ok) {
        const data: { name: string } = await response.json()
        setGeneratedString(data.name)
      } else {
        throw new Error('Failed to generate string')
      }
    } catch (error) {
      console.error(error)
    }
  }

  return (...)
}

404ページ

デフォルトのfallbackのままだと、存在しないパスにアクセスした場合にも全てindexページがレスポンスされてしまうので、404ページを返すようにします。

frontend/src/404.tsx
import React from "react";
import ReactDOM from "react-dom/client";

import "./index.css";

const root = document.getElementById("root");

if (!root) {
  throw new Error("No root element found");
}

ReactDOM.createRoot(root).render(
  <React.StrictMode>
    <div className="flex items-center justify-center h-screen">
      <h1 className="text-4xl font-bold text-gray-500">404 Not Found</h1>
    </div>
  </React.StrictMode>
);
config/development.yml
    # (client-block-start)
    static:
      ... 
      fallback: "frontend/dist/404.html"
    # (client-block-end)

他にもいくつか設定していくと、こんな感じに表示できました。 :raised_hands:
レスポンスステータスもきちんと404でした。

image.png

Shuttleにデプロイ

Shuttleへはとても簡単にデプロイできるので詳細は省略します。

注意点としては、ShuttleにはGit管理下のファイルしか転送できないため、フロントエンドのビルド成果物は一時的にでもGit管理下に追加する必要があります。
(あまりにも不便なので今後改善されると期待してます)

これを踏まえて、デプロイの流れは以下のようになります。

$ pushd frontend
$ echo !dist/ >> .gitignore
$ pnpm build
$ popd
$ cargo shuttle login
$ cargo shuttle deploy --allow-dirty
$ git checkout frontend/.gitignore
  1. frontend/dist を一時的にGit管理に追加(無視を無視)
  2. フロントエンドをビルド
  3. Shuttleにログイン
  4. Shuttleにデプロイ
  5. 一時的な変更を元に戻す

ちょっとめんどいですが、デプロイも自動化する前提であれば大したことではない気もします。

おわり

以上です。
とても簡単にサービスをひとつリリースできてしまいました。

Locoの所感としては、Railsのようなフルスタックフレームワークというよりも、Rustでバックエンドを構築する際の雛形として優秀という印象でした。
今回のように、使わない機能は全て削除してしまえるし、雛形で用意されているモジュールが自分好みでなければ差し替えてしまうこともできるしといった感じで、柔軟に使えるような気がしました。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5