Terraform環境構築からAmazon linuxの最新イメージを立てるまでのメモ

前提

  • aws configureでアクセスキーなどの設定を済ましている。

Terraformのインストール

$ brew install terraform

tfenvのインストール

$ brew install tfenv

そして怒られ

Error: Cannot install tfenv because conflicting formulae are installed.
Please `brew unlink terraform` before continuing.

言われたとおりunlinkを実行

$ brew unlink terraform

再実行

$brew install tfenv

うまく行ったっぽいので、いろいろ設定。 設定は、実践Terraformに合わせる。

$ tfenv use 0.12.5
$ echo 0.12.5 > .terraform-version
$ tfenv install

定義ファイル書く

data>aws_ssm_parameter>amzn2_am>value名前空間をたどっていくとAMI IDが取得できる。

provider aws {
  region = "ap-northeast-1"
}

# create ami amazon linux 2 latest version
data aws_ssm_parameter amzn2_ami {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

# get ami id from Systems Manager Parameter Store
resource aws_instance tftest {
  ami           = data.aws_ssm_parameter.amzn2_ami.value
  instance_type = "t2.micro"
}

Terraform initとapply

$ terraform init
$ terraform apply

applyにあたって合意を求められるのでyes

  Enter a value: yes

apply完了

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

コンソールで確認

f:id:hagityann224:20200629172920p:plain
居てる

参考資料

dev.classmethod.jp

aws.amazon.com

runcのmain.goを読んでみる

runcのmain.goだけ読んでみる

自分の勉強ついでにruncの記事書きたいなと思い、まずはコードリーディングから始めようと思った。 まず起点となるmain.goの中身を見て、そこから各コマンド郡を見渡してみたい。

https://github.com/opencontainers/runc/blob/master/main.go

TL;DR

  • func main() ではunfave/cliを使ってApp構造体にコマンド実行に必要な情報を定義していく。
  • XDG_RUNTIME_DIRの存在有無とユーザ名前空間での実行かどうかによってrootディレクトリをどこに設定するか制御している。

main.go

まずコード全体。

package main

import (
    "fmt"
    "io"
    "os"
    "strings"

    "github.com/opencontainers/runc/libcontainer/logs"

    "github.com/opencontainers/runtime-spec/specs-go"

    "github.com/sirupsen/logrus"
    "github.com/urfave/cli"
)

// version will be populated by the Makefile, read from
// VERSION file of the source code.
var version = ""

// gitCommit will be the hash that the binary was built from
// and will be populated by the Makefile
var gitCommit = ""

const (
    specConfig = "config.json"
    usage      = `長いので略`
)

func main() {
    app := cli.NewApp()
    app.Name = "runc"
    app.Usage = usage

    var v []string
    if version != "" {
        v = append(v, version)
    }
    if gitCommit != "" {
        v = append(v, fmt.Sprintf("commit: %s", gitCommit))
    }
    v = append(v, fmt.Sprintf("spec: %s", specs.Version))
    app.Version = strings.Join(v, "\n")

    xdgRuntimeDir := ""
    root := "/run/runc"
    if shouldHonorXDGRuntimeDir() {
        if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" {
            root = runtimeDir + "/runc"
            xdgRuntimeDir = root
        }
    }

    app.Flags = []cli.Flag{
        cli.BoolFlag{
            Name:  "debug",
            Usage: "enable debug output for logging",
        },
        cli.StringFlag{
            Name:  "log",
            Value: "",
            Usage: "set the log file path where internal debug information is written",
        },
        cli.StringFlag{
            Name:  "log-format",
            Value: "text",
            Usage: "set the format used by logs ('text' (default), or 'json')",
        },
        cli.StringFlag{
            Name:  "root",
            Value: root,
            Usage: "root directory for storage of container state (this should be located in tmpfs)",
        },
        cli.StringFlag{
            Name:  "criu",
            Value: "criu",
            Usage: "path to the criu binary used for checkpoint and restore",
        },
        cli.BoolFlag{
            Name:  "systemd-cgroup",
            Usage: "enable systemd cgroup support, expects cgroupsPath to be of form \"slice:prefix:name\" for e.g. \"system.slice:runc:434234\"",
        },
        cli.StringFlag{
            Name:  "rootless",
            Value: "auto",
            Usage: "ignore cgroup permission errors ('true', 'false', or 'auto')",
        },
    }
    app.Commands = []cli.Command{
        checkpointCommand,
        createCommand,
        deleteCommand,
        eventsCommand,
        execCommand,
        initCommand,
        killCommand,
        listCommand,
        pauseCommand,
        psCommand,
        restoreCommand,
        resumeCommand,
        runCommand,
        specCommand,
        startCommand,
        stateCommand,
        updateCommand,
    }
    app.Before = func(context *cli.Context) error {
        if !context.IsSet("root") && xdgRuntimeDir != "" {
            // According to the XDG specification, we need to set anything in
            // XDG_RUNTIME_DIR to have a sticky bit if we don't want it to get
            // auto-pruned.
            if err := os.MkdirAll(root, 0700); err != nil {
                fmt.Fprintln(os.Stderr, "the path in $XDG_RUNTIME_DIR must be writable by the user")
                fatal(err)
            }
            if err := os.Chmod(root, 0700|os.ModeSticky); err != nil {
                fmt.Fprintln(os.Stderr, "you should check permission of the path in $XDG_RUNTIME_DIR")
                fatal(err)
            }
        }
        return logs.ConfigureLogging(createLogConfig(context))
    }

    // If the command returns an error, cli takes upon itself to print
    // the error on cli.ErrWriter and exit.
    // Use our own writer here to ensure the log gets sent to the right location.
    cli.ErrWriter = &FatalWriter{cli.ErrWriter}
    if err := app.Run(os.Args); err != nil {
        fatal(err)
    }
}

type FatalWriter struct {
    cliErrWriter io.Writer
}

func (f *FatalWriter) Write(p []byte) (n int, err error) {
    logrus.Error(string(p))
    if !logrusToStderr() {
        return f.cliErrWriter.Write(p)
    }
    return len(p), nil
}

