tech.guitarrapc.cóm

Technical updates

GitHub Actions の setup-dotnet と ホストランナーの .NET SDK

GitHub Actions で .NET Core ビルドを行いたいときに利用するものといえば、setup-dotnet があります。

github.com

今回は Ubuntu-latest な環境で setup-dotnet がいる場合と、そうでない場合について考えてみます。

tl;dr;

  • GitHub Actions ホストランナーのデフォルト環境で .NET はビルドでき、最新が1-2週で入ってくるのでOSS はsetup-dotnet なしで十分。
  • プロダクトなど .NET SDK バージョンを厳密に制御する場合は、setup-dotnet を使うとよい。
  • setup-dotnet は global.json を読んでくれるので、バージョン固定するなら global.json 便利

GitHub Actions と ホストランナー

GitHub Actions のホストランナーは環境ごとに特定のツールがインストールされた状態になっています。 例えば、ubuntu-latest である Ubuntu 20.04 LTS 環境 なら、以下のツールが入っています。

github.com

2021年5月13日時点で .NET SDK は次のバージョンが入っており、かなり網羅されていることがわかります。 2.1 / 3.1 / 5.0 に関してはおおむね困らないでしょう。

2.1.300 2.1.301 2.1.302 2.1.401 2.1.402 2.1.403 2.1.500 2.1.502 2.1.503 2.1.504 2.1.505 2.1.506 2.1.507 2.1.508 2.1.509 2.1.510 2.1.511 2.1.512 2.1.513 2.1.514 2.1.515 2.1.516 2.1.517 2.1.518 2.1.519 2.1.520 2.1.521 2.1.522 2.1.523 2.1.602 2.1.603 2.1.604 2.1.605 2.1.606 2.1.607 2.1.608 2.1.609 2.1.610 2.1.611 2.1.612 2.1.613 2.1.614 2.1.615 2.1.616 2.1.617 2.1.700 2.1.701 2.1.801 2.1.802 2.1.803 2.1.804 2.1.805 2.1.806 2.1.807 2.1.808 2.1.809 2.1.810 2.1.811 2.1.812 2.1.813 2.1.814 2.1.815 3.1.100 3.1.101 3.1.102 3.1.103 3.1.104 3.1.105 3.1.106 3.1.107 3.1.108 3.1.109 3.1.110 3.1.111 3.1.112 3.1.113 3.1.114 3.1.200 3.1.201 3.1.202 3.1.300 3.1.301 3.1.302 3.1.401 3.1.402 3.1.403 3.1.404 3.1.405 3.1.406 3.1.407 3.1.408 5.0.100 5.0.101 5.0.102 5.0.103 5.0.104 5.0.200 5.0.201 5.0.202

このため、わざわざ setup-dotnet を使わずともdotnet cli は参照できますし、ビルドも可能です。 つまり、setup-dotnet を使わない場合、GitHub Actions のホストランナーに事前インストールされた複数の .NET SDK から必要なバージョンが参照されます。

setup-dotnet

setup-dotnet がやっていることは、「特定のバージョンの .NET SDK を azure feed からダウンロード、展開して参照できるようにする」ということです。

GitHub ホストランナーは Prerelease の .NET SDK には未対応で、ホストランナーの環境更新も 1週間に一度程度なので待ってられないというケースが稀にあります。 そんなときsetup-dotnet は Prerelease にも対応しており、まだfeed に乗っていないリリース直後の .NET SDK バージョンを利用するときに便利です。

f:id:guitarrapc_tech:20210520024025p:plain
GitHub Actions ホストランナーUbuntu 20.04 LTS の環境更新頻度

