tech.guitarrapc.cóm

Technical updates

Windows 11 で Microsoft Teams がログオン時に勝手に起動するのを止めたい

Windows 11 をインストールしたときに戸惑ったのが Microsoft Teams がログオン時に勝手に起動することでした。

使ってないこともあって、無意識に無効にしたのですが先日困っている人がいたので記事にしておきます。

tl;dr;

Windows 11 では、スタートアップアプリ に Microsoft Teams が登録されています。 これを無効にすると、ログオン時に自動的に Microsoft Teams が起動してくるのを止めることができます。

Settings ->

スタートアップ アプリから Microsoft Teams を無効にする

Windows 11 のスタートアップアプリがどうなっているか確認する

Windows 11 の場合、2か所を確認するといいでしょう。

  1. タスクマネージャーの スタートアップアプリ
  2. スタートアップフォルダのショートカット

どちらもこれまでのWindows でも同じものがありますが、Windows 11 では少しアクセスしやすくなった気がします。

タスクマネージャーの スタートアップアプリ

タスクマネージャーのスタートアップ で確認できます。 ユーザーがログオンしたときに起動するアプリが登録されています。

これらのアプリをログオン時に起動するかどうかは、タスクマネージャーで有効/無効の切り替えできます。

Ctrl + Shift + Esc

タスクマネージャーのスタートアップアプリ

また、タスクマネージャーではなく 設定 (Settings) から行うこともできます。

Settings > Apps > Startup Apps に移動

スタートアップアプリ

なお、以前のWindows にあったシステムコンフィグのスタートアップ設定は、前述のタスクマネージャーに移動しています。

スタートアップメニュー や Ctrl +R で起動した実行ウィンドウで msconfig を入力

システムコンフィグのスタートアップ

スタートアップ

Windows の持つ専用フォルダ %appdata%\Microsoft\Windows\Start Menu\Programs\Startup を開くと確認できます。 ユーザーがログオンしたときに実行するアプリのショートカットが登録されています。

このパスにショートカットがおいてあれば起動するので、起動を無効にするにはショートカットを消すか移動します。

Ctrl +R で起動した実行ウィンドウで、 shell:startup を入力

shell:startup

スタートアップフォルダにはショートカットが登録されている

そのほか

Windos サービスやレジストリでの登録などもありますが、直接触る機会はかなり減ったように思います。 その背景には、アプリが自分自身でスタートアップを設定するか設定に持つことが増えたこともある気がしますね。

Ubuntu 22.04 と WSL

Ubuntu 20.04 on WSL は非常に安定しており、また便利で好んで利用しています。 さて、Ubuntu 22.04 がリリースされ、WSL にも来ました。

そこで今回、今の Ubuntu 20.04 on WSL な環境を Ubuntu 22.04 にするにあたり対応したことをメモしておきます。

tl;dr;

  • Ubuntu 22.04 を WSL で動かすなら WSL2 一択になりそう
  • Ubuntu 22.04 on WSL1 は BSoDが頻発し needrestart の制御が効かないためあきらめた
  • WSL2 は VPN 周りで困るので今後向き合っていくことになりそう

Ubuntu 22.04 での大きな変更点

needrestart パッケージの導入

needrestart パッケージの導入は、Ubuntu 20.04 まではなかった変更で直面することになります。

gihyo.jp

特に私は Ansible で WSL の Ubuntu環境を構成しているので、apt パッケージの実行のたびにエラーになるのは困ります。 あと、WSL なので ホストOS の再起動もそれなりにすることもあり、結構いらない感じがあります。

ということで、適当に /etc/needrestart/conf.d/99_ansible.conf などをこしらえて、警告表示を無効にしています。

Ubuntu でアップグレードした場合の警告表示を無効化する | らくがきちょう v3 https://sig9.org/archives/4580

# use stdin to select restart service
$nrconf{ui} = 'NeedRestart::UI::stdio';
# control restart service
$nrconf{kernelhints} = '0';
$nrconf{ucodehints} = 0;
$nrconf{restart} = 'a';

これを Ansible での apt 処理の前段で設定することでok、となります。

- name: "needrestart - check OS is using needrestart"
  become: yes
  stat:
    path: /usr/sbin/needrestart
  register: needrestart_exists

- name: "needrestart - place custom conf.d"
  become: yes
  copy:
    dest: /etc/needrestart/conf.d/99_ansible.conf
    content: |
      # use stdin to select restart service
      $nrconf{ui} = 'NeedRestart::UI::stdio';
      # control restart service
      $nrconf{kernelhints} = '0';
      $nrconf{ucodehints} = 0;
      $nrconf{restart} = 'a';
  when: needrestart_exists.stat.exists

再インストールにはホストWindowsの再起動が必要

これまでの WSL での Ubuntu 再インストールは、アンインストール後に ストアから再インストールが可能でした。 Ubuntu 22.04 では、アンインストール後に Windows を再起動するまでストアから再インストールできなくなっているようです。 試行錯誤がちょっとめんどくさくなりました。

アンインストール

アンインストール後のストアからの再インストールがダウンロードで止まる
アンインストール後のストアからのインストールが失敗する

Ubuntu 22.04 をWSL2 に構成する

大きな変更以外は調整不要です。 Ubuntu 20.04 までの Ansible での構成は 22.04 でも問題なく機能するでしょう。

