zaki work log

作業ログやら生活ログやらなんやら

オフラインK3sクラスタでworkerノード追加

少し前に実施したシングルノード構成の「オフライン環境へのK3sインストール」に、workerノードを追加する。
環境は引き続きインターネット接続のないオフライン環境。

zaki-hmkc.hatenablog.com

手順はAir-Gap Installの通りで大丈夫。

docs.k3s.io

構成 (再掲)

  • ホストOS: Ubuntu 22.04.03 LTS
  • K3s: v1.29.2
  • プライベートコンテナレジストリ: ローカルのGitLab使用
  • ネットワークはcontrol planeと同じサブネットにいて、control planeともプライベートレジストリとも直接疎通がある構成 (インターネットには接続できない)

root@k3s-offline2:~# ip r
default via 172.29.0.1 dev eth0 proto static 
172.29.0.0/24 dev eth0 proto kernel scope link src 172.29.0.78

control planeは以下

root@k3s-offline1:~# ip r
default via 172.29.0.1 dev eth0 proto static 
10.42.0.0/24 dev cni0 proto kernel scope link src 10.42.0.1 
172.29.0.0/24 dev eth0 proto kernel scope link src 172.29.0.77 

イメージの準備

これはcontrol planeを構築したときのものをそのまま使用。

プライベートレジストリのリダイレクト設定

これはcontrol planeノード同様に各ノードで実施。
以下の内容の/etc/rancher/k3s/registries.yamlを作成する。

mirrors:
  docker.io:
    endpoint:
      - "https://gitlab.example.jp:25000"
    rewrite:
      "^rancher/(.*)": "zaki/images/$1"
configs:
  "gitlab.example.jp:25000":
    tls:
      insecure_skip_verify: true

インストールスクリプトの配置

これもcontrol plane構築のものをそのまま使用。
構築対象ノードのどこかに配置しておく。
workerの台数が多い場合はIPリーチするwebサーバーを立てるのが楽かもしれない。(が、今回はローカル配置で実施)

https://get.k3s.io

実行バイナリの配置

同じくcontrol plane構築の(ry
追加するノードの/usr/local/bin/k3sへ配置。

zaki@k3s-offline2:~$ sudo cp k3s /usr/local/bin/k3s 
zaki@k3s-offline2:~$ sudo chmod 755 /usr/local/bin/k3s
zaki@k3s-offline2:~$ k3s --version
k3s version v1.29.2+k3s1 (86f10213)
go version go1.21.7

トークン確認

クラスターへ参加するためのトークンをcontrol planeノードで確認。
ここはオンラインの場合の手順と同様。

root@k3s-offline1:~# cat /var/lib/rancher/k3s/server/token

内容をworkerノードで環境変数あたりにセットしておく。

インストール

イメージ、バイナリ、インストールスクリプトトークンの準備ができたら、以下のオプションを付与してインストールスクリプトを実行する。

  • INSTALL_K3S_SKIP_DOWNLOAD=true (ダウンロード処理をskip / control planeと同様)
  • K3S_URL=control planeのアドレス (構築済みcontrol planeを指定 / オンライン時の手順と同様)
  • K3S_TOKEN=上記で確認したトークン値 (クラスター参加用トークン / オンライン時の手順と同様)
zaki@k3s-offline2:~$ INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://172.29.0.77:6443 K3S_TOKEN=${K3S_TOKEN} ./install.sh 
[INFO]  Skipping k3s download and verify
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO]  systemd: Enabling k3s-agent unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s-agent.service → /etc/systemd/system/k3s-agent.service.
[INFO]  systemd: Starting k3s-agent

比較的すぐ完了し、get nodeでノードが追加されることを確認できる。

root@k3s-offline1:~# kubectl get node
NAME           STATUS   ROLES                  AGE   VERSION
k3s-offline1   Ready    control-plane,master   50d   v1.29.2+k3s1
k3s-offline2   Ready    <none>                 4s    v1.29.2+k3s1

root@k3s-offline1:~# kubectl get pod -A -o wide
NAMESPACE     NAME                                      READY   STATUS      RESTARTS       AGE    IP           NODE           NOMINATED NODE   READINESS GATES
kube-system   helm-install-traefik-crd-xsblz            0/1     Completed   0              50d    <none>       k3s-offline1   <none>           <none>
kube-system   helm-install-traefik-gvq48                0/1     Completed   1              50d    <none>       k3s-offline1   <none>           <none>
kube-system   svclb-sample-http-4cda7872-mdm9q          1/1     Running     6 (25h ago)    50d    10.42.0.58   k3s-offline1   <none>           <none>
kube-system   svclb-traefik-10a1b448-6h6c9              2/2     Running     12 (25h ago)   50d    10.42.0.55   k3s-offline1   <none>           <none>
kube-system   traefik-f4564c4f4-gg6mv                   1/1     Running     6 (25h ago)    50d    10.42.0.62   k3s-offline1   <none>           <none>
kube-system   coredns-6799fbcd5-vvxrq                   1/1     Running     4 (25h ago)    46d    10.42.0.59   k3s-offline1   <none>           <none>
kube-system   local-path-provisioner-6c86858495-qznr7   1/1     Running     12 (25h ago)   50d    10.42.0.61   k3s-offline1   <none>           <none>
kube-system   metrics-server-67c658944b-r655h           1/1     Running     12 (25h ago)   50d    10.42.0.60   k3s-offline1   <none>           <none>
sample-app    sample-http-5cd4944c69-6knfd              1/1     Running     2 (46d ago)    50d    10.42.0.56   k3s-offline1   <none>           <none>
sample-app    sample-http-5cd4944c69-l7xnb              1/1     Running     2 (46d ago)    50d    10.42.0.57   k3s-offline1   <none>           <none>
kube-system   svclb-traefik-10a1b448-8vhz9              2/2     Running     0              4m5s   10.42.1.3    k3s-offline2   <none>           <none>
kube-system   svclb-sample-http-4cda7872-f6jzs          1/1     Running     0              4m5s   10.42.1.2    k3s-offline2   <none>           <none>

