Appsody をコンパニオンとして、Rust アプリケーションをビルドしてデプロイする

Rust プログラミング言語がソフトウェア開発コミュニティーで大ヒットしています。その人気を確立しているのは、Rust の多用途性、コンパイルされたコードのパフォーマンス、革新的なメモリー管理モデルという利点です。

Appsody は、アプリケーション開発サイクルの各ステージでソフトウェア開発者の生産性を高めます。Appsody では、アプリケーション・コードが変更されるたびにコードを再アセンブルして再実行するコマンドを使用できるだけでなく、開発者が標準的な Docker と Kubernetes の scaffold 生成を処理しなくてもアプリケーションを開発用クラスターにデプロイできるコマンドもあります。

このチュートリアルでは、最近リリースされた Rust 用の Appsody 試験版スタックを取り上げます。「The Rust Programming Language」という書籍で使用されているいくつかのサンプルに従って小さな Web サーバー・アプリケーションに取り組んだ後、そのアプリケーションをパッケージ化して稼働中の Kubernetes クラスターにデプロイします。

前提条件

以下の手順に従って、ローカル・ワークステーション上でアプリケーションをビルドしてテストしてください。

  • Appsody CLI をインストールします。

  • Docker をインストールします。Windows または macOS を使用している場合は、おそらく Docker Desktop が最善の選択肢でしょう。Unix システムを使用している場合は、minikube とその内部 Docker レジストリーを使用することをお勧めします。

  • Kubernetes をインストールします。minikube はほとんどのプラットフォーム上で機能しますが、Docker Desktop を使用する場合、Docker Desktop の「Preferences (環境設定)」パネル (macOS) または「Settings (設定)」パネル (Windows) で内部 Kubernetes を有効にして使用するほうが便利です。

所要時間

前提条件が満たされていれば、30 分未満でこのチュートリアルを完了できるはずです。

手順

このチュートリアルに従って、次の手順を行います。

  1. アプリケーションを作成する
  2. アプリケーションを実行する
  3. インタラクティブなアプリケーションを実行する
  4. Web サーバーを実装する
  5. (オプション) 完全な Web サーバーをパッケージ化する
  6. アプリケーションをクラスターにデプロイする

以下に示す各フェーズを進めて、チュートリアルを完了します。

チュートリアルの進捗

1. アプリケーションを作成する

Appsody は Appsody スタックの概念を中心に、コンテナー・スタックを事前パッケージ化されたコード・テンプレートと組み合わせて作成されています。この手順では、まず始めに Appsody コマンド・ライン・インターフェース (CLI) を使用して、試験版 Rust スタックを使ったアプリケーションを作成します。

ターミナル・ウィンドウを開いて、以下のコマンドを実行します。これらのコマンドにより、アプリケーション・ディレクトリーが作成されて、試験版 Rust スタックにバンドルされている単純な Rust アプリケーションのテンプレートがディレクトリー内に取り込まれます。

mkdir rust-microservice
cd rust-microservice

appsody init experimental/rust

次のステップでは、アプリケーション・ディレクトリー内にすべての Rust ソース・コードと Cargo.toml ファイルを生成します。生成されるファイルのうち、以下にリストアップされているのがコア・ファイルです。

Cargo.toml
src/main.rs

ここで少し時間を取って Cargo.toml ファイルの [package] セクションに変更を加えることもできますが、このチュートリアルの場合、その必要はありません。

src/main.rs ソース・ファイルには、次の小さな Rust プログラムが含まれています。

fn main() {
    println!("Hello from Appsody!");
}

2. アプリケーションを実行する

作成されたアプリケーションを初めて実行します。

アプリケーションを作成したときと同じターミナルとディレクトリーをそのまま使用して、以下のコマンドを実行します。

appsody run

Appsody の最初の実行では、アプリケーションの依存関係をダウンロードしてキャッシングする処理が行われるため、通常よりも少し時間がかかります。Appsody は前提条件をダウンロードしてソース・コードをコンパイルした時点でアプリケーションを起動します。