func createLogConfig(context *cli.Context) logs.Config {
    logFilePath := context.GlobalString("log")
    logPipeFd := ""
    if logFilePath == "" {
        logPipeFd = "2"
    }
    config := logs.Config{
        LogPipeFd:   logPipeFd,
        LogLevel:    logrus.InfoLevel,
        LogFilePath: logFilePath,
        LogFormat:   context.GlobalString("log-format"),
    }
    if context.GlobalBool("debug") {
        config.LogLevel = logrus.DebugLevel
    }

    return config
}

importの中身

import (
    "fmt"
    "io"
    "os"
    "strings"

    "github.com/opencontainers/runc/libcontainer/logs"

    "github.com/opencontainers/runtime-spec/specs-go"

    "github.com/sirupsen/logrus"
    "github.com/urfave/cli"
)

こっから見ていくのかよと思いつつ。 urfave/cliがベースになっている。

opencontainers/runtime-spec/specs-goには3つのgoのファイルがあって、

  1. config.go
  2. state.go
  3. version.go

3ファイルとも実装はほとんどが構造体。どこで使用しているか調べるのを諦めた。

runc/libcontainer/logsはlog生成用。 app.Beforeを定義する際の即時関数の返り値のところで使っている(L144あたり)

constのusageに唐突のrunc説明文

宣言部にあたる部分。 コンテナランタイムの実装を見るのはruncが初めてなのですが、なんとも不思議なところにruncの説明文が、、 と思ったら全部usageだった。

// version will be populated by the Makefile, read from
// VERSION file of the source code.
var version = ""

// gitCommit will be the hash that the binary was built from
// and will be populated by the Makefile
var gitCommit = ""

const (
    specConfig = "config.json"
    usage      = `Open Container Initiative runtime
runc is a command line client for running applications packaged according to
the Open Container Initiative (OCI) format and is a compliant implementation of the
Open Container Initiative specification.
runc integrates well with existing process supervisors to provide a production
container runtime environment for applications. It can be used with your
existing process monitoring tools and the container will be spawned as a
direct child of the process supervisor.
Containers are configured using bundles. A bundle for a container is a directory
that includes a specification file named "` + specConfig + `" and a root filesystem.
The root filesystem contains the contents of the container.
To start a new instance of a container:
    # runc run [ -b bundle ] <container-id>
Where "<container-id>" is your name for the instance of the container that you
are starting. The name you provide for the container instance must be unique on
your host. Providing the bundle directory using "-b" is optional. The default
value for "bundle" is the current directory.`
)

usageに入る文字列を要約すると

  • runcはコンテナを実行するためのCLIツールです。
  • OCI仕様に準拠しています。
  • runcは既存のプロセススーパーバイザーと統合して、アプリケーションのためのプロダクションコンテナランタイム環境を提供します。
  • コンテナはbundle使って設定されます
  • bundleとは、ディレクトリ(仕様ファイル)とルートファイルシステムを含むディレクトリを指します。
  • ルートファイルシステムにはコンテナの中身が入ります。
  • コンテナを起動するときは、runc run [-b bundle] [container-id] と打ってください。
  • container-idはコンテナのインスタンスの名前です
  • コンテナのインスタンスの名前は一意にしてください。
  • bundleは、デフォルトではカレントディレクトリになります。

cli実行用基盤の作成

ここからfunc main() cli.NewAppで構成したアプリケーション情報に値を詰めていく作業がメイン。

app := cli.NewApp()
app.Name = "runc"
app.Usage = usage

ここで、appのインスタンス(?)を生成。 appに値をどんどん埋めていく。まずはアプリケーションの名前と先程の長いusage

var v []string
if version != "" {
    v = append(v, version)
}
if gitCommit != "" {
    v = append(v, fmt.Sprintf("commit: %s", gitCommit))
}
v = append(v, fmt.Sprintf("spec: %s", specs.Version))
app.Version = strings.Join(v, "\n")

versionとgitCommitの値を結合していく。 versionとgitCommitの値は、Makefileから生成される。

どこをファイルシステムのrootディレクトリにするか

つまるところ、環境変数で定義したXDG_RUNTIME_DIRの配下にruncのディレクトリを置くか、runの配下に置くかを制御する。

xdgRuntimeDir := ""
root := "/run/runc"
if shouldHonorXDGRuntimeDir() {
    if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" {
        root = runtimeDir + "/runc"
        xdgRuntimeDir = root
    }
}

ここが困惑した箇所。OSについてはてんで素人のため、このような処理がなぜ必要とされているのかがピンと来ていない。

  1. shouldHonorXDGRuntimeDir()
  2. os.Getenv("XDG_RUNTIME_DIR")

この2つの関数の返り値を確認する。

shouldHonorXDGRuntimeDir()

shouldHonorXDGRuntimeDir()はrunc/rootless_linux.go内に存在しているので見に行く。

https://github.com/opencontainers/runc/blob/0fa097fc37c5d89e4cea4fda4663d1239e12a6fe/rootless_linux.go#L37

はじめの分岐
if os.Getenv("XDG_RUNTIME_DIR") == "" {
    return false
}

XDG_RUNTIME_DIRは一時ファイルの保存場所として、ログイン時に自動的に充てがわれるディレクトリの環境変数https://askubuntu.com/questions/872792/what-is-xdg-runtime-dir この環境変数が設定されていない場合はfalseを返す。

euidのチェック
if os.Geteuid() != 0 {
    return true
}

os.Geteuid() != 0から見ていく。ドキュメントをみると以下のような説明になっている。

func Geteuid func Geteuid() int Geteuid returns the numeric effective user id of the caller. On Windows, it returns -1. https://golang.org/pkg/os/#Geteuid

そもそもeffective user id ってなんや。 と思って調べたらここにかいてありました。

https://imokuri123.com/blog/2016/01/linux-ids/

Effective User ID(EUID) - プロセスやユーザが何かオブジェクトにアクセスしたり、作成しようとした時、カーネルは、そのプロセスやユーザがアクションに必要な権限を持っているか、EUIDでチェックします。

euidが0になるときは、ユーザが本当のroot(本当のrootってなんだという話ではあるが)の場合。

link.springer.com

ここに説明が書いてありそうなので後で調べる。

改めて確認だが、やりたいことは環境変数で定義したXDG_RUNTIME_DIRの配下にruncのディレクトリを置くか、runの配下に置くかを決めること。