「オフライン構築」と「workerノードの追加」をミックスした手順なので、それぞれ実施経験があれば簡単に構築できる。

[rsyslog] タイムゾーンが異なるリモートからのログ受信時にローカルの時刻で記録する (RHEL/Ubuntu)

リモートのrsyslogへのログ転送は、ログ情報としてはタイムゾーンは持っていないため、例えば日本時間4月30日の20時に、JSTに設定されているrsyslogサーバーへ、UTCのシステムからログを送信すると、9時間ズレて11時のログとして記録されてしまう。

手っ取り早い解決策は送信側システムのタイムゾーンを受信側と合わせれば良いけど、OSSミドルウェアなどがタイムゾーン変更でどこに影響が出るかの確認が取れないとか、Distrolessコンテナで動いててタイムゾーンの変更が気軽にできないとか、いろんな事情でタイムゾーンの変更ができない場合もあると思う。
たとえばDistrolessベースでビルドされているFluent Bit集めたPodのログをJSTのrsyslogサーバーへ転送する場合とか、ね。

そんな場合は受信側のrsyslogで、「受信側rsyslogでリモートからのログを受信した時刻で記録する」設定を入れてやれば、「タイムゾーンを変更したくないシステム」には変更を入れずに、rsyslogサーバーのタイムゾーンでログを集約できる。
(正確には時刻情報を受信側で上書きする、という動作)

設定方法

前置きが長くなったけど、設定は以下の通りで、テンプレート機能を使う。

www.rsyslog.com

デフォルトではRSYSLOG_TraditionalFileFormatという書式が使用されており、以下のようにタイムゾーンや年の情報が含まれていない内容になっている。

template(name="RSYSLOG_TraditionalFileFormat" type="string"
     string="%TIMESTAMP% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n")

これを「受信時の時刻を使用する」に変更するには、時刻の書式の%TIMESTAMP%部分を%timegenerated%に変更した内容の書式を別途定義してやる。こんな感じ。
(書式名はdate_localize_TraditionalFileFormat)

template(name="date_localize_TraditionalFileFormat" type="string"
     string="%timegenerated% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n")

定義した時刻更新するフォーマットを使用するように、omfileプラグインの設定を変更する。

-module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat")
+module(load="builtin:omfile" Template="date_localize_TraditionalFileFormat")

これで、タイムゾーンの異なるシステムから送信されるログも、受信時に受信したrsyslogの時刻でログが記録されうようになる。

全体の設定(抜粋)はこんな感じ。(UDP受信設定)

# customize timestamp format
module(load="builtin:omfile" Template="date_localize_TraditionalFileFormat")
template(name="date_localize_TraditionalFileFormat" type="string"
     string="%timegenerated% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n")

module(load="imudp") # needs to be done just once
input(type="imudp" port="514")

環境

Ubuntuの場合

バージョンが古いわけじゃなさそうだけど、$で始まる以前からある書式だとこんな感じ。

$template LocalTimeTraditionalFileFormat,"%timegenerated% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n"
$ActionFileDefaultTemplate LocalTimeTraditionalFileFormat
  • Ubuntu 24.04 LTS
  • 8.2312.0-3ubuntu9

参考

余談

Fluent Bitで送信時にタイムゾーンを弄る方向で最初調べてて、Time_OffsetというパラメタがParserにあるんだけど使い方がわからなかったため、発想を変えて受信側rsyslogの設定でどうにかならないか調べたという流れ。

docs.fluentbit.io

github.com

[Ansible Builder / AWS] CodeCommit + CodeBuild 環境でEEの自動ビルドとECRへpush (起動は手動)

プライベートではGitHub Actions使ったEEの自動ビルドを1年前に試したけど、業務だとCodeCommit環境を使ってたりするので、せっかくなのでAWSのサービスを使ったビルド環境を試してみた。

初めに軽く検索した感じだと、CodeBuildではコンテナで処理がされるという情報を見かけたので、コンテナビルドはどうするのかと思ったけど、普通にCodeBuild内でコンテナビルドも動くようだ。

やりたいことは以下。

  • EEのソースはCodeCommitのものを使用
  • Ansible Builderをインストール
  • Red Hat RegistryからEEとRunnerのベースイメージをpull
  • イメージビルド
  • ビルドが完了したらECRへpush

これをCodeBuildを使って処理する。

CodeCommit

ポイントはビルドの定義を行うbuildspec.ymlファイル。
ほかは基本的には特記事項なし。

Buildspec

buildspec.ymlファイルを作成。
中身はこんな感じ。 この中身に従ってCodeBuildが動作する。 GitHub Actionsにおける.github/workflows/main.ymlみたいな位置づけのファイル。

