zaki work log

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

WSLでVM環境のコピー(exportとimport)

WSLにはVMの内容をtar形式のアーカイブファイルへエクスポートする機能がある。
これでVMのバックアップやスナップショット的な状態保存、別名でインポートすることで同じディストリビューションVMを複数作ったりできる。
Dockerでいうとexport/importに相当。

VM環境の保存(エクスポート)

tar形式にエクスポートするには--exportオプションを使う。
Ubuntu-20.04」ディストリビューションUbuntu-20.04.tarファイルへエクスポートするには以下。

PS C:\Users\zaki> wsl --export Ubuntu-20.04 Ubuntu-20.04.tar

tarファイルのインポート

エクスポートして保存したtarファイルは、--importオプションを使ってインポートできる。 名前(ディストリビューション名)をエクスポート元のディストリビューションと異なるものを指定することで、環境のコピーとなる。

書式は wsl --import ディストリビューション名 インストール先ファイルパス インポートするtarファイル

PS C:\Users\zaki> wsl --import ubuntu-20.04-container .\work\wsl\ubuntu-20.04-container .\Ubuntu-20.04.tar

インストール先ファイルパスには、インポートしたあとのvhdxファイルの保存場所を指定する。

デフォルトユーザーの変更

tarファイルをインポートしただけの状態だと、シェルを起動するとなぜかrootユーザーになる(もともと作成済みのユーザーへsuで切り替えは可能)。
ちょっと不便なのでデフォルトユーザーを以下PowerShellで更新する。

PS C:\Users\zaki> Function WSL-SetDefaultUser ($distro, $user) { Get-ItemProperty Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Lxss\*\ DistributionName | Where-Object -Property DistributionName -eq $distro | Set-ItemProperty -Name DefaultUid -Value ((wsl -d $distro -u $user -e id -u) | Out-String); };
PS C:\Users\zaki> WSL-SetDefaultUser ubuntu-20.04-container zaki

やっていることはレジストリ値の更新で、指定ディストリビューションDefaultUidの値を指定のユーザーからuidを取得して設定している。
元ネタはこちら

github.com

一つ目のコマンドで更新用関数を定義し、二つ目のコマンドで、第1引数に対象ディストリビューション名、第2引数にデフォルトにするユーザー名を指定する。

VMの削除

削除するにはディストリビューション名を引数に--unregisterオプションを使う。

PS C:\Users\zaki> wsl --unregister ubuntu-20.04-container
登録を解除しています...

これで気軽に環境のコピーを作れるので、コンテナほどサイズは小さくはないけど、サービス用・開発用・ステージング用などいろいろ試して壊す環境を気軽に用意できる。

オフライン環境へのRHEL8.6インストールとサブスクリプション割り当て

ちょっと構築する機会があったのでメモ。

OSのインストール

インストール用のisoファイルを入手し、これは普通に。
メディアから起動し、OSのインストールを行う。
お試しとして、今回は最小構成設定のインストール。

今回の環境は、ESXi上のVMで、CPUは2コア・メモリ8GBで、閉塞ネットワークには繋がっている状態。
(インターネットへの接続は無し)

サブスクリプション割り当て

オフライン環境のサブスクリプション割り当ての手順はこちら。

access.redhat.com

大まかな流れは、以下の通り。

  1. カスタマポータル上で手動でシステムを登録
  2. 証明書をダウンロード
  3. ダウンロードした証明書をオフラインRHELsubscription-manager で登録

カスタマポータル上でシステム登録

システムの画面で、「新規作成」を押下。

システム情報の入力画面になるので、ホスト名やCPU数などの項目を入力。
今回はVMなので仮想システムを選択。

システムを作成すると、サブスクリプションのアタッチボタンが表示されるので押下。

割り当て可能なサブスクリプションがチェックできるようになるので、使用するサブスクリプションをチェックして「サブスクリプションのアタッチ」を押下

証明書をダウンロードできる画面に遷移するので、「証明書のダウンロード」を押下。

これで証明書ファイル(pem形式)を含むzipファイルがダウンロードできる。
このzipファイルまたは展開したpemファイルをサブスクリプション登録したいRHELへ転送する。

証明書ファイルを使ったサブスクリプション登録

ファイルシステム上にダウンロードしたpemファイルを配置し、subscription-manager import --certificate コマンドを使ってインポートする。
複数のサブスクリプション割り当ての場合は、ファイル数分実行すればOK

[root@rhel8-restricted ~]# subscription-manager import --certificate /tmp/4199243939243262298.pem 
証明書 4199243939243262298.pem は正常にインポートされました
[root@rhel8-restricted ~]# subscription-manager import --certificate /tmp/8034611573323406627.pem 
証明書 8034611573323406627.pem は正常にインポートされました
[root@rhel8-restricted ~]# 

Yumリポジトリ設定

デフォルトのリポジトリの無効化

オフライン環境でセットアップしたRHELでも、デフォルトでは https://cdn.redhat.com/リポジトリを見に行くように設定されているので、これを無効にする。