実行時の名前空間のチェック
if !system.RunningInUserNS() {
    // euid == 0 , in the initial ns (i.e. the real root)
    // in this case, we should use /run/runc and ignore
    // $XDG_RUNTIME_DIR (e.g. /run/user/0) for backward
    // compatibility.
    return false
}

ここに至る条件は、

  1. rootユーザによる実行である場合
  2. XDG_RUNTIME_DIRが存在する場合

である。 ここでは単純に、usernamespaceでの実行かどうかをチェックしている。system.RunningInUserNS()もruncの中にあり実装を確認した。

func RunningInUserNS() bool {
    nsOnce.Do(func() {
        uidmap, err := user.CurrentProcessUIDMap()
        if err != nil {
            // This kernel-provided file only exists if user namespaces are supported
            return
        }
        inUserNS = UIDMapInUserNS(uidmap)
    })
    return inUserNS
}

すこし気になったのが、nsOnce.Doのところ。1回だけ実行させたい理由を考えたがわからなかったのでこれも宿題。

環境変数USERに定義された値をチェック

// euid = 0, in a userns.
u, ok := os.LookupEnv("USER")
return !ok || u != "root"

環境変数"USER"に定義された値がrootではない場合はtrueを返し、rootだった場合はfalseを返す。 そもそもここに至るための条件が、

  1. rootユーザによる実行である場合
  2. XDG_RUNTIME_DIRが存在する場合
  3. usernamespaceでの実行である場合

であるため、いよいよどのような状況かわからなくなってきた。

今一度確認だが、やりたいことは環境変数で定義したXDG_RUNTIME_DIRの配下にruncのディレクトリを置くか、runの配下に置くかを決めること。

フラグの定義をする

Flagsには解析対象となるフラグを定義する。

app.Flags = []cli.Flag{
        cli.BoolFlag{
            Name:  "debug",
            Usage: "enable debug output for logging",
        },
        cli.StringFlag{
            Name:  "log",
            Value: "",
            Usage: "set the log file path where internal debug information is written",
        },
        cli.StringFlag{
            Name:  "log-format",
            Value: "text",
            Usage: "set the format used by logs ('text' (default), or 'json')",
        },
        cli.StringFlag{
            Name:  "root",
            Value: root,
            Usage: "root directory for storage of container state (this should be located in tmpfs)",
        },
        cli.StringFlag{
            Name:  "criu",
            Value: "criu",
            Usage: "path to the criu binary used for checkpoint and restore",
        },
        cli.BoolFlag{
            Name:  "systemd-cgroup",
            Usage: "enable systemd cgroup support, expects cgroupsPath to be of form \"slice:prefix:name\" for e.g. \"system.slice:runc:434234\"",
        },
        cli.StringFlag{
            Name:  "rootless",
            Value: "auto",
            Usage: "ignore cgroup permission errors ('true', 'false', or 'auto')",
        },
    }

フラグは以下の7つ。それぞれの使いみちはいずれ。

  • debug
  • log
  • log-format
  • root
  • criu
  • systemd-cgroup
  • rootless

コマンドを定義する

Commandは実行できるコマンド群。ここで定義しておく。

app.Commands = []cli.Command{
    checkpointCommand,
    createCommand,
    deleteCommand,
    eventsCommand,
    execCommand,
    initCommand,
    killCommand,
    listCommand,
    pauseCommand,
    psCommand,
    restoreCommand,
    resumeCommand,
    runCommand,
    specCommand,
    startCommand,
    stateCommand,
    updateCommand,
}

多い。多いので、それぞれのコマンドの動作を調べていく際にそれぞれ記事にしたい。

サブコマンド実行前の処理を行う。

Beforeはサブコマンドが実行される前に実行されるアクションで、コンテキストの準備ができた後に実行される。 以下の即時関数の中身を見ていくことで、サブコマンド実行前の挙動を見ることができる。

app.Before = func(context *cli.Context) error {
    if !context.IsSet("root") && xdgRuntimeDir != "" {
        // According to the XDG specification, we need to set anything in
        // XDG_RUNTIME_DIR to have a sticky bit if we don't want it to get
        // auto-pruned.
        if err := os.MkdirAll(root, 0700); err != nil {
            fmt.Fprintln(os.Stderr, "the path in $XDG_RUNTIME_DIR must be writable by the user")
            fatal(err)
        }
        if err := os.Chmod(root, 0700|os.ModeSticky); err != nil {
            fmt.Fprintln(os.Stderr, "you should check permission of the path in $XDG_RUNTIME_DIR")
            fatal(err)
        }
    }
    return logs.ConfigureLogging(createLogConfig(context))
}

コメントを読む

According to the XDG specification, we need to set anything in XDG_RUNTIME_DIR to have a sticky bit if we don't want it to get auto-pruned.

訳:XDG の仕様によると、自動的にpruneされないよう、XDG_RUNTIME_DIR にはスティッキービットを設定する必要がある。

スティッキービット(Sticky Bit)とは、ディレクトリに対する特殊なアクセス権である。具体的には、すべてのユーザがこのディレクトリに対して書き込み可能になるが、root権限ないしディレクトリの所有者でしか消せない、という権限である。端的に言えば、ルートディレクトリの保護のために権限の付与が必要。

コマンドを実行する

app.Runが一見見えづらいところにある。 Runがコマンド実行のエントリーポイントになっており、Runの中でargsの文字列操作などを実施する。

// If the command returns an error, cli takes upon itself to print
// the error on cli.ErrWriter and exit.
// Use our own writer here to ensure the log gets sent to the right location.
cli.ErrWriter = &FatalWriter{cli.ErrWriter}
if err := app.Run(os.Args); err != nil {
    fatal(err)
}

コメント部分も読んでいく。

If the command returns an error, cli takes upon itself to print the error on cli.ErrWriter and exit. Use our own writer here to ensure the log gets sent to the right location.

訳:コマンドがエラーを返した場合は cli.ErrWriter にエラーを表示して終了します。 ここでは独自のライターを使ってログを適切な場所に送るようにします。