rh_account,rh_username,rh_passwordについては、あとでSecret Managerで使うキー文字列。

version: 0.2

env:
  secrets-manager:
    USERNAME: rh_account:rh_username
    PASSWORD: rh_account:rh_password

phases:
  pre_build:
    commands:
    - pip install ansible-builder
    - docker login registry.redhat.io -u ${USERNAME} -p ${PASSWORD}
    - aws ecr get-login-password --region ap-northeast-1 | docker login ********.***.ecr.ap-northeast-1.amazonaws.com --username AWS --password-stdin

  build:
    commands:
    - echo "build image"
    - ansible-builder build -t ********.***.ecr.ap-northeast-1.amazonaws.com/sample-repository:develop

  post_build:
    commands:
    - echo "push image to ecr"
    - docker push ********.***.ecr.ap-northeast-1.amazonaws.com/sample-repository:develop

execution-environment.yml

お題はterraformを実行できるEEを作る。 今のところ必要がないので定義ファイルはver1のまま。

version: 1
build_arg_defaults:
  EE_BASE_IMAGE: registry.redhat.io/ansible-automation-platform/ee-minimal-rhel8:2.16.5-1
  EE_BUILDER_IMAGE: registry.redhat.io/ansible-automation-platform/ansible-builder-rhel8:1.2.0-75

ansible_config: 'ansible.cfg'

dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

additional_build_steps:
  prepend:
    - RUN microdnf install -y yum-utils
    - RUN yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
    - RUN dnf -y install terraform

requirements.yml

---
collections:
- community.general

bindep.txt

git

CodeBuild

プロジェクトを作成

プロジェクト名に任意の名前を指定。
ソースプロバイダに「AWS CodeCommit」を指定、リポジトリとブランチを入力する。
(ちなみにGitHubなど外部のリポジトリも選択可能)

環境はデフォルトでOK (というかデフォルト以外の選択を試していない)
「追加設定」をクリックし、必要に応じてパラメタとして使用する環境変数を設定する。
今回は、registry.redhat.ioにログインするための認証情報を外部からセットするため、環境変数でなくSecret Managerを使用する予定。これも環境変数のところで「タイプ」から「Secret Manager」を選択することで使用できる。
Secret Manager側が未設定なので、とりあえず今は何も設定せずに進める。(あとから編集可能)
ほかにはビルドするマシンサイズ(CPU/メモリ)を選択することもできる。デフォルトはRAM3GB/2vCPUs

Buildspecは「buildspecファイルを使用する」を選択。

他はデフォルトで「ビルドプロジェクトを作成する」を押下。

作成完了したら、「サービスロール」の文字列を確認しておく。
(次のSecret Managerからのアクセス許可で使用する)

Secret Managerの作成とアクセス権の設定

registry.redhat.ioにログインするための認証情報を作成する。

「新しいシークレットを保存する」から、「その他のシークレットのタイプ」を選択。
key/value形式のデータを登録できるので、ユーザー名とパスワードそれぞれキーと値を入力。

「暗号化キー」はデフォルトのまま「次」押下。

シークレットの設定で名称を入力し、「リソースのアクセス許可」で、CodeBuildからのアクセスを許可する編集を行う。
「許可を編集」を押下し、"Statement"へ以下を入力する。

{
    "Version":"2012-10-17",
    "Statement": [ {
        "Effect" : "Allow",
        "Principal" : {
            "AWS" : "CodeBuildのサービスロールのARN"
        },
        "Action" : "secretsmanager:GetSecretValue",
        "Resource" : "*"
    } ]
}

「次」で進んだ先はデフォルトでOKなので、そのまま「保存」でSecretを作成する。

※ ちなみに環境変数だと機密情報が「見えてしまう」のでSecret Managerを使おう、という文脈のはずだけど、他のIAMユーザーでもAWSのマネジメントコンソールからSecret Managerに保存している値はどうも参照できるっぽいので、個人に割り当てられてるパスワード情報とかは…どうすればいいんだ?

CodeBuildでSecret Manager値参照の設定

プロジェクトの編集の、環境の追加設定以下にある環境変数パートで、以下入力

名前 タイプ
RH_ACCOUNT (任意) シークレットの名前 (上の例だとrh_account) Secret Manager

これを入力して「プロジェクトを更新する」押下で保存。

CodeBuildからECRのアクセス権の設定

ビルドしたイメージをpushするために、CodeBuildの処理内からECRへのアクセス権を設定する。
具体的にはaws ecr get-login-passwordを使えるようにする設定。 (無いとno identity-based policy allows the ecr:GetAuthorizationToken actionエラー)

まず作成したCodeBuildのプロジェクトのサービスロール(リンクになってる)をクリックし、サービスロールのページへ移動する。

サービスロールの設定では「許可ポリシー」が(デフォルトのポリシーと、↑でセットしたSecret Managerの)2つセットされてるはずで、ここでさらに「許可を追加」->「ポリシーをアタッチ」押下。

検索欄にcontainerregistryを入力して出てくるAmazonEC2ContainerRegistryPowerUserをチェックして「許可を追加」する。

これで準備OK

ビルド

CodeBuildのプロジェクトに戻り、「ビルドを開始」を押下。

問題がなければansible-builderのインストール、各コンテナレジストリへのログイン、ビルドが行われ、最後にイメージをECRへpushするところまで処理される。

ECRでもpushされたイメージを確認できる。

まとめ