[root@rhel8-restricted etc]# dnf repolist
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
repo id                                        repo の名前
rhel-8-for-x86_64-appstream-rpms               Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)
rhel-8-for-x86_64-baseos-rpms                  Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)
[root@rhel8-restricted etc]# dnf config-manager --disable rhel-8-for-x86_64-appstream-rpms rhel-8-for-x86_64-baseos-rpms
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません

ISOイメージ内のパッケージファイルをYumリポジトリとして利用する

access.redhat.com

マウント

まずはDVDのマウント。
ESXiの場合で、ホストの設定としてデータストアのISOファイルを使用する設定にしてる場合であれば、/etc/fstab に以下設定を追加しておき、mount -amount /mnt/cdrom でメディアをマウントする。

/dev/cdrom              /mnt/cdrom           iso9660    loop           0 0
[root@rhel8-restricted ~]# ls -F /mnt/cdrom/
AppStream/  EULA                     RPM-GPG-KEY-redhat-release  images/
BaseOS/     GPL                      TRANS.TBL                   isolinux/
EFI/        RPM-GPG-KEY-redhat-beta  extra_files.json            media.repo

ISOファイルがファイルシステム上にある場合は、/dev/cdrom のところをファイルシステム上のパスに置き換えればOK

/path/to/rhel-8.6-x86_64-dvd.iso /mnt/tmp iso9660 loop 0 0 

mountコマンドを使う場合は以下。

# mount -o loop /path/to/rhel-8.6-x86_64-dvd.iso /mnt/tmp

リポジトリ設定

ファイルを確認できたら、Yumリポジトリ設定を行う。
ファイル名は任意なので、サンプルとして /etc/yum.repos.d/local.repo ファイルを作成。内容は以下の通り。
これはマウントポイントが /mnt/cdrom の場合。異なる場合は baseurl= の行の設定を環境に合わせる。

[dvd-BaseOS]
name=DVD for RHEL - BaseOS
baseurl=file:///mnt/cdrom/BaseOS
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[dvd-AppStream]
name=DVD for RHEL - AppStream
baseurl=file:///mnt/cdrom/AppStream
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

最後にキャッシュをクリアする。

[root@rhel8-restricted ~]# yum clean all
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
13 ファイルが削除されました

これでDVD内のパッケージをYumリポジトリとして参照できるようになるので、追加のパッケージインストールなどがあればdnf installが可能になる。

[root@rhel8-restricted ~]# dnf repolist
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
repo id                              repo の名前
dvd-AppStream                        DVD for RHEL - AppStream
dvd-BaseOS                           DVD for RHEL - BaseOS

その他、制限環境で使える参考情報

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

Docker版GuacamoleでSSH接続の日本語対応 (Debianイメージへの日本語フォント追加)

前夜まとめたDockerコンテナでデプロイした素のGuacamoleは、SSH接続しても日本語の表示や入力が文字化けする。

zaki-hmkc.hatenablog.com

今回はこの対応。

といっても先に結論を書くと、guacdコンテナに日本語フォントを追加すればOK

日本語フォント入りイメージビルド

今回は(個人的に見慣れたフォントということで)VLゴシックフォントをapt-getで追加。
fonts-takao-gothicもあるみたい。

ビルドに使うDockerfileは以下の通り、フォントをインストールするのみ。
実行権限がrootじゃなくなってるので少しイジる。

FROM guacamole/guacd:1.4.0

USER 0
RUN apt-get update \
    && apt-get install -y fonts-vlgothic \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

USER 1000

ビルド

docker build . -t guacad:1.4.0-ja -f Dockerfile.guacd-ja-vlgothic

ここではイメージ名をguacad:1.4.0-jaにしている。Docekrfileのファイル名はDockerfile.guacd-ja-vlgothic

デプロイ

動作しているguacamole/guacdコンテナがある場合は、先に停止・削除する(guacamole/guacamoleとDBはそのままでOK)。
停止させたら、オリジナルの替わりにビルドした日本語フォント入りのコンテナをデプロイする。
コマンドは以下の通り。というかオリジナルと同じ。

docker run --name guacd -d --network guacamole-network guacd:1.4.0-ja 

これでGuacamoleのwebからSSHアクセスすると、日本語の入出力ができるようになる。


あとはCompose対応すれば構築は簡単にできるけど、DBの初期化ファイル作成、あの処理をComposeに入れられるんだろーか。。

参考

3. 接続方法 | Apache Guacamole利用マニュアルの「3.2.1. 日本語入力方法」

Apache Guacamoleを使ってwebブラウザでSSH接続をお試し (Docker版)

ネットワークの制限でwebアクセス(HTTP/HTTPS)しか許可されておらず、SSHアクセスが使えないような環境でリモートのLinuxサーバーのシェルへアクセスしたい場合、Apache Guacamoleを使うことでwebブラウザの画面でSSHログインを行う。(webページ内にSSHアクセスしたリモートサーバーのシェルが画面表示される)

接続イメージは以下の通り。

本エントリではとりあえずSSH接続のみ。
ただしGuacamoleはSSH以外にもVNCやRDPなどいくつかのリモート接続用のプロトコルに対応している。
Kubernetesもあるのが気になる。

Guacamoleのデプロイ

