A

Amazon Prime Nowを使いました

[https://twitter.com/naari/status/1247377260232892416:embed]

さようならではない

Amazon Prime Nowとは

ここを見てください

Amazon Prime Now(プライム ナウ) - 生鮮食品、日用品を最短2時間でお届け

なぜ今

(ずっとだけど)コロナが怖くなって外に出ていないが、食料を用意するのはやはり必要なようで、その手段が必要だった

Uber eatsを週7回利用してみると高くつき、週1で外に出るのもなかなかに面倒なうえに人混みを避けることが出来なさそうだった

そのタイミングでAmazon Prime Nowの広告が飛んできたので使ってみる

  • 2年くらい前に一度だけ使ったが、最短で1時間以内の配送が可能だった覚えがあったのでそれも動機だった

Amazon freshにあるミールキットが購入できる

Amazon freshに対応している地域ではないが、一部はAmazon Prime Now経由で購入できた

生鮮食品のレパートリーはごく一部のみ

ライフ(スーパーマーケット)から注文できるっぽい

Amazon Prime Nowを通して↑ができるらしい

ライフは自前でネットスーパーをやっているはずだが、それでも共通のインターフェイスで触れるのは素晴らしい

ただしAmazonとは別で発送が行われるので考慮する必要がある

計画性が必要

Amazon Prime Nowは2000円とか2500円とかの下限を超えないと注文することが出来ない

また配達方法についても以下の二種類あるが、どちらも少し難点がある

  • 2時間便

    • これは2時間以内に来るものではなく、「明日以降で2時間ごとの枠のうちいつ届くか指定できる」というもの
    • 送料無料
  • 1時間便

    • 本当に一時間以内で届く(らしい)
    • 送料890円

本当にすぐ欲しい場合は1000円弱払えば良いが、それだったらUber eatsとあまり変わらない気がする

下限と送料の2つを考慮すると、結局のところ食料を調達するにはある程度の計画性をもって注文、行動しないといけないことがわかってしまった

食は難しい

GoのCLI用ライブラリcobraのzsh用の補完生成機能は弱い (?) のと、それをどうするかの話

フラグのカスタム補完がうまくできなかったので書きました

cobra の補完生成機能とは

みんな大好き cobra には bashzsh 向けの補完生成機能が存在する

詳しくは cobra/bash_completions.md at master · spf13/cobra · GitHub を見てほしいけど、こんな感じ ↓ で簡単に補完の機能を生成し、出力させることが出来る

# https://qiita.com/minamijoyo/items/9dceb1d8a66e48ab45cd
package cmd

import (
    "os"

    "github.com/spf13/cobra"
)

func init() {
    RootCmd.AddCommand(newCompletionCmd())
}

func newCompletionCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "completion",
        Short: "Generates shell completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            cmd.Help()
        },
    }

    cmd.AddCommand(
        newCompletionBashCmd(),
        newCompletionZshCmd(),
    )

    return cmd
}

func newCompletionBashCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "bash",
        Short: "Generates bash completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            RootCmd.GenBashCompletion(os.Stdout)
        },
    }

    return cmd
}

func newCompletionZshCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "zsh",
        Short: "Generates zsh completion scripts",
        Run: func(cmd *cobra.Command, args []string) {
            RootCmd.GenZshCompletion(os.Stdout)
        },
    }

    return cmd
}

cobra.CommandGen{Ba,Z}shCompletion という関数が生えているので、それを叩くだけ

例に示した通りに bash 用、zsh 用の関数があることがわかる

これを実装した後はこんな感じ ↓ で補完を読み込むようにして、再度 shell を開くなりして補完を利かすことが出来る

$ echo ". <(hogefuga completion bash)" >> ~/.bashrc
$ hogefuga completion zsh > /usr/local/share/zsh/site-functions/_hogefuga

すごくシンプルなものであればこれで結構事が足りて、用意したフラグ名についても正しく補完されるようになる

custom completion について

たとえば docker の補完の場合、 docker container rm <TAB> と入力すると現在起動しているコンテナ一覧が表示され、容易に好きなコンテナを指定することが出来る

これは、補完のスクリプト内で docker のコンテナ一覧を取得する処理が走っており、任意の変数に代入されることで実現している