現状まだ処理の起動はボタンぽちが必要だけど、それまでAnsible Builderを用意して、コードをGitから持ってきて、ビルドのコマンドラインを入力・tagを設定してpushという手順だったのが、ボタン押下1アクションで全部終わるのはだいぶラクチン。しかもローカルPCでやるより断然速い。(スペックの問題)

Gitのpushやtagの作成に反応させるにはCodeBuildの設定にはなさそうだったので、おそらくCodePipelineを使うと思う。これもそのうちやってみる。

ちなみに一番ハマったのはSecret Managerからの値の取得だった。キーの関係がよくわからず試行錯誤。。


ところで1年前に書いたGitHub ActionsのEEイメージビルド、これも4月16日だったんだねぇ(16日中の公開は間に合わなかったけど笑)

zaki-hmkc.hatenablog.com

参考

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com

dev.classmethod.jp

qiita.com

[Proxmox VE] Cloud-Initのvendorデータを使ってサブスクリプション済みRHELのVMをサクッと作成する

Proxmox VEのWeb UIから設定できるCloud-Init項目は以下の通りで、最低限必要な設定はできるけど、追加パッケージの指定とかがデフォルトでは現状できない。

  • ユーザ
  • パスワード
  • DNSドメイン
  • DNSサーバ
  • SSH公開鍵
  • プロビジョニング時のパッケージアップグレード(apt upgradeやdnf update)の実行有無
  • IPアドレス設定 (固定 / DHCP)

ただしCloud-Initの機能そのものにはもっと様々な初期設定を指定でき、例えばRHELのサブスクリプションはrh_subscriptionで設定可能。

cloudinit.readthedocs.io

今回はこれをProxmox VEから処理するための設定を使って、Proxmox VEでサブスクリプション済みのRHELVMをテンプレートとCloud-Initを使ってプロビジョニングする仕組みを確認。

pve.proxmox.com

RHELクラウドイメージ

カスタマポータルのDownloadから、Red Hat Enterprise Linuxへ進む。
リストから普通のイメージでなく、「Virtualization Images」の「Red Hat Enterprise Linux . KVM Guest Image」をダウンロード。

今回は諸事情により8.9を使用。

カスタムCloud-Initの設定

テンプレート作成部分は先日書いた以下のエントリ参照。

zaki-hmkc.hatenablog.com

テンプレートの作成

テンプレート9010の「rhel8.9-template」を作成。

qm create 9010 --name rhel8.9-template --memory 2048 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci
qm set 9010 --scsi0 S870QVO2TB:0,import-from=/mnt/pve/pecorino-dev/template/iso/rhel-8.9-x86_64-kvm.qcow2
qm set 9010 --ide2 S870QVO2TB:cloudinit
qm set 9010 --boot order=scsi0
qm set 9010 --serial0 socket --vga serial0
qm template 9010

ここでUIで可能なCloud-Initの設定もセットする。

カスタム設定配置用snippetsの設定

カスタム設定ファイルを置くために、ストレージの設定でスニペットを有効にする。今回はNFSのストレージに設定。
(このストレージはテンプレートのストレージと異なっていても問題ない)

有効にするとディレクトリが作成される。

root@pve:~# ls -al /mnt/pve/pecorino-dev/snippets/
total 8
drwxr-xr-x  2 root root 4096 Apr 14 14:27 .
drwxrwxrwx 13 root root 4096 Apr 14 14:27 ..

vendor設定ファイルの作成

認証情報がplaintextのままなのはちょっと気になるところだけど個人環境なのでとりあえず。
サブスクリプション割り当てのためのrh_subscriptionと、追加パッケージのpackagespodmanを初期状態でインストールされるようにしてみる。

root@pve:~# ls -l /mnt/pve/pecorino-dev/snippets/
total 4
-rw-r--r-- 1 root root 99 Apr 14 17:10 rhel_subscription_vendor_template.yaml
root@pve:~# cat /mnt/pve/pecorino-dev/snippets/rhel_subscription_vendor_template.yaml 
#cloud-config
rh_subscription:
  username: ユーザー名を記載
  password: パスワードを記載
  auto-attach: True

packages:
- podman

vendorデータのセット

作成したvendorデータ定義が書かれたYAMLファイルを、テンプレートのCloud-Initへセットする。
ファイルシステム上のパスでなく、ストレージ名とその中でのパスを指定する。

root@pve:~# qm set 9010 --cicustom "vendor=pecorino-dev:snippets/rhel_subscription_vendor_template.yaml"
update VM 9010: -cicustom vendor=pecorino-dev:snippets/rhel_subscription_vendor_template.yaml

これですべての準備が完了。

※ ただし、Web UIのCloud-Initの画面では、vendor設定に関して現バージョンでは確認はできない。

VMのクローン

とりあえずUIのクローンでVM IDとホスト名を入力してVMを作成し、電源投入

この通りSubscription Managementのログが表示され、(Cloud-Initで更新を有効にしてるので)パッケージアップデートも動作しはじめる。
Red Hatのカスタマポータルでも、システムが登録されていることを確認できる。

パッケージアップデートを有効にしていると処理時間の関係で時間は多少かかるが、podmanも自動的にインストールされる。

[zaki@rhel8-dev ~]$ podman --version
podman version 4.6.1

環境

  • PVE 8.1.10
  • RHEL 8.9 (Virtualization Images)