サーバーへ直接インストールする方式と、Docker Hubで公開されているDockerコンテナ版のGuacamoleを使う方法がある。 本エントリではDockerイメージを使うパターンで構築。

guacamole.apache.org

Guacamoleは、以下の3つのコンポーネント(説明は意訳)で動作し、それぞれコンテナを実行する。(DBだけ外部とかできるけど、とりあえず全部Dockerで)

ちなみにGuacamoleのコンテナイメージは、latestタグはデイリーで更新されているため、最新安定版のバージョンのタグを指定した方が良さそう(おそらくCIで自動ビルドされてるっぽい)。
現時点で1.4.0が最新。

docker networkの作成

公式ドキュメントだと--link使ってコンテナ間通信する手順になってるけどさすがにもうこのオプションを使うのも微妙なので、コンテナ間通信を行うためのDocekr Networkを作成。

docker network create guacamole-network

以降のコンテナデプロイは、このDocker Networkを--networkで指定。

guacdの起動

docker run --name guacd -d --network guacamole-network guacamole/guacd:1.4.0

MySQL

MySQLPostgreSQLが使用できるが、今回はMySQLを使用。

初期化ファイル作成

まずは事前にguacamole/guacamoleコンテナ内のスクリプトを使って、DBの初期化スクリプトを作成。

docker run --rm guacamole/guacamole:1.4.0 /opt/guacamole/bin/initdb.sh --mysql > initdb.sql

DBの起動

MySQLコンテナは、起動時に/docker-entrypoint-initdb.dディレクトリ以下の*.sqlファイルでDBを初期化するので、↑で作成した初期化スクリプトをバインドマウントして起動する。
また、DBのデータ(/var/lib/mysql)をボリュームマウントする。

docker run --name guacamole_db \
  -d \
  -e MYSQL_DATABASE=guacamole_db \
  -e MYSQL_USER=guacamole_user \
  -e MYSQL_PASSWORD=guacamole_passwd \
  -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
  -v $PWD:/docker-entrypoint-initdb.d \
  -v guacamole_db:/var/lib/mysql \
  --network guacamole-network \
  mysql

guacamole

アプリケーションサーバーを起動する。
GUACD_HOSTNAMEにguacdコンテナのコンテナ名、MYSQL_HOSTNAMEmysqlコンテナのコンテナ名を指定する。
DBの接続情報はmysql起動時に指定したものと同じものを指定する。

また、このコンテナにwebブラウザでアクセスするので、ポート番号をpublishする。

docker run --name guacamole \
  -d \
  -e GUACD_HOSTNAME=guacd \
  -e MYSQL_HOSTNAME=guacamole_db \
  -e MYSQL_DATABASE=guacamole_db \
  -e MYSQL_USER=guacamole_user \
  -e MYSQL_PASSWORD=guacamole_passwd \
  --network guacamole-network \
  -p 8080:8080 \
  guacamole/guacamole:1.4.0

デプロイはこれで完了。

webアクセスと接続設定

http://<docker host>:8080/guacamole にアクセス。(ドキュメントルートでなくパス指定があるので注意)

認証

初期値としてユーザー名・パスワードともにguacadminがセットされている(たぶんDBの初期化ファイル作成の時点にこれになってる)のでこれでログイン。

Verifying the Guacamole install

adminのパスワードを変更するには、ログインして右上にあるメニューから「設定」を選択。

ユーザ設定メニューにパスワード変更のフィールドがあるので現在のパスワードと新しいパスワードを入力する。

接続先設定

接続先の設定は、メニューの「接続」→「接続の追加」でサーバー設定を行う。 SSH接続のために最低限必要なのは以下の通り。

  • 接続の編集
    • 名前: 設定の名前。わかりやすいのを入力しておく
    • プロトコル: SSH
  • パラメータ
    • ネットワーク
      • ホスト名: 接続先SSHサーバーのアドレス

これだけ入力して「保存」、ホームに戻ると接続情報に設定が追加されるのでマウスクリックすれば、ユーザー名とパスワードを聞かれてパスワード認証でSSH接続できる。

認証パラメータの「ユーザ名」「パスワード」を事前に入力しておけば、接続時の認証も省略できる。

公開鍵認証設定

公開鍵認証の設定は、パラメータセクションに最低限以下を入力すればOK。

ただし、秘密鍵の登録はOpenSSH形式とドキュメントに記載があるが、実際にはPEM形式である必要がある。

The private key must be in OpenSSH format, as would be generated by the OpenSSH ssh-keygen utility.

https://guacamole.apache.org/doc/gug/configuring-guacamole.html#ssh-authentication

現バージョンのOpenSSHのssh-keygenでキーペア作成すると、デフォルトではOpenSSH形式(ファイル先頭が「-----BEGIN OPENSSH PRIVATE KEY-----」で始まる)になっているが、この形式だと公開鍵認証が上手くいかない。
PEM形式(ファイル先頭が「-----BEGIN RSA PRIVATE KEY-----」で始まる)のキーペアを作成するには、ssh-keygen-mオプションで形式を指定する。

$ ssh-keygen -m PEM ...