これを cobra で実現する場合、次のようなコードを書くことになる

const (
        bash_completion_func = `__docker_get_container()
{
    local docker_output out
    if docker_output=$(docker container ls -q 2>/dev/null); then
        out=($(echo "${docker_output}" | awk '{print $1}'))
        COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
    fi
}

__docker_custom_func() {
    case ${last_command} in
        docker_container_rm)
            __docker_get_container
            return
            ;;
        *)
            ;;
    esac
}
`)

// 省略

cmds := &cobra.Command{
    Run: runSomething,
    BashCompletionFunction: bash_completion_func,
}

詳しく説明しないし、↑ のコードは動かしてないから動作しないかもしれないけど簡単に言うと補完するコマンドが見つからない際に __docker_custom_func が発火され、そこでなんというコマンドが実行されたか見て、ここで docker_container_rm だった場合は __docker_get_container が発火される

__docker_get_container はいろいろやってるけど結局は COMPREPLY にコンテナの id 一覧を代入している

ちなみにフラグにわたす場合はこんな感じ

func rootCmd() *cobra.Command {
    cmd := &cobra.Command{
        RunE: func(c *cobra.Command, args []string) error {
            // something
        },
        BashCompletionFunction: `__docker_get_container()
{
    local docker_output out
    if docker_output=$(docker container ls -q 2>/dev/null); then
        out=($(echo "${docker_output}" | awk '{print $1}'))
        COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
    fi
}
`,
    }

    # しょうりゃく

    cmd.Flags().StringVar(&opts.profile, "container", "fuga", "set container id")
    cmd.MarkFlagCustom("profile", "__docker_get_container")

    # しょうりゃく
}

cmd.MarkFlagCustom で指定のフラグに対する補完用のスクリプトを指定する

zsh では custom completion が効かない

他は大体いい感じにしてくれるのに、custom completion だけ使えない

公式にある zsh_completions.md にはこう書かれている

What's not yet Supported

  • Custom completion scripts are not supported yet (We should probably create zsh specific one, doesn't make sense to re-use the bash one as the functions will be different).
  • Whatever other feature you're looking for and doesn't exist :)

なので、上の例に登場していた BashCompletionFunctionzsh 版みたいなものは存在しない

つまり、素直に GenZshCompletion を叩いたところで custom completion は全く効かない

じゃあどうすればよいのか

でも我々は cobra 製である kubectl、minikube(CLI)あたりに zsh 用の素晴らしい補完が用意されていることを知っている

あれはどうやって custom completion を実現しているのだろうか?

結論はこんなかんじ ↓ だった

  1. bash 用の補完スクリプトを生成する
  2. bash 用の補完スクリプトから zsh 用の補完スクリプトに変換するスクリプトを生成する
  3. 1 と 2 を結合して完成

まじかよ…

実際のコードはこんな感じ

bash to zsh 用テンプレートの中に GenBashCompletion の結果を埋めていることがわかる

各自で開発している CLI にこのテンプレートを埋め込む場合は minikube の GenerateZshCompletion を参考にするとよさそう ( __minikube などの文字列を __hogefuga に置き換えるだけで動いた)

おわりに

あまり zsh と補完に詳しくないからわからないけど、なんで cobraZshCompletionFunction を実装しないの…?

full_joinというgemを作った

rubygems.org

github.com

名前が悪い

どういうgemか

こういう処理をしてくれる Array#full_join を提供する

Hoge = Struct.new(:id, keyword_init: true)

array1 = [Hoge.new(id: 1), Hoge.new(id: 2), Hoge.new(id: 3)]
array2 = [Hoge.new(id: 2), Hoge.new(id: 3), Hoge.new(id: 4)]

array1.full_join(array2)
#=> [
#  [#<struct Hoge id=1>, nil]
#  [#<struct Hoge id=2>, #<struct Hoge id=2>]
#  [#<struct Hoge id=3>, #<struct Hoge id=3>]
#  [nil, #<struct Hoge id=4>]
#]

こんな使い方もできる

Hoge = Struct.new(:id, :name, keyword_init: true)
Fuga = Struct.new(:id, :name, keyword_init: true)