Proxmox VEのCloud-Initは、vendorデータのカスタム設定でWeb UIから設定・確認できない項目も初期設定として使えることが確認できた。
ちなみにuserもファイルから設定できるが、これをやるとWebのUIから設定ができなくなる(qm set <vmid> --cicustomでセットした内容で上書きされる)ので、画面から設定できない項目は基本的にvendorデータを使用するのが良さそう。

参考URL

ainoniwa.net

ainoniwa.net

この2つの記事は大変参考になりました。
コマンドはCustom Cloud-Init COnfigurationに記載がある。が、確かにわかりづらい…

[AWS / Terraform] EC2作成時のユーザーデータ内で動的に割り当てられるIPアドレスやパブリックDNSの参照とIMDSv2の設定

TerraformでEC2プロビジョニング時に、ユーザーデータ内でパブリックDNS名を参照しようとしたらすんなり実装できなかったのでメモ。

AWSでEC2をプロビジョニングする際にホスト上の初期処理を投入したい場合は、実行したいコマンドをシェルスクリプトとして記述できるユーザーデータが便利。 ただし、Terraformなどから利用する場合にテンプレートを使ってパラメタも指定できてさらに便利に使えるけど、EC2をデプロイした結果決まる値を渡すことはできない。たとえばIPアドレスとか。

TerraformのNG実装例

よくある(?)例として、EC2上にデプロイするアプリケーションに自信のホスト名やアドレスを設定したいケースで、テンプレートにpublic_dnsを指定してもエラーになる。

