この記事はRustその2 Advent Calendar 2018の22日目の記事です。
現在個人でRustを使ってコンテナランタイムの開発に挑戦してみているのですが、その中で利用しているcrateを知見として紹介したいと思います。
#作ろうと思ったきっかけ
コンテナ型仮想化の情報交換会に参加させて頂き、自作コンテナランタイムの話を聞いて興味をもったのがきっかけです。
もともと当時はDockerひいてはコンテナに興味をもっていた時期だったのですが、コンテナの事をもっと詳しく知りたいと思い上記の勉強会に参加させてもらったところ、自作コンテナランタイムを開発している方の話を聞き「自分も開発してみたい!」と思うようになり開発に乗り出しました。
#何故Rustなのか
DockerはGoで開発されているので最初はGoを使おうかと思っていたのですが、上記の勉強会でRustという選択肢もあると教えて頂きました。
当時はRustを知らなかったのですが調べて行くうちに興味が出たのと、Goと同じく低レイヤーのプログラミングも可能だったのでRustで作る事を決定しました。
#自作コンテナランタイムについて
smiyaguchi/miniconという名で開発しています。
コンテナランタイムの標準仕様のOCI(Open Container Initiative)とkubernetesのインタフェースであるCRI(Container Runtime Interface)の仕様を満たした実装にできればと思い開発しています。
#使用しているcrate
前置きが長くなってしまいましたが、開発で使用しているcrateを紹介していきます。
##nix
nix-rust/nix
自作コンテナランタイムの開発で一番使用しているcrateです。
nix
は一言でいうとlibc
のラッパーです。
コンテナを作る際はkernelの機能を利用するのでシステムコールを発行する事になります。
システムコールの発行はlibc
等を使っても可能なのですがlibc
を使う場合、自分でunsafeを使用してコンパイル時の安全を保証しないと行けなくなってしまいます。
nix
を使用するとlibc
をラップしているのでRustの強力な型システムに乗せることができ、コンパイル時に安全性を保証できるようになる為、開発でもlibc
ではなくnix
を使っています。
use nix::sys::socket::{connect, socket};
use nix::sys::socket::{AddressFamily, SockAddr, SockType, SockFlag, UnixAddr};
#socketの作成・接続
fn create_socket() -> Result<(), Error> {
let esocket = "example-socket";
let mut esocket = socket(AddressFamily::Unix, SockType::Stream, SockFlag::empty(), None)?;
esocket = match connect(esocket, &SockAddr::Unix(UnixAddr::new(&*csocket)?)) {
Ok(()) => esocket,
Err(_) => -1,
};
Ok()
}
nix
で対応していないシステムコールもありますので、その場合は自身で対応する必要があります。
##clap
clap-rs/clap
有名なcrateなのでご存知の方も多いかと思います。cliを作る際に便利なコマンドライン解析ライブラリですね。
OCIではランタイムからコンテナを操作する際の仕様を定義しています。
(コンテナを作る時はcreate
・start
で操作できる様にする等)
自作コンテナランタイムではDockerと同じくcliとして上記コンテナの操作の仕様を満たせる様にしたいと思っていますのでclap
を利用しています。
cliの定義を記載してyamlファイルを用意して読み込む事も可能です。
name: example
version: "0.1.0"
args:
- version:
short: v
long: version
help: Show version
subcommands:
- create:
about: create a container
args:
- id:
required: true
help: container id
- bundle:
short: b
long: bundle
required: true
default_value: .
help: path to bundle
#[macro_use]
extern crate clap;
use clap::App;
fn main() {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml).get_matches();
// 以下処理
}
##serde, serde_derive,serde_json
serde-rs/serde
serde-rs/serde-derive
serde-rs/serde-json
こちらも超有名crateなので皆さんご存知かと思います。シリアライズ/デシリアライズのフレームワークですね。
OCIではconfig.json
というファイルにコンテナの構成情報が記載される事になっており、ランタイムはその情報を元にコンテナを作成するという仕様になっています。
なので開発ではconfig.json
のシリアライズ/デシリアライズに利用しています。
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
#[derive(Serialize, Deserialize, Debug)]
pub struct Spec {
#[serde(default, rename = "ociVersion")]
pub oci_version: String,
}
impl Spec {
pub fn load() -> Result<Spec, Error> {
let file = File::open("config.json")?;
let buf_reader = BufReader::new(file);
let spec: Spec = from_reader(buf_reader)?;
Ok(spec)
}
}
fn load_spec() -> Result<Spec, Error> {
let spec = Spec::load()?;
Ok(spec)
}
##failure
rust-lang-nursery/failure
ライブラリのエラーを定義するのに便利なcrateです。
私も使い初めて日が浅いので使い方を学んでいる最中ですが、自身のライブラリ用に新しくエラーを定義するのに便利なcrateだという認識です。
(このあたりもしっかり理解したい...)
自作コンテナランタイムの中で独自のエラー定義を行えるように利用しています。
extern crate serde;
extern crate toml;
#[macro_use] extern crate failure;
#[macro_use] extern crate serde_derive;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use failure::Error;
// This is a new error type that you've created. It represents the ways a
// toolchain could be invalid.
//
// The custom derive for Fail derives an impl of both Fail and Display.
// We don't do any other magic like creating new types.
#[derive(Debug, Fail)]
enum ToolchainError {
#[fail(display = "invalid toolchain name: {}", name)]
InvalidToolchainName {
name: String,
},
#[fail(display = "unknown toolchain version: {}", version)]
UnknownToolchainVersion {
version: String,
}
}
pub struct ToolchainId {
// ... etc
}
impl FromStr for ToolchainId {
type Err = ToolchainError;
fn from_str(s: &str) -> Result<ToolchainId, ToolchainError> {
// ... etc
}
}
pub type Toolchains = HashMap<ToolchainId, PathBuf>;
// This opens a toml file containing associations between ToolchainIds and
// Paths (the roots of those toolchains).
//
// This could encounter an io Error, a toml parsing error, or a ToolchainError,
// all of them will be thrown into the special Error type
pub fn read_toolchains(path: PathBuf) -> Result<Toolchains, Error>
{
use std::fs::File;
use std::io::Read;
let mut string = String::new();
File::open(path)?.read_to_string(&mut string)?;
let toml: HashMap<String, PathBuf> = toml::from_str(&string)?;
let toolchains = toml.iter().map(|(key, path)| {
let toolchain_id = key.parse()?;
Ok((toolchain_id, path))
}).collect::<Result<Toolchains, ToolchainError>>()?;
Ok(toolchains)
}
※公式より引用
#終わりに
以上が現在開発で使用している主なcrateになります。
いろいろ調べながら進めており、まだまだ未完成な為あまり数がないですが現在利用しているcrateを紹介させてもらいました。
また、便利なcrateがあれば紹介したいと思います。