まとめ

  • 細かく見ていくと、なぜその実装なのか解釈すると面白そうなところが多い。rootディレクトリの処理など。
  • 全体の流れは比較的わかりやすく、unfave/cliの使い方の参考になった。

以降、各コマンド群を見ていこうと思う。

コンテナランタイムのことをもっと知ろうとした話(未完成記事)

はじめに

がんばって書いていたのですが、途中で投げ出してしまった記事です。なんで投げ出してしまったかというと、単純に興味のスコープが散らばりすぎて、記事としてまとまりがなくなってしまったからです。ではなぜ公開しているかというと、CS学位なし新卒には、コンテナランタイムってこういう見え方をしているんだよという資料として、案外価値があるんじゃないかと思ったためです。書き直そうとすると面倒だったのでこのままぶん投げてしまえという気持ちになった

コンテナランタイムの記事は、また改めて書いていこうかなと思います。

第一層、コンテナランタイムなんもわからん

 メイドインアビス面白いですよね。ニチアサにアニメ一気観したので軽く鬱ってました。

 第一層ということで、コンテナランタイムの仕組みというより、コンテナランタイムにDeepDiveするための興味関心の準備からします。Docker/Kubernetesは先週勉強を始めたくらいのペーペーなのですが、そのときふと思ったのが、「コンテナランタイムってなんかいっぱいあるよね」ということでした。containerdとかrunCとか、よくわからん。なんだこいつら。

 そもそも「コンテナ」という単語自体とても曖昧だと思うんですよ。コンテナに荷物、つまり、必要なミドルウェアとかいれてrunすると勝手にウェイって動くぜみたいな。定義なく進めるのはアレなので、ここではコンテナを「実行状態のプロセス」とします。なんとなくそういう説明が多かった気がするので。ランタイムは「実行環境」ですね。「コンテナランタイム」っていう単語の整理だけすると、概観は下図の状態になりそう。

図1 コンテナとコンテナランタイムの上下関係?
図1 コンテナランタイムによってプロセスを動かす。画像がでかい。

 なんてお粗末なんだこの図は、と思いはするのですが、この図だけ見ると、コンテナランタイムというのはコンテナの管理をしてそうだねというのはなんとなくわかります。 じゃあコンテナの管理ってなんだろうということを考えると、コンテナの作成や削除とかそういうことしてるのかなーと思うわけです*1。 といっても、作成や削除ってどうやってるのでしょう。それ以前に、コンテナってどこからリソースを引っ張ってくるんでしたっけ。 コンテナ仮想化技術のおさらいからやります。。

第二層、コンテナ仮想化技術もなんもわからん

 コンテナってどこで作られてるの?コンテナってそもそもどういう仕組み?ということで、コンテナ仮想化技術について軽く触れておきます。

 コンテナ仮想化技術について一言で「コンテナ仮想化技術では、OSのリソースを隔離して仮想OSにする」と言われてもピンと来ないですね。僕はピンと来なかったです。 ですので、ここでは「アプリケーションやライブラリなど実行環境の塊をプロセスとして切り出す」という表現にしますね。多少は良くなった?マサカリが怖い。

 言葉だけでは全くわからんので、まずはコンテナを考慮しない場合について図を交えて考えて見ましょう。下図のようになりそうですね

ホストOSで普通にアプリケーションを動かすとき。それはそう、みたいな図になる
ホストOSで普通にアプリケーションを動かすとき。それはそう、みたいな図になる

これを、コンテナにアプリケーションやライブラリ、ミドルウェアなどを突っ込んだ状態にすると、以下のようになります。

図3 コンテナにアプリケーションなどを詰めてプロセスとみなす様子。
独自解釈に塗れた図である。本当はカーネルを間借りしたりするので注意されたし。

 あとはこの塊を、どうやって仮想OSっぽくしていくかについて考えましょう。

 仮想OSなのでOSっぽいものが欲しいですね。例えば、ルートファイルシステムとか*2。特定のコンテナ固有のリソースに無制限にアクセスできてしまうのは問題なので、リソース隔離もされていて欲しい。ネットワーク空間もコンテナ内で隔離させたい。隔離するためには名前空間が必要になりますね。あとは、複数のコンテナを立てたりすると、ホストのOSに対する負荷がどんな状況か見たくなる*3ので、プロセスが使用しているCPU時間やメモリ使用量の監視がしたいですね。ついでに使用量の制限とかできたら最高です*4

 これらの振る舞いの実現を考えると、名前空間を提供したり、プロセスを監視したり、つまり、Linuxカーネルでいうところのnamespaceやcgroupが必要*5になってくるんですね。なるなる。発想としては「別にハードウェアのリソース使って仮想のホストを立てなくても、共有できるところは共有してプロセスとして切り出したほうがコスパいいんじゃないか?」といったところでしょうか*6

 とはいえ、あまりにざっくり過ぎますね。隔離ってじゃあ具体的になにを隔離すればいいの?リソースって具体的になに?リソースの管理ってどこまでやるの?、、など、このままではガン詰めされてしまいそう*7なので、隔離とリソース管理についてもう少しだけ詰めていきましょう。

第三層、「一生Namespaceに閉じ込めてかわいがってあげたい」

 かぐや様二期はあざといハーサカが出てきたり雑魚ミコちゃんが出てきたり面白くなってきましたね。萌葉ちゃんドぎつくていい。コンテナの一生は短いからあまり楽しめなさそうだけども。

 上層で、コンテナを仮想OSとしてみなすためのざっくり条件を掲示しました。あとは仮想OSとして何が必要なのか、もう少し考えていくことにしましょう。Dockerによるコンテナの起動を考えます。ざっくりイメージですが、以下の図のような具合でしょうか。

f:id:hagityann224:20200503115018p:plain
例として、コンテナでNginxを動かす場合を考えてみる。
pullしてrunしておけい!みたいなざっくり感を表しています

 とりあえず例をNginxにしましたが、なにはともあれコンテナを動かすためには、そのコンテナに何を突っ込むか、つまりコンテナイメージが必要ですね。次は、いざNginxを動かすときですね。 そもそもプログラムの実行はプロセスとみなされているわけですが、アプリケーションの実行はコンテナの中で閉じていればいいので、コンテナのプロセスだけ見えればいいですね。ということプロセスツリーを隔離します。プロセスツリーがコンテナ内に隔離されたので、プロセス間通信ができる範囲(プロセス集合)も隔離します。コンテナの中にプロセス集合を閉じ込めることができました。これが、PID namespaceとIPC namespaceによる隔離です。