既存のOpenSSH形式の鍵をPEM形式に変換するには以下。この場合元のファイルが更新されるので、必要があれば事前にコピーしておく。

$ ssh-keygen -p -m PEM -f ~/.ssh/id_rsa

秘密鍵の入力フィールドに、秘密鍵ファイルの中身を全て貼り付けて保存する。

これでホームに戻り、接続先を選択すれば、公開鍵認証でSSH接続できる。

環境

  • ホストOS: Fedora 35
  • Docker: 20.10
  • Guacamole: 1.4.0

補足

Guacamoleのイメージはamd64版しかないので、arm64ではそのままでは動かせない。

デフォルトだと日本語入出力できないので要調査。
多分コンテナ内のロケールがどうとか、、、と思う。
ロケールの追加と設定は関係なく(ja_JP.UTF-8とか入れても変化無し)、原因は日本語フォントでした。あとで別途まとめる。

まとめました。

zaki-hmkc.hatenablog.com

あと今回はSSHサーバーとGuacamoleを立てたDockerホストを別にしてるが、ここは同じホストでも疎通があるなら問題ない。はず。

[Ansible] Gitリポジトリにあるコレクションのインストール

Ansible Galaxyと疎通がないとか、ローカル内でGit管理されているプライベート開発なコレクションのように、任意のGitリポジトリにあるコレクションをインストールする際のrequirements.ymlの記述例は以下の通り。

---
collections:
  - name: https://github.com/zaki-lknr/esxissh-ansible.git
    version: main
    type: git

指定するキーと値は以下の通り。

キー
name リポジトリURL
version ブランチ名
type gitを指定

この内容のファイルを指定することで、各種環境でコレクションをインストールできる。

(配布側) リポジトリ構成

リポジトリのルートディレクトリにgalaxy.ymlファイルやpluginsディレクトリなどのコレクションを構成するディレクトリとその中身を配置する。
ない場合(サブディレクトリ以下にある等)だとエラーになるので注意。

今回のサンプルコレクションはこちら。

github.com

コレクション作成の参考

qiita.com

(利用側) インストール

ansible-galaxy

任意のファイル名で前述の書式のファイルを作成し、以下コマンドを実行。
お作法的には collections/requirements.yml が好ましい。多分。

$ ansible-galaxy install -r collections/requirements.yml

インストール先ディレクトリはデフォルトでは~/.ansible以下。
変更したい場合はansible.cfgなどでインストール先を指定する。

[defaults]
collections_path = /path/to/ansible/collections

ansible-builder

execution-environment.ymldependenciesに指定するrequirements.ymlでインストール内容を記述して、あとは普通にビルドする。

$ ansible-builder build -t esxi-ssh:latest -v 3
Ansible Builder is building your execution environment image, "esxi-ssh:latest".
File context/_build/requirements.yml is already up-to-date.
File context/_build/requirements.txt is already up-to-date.
File context/_build/bindep.txt is already up-to-date.
File context/_build/ansible.cfg is already up-to-date.
:
:
[1/3] STEP 8/8: RUN ansible-galaxy collection install $ANSIBLE_GALAXY_CLI_COLLECTION_OPTS -r requirements.yml --collections-path /usr/share/ansible/collections
Starting galaxy collection install process
Process install dependency map
Cloning into '/home/runner/.ansible/tmp/ansible-local-116wclers/tmptw5tpno2/esxissh-ansible347k3r33'...
Already on 'main'
Your branch is up to date with 'origin/main'.
Starting collection install process
Installing 'zaki_lknr.esxissh:1.1.0' to '/usr/share/ansible/collections/ansible_collections/zaki_lknr/esxissh'
Created collection for zaki_lknr.esxissh:1.1.0 at /usr/share/ansible/collections/ansible_collections/zaki_lknr/esxissh
zaki_lknr.esxissh:1.1.0 was installed successfully
--> 304eaaf07f6
:
:

AAP / AWX / Tower

(確認はAAP 2.1のみ実施)
リポジトリcollections/requirements.yml にインストールしたいコレクションを記述する。

サンプルとしては以下。

github.com

これでソースコントロールの更新時にコレクションがインストールされる。

参考

docs.ansible.com

ドキュメントは以下、Install multiple collections with a requirements fileより。

また、Git上にあるRoleのインストール関連は以下。

zaki-hmkc.hatenablog.com

[Ansible / Docker / K3s] AWX 20.0.0をarm64環境でビルド&デプロイするplaybook

AWXはamd64向けのイメージしか提供されてないので、Oracle CloudのAlways Free枠で使用できる4コア・RAM24GBのA1.FlexインスタンスのようなARMアーキテクチャの環境ではそのままでは利用できません。
イメージをARM環境でリビルドしてやれば動作するので、さくっと構築できるようにplaybookを作ったのでその内容についてメモしました。

ビルド済みarm64イメージのデプロイをちょっと試す

私がビルドしたarm64のイメージをDockerHubとGitHub Container Registryで公開しています。
このイメージをデプロイするだけで良いのであれば、arm64環境で以下のコマンドでデプロイできます。