array1 = [
  Hoge.new(id: 1, name: "AAA"),
  Hoge.new(id: 2, name: "BBB"),
  Hoge.new(id: 3, name: "CCC")
]
array2 = [
  Fuga.new(id: 101, name: "BBB"),
  Fuga.new(id: 102, name: "CCC"),
  Fuga.new(id: 103, name: "DDD")
]

array1.full_join(array2, &:name)
#=> [
#  [#<struct Hoge id=1, name="AAA">, nil],
#  [#<struct Hoge id=2, name="BBB">, #<struct Fuga id=101, name="BBB">],
#  [#<struct Hoge id=3, name="CCC">, #<struct Fuga id=102, name="CCC">],
#  [nil, #<struct Fuga id=103, name="DDD">]
#]

ようはFull Outer Joinみたいなまとめ方をしつつArray#zipするようなメソッドを追加してくれる

業務のコードを書くときにこれと同じ動きしてほしい*1、という場面に数回ぶつかったのでgemを作っちゃった

名前が悪い

最初は「こういうのってなんていう名前の配列操作なんだろう」と悩んで自分なりに検索したけど見つからなかった

どう形容すべきか分からなかったし、full_joinって名前もこの操作にピッタリあっているとは思えない

*1:とあるワードごとのモデルの対応表を作ることになっている

Railsの静的なエラー画面表示時にhtml以外でもpublic以下のファイルを読むようにするRack middleware

Railsで例外が発生し、かつApplicationControllerとかで拾われない場合、通常は設定された exceptions_app ( デフォルトだと ActionDispatch::PublicExceptions ) が呼ばれてエラー画面表示の処理が行われる

application/html がcontent-typeに指定されてると public/500.html とか public/404.html とかを返してくれるけど、htmlじゃない場合はある規定された形のhashに対して to_xxx したものをbodyとして返すだけになってしまい、こちらが定めた定形のメッセージを返そうとした際に少し不便になる

実際に public/500.html を返すMiddlewareはどうなっているかというと以下の通りになっている

rails/public_exceptions.rb at 4dcb46182a4aaa57f44f3eb722c1db54fa0ff843 · rails/rails · GitHub

今回はこれの各種 content-type 対応版を書いた

まだgemにはしてないが以下の通り (というか既に存在しそうだし)

class MimeTypePublicExceptions < ActionDispatch::PublicExceptions
  private

  def render(status, content_type, _body)
    ext = content_type.symbol || 'html' # symbolは拡張子を表すメソッドではない
    path = [
      "#{public_path}/#{status}.#{I18n.locale}.#{ext}",
      "#{public_path}/#{status}.#{ext}"
    ].find { |fp| File.exist?(fp) }
    if path
      render_format(status, content_type, File.read(path))
    else
      [404, { 'X-Cascade' => 'pass' }, []]
    end
  end
end

こんなかんじでつかう

Rails.application.configure do
  config.exceptions_app = lambda do |env|
    MimeTypePublicExceptions.new(Rails.public_path).call(env)
  end
end

おわり

pumaのthread数の伸縮がどのように行われているか

pumaが同時に建てるthread数はこちらで指定することが出来る

実際の定義を見るとこのように2つの値を入れることが出来る

threads 5, 16

第一引数が最小値、第二引数が最大値を表しているらしく、処理の際に自動でスレッド数をスケールしてくれるらしい

実際どのように動くか空気感を調べる必要があったためメモ

最初にまとめ

語弊がある気もするけどこんな感じ

  • 最初は最小値の数だけthreadを用意する (thread poolに格納される)
  • 既存のthreadが全てビジーな状態になった際、現在のthread poolのスレッド数が最大値に達していない場合は Thread.new して新しいthreadを確保
    • thread数が +1 される
  • 30秒おきにthread poolの状況を確認し、もし待ち状態のthreadが存在するのであればthreadを1つ削除するよう宣言
  • threadの処理に入る際、実際に待ち状態だった場合に自身を削除する

表題の関心事について調べた時に見たところ

以下のh4要素はそれぞれgithubへのリンクになっているので自分で実際のコードを見る場合はそちらも参考にしてください

Puma::Server#run

Puma::Server#run の中という如何にもなメソッドの中に如何にもな記述があり、ここで設定した最小値と最大値を渡している