f:id:hagityann224:20200503215621p:plain
プロセスツリーはコンテナごとに独立させる。同じIDのプロセスをコンテナごとに持つこともできるわけだが、ホストOS側から見ると混乱のもとになるので、ホストOSが見るときのために別のプロセスIDが付与される

 一旦、プロセスがコンテナ内だけで動くようになった、はず、です。しかし仮想OSであるわけですからroot権限を持ったユーザーが要りそう。でもこのユーザにホストOSの権限も持てちゃったら危険ですよね。なので、ユーザーも隔離してしまいます。これがUser namespaceによる隔離です。

f:id:hagityann224:20200503220857p:plain
隔離されたコンテナの中でのみrootとして振る舞うことができる。
   コンテナ内でのroot権限はゲットしましたが肝心のファイル操作がまだできそうにないです。なにせ、ファイルシステムがマウントされていないので。なければマウントしましょう。もちろんそのファイルシステムツリーもコンテナ内に閉じるよう隔離させます。他のプロセスからファイルが自由に見れてしまうのはよくない。ここがMount namespaceによる隔離になります。

f:id:hagityann224:20200503222230p:plain
manページの説明ではあまり良くわからなかったが図の通りで合っていると思う

 これでだんだん仮想OSっぽくなってきました。

 コンテナ実行のイメージを整理しました。  隔離対象は以下ですね

こいつらが隔離対象なので、あとはこれをコンテナランタイムがどうやって表現するかということに関心が向いてきましたね。とはいえ、コンテナ作成にあたって、コンテナランタイムがどこまで担当するのかというスコープがはっきりしないと整理できないので、まずはコンテナの作成フローを、Kubernetesを交えながら整理しましょう

 

第四層、コンテナを作っていいのは誰

 コンテナ仮想化技術について触れたところで、話は「コンテナの作成や削除ってどうやってやってるの」という話題に戻ります。 Kubernetesも勉強したてなので、Kubernetesにおけるコンテナの作成もからめていきます。Kubernetesに触れた*8ことがある方のほぼ100%が「kubectl」というコマンドを叩いていると思います。kubectlでPodのデプロイを指示したときはコンテナが作成されるわけですが、実際どんなふうになっているのでしょう。

 kubectlを叩くと、そのリクエストはAPI Serverに飛んでいきます。今回は「Pod作って」という依頼を投げます。そうすると、API Serverはkubectlによって依頼された情報をもとにetcdを更新します。とetcdはクラスタの構成情報を管理するKVSです。ここに、Podがいくつ必要なのか、どのNodeに割り当てているのかといった情報を格納していきます*9。 とりあえず、クラスタのあるべき姿を先に更新しておいて、あとは各々よしなによろしくお願いします、という具合ですね。

f:id:hagityann224:20200502232902p:plain
まず、etcdにPodのあるべき姿を登録しておく。本来であれば、ControllerManager内にあるDeploymentController、ReplicaSetControllerとのやり取りが存在するが、ここでは割愛*10

 そして、APIServerをウォッチしていたSchedulerが構成情報の更新を検知して、Podをどこに置くか決めます。ちょうど図5のような感じでやりとりをします。 SchedulerはAPIを介してetcd上のPodの情報、ここでは配置するNodeの情報を更新します。

f:id:hagityann224:20200503080100p:plain
図5 ラベルなどのメタデータやCPU・メモリのリソース上限などによってNodeが選択される

 Schedulerはあくまでetcd上の情報の依頼をするだけで、直接Pod作成の依頼をするわけではありません。では作成はどうするのかというと、NodeにあるのkubeletがAPIServerを常に監視しているのでPodの情報が更新されたらそれに気づけるようになっており、変化を検知したkubeletがPodの作成を開始します。

f:id:hagityann224:20200503083556p:plain
図5 変化を検知できるよう、Kubeletも常にAPIServerを監視している。

 とはいえ、kubeletは監視と指示出しがメインの仕事です。ですから、kubelet自身はPod(=コンテナ)を作成しません。代わりに、コンテナランタイムにPodの作成を依頼します。やっとここでコンテナランタイムが登場します。

f:id:hagityann224:20200503083939p:plain
図6 コンテナランタイムにコンテナ作成の依頼をする

 コンテナランタイムがコンテナを作成していますね。1層で想像した感じになりました。

 ここまで、コンテナに必要なものはなにかという材料と、コンテナを作るのはだれかということについて整理ができました。次は、実際にコンテナランタイムがどのようにしてコンテナを作成しているのかという話題に移りましょう。ここまで長かった。

第五層、コンテナ

 上層でいろいろ見てきたので、ここから本格的にコンテナランタイムそのものについて*11です。コンテナに必要なモノは以下でしたね

  1. コンテナ実行環境のイメージファイル
  2. ルートファイルシステム
  3. Pod用の仮想NIC
  4. 隔離環境

 ここで整理したいのは、Linuxカーネルの技術とどこで交わるかという点です。Linuxカーネルの機能として使いたいのは、namespaceとcgroupです*12。イメージファイルの獲得や仮想NICのセットアップは、このLinuxカーネル機能と直接的に関わるものではありません。なので、namespaceとcgroupを使うタイミングとその下準備で処理のレイヤーは分ける必要があります。実際にnamespace、cgroupを使うのが低レベルランタイム、使う準備をするのが高レベルランタイムになります。図にすると以下のようになります。

f:id:hagityann224:20200503170711p:plain
ざっくりではあるが、高レベルランタイムと低レベルランタイムの役割分担
高レベルのコンテナランタイムにはそれぞれ個性がある。Dockerにはビルド機能がついているし、CRI-Oにはログ収集・監視用のデーモンが動くような仕組みがある。containerdはplugableで汎用的。)

 高レベルランタイムの処理から低レベルランタイムの処理まで、コンテナの作成を例にして処理を追っていきます。まず、イメージの取得からです。以下の図のように、イメージはレジストリに置いてあるのでそれを取得してきます。kubectlはあくまで指示を出すだけで、pullしてくるのはコンテナランタイムなのがポイントです。

