google cloudのラベルとAWSのタグに関する制約

Google Cloudにおいてリソースへのメタデータとして利用できるラベルという機能がある。

ラベルを使用してリソースを整理する  |  Compute Engine Documentation  |  Google Cloud

これはAWSタグ機能とほぼ同等の機能を提供する。

docs.aws.amazon.com

Google Cloudの場合

Google Cloudにおけるラベルの要件は以下の通り

  • 各リソースには、最大 64 個のラベルを設定できます。
  • ラベルは、Key-Value ペアでなければなりません。
  • キーは 1 文字以上、63 文字までにする必要があります。空にすることはできません。値は 63 文字以下にします。空にすることもできます。
  • キーと値には、小文字、数字、アンダースコア、ダッシュのみを使用できます。すべての文字は UTF-8エンコードする必要があります。国際文字も使用できます。キーは、小文字または国際文字で始める必要があります。
  • ラベルのキー部分は、単一のリソース内では一意である必要があります。ただし、複数のリソースで同じキーを使用できます。

利用できる記号が制限されていること、大文字が使えないこと、文字数が63文字までなところはだいぶ辛い。 特に記号の制限が辛い。terraformでリソース構築するときに構築に利用したterraformのコードのリポジトリパスをラベルとして付与したかった。パスを指定しようとすると63文字も注意が必要になる。

AWSの場合

AWSタグ機能は全リソースで共通仕様であるかは保証されておらず、詳細な仕様は各サービスのドキュメントを参照してねとある。 AWSは汎用的な機能がサービスごとに異なるケースが多々あり、だいぶあやしい。

AWS のサービス が提供するタグ付け機能の詳細については、ドキュメントAWS インデックス のサービスのドキュメントを参照してください。

ただし、実際のところはほとんどのサービスでタグ仕様は同一のようだった。 EC2のタグ制限は以下の通り。

  • リソースあたりのタグの最大数 - 50 件
  • タグキーは、リソースごとにそれぞれ一意である必要があります。また、各タグキーに設定できる値は 1 つのみです。
  • キーの最大長 - UTF-8 の 128 Unicode 文字
  • 値の最大長 - UTF-8 の 256 Unicode 文字
  • 使用できる文字
    • EC2 ではタグ内に任意の文字を使用できますが、他の AWS のサービスでは制限があります。すべての AWS のサービスで使用できる文字は、UTF-8 で表現できる英字 (a-z、A-Z)、数字 (0-9)、スペース、および + - = . _ : / @ です。
    • インスタンスメタデータインスタンスタグを有効にすると、インスタンスタグキーは文字 (a-z、A-Z)、数字 (0-9)、および次の文字のみを使用できます: + - = . , _ : @。インスタンスタグキーは、スペースまたは / を含めることはできず、. (1 つのピリオド)、.. (2 つのピリオド)、または _index のみを含めることはできません。詳細については、インスタンスメタデータを使用して EC2 インスタンスのタグを表示するを参照してください。
  • タグのキーと値は大文字と小文字が区別されます。
  • aws: プレフィックスAWS 用に限定されています。タグにこのプレフィックスが付いたタグキーがある場合、タグのキーまたは値を編集、削除することはできません。aws: プレフィックスを持つタグは、リソースあたりのタグ数の制限時には計算されません。

インスタンスメタデータインスタンスタグを有効にすると利用可能な機能に制限が掛かることは知らなかった。 このとき付与したタグが破棄されるので、メタデータでのみ読み取れないのか、別の文字で置換されるのかはドキュメントからは読み取れなかった。

permadiff: 何度applyしても消えない差分

terraformを使っていたら誰もが意図しない差分と格闘したことがあるはず。 その中でも特に何度applyしても差分が消えず残り続けるものがある。このようなdiffのことを permadiff と呼ぶ。

googlecloudplatform.github.io

permadiffについての解説はMagic ModulesというGoogle Cloud向けproviderのコード生成器におけるドキュメントで取り上げられている。このためどちらかというとGoogle系のエコシステムの中で用いられる用語かもしれない。 実際、terraform-provider-google ではpermadiffの単語は数多く登場するが、terraform-provider-awsではたまに用いられる程度である。

とはいえ、AWS向けかGoogle Cloud向けかに限らず、よく知られる概念でありterraformを使っていると多くの人が遭遇すると思われる。なのでこういったパターンをきちんと定義することは非常に有効に感じた。

具体例

terraform-provider-google v5.44.2のリリースノートにpermadiffという単語が記載されており初めて知った。