@thread_pool = ThreadPool.new(@min_threads,
                              @max_threads,
                              IOBuffer) do |client, buffer|

#run メソッド内部で Puma::ThreadPool#auto_trim! というメソッドを呼んでいる

if @auto_trim_time # デフォルト値は 30秒
  @thread_pool.auto_trim!(@auto_trim_time)
end

その後 #handle_servers を呼び、実際にハンドリングされるっぽい (今回リクエストがどうハンドルされるかという点はちゃんと追ってない)

if background
  @thread = Thread.new { handle_servers }
  return @thread
else
  handle_servers
end

今回は先に #handle_servers の処理を追う

Puma::Server#handle_servers

無限ループになっていて、Puma::Client (実際にリクエスト/レスポンスを処理する実装があるっぽい) をthread poolに渡す箇所がある

pool = @thread_pool
# 無限ループの中
client = Client.new io, @binder.env(sock)
# 中略
pool << client

Puma::ThreadPool#<<

@todo (ただの配列) にclientを挿入する

ここで今回の関心事について重要っぽい条件分岐が存在していて、

  • 待ち状態のthreadの数(@waiting) が todoの要素数 より小さい かつ
  • 現在存在するthreadの数(@spawned) が 設定した最大値(@max) より小さい

という場合にのみ #spawn_thread を呼び出す

def <<(work) # work には前述した client が渡される
  @mutex.synchronize do
    # 略
    @todo << work

    if @waiting < @todo.size and @spawned < @max
      spawn_thread
    end
    # 略
  end
end

Puma::ThreadPool#spawn_thread

実際にthreadを新しく作って @workers に溜めるところ

threadの内容的には無限ループしていて、処理内容は

  • todoに要素が残っている場合
    • その処理
  • todoに要素が残っていない場合
    • @trim_requested(threadを削除したいという要求の数が格納された変数) が 1以上の場合
      • ループから抜ける
    • @trim_requested が 0の場合
      • todoに要素が入ったことを受け取るまで待つ ( @waiting に +1 する)
      • 待ち終わったら @waiting -1

となっていて、ループから抜けるとworkersから自身を削除する、という動きをする

(todoの各要素は前述したがclientとなっていて、実際にhttpのリクエストを処理するものとなっている)

ここまでがthreadsを実際に増やすまでの流れと、減らす機構の実装で、ここからは減らす起因となる部分を追う

Puma::ThreadPool#auto_trim!