f:id:hagityann224:20200503143123p:plain
イメージをレジストリから取得する。マネージドのkubernetesやその他コンテナオーケストレーションサービスの場合は、そのベンダーのマネージドのレジストリサービスを使うことが多い。

層 ランタイムのつかいわけ

 ここまで、コンテナ仮想化技術からPod作成を経由してコンテナランタイムを理解するところまで行いました。これらを踏まえて、各ランタイムの特徴を抑えていきます。

Docker

 Dockerが高レベルランタイムという位置づけになるのは少し不思議な感覚ですね。とはいえ、コンテナイメージを持ってきてコンテナを実行するのですからコンテナランタイムですよね。Dockerはビルドの機能やSwarmによるオーケストレーション機能など、これ自体でかなり高機能です。とはいえ、Kubernetesなどによるオーケストレーションの場合、そもそもコンテナランタイムがイメージのビルドをする必要はないので、もう少し軽くていいよねとは思います。

containerd

 例の、軽くていいよね、がこいつです。もともとDockerの一部でした。containerdの最大の特徴は「改造できる」ことです。プラグインを入れていくことでユースケースに対応していくようなアーキテクチャになっています。 AWSのFirecrackerもcontainerdにプラグインを足していく形態をとっているそう。

低レベルランタイム

 低レベルランタイムではホストのカーネルを借りるわけですが、ちゃんと隔離されていないと、他のコンテナで動いている悪いアプリケーションにNodeごとふっとばされたりするかもしれないのが怖いですね。なので、このレイヤーでは、「コンテナの隔離の方法」ということへの関心が強くなります。どうやってセキュアにしていくかという問題に対するアプローチは様々なので、低レベルランタイムの標準仕様さえ守っておけばいろんな実装が考えられます。今後も動向を見守りたい分野ですね。

runC

 Dockerのデフォルトランタイムです。Linuxカーネルのnamespace, cgroupを使って隔離しています。セキュリティの方ですが、AppArmorやSELinuxなどによってコンテナの挙動制限をするにとどまっていそう。runCの面白いところは、中にあるlibcontainerというディレクトリです。これ実はもともとDockerの中にあるライブラリでした。DockerとCoreOSとでコンテナランタイムの標準化戦争やっていたみたいですがなんやかんや落ち着き、runCという標準化されたランタイムになったという歴史があります。このlibcontainerがコンテナ操作用のライブラリですから核はこちらになります。あとはOCI標準*13のインターフェースに合わせるようにコマンドが拡張されています。createコマンドやrunコマンドなど、馴染みのあるインターフェースの実装が追加されてくるわけですね。

gVisor

 Google発コンテナランタイムです。runCとの違いは、ユーザー空間にカーネルを持ってきているところです。なんでこんなことしているのかというと、そもそも使えるシステムコールを安全なものに制限してしまいたいから。

gvisor.dev

f:id:hagityann224:20200503215107p:plain
ホストOSへのシステムコールは68個にまで減らされる

アプリケーションから飛んできたシステムコールは、本来であればホストのカーネルに行くはずなんですが、ユーザ空間にあるカーネルがそのシステムコールを横取りして代わりにコールするという具合。こうすれば、セキュアなシステムコールだけ飛ばしてホストOSを守るということができますね。ただ欠点もあって、システムコールが制限されているので、一部互換性に問題があるらしい。

f:id:hagityann224:20200503214116p:plain
Google色で激しい。SentryというLinuxカーネルエミュレータシステムコールを横取りしているのがわかる。加えて、ファイルアクセスはGoferというまた別のプロセスを介して行われる。

Kata Containers

 OpenStack Foundationで開発を推しているコンテナランタイムです。こちらはさらにセキュリティ志向が高いです。なんと仮想マシンを乗っけてしまいます。たしかにカーネルごと隔離されてしまえば、ホストOSとの隔離はかなり強くなりますね、しかも仮想マシンはコンテナではなくPodとして扱うので、Podごとにカーネルを持つという贅沢 。ただ仮想化してさらに仮想化してなので、パフォーマンスは落ちてしまうそう*14

f:id:hagityann224:20200503212650p:plain

About Kata Containers | Kata Containers

Kata ContainersのアーキテクチャVMの中にカーネルを置いてありコンテナはその上においてある。この図だけだとコンテナが一つなので贅沢感が伝わらないのが残念。

「コンテナランタイムなんかいっぱいあるよね」というなんとも情けないスタートでしたが、それぞれのコンテナランタイムの特徴を抑える前提知識を一通り揃えることができたような気がします。

終わりに

 最後まで読んでいただいた方、インプットをごった煮にした文章を読んで頂きありがとうございました。構造化された文章はわかりやすいですが、無味乾燥としていて読んでいて面白くないなぁと思い、関心を深く掘り下げていくスタイルをとりました。関心が分散しないよう努めましたが、結局この長さになってしまいました。また本音を言えばもう少し実務によった話が書けるようになりたいなぁと思ってます。とはいえ実務でKubernetesもDockerも使っていないので現場志向の記事が書けるはずもなく。転職したい。次はrunCについて書きたいですが、デレステフルコンリレーをやっているせいかキーボードが打てないのでまた今度。

*1:もう少し細かい解釈を加えます。コンテナのライフサイクルとそれに対する操作を考えると、コンテナの状態の取得 / コンテナの作成 / コンテナ内でのアプリケーションの実行 / コンテナの終了/ コンテナの削除の5つの操作があります。これらを踏まえ、コンテナ作成の下準備である高レベルランタイムと実際のコンテナ操作を行う低レベルランタイムをまとめてコンテナランタイムと呼ぶのが一般的なのでしょう

*2:さも「当然じゃ~ん」みたいな物言いですが、筆者はこのへんあまりわかってないです。入門記事程度の知識しかありません。 www.linuxmaster.jp

*3:同一ホスト上で何個もコンテナを動かすわけですから、上限超えてコンテナ動かそうとしてマシンが飛んだりしようもんなら大変なわけです。リソース監視をして、どこまで使えるかは常にウォッチしていないといけない。

*4:この段階ではかなりざっくり解釈です。次の章でもう少しだけ掘ります

