🏃‍♂️

Rust | AWS App Runner で Axum 製 Web アプリケーションを動かす

2023/05/01に公開

Rust で開発がしたい... そうだ、AWS App Runner を使おう😈😈😈

Rust とその Web アプリケーションフレームワークである Axum を使って実装した Web アプリケーション(REST API)を AWS App Runner にデプロイしてみたいと思います。

AWS App Runner とは

AWS App Runner は、コンテナ化されたウェブアプリケーションを自動的に構築してデプロイし、暗号化でトラフィックの負荷を分散し、トラフィックのニーズに合わせてスケールし、サービスがアクセスされる方法の設定を可能にし、プライベート Amazon VPC 内の他の AWS アプリケーションと通信できるようにします。

The easiest and fastest way to build, deploy, and run containerzed web apps on AWS.

AWS の中でも最も簡単に、かつ素早くコンテナ化された Web アプリケーションをビルド、デプロイ、実行できるサービスです。

コンテナ化されたウェブアプリケーション を使うことができるので、 以下の記事で GCP Cloud Run で動かすために実装したコンテナ化された資産を流用していきます。

https://zenn.dev/collabostyle/articles/89a9171ab0c0e5

App Runner のデプロイについて

GitHub との連携

GitHub と連携することで、ソースコードをプッシュすれば自動デプロイが可能です。

ただし、対応言語(ランタイム)に制約があります。

  • Python 3
  • Node.js (12,14,16)
  • Java (corretto8, corretto11)
  • .NET 9
  • PHP 8.1
  • Ruby 3.1
  • Go 1.18

ECR との連携

ECR - Elastic Container Registry と連携することで、イメージのプッシュをトリガーに自動デプロイが可能です。

Dockerfile を作成する必要がありますが、対応言語の制約を受けません。

Rust が安心して使えます🥰

Rust と Axum で Web アプリケーションを実装する

今回は Axum という Web アプリケーションフレームワークを使っていきます。

Cargo.toml

使用するクレートは、メインである Axum 、非同期ランタイムである tokio や JSON シリアライズに使用する serde と serde_json です。

Cargo.toml
[package]
name = "axum-app-runner"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.6.17"
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"
tokio = { version = "1.28.0", features = ["full"] }

main.rs

一定のレスポンスが返る、簡単な REST API を実装しました。

エンドポイントの構成は以下のようになっています。

  • /hc : ヘルスチェック用で 200 応答だけを返す
  • /mountains : 山一覧を返す

登山が趣味ゆえ、こういうときに山のデータを使いがちです⛰️🏔️

main.rs
use axum::http::StatusCode;
use axum::routing::get;
use axum::{Json, Router};
use serde::Serialize;
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let hc_router = Router::new().route("/", get(health_check));
    let mountain_router = Router::new().route("/", get(find_sacred_mountains));

    let app = Router::new()
        .nest("/hc", hc_router)
        .nest("/mountains", mountain_router);

    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap_or_else(|_| panic!("Server cannot launch."));
}

async fn health_check() -> StatusCode {
    StatusCode::OK
}