Running development environment...
Pulling docker image appsody/rust:0.1
Running command: docker pull appsody/rust:0.1
0.1: Pulling from appsody/rust
Digest: sha256:f9096a8f7ba742497ffb6e69f3dff3e5d2a01a63e973e917752f2df1abdf3c63
Status: Image is up to date for appsody/rust:0.1
docker.io/appsody/rust:0.1
Running docker command: docker run --rm -p 1234:1234 -p 5000:5000 -p 8000:8000 --name rust-microservice-dev -v /Users/myuser/workspace/rust-microservice/.:/project/user-app -v rust-microservice-deps:/usr/local/cargo/deps -v /Users/myuser/.appsody/appsody-controller:/appsody/appsody-controller -t --entrypoint /appsody/appsody-controller appsody/rust:0.1 --mode=run
[Container] Running command:  cargo run
[Container]    Compiling rust-simple v0.1.0 (/project/user-app)
[Container]     Finished dev [unoptimized + debuginfo] target(s) in 1.82s
[Container]      Running `target/debug/rust-simple`
[Container] Hello from Appsody!

この時点でお気付きかもしれませんが、Rust 開発ツールをダウンロードしてセットアップする必要はありませんでした。これらのツールは Appsody Rust スタックにバンドルされています。

この小さなアプリケーションに変更を加えて、どこから Appsody が Rust アプリケーションの開発に役立つコンパニオンとしての才覚を際立たせてくるか確認しましょう。

src/main.rs ファイル内の println!("Hello from Appsody!"); の行を、次のコードで置き換えます。

    println!("Hello from Appsody (modified)!");

Appsody は変更を検出すると、コンテナー内で cargo run の実行を再度トリガーすることがわかるはずです。

[Container]    Compiling rust-simple v0.1.0 (/project/user-app)
[Container]     Finished dev [unoptimized + debuginfo] target(s) in 0.88s
[Container]      Running `target/debug/rust-simple`
[Container] Hello from Appsody (modified)!

「The Rust Programming Language」という書籍で行っているようなコード変更を行うとしたら、ほとんどのサンプルを実行するにはこのセットアップで十分です。ただし、ユーザー入力が必要になるサンプルとなると話は違ってきます。次のステップでは、こうしたサンプルに取り組みます。

とりあえず、アプリケーション・コンテナーを停止してください。それには、"Ctrl+C" キーを押した後、次のコマンドを実行します。

appsody stop

3. インタラクティブなアプリケーションを実行する

「The Rust Programming Language」ブックに記載されているサンプルに目を通していくうちに、乱数ジェネレーターを使用して数字を当てるゲームのサンプルに突きあたります。このような場合、Appsody をインタラクティブ・モードで実行して、コマンド・ラインからの入力を受け入れる必要があります。

Appsody は独自のコマンド・ライン・オプションを使用して、コマンド・ラインからの入力を受け入れます。アプリケーションをインタラクティブ・モードで起動するには、次のコマンドを使用します。

appsody run --interactive

src/main.rs ファイルの内容を、「The Rust Programming Language」ブックで使用しているこのサンプルで置き換えてください。

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

コマンド・ラインにコンパイル・エラーが示されて、Rust 開発者が高く評価する、わかりやすく色分けされたコードが表示されます。この時点では Cargo.toml ファイル内に rand クレート依存関係が含まれていないため、エラーが発生するのは当然です。

コンパイル・エラー

この問題を修正するには、以下に示すように、Cargo.toml ファイルの [dependencies] セクションに rand クレートを挿入します。

...
edition = "2018"

[dependencies]
rand = "0.7.2"

[[bin]]
...

Appsody が新しい依存関係をダウンロードし、アプリケーションを再コンパイルして再起動します。

[Container] Running command:  cargo run
[Container] [Warning] Wait Received error starting process of type APPSODY_RUN/DEBUG/TEST_ON_CHANGE while running command: cargo run error received was: signal: interrupt
[Container]    Compiling rust-simple v0.1.0 (/project/user-app)
[Container]     Finished dev [unoptimized + debuginfo] target(s) in 1.86s
[Container]      Running `target/debug/rust-simple`
[Container] Guess the number!
[Container] Please input your guess.

すべてのステップを完了したら (または、数字当てゲームを 1 回終えた後)、コンテナーを終了してもかまいませんが、次のステップに向けて時間と入力の手間を省くために、今のところは稼動中のままにしておきましょう。稼動中のままにしていても、コードの変更によってダウンロードと再コンパイルが必要になれば、いつでも Appsody がその必要を満たしてくれます。