また、setup-dotnet はバージョン省略時にglobal.json があればそのバージョンを読む機能もあります。global.json と CI のバージョン固定なら global.json でいいというのはあるので、これはこれで便利です。(global.json の配置場所がリポジトリルートではない場合は少しアレってなりますが)

    if (!version) {
      // Try to fall back to global.json
      core.debug('No version found, trying to find version from global.json');
      const globalJsonPath = path.join(process.cwd(), 'global.json');
      if (fs.existsSync(globalJsonPath)) {
        const globalJson = JSON.parse(
          // .trim() is necessary to strip BOM https://github.com/nodejs/node/issues/20649
          fs.readFileSync(globalJsonPath, {encoding: 'utf8'}).trim()
        );
        if (globalJson.sdk && globalJson.sdk.version) {
          version = globalJson.sdk.version;
        }
      }

setup-dotnet/setup-dotnet.ts at bf3c3eb1fdba530a22805f082a2dcebc125d6ce4 · actions/setup-dotnet · GitHub

setup-dotnet の副作用

さて、この記事をここまで読んでいる人は「setup-dotnet を使うとインストールした以外の SDK が使えなくなる」という状況になったことがあるのではないでしょうか? ホストランナーの環境には、複数の .NET SDK が入っているにも関わらず、です。

これは setup-dotnet が、SDKのインストール時に .NET SDK の参照ルートパスを ホストランナーのデフォルトパス /usr/bin/dotnetから setup-dotnet で入れたSDK のパス $HOME/.dotnetに切り替えているために起こります。 結果、 setup-dotnet でインストールした以外のSDK バージョンは参照できなくなります。 .NET SDK のルートパスは、環境変数 DOTNET_ROOT に任意のパスを設定することでサクッと切り替えることができます。というのが気付きでした。便利。

setup-dotnet は installer.ts でこれを設定しています。

        // 184 行目
        core.exportVariable(
          'DOTNET_ROOT',
          path.join(process.env['HOME'] + '', '.dotnet')
        );

setup-dotnet/installer.ts at bf3c3eb1fdba530a22805f082a2dcebc125d6ce4 · actions/setup-dotnet · GitHub

実際に GitHub Actions で .NET SDK の参照パスと利用可能バージョン一覧を見てみましょう。 setup-dotnet を利用する前は、標準の/usr/bin/dotnetが利用されています。

f:id:guitarrapc_tech:20210520025106p:plain
GitHub Actions ホストランナーデフォルトの .NET SDK 状態

setup-dotnet を利用すると、パスが /home/runner/.dotnet/dotnet になり、利用可能な SDK も限定されていることがわかります。

f:id:guitarrapc_tech:20210520025301p:plain
setup-dotnet利用後の.NET SDK の参照パスと利用可能バージョン一覧

まとめ

一見すると、setup-dotnetを使うとめんどくささが増えてメリットがスポイルされているように見えます。 しかし、プロダクトなど厳密に .NET SDK のバージョンを管理したい場合は、setup-dotnet を使うのは合理的でしょう。 一方で、基本的にホストランナーには最新バージョンの.NET SDKが入っているので、OSS では 脆弱性情報などの対応を除き ホストランナーのデフォルトで十分というのは言えそうです。

  • setup-dotnet を使うと、.NET SDK preview や任意の.NET SDK をインストールができる。
  • setup-dotnet を使うと、デフォルトの .NET SDK のパスから、setup-dotnet でインストールした .NET SDK のパスに変更されるため、ホストランナーのデフォルト .NET SDK 達はパス省略で使えなくなる。

今日の学びでした。

Terraform の構成

ベストプラクティスといいつつ、どういう風にやりたいかで変わるというのは往々にしてあります。 ベストプラクティスは求めても意味ないのでどうでもいいとして、いろんなパターンのメリット/デメリットを把握して現状に即しているのかどうかは考え続ける必要があります。

ということで、長年頭を悩ませて納得感があまりない代表が Terraform です。 今回は以下の記事を読んでて、普段やっている Terraform の構成について書いてなかったので記録として残しておきます。

blog.serverworks.co.jp

TL;DR;

  • ディレクトリ分離 + モジュールでやっているけど、基本的にローカルモジュールに寄せている。
  • locals.tf で差分を完全に抑えて、モジュール内は分岐なしの型で環境の違いを表現している。

実は Workspace で分離してもさほど問題ないといえば問題ないことに読み直して、ふと考えが変わってきました。 ただし、Workspace の state後戻りができないのが厳しく、この環境だけしれっと追加みたいなカジュアルさは欠けるというのはあります。 そういう意味で、やはり Workspace は手放しで使う気にはなれないものがあり、微妙だなぁと感じます。

HashiCorp社の Terraform ベストプラクティス

ベストプラクティス自体よりも、開発元がどのようなコンセプトを持っているかは注目に値します。

Terraform開発元のHashiCorp 社(以降、公式)はたびたびBest Practice 的なことを謳いますが、具体的な構成を示していた昔のリポジトリは archive されて久しいものがあります。だめじゃん。

github.com

Nebulaworks の例

公式はさまざまなリポジトリ構成があるといいつつ、具体例をほぼ示していない。と感じつつも、Nebulaworks の例を2020/Jan/18 にブログで紹介しプレゼン動画で構成を見ることができます。(0.11.11 の頃なので0.14の今ではproviderやversions などは古く感じます)

www.hashicorp.com

youtu.be

youtu.be

この中で以下のモジュール構成例を示しています。(いわゆるよくあるモジュール構成) プレゼンの内容はいうほど特筆することはないです。

.
├── env
│   ├── dev
│   │   ├── main.tf
│   │   ├── module.tf
│   │   └── reseources.tf
│   ├── prod
│   │   ├── main.tf
│   │   ├── module.tf
│   │   └── reseources.tf
│   └── staging
│       ├── main.tf
│       ├── module.tf
│       └── reseources.tf
└── modules
    └── resources.tf

f:id:guitarrapc_tech:20210516222216p:plain
Nebulaworks Module Directory Structure

公式では構成は示されないので、仕方ないのでコミュニティに学びにいく旅に出ることになります。(2021年になっても変わってない)

なぜ Terraform の構成は難しいのか

改めて、Terraformの構成が難しいのはなぜか考えてみると、terraform はパスベース + .tf 拡張子で参照ファイルや変数のスコープ1が決まります。 スコープが驚くほど広いわりに、言語機能としてそのスコープ内でファイル限定といった表現力は持たないため、ファイル構造でカバーすることになります。 これが難しい原因の多くを生み出しているように感じます。 具体的にあげると、

  1. 言語としてはシンプル、だけどスコープも広いために収拾がつきにくい。
  2. インフラ構成はただでさえブラストラディウスが広いにも関わらず、言語による「影響を限定させるサポート」に乏しい。2
  3. 同じコードを何度も書くのは事故の原因なので、なるべくDRYにいきたいが運用しやすさとのトレードオフはしないようにするサポートが乏しい。3

言語機能が貧弱というほうが適切かもしれないですね。

ということで、今現在よく使っている構成と、きっかけとなったブログのセクションについて考えます。

よく使っている構成

私は実行環境をTerraform Cloud に一本化しています。(以前はatlantis を用いていました)

今現在よく使うディレクトリ構成は次のものです。 EKS は特に悩みやすいのでサンプルを置いておきます。

├── common (VPC分離の場合のみ)
├── dev
│   ├── compute.tf
│   ├── data.tf
│   ├── iam.tf
│   ├── locals.tf
│   ├── modules
│   │   ├── compute
│   │   │   ├── ecr
│   │   │   │   ├── main.tf
│   │   │   │   ├── outputs.tf
│   │   │   │   └── variables.tf
│   │   │   ├── eks
│   │   │   │   ├── iam
│   │   │   │   │   ├── eks_cluster_role
│   │   │   │   │   │   ├── main.tf
│   │   │   │   │   │   ├── outputs.tf
│   │   │   │   │   │   └── variables.tf
│   │   │   │   │   ├── eks_node_role
│   │   │   │   │   │   ├── main.tf
│   │   │   │   │   │   ├── outputs.tf
│   │   │   │   │   │   └── variables.tf
│   │   │   │   │   └── eks_pod_role
│   │   │   │   │       ├── data.tf
│   │   │   │   │       ├── main.tf
│   │   │   │   │       ├── outputs.tf
│   │   │   │   │       └── variables.tf
│   │   │   │   ├── main.tf
│   │   │   │   ├── oidc
│   │   │   │   │   ├── bin
│   │   │   │   │   │   └── oidc_thumbprint.sh
│   │   │   │   │   ├── main.tf
│   │   │   │   │   ├── outputs.tf
│   │   │   │   │   └── variables.tf
│   │   │   │   ├── outputs.tf
│   │   │   │   └── variables.tf
│   │   │   └── kubernetes
│   │   │       ├── main.tf
│   │   │       └── variables.tf
│   │   └── iam
│   │       ├── account
│   │       │   ├── main.tf
│   │       │   ├── outputs.tf
│   │       │   └── variables.tf
│   │       ├── role
│   │       │   ├── main.tf
│   │       │   ├── outputs.tf
│   │       │   └── variables.tf
│   │       └── user
│   │           ├── main.tf
│   │           ├── outputs.tf
│   │           └── variables.tf
│   ├── outputs.tf
│   ├── providers.tf
│   ├── variables.tf
│   └── versions.tf
├── production
├── staging
└── modules
    ├── iam
    │   └── iam_role
    │       ├── main.tf
    │       ├── outputs.tf
    │       └── variable.tf
    └── xxxxx

原則

この構成は、運用においてモジュール構成に分岐や入れ替えなどを起こさないことを目的に組んでいます。 そのため、次の原則に基づいています。

  • Workspace を使った dev/staging/production の環境切り替えは用いない。
  • 環境ごとの差分は local変数で表現し、モジュールやdataを含むコードは、構成が全く同じなら同じとする。(./locals.tf)
  • ルートの共通Module には普遍的で必ず環境差分が起こらない共通化するモジュールのみ配置する。(./modules)
  • モジュールには、アプリの事情を込みで記述し、ルートのlocals.tf に記述した環境ごとのパラメーターを variables.tf で受け取る。(./dev/modules など)
  • 環境の違いがある場合、その環境のローカルモジュールのみ変更を加える。(基本的に locals.tf のパラメーターのみで対応できるようにモジュールに改修を入れる)

この原則だけわかっていると、ただのシンプルなModule構成です。 誰が見ても初見ですぐにわかり、影響を分離できるようにするのがゴールです。

要はモジュール構成ではモジュールを共通化するのが前提ですが、初めからローカルモジュールに展開するように割り切った構成です。

terraform ファイルの記載内容

モジュール内部の構成は見ればわかると思うので、ざっくりとdevフォルダ配下のファイル説明だけ見てみます。

  • data.tf: data リソースの定義を行っています。
  • locals.tf: local変数定義を行います。環境ごとの定義はここですべて表現されます。環境差分はここだけ発生します。
  • modules: ロジックはすべてモジュールに閉じ込めます。compute.tf などにresourceを直接書くことはなしです。
  • outputs.tf: よくある出力定義です。
  • providers.tf: プロバイダー一覧。
    • 0.13 から provider がいい感じになったので、main.tf をやめて providers.tf と versions.tf にきっちり分離するようになりました。
  • variables.tf 実行時の variables.tf 差し込みはterraform cloud でのクラウドへの認証差し込みなど必須な情報以外入れない。
  • versions.tf: terraform ブロックとプロバイダーバージョン、backend 指定。

残りのcompute.tfやiam.tfは、ただのモジュール参照です。 main.tf に羅列していると使いにくいので、いつでもリソースをパージできるように分けています。 tfstate の分離はもちろんしますが、するしないはプロジェクト規模でも変わって来たりするので、ここでは簡単のため同一stateとしました。

NOTE: terraform を使って upgrade とかしていると、providerとversionsはこういう形になる気がします。(0.12で自動生成されたversions.tf にはじめ戸惑いましたが、0.13で納得いく変更がきた)

www.terraform.io

この構成で最も設計しないといけないこと

ローカルモジュールの設計が大事になります。

  • 異なる環境でも同じlocal変数の型でモジュールが受けるようにできるようにすること
  • ローカルモジュールの凝集
  • module のoutput

環境差分は locals.tf にのみ許容することはやってみると全然できます。 しかし、いざやると dev だけ 2つリソース作って、stagingやproduction では 1つだけ、などといった差に出会ったりするでしょう。 そういったものは型でうまく解決するようにします。

例えば、1つ、2つといった数の差異がモジュールで閉じ込められるように mapを使ったり map は型表現が弱いので set(object) を使ったりといった工夫は必要です。 共通モジュールでも、分岐ではなく、こういう型での解決をするほうが望ましいことは多いので別に違いはないといわれるとそうですね。

元記事の迷子

私もいつも迷子なので今時点の考えを書いておきます。

workspace 使うのか使わないのか問題

使いません。

とはいっても、元記事の「今どの workspace にいるのか」という問題が発生しません。 これは、Terraform Cloud の remote apply を必須にしているため、Workspaceを使わずとも環境ごとに Terraform Cloud の Workspaceは分離されているからです。

使わない理由は別にあります。 workspace 機能を用いた場合、環境ごとに差分が生じた場合に、差分を吸収するために分岐を用いたりするのがいやだったからです。 workspace の効果で得られる共通リソースで完結するメリットは、「分岐を見逃した場合など発生することが当然あるであろう状況で環境を破壊するリスク」に比べて割が合わないと思っているからです。

私は人間はミスをすると思っているので、ミスが起こっても影響が抑えられる構成を好みます。

環境の分割方法問題

環境の分割は、見ての通り「環境ごとにディレクトリを作る」です。 シンプルな反面めんどくさくなるのが「漏れがあったら」という問題ですが、それは locals.tf への環境差分の限定とモジュール設計で対応します。

module 設計問題

  • prod/stg それぞれで構築する: ローカルモジュール
    • 将来にわたって完全に同一で差分がない場合のみ、共通モジュールで定義します。
  • 環境別に構築したりはしないが両方で利用する: ローカルモジュール
    • 将来にわたって環境ごとの破壊的な変更が起こらず、完全に共通利用でき環境差分がない場合は、共通モジュールへ配置。
  • 片方だけ構築する: ローカルモジュール

今行っている構成では、まったく同じ構成なら、diff を行っても locals.tf 以外に差分が出ません。 そのため、devで検証が終わりstagingやprodに適用する場合も、モジュールは丸っとディレクトリごとコピーで構わないので変更もれリスクが抑えられます。(localst.tf だけ残して残り丸っとコピーでいい)

共通Modules + 分岐が必要なものを、ローカルモジュールに振っているので、そういう悩みが起こらない設計にしています。 同じものを複数書くのではなくコピーにしているのは、まったくスマートではないですが、悩むことより事故は起こらないものです。

自作 module か、公式 module か問題

私は自作module が多いようです。

公式module を一時期に優先して使っていたのですが、awsモジュールなどで結構破壊的変更があったので苦しくなってやめました。 とはいえ、vpcなどは公式モジュールでも十分に柔軟で、破壊的変更も起こされた記憶がないので使ってもいいです。(この見極めが難しいのが公式モジュールの問題だと思います)

variable の配置方法問題

これはベストプラクティスに乗らない理由がないので遵守します。

  • ファイル分割: 分割する。
  • 格納データの型: 厳密に指定する。map(string) よりは object(型) で定義して、差し込むときは map で指定するのがいいです。
  • デフォルトの値有無: 基本的にデフォルト値はなし。
    • デフォルト値があってもいいのは、入らなくても動作にまずい影響が起こらない場合のみです。
    • デフォルト値をないようにすると、値を意味を持って指定することになるため意図のない設計が発生しないためです。

NOTE: 事故があってからデフォルト値なしに考えを変えました。デフォルト値、意図がない限り基本的に避けたほうがいいです。

過去に参考にした構成

コミュニティから学ぶことは多くあります。 今の構成はこれらの構成を自分でやってみて、納得がいかない部分を変えています。

terraform-aws-providerコミッターが公開している 以下の構成はよく見かけるものです。

github.com

small

.
├── README.md
├── main.tf
├── outputs.tf
├── terraform.tfvars
└── variables.tf

medium (large は未完成で medium と同じ)

.
├── README.md
├── modules
│   └── network
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── prod
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── stage
    ├── main.tf
    ├── outputs.tf
    ├── terraform.tfvars
    └── variables.tf

他にもありますが、コンセプト似てますね。

kenzo0107.github.io

dev.classmethod.jp

future-architect.github.io

他にもレイアード構成とかもやってた時期がありました。

書籍としては、「実践Terraform AWSにおけるシステム設計とベストプラクティス 」が良書です。module構成から、ローカルモジュールにもう少し振ってもいいと思ったきっかけは、この本でした。

www.amazon.co.jp

他に、「Google Cloud Platformで学ぶTerraform 〜実践編〜」などもテストが手集めに書かれています。(一方で構成はあまり触れていません)

techbookfest.org

おわり

terraformを使い始めて、もう使って5年たつような気がしますが、ディレクトリ構成には今も納得がいかないです。とはいえ、過去に試した構成よりも納得度と、変更の楽さ、影響度の限定ができているので今はこれで。

CDK や Pulumi のような、プログラミングの一般的なルールで構成できるものはこういうところが圧倒的に楽です。 とはいえ、Pulumi や CDK を書いてから terraform に戻ると、型を意識せずに依存関係が解決されて楽極まりないのもあり、なんとも難しいものだと感じます。

どっちもいいし、どっちもまだまだ改善の余地があるので、引き続き迷子を楽しんでいきたいものです。

ベストプラクティスがないのはいいですが、公式でそろそろモジュールの典例はしめしてもいいのではと思いますが、HashiCorp社なのでしないでしょう。残念ですが。

蛇足

リソース名のsuffix

いろんな経験を積んだ結果、アカウント分離だろうと、リソース分離だろうと、リソース名のsuffix には _dev などの環境名を含んで設計するといいと思っています。 あと、CDK や Pulumi のようなランダムリソースsuffix も好ましいですが、文字長制限で怒られたりするので 2021年にもなって各クラウドはいい加減にしてくれと引っかかる度に思っています。

文字種 4もそうだけど、文字長制限5が許されるのは201x年で終わってほしかったけど、現実はそんなに甘くない。

Terraform公式で示しているベストプラクティス

公式は、主に次のようにTerraform をどのように運用に乗せていくかのベストプラクティスを示しています。 とはいえ、内容はどのように現状を把握して使える状態にもっていくかがメインです。

www.terraform.io

具体的なTerraform 構成は触れていないものの、Terraform Cloudの文脈でプラクティスをいくつか示しています。 3.3.4 を要約します。(要約は気になる方はぜひ元文章もどうぞ)

  • VCS リポジトリ/ブランチと Terraform Cloud のWorkspace をマッピングさせて
  • AppやServiceの各環境は、同じリポジトリ/ブランチのTerraformコードで管理し
  • 異なる環境は variables で表現しつつ Workspace 事に適切に設定する

NOTE: 加えて、環境ごとにブランチを分けるのではなく、1つの正規ブランチをもとに全環境に適用することにも触れています。

f:id:guitarrapc_tech:20210516213408p:plain
Part 3.3.4 Create Workspaces

また、この中で自作モジュールの作成基準についてダイアグラムを示しています。(同時にナレッジシェアしようといったことも言っています)

f:id:guitarrapc_tech:20210516214532p:plain
Part 3.2.3 Create Your First Module

また、Terraform のチュートリアルにおいて、モジュールをプログラミング言語によくあるライブラリ、パッケージなどと似たものと触れつつ、ベストプラクティスを示しています。

learn.hashicorp.com

We recommend that every Terraform practitioner use modules by following these best practices:

  1. Start writing your configuration with modules in mind. Even for modestly complex Terraform configurations managed by a single person, you'll find the benefits of using modules outweigh the time it takes to use them properly.
  2. Use local modules to organize and encapsulate your code. Even if you aren't using or publishing remote modules, organizing your configuration in terms of modules from the beginning will significantly reduce the burden of maintaining and updating your configuration as your infrastructure grows in complexity.
  3. Use the public Terraform Registry to find useful modules. This way you can more quickly and confidently implement your configuration by relying on the work of others to implement common infrastructure scenarios.
  4. Publish and share modules with your team. Most infrastructure is managed by a team of people, and modules are important way that teams can work together to create and maintain infrastructure. As mentioned earlier, you can publish modules either publicly or privately. We will see how to do this in a future tutorial in this series.

しかし私は構成の例が欲しいのであった。

モジュールのベストプラクティス

モジュールを書く時の注意がいくつかあります。 とはいえ、別に大したことはいってないので普通にやればいいのではないでしょう。

  • Dependency Inversion
  • Multi-cloud Abstractions
  • Data-only Modules

www.terraform.io


  1. せめて local 変数がファイル内部でスコープがとどまるならまだしも

  2. 影響を限定させる手段としてモジュールやstate分割を使っていくことになる

  3. Workspace やモジュール参照で同一ソースを使うなどを使っていくことなる。

  4. - とか _ とか使える文字種がずれたりするのはとても悪い文化

  5. 64文字制限もあれだけど、32文字制限とか許せない

WSL2 環境でのローカル Kubernetes クラスタ構築を検討した話

私はローカル環境の Kubernetes にDocker Desktop for Windows を用いています。

Minikube や kubeadm、k3s、kind、microk8s など各種クラスタ構成がある中で、WSL2にローカルのクラスタ環境を他で組んだ場合の違いを改めてみておくことにしました。

今回は ローカル Kubernetes クラスタの構成にMinikubeを用いて構築してみて、最終的にどれがいいかの所感です。

目次

TL;DR

Windows/WSL で共通のコードベースを Windows にファイルを置きつつ、Windows と WSL でファイルマウントして、docker-compose も使って、Kubernetesも使いたい、という環境だとDocker Desktop (Hyper-V) + WSL1 が現時点ではおすすめ。 Docker Desktop での Kubernetes Cluster は、展開も極めて容易で local docker registry も共用され、Windows/WSLの両方からアクセスが容易なのでとてもいいです。Minikubeやkindと比べても何気に構成も奇をてらっていないのもいいです。今後のdockershimsに対する同行が気になります。

  • Docker Desktop (Hyper-V) + WSL1 がストレスなく快適。WSL1 Ubuntu での docker volume マウントだけ注意。
  • Docker Desktop (WSL2) + WSL2 は、Kubernetes は快適だが docker-compose でファイルマウント、dockerアプリのWebレスポンスが絶望なので無理筋。
  • Minikube を docker-driver でWSL2 に建てる場合、Windows からのアクセスは minikube tunnel で Load Balancerをトンネルする以外は手がない、悲しい。
  • Docker Desktop (Hyper-V) + WSL2 は ホスト docker イメージの共有ができず、大変に厳しいものがある。

半年余り WSL2 で組んでいましたが、Minikube の結果も受けたうえで、Hyper-V + WSL1 に戻ることにしました。 2020年は WSL2 で喜び、WSL2 に泣いた。 2021年がどうなるのか... WSL2 の 9P が劇的改善、vmmem/COM Surrogate の CPUあげあげも解消したら最高ですね、WSL2に戻ります。

なお、Linux 上でファイル操作が完結するなら WSL2 が圧倒的におすすめです。WSL と違って妙な制約一切ないので WSL2 最高。Windows と Linux でファイル操作をまたぐ頻度、ファイル量がほぼないなら WSL2 でいいでしょう。

はじめに

この記事の視点は利用用途に強く依存しています。 私と同じ使い方をしたい方でないとまったく違う検討結果になると予想されます。

環境と利用方法が合致するときにだけ参考になるかもしれませんし、あるいは参考にもならないかもしれません。

環境

  • Windows 10 Pro 20H2
  • Docker for Windows 3.0.0
  • Ubuntu 20.04 on WSL2

期待する利用方法

  • Windows から WSL にたてたKubernetes の Ingress/LoadBalancer へのアクセスを行う。
  • Kubernetes より簡便に動作確認を行う環境として docker-compose も同様の構成で動かす

NOTE: WSL内部からの Kubernetes アクセスのしやすさは評価対象ではない。

Kubernetes のローカルクラスター構成

ローカルのKubernetes Cluster構成には選択肢がいくつかあります。 この記事ではMinikube on WSL2 で見ていきますがちらっと見ておきましょう。

NOTE: おおむね今のローカルKubernetesクラスタの選択肢はなんとなくここをみていただくとして。

  • kind
  • microk8s
  • k3s
  • k3d

それぞれ WSL2 と組み合わせて動作させる方法があるので興味ある人はみるといいでしょう。

NOTE: kind は LoadBalancer に対応していないので、こういう用途では使いにくいのを付記しておきます。インストールは本当に簡単なのですが。

kind.sigs.k8s.io

wsl.dev

k3s on WSL2 · GitHub

wsl.dev

WSL2 ではなく、ホストOS に Kubernetes クラスターを組むのも十分に有用な方法です。 Windows/macOS で最も簡便、かつ構成の組みなおしも用意なのは Docker Desktop の Kubernetes クラスターでしょう。

docs.docker.com

VMに建てるという意味では、Minikube を Hyper-V/Virtual Box で入れるという手もあります。(よくやられているやつ) Minikube に限らず、WSL2 で建てるのとVMで建てるのはおおむね概念は同じなので、どれもそういった方法を提供しています。

kubernetes.io

Minikube on WSL2 のインストール

WSL2 で起動した Ubuntu 20.04 に Minikube を入れて、Minikubeの自動起動をsystemd にさせることろまで構築します。 おおむね Kubernetesブログ に沿ってみていきましょう。

kubernetes.io

記事では Docker Desktop の WSL2 Integration を有効にしていますが、docker のファイルマウントの遅さを排除するため Hyper-V のまま動作させます。

f:id:guitarrapc_tech:20210101032549p:plain

Hyper-Vバックエンドの場合、WSL2 Ubuntu には docker /docker-compose / kubectl は入らないので、Ubuntuには自分で入れましょう。

systemd の構成

Minikube は --vm-driver=none の場合 systemd が必要ですが、docker の場合不要です。

--vm-driver=docker でも、WSL2ログインごとに minikube 起動をやっていられない + SysV Init を用意するのもつらいという場合、WSL2でSystemdを動かして minikube.service を作って調整してもいいでしょう。

NOTE: もちろん、.bashrc で適当に開始させてもいいでしょう。

systemd を WSL2 に入れる方法は、Kubernetes のminikube ブログやいい記事が紹介しています。

qiita.com

これで systemd の起動が確認できます。

f:id:guitarrapc_tech:20210101033522p:plain

systemd で自動起動するためのminikube.service は適当にこんな感じで。

[Unit]
Description=Runs minikube on startup
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/minikube start --vm-driver=docker --addons ingress --kubernetes-version v1.18.0
ExecStop=/usr/local/bin/minikube stop
User={{ ansible_user_id }}
Group={{ ansible_user_id }}

[Install]
WantedBy=multi-user.target

systemd を構成する場合、genie ではなく enter-systemd-namespace スクリプトの方がカスタマイズしやすいのでおすすめです。 例えば、systemd にすると Windows からの WSL2 ログイン時に bash loginをするため /mnt/Windowsパス が維持されず ~/ になってしまいますが、enter-systemd-namespace で bash login 前に今のパスを保持しておいて、ログイン後に自動的に cd するなどの対処が取れます。

minikube の動作確認

minikube を docker driver で起動させてみます。

minikube start --vm-driver=docker

minikube の起動が確認できます。

$ minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
timeToStop: Nonexistent
$ kubectl get node
NAME       STATUS   ROLES    AGE   VERSION
minikube   Ready    master   21h   v1.18.0

$ k get pod -A
NAMESPACE     NAME                                        READY   STATUS      RESTARTS   AGE
kube-system   coredns-66bff467f8-tmqw6                    1/1     Running     1          21h
kube-system   etcd-minikube                               1/1     Running     1          21h
kube-system   ingress-nginx-admission-create-49j8v        0/1     Completed   0          21h
kube-system   ingress-nginx-admission-patch-cwrkz         0/1     Completed   0          21h
kube-system   ingress-nginx-controller-6f5f4f5cfc-9hw8f   1/1     Running     1          21h
kube-system   kube-apiserver-minikube                     1/1     Running     1          21h
kube-system   kube-controller-manager-minikube            1/1     Running     1          21h
kube-system   kube-proxy-72482                            1/1     Running     1          21h
kube-system   kube-scheduler-minikube                     1/1     Running     1          21h
kube-system   storage-provisioner                         1/1     Running     3          21h

minikube on WSL2 の面倒な点

minikube を --vm-driver=docker-driver で起動した場合に面倒なのは2点です。

  1. minikube の docker registry に push しないといけない
  2. ingress に Windows ホストからアクセスできない

minikube を docker driver で起動した場合、minikube の docker registry は Ubuntu とは別に構成されます。 そのため、bashログイン時に eval $(minikube docker-env) をする必要があるので注意です。

minikube を docker driver で起動した場合、ingress のAddress は docker ドライバーで起動した minikube ip のアドレスになります。 localhost ではないため、Windows から WSL2 への直接のアクセスはできません。 この場合、Service: LoadBalancer にしておくと minikube tunnel でlocalhost にトンネルされるので Windows からアクセス可能になります。

余談: なぜローカル Kubernetes 環境を必要とするのか

開発環境、開発ブランチ環境に対応するKubernetes があるとき、なぜローカルのKubernetes は必要なのかという話があります。

私はKubernetesの管理者であり、Devでもあるので、ローカル開発は何にもまして最速でプロダクトに期待する動作を確認する必須の場といえます。 ローカル開発は PoC であり、実際の運用環境とくらべて構成要素はミニマムだったりクラウド依存のなさなど差異もあるでしょう。 しかし、想定するアプリ動作を満たす場としては最適であり、常にメンテされていくべきと考えています。 また、自分のみに閉じているという意味でもローカル開発環境は整備する価値があります。 Kubernetes に各種 Operator/Controller を入れて動作させる場合は、ローカルKubernetes がない状況で、Devなどに展開するのは開発効率の面からみて避けたいものです。

ただしこれらの前提として、実際に動作する環境とかけ離れた構成になることは望ましくありません。 例えば、ローカルKubernetesではIngress に何かしらの制約がありLoad Balancer にする、といった状況は絶対的に避けたいものです。 運用環境とかけ離れ、期待する動作環境をローカルに作れず、メンテもできない場合、そのローカルKubernetes 環境はむしろ害悪になるので避けたいものがあります。

別の視点としてKubernetesの管理者という前提がない場合、ローカルKubernetes が必要である必然性が消えます。 例えば、通常のアプリコードを書いて動かすだけにおいては、Kubernetes だろうとどこででもいいからコードがデプロイされて動作すればいいです。 というか Kubernetes とか知りたくないし、ぐらいでもいいでしょう。

Kubernetes の管理をしつつDevもする身としては、ローカルKubernetes 環境はほしいものですが、メンテできるか、その構成は常に今のスタンダードからみてかけ離れていないかは常に気にする必要があります。Minikube はそういう意味ではスタンダードですが、悩ましい側面も多いですね。

kind on WSL2 はどうなのか

インストールはスムーズです。minikube より楽。 ただし、kind は Load Balancer に対応していないので使いにくさは否めません。

おまけ: WSL環境の選定

WSL には、WSL1 と WSL2 があり、この二つはホストOS のWindows との相互利用で結構な違いがあります。 Kubernetes を WSL に建てる場合、これは大きな違いになります。

docs.microsoft.com

f:id:guitarrapc_tech:20210101024249p:plain

WSL2 なのか WSL1 なのか

選択肢はおおむね2択であることが多いかと思います。

  • WSL2のみで利用し純粋にLinuxとほぼ同様に扱いたい、WSL2とWindows は速度面などで気にならない場合、WSL2 + Docker on WSL2 がいいでしょう。
  • Windows と WSL2 のファイルマウント1 の9Pに由来する遅さ、Docker on WSL2のパフォーマンス制約、ホストOSのCPU/メモリ負荷が気になり許容できない場合、WSL1 がいいでしょう。

別の構成として、Docker のみ Hyper-V で動かし、WSL2 でUbuntu を動かすことも考えられます。 この場合、docker 利用時のファイルマウントの遅さは生じず、WSL2 の Ubuntu はLinux 同様に扱えますが ホストWindows の Docker は共有できません。(tls:localhost:2375 がだめなんですよね)

WSL2 でないといけないのか WSL1 ではだめなのか

Windows/WSLの相互利用時のパフォーマンス、docker volume のマウントの取捨選択になります。 私の利用ケースはWindowsとWSLの相互利用がメインなので、WSL2 よりもWSL1 のほうが望ましいとなります。

ファイルマウント、ホストCPU負荷の両面でWSL1 は優れています。 一方で、純粋なLinux として使うには細かな差異とdocker 動作の制約が大きなハンデになります。(エラーもでないので罠に感じやすい)

WSL2

ほとんどのケースで問題ないことが多いでしょうが、パフォーマンス制約は軽く見るのは避けるほうがいいでしょう。

NOTE: Zen3 5900X + PCIE 4.0 NVMe でCPU負荷は無視できます。しかしWindowsホストファイルのマウントはどうしようもなく遅く回避策が現状ありません。

回避不可能な Windows ホストとのファイルボリュームマウント制約、そしてCPU負荷 (vmmem / COM Surrogate) があることに注意です。

NOTE: .wslconfing でメモリ、CPUの上昇をある程度止められますが十分に負荷が大きいので厄介です。

ただ、WSL2 Ubuntu と Docker WSL2 で Docker registry も共有され、docker/kubectl も入ってくるので非常に使いやすいです。

WSL1

WSLでdocker volume マウントができない制約があります。また、単純なWSL内部のファイルアクセスもWSL2 に比べて格段に遅いので注意がいります。

NOTE: docker実行はWindows でdocker run している限りは起こらないので回避策はあります。

WSL2 と違って、Windows ホストに対するパフォーマンスペナルティがなく、WSL での動作制約があるだけなのはバランスがいい選択になります。

WSL Ubuntu から docker を使うには、Docker Desktop で tls:localhost:2375 を開放して、WSLのDOCKER_HOST にする必要があります。 これで Windows の docker registry が WSL Ubuntu でも共有されるので、使い勝手の面で非常に楽になります。

おまけ: Kubernetes のインストール先の検討

ホストOSのWindowsでKubernetes クラスターを組んだ場合、Windows/WSLの両方からアクセスしても問題ありません。 WSL2 でKubernetes を組んだ場合、Windows からのアクセスには WSL2 の localhost でアクセスができる必要があります。

こういう意味では、Windows ホストにサクッとクラスターを組んで、いつでも爆破できる状況を作るのが最も使いやすいでしょう。

Docker Desktop for Windows なら、Settings 画面から Kubernetes Cluster のファクトリーリセット、再構成も用意なのでいいでしょう。


  1. それに伴うネットワーク速度の遅さも発生する

Unifi AP AC-Pro が永遠に消えたVLAN への接続を試みる件

Unifi のアクセスポイントである Uniti AP AC-Pro (UAP) を使っています。

www.ui.com

今買うなら Unifi nanoHD が本来筋なのですが、いろいろあってメッシュ先のLAN ポイントとして使いたかったのでシカタナイ。

unifi-nanohd.ui.com

今回は、UAPのアップグレード中にVLAN の構成を変更したらエラーが出続けてちょっと困ったことになったので対処をメモ。

目次

再現手順

  • UAP のファームアップデートがきた通知があったので、Unifi Deram Machine(UDM) のコンパネからUAPのファームを更新
  • 更新中に 不要になったVLAN2000の削除
  • UAP が起動後、UAP が消えたVLAN へのアクセスを試みて失敗するエラーが出続けます。
Message: AP[MAC ADDRESS] event: {"event_string":"EVT_AP_Notification","payload":"UAP-AC-Pro is unable to reach the DHCP server for VLAN 2000"}

f:id:guitarrapc_tech:20201229021444p:plain

どういうトラブルか

私の場合消したVLANはVLAN2000 だったのですが、インシデントに対してメール通知を仕掛けていたので1分に一度エラー通知メールが飛んでしまいました。

さて、ファームアップデート後に設定が同期していないと思われるので対処をやってみます。 上から順に解決すればそれでok でしょう。

  1. Provision のし直し
  2. Fatory Reset からのデバイス追加
  3. 適当な既存VLANネットワークの構成をし直.してVLAN設定の同期

結論からいうと、適当な既存VLANネットワークの構成をし直せば同期しました。

復旧の流れ

Provision をすると、設定が同期されるので解消を期待しましたがエラーは出続けました。 Provision あんまりうまくいくことないのでぐぬぬ。

仕方ないので、UAPのFactory Reset、 UDM上から UAP を Forget、iOS の Unifi App からUAPを追加し直しても復帰後に同じエラーが出続けました。 UAP の Factory Reset 後にUDM上からデバイスをForget 忘れてて追加できなくて??っとなったのでマニュアルは読みましょう。

help.ui.com

ここまでで、UAP自体というより同期されるべきVLANの構成が正常にUAP向けに生成されているない気配なので、VLAN を修正して強制的に同期させます。 既存の適当なVLAN を修正します、私の場合 VLAN ID を修正したかったところだったので直してSave、これでUAP が自動的に構成を受け取ってエラーが消えました。

ちなみに、エラーが出てる間は既存の Guest Portal な SSID が不正なアクセスポイントとして検出もされていました。コミュニティフォーラム見るとGuest Portal を有効にしていると起こる気配も。

まとめ

自動構成同期、いいのですがどういう構成を同期しているのか、そういえばあんまり気にしたことありませんでした。 Unifi の場合、Device > UAP > Settings > Download Device Info で動作状況がダウンロードできるので良いのですが、ちょっと困りますね。

余談

そういえばこういう古い記事はいい加減直してほしいものです。VHT20 以外も選べるようになってるのですよ。

internet.watch.impress.co.jp

f:id:guitarrapc_tech:20201229015843p:plain
UAPのVHTは選択可能になっている