Open6

Ansibleに入門してみた

tomozosantomozosan

同時適用したいホスト一覧を設定ファイルに書く

↓はリモートのホスト一覧を記述した設定ファイル.$HOME/.ssh/configを利用しないやり方は
ssh user@hostと通常接続するときに ansible_userでuser、ansible_hostでansible_hostがを定義することができる.

inventory.yaml
all:
  children:
   homelab:
      hosts:
        raspi1:  # $HOME/.ssh/configの設定を暗黙に適用する
        raspi2:
        raspi3:
        ubuntu1:
        # ansible_become_passは予約変数
          ansible_become_pass: "{{ ub1_password }}" # ub1_passwordは独自定義の変数. 
        ubuntu2:
          ansible_become_pass: "{{ ub2_password }}"

    vultr:
      hosts:
        vubuntu1:  # $HOME/.ssh/configを利用しないやり方.
          ansible_host: "{{ vub1_ipv4 }}"
          ansible_user: "{{ vub1_user }}"

このそれぞれallとかhomelabとかがあとでPlaybookと呼ばれるレシピファイルで利用するグループ単位になる. なお hosts下のraspi1などの名前は何も書かなければ 暗黙に$HOME/.ssh/configファイルにかかれている内容のHost変数を参照する.
例↓

$HOME/.ssh/config
Host raspi1  # ここの名前が下のinventory.yamlのhostsの名前で参照される
  User raspi1
  HostName exmple.com
  Port 22
  IdentityFile ~/.ssh/id_ed25519
  AddKeysToAgent yes
  UseKeychain yes
  ProxyCommand cloudflared access ssh --hostname %h # Cloudflaredなどを利用したProxyコマンドでも問題なく動く

公開したくない変数などの保存どうするの問題 -> ansible-vaultを利用

やり方

ansibleの レシピに相当する Playbookや リモートホストの設定一覧を記述するInventoryファイルなどで秘密鍵やAPI tokenやCloudインスタンスのIPアドレスなど外部に公開すると、セキュリティ上問題のある変数をJinjaのtemplate変数の方法で記述できる.

  1. パスワードをローカルのPrivateなディレクトリに置いておき $HOME/.local/share/ansible_pwd.txt
    などにランダムな文字列をbcrypt-toolなどで生成して保存するたとえば
$HOME/.local/share/ansible_pwd.txt
$2a$10$0k./H/G16ejeoT612nSWR.j9djAsQIcPuU9MAmCXD2fQSCOxevbuC
  1. 上記ファイルをansibleファイルの暗号化のための鍵🔑として秘密設定ファイルを作る.
ansible-vault create --vault-password $HOME/.local/share/ansible_pwd.txt vault.yaml

とやるとファイルが作成されモーダルエディターが開く. 自分のMacBook ProではデフォルトでVimと同じコマンドで編集できた.

vault.yaml
# この変数を他のレシピやホスト設定に {{ub1_password}} などの形で利用できる.
- ub1_password: xxxx
  ub2_password: yyyy
  vub1_user: john
  vub1_ipv4: 43.xxx.xxx.xxx

:wqで閉じる.
中身はテキストファイルになっていて

vault.yaml
$ANSIBLE_VAULT;1.1;AES256
343231613831313161346165376530353264373037376...

のように暗号化されている.


ここまでの感想

Ansibleはシンプルと言われるがどちらかというとイージーという意味合いのほうがあっているのではないだろうか。暗黙に設定される内容が多すぎて、迷いそう。とくに編集するときに、プログラミング言語と違って、参照する変数がどこのファイルから来ているのか明示的にわからないので、絵で図示などをしながらなどしておかないとあとでわけが分からなくなりそう.

今のところ新たに設定したファイルと利用したファイルは

├── inventory.yaml
└── vault.yaml  # 暗号化されたファイル
$HOME/.ssh
└── config  # inventory.yamlで参照される sshの設定
$HOME/.local/share
└──ansible_pwd.txt  # Ansibleにおける鍵, 外部に公開✘

Mermaidで図示しておくと安心かも

tomozosantomozosan

例: apt upgradeをすべてのホストに適用する.