resource "aws_instance" "server" {
  ami                         = var.server_ami_id
  instance_type               = var.server_instance_type
  subnet_id                   = aws_subnet.subnet.id
  associate_public_ip_address = true

[...]

  user_data = templatefile("${path.module}/user_data.sh", {
    public_dns = aws_instance.server.public_dns
  })

上記の定義だと以下のように自身を参照できずにエラーになる。

╷
│ Error: Self-referential block
│ 
│   on online/main.tf line 115, in resource "aws_instance" "server":
│  115:     public_dns = aws_instance.server.public_dns
│ 
│ Configuration for aws_instance.server may not refer to itself.
╵

メタデータの参照

検索すると「ネットワークインタフェースを先に単体で作成・アドレスを決定し、EC2を作成してアタッチ」という方法もあったけど、インフラ構成が変わるのが個人的には微妙だったので、「ユーザーデータ内でメタデータを返すAPIを使用する」で対処。

docs.aws.amazon.com

[ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hibernation/
hostname
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/
system

このように、様々な情報を参照できるエンドポイントが提供されている。

DNS名であればpublic-hostnameIPアドレス(GIP)ならpublic-ipv4という具合。

[ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/public-hostname
ec2-**-**-**-**.ap-northeast-1.compute.amazonaws.com

プライベートIPアドレスも取れる。

[ec2-user@ip-10-1-1-227 ~]$ ip a s eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 06:c9:30:bd:85:55 brd ff:ff:ff:ff:ff:ff
    inet 10.1.1.227/24 brd 10.1.1.255 scope global dynamic noprefixroute eth0
       valid_lft 1931sec preferred_lft 1931sec
    inet6 fe80::4c9:30ff:febd:8555/64 scope link 
       valid_lft forever preferred_lft forever
[ec2-user@ip-10-1-1-227 ~]$ curl http://169.254.169.254/latest/meta-data/local-ipv4
10.1.1.227

このAPIを使えば、テンプレートの引数も使わず、例えばIPアドレスをセットしたい箇所で $(curl http://169.254.169.254/latest/meta-data/public-ipv4) と記述すれば、EC2プロビジョニング時にはEC2のグローバルIPに変換される、という寸法。


と、ここまで書いてシェアしたところ、Xで「IMDSはv2を使うように」とコメントをいただいたので追加調査。 (認証不要のv1の方で動いたのでこれでいいやと思ってブログにしたのが本音w)

コメントありがとうございます!

IMDSv2

上記はIMDS(Instance Metadata Service)はv1の例で、現在はv2が推奨。

docs.aws.amazon.com

IMDSv2を使うにはまず認証トークンを取得する必要がある。

TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 600")

このトークンをヘッダに指定して実行すればOK

[ec2-user@ip-10-1-1-229 ~]$ curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4
52.*.*.*

ここでトークンの期限を600秒に指定しているので、時間が過ぎれば使用不可になる。

[ec2-user@ip-10-1-1-229 ~]$ curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>401 - Unauthorized</title>
 </head>
 <body>
  <h1>401 - Unauthorized</h1>
 </body>
</html>

IMDSv2を必須にするにはEC2の設定で変更でき、v2を必須にするとトークン不要のv1のアクセスが使用不能になる。

[ec2-user@ip-10-1-1-229 ~]$ curl http://169.254.169.254/latest/meta-data/public-ipv4
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>401 - Unauthorized</title>
 </head>
 <body>
  <h1>401 - Unauthorized</h1>
 </body>
</html>

IMDSv2必須設定

設定変更はUIであれば以下の通り。

Terraformの場合はmetadata_optionsで指定可能。

resource "aws_instance" "server" {

  [...]

  metadata_options {
    http_tokens = "required"
  }

まぁ商用だったらEC2デプロイ時に決定するIPアドレスDNS名を使うことはなくて別途レコード作成した自前のDNS名を使うんだろうけど、使い捨ての検証環境とかならこんな感じで十分かな。

discuss.hashicorp.com

[Proxmox VE] テンプレートとクローンとCloud-Initを使ってVMをサクサクシュッと生やす (Fedora / Ubuntu)

Proxmox VEには、VMのテンプレート化とクローン機能があり、さらにCloud-Initを使って初期設定込みで簡単にVMをプロビジョニングできる機能がある。
もうすぐFedora40やUbuntu 24.04がリリースされそうなので、その時にサラサラとテンプレート作成できるように、この機能を使う準備についておさらい。
といっても、ドキュメントの通り順番にコマンド実行していけば作成できる。

pve.proxmox.com

ちなみにここではUbuntuの情報はあふれてるので、ここではFedora 40 betaでお試し。 (Ubuntuの場合も参考程度に記載してる)

テンプレートの作成

まずはCloudイメージを使ったVMを作り、それをテンプレートに変換、という手順。
UIが用意されてるか微妙なので、ドキュメント通りCLIで実行する。

ベースになるCloudイメージの入手

Proxmox VEのテンプレート・クローン・Cloud-Init機能を使うためのベースイメージはクラウドイメージを使用する。
Fedoraはここから入手。

fedoraproject.org

ここからbeta版のFedora Cloud 40 QEMU (qcow2形式)をダウンロードする。(betaの場合は「Show Beta downloads」のボタンで切り替える)

curl -LO https://download.fedoraproject.org/pub/fedora/linux/releases/test/40_Beta/Cloud/x86_64/images/Fedora-Cloud-Base-Generic.x86_64-40-1.10.qcow2

ちなみにUbuntuクラウドイメージはこちらから。

cloud-images.ubuntu.com

Ubuntu 22.04 LTS (Jammy)であれば以下。

curl -LO https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img

試してないけど 24.04 LTSだとこれかな?今はdaily buildっぽいけど。

curl -LO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

これをローカルやNFSストレージなど、PVEで使える場所に配置。

テンプレート元にするVM作成

普通のVMを作るときはIDはだいたい3桁にしてるかもだけど、ドキュメントのサンプルは9000と4桁になってて、実際桁数が異なっているとわかりやすいので、それに倣って4桁IDのテンプレート作成。
手元の環境ではすでにいくつか作成済みなので、ここでは9002を使用。

qm create 9002 --name fedora40beta-template --memory 2048 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci

これで仮想マシン一覧のところに、ID:9002のVMが作成される。

イメージのセット

ダウンロードしたクラウドイメージをローカルストレージ(以下はlocal-lvm)のVMとしてインポート。
import-fromにPVE上で参照できるイメージのパスを指定。

qm set 9002 --scsi0 local-lvm:0,import-from=/mnt/pve/pecorino-dev/template/iso/Fedora-Cloud-Base-Generic.x86_64-40-1.10.qcow2

実行例

root@pve:~# qm set 9002 --scsi0 local-lvm:0,import-from=/mnt/pve/pecorino-dev/template/iso/Fedora-Cloud-Base-Generic.x86_64-40-1.10.qcow2
update VM 9002: -scsi0 local-lvm:0,import-from=/mnt/pve/pecorino-dev/template/iso/Fedora-Cloud-Base-Generic.x86_64-40-1.10.qcow2
  Logical volume "vm-9002-disk-0" created.
transferred 0.0 B of 5.0 GiB (0.00%)
transferred 51.7 MiB of 5.0 GiB (1.01%)
transferred 102.9 MiB of 5.0 GiB (2.01%)
transferred 154.6 MiB of 5.0 GiB (3.02%)
:
:
transferred 5.0 GiB of 5.0 GiB (99.85%)
transferred 5.0 GiB of 5.0 GiB (100.00%)
transferred 5.0 GiB of 5.0 GiB (100.00%)
scsi0: successfully created disk 'local-lvm:vm-9002-disk-0,size=5G'
root@pve:~#

これでVMにディスクがセットされる。

Cloud-Init CD-ROMドライブの追加

Cloud-Initの情報をVM起動時にセットするためのCD-ROMドライブを作成する。

qm set 9002 --ide2 local-lvm:cloudinit

実行例

root@pve:~# qm set 9002 --ide2 local-lvm:cloudinit
update VM 9002: -ide2 local-lvm:cloudinit
  Logical volume "vm-9002-cloudinit" created.
ide2: successfully created disk 'local-lvm:vm-9002-cloudinit,media=cdrom'
generating cloud-init ISO
root@pve:~# 

これで「CloudInitデバイス」が追加される。

さらに、追加したイメージ(ディスク)からブートするように、ブート順を変更。

qm set 9002 --boot order=scsi0

実行例

root@pve:~# qm set 9002 --boot order=scsi0
update VM 9002: -boot order=scsi0
root@pve:~# 

シリアルコンソールの設定を追加。

qm set 9002 --serial0 socket --vga serial0

実行例

root@pve:~# qm set 9002 --serial0 socket --vga serial0
update VM 9002: -serial0 socket -vga serial0
root@pve:~# 

テンプレートへの変換

最後にテンプレートへ変換。

qm template 9002

実行例

root@pve:~# qm template 9002
  Renamed "vm-9002-disk-0" to "base-9002-disk-0" in volume group "pve"
  Logical volume pve/base-9002-disk-0 changed.
  WARNING: Combining activation change with other commands is not advised.
root@pve:~# 

warning出てるけどいつも出るのでたぶん無視していい。(本当?) 変換が完了すると、「仮想マシン一覧」のアイコンが変わりテンプレートになる。

これでいつでもクローン機能を使ってVMをポコポコ生やす準備が「ほぼ」完了。

テンプレートの設定

ベースになるイメージの設定によるけど、CPUやメモリ、ストレージなどがデフォルト値のままで、特にストレージは全然足りない設定になってることが多いので、自分が使いたい初期値に更新する。

ここからはWeb UIで実施

VM設定

VMの設定は基本的にUIでVMを新規作成するときに指定するものと同じで、主に以下の項目。「ハードウェア」から更新する。
(ディスクは「ディスクの動作」->「リサイズ」で増分を指定。減らすのは少なくともUIは無理そう?)

  • メモリ
  • プロセッサ(cpu数/種別)
  • ディスクサイズ

Fedora 40 Betaだと、クラウドイメージからVMを作るとメモリが2GB、CPUが1socket/1core、ストレージが5GBになってるので、欲しいスペックの値に更新する。
この値は「クローン」をした時の初期値になるため、「VMをサクッと作ったときに設定されていたい値」をセットする。
クローン後にも設定できるので、「今回はパワーあるVMが欲しい」みたいな個別設定は、クローンのあとに設定すればよい。

特にRHEL系v9のCPU種別で「x86-64-v2-AES」でないとエラーになるパターンがあるので、CPU種別は設定しておくのと、デフォルトのストレージサイズは結構少ないので、20GBとか検証その他で困らないサイズを指定しておくのが良い。
(イメージの種類によってはデフォルト値のままだと、ブート中にディスクフルになったりするサイズだったりする笑)

他には「オプション」の「QEMU Guest Agent」とかの有効化ね。

Cloud-Init設定

今回のキモがここで、VMをプロビジョニングした時点でユーザーアカウントやネットワーク設定など簡単なOS設定が完了した状態にする、パブリッククラウドのコンピューティングリソースでおなじみの機能を設定。
Proxmox VEのWeb UIでは以下の項目を設定できる。

  • ユーザ
  • パスワード
  • DNSドメイン
  • DNSサーバ
  • SSH公開鍵
  • プロビジョニング時のパッケージアップグレード(apt upgradeやdnf update)の実行有無
  • IPアドレス設定 (固定 / DHCP)

アカウント関連は自分だけが使う環境であればテンプレートの時点でセットしておくと便利。
IPアドレスDHCPでなければVMごとに変わってくるはずなので、テンプレートの時点では空欄にしておいて、クローン後に設定することが多いかも。
この辺りは使い方次第。

ここまで設定できればテンプレートの完成。 あとは収穫。

VMのクローンと起動

仮想マシン一覧」から右クリックメニューの「クローン」で、VMIDとマシン名を入力すればVMがプロビジョニングされる。
(実は「モード」がよくわかってない。完全クローンだと結構時間がかかる)

このVMは前述テンプレートの設定で入力したCPUやメモリ・ストレージサイズ、Cloud-Initの内容がそっくりコピーされているので、テンプレートの内容のままで良ければそのまま開始すればOK
ネットワーク設定などVM毎の設定が必要であれば、クローン直後(初回起動前)に設定する。
(Cloud-Initは初回起動時のみに処理されるので)

今回はネットワーク設定を更新して起動。

コンソールにアクセスすれば、起動は割とすぐ完了してログインプロンプトが表示されるので、Cloud-Initで設定したユーザー名とパスワードでログインができる。
ただ、Cloud-Init自体はログインプロンプト表示後も非同期で処理されており、特にパッケージアップデートを有効にしていると、コンソールログイン後も並行して処理される。

SSHサーバーも早い段階で起動しているので、リモートからSSHアクセスが(公開鍵を設定していればその鍵を使った公開鍵認証で)可能。

環境

PVE 8.1.10 無償ライセンス版


というわけでProxmox VEのテンプレートとクローンとCloud-InitでVMを簡単にプロビジョニングできる機能についてのまとめ。
使える状態のVMを秒で作成できる手軽さは、コンテナ利用に近いものがあるスピード感で、OS設定のレイヤーから環境が欲しい場合はこの便利さはかなり感動もの。

ちなみに機能(感覚的にネットワークセキュリティ系)は結構少なかったりする。
iptablesやfirewalldは入ってないので、IaaSの同等機能を使う想定だったりするのかな?

[Kubernetes] DaemonSetで動いてるPodを停止(スケールを0に)したい

業務で元リソースはそのままでPodを一時的に停止していろいろ確認するなんやかやがあり、DeploymentやStatefulSetはkubectl scaleでレプリカ数を0にすれば良かったんだけど、DaemonSetってそういえばレプリカ0にできないというかそもそもレプリカの概念がなく、なんとかできそうにないかと思いつつ検索してみるとnodeSelectorを使ってデプロイできるノードを無くすという技がいくつかヒットしたので試してみた。

stackoverflow.com

まとめておくと以下でOK

# 停止
kubectl -n <namespace> patch daemonset <daemonset-resource> -p '{"spec": {"template": {"spec": {"nodeSelector": {"non-existing": "true"}}}}}'

# 起動
kubectl -n <namespace> patch daemonset <daemonset-resource> --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/non-existing"}]'

サンプルDaemonSet

GitHubに置いてあるDaemonSet版webサーバーマニフェストを使用。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: http-daemon
  name: http-daemon
spec:
  selector:
    matchLabels:
      app: http-daemon
  template:
    metadata:
      labels:
        app: http-daemon
    spec:
      containers:
      - image: httpd
        name: httpd
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: http-daemon
  name: http-daemon
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    name: http
  selector:
    app: http-daemon
  type: ClusterIP
$ kubectl apply -f https://raw.githubusercontent.com/zaki-lknr/k8s-samples/master/sample-web/httpd-daemonset/sample-http.yaml
daemonset.apps/http-daemon created
service/http-daemon created

zaki@cloud-dev2:~$ kubectl get pod -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP          NODE           NOMINATED NODE   READINESS GATES
http-daemon-g6glw   1/1     Running   0          23s   10.42.0.6   730dd035b3ef   <none>           <none>
http-daemon-rxxbk   1/1     Running   0          23s   10.42.1.6   e35370123528   <none>           <none>

このDaemonSetリソースに対してレプリカ数を操作しようとしてもエラーになる。

zaki@cloud-dev2:~$ kubectl scale ds http-daemon --replicas=0
Error from server (NotFound): the server could not find the requested resource

ノードセレクタを設定

kubectl patchでノードセレクタを追加

zaki@cloud-dev2:~$ kubectl -n default patch daemonset http-daemon -p '{"spec": {"template": {"spec": {"nodeSelector": {"non-existing": "true"}}}}}'
daemonset.apps/http-daemon patched

するとPodが停止する。

zaki@cloud-dev2:~$ kubectl get pod -o wide
NAME                READY   STATUS        RESTARTS   AGE     IP       NODE           NOMINATED NODE   READINESS GATES
http-daemon-rxxbk   0/1     Terminating   0          2m18s   <none>   e35370123528   <none>           <none>
zaki@cloud-dev2:~$ kubectl get pod -o wide
No resources found in default namespace.

DaemonSetリソースはこの通り、ノードセレクタが設定され、DESIREDが0になる。

zaki@cloud-dev2:~$ kubectl get daemonset
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR       AGE
http-daemon   0         0         0       0            0           non-existing=true   4m5s

元に戻す

zaki@cloud-dev2:~$ kubectl -n default patch daemonset http-daemon --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/non-existing"}]'
daemonset.apps/http-daemon patched

ノードセレクタの設定を削除するとPodが再度デプロイされる。

zaki@cloud-dev2:~$ kubectl get daemonset
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
http-daemon   2         2         1       2            1           <none>          5m7s
zaki@cloud-dev2:~$ kubectl get pod -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP          NODE           NOMINATED NODE   READINESS GATES
http-daemon-j84kz   1/1     Running   0          7s    10.42.1.7   e35370123528   <none>           <none>
http-daemon-whp2q   1/1     Running   0          7s    10.42.0.7   730dd035b3ef   <none>           <none>

(余談) 停止中に対象ラベルをノードに設定すると…

もちろんラベルをセットしたノードではデプロイされる。

# podが停止している状態
zaki@cloud-dev2:~$ kubectl get pod 
No resources found in default namespace.

# ノードのラベル状態
zaki@cloud-dev2:~$ kubectl get nodes --show-labels 
NAME           STATUS   ROLES                  AGE   VERSION        LABELS
e35370123528   Ready    control-plane,master   11m   v1.29.1+k3s2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=e35370123528,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=true,node-role.kubernetes.io/master=true,node.kubernetes.io/instance-type=k3s
730dd035b3ef   Ready    <none>                 11m   v1.29.1+k3s2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=730dd035b3ef,kubernetes.io/os=linux,node.kubernetes.io/instance-type=k3s

# ラベル設定
zaki@cloud-dev2:~$ kubectl label node 730dd035b3ef non-existing=true
node/730dd035b3ef labeled
zaki@cloud-dev2:~$ kubectl get nodes --show-labels 
NAME           STATUS   ROLES                  AGE   VERSION        LABELS
e35370123528   Ready    control-plane,master   11m   v1.29.1+k3s2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=e35370123528,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=true,node-role.kubernetes.io/master=true,node.kubernetes.io/instance-type=k3s
730dd035b3ef   Ready    <none>                 11m   v1.29.1+k3s2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=730dd035b3ef,kubernetes.io/os=linux,node.kubernetes.io/instance-type=k3s,non-existing=true

# ラベルを設定したノードにはpodがデプロイされる
zaki@cloud-dev2:~$ kubectl get pod -o wide
NAME                READY   STATUS              RESTARTS   AGE   IP       NODE           NOMINATED NODE   READINESS GATES
http-daemon-jwg8d   0/1     ContainerCreating   0          9s    <none>   730dd035b3ef   <none>           <none>
zaki@cloud-dev2:~$ kubectl get pod -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP          NODE           NOMINATED NODE   READINESS GATES
http-daemon-jwg8d   1/1     Running   0          29s   10.42.0.8   730dd035b3ef   <none>           <none>

環境

K3s v1.29(コンテナ版)で確認

zaki@cloud-dev2:~$ kubectl get node
NAME           STATUS   ROLES                  AGE    VERSION
730dd035b3ef   Ready    <none>                 115s   v1.29.1+k3s2
e35370123528   Ready    control-plane,master   2m6s   v1.29.1+k3s2

nodeSelectorを使ってPodをデプロイするノードを制御、みたいなまとめを書いてたような気がしてたけど気のせいだったか。。