*5:namespaceやcgroupについて、日本語記事でわかりやすかったのはこちらです。コンテナ作ってます。 employment.en-japan.com

*6:コンテナ仮想化についてざっくり理解するときに一番しっくり来たのは、この本のP3の「Linuxコンテナ技術とDocker」の部分です。

コンテナはOSレベルのリソースの分離に過ぎず、ホストOSのカーネルを共有している。

この「大した話じゃない」感がきっかけになって理解がバチッとハマりました。

*7:曖昧でふわっとしたこと言いがちなのでガン詰めされることがすごい多いんですよね。怖い。人類みなしゅがーはぁと☆の精神でいてほしい。

*8:Kubernetes触れたことがない!?!?今すぐポチってください。

*9:あくまでコンテナランタイムの記事なのでetcdの詳細については省きますが、

*10:Kubernetesにおける「イベントチェーン」を調べるのがわかりやすいかと思います。Reconciliation Loopsの意味を理解するためにもイベントチェーンの理解は大事だと思いました(Kubernetes歴1週間程度の感想)

*11:基本的には高レベルランタイムはcontainerd、低レベルランタイムはrunCを前提にしています。誤解が起きそうな表現には注釈をつけます。

*12:ただし、gVisorはユーザー空間カーネルを使っていたりするのでちょっと話が変わったりする。gVisorはユーザー空間にLinuxシステムコールの実装を置いているので、コンテナとホストOSの間は強く隔離されていてセキュリティ志向。

*13:Open Container Initiativeというコンテナ技術の標準化団体があります。先のDockerとCoreOSの標準化戦争によってコンテナ界隈はギスギスしていたそうですが、LinuxFoundationがさすがにコンテナ技術を潰すわけにはいかないと、標準化団体の設立プロジェクトを発足したそうです。 cloud.watch.impress.co.jp

*14:結論として、KubernetesよりセキュアにしたければKata Containersは選択肢としてありだぜという話。一応AutoScaleもRoolingUpdateもいける。ただコンテナ起動のオーバーヘッドが発生したりなどするので、パフォーマンスとのトレードオフubuntu.com

なんでlambdaのハンドラってhelloがデフォルトなんだ

TL;DR

  • lambdaでgoの関数書くときハンドラをmainにするのを忘れるな
  • デフォルトでmainにしてくれ
  • デフォルトのハンドラがhelloになっている背景を調べたわけではない

goで書いたlambda関数のテストで早速怒られた

f:id:hagityann224:20200621102029p:plain
task/helloなんてねぇと怒られる

原因

エラーメッセージ

{
  "errorMessage": "fork/exec /var/task/hello: no such file or directory",
  "errorType": "PathError"
}

task/helloがない。 いや、helloなんて作った覚えもない

ハンドラhello(煽り)

お前か

f:id:hagityann224:20200621102709p:plain
おん・・・?

なおす

f:id:hagityann224:20200621102848p:plain
ハンドラをmainにした

なおった

f:id:hagityann224:20200621102951p:plain
とおった

さいしょからmainにしておいてくれ Node.jsはハンドラがindex.handlerなんすよね。。。なんで、、、

これからAWSを触ろうとする人こそ、IAM本を読もう

はじめに:最近話題のIAM本を読みました

読者モニターの抽選に当たりました。やったぜ。

なんと、本当にありがたいことに、著者ご本人様よりキャンペーンのご案内が!!懺悔したらリプが飛んできました。即応募。 そんなこんなで、僭越ながら「IAM本」書評記事、書いてまいります

もくじ

この本を読むモチベーションについて

※このセクション、ポエムです

ぼくらはリソースを借りているにすぎないし、危ない借り方なんかしちゃいかん

当たり前なのだが、クラウドの計算リソースをお金を払って借りている。 計算リソースといってもコンピュータなので、安いわけではない。

ただ、問題なのが、最近プログラミングはじめました→AWSいじってみたいです!みたいな人がいたときに、

  • サーバー代の相場がわからないのでリソースを借りる際のコストの想定が困難*1
  • アクセスキーなど、認証・認可・権限まわりに対して理解が浅い可能性あり
  • etc..

といった状態に陥ってしまう懸念がある。 もっともクリティカルな問題が上から2番目で、IAMのアクセスキーをGithubに誤ミットしてAWSが不正利用されて、100万円請求されたという問題が発生している。*2(これについてはIAM本内で言及あり) てなわけで、自分の意志とは関係なく勝手に借りられる危険性をはらんでいるという自覚が必要なのである。

時はクラウド時代、セキュリティの知識と実践方法を更新せねば

もうクラウドが当たり前な時代になったので「ミスってAWSに100万請求されそうになるなら自前で」というわけにもいかない。 クラウドリソースは当たり前のように使うし、クラウドネイティブなアーキテクチャも当たり前になっていく。 これからの”当たり前”を”安全に”使えるようなナレッジは、どうしても必要なのである。 そして、AWSのセキュリティは、IAMの運用・設計が適切がどうかにかかっているのである!

AWSを扱う上でセキュリティは最重要項目の一つであり、AWSのセキュリティの半分はIAMの適切な設計と運用に掛かっていると言っても過言ではありません。 出典:IAMのマニアックな話 佐々木拓郎著

IAM本の守備範囲

ここから「IAM本」のおはなし

1章から3章まででIAMの概要とIAMユーザの作成

まず、IAMの概要とチュートリアルがあり、ここまでは

books.rakuten.co.jp

こちらでもある程度言及されている。(AWSアカウント作成手順などはIAM本では言及されないので注意) そしてIAM本の親切なポイントが、 * 「ポリシーの作成 → グループの作成 → ユーザの作成」 という順でチュートリアルを組んでいる点である。これによって、「いきなりIAMを作って権限を直接付与するとあとあと管理がめんどくさくなる」という問題を回避できる。

4章からはIAM運用のプラクティスについて

当たり前だが、IAM(Identity and Access Management)に特化しているので、”安全”かつ"効率的に"というところまで言及している。 具体的には、

などである。 どれも実践的な内容なので「理屈はわかるが、手が動かない」という部分がスルッと解消される*3

