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-Origin
と Vary
が増えています。
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チャレンジがあるらしいですね
トークン…
PHPer Kaigi めちゃ楽しかったです!
— おおしげ🎲 (@_ohshige) 2019年3月31日
登壇する同僚たちや強強エンジニアな方々を見るだけの予定でしたが、とても楽しめました。
そして、まさかのPHPerチャレンジもタナボタ3位(実質4位)の入賞で嬉しかったです。が、トークンや # はもうお腹いっぱいです。
とりあえず来年も参加したい!#phperkaigi
今年も楽しみたいと思います!
ちなみに、今年は弊社スポンサーです。
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()
しすぎな感じもありますが、今後の自分に期待します。