例えば私は次のようなコマンドで構成しています。

mkdir -p ~/github/guitarrapc && cd ~/github/guitarrapc
git clone https://github.com/guitarrapc/local-provisioner
cd ~/github/guitarrapc/local-provisioner/envs/ubuntu_wsl2

# ansible の導入
. ./prerequisites.sh
ansible-playbook -i hosts site.yml -K

リポジトリはこちらです。

github.com

docker WSL2 にインストールする場合

Docker for Windows ではなく、Ubuntu 22.04 に docker をインストールする場合、iptable を古いのを指定しないと動かないのは変わっていません。

$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
$ sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
$ sudo update-alternatives --config iptables

There are 2 choices for the alternative iptables (providing /usr/sbin/iptables).

  Selection    Path                       Priority   Status
------------------------------------------------------------
  0            /usr/sbin/iptables-nft      20        auto mode
* 1            /usr/sbin/iptables-legacy   10        manual mode
  2            /usr/sbin/iptables-nft      20        manual mode

設定してから docker を開始するとうまく動きます。

$ sudo service docker start

Ubuntu 22.04 をWSL1 に構成する (断念)

手元では、再現する問題が2点あり解消が難しそうなので断念しました。

  1. needrestart の制御が needrestart.conf でも効かずAnsibleでの構成でaptパッケージごとにエラーが生じる
  2. Ubuntu 22.04 でパッケージ構成時にBSoD が頻発する

needrestart を purge してもダメなので何かやり方が間違えている気がするものの、BSoD は解消できないのでちょっと難しそうです。 BSoD は、今のところ dotnet SDK の導入時に起こっています。

# ref: https://dotnet.microsoft.com/download/linux-package-manager/ubuntu18-04/sdk-current
# ref: https://github.com/ocha/ansible-role-dotnet-core
- name: "dotnet - make sure HTTPS is supported by apt"
  become: yes
  apt:
    name: apt-transport-https
    state: present
    update_cache: yes

- name: "dotnet - import Microsoft apt key"
  become: yes
  shell: wget https://packages.microsoft.com/config/ubuntu/{{ ansible_distribution_version }}/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb
  changed_when: false

- name: "dotnet - add repo for Ubuntu"
  become: yes
  shell: dpkg -i /tmp/packages-microsoft-prod.deb
  changed_when: false

- name: "dotnet - install dotnet sdk ({{ args.version }})"
  become: yes
  package:
    name: "dotnet-sdk-{{ args.version }}"
    state: present
    update_cache: true

おまけ: WSL2 と VPN の問題

よく知られている問題なので、これをやっていきましょう。

qiita.com

Workaround for WSL2 network broken on VPN · GitHub

Pulumi で今のスタックを異なるプロジェクトのスタックに移動させる

Pulumi はステートを スタック (Stack) に保持しています。 スタックはリネームや別の Organization への移動はサポートされていますが、自分のOrganization の別プロジェクトへの移動はサポートされていません。

しかしスタックを後から整理したいときには、このスタックを別のプロジェクトに移動させたいというのはやりたくなるでしょう。 公式にはまとまっていないので、いずれサポートされるまでのワークアラウンドを見てみましょう。

tl;dr;

  • Pulumi CLI で pulumi importpulumi export を使ってステートを取り込みなおせばステートを別プロジェクトのスタックに移動できる
  • スタックの移動前にスタック名を移動先で使いたい名前にリネームしましょう
  • 今あるプロジェクトにスタックを追加するのが忘れがち

主にこの流れですが、別プロジェクト名なのでステートの編集が必要です。

github.com

何をしたいのか

スタック移動したい例を考えてみましょう。 下の図は、FooFoo-GuardDuty の2プロジェクトがあり、それぞれ master というスタックがある状態です。

Foo
  Master
Foo-GuardDuty
  Master

プロジェクト名を見てわかる通り、 Foo-GuardDuty は AWS GuardDuty 専用の処理なので Foo とは別に作りたかったようですが、プロジェクトで分けてしまいました。このやり方だと、同じプロジェクトなのにその関係が疑問付きになりますし、Pulumi UI 上でも縦に伸びて見にくなります。

そこで下の図のように、Foo-Dev プロジェクトの中に master と guardduty というスタックを持つ構成に変えましょう。

Foo
  Master
  Guardduty

これなら関連のあるプロジェクト Foo でまとめつつ、その構成の違いはスタックで示すことができます。

変更の流れ

次の流れでやっていきます。

  1. 移動先のスタックを既存プロジェクトに作成
  2. 移動元のスタック名を移動先のスタック名にリネーム
  3. 移動元のスタックをエキスポート
  4. エキスポートした json を編集
  5. 移動先のスタックに編集したjsonを インポート
  6. 移動先のスタックでシークレットやコンフィグをセットしなおし
  7. pulumi refresh && pulumi preview && pulumi up

変更前のフォルダ構成

ちなみに変更前は次のようなフォルダ構成です。

$ tree
.
├── Foo-GuardDuty.Master
└── Foo.Master

変更後のフォルダ構成

変更後は次のようになります。

.
├── Foo.GuardDuty
└── Foo.Master

移動先のスタックを既存プロジェクトに作成

すでにあるプロジェクト Foo にスタック guardduty を追加するには、pulumi init をプロジェクトとスタック名で初期化します。 今回スタックを置くフォルダ名を、Foo.GuardDuty としましょう。