$ git clone -b 0.17.0 https://github.com/ansible/awx-operator.git
$ cd awx-operator/
$ export NAMESPACE=awx
$ make deploy IMG=zakihmkc/awx-operator:0.17.0

※ 記事作成時点で最新は0.18.0だけど、operatorのデプロイ後カスタムリソースからpodが何故か作成されなかったので、動作確認できてる0.17.0を使ってます。

awx-operatorがデプロイされたら、以下の内容のAWXカスタムリソースをデプロイ。

---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
  name: awx-demo
  namespace: awx
spec:
  service_type: nodeport
  image: zakihmkc/awx
  image_version: 20.0.0
  image_pull_policy: Always
  ee_images:
    - name: awx-ee-arm64
      image: zakihmkc/awx-ee:0.6.0
    - name: AWX EE (latest)
      image: zakihmkc/awx-ee:0.6.0
  control_plane_ee_image: zakihmkc/awx-ee:0.6.0

ノードのCPUやメモリリソースが少なくてpodがpendingになる場合は、Containers Resource Requirementsのパラメタを追加してrequestsの制限を入れてください。

AWXをビルド・デプロイするplaybook

Oracle CloudのA1.FlexシェイプのUbuntu 20.04 (4コア・RAM24GB)で動作確認しています。
デプロイ直後のまっさらな状態のUbuntu 20.04インスタンスに対して、Ansibleを使って、Dockerのインストールとイメージのbuildとpushを行い、K3sのインストールとAWXのデプロイを行います。外部アクセス用には、K3s標準のTraefikのIngressを使用します。

github.com

使い方

リポジトリのクローン

$ git clone https://github.com/zaki-lknr/awx_build_and_deploy.git
$ cd awx_build_and_deploy

インベントリ

inventory-example.iniで定義されている変数は以下の通り。
ビルドしたイメージは、registry_urlregistry_usernameを元にレジストリURLを生成しpushされます。
ビルドは行わずに、前述手動の手順と同様にビルド済みのイメージをデプロイするだけであれば、buildホストグループとregistry_*コメントアウトし、後述のイメージ名のオプションの*_image_repositoryを初期値のままにしておけば、デプロイだけ処理します。
このplaybookでは動作環境としてK3sをインストールしてそこへAWXをデプロイします。K3sで標準で使用できるIngressを使って外部公開するため、アクセス用FQDNawx_service_fqdnに指定します。ここで指定したFQDNDNShostsファイルを使って名前解決できるようにすればwebアクセスできます。

項目 説明
build ホストグループ イメージビルドするホストを指定
deployホストグループ AWXをデプロイするホストを指定 (buildと同じでOK)
awx_service_fqdn Ingressで使用するアクセス用FQDN
awx_namespace AWXをデプロイするk8sのネームスペース
registry_username ビルドしたイメージをpushするレジストリのログインユーザー名
registry_password ビルドしたイメージをpushするレジストリのログインパスワード
registry_url ビルドしたイメージをpushするレジストリURL (省略時はDocker Hub)

イメージのレジストリURLに関するオプションは以下。
未指定の場合は前述の通りregistry_urlregistry_usernameを元にレジストリURLが決まりますが、これらの変数でレジストリURLを上書きできます。
自分のアカウントのレジストリへpushする場合は、exampleのインベントリファイルの設定はコメントアウトしてください。

項目 説明
operator_image_repository ビルド・デプロイするawx-operatorのイメージ名
awx_image_repository ビルド・デプロイするawxのイメージ名
awx_ee_image_repository ビルド・デプロイするawx-eeのイメージ名

リソース制限のオプションは以下。
未指定の場合はデフォルト値が使用されます。デプロイ先のノードスペックが低い場合等はlimitsの値をノードサイズに収まるように指定してください。
(例えば2コア・RAM12GBの場合は、CPU 400m・メモリ1GBとかにすれば大体行けます)

項目 説明
web_resource_requests_cpu Web containerのCPU requests値
web_resource_requests_mem Web containerのメモリrequests値
web_resource_limits_cpu Web containerのCPU limits値
web_resource_limits_mem Web containerのメモリlimits値
task_resource_requests_cpu Task containerのCPU requests値
task_resource_requests_mem Task containerのメモリrequests値
task_resource_limits_cpu Task containerのCPU limits値
task_resource_limits_mem Task containerのメモリlimits値
ee_resource_requests_cpu EE control plane containerのCPU requests値
ee_resource_requests_mem EE control plane containerのメモリrequests値
ee_resource_limits_cpu EE control plane containerのCPU limits値
ee_resource_limits_mem EE control plane containerのメモリlimits値

バージョンに関するオプションは以下。
該当Gitリポジトリのタグorブランチ名に対応しています。
ただし、現状では指定のバージョン以外の動作は基本的に未確認。

項目 説明
operator_version AWX Operatorのバージョン
awx_version AWXのバージョン
awx_ee_version AWX EEのバージョン
ansible_runner_version Ansible Runnerのバージョン
ansible_builder_version Ansible Builderのバージョン
python_base_version python-base-imageのバージョン
python_builder_version python-builder-imageのバージョン
receptor_version Receptorのバージョン

実行