4. Web サーバーを実装する

RocketActixTower-Web などのフレームワークを使用する Rust アプリケーションも、RESTful なアプリケーションの Web サーバーとして役立ちます。

簡単な例として、以下に記載するコードを使用してポート 8000 上で listen すると、常に挨拶メッセージが返されます。

use std::io::Write;
use std::net::{TcpListener, TcpStream};

fn main() {
    let listener = TcpListener::bind("0.0.0.0:8000").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let header = b"HTTP/1.1 200 OK\n\n";
    let contents = b"<html><body><p>Hello from Appsody!</p></body></html>";
    stream.write(header).unwrap();
    stream.write(contents).unwrap();
    stream.flush().unwrap();
}

ネットワーク・バインド・アドレスには、ほとんどの Rust チュートリアルで通常使用されている 127.0.01:8000 ではなく、0.0.0.0:8000 を使用している点に注意してください。このネットワーク・バインド・アドレスは、Rust アプリケーションを実行するコンテナー・イメージ内で使用される内部ネットワーク・インターフェースに合わせて選択したものです。

main.rs ファイルの内容を上記のコードで置き換えてから保存し、Appsody が cargo run を改めてトリガーして実行し、完了するまで待ちます。

完了したら、ブラウザー内で http://localhost:8000 URL にアクセスすると、Hello from Appsody! というメッセージが表示されるはずです。

5. (オプション) 完全な Web サーバーをパッケージ化する

次のステップでは、アプリケーションをパッケージ化して Kubernetes クラスターにデプロイします。けれどもその前に、実際的な Web サーバーを使用したいという場合もあるでしょう。このチュートリアルを完了した後も Rust での実験を続けるとしたら、実際の Web サーバーを使っておくと便利です。

このセクションでは Web フレームワークとして Actix を使用します。Tower Web などの他のフレームワークでも Rust 試験版スタックを使用できますが、Rocket についてはその限りでありません。Rust nightly の要件により、Rocket では専用の Appsody スタックを使用するためです。

このステップでのコンパイルには数分かかります。この時間はチュートリアルの所要時間には含まれていないので、時間に追われている場合はスキップしても構いません。

最初のステップとして、Cargo.toml ファイルの [dependencies] セクション内に Actix クレートを追加します。

...

[dependencies]
actix-web = "1.0.8"
...

次のステップで、src/main.rs ファイルの内容を、Actix のホームページから直接引用してきたスニペットで置き換え、以下のようにネットワーク・バインド・アドレスを 127.0.0.1:8000 から 0.0.0.0:8000 へと変更してコンテナーの内部ネットワーク・インターフェースの設定に合わせます。

use actix_web::{web, App, HttpRequest, HttpServer, Responder};

fn greet(req: HttpRequest) -> impl Responder {
    let name = req.match_info().get("name").unwrap_or("from Appsody");
    format!("Hello {}!", &name)
}

fn main() {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(greet))
            .route("/{name}", web::get().to(greet))
    })
    .bind("0.0.0.0:8000")
    .expect("Can not bind to port 8000")
    .run()
    .unwrap();
}

最初の実行が完了するまでには数分かかります。Appsody が cargo run の呼び出しをトリガーし、新しいクレートのすべての依存関係をダウンロードしてコンパイルするためです。コンパイル・フェーズが完了すると、コマンド・ラインの出力の最後に以下の行が示されます。

...
[Container]    Compiling awc v0.2.8
[Container]    Compiling actix-web v1.0.8
[Container]    Compiling rust-appsody v0.1.0 (/project/user-app)
[Container]     Finished dev [unoptimized + debuginfo] target(s) in 2m 22s
[Container]      Running `target/debug/rust-simple`

Web ブラウザーから、http://localhost:8000/Appsody で実行中のアプリケーションの /{name} エンドポイントを起動します。

6. アプリケーションをクラスターにデプロイする

サンプル Web サーバーが使用できるようになったので、Appsody に備わっている別の利点を確認します。その利点とは、Dockerfile やデプロイメント YAML ファイルを操作しなくても、アプリケーション全体をパッケージ化して Kubernetes クラスターにデプロイできることです。

前のステップで扱ったアプリケーションがまだコンテナー内で実行されている場合は、Ctrl+C キーを押してから、コマンド・ラインで appsody stop を入力して停止してください。