mkdir Foo.GuardDuty
cat <<EOF > Foo.GuardDuty/Pulumi.yaml
name: Foo
runtime: dotnet
description: AWS Foo account
EOF
cat <<EOF > Foo.GuardDuty/Pulumi.GuardDuty.yaml
config:
  aws:region: ap-northeast-1
EOF

これで次のようなフォルダとファイルができたはずです。

$ tree
.
└── Foo.GuardDuty
    ├── Pulumi.GuardDuty.yaml
    └── Pulumi.yaml

既存プロジェクトにスタックを作る準備ができたので、 pulumi cli で Foo プロジェクトにguarddutyスタックを作ります。

pulumi stack init guardduty

これで既存の Fooプロジェクトに 空のスタック guardduty が追加されます。

移動元のスタック名を移動先のスタック名にリネーム

続いて移動元のプロジェクトのスタック、Foo-GuardDuty/Master のスタック名を移動先のスタック名 GuardDuty に変えましょう。 pulumi cli スタック名がリネームできます。

cd Foo-GuardDuty.Master
pulumi stack rename GuardDuty
cd ..

スタックのエキスポート前にやっておくと、エキスポートしたスタックのjsonを編集する手間が減るのでオススメです。

移動元のスタックをエキスポート

pulumi cli で Foo-GuardDuty/GuardDuty のステートをエキスポートして、Foo.GuardDuty にコピーしておきましょう。

cd Foo-GuardDuty.Master
pulumi stack export --file guardduty.stack.checkpoint.json
cp guardduty.stack.checkpoint.json ../Foo.GuardDuty/.
cd ../Foo.Guard

エキスポートした json を編集

json には、プロジェクト名とスタック名が書かれており、これが一致しないとインポートできません。 スタックのプロジェクト名が変わるので、json を sed や VS Code などで開いて一括置換しましょう。

置換は、::プロジェクト名:: で行うと間違えた場所を置換する心配がありません。

  • 検索文字列: ::Foo-GuardDuty::
  • 置換文字列: ::Foo::

移動先のスタックに編集したjsonを インポート

移動先のスタックで置換した json をインポートします。

cd Foo.GuardDuty
pulumi stack import --file guardduty.stack.checkpoint.json

正常に取り込めたはずです。

移動先のスタックでシークレットやコンフィグをセットしなおし

スタックをインポートで取り込んでも、pulumi config で設定していたコンフィグやシークレットは入りません。 適当にいい感じに設定しましょう。

pulumi config set foo bar

pulumi refresh && pulumi preview && pulumi up

Pulumiコード を元の環境から持ってきたら、新しいスタック環境でリソースとスタックの同期をとっていきましょう。 まずは実環境の状態をステートに取り込んでおきます。特に差分は出ないはず。

pulumi refresh

最後に実行して終わりです。

pulumi preview
pulumi up

プロジェクト名が変わったので、最上位urnであるプロジェクト名だけ入れ替えが出ますが、個別のリソースステートに差分は出ず影響ありません。

終わったらエキスポートした json を消したり、元のプロジェクトを消しましょう。 CI を組んであるなら、pulumi のプロジェクトパスを直したりすれば完璧ですね。

まとめ

pulumi はステートの扱いがかなり緩いので、比較的 json をいじる力業で何とかなります。 とはいえ、ミスをすると怖いので、やるときは実験プロジェクトなどで要領を把握してからやるといいでしょう。

RegoとConftest ことはじめ

Kubernetes の面倒なことといえばYAML、というのはよく聞くし私もそう思います。 YAMLが面倒なことはいくつもありますが、その1つに「YAMLの定義がポリシーとして正しいかの検証」があります。

コードを書いているときのように、YAML もポリシーにあっているのかユニットテストができれば、いざデプロイするときまでわからないという事態は避けられそうですね?

ということで、今回はKubernetes でポリシーチェックをする方法として良くあげられる Open Policy Agent のRego言語とContest を使っていくメモです。

なお、ポリシーチェックというとConftest や Gatekeeper がありますが、個人的にはローカルで気軽に始められるConftestから慣れていくのがいいと思います。試行錯誤しやすいところから入るのが大事。

tl;dr;

  • Rego のポリシーは、それぞれに名前つけたほうがテスト書いていて指定したポリシーと明示できるのがいい。デバッグのためにも判別できる工夫がかかせない
  • Rego のポリシー自体が正しいか試行錯誤することになるので、先にテスト書いたほうがいい。テストを書いてから、実際のYAMLに流すの流れが効率的
  • Rego のテストで引数使うの避けている。テストが通らなくなってしんどい。ダメな例: test_foo[msg] { }。いい例: test_foo { }
  • Rego のポリシーと結果をサクッと試すにはPlayground が便利
  • 公式サイトが充実しており、何度も公式サイトをリファレンスとして使うことになるので積極的に使っていきましょう

www.openpolicyagent.org

Rego基本情報

基本的なコンセプト、構文、関数はここを参照

Open Policy Agent | Policy Language

拡張子は .rego。 DSLでの記述と宣言的な実行に割り切っている。

全般的に package mainsprintf("%s", string) を含めて Golang っぽさがそこかしこにある。

2つ特徴がある。

  • 代入を除くと、式は同じポリシー内でも順不同に配置しても問題なく評価される
  • itelerator を使うため foo = input.keys[_] のように書くと、foo にはarray/set/mapの要素1が入ってくる