apt_upgrade.yaml
---
- name: Update and upgrade apt packages
  hosts: all  # inventory.yamlのblockの名前で指定できる.
  become: true
  vars_files:  # ここで変数を適用するファイルを指定している.今回はリモートサーバーのパスワードやIPなどを参照してくれている.
    - vault.yaml
  tasks:
  - name: Update and upgrade apt packages
    apt: # このブロックはAnsibleがDebian系ディストリビューション用にビルドインで用意してくれてる.
      upgrade: yes
      update_cache: yes
      cache_valid_time: 86400

重要な点

inventory.yamlに設定したansible_become_pass: "{{ ub1_password }}"vault.yamlファイルから読み込んで自動で設定してくれるので、 Rootユーザーとして sudoコマンドなども実行できるようになっている. become: true で明示的にルートユーザーになることを指定している.

# -vvは冗長な出力をしてくれる. vの数で出力内容がより冗長になる 例: -vvvもっと冗長に
ansible-playbook --vault-password-file $HOME/.local/share/ansible_pwd.txt -i inventory.yaml apt_upgrade.yaml -vv
tomozosantomozosan

上の図を見ていて思ったこと

  • あれ ? apt_upgrade.yaml -> vault.yamlという依存の向きは実際には apt_upgrade.yamlで vault.yamlの変数利用してないからいらなくね?
  • シンプルに inventory.yaml -> vault.yamlの記述をinventory.yamlに書いたらいいやん?
    さっきのファイルに
inventory.yaml
vars_files:
  - vault.yaml

追加したらいけるんじゃね?
結果 => だめ
どうしてだめな仕様になっているのかはわからん..
とりあえず変数が記述されたファイルを読み込むには playbook.yamlのほうで明示しないとエラーになるらしい.

tomozosantomozosan

Go をInstallもしくはUpgradeするスクリプトをすべてのホストに適用する.

公式に従って /usr/local/go に実行バイナリ /usr/local/bin/goをinstallするという想定. Ansibleでパスを指定するときにシェルスクリプト内では絶対パスで指定しないとうまくいかなかった.
なにかうまい方法で$HOME/.bashrcなどの環境変数の設定を読みに行くこともできるのかもしれないが、今のところやり方はわからない.

GoをInstallするシェルスクリプトをリモートにコピーして実行するという作戦.
リモートの環境変数の読み込みが環境によってまちまちなので、Debianだけでやるなら、絶対パス決め打ちのほうが確実.

src/install_go.sh
#!/usr/bin/bash

GO_JSON=$(curl -s "https://go.dev/dl/?mode=json")

LATEST_VERSION=$(echo "$GO_JSON" | jq -r '.[0].version')
echo Latest version go: "$LATEST_VERSION"


if ! /usr/local/go/bin/go version; then
  echo "Go is not installed"
  CURRENT_VERSION=""
else
  CURRENT_VERSION=$(/usr/local/go/bin/go version | awk '{print $3}')
  echo Current version go: "$CURRENT_VERSION"
fi

ARCH_NAME=$(uname -m)
echo "Arch name" "$ARCH_NAME"
if [ "$ARCH_NAME" = "x86_64" ]; then
    ARCH_NAME="amd64"
fi

if [ "$ARCH_NAME" = "aarch64" ]; then
    ARCH_NAME="arm64"
fi


if [ "$LATEST_VERSION" = "$CURRENT_VERSION" ]; then
    echo "The go version is latest" "$CURRENT_VERSION"
    exit 0
fi

FILE_NAME="$LATEST_VERSION".linux-"$ARCH_NAME".tar.gz
DOWNLOAD_URL="https://go.dev/dl/$FILE_NAME"

echo "$DOWNLOAD_URL"
mkdir -p /tmp/go_install
wget -P /tmp/go_install "$DOWNLOAD_URL"
tar -xzf /tmp/go_install/"$FILE_NAME" -C /tmp/go_install
INSTALLED=/tmp/go_install/go

if [ -z "$CURRENT_VERSION" ]; then
    sudo mv "$INSTALLED" /usr/local
    # Intentionally not expand the $PATH variable because of
    echo 'export PATH="$PATH":/usr/local/go/bin:"$HOME"/go/bin' >> "$HOME"/.bashrc
    source "$HOME"/.bashrc && \
    echo "Installed go version:" "$(go version)" "installed"