google_container_node_poolnode_config.0.kubelet_config の算出ロジックに不具合があり、kubelet_configの値を未指定にするとエラーになるというもの。 具体的には以下のコードをapplyした上で node_config.0.kubelet_config を削除するとapply エラーになる。

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "5.44.1"
    }
  }
}


resource "google_service_account" "default" {
  account_id   = "service-account-id"
  display_name = "Service Account"
}

resource "google_container_cluster" "primary" {
  name     = "my-gke-cluster"
  location = "asia-northeast1"

  remove_default_node_pool = true
  initial_node_count       = 1
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "my-node-pool"
  location   = "asia-northeast1"
  cluster    = google_container_cluster.primary.name
  node_count = 1

  node_config {
    preemptible  = true
    machine_type = "e2-medium"

    service_account = google_service_account.default.email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]

    kubelet_config {
      cpu_manager_policy                     = "static"
      insecure_kubelet_readonly_port_enabled = "TRUE"
    }
  }
}

plan実行時にdiffが出て、applyは正常終了するけどdiffは解消しない、というケースを想定していたので このようにapplyエラーになるケースでもエラーの原因によってはpermadiffと呼ぶんだなという学びがあった。

もっとわかりやすい例は Cloud Runの metadata.annotations にlaunch-stageを指定する例。 Cloud Runにて preview featureを利用したい場合は metadata.annotations"run.googleapis.com/launch-stage": "BETA" の値を指定する必要がある。

run.googleapis.com/launch-stage sets the launch stage when a preview feature is used.

registry.terraform.io

Cloud Run のトラブルシューティング  |  Cloud Run Documentation  |  Google Cloud

resource "google_cloud_run_service" "default" {
  name     = "cloudrun-srv"
  location = "asia-northeast1"

  template {
    spec {
      containers {
        image = "gcr.io/cloudrun/hello"
      }
    }
  }

  metadata {
    annotations = {
      "run.googleapis.com/launch-stage" = "BETA"
    }
  }
}