エディタ環境

VS Code + Remote Container で書くのがおすすめ。

VS Code に Open Policy Agent 拡張を入れると、フォーマッタが効くようになり、構文解析まで行ってくれる。おまけで、コマンドパレットで validate とかも出て VSCodeでプレイグラウンド的に使えるがそこまではいらない。

ただし、Open Policy Agent 拡張を動かすには、Open Policy Agent実行ファイル opa をインストールする必要があるので、Remote Container環境と相性がいい。

インテリセンスもないなど、IDEとして機能十分で使いやすいかというと微妙だが、正しい構文とフォーマットが維持できるのでないより圧倒的にいい。

実行方法

conftest + Open Policy Agent で実行するのが隙がない。

  • CI やローカルで実行するときは、conftest を用いることでサクッと実行できる、便利、よい
  • Kubernetes Cluster 内部でデプロイされる前にチェックする(GitOpsは典型) 場合は、Open Policy Agent | Kubernetes Admission Control を用いる

まずは conftest で実行できるようにすると、動作が把握できてカジュアルに試せるのでいい。 もっとカジュアルに試すなら Playground を活用するといい。

https://play.openpolicyagent.org/

ポリシーの基本形式

ポリシーの基本は次の通り。

  • ポリシーは複数のルール(=式) を持つ
  • ルールは bool を返し、AND評価(すべてのルールが true になるとポリシーに該当しているとみなされる)を行う
  • ポリシーの必須要素は、package宣言、violation などのポリシーの評価方法、msg
  • pacakge はmain以外は実行時に評価されない。このため、別名フォルダにおいて隔離しておいてimportで取り込むことで、 data.パッケージ名 として必要な関数を呼び出すことができる
package main

# 評価結果は、violation / deny / warn / allow で選べる。名前を付ける場合、先頭の文字がいずれかに該当していれば ok
# violation と deny は ポリシーを満たすとダメ扱い。warn は警告扱い。BlackListでのポリシー評価が楽なので、allow は使わない
violation[msg] {
    # boolを返すルールを複数記述できる。入力は、外部ライブラリを使わない限り input に入ってくる。
    msg := "ポリシー該当時のメッセージを記述する"
}