else
    # Remove the current version
    sudo rm -fr /usr/local/go
    sudo mv "$INSTALLED" /usr/local
    echo "New go version:" "$(/usr/local/go/bin/go version)" "installed"
fi

rm -fr /tmp/go_install

以下のようなPlaybookから実行するようにする.

install_go.yaml
---
- name: Copy Go install code
  hosts: all
  vars_files:
    - vault.yaml
  tasks:
    - name: Create ~/script directory
      file:
        path: "{{ ansible_env.HOME }}/scripts"
        state: directory
        mode: '0755'

    - name: Copy install_go.sh file
      copy:
        src: "src/install_go.sh"
        dest: "{{ ansible_env.HOME }}/scripts/install_go.sh"
        mode: '0644'

    - name: Execute install_go.sh file
      shell: /usr/bin/bash "{{ ansible_env.HOME }}/scripts/install_go.sh"
      become: true
     # become_method: sudoとするとsudo がついたコマンド以外はログインユーザーとして実行する
      become_method: sudo
ansible-playbook --vault-password-file $HOME/.local/share/ansible_pwd.txt -i inventory.yaml go_install.yaml -vv
tomozosantomozosan

ここまでのDirectory構成

├── install_go.yaml  # playbook
├──apt_upgrade.yaml # playbook
├── inventory.yaml # sshの接続設定とか
├── src
│   └── install_go.sh  # リモートに渡したいやつら
└── vault.yaml  # 秘密のやーつ
tomozosantomozosan

Go install upgradeの別のPlaybookの書き方

ChatGPT-4に手伝ってもらったやり方. シェルスクリプトの実行内容をすべてAnsibleの機能だけでやっている.
こちらのほうが Ansibleの機能をフルに利用してるので、実行時の出力が何が起こっているか見やすい.
set_factregisterなどを用いて変数を指定して他のブロックでも利用できるということらしい.

go_install2.yaml
- hosts: all
  become: true
  vars_files:
    - vault.yaml
  tasks:
    - name: Goの最新バージョン情報を取得
      uri:
        url: https://go.dev/dl/?mode=json
        return_content: yes
      register: go_latest

    - name: Goの最新バージョンを変数に設定
      set_fact:
        # Jinja2のjson arrayから0番目のindex `first`を指定するやり方
        latest_go_version: "{{ (go_latest.json | first).version }}"  

    - name: 現在のGoバージョンを確認
      shell: /usr/local/go/bin/go version | awk '{print $3}'
      register: current_go_version
      ignore_errors: yes

    - name: 現在と最新のGoバージョンを表示
      debug:
        msg: "現在のGoバージョンは: {{ current_go_version.stdout }}, 最新のGoバージョンは: {{ latest_go_version }}です。"


    - name: アーキテクチャを確認
      set_fact:
        arch_name: "{{ 'amd64' if ansible_architecture == 'x86_64' else ('arm64' if ansible_architecture == 'aarch64' else ansible_architecture) }}"

    - name: 最新バージョンをダウンロードして展開(アップグレードが必要な場合)
      block:
        - name: ダウンロードURLを設定
          set_fact:
            go_download_url: "https://go.dev/dl/{{ latest_go_version }}.linux-{{ arch_name }}.tar.gz"
        - name: /tmp/go_installディレクトリを作成
          file:
            path: /tmp/go_install
            state: directory

        - name: Goのダウンロードと展開
          unarchive:
            src: "{{ go_download_url }}"
            dest: /tmp/go_install
            remote_src: yes

        - name: Goのインストール
          block:
            - name: 既存のGoを削除
              file:
                path: /usr/local/go
                state: absent

            - name: 新しいGoを移動
              command: mv /tmp/go_install/go /usr/local

            - name: 環境変数の更新
              lineinfile:
                path: "{{ ansible_env.HOME }}/.bashrc"
                line: 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin'
                create: yes

            - name: 一時ファイルの削除
              file:
                path: /tmp/go_install
                state: absent
      when: current_go_version.stdout != latest_go_version