やはり、この本のすごいところは実践的なプラクティスの多さだと思う。 デザインパターンごとのユースケースに言及しているところがとてもありがたい(ありがたい)。 第7章のIAMの運用も、「現実に即した運用」をテーマにしているので、知識を実戦投入しやすい。

なんと親切な付録A

ここでは、アカウント開設時に設定すべき項目のチェックリストが掲載されている。 IAMグループ、IAMユーザの作成を含め12項目挙げられている*4。 これからAWSを触る人向けの親切設計!!

IAM本で得られるもの

  • IAM管理の必要性
  • IAMユーザとグループの作成、ポリシーとロールの付与
  • デザインパターンに沿ったIAMユーザ・グループの設計方法
  • デザインパターンごとのユースケース
  • MFA使ってないユーザの権限を制限する方法
  • IAM管理に求められるセキュリティ要件の知識
  • CloudFormationによるポリシー付与のテンプレート化

IAM本で得られないもの

  • AWSアカウントの作成方法(まあいらんやろ)
  • IAMの認証系の内部構造

まとめ:これからAWSを触ろうとする人こそ、IAM本を読もう

本書は、AWS・IAMを効率的に安全な状態で保ち続けるためのノウハウの提供を目的としている*5。 IAMへの興味の導線は以下のような具合。

  • クラウドが当たり前*6
  • セキュリティについての考え方が少し変わる*7
  • AWSのセキュリティの半分はIAMにかかっている

AWSのサービスそのものは本当に追いつくことが不可能なスピードで変化しているけども、セキュリティの基本的な部分*8が急激に変わることはなさそう。 AWSの入り口として、AWSを安全に使えるようにするのは必須要件になりそうなので、これからAWS触ろうとする人はちゃんとIAM本読みましょう(IAMマニアの称号ももらえるし!!)

booth.pm

*1:AWSの料金体系は、マシン特性や稼働時間によって変わってくるが、プログラミングはじめたてでインフラの知識が少ない状況でこれを読んでもちんぷんかんぷんなのである。https://d1.awsstatic.com/whitepapers/ja_JP/aws_pricing_overview.pdf

*2:これについては、なにも個人利用だけで起こるものではない。普通に企業もやらかしている(ググればいろいろ出てくる)

*3:ただ、AWSはじめましての人向けの超親切ハンズオン形式というわけでもないので、楽天ブックス: Amazon Web Services パターン別構築・運用ガイド 改訂第2版 - NRIネットコム株式会社 - 9784797392579 : 本との併用がよいかも

*4:気になる人はBOOTHでIAM本買いましょう【ダウンロード版】AWSの薄い本 IAMのマニアックな話 - 佐々木拓郎のオンライン本屋 - BOOTH

*5:個人的には、IAM本の立ち位置は「IAMの重要性についての啓蒙」といった見方です。

*6:ちょっと古いが、2017年時点で、サーバー目的のクラウド利用が50%弱である。総務省|平成30年版 情報通信白書|企業におけるクラウドサービスの利用動向

*7:AWSにおいて、ユーザの責任範囲はOSレイヤーよりも上なので、世にいうセキュリティの範疇(たとえば暗号化やファイアウォール)をユーザ側がほとんど包括することにはなる。ただ、不正利用による運用コスト増といったリスク視点が加わったので、”少し変わる”という表現をした

*8:ここでいうセキュリティの基本的な部分は、ユーザ・グループの権限管理等を想定してる。

プログラミング初心者がハッカソン型インターンに行った話

 

結論:一通り遊んだらハッカソンに必ず行け

 

超初心者がハッカソンに出るとこうなる。

・言っていることがよくわからない

・質問内容もよくわからない

・急に使ったこと無い言語で開発する

・寝れない(これは初心者じゃなくてもこうなるらしい)

・とりあえずできたことはUIデザインくらい(サーバー関連意味不)

・とにかく周りのレベルが高すぎて泣ける

 

 

 

反省も込めて時系列で振り返り

 

インターン開始時の自分の状況

XcodeiOSサンプルアプリを作った程度。その他は正直ムリ。

・サーバーのことはよく知らん

ビットコインで頭がいっぱい

 

 

・どんな状況だったか

今回は音楽アプリの作成。事前にどんなアプリかは通知された。ただし、打ち合わせとかはさすがにNG。何作るのかしらんがとりあえずiOSアプリならいけますって書いたし、Xcodeは毎日触ってとりあえずの準備していった。(といってもIBActionとかDelegatesをちょっこと理解した程度)

 

 

そして当日、再生方法とかの都合もあって急遽Webアプリに変更。早速詰みそう。とりあえず、「ゴリゴリ調べる」→「コピペ」→「動かす」→「直す」→「ゴリゴリ調べる」のループ。ただ意外と、html+CSSはその場しのぎでなんとかいける(htmlのdivごとにCSS割り当てるのとかも、なんとなく読んでたらわかった)

 

画面構成の話の最中。「2カラム?3カラム?」「ここフロートで」「いきなりだけどJS書ける?」「JQUERYで〜・・・」html等々書いたことないので全部呪文。「フロントやってもらおうと思うんだけど大丈夫??」って心配されたのも納得。そしてhtml書いたこと無いけどフロント担当とか言ってごめんなさい。

 

といいつつもなんやかんやで完成。デザインは自分の案がかなり反映されたのでビギナー大喜び。

 

・振り返ってみて

始めたてのころは、自分の書いたコードが実際に動いたり、競プロの問題が解けたり、そういうことで喜びを得てたんだけど、ただ、本当に、趣味ではなく仕事としてプログラミングするというときには、協同でなにかを作る経験がないと何もできないかもしんない。

ハッカソンインターンは、自分と同じ世代の人と自分を相対評価する場なので、絶対に行ってください。お願いします。

 

 

 

 

 

 

 

 

 

 

python3 : venvで仮想環境 in Ubuntu

  1. APTでpythonをインストー
  2. venvで仮想環境を構築
  3. 仮想環境に入る

1.APTでpythonをインストー

$ sudo apt-get install -y python3 

これを実行

2.venvで仮想環境を構築

$ python3 -m venv (ディレクトリ名)

3.仮想環境に入る

$ . (ディレクトリ名)/bin/activate

ドットと引数の間に半角スペース