violation_何か名前をつけたり[msg] {}
violation_引数をマップにしたり[{"msg": msg}] {

式や組み込み関数の基本的な記述は公式をみるといい。

Open Policy Agent | Policy Language Open Policy Agent | Policy Reference | Built-in Functions

比較

比較だけややこしいので特記しておく。

同値の評価に :==== 演算子があるが、基本的に代入に :=、値比較に == を使うように公式にも記述があるので従っておくのがいい。

Equality  Applicable    Compiler Errors            Use Case
--------  -----------   -------------------------  ----------------------
:=        Inside rule   Var already assigned       Assign local variable
==        Inside rule   Var not assigned           Compare values
=         Everywhere    Values cannot be computed  Express query

例外的に、式の結果を受けるときに = を使って評価と代入を同時に行うケースがある。

[image_name, "latest"] = split_image(container.image)

split_image(image) = [image, "latest"] {
    not contains(image, ":")
}

split_image(image) = [image_name, tag] {
    [image_name, tag] = split(image, ":")
}

なお、数値の比較演算子には次のものがある。そろってるので困ったことはない。

a  ==  b  #  `a` is equal to `b`.
a  !=  b  #  `a` is not equal to `b`.
a  <   b  #  `a` is less than `b`.
a  <=  b  #  `a` is less than or equal to `b`.
a  >   b  #  `a` is greater than `b`.
a  >=  b  #  `a` is greater than or equal to `b`.

関数

関数は、戻り値を指定しない限りは bool が返る前提になっている。

is_foo {
    input.name == "foo"
}

パラメーターを与える場合は、 関数名(引数名){} と書く。

is_foo(value) {
    value == "foo"
}

bool ではなく、関数から特定の返り値をも耐える場合は 関数名() = 返り値{} と書く。複数の返り値があるなら、関数名() = [返り値1, 返り値2]{} となる。

get_name_age() = [name, age] {
    name := input.name
    age := input.age
}

例えば次のような input とポリシーが書ける。PlayGround で試しておみるといい。

Input

{
    "name": "John Doe",
    "age": 100
}

ポリシー

package play

deny[msg] {
    [name, age] = get_name_age
    msg := sprintf("name: %s, age: %v", [name, age])
}
get_name_age() = [name, age] {
    name := input.name
    age := input.age
}

Output

{
    "deny": [
        "name: John Doe, age: 100"
    ],
    "get_name_age": [
        "John Doe",
        100
    ]
}

OR評価

ポリシーは基本的に AND評価。 OR評価を行いたい場合は2つ方法がある。

  1. 同名の関数を用意する
  2. 配列に対して評価する

関数の場合、同名の関数を用意するとそれぞれの関数が個別に評価され、true が返った関数が利用される。

例えば、次のような is_byte_format 関数を用意すると、Gi でも Mi でもフォーマットを評価できる。

is_byte_format(size) {
    endswith(size, "Gi")
}

is_byte_format(size) {
    endswith(size, "Mi")
}

配列を使う場合、配列に許可する要素を記述しておいて、itelator を使って順次結果が評価したい値と一致するかを判定する。 イテレーター自体は普通なのだが、式を見ても直感的じゃないのであまり好きじゃない。

workload_resources := ["Deployment", "StatefulSet"]
input.kind == workload_resources[_] # kind が Deployment / StatefulSet の時だけ true になる。

いずれの方法でも、デバッグログが汚くなるので利用したらデバッグログは読みにくくなることを覚悟するしかない。

Kubernetes での利用

Kubernetes で使うにあたってポイント。

評価対象リソースの限定

Kubernetes で利用する場合、様々なリソースYAML に同じルールが適用されることを前提に書くことになる。 例えば、pod や container に関する記述は、 Pod / Deployment / StatefulSet / DaemonSet / Job / Cronjob に適用してほしいだろうが、Service や Ingress には適用してほしくない。

そのため、ポリシーの AND評価を利用して先に kindapiVersion で絞りこむことになる。

violate[msg] {
    input.kind == "Deployment"
}

だがこのような記述にすると、StatefulSet と Deployment にというのが書きにくい。そのため、先に配列で許可リストを用意するといい。

workload_resources := ["Deployment", "StatefulSet"]
is_deployment_or_statefulset {
    input.kind == workload_resources[_]
}

ポリシーでis_deployment_or_statefulset関数を呼び出すことで、そのポリシーは DeploymentかStatefulSetでのみ評価されることが保障できる。

violation[msg] {
    kubernetes.is_deployment_or_statefulset
    # 何かルール

    msg = "ポリシー違反です"
}

キーがないことの評価

「特定のラベルがないときにエラーにする」といったキーがないことを評価するときは not を使う。

violation[msg] {
    kubernetes.is_deployment_or_statefulset
    not input.metadata.labels["not-found"] # ココ
    msg = "ポリシー違反です"
}

複数のラベルに対して、どれか1つでもない場合にエラーにすることを評価するときは、配列 + not を使う。これで、どれか一つでもラベルにないとポリシーがひっかかる。

recommended_labels {
    input.metadata.labels["app.kubernetes.io/name"]
    input.metadata.labels["app.kubernetes.io/instance"]
    input.metadata.labels["app.kubernetes.io/version"]
    input.metadata.labels["app.kubernetes.io/component"]
    input.metadata.labels["app.kubernetes.io/part-of"]
    input.metadata.labels["app.kubernetes.io/managed-by"]
}
violation[msg] {
    kubernetes.is_deployment_or_statefulset
    not recommended_labels # ここ
    msg = "ポリシー違反です"
}

よく使う一式

これを kubernetes.rego として保存しておいて、使いたいポリシーで、import data.kubernetes してから kubernetes.is_service などのようにして使っている。必要に応じてヘルパーが増えていったりする。

package kubernetes

# properties
name := input.metadata.name

kind := input.kind

is_service {
    kind == "Service"
}

workload_resources := ["Deployment", "StatefulSet"]

environment_labels := ["development", "staging", "production"]

is_deployment_or_statefulset {
    input.kind == workload_resources[_]
}

is_pod {
    kind == "Pod"
}

is_service {
    kind == "Service"
}

is_not_local {
    input.metadata.labels.environment == environment_labels[_]
}

is_ingress {
    kind == "Ingress"
}

pod_containers(pod) = all_containers {
    keys := {"containers", "initContainers"}
    all_containers = [c | keys[k]; c = pod.spec[k][_]]
}

containers[container] {
    pods[pod]
    all_containers := pod_containers(pod)
    container := all_containers[_]
}

containers[container] {
    all_containers := pod_containers(input)
    container := all_containers[_]
}

pods[pod] {
    is_deployment_or_statefulset
    pod := input.spec.template
}

pods[pod] {
    is_pod
    pod := input
}

volumes[volume] {
    pods[pod]
    volume := pod.spec.volumes[_]
}

# image functions
split_image(image) = [image, "latest"] {
    not contains(image, ":")
}

split_image(image) = [image_name, tag] {
    [image_name, tag] = split(image, ":")
}

# security functions
dropped_capability(container, cap) {
    container.securityContext.capabilities.drop[_] == cap
}

added_capability(container, cap) {
    container.securityContext.capabilities.add[_] == cap
}

テスト

ポリシーは、それ正常に評価されているのか、使ってはだめなINPUT例と使っていいINPUT例 で繰り返し試しながら書くことになる。 実際の YAML に対して変更かけつつ試すと、普段のYAMLに対してどう追加すればいいのかを試す / ポリシー評価が意図通りか試す、試したものを戻す、といった余計は作業が多く発生する。

このため、ポリシーは「想定するINPUTを用意」して、「INPUTに対してポリシーを記述」して、「ポリシーをテストで通るか確認」して、「実際のYAMLに対して実行」するという流れがうまく当てはまる。

テストでは、次の3つを行う。

  • 想定するINPUTを用意
  • INPUTに対してポリシーを記述
  • ポリシーをテストで通るか確認

conftest を用いる場合、テストは次の形式で実行できる。

# テスト結果概要だけ表示
$ conftest verify --policy ./path/to/policy

# テスト一つずつの評価経過をtrace表示
$ conftest verify --policy ./path/to/policy --trace

# テスト結果概要 + 失敗したテストのみ評価経過を表示
$ conftest verify --policy ./path/to/policy --report failed

# テスト結果を一つずつPASS/FAILED表示 + 失敗したテストは評価経過を表示
$ conftest verify --policy ./path/to/policy --report full

テストのテンプレ

テストは、「ポリシーファイル_test.rego」が定番なので従っておく。(_test は必須じゃないが、ポリシーと同階層におくので自然とそうなる) テストには注意が3つある。

  • テストは test_ でポリシー命名が始まる
  • テストは、ポリシーに引数がない
  • テストの msg は、評価するポリシーと同じ出力になるようにしないといけない

引数がないのは忘れがち。万が一引数があるとポリシーが正常に評価されてもテストは通らない。

# ダメな例
test_violation_labels_recommended_missing[msg] {}

# いい例
test_violation_labels_recommended_missing {}

テストの msg はポリシーで固定文字列なら同じものを入れればいい。 だが、どのリソースでポリシーが違反したか判別のために input.kindinput.metadata.name を使っている場合は、テストでも同じ結果になるように注意が必要だ。

# 固定のメッセージならポリシーとテストで同じものを指定でok
msg = "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels)"
# 動的なmsgなポリシー
violation_some_policy[msg] {
    input.kind == "Deployment"
    input.metadata.name == "test-data"
    msg = sprintf("推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=%s,Name=%s]", [input.kind, input.metadata.name])
}

# ポリシーに該当するテスト
test_violation_some_policy {
    # テストのmsgはinput に合わせる必要がある。
    msg = "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
    violation_some_policy[msg] with input as {
        "kind": "Deployment",
        "metadata": {
            "name": "test-data",
        },
    }
}

テストのサンプル

例えばKubernetes推奨ラベルがあることを保証するポリシーがあるとしよう。

package main

recommended_labels {
    input.metadata.labels["app.kubernetes.io/name"]
    input.metadata.labels["app.kubernetes.io/instance"]
    input.metadata.labels["app.kubernetes.io/version"]
    input.metadata.labels["app.kubernetes.io/component"]
    input.metadata.labels["app.kubernetes.io/part-of"]
    input.metadata.labels["app.kubernetes.io/managed-by"]
}
workload_resources := ["Deployment", "StatefulSet"]

is_deployment_or_statefulset {
    input.kind == workload_resources[_]
}

# recommented labels must exists
violation_labels_recommended_exists[{"msg": msg}] {
    is_deployment_or_statefulset
    not recommended_labels

    msg = sprintf("推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=%s,Name=%s]", [input.kind, input.metadata.name])
}

これに対して、labelsがないテストとあるテストを用意すれば、実際のYAMLで確認することなくポリシーが妥当かユニットテストできる。 ポリシー違反しない場合は、not を付けることで正常にとおったことをテストできる。

test_violation_labels_recommended_missing {
    msg := "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
    input := {
        "kind": "Deployment",
        "metadata": {"name": "test-data"},
    }

    violation_labels_recommended_exists[{"msg": msg}] with input as input
}

test_violation_labels_recommended_exists {
    msg := "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
    input := {
        "kind": "Deployment",
        "metadata": {
            "name": "test-data",
            "labels": {
                "app.kubernetes.io/name": "",
                "app.kubernetes.io/instance": "",
                "app.kubernetes.io/version": "",
                "app.kubernetes.io/component": "",
                "app.kubernetes.io/part-of": "",
                "app.kubernetes.io/managed-by": "",
            },
        },
    }

    not violation_labels_recommended_exists[{"msg": msg}] with input as input # 正常評価なら not を付ける
}

テストが通らないときのデバッグ

デバッガで止まるわけじゃないので、2つ使って評価を試す。

  1. Playground でポリシーとinput を用意して結果を見る
  2. conftest verify --report failedconftest verify --trace で評価経過を追いかける

ポリシーがそもそもおかしいかもしれない場合は、Playground で仮Input に対してポリシーがちゃんとかかっているか試行錯誤するのが手っ取り早い。なので、ポリシー取り合えず書いてみて、うまく当たるか見てみたい、という場合は Playground でサクッとやってみるのは結構オススメ。

プレイグラウンドでポリシーは当たったけど、どう評価されているか確認しながらやりたいだろう。そういうときは --trace したり、--report で見る。--trace は、全部のテストの trace が出るので注意。(最後のテストになるようにするといい) trace は正直読みにくいが、一行一行追っていけば、なるほど確かにという感じで評価されているのがわかるので、困ったら読む価値は十分ある。

テストの trace を読みやすくする

traceを使う注意点が2つある。

  • OR評価をすると trace ログが入れ子になってハイパー読みにくくなる
  • data に関数を逃がすとそれだけで入れ子になって読みにくい

このため、まず試す、というときは決め打ちでOR評価せず、インラインでポリシーに直接パスを指定して書くほうが trace は圧倒的に追いやすい。

「なんでテストが通らないかわからないけど、traceが読みにくくて追いきれない」そんなときは、dataもOR評価もせず、決め打ちのinputに対するミニマムポリシーを用意してルールを直書きしてみるといい。

traceを見てもポリシーはあってるがテストが通らない

テストに引数がついていないだろうか。 私はこれで数時間溶かした。

# ダメな例
test_violation_labels_recommended_missing[msg] {}

# いい例
test_violation_labels_recommended_missing {}

Reference

考え方

Conftest で CI 時に Rego で記述したテストを行う - @amsy810's Blog

ポリシーサンプル

conftest/examples/kustomize/policy at master · open-policy-agent/conftest

GitHub - redhat-cop/rego-policies: Rego policies collection

GitHub - swade1987/deprek8ion: Rego policies to monitor Kubernetes APIs deprecations.

Collecting together Kubernetes rego examples, including porting the https://kubesec.io rules to rego


  1. 正直気持ちが悪いしデバッグがしにくい原因の1つ。

Infrastructure as Code に最低限求めること

Infrastructure as Code (以降 IaC) で組むときに IaC ツールを選ぶ基準はいろいろあります。 IaCの言語がチームとって扱いやすいか、クラウドやサービスへのIaCの対応状況は早い/十分か、コミュニティの大きさは十分でググれば情報を入手できるか、実装がオープンソースで公開されているか、利用事例が欲しいか、など様々な理由で選ぶことでしょう。

しかし、こういった基準よりまず 「IaC というからには最低限出来ていないと困ること」というのが個人的にあります。

最近、Bicep で組んでいた Azure の環境を Pulumi に移行したのですが、その理由は Bicep では IaC が維持できないと判断したためでした。 IaC ツールを使っていて IaC が維持できないと判断したのは初めてだったので、いい機会ですし IaC に最低限求めることを言語化しようと思います。

tl;dr;

私はIaC に、コードとリソース1 状態の一致保障、実行プランが出力できること、ステート2 を持てることを求めます。 これらが不完全なIaCでは、コードでのリソース管理は不完全なものになり、今回のように維持できない原因になるため避けます。

  • コードとリソース状態の一致を保障できることを求めます。一致が保障できることで、コードからリソースの状態を完全に制御できます。(コードファースト)

コードとリソースの状態が一致

  • 実行前に実行プランを出力できることを求めます。実行プランによりコードの変更適用前にリソースへの影響がわかるため、PRベースでレビューをうけつつ開発ができます。また、正確な実行プランには、ステートかそれに準じたものと継続した開発体制が必要になるでしょう

実行プランによるコード変更によるリソース差分の確認

  • ステートをIaCサービスやオブジェクトストレージに配置し、ステート実行時にロックできることを求めます。ステートをローカル以外に持ち、実行ごとにロックがかかることで、チーム開発でブランチを同時に複数稼働させることができます

ステートファイルは実行ごとにロック

IaC でやりたいこと

IaC でやりたいことは「コードでクラウドリソースを管理できる」 これだけです。しかし、コードでクラウドリソースを管理できるとはどういうことでしょうか?

私は、コードで定義を書いたらリソースが追加されてほしいし、コードの定義を変更したらリソースが変更されてほしいし、コードの定義を消したらリソースが削除されてほしいです。 コードの定義 = リソースの状態と一致するとコードでクラウドリソースが管理でき、クラウドコンソールで操作する機会を大きく減らすことができます。3

コードでリソースを管理するためにはチームでスムーズに開発を行いたいです。 チーム各自でブランチを切って並行してコードを修正、修正ごとに実行プランで差分を確認し、レビューを受け、問題ないならマージしたいでしょう。 ステートをリモートに置いたり、実行ごとにロックされるとチーム開発で扱いやすくなります。

レビューの漏れに対応するためには、コードで誤って消されてもリソースの削除をブロックする機能も必要になるでしょう。 コードをリソースに適用した後に、クラウド側でリソースに変更がかかったりすることがあるのを考慮すると、リソースの変更差分を無視する機能も必要になるでしょう。

IaC に最低限求めること

IaC でやりたいことから、IaC に最低限求めることがわかります。

  • コード = リソース状態になることを期待します
    • コードを追加したときに、リソースでも追加されることを期待します
    • コードを変更したときに、リソースでも変更されることを期待します
    • コードを削除したときに、リソースは削除されることを期待します
    • コードで誤って削除がかかる変更を記述したときに、リソースの削除をブロックする記述ができることを期待します。(protection)
    • コードの適用時に、リソースで起こった変更を無視したりコードの変更を無視する記述ができることを期待します。(ignore)
  • コードの適用前に、実行プランを表示して実リソースとの差分を正確に示せることを期待します
    • 実行プランは、「現在のリソース状態とコードの差分」を示せることを期待します。(ステートとの差分では実リソースとずれることが多くある)
  • ステートは、オブジェクトストレージやIaCのサービスで保持できることを期待します
    • 実行ごとに透過的にロックがかかるとよいでしょう

IaC でコードとリソースの状態一致が保証できないとどうなるのか

やりたいことや求めることは分かりましたが、それは本当に必要なのでしょうか? IaC でコードとリソースの一致を保証できないと何が起こるのかを考えてみましょう。

題材は、冒頭にあげたAzure の Bicep です。

ARM Template と Bicep について軽く説明しましょう。ARM TemplateBicep は、Azureにおいて IaC の位置づけにあります。Bicep は ARM Template のための DSL という位置づけでビルドすると ARM Template (JSON定義) を出力します。

Bicep でコードを記述し、Azure にデプロイしたときに次の挙動をします。

  1. Bicepコードをビルドして ARM Template を出力
  2. Azure に ARM Template をデプロイ
  3. Azure は ARM Template に基づいてリソースの作成、既存のリソースがある場合は変更を実施
  4. Azure へデプロイを Complete モードで実施した場合は、Azure のリソースが削除対象か見て削除対象ならリソースを削除、削除対象じゃない場合は ARM Template からは消すがリソースは維持

一連の流れの中で特徴的なのが、Step3 と Step4 です。

ステートを持たない

Step3 は、Bicep/ARM Template がステートを持たない IaC であり、展開されたARM Template に基づいてリソースを更新することを示します。 このためか、Bicep でコードとリソースの実行プランには6つのステータスがあります。4 ステータスで注目すべきは デプロイ という適用まで挙動が不定なケースが含まれていることです。

  • デプロイ: リソースは存在し、Bicep ファイル内で定義されています。 リソースは再デプロイされます。 リソースのプロパティは、変更される場合と変更されない場合があります。 いずれかのプロパティが変更されるかどうか判断するのに十分な情報がない場合、操作ではこの変更の種類が返されます。 この状況は、ResultFormat が ResourceIdOnly に設定されている場合にのみ表示されます

ARM Template は、ステートではなく変更する予定の定義になります。定義と実リソースでも差分が取得できるか不明というのは、実行プランが信用できないということであり残念な体験に思います。

コードとリソースは一致が保証されていない

Step4 は、Bicep/ARM Template は、その定義を消してもリソースが削除される保証はないことを示します。 これは 削除のステータス説明にも明記されています。

  • 削除: この変更の種類は、JSON テンプレートのデプロイに完全モードを使用する場合にのみ適用されます。 リソースは存在しますが、Bicep ファイル内では定義されていません。 完全モードでは、リソースが削除されます。 この変更の種類には、完全モードの削除をサポートしているリソースのみが含まれます

BicepのデプロイにはIncrement と Complete の2モードがあります。 Increment は増分デプロイで、Bicep コードから定義を消してもリソースは残ります。 Complete は 完全デプロイで、ARM Template とリソースの同期のため変更や削除をします。コードとリソースの一致を考えると Complete モード一択ですが、コードを削除してもリソースが消えるかはリソース次第です。

Bicep でIaC を行おうとしてもコードとリソースの一致保障がなく、コードとリソースの乖離が進みます。 コードを見てもリソースの状態は保証できないため、今の状態、適用した後の状態を正確に把握するには Azure ポータルやCLIで状態を取得するしかありません。IaC とは?

残念ながら、Bicep や ARM Template は IaC に最低限求めることができず、継続的に IaC をすることはできないと判断しPulumi に移行したのでした。 Pulumi に移行したのは、Azure のネイティブ実装がありAzure の新規API公開から最速1日で対応するなどの高速な対応と Terraform に比べた時のAzure API の網羅性からです。

まとめ

リソースを作る、変更する、削除するをすべてコードで行うには、TerraformPulumiCDK などを採用します。いずれも、現在のリソースの状態とコードの差分を常に管理でき、正確な実行プランを持ち、ステートを安全に維持できます。

リソースを作ることに注力し、変更や削除はコンソールでいいという場合、ARM TemplateBicep が候補に入ります。しかし、作りっぱなしでメンテナンスされなくなるコードは、限定的なシーンでしか活用できない上に負債返却の機会を失うため、採用には慎重です。(再現環境を作ったりするのにはいいんですけどね!)

IaC を選ぶ前に、それが自分の期待する IaC かは考えたいと思う日々でした。

おまけ

書こう書こうと思って、ずっと書いていなかったので、IaC を使うにあたっての注意をいくつかおまけにどうぞ。

IaC やクラウドによる挙動の違い

IaC実行時の挙動は、IaCの実装やクラウド側のAPIにより異なります。同じIaC でもクラウドによって挙動が変わることがあるのは、マルチクラウドをしたことがある人なら経験があるのではないでしょうか。

  • リソースの設定がコード上でリソースのインラインで設定するのではなく別リソース定義を作る場合、コード削除時のリソース状態は IaC によって変わります。コード未設定時の値戻らず、設定されたときの状態が残るケースが散見されます。この場合、コードとリソースが不一致になるため使いにくいと感じるでしょう
  • 連続でIaC を実行すると、前回の実行が残っていて後から実行してね、と言われれる IaC とクラウドの組み合わせがあったりします

IaC とコンソールやCLIの違い

クラウドコンソールや CLI でリソースを作ると、クラウド側で設定していないことまで設定してくれます。

一方で、IaC はコードで設定したことしか設定しない (デフォルト値はある) ということ多いです。

これにより、IaC で作ったときはコンソールやCLI より手間がかかるという面もありますが、コードとリソースの状態が一致する要素になっていると感じます。

IaC 実行はローカルかリモートか

IaC を実行したときに、ローカル実行されるか、IaCのサービスでリモート実行されるかは、 IaC や連携方法によって変わります。 例えば Terraform は Terraform Cloud を使うとリモート実行することもできますし、Terraform Cloud には ステートだけ保持してローカル実行をすることもできます。


  1. リソース = クラウド環境における設定対象とします。
  2. ステート = 前回IaCを適用した状態とします。
  3. クラウドによる面があるのは同意するところです。AWS や GCP の場合ほぼコンソール操作がほぼなくなりますが、Azure では何かとポータルを使う機会があります。
  4. 6つもある理由は不明ですが、ステートがないことに由来すると考えています。多くの IaC のステータスは 4つ (create、update、delete、ignore) なのを考えると多いですね。