インベントリファイルが用意できたら、ansible-playbookコマンドで実行。

$ ansible-playbook -i inventory.ini playbook.yml

[build]ホストグループに指定したホストに対してDockerのインストールを行い、必要な全イメージをbuildし、K3sへデプロイするイメージをpushします。
push対象のレジストリには、インベントリに指定したユーザー名とパスワードで認証を行います。

ビルドが完了したら、[deploy]ホストグループに指定したホストへK3sをインストールし、ビルド処理でpushしたコンテナイメージをデプロイします。
デプロイ後しばらく待てば、awx_service_fqdnに指定したFQDNにブラウザでアクセスすればAWXの画面が表示されます。
(クラウドのセキュリティグループは別途80/TCPを許可しておいてください)

AWXへログインするには、ユーザー名はadminで、パスワードは以下のコマンドで確認できます。

$ kubectl get secret -n awx awx-demo-admin-password -o jsonpath="{.data.password}" | base64 -d

解説 (という名の備忘録)

AWX 20.0.0はKubernetesクラスタへデプロイするために、operatorのイメージ(awx-operator)と、AWX本体(awx)Ansible Execution Environment for AWX(awx-ee)の3つのイメージが必要です。
awx-operatorawxはarm64環境でそれぞれビルドすればよいので簡単ですが、awx-eeのイメージは、ベースイメージや内部で使用する実行用バイナリファイルもamd64版しかないため、以下のイメージのビルドも必要です。

awx-eeのイメージビルドにはansible-runneransible-builderが必要で、ansible-runneransible-builderはどちらもpython-baseansible-builderが必要です。この4イメージはmainブランチや最新リリース版をビルドすれば現状は大丈夫です。イメージビルドはpython-base/python-builderイメージをビルドしたあとにansible-runner/ansible-builderをビルドします。 また、/usr/bin/receptorとしてreceptorイメージから実行ファイルをコピーしているので、receptorイメージもarm64環境でビルドする必要があります。

python-base

github.com

ビルド手順のドキュメントは特に見当たらなかったけど、リポジトリルートにContainerfileがあるのでこのファイルでdocker buildします。
後続のansible-runneransible-builderのビルドを簡単にするために、quay.io/ansible/python-base:latestというタグを指定しておきます。

python-builder

github.com

これもpython-baseと同様に、Containerfileがあるのでこのファイルでdocker buildします。
また、これもansible-runneransible-builderのビルド用にquay.io/ansible/python-builder:latestタグを指定します。

ansible-runner

github.com

これはMakefileがあるので、makeを使ってビルドします。ファイルをチェックするとimageというターゲットがあるので、以下のコマンドでイメージビルドできます。
ビルドにはquay.io/ansible/python-base:latestquay.io/ansible/python-builder:latestが必要なので、必ず事前にarm64版をローカルに保持しておきます。(無い場合quayからamd64版をpullしてしまうので実行時にエラーになる)

$ make image

ansible-builder

github.com

これもansible-runnerと同様にMakefileがあるのでmakeコマンドを使います。同じく'image`ターゲットがあるので、コマンドは以下です。
ansible-runner同様に、quay.io/ansible/python-base:latestquay.io/ansible/python-builder:latestが必要なので事前にarm64版をビルドしておきます。

$ make image

receptor

github.com

receptorは他よりちょっとハマりがあって、2022.03時点の最新リリース版のv1.1.1だと、ベースイメージにcentos:8が指定されており、CentOS 8はすでにサポート終了してYumリポジトリが無効になっているため、以下のエラーメッセージでイメージビルドに失敗します。

Error: Failed to download metadata for repo 'appstream': Cannot prepare internal mirrorlist: No URLs in mirrorlist

ということで1.1系はこの状態っぽいので、1.0系を使うとよさげ。
現在1.2の開発が行われてるっぽいですが、最初に試した2月時点だと、golangのインストールをパッケージでなく RUN curl -LO https://go.dev/dl/go1.16.13.linux-amd64.tar.gz を実行して行っており、当時はダウンロードするバイナリのURLをarm64版に修正するなど試したりして回避してたので、開発版や1.2は様子見で、少し古いリリース版を使うのが現状では手間が少なそうです。

awx-ee

github.com

依存する5つのarm64版イメージビルドが完了したらawx-eeをビルドすればOK。これらの依存イメージはpushする必要はなく、ローカルにあればそれをベースイメージとしてビルドします。

awx-eeのビルドはREADMEに書かれており、toxコマンドを使ってDockerの場合は以下を実行します。

$ tox -edocker

ただawx-eeのイメージは原因が結局よくわからなかったんだけど、これでビルドしたイメージでデプロイしたAWXで、(Demoを含む)ジョブテンプレート実行時に以下のPermissionErrorが発生しました。