ただし、preview featureを利用していない場合はmetadata.annotations に "run.googleapis.com/launch-stage": "BETA" を指定してもGoogle Cloud側で自動的に除去されてしまう。このため、次にplanを実行するときにはまた差分として出力されてしまう。
(この挙動は GitHubのコメント にある程度で、公式な資料としては見付けられなかった

  # google_cloud_run_service.default will be updated in-place
  ~ resource "google_cloud_run_service" "default" {
        id                         = "xxxxx"
        name                       = "cloudrun-srv"
        # (4 unchanged attributes hidden)

      ~ metadata {
          ~ annotations           = {
              ~ "run.googleapis.com/launch-stage" = null -> "BETA"
            }
          ~ effective_annotations = {
              + "run.googleapis.com/launch-stage"   = "BETA"
                # (6 unchanged elements hidden)
            }
            # (8 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

preview featureを利用するときのみ metadata.annotations に "run.googleapis.com/launch-stage": "BETA" を指定し、利用しない場合は指定を解除する必要がある。

pdfファイルのサムネイルが表示されない

Ubuntu 24.04のファイルマネージャーNautilusにてpdfファイルのサムネイルとして、1枚目の内容ではなく汎用のpdfアイコンが表示されて困った。 特に電子書籍を大量に所有しているのでサムネイルとして表紙を描画して欲しい。

サムネイル描画のためだけに追加の設定等は不要なはず?と思ったらどうやらキャッシュの影響らしい。

https://www.reddit.com/r/Ubuntu/comments/1cf3vgz/there_is_no_thumbnail_of_the_first_page_for_pdf/

rm -r ~/.cache/thumbnails/* を実行してキャッシュを削除した上でF5ボタンで表示を更新したところ、サムネイルが描画されるようになった。

CloudflareのAPIトークンにおいて複数ゾーンリソースを管理する

TerraformにてCloudflareのゾーンリソースを管理したい。 特に単一のゾーンではなく複数のゾーンを扱いたい。

Terraformのcloudflare providerには cloudflare_zoneリソースがあるのでこれを利用すればよい。 Cloudflareとの認証にはAPI_TOKENを利用すればよい。

developers.cloudflare.com

単一のゾーンを扱うときは上記操作で問題なかったが、2つ目のゾーンを追加したところ認証エラーが発生するようになった。

$ terraform plan
...
╷
│ Error: error finding Zone "xxxxxxxxxxxxxxxxxxxxxxx": Unauthorized to access requested resource (9109)
│ 
│ 
╵
Operation failed: failed running terraform plan (exit 1)

原因はAPIトークンの有効範囲として1つ目のゾーンに制限していたこと。 これはデフォルトの設定でこのようになっているのか自分が制限したのかは定かではないが、APIトークンの対象ゾーンリソースとして特定のゾーンのみが選択されていた。

このリソースの範囲をすべてのゾーンに変更すると正常にterraformがAPI経由でリソースにアクセスできるようになった。

kubernetesクラスタにおけるetcdコンポーネントの動作検証

Kubernetesクラスタにおけるデータストアとしてetcdがある。 Kubernets The Hard Wayでetcdのセットアップを行ったので、その設定詳細について確認する。

また、Kubernetesにおける etcdクラスタについては以下のドキュメントが参考になる。

kubernetes.io

ユニットファイル

etcdの起動はsystemdで管理しており、そのunitファイルがリポジトリに格納されている これらの内容は以下の通り。詳細なオプション説明については以下が参考になる。

etcd.io

ところで、etcd v3.4までTo start etcd automatically using custom settings at startup in Linux, using a systemd unit is highly recommended.と説明記載あったが、v3.5からは削除されたことが気になる。 また、各種オプションの説明についてもv3.4までの方がより詳細な説明が記載されており、v3.5は情報量が落ちている。これらの説明がv3.5では他に移動したのか、意図して削除したのか気になる。基本的には利用バージョンのドキュメントを参照しつつも、自分のように新しく学ぶ段階ではv3.4のドキュメントも参照するとよさそう。

name
etcdメンバを識別するためのHuman-readableな名前。Kubernetes The Hard Wayではcontrollerと設定されている。 このオプションはinitial-clusterオプションで利用する。

特に複数のetcdメンバーから構成されるetcdクラスタを構築するときに重要になる。 etcdクラスタの構築方法がstatic bootstrappingであってもdiscovery bootstrappingであっても、クラスタ内でユニークな名前となるように設定した方がよさそう。シングルメンバetcdクラスタの場合はおそらくnameは重要ではない。

initial-advertise-peer-urls
etcdがクラスタに接続するときに利用するURL。Kubernetes The Hard Wayではシングルノードetcdで動作するので利用しない。 プロダクションレディなKubernetesではコントロールプレーンの全ノードに対するドメイン名になるのだろうか。

listen-peer-urls
etcdクラスタの接続を許可するURL。Kubernetes The Hard Wayではシングルノードetcdで動作するので利用しないし、 http://127.0.0.1:2380 の設定なので他ノードから接続できない。

initial-advertise-peer-urls とは異なり、どのアドレスでリッスンするかを指定するので、ドメイン名は指定できない。全許可なら http://0.0.0.0:2380 のような設定になるし、コントロールプレーン用のネットワークが 192.168.128.0/24 だったりしたら http://192.168.128.0:2480 のような設定になるはず? v3.4までのドキュメントには invalid exampleやv3.5以降より具体的な説明が記載されていたのでわかりやすかった。

listen-client-urls
etcdクライアントが接続するURL。Kubernetesにおいてetcdのclientとは kube-apiserverのことで、kube-apiserver以外のコンポーネントのユニットファイルにはetcdの接続URLは記載されていないし、Kubernetesコンポーネント図にはetcdとkube-apiserverのみ接続線が図示されている。

Kubernetes The Hard Wayの設定においては http://127.0.0.1:2379 が指定されているので、コントロールプレーンノードにおいてetcdとkube-apiserverが必ず動作し、kube-apiserverはローカルetcdに接続することが想定されていると理解できる。

advertise-client-urls
etcdクライアント(kube-apiserver)がetcdクラスタに接続するときに利用するURL。Kubernetes The Hard Wayにおいては http://127.0.0.1:2379の設定になっており、kube-apiserverのローカルでetcdが動作すればよいという設計になっていることがわかる。

listen-client-urlsadvertise-client-urlsの関係は listen-peer-urlsinitial-advertise-peer-urlsの関係に等しく、etcdがどのURLで接続許可するかとkube-apiserverがどのURLで接続するのかを設定できる。ただ、Kubernetesにおいて、etcdとkube-apiserverが同居しないコントロールプレーンの設計が存在しうるのか気になるところ。

initial-cluster-token
etcdクラスタを識別するためのトークン。Kubernetes The Hard Wayでは複数のクラスタを同時に構築することは想定していないので etcd-cluster-0 で固定。 複数クラスタを同じ設定で構築する場合もinitial-cluster-tokenはそれぞれユニークな値を指定することで、クラスタを識別できるようになるしetcdメンバが混在すること防ぐことができる。

initial-cluster
etcdクラスタにおける初期メンバ一覧。Kubernetes The Hard Wayにおいてはシングルメンバのetcdクラスタなのであまり意味はない。

マルチメンバかつstatic bootstrappingの場合は事前に全メンバがわかっているので、メンバの名前と接続用のURLを記載できる。 ドキュメントには --initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380のような記載例が提示されておりイメージしやすい。

etcdクラスタメンバが動的に構成される場合はこのようにメンバのIP一覧を事前に列挙することはできないので、Discoveryの仕組みを利用する。

initial-cluster-state
etcdクラスタの初期構築(new)か、既存のetcdクラスタへの追加(existing)なのかを指定する。

もし間違った値を指定した場合もetcdクラスタを壊さず安全に停止してくれるらしいが、この値の指定がどのような意味を持つのかよくわかっていない。とりあえず既存クラスタにjoinするように動作し、joinできなかったら新規クラスタを構築するような動作はできないのだろうか。

バックアップ・リストア

Kuberneteのetcdクラスタ運用において、バックアッププランはちゃんと作成しておけよと注意書きがあるのでここだけ検証しておく。

Kubernetes The Hard Wayでは --data-dir=/var/lib/etcd を指定しているのでこのディレクトリをバックアップしてもよい(volume snapshot)し、etcdctl snapshot save snapshot.dbのコマンドでスナップショットを作成してもよい(built-in snapshot)。
Kubernetesのドキュメントには --endpointの指定もあるが、これは指定するなら --endpints=http://127.0.0.1:2379 のようになる。これがデフォルト値なので変更なしなら指定しなくてよい。運用系の操作なので2380ポートかと思ったが、etcdctlからの操作はクライアント接続でありgRPCプロトコルなので2379ポートらしい。また、クライアント接続なので --listen-client-urlsで指定したURLからでないと接続できない。

root@khw-server:~# etcdctl snapshot save snapshot.db
{"level":"info","ts":1726459074.9597218,"caller":"snapshot/v3_snapshot.go:119","msg":"created temporary db file","path":"snapshot.db.part"}
{"level":"info","ts":"2024-09-16T12:57:54.988563+0900","caller":"clientv3/maintenance.go:200","msg":"opened snapshot stream; downloading"}
{"level":"info","ts":1726459074.99662,"caller":"snapshot/v3_snapshot.go:127","msg":"fetching snapshot","endpoint":"127.0.0.1:2379"}
{"level":"info","ts":"2024-09-16T12:57:55.494366+0900","caller":"clientv3/maintenance.go:208","msg":"completed snapshot read; closing"}
{"level":"info","ts":1726459075.562186,"caller":"snapshot/v3_snapshot.go:142","msg":"fetched snapshot","endpoint":"127.0.0.1:2379","size":"1.2 MB","took":0.594269111}
{"level":"info","ts":1726459075.573733,"caller":"snapshot/v3_snapshot.go:152","msg":"saved","path":"snapshot.db"}
Snapshot saved at snapshot.db
root@khw-server:~# etcdctl  --write-out=table snapshot status snapshot.db
+----------+----------+------------+------------+
|   HASH   | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 45051425 |   163644 |        927 |     1.2 MB |
+----------+----------+------------+------------+

このファイルを別サーバに転送してリストアする。 etcdctlのスナップショットリストアはetcdメンバにバックアップデータを投入するのではなく、データディレクトリに展開する役目を持つ。このため、リストアする際はetcdメンバを停止させ、データディレクトリを空にした上で実行する。その上でetcdを起動すればリストアできる。

$ etcdctl --data-dir /tmp/etcd/ snapshot restore snapshot.db
Deprecated: Use `etcdutl snapshot restore` instead.

2024-09-16T13:18:39+09:00   info    snapshot/v3_snapshot.go:265 restoring snapshot  {"path": "snapshot.db", "wal-dir": "/tmp/etcd/member/wal", "data-dir": "/tmp/etcd/", "snap-dir": "/tmp/etcd/member/snap", "initial-memory-map-size": 0}
2024-09-16T13:18:39+09:00   info    membership/store.go:141 Trimming membership information from the backend...
2024-09-16T13:18:39+09:00   info    membership/cluster.go:421   added member    {"cluster-id": "cdf818194e3a8c32", "local-member-id": "0", "added-peer-id": "8e9e05c52164694d", "added-peer-peer-urls": ["http://localhost:2380"]}
2024-09-16T13:18:39+09:00   info    snapshot/v3_snapshot.go:293 restored snapshot   {"path": "snapshot.db", "wal-dir": "/tmp/etcd/member/wal", "data-dir": "/tmp/etcd/", "snap-dir": "/tmp/etcd/member/snap", "initial-memory-map-size": 0}

# etcdを別ターミナルで起動する

$ etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|
00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
00000040  3a 76 31 3a 6b 65 79 31  3a b8 f6 3b 4f 4d 76 b4  |:v1:key1:..;OMv.|
00000050  ea 1d 0f 0f c4 f5 0d ad  19 bb 4e ef 97 bd 35 df  |..........N...5.|
00000060  50 ef db a6 23 29 58 f1  5a 73 d7 b9 83 a3 e0 b1  |P...#)X.Zs......|
00000070  a0 e4 24 3c 7d 5b ab de  5d 62 a6 e1 59 48 84 f2  |..$<}[..]b..YH..|
00000080  40 58 1f ec 5a 9b e2 a5  15 c8 58 b3 78 36 2f eb  |@X..Z.....X.x6/.|
00000090  90 0e 31 86 53 c5 1d 17  78 43 4d 10 69 55 61 42  |..1.S...xCM.iUaB|
000000a0  df 5c 76 70 64 3f 47 db  09 14 a4 bb ca 7e 26 0e  |.\vpd?G......~&.|
000000b0  c7 2b c6 97 54 68 4d 35  54 11 96 38 25 81 d6 98  |.+..ThM5T..8%...|
000000c0  6a 46 77 0b e6 12 32 db  cf 84 98 11 01 5b cf 49  |jFw...2......[.I|
000000d0  7a 75 92 48 54 57 e0 6b  14 9f 48 05 2a 3b d0 23  |zu.HTW.k..H.*;.#|
000000e0  ed a0 09 56 37 0b fa 48  5c 87 99 bd 34 e3 41 e0  |...V7..H\...4.A.|
000000f0  76 29 87 35 eb 8c fc 09  ff e8 ad 8e ac 6e 57 c5  |v).5.........nW.|
00000100  7b 3e cf ea 93 31 c5 4d  48 88 b9 d8 b3 7f 5c 85  |{>...1.MH.....\.|
00000110  a1 97 a6 0b 65 52 3f b1  35 4b 26 eb b9 de 3d 42  |....eR?.5K&...=B|
00000120  73 e3 93 c8 74 08 37 7b  74 6b bf 91 e8 8e 21 1a  |s...t.7{tk....!.|
00000130  8d 76 33 2c 15 13 76 cf  2f 1e 38 24 eb 91 9a dc  |.v3,..v./.8$....|
00000140  98 0f a8 5d 53 36 83 e4  58 22 9f 82 8c 2b 2e 3e  |...]S6..X"...+.>|
00000150  3c 79 98 ab 12 d2 44 19  fb 0a                    |<y....D...|
0000015a

envsubstで環境変数をテンプレートに展開する

Kubernetes The Hard Way を進めていたら、見知らぬ envsubstコマンドに遭遇した。

$ export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
$ envsubst < configs/encryption-config.yaml \
  > encryption-config.yaml

これは環境変数を展開して埋め込むコマンド。manによると substitutes environment variables in shell format stringsと説明がある。 上記の例ではconfigs/encryption-cnofig.yaml環境変数 ENCRYPTION_KEY を参照しており、これを環境変数の値で埋めて encryption-config.yamlとして生成し直している。 このようにシェルで簡単にテンプレートの仕組みが実現できる。

ちなみに、envsubstはDebian系では gettext-baseパッケージに含まれており、大抵の場合は最初からインストールされている。

$ dpkg -S /usr/bin/envsubst 
gettext-base: /usr/bin/envsubst

packages.debian.org

参考:

qiita.com

LinuxのWeb管理コンソール: Cockpit

前回記事の検証のため、Fedorasshしたところ見慣れないメッセージとしてwebコンソールの案内が表示された。

$ ssh 192.168.10.124
Web console: https://localhost:9090/ or https://192.168.10.124:9090/
...

実際にこのURLにアクセスしたところ、該当サーバのWeb管理コンソールが表示された。

確認したところ、これはCockpitというソフトウェアでFedoraRHEL系ではデフォルトで有効になっている。 また、DebianUbuntuもサポートされておりインストール可能。

cockpit-project.org

そういえば以前CoreOSなどのコンテナOSに対する管理GUIとして登場した記憶がある。 最近RHEL系を利用していなかったのでCockpitの存在やデフォルトで有効なことを知らなかった。