async fn find_sacred_mountains() -> (StatusCode, Json<JsonResponse>) {
    let response: JsonResponse = Default::default();
    (StatusCode::OK, Json(response))
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonResponse {
    mountains: Vec<EightThousander>,
    total: usize,
}

impl Default for JsonResponse {
    fn default() -> Self {
        let mountains = vec![
            EightThousander::new(1, "エベレスト".to_string(), 8848),
            EightThousander::new(2, "K2".to_string(), 8611),
            EightThousander::new(3, "カンチェンジェンガ".to_string(), 8586),
            EightThousander::new(4, "ローツェ".to_string(), 8516),
            EightThousander::new(5, "マカルー".to_string(), 8463),
            EightThousander::new(6, "チョ・オユー".to_string(), 8188),
            EightThousander::new(7, "ダウラギリ".to_string(), 8167),
            EightThousander::new(8, "マナスル".to_string(), 8163),
            EightThousander::new(9, "ナンガ・パルバット".to_string(), 8126),
            EightThousander::new(10, "アンナプルナ".to_string(), 8091),
            EightThousander::new(11, "ガッシャーブルⅠ峰".to_string(), 8080),
            EightThousander::new(12, "ブロード・ピーク".to_string(), 8051),
            EightThousander::new(13, "ガッシャーブルムⅡ峰".to_string(), 8035),
            EightThousander::new(14, "シシャパンマ".to_string(), 8027),
        ];
        let total = mountains.len();

        Self { mountains, total }
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EightThousander {
    id: i32,
    name: String,
    elevation: i32,
}

impl EightThousander {
    fn new(id: i32, name: String, elevation: i32) -> Self {
        Self {
            id,
            name,
            elevation,
        }
    }
}

Dockerfile

Dockerfile
# Use the official Rust image.
# https://hub.docker.com/_/rust
FROM rust

# Copy local code to the container image.
WORKDIR /usr/src/app
COPY . .

# Install production dependencies and build a release artifact.
RUN cargo build --release

# Service must listen to $PORT environment variable.
# This default value facilitates local development.
ENV PORT 8080

# Run the web service on container startup.
ENTRYPOINT ["target/release/axum-app-runner"]

App Runner にデプロイ

AWS コンソールからポチポチしていきます。

Rust で実装しているため、ECR と連携する方法で進めます。

ECR にイメージをプッシュする

  1. プライベートもしくはパブリックリポジトリを作成します
  2. プッシュコマンドを表示し、順番に実行していきます

認証トークンの取得に失敗する場合

~/.docker/config.json が悪さをしているようです。
~/.docker/config.json を削除して、Docker を再起動すれば解決しました。

https://zenn.dev/bon/scraps/d0099eddd9f215

App Runner にデプロイ

簡単な設定でデプロイが可能です。

デプロイには数分かかります。

  1. ソースで ECR のリポジトリを選択します
  2. デプロイトリガーを選択します(手動 or 自動)
  3. サービス名を入力します
  4. ヘルスチェックで /hc を指定します(必要に応じて変更)
  5. デプロイ

URL にアクセスしてみる

今回は /mountains でネストしているので、https://{your_id}.awsapprunner.com/mountains にアクセスすると以下のようなレスポンスが200応答で返ってきます。

{
    "mountains": [
        {
            "id": 1,
            "name": "エベレスト",
            "elevation": 8848
        },
        {
            "id": 2,
            "name": "K2",
            "elevation": 8611
        },
        {
            "id": 3,
            "name": "カンチェンジェンガ",
            "elevation": 8586
        },
        {
            "id": 4,
            "name": "ローツェ",
            "elevation": 8516
        },
        {
            "id": 5,
            "name": "マカルー",
            "elevation": 8463
        },
        {
            "id": 6,
            "name": "チョ・オユー",
            "elevation": 8188
        },
        {
            "id": 7,
            "name": "ダウラギリ",
            "elevation": 8167
        },
        {
            "id": 8,
            "name": "マナスル",
            "elevation": 8163
        },
        {
            "id": 9,
            "name": "ナンガ・パルバット",
            "elevation": 8126
        },
        {
            "id": 10,
            "name": "アンナプルナ",
            "elevation": 8091
        },
        {
            "id": 11,
            "name": "ガッシャーブルⅠ峰",
            "elevation": 8080
        },
        {
            "id": 12,
            "name": "ブロード・ピーク",
            "elevation": 8051
        },
        {
            "id": 13,
            "name": "ガッシャーブルムⅡ峰",
            "elevation": 8035
        },
        {
            "id": 14,
            "name": "シシャパンマ",
            "elevation": 8027
        }
    ],
    "total": 14
}
この14座の山は何?

標高が 8000m を越える「8000m峰」と呼ばれる山々です😉
もちろん、全部登ったことないです笑

ソースコード

ソースコード一式は GitHub で公開してます。

https://github.com/codemountains/axum-app-runner

おわりに

GCP Cloud Run の対抗馬である App Runner に Rust で実装した Web アプリケーションをデプロイできました。

App Runner も非常に便利でした。
まだ比較的新しいサービスなので、今後のアップデートに注目したいです!

細かい制御をしたいというユースケースでは ECS や Fargate を検討する必要があると思いますが、
フルマネージドということで、ECS や Fargate とは違う良さがありますね🫶

コラボスタイル Developers

Discussion