Traceback (most recent call last):
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/main/tasks/jobs.py", line 449, in run
    self.pre_run_hook(self.instance, private_data_dir)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/main/tasks/jobs.py", line 979, in pre_run_hook
    RunProjectUpdate.make_local_copy(job.project, private_data_dir, scm_revision=job_revision)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/awx/main/tasks/jobs.py", line 1380, in make_local_copy
    source_branch = git_repo.create_head(tmp_branch_name, p.scm_revision)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/git/repo/base.py", line 386, in create_head
    return Head.create(self, path, commit, force, logmsg)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/git/refs/symbolic.py", line 543, in create
    return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/git/refs/symbolic.py", line 510, in _create
    ref.set_reference(target, logmsg)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/git/refs/symbolic.py", line 326, in set_reference
    assure_directory_exists(fpath, is_file=True)
  File "/var/lib/awx/venv/awx/lib64/python3.9/site-packages/git/util.py", line 177, in assure_directory_exists
    os.makedirs(path, exist_ok=True)
  File "/usr/lib64/python3.9/os.py", line 225, in makedirs
    mkdir(name, mode)
PermissionError: [Errno 13] Permission denied: '/var/lib/awx/projects/_6__demo_project/.git/refs/heads/awx_internal'

検索するとIssueでも見つかったけど、結局どう解決したかわからず…

いろいろ試行錯誤(↑のツリー参照)した結果、k8sへデプロイした際にawx-eeの実行ユーザーがrootになってるのがマズイっぽい。
AWXのpodは、awxとawx-eeの複数コンテナから構成されており、awxコンテナはuid=1000(awx)で動作しているのに対して、awx-eeコンテナはuid=0(root)で動作した状態で、両方のコンテナからemptyDirで/var/lib/awx/projectsを共有している、というpod構成。
ジョブテンプレート実行時には、awx-eeコンテナでリポジトリをcloneするため、.git以下にroot権限でGitの制御ファイルが作成され、このファイルをawxコンテナからの参照(更新?)時に権限エラーになっている、というのが原因のようでした。

この問題を回避するため、コンテナの実行権限をコントロールできたら早そうだけどOperatorのカスタムリソース経由のためだいぶ厳しそうだったので、コンテナビルドの段階で実行ユーザーの指定を入れました。
具体的には、一旦awx-eeイメージを普通にビルドし、そのawx-eeイメージをベースイメージとして、更にUSER 1000だけ指定したDockerfileでビルドするというもの。この追加のイメージであれば、実行時にawxコンテナと同じuid=1000で動作するため、ジョブテンプレートも正常に動作しました。

awx

github.com

これはMakefileはあるけど、使い方がよくわからなかった。docker-composeでイメージビルドできるっぽいけど多分Docker Compose環境用らしく、K8sへデプロイしてもストレージドライバ周りでエラーが出たので使えなかった。
(ブログ作成時に改めてよくみたらawx-kube-dev-buildというターゲットがK8s用イメージビルドかも…未確認)

リポジトリ内のファイルをチェックすると、Dockerfileを生成してるplaybookがtools/ansibleディレクトリ以下にあるので、このplaybookを使ってイメージビルドを確認できた。実行するコマンドは以下の通り。

$ ansible-playbook build.yml

インベントリファイルなどで以下の変数を定義して実行時に指定すれば、このplay実行によってレジストリへのpushも実行される。

[all:vars]
push=1
registry=docker.io
awx_image=zakihmkc/awx
awx_image_tag=20.0.0

ただ今回作った自作のplaybookでは、レジストリとイメージパスを分割で定義されると都合が悪かったのでこの機能は使わず、自前でpushしてます

awx-operator

github.com

Makefileがあるのでmakeコマンドでビルドできます。
ansibleディレクトリplaybooksディレクトリはあるんだけど、ビルド用のplaybookファイルは見当たらなかったので、多分makeするしかないかも。以前デプロイのつもりでミスってビルドしてしまったこともあるので…(笑)

ターゲットはdocker-buildで、IMGパラメタを指定すれば、ビルドするイメージのタグも指定できるので、自分のレジストリへpushするようにタグ指定しておくと便利です。

$ make docker-build IMG=zakihmkc/awx-operator:0.17.0

試してないけどmake docker-pushでpushもできると思う。


参考情報

2月中旬にAWX 19.5.1のときに試した作業ログ。

zaki-lknr.github.io

CentOS 8のリポジトリがクローズした直後だったらしく、タイミングが良かったのやら悪かったのやら…(笑)

あと、去年の時点で同じようにarm64環境(Raspberry Pi)向けにビルド・デプロイされてる方もいらっしゃいました。
receptorのバイナリ取得がシンプルで良いですね。。

qiita.com

あと今回はコンテナ操作が多かったですが、発売されたばかりのこちらの本にDockerやKubernetesで使えるサンプルコードがたくさん載ってるので、使い方をさっと確認できて便利でした(ステマ)

book.impress.co.jp

[Ansible] metaモジュールを使ってPlayの処理を途中で終了する

プログラミングだとreturnexit(0)などで、以降の処理が不要な場合に、その時点で処理を終了させることができる。
Ansibleのplaybookでも似たようなことができ、metaモジュールを使用し、任意のタイミングで処理を終了させるタスクを作成できる。

docs.ansible.com

このモジュールはパラメタに指定するワードによっていろいろな動作を指せることができ、本エントリではend_playend_hostについて説明する。

以下、ansible-core 2.12.2で確認。

end_play

Playの実行をその時点で終了する。