記事の前半 (Puma::Server#run のあたり) で追従せずにすっとばした処理

AutoTrimなるものを動かす timeoutに渡される値は Puma::Server#run で渡されたものとなっており、デフォルトだと30秒

見たままのことをしている

def auto_trim!(timeout=30)
  @auto_trim = AutoTrim.new(self, timeout)
  @auto_trim.start!
end

Puma::ThreadPool::AutoTrim#start!

timeoutの分だけsleepを挟みつつ Puma::ThreadPool#trim を呼び出している

def start!
  @running = true

  @thread = Thread.new do
    while @running
      @pool.trim
      sleep @timeout
    end
  end
end

Puma::ThreadPool#trim

  • 待ち状態のスレッド(@waiting) が 1以上 かつ
  • 削除予定スレッド数(@trim_requested) を差し引いた現在のスレッド数 が 設定した最小値 より大きい

場合のみ @trim_requested を +1 する

def trim(force=false)
  @mutex.synchronize do
    if (force or @waiting > 0) and @spawned - @trim_requested > @min
      @trim_requested += 1
      @not_empty.signal
    end
  end
end

ここで +1 され、thread側のループが回ることでようやくthreadが削除される

わかること

ここまで追うと今回知りたかったことはだいたいわかる

以下2つの機構によってthreads数が伸縮する

  • リクエストを処理するところ
    • リクエストが来た場合はtodoに格納する
    • todoに入るリクエストは既存のthreadが処理していく
    • 既存のthreadがすべて処理中だった場合新規にthreadを作成
    • もし @trim_requested があった場合はthreadを削除する
  • threadを少なくするところ
    • 30秒おきにthreadsの状態を鑑みて @trim_requested を +1 する

こういうスレッドベースな処理を書いたこともコードを読んだこともなかったのでこの処理が良いのか悪いのか普通のことなのかわからないが、全体的に宣言的に動いているんだなというのがわかる (各処理ごとに現在のステータスを変更していき、各処理ごとにそのステータスに合わせて動くような実装だった)

しかもそういうものって非同期であることを意識して書かないといけないのでちょっと億劫になってしまうものだと思っていたが、そのあたりはruby標準ライブラリの良い感じな機構提供のおかげでわかりやすさが保たれているように感じた、Mutex と ConditionVariable すごい (Rubyもすごい)

今は別に非同期ななにかを書くつもりはないが、デザインのパターンとその実装例として大いに参考になるコードだった

カレントディレクトリにあるリポジトリと同じ名前の別リポジトリにcdするスクリプト

はじめに

自分でリポジトリをフォーク、クローンしてるけど、ローカルの別ディレクトリにもoriginのリポジトリをクローンしているってことよくあるだろうし、そのoriginのリポジトリまでcdしたいこともよくあると思う

$ echo $PWD
/Users/naari3/src/github.com/naari3/hyper-nice-repo
# ここにいるけど

$ cd /Users/naari3/src/github.com/alice/hyper-nice-repo
# ここに移動したい

ので移動するためのzsh functionを書いた

requirements

多分よくある組み合わせだと思います みなさんpecoもghqも使ってますよね?

ソース

function peco-cd-forked-repo () {
    local current_repo_name="$(basename "$PWD")"
    local selected_dir=$(ghq list --full-path --exact $current_repo_name |
        peco --prompt="cd-ghq-fork >" --query "$LBUFFER")
    if [ -n "$selected_dir" ]; then
        BUFFER="cd $selected_dir"
        zle accept-line
    fi
}

変哲なことはしていない

僕はこれを適当なkeybindで発火するように設定していていつでもリポジトリを反復横跳び出来るようにしている

おわりに

pecoはすごい

k3sを使う

はじめに

k3s.io

公式サイトがイカ

k3s とは

github.com

k8sの色々を削ぎ落としたものらしい

  • 使われていないコードなど

バイナリ一枚で40MB程度で軽い、とのこと (はたして40MBは軽いと言えるのか)

基本の使い方

Linux 3.10以上のマシンの上で以下を実行

sh curl -sfL https://get.k3s.io | sh -

これでk8sクラスタが完成する、とてもお手軽

agent を追加する

これも簡単で、k3sのバイナリを以下のように実行する

$ sudo k3s agent --server https://参加したいクラスタのIP:6443 --token ${NODE_TOKEN}
NAME STATUS ROLES AGE VERSION
ip-172-31-38-39 Ready 21m v1.13.3-k3s.6
ip-172-31-38-54 Ready 97m v1.13.3-k3s.6

やったね、楽勝

リモートで使いたい

同じネットワーク内でk3sを立ち上げて使う

これは特に問題ないはず

k3sを立ち上げると /etc/rancher/k3s/k3s.yaml にファイルが配置される

これは同じマシン上に存在するクラスタに接続するためのファイルで、別のマシンから接続するにはこのファイルの一部を同じネットワーク内のk3sが建っているマシンに向ける

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: hogefuga
-    server: https://localhost:6443
+    server: https://k3sが建っているマシンのプライベートIPを入れる:6443
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: hogefuga
    username: admin

k3sが建っているマシンのプライベートIPを入れる は 以降 (プライベートIP) と表記する

あとはこのymlを指定しながら kubectl を叩けばよさそう

kubectl --kubeconfig ~/.kube/k3s.yml get all

外部ネットワークからk3sのクラスタに接続する

頑張ってセキュア(?)なままアクセスしてみる

外部から愚直に(グローバルIPを直接指定して)接続する場合、上記手順だけでは接続できない

$ kubectl --kubeconfig ~/.kube/k3s.yml get all Unable to connect to the server: x509: certificate is valid for 127.0.0.1,(プライベートIP), not (グローバルIP)

どうやら証明書の対象が127.0.0.1とプライベートIPのみらしい 確認してみる

$ openssl s_client -connect localhost:6443 2>/dev/null | openssl x509 -text | grep -A 1 "Subject Alternative Name"
  X509v3 Subject Alternative Name:
    DNS:, IP Address:127.0.0.1, IP Address:(プライベートIP)

つまり、表示されたIP以外ではこの証明書は有効にならないのでクライアント側から通信を弾く状態となる

プライベートIPが動的にSANに割り当てられるところを見るとどうやらこの証明書はk3s内部で動的に生成しているようだ

どこで作成しているか追ってみる

k3s/server.go at f90cbed4081e7e1e6972861c196543b2d253bfcc · rancher/k3s · GitHub

func knownIPs() []string {
  ips := []string{
    "127.0.0.1",
  }
  ip, err := net.ChooseHostInterface()
  if err == nil {
    ips = append(ips, ip.String())
  }
  return ips
}

IPアドレスの文字列の配列を返す関数が見つかる

(このオブジェクトは serverConfig.TLSConfig.KnownIPs に渡され、追っていくと後に norman(rancher製のサーバーアプリケーションの実装?) に渡される)

net.ChooseHostInterface というのはk8sでも使われているutilに生えている関数で、ネットワークインターフェイスからIPを1つ取得して返す (Golangわからんけど合ってるよね?)

  • 先程まで雑に「プライベートIP」と読んでいたものの正体はこれっぽい

GoDoc k8s.io/apimachinery/pkg/util/net#ChooseHostInterface

apimachinery/interface.go at master · kubernetes/apimachinery · GitHub

この関数のIPのリストにグローバルIPを追加で書き込んでビルド、起動してみる (良い手ではないと思う)

func knownIPs() []string {
  ips := []string{
    "127.0.0.1",
+    "グローバルIP",
  }
  ip, err := net.ChooseHostInterface()
  if err == nil {
    ips = append(ips, ip.String())
  }
  return ips
}

すると、SANにもグローバルIPが追加される事が分かる

$ openssl s_client -connect localhost:6443 2>/dev/null | openssl x509 -text | grep -A 1 "Subject Alternative Name"
  X509v3 Subject Alternative Name:
    DNS:, IP Address:127.0.0.1, IP Address:(グローバルIP), IP Address:(プライベートIP)

これで kubectl を叩くと接続できるようになる

$ kubectl --kubeconfig ~/.kube/k3s.yml get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.43.0.1 443/TCP 49m

これ、手法としてだいぶダーティーだし、本当はもうちょっと別の手を取るべきなんだろうけど全然わからなかった

誰か正しい方法を教えてくれ〜

(もしこれが正しいのであれば何故k3sにはhostnameを受け取る起動引数が無いのだろうか…)

insecure-skip-tls-verify: true する

単にこちら側が弾いているだけなので、tls verifyをスキップすることでクラスターにアクセスできるようになる

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: hogefuga
    server: https://グローバルIP:6443
+    insecure-skip-tls-verify: true
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: hogefuga
    username: admin

または

$ kubectl --kubeconfig ~/.kube/k3s.yml get all --insecure-skip-tls-verify=true

結局どうするのが正解なの??

k3sのユースケース

k3sリポジトリの記念すべき1つめのissueもユースケースを尋ねるものだった

github.com

製作者のDarren Shepherd曰く、これは軽量のk8sを作るためのプロダクトだとのこと

https://github.com/rancher/k3s/issues/1#issuecomment-424943047

軽く使えることが大きな理由となるような事に利用するものなのだろうと想像している

  • 実際公式サイトの方でもEdgeとかIoTとかCIとか書かれている

個人的にとても良いと思っているのはk8sの練習台として使うユースケースがある

k8sの練習を行う場合、勿論ながら何かしらの形でクラスタを用意する必要がある

おおよそminikubeかDocker for Desktopに付属のk8sを使うことになると思うが、ここに第三の選択肢としてk3sを上げることが出来るようになった

これは上で説明したように簡単に立ち上げることが出来るし、他より動作が軽いために気軽さはとても大きいと思う

しかもノードの追加も容易なため、複数ノード構成の練習も簡単に出来る

  • 多分先程挙げたminikube, Docker for Desktopのk8sで複数ノード構成を取るより容易
    • そもそもこの2つで複数ノード構成出来るのか?

↓こんな docker-compose.yml もあるので、 github.com

$ docker-compose up -d
$ kubectl get node --kubeconfig kubeconfig.yaml

とすればすぐにk8sの環境が立ち上がるの、普通に便利では?