次のステップでは、Docker Desktop または minikube 内で有効にされている Kubernetes クラスターを使用してローカルの Kubernetes インスタンスが稼動していること、そのクラスターの Docker レジストリーに Docker コマンド・ラインでログインすることを前提とします。

以前にビルドされたことのないコンテナーについては、Appsody のデプロイ・コマンドを実行するとコンテナーがビルドされてからデプロイされますが、一応、ビルド・コマンドから見ていましょう。ターミナル内で次のコマンドを入力してください。

appsody build

ビルド・プロセスではイメージのローカル・キャッシュを更新して新しいイメージを生成しなければならないため、この処理には appsody run よりも時間がかかります。

build コマンドにはパラメーターを Docker ビルドに渡すためのフラグに加え、最終イメージにデフォルトのイメージ名とは異なる名前でタグを付けるためのフラグもあります (デフォルトのイメージ名としては、アプリケーション・ディレクトリー名が使用されます)。

ビルドが完了したら、アプリケーションをクラスターにデプロイします。それには、次のコマンドを入力します。

appsody deploy

このリクエストにより、アプリケーション用のコンテナー・イメージの作成プロセスがトリガーされたた後、クラスターへのデプロイ・プロセスがトリガーされます。コンテナーのビルドが完了すると、次のような出力が表示されます。

Running command: kubectl apply -f app-deploy.yaml --namespace default
Deployment succeeded.
Appsody Deployment name is: rust-microservice
Running command: kubectl get rt rust-microservice -o jsonpath="{.status.url}" --namespace default
Attempting to get resource from Kubernetes ...
Running command: kubectl get route rust-microservice -o jsonpath={.status.ingress[0].host} --namespace default
Attempting to get resource from Kubernetes ...
Running command: kubectl get svc rust-microservice -o jsonpath=http://{.status.loadBalancer.ingress[0].hostname}:{.spec.ports[0].nodePort} --namespace default
Deployed project running at http://localhost:30728

最後の行に示されているのは、クラスター内のアプリケーションの最終 URL です。minikube を使用している場合、ローカル・ネットワークがどのようにセットアップされているかによっては、次のコマンドを実行してアプリケーションの URL を取得しなければならない場合もあります。

minikube service rust-microservice --url

取得した URL に Web ブラウザーからアクセスすると、Hello from Appsody! メッセージが再び表示されます。

環境全体をアンワインドする

すべてのテストを行った後、クラスター・デプロイメントと、このチュートリアルで作成したコンテナーを削除してください。

Kubernetes クラスターからデプロイメントを削除するには、次のコマンドを実行します。

appsody deploy delete

まとめ

このチュートリアルを完了した今、ローカルでの継続的ビルド・サイクルから Kubernetes クラスターへのデプロイに至るまで、Rust アプリケーション開発のコンパニオンとして Appsody を使いこなせるようになったはずです。

次のステップ

  • Codewind でアプリケーションを拡張する: Appsody 開発者としての経験を広げるには、よく使われている IDE の Codewind 拡張機能を取り上げているこのリンク先のチュートリアルを読んでください。Codewind を使用して、このチュートリアルで作成したアプリケーションを IDE にインポートすれば、より円滑にコード変更を繰り返すことができるようになります。
  • 新しい Appsody スタックまたはテンプレートを作成する: 独自のスタックの作成に取り掛かる場合、あるいは Appsody 内の Rust コレクションに新しいアプリケーション・テンプレートを追加する場合は、Appsody のカスタマイズに関する記事を読んでください。この記事で Appsody コレクションの概念について理解した後は、このリンク先のチュートリアルに従って独自の Appsody スタックを作成する方法を学んでください。
  • Kabanero について調べる: 最後の提案として、Kabanero.io プロジェクトについて調べることをお勧めします。このプロジェクトは Appsody、Codewind、およびその他のオープンソース・プロジェクトを 1 つにまとめ、アプリケーション開発者の DevOps 操作をアプリケーションのデプロイメントおよび運用に統合します。

謝辞

このチュートリアルの大半をレビューしてくれた Hans Uhlig 氏に感謝の意を表します。特に、サンプルで使用した手法に対する彼のアドバイスは非常に役立ちました。