---
- hosts: localhost
  gather_facts: false

  tasks:
  - name: task1
    debug:
      msg: task1

  - name: task2
    debug:
      msg: task2

  - name: end play
    ansible.builtin.meta: end_play

  - name: task3
    debug:
      msg: task3

このplaybookの内容でAnsibleを実行すると、end playタスクで処理が終了し、task3は実行されない。

PLAY [localhost] ****************************************************************

TASK [task1] ********************************************************************
ok: [localhost] => 
  msg: task1

TASK [task2] ********************************************************************
ok: [localhost] => 
  msg: task2

TASK [end play] *****************************************************************

PLAY RECAP **********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

条件によって以降の全ての処理を丸ごとスキップしたい場合に使える。

end_host

end_playが全処理を終了するのに対し、end_hostは指定ホストを対象とした処理のみその時点で終了する。
たとえば処理対象ホストにRHELホストとDebianホストがあり、Debianの場合は後続の処理が何もないので処理終了させて、RHELは続きの処理を行う、みたいなことができる。

---
- hosts: all
  gather_facts: true
  gather_subset:
    - min

  tasks:
  - name: task1
    debug:
      msg: task1

  - name: end host
    ansible.builtin.meta: end_host
    when: ansible_os_family == 'Debian'

  - name: task2
    debug:
      msg: task2

  - name: task3
    debug:
      msg: task3

このplaybookの内容でAnsibleを実行すると、facts変数のansible_os_familyの値がDebianのホストはend hostタスク以降の処理が行われず、それ以外のホストはtask2task3と処理が継続する。

実行例の処理対象ホストは以下の通り。

host distro
cheddar Debian
cloud-dev CentOS

これでansible-playbookを実行すると以下の通り。

PLAY [all] **********************************************************************

TASK [Gathering Facts] **********************************************************
ok: [cheddar]
ok: [cloud-dev]

TASK [task1] ********************************************************************
ok: [cloud-dev] => 
  msg: task1
ok: [cheddar] => 
  msg: task1

TASK [end host] *****************************************************************
skipping: [cloud-dev]

TASK [end host] *****************************************************************

TASK [task2] ********************************************************************
ok: [cloud-dev] => 
  msg: task2

TASK [task3] ********************************************************************
ok: [cloud-dev] => 
  msg: task3

PLAY RECAP **********************************************************************
cheddar                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

(追記) 複数のPlayの場合

end_playend_hostでPlayが終了しても、後続のPlayがある場合は、そこからは処理が始まる。

---
- name: play1
  hosts: localhost
  gather_facts: false

  tasks:
  - name: task1
    debug:
      msg: task1

  - name: task2
    debug:
      msg: task2

  - name: end play
    ansible.builtin.meta: end_play

  - name: task3
    debug:
      msg: task3

- name: play2
  hosts: localhost
  gather_facts: false

  tasks:
  - name: task1
    debug:
      msg: task1

このようにplay1のPlayがend_playで終了した場合、task3は処理されないがplay2は処理が始まる。
実行結果は以下。

PLAY [play1] ********************************************************************

TASK [task1] ********************************************************************
ok: [localhost] => 
  msg: task1

TASK [task2] ********************************************************************
ok: [localhost] => 
  msg: task2

TASK [end play] *****************************************************************

PLAY [play2] ********************************************************************

TASK [task1] ********************************************************************
ok: [localhost] => 
  msg: task1

PLAY RECAP **********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

end_hostも同様。

---
- name: play1
  hosts: all
  gather_facts: true
  gather_subset:
    - min

  tasks:
  - name: task1
    debug:
      msg: task1

  - name: end host
    ansible.builtin.meta: end_host
    when: ansible_os_family == 'Debian'

  - name: task2
    debug:
      msg: task2

  - name: task3
    debug:
      msg: task3

- name: play2
  hosts: all
  gather_facts: false

  tasks:
  - name: task1
    debug:
      msg: task1

この内容のPlaybookで実行すると、play2は全ホストで処理が開始する。

PLAY [play1] ********************************************************************

TASK [Gathering Facts] **********************************************************
ok: [cheddar]
ok: [cloud-dev]

TASK [task1] ********************************************************************
ok: [cloud-dev] => 
  msg: task1
ok: [cheddar] => 
  msg: task1

TASK [end host] *****************************************************************
skipping: [cloud-dev]

TASK [end host] *****************************************************************

TASK [task2] ********************************************************************
ok: [cloud-dev] => 
  msg: task2

TASK [task3] ********************************************************************
ok: [cloud-dev] => 
  msg: task3

PLAY [play2] ********************************************************************

TASK [task1] ********************************************************************
ok: [cloud-dev] => 
  msg: task1
ok: [cheddar] => 
  msg: task1

PLAY RECAP **********************************************************************
cheddar                    : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

後続のPlayは実行されるので、イメージとしてはexit()ではなくreturnだね。


whenを使ってスキップしてももちろん良いが、実行しない後続のタスク定義数が多い場合はend_hsotでスパッと抜けるという実装もできる。
え、MISRA-C準拠で実装してる?playbookを?まさかそんな。。