Rocket (Rust) を nginx と systemd を使ってデプロイしてみる

今回も前回の続きでRustの話が続きます。
ohshige.hatenablog.com

クロスドメインも気にせずAPIが使えるようになったので、そろそろ cargo run での実行ではなく、いい感じでデプロイしたいところです。

そこで、今回はリバースプロキシとしてnginxを、デーモン化としてsystemdを使ってみます。

こちらを参考にしました。
medium.com

nginx

nginxはインストール済みだとして、適当な設定ファイルに以下を書きます。(例えば /etc/nginx/conf.d/rocket.conf
必要であれば適宜 server_name 等を書きます。

server {
    listen 80 default_server;

    location / {
        proxy_pass http://0.0.0.0:8000;
        proxy_buffering off;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

sudo nginx -tシンタックスをチェックし、問題なければ sudo nginx -s start 等で起動しておきます。

systemd

続いてsystemdの設定です。

以下の通り、設定をユニットファイルに記述します。(例えば /etc/systemd/system/rocket.service
今回はアプリケーションが /home/ohshige/rocket/ 配下にある場合を想定していて、既に cargo build 済みであるとします。

[Unit]
Description=RocketAPI
[Service]
User=www-data
Group=www-data
WorkingDirectory=/home/ohshige/rocket/
Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=0.0.0.0"
Environment="ROCKET_PORT=8000"
Environment="ROCKET_LOG=critical"
ExecStart=/home/ohshige/rocket/target/debug/rocket
[Install]
WantedBy=multi-user.target

本番ではないので ExecStartデバッグのバイナリを指しています。( ROCKET_ENV=prod ではありますが)
しっかりやるなら cargo build --release でビルドしたリリース版を指すようにすると良いと思います。

そして、 sudo systemctl start rocket.service でサービスを起動します。
また、 サーバ起動時にサービスも起動するように sudo systemctl enable rocket.service も実行しておきます。

これでデーモン化できました。

.

実際にアクセスしてみると問題無く動作するはずです。

アプリケーションに変更がある場合には、再度ビルドした上で、 sudo systemctl restart rocket.service で再起動する必要があります。

Rocket (Rust) で CORS の設定をする

今回も前回の軽い続きです。
ohshige.hatenablog.com

APIは作れるようになったので、今度はこれを呼び出したい思いです。
同じドメインなら問題無いですが、異なるドメインから呼び出したい場合に設定しないといけないがCORSです。

前回の状態で適当にJSからXHRを試して http://localhost:8000/users を呼び出そうとすると、以下のようにエラーとなります。

Access to XMLHttpRequest at 'http://localhost:8000/users' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

実際にレスポンスヘッダーを見ても、当たり前ですがCORSの設定は何もありません。

HTTP/1.1 200 OK
Content-Type: application/json
Server: Rocket
Content-Length: 73
Date: Mon, 24 Feb 2020 19:00:00 GMT

というわけで、 rocket_cors を使います。
docs.rs

Cargo.toml

rocket_cors = "*" を追記します。

[package]
name = "demo"
version = "0.1.0"

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

[dependencies]
rusqlite = "*"
rocket = "*"
rocket_contrib = { version = "*", features = ["json"] }
rocket_cors = "*"
serde = "*"
serde_derive = "*"

main.rs

CorsOptions::default() をアタッチすることでCORSを設定できます。
デフォルトだとオールオッケーな感じなので、適宜必要な範囲に絞る必要があります。

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;

extern crate rusqlite;
extern crate rocket_contrib;
extern crate rocket_cors;

use rusqlite::{Connection, NO_PARAMS};
use rocket_contrib::json::Json;
use rocket_cors::CorsOptions;

(略)

fn main() {
  rocket::ignite()
      .mount("/", routes![get_users, get_user])
      .attach(CorsOptions::default().to_cors().expect("error"))
      .launch();
}

上記を実行すると以下のようになります。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/demo`
🔧 Configured for development.
(略)
🛰  Mounting /:
    => GET /users (get_users)
    => GET /users/<user_id> (get_user)
🛰  Mounting /cors:
    => GET /cors/<status>
📡 Fairings:
    => 1 request: CORS
    => 1 response: CORS
🚀 Rocket has launched from http://localhost:8000

それらしい起動ができている気がします。

この状態で新たにJSからXHRを試してみると、エラーは起きず無事にできていることがわかります。
レスポンスヘッダーを見てみると、 null ではありますが Access-Control-Allow-OriginVary が増えています。

HTTP/1.1 200 OK
Content-Type: application/json
Server: Rocket
Access-Control-Allow-Origin: null
Vary: Origin
Content-Length: 73
Date: Mon, 24 Feb 2020 19:00:00 GMT

RustでRocketを使ってAPIを作ってみる

なんとなくの前回の続きで、だいたい自分用メモです。
ohshige.hatenablog.com

RustでAPIを実現するためのフレームワークはいくつかあるようですが、今回はRocketを使います。
基本的にはチュートリアルの通りにやっています。
rocket.rs

やりたいことは前回作ったDBからデータを取得するメソッドをAPI化して、JSONを返却するようにするというものです。

以降は、rustc 1.42.0-nightly で動作確認済みです。

Cargo.toml

[package]
name = "demo"
version = "0.1.0"

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

[dependencies]
rusqlite = "*"
rocket = "*"
rocket_contrib = { version = "*", features = ["json"] }
serde = "*"
serde_derive = "*"

main.rs

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;

extern crate rusqlite;
extern crate rocket_contrib;

use rusqlite::{Connection, NO_PARAMS};
use rocket_contrib::json::Json;

#[derive(Debug, Serialize)]
struct User {
    user_id: usize,
    name: String,
}

#[get("/users")]
fn get_users() -> Json<Vec<User>> {
    let conn = Connection::open("my.db").unwrap();
    let mut stmt = conn.prepare("
        SELECT
            user_id, name
        FROM
            users
        ORDER BY
            user_id ASC
    ",
    ).unwrap();

    let users = stmt.query_map(NO_PARAMS, |row| {
        Ok(User {
            user_id: row.get::<_, i64>(0).unwrap() as usize,
            name: row.get::<_, String>(1).unwrap(),
        })
    }).unwrap();

    let mut result: Vec<User> = vec![];
    for user in users {
        result.push(user.unwrap());
    }

    Json(result)
}

#[get("/users/<user_id>")]
fn get_user(user_id: usize) -> Option<Json<User>> {
    let conn = Connection::open("my.db").unwrap();
    let mut stmt = conn.prepare("
        SELECT
            user_id, name
        FROM
            users
        WHERE
            user_id = ?
    ",
    ).unwrap();

    let id = user_id as i64;
    let mut users = stmt.query_map(&[&id], |row| {
        Ok(User {
            user_id: row.get::<_, i64>(0).unwrap() as usize,
            name: row.get::<_, String>(1).unwrap(),
        })
    }).unwrap();

    let user = users.next();
    match user {
        Some(user) => Some(Json(user.unwrap())),
        None => None,
    }
}

fn main() {
  rocket::ignite().mount("/", routes![get_users, get_user]).launch();
}

メインの処理はほぼ変わっておらず、User構造体をシリアライズ可能にしたのとレスポンスをJSONにしたくらいです。

上記を実行すると以下のようになります。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/demo`
🔧 Configured for development.
    => address: localhost
    => port: 8000
    => log: normal
    => workers: 32
    => secret key: generated
    => limits: forms = 32KiB
    => keep-alive: 5s
    => tls: disabled
🛰  Mounting /:
    => GET /users (get_users)
    => GET /users/<user_id> (get_user)
🚀 Rocket has launched from http://localhost:8000

curlで確認してみると、問題なく動いています。

$ curl http://localhost:8000/users
[{"user_id":1,"name":"田中太郎"},{"user_id":2,"name":"鈴木次郎"}]
$ curl http://localhost:8000/users/1
{"user_id":1,"name":"田中太郎"}
$ curl http://localhost:8000/users/999
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>404 Not Found</title>
</head>
<body align="center">
    <div align="center">
        <h1>404: Not Found</h1>
        <p>The requested resource could not be found.</p>
        <hr />
        <small>Rocket</small>
    </div>
</body>
</html>

現状だと、存在しないユーザIDを指定した場合でも404は返りますが、デフォルトのままHTMLになってしまっているので、しっかりやるならエラーハンドリングが必要です。

PHPerKaigi 2020 に参加する

今年も参加します PHPerKaigi。

 

去年のはこちら。

 

今年もPHPerチャレンジがあるらしいですね

トークン…

 

今年も楽しみたいと思います!

ちなみに、今年は弊社スポンサーです。

 

Rustを初めて触ってその流れでDBからデータ取得までを試してみた

とある理由で、ほぼ遊び感覚で、Rustを使う機会がありました。
多少勉強しましたが、新しく難しい概念に悩まされ、イマイチ理解は進みません。
とりあえず手を動かしたかったので、DB取得までをやってみました。
メモとして残しますが、もっとRustらしく書けるんだろうなと感じています。
rusqliteを使って、sqlite3からデータを取得し表示するまでです。

環境構築は本家を参考にしました。
www.rust-lang.org

以降は、 rustc 1.42.0-nightly で動作確認済みです。
また、 my.db という名前でsqlite3のDBを用意していて、スキーマ等は以下の通りです。

$ sqlite3 my.db
sqlite> .schema users
CREATE TABLE users (user_id int primary key, name text not null);
sqlite> select * from users;
1|田中太郎
2|鈴木次郎

実際に書いたコードは以下です。

Cargo.toml

[package]
name = "demo"
version = "0.1.0"

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

[dependencies]
rusqlite = "*"

main.rs

extern crate rusqlite;

use rusqlite::{Connection, NO_PARAMS};

#[derive(Debug)]
struct User {
    user_id: usize,
    name: String,
}

fn get_users() -> Vec<User> {
    let conn = Connection::open("my.db").unwrap();
    let mut stmt = conn.prepare("
        SELECT
            user_id, name
        FROM
            users
        ORDER BY
            user_id ASC
    ",
    ).unwrap();

    let users = stmt.query_map(NO_PARAMS, |row| {
        Ok(User {
            user_id: row.get::<_, i64>(0).unwrap() as usize,
            name: row.get::<_, String>(1).unwrap(),
        })
    }).unwrap();

    let mut result: Vec<User> = vec![];
    for user in users {
        result.push(user.unwrap());
    }

    result
}

fn get_user(user_id: usize) -> Option<User> {
    let conn = Connection::open("my.db").unwrap();
    let mut stmt = conn.prepare("
        SELECT
            user_id, name
        FROM
            users
        WHERE
            user_id = ?
    ",
    ).unwrap();

    let id = user_id as i64;
    let mut users = stmt.query_map(&[&id], |row| {
        Ok(User {
            user_id: row.get::<_, i64>(0).unwrap() as usize,
            name: row.get::<_, String>(1).unwrap(),
        })
    }).unwrap();

    let user = users.next();
    match user {
        Some(user) => Some(user.unwrap()),
        None => None,
    }
}

fn main() {
    // 一覧取得
    println!("{:?}", get_users());
    // 存在するidを取得
    println!("{:?}", get_user(1));
    // 存在しないidを取得
    println!("{:?}", get_user(999));
}

上記を実行すると以下のようになります。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/demo`
[User { user_id: 1, name: "田中太郎" }, User { user_id: 2, name: "鈴木次郎" }]
Some(User { user_id: 1, name: "田中太郎" })
None

ユーザ一覧の取得とユーザ単体の取得(いる場合といない場合)についてうまくできました。
もうちょっとRustらしく書けそうなところもあり、 unwrap() しすぎな感じもありますが、今後の自分に期待します。