🍬

Ansibleでロードバランサのアップグレードを自動化してみた

2022/02/06に公開

こんちわ、今回はF5の自動化をしてみました。

設計と概要

今回の目的は主に多様性です。
HAであろうが、プラットフォームがなんであれ、複数のF5をシームレスにアップグレードできるプレイが目標となってます。

必要条件:
■アップグレード用のISOはパブリッククラウド上のストレージに存在し(今回はAzure Blob)、アップグレード時にF5がプルする。
■F5のライセンスリアクティベーションはライセンスサーバー経由
■上記の動作などF5がインターネット接続を必要とする場合、プロキシ経由で通信を確保する(もちろん、プロキシなしでの接続も対応要)
■アップグレード作業はAnsibleにて、エンドツーエンドで行われる
■対象のF5が隔離ネットワークに存在する場合、踏み台経由での接続も要対応
■対象のF5がHAでも冗長なしでも1つのプレイブックでシームレスに要対応

では、動作とプレイブックの解説です
Repo: https://gitlab.com/knagatori/ansible-f5-upgrade
Requirement: https://galaxy.ansible.com/f5networks/f5_modules

主にこちらからアイディアとヒントいただきました:
https://github.com/twpsyn/f5-upgrade

ディレクトリはこんな感じ:

bigip
├── bigip_upgrade.yaml
├── host_vars
│   └── samplelb.yaml
├── roles
│   ├── bigip_cleanup_image
│   ├── bigip_download_image
│   ├── bigip_license_reactivate_active
│   ├── bigip_license_reactivate_standby
│   ├── bigip_software_activation_active
│   ├── bigip_software_activation_standby
│   ├── bigip_traffic_failover
│   └── bigip_ucs_backup

メインのプレイはこんな感じです:

---
- name: Upgrade software on F5
  hosts: bigip
  gather_facts: no
  vars_prompt:
    - name: ansible_user
      prompt: Input F5 TMUI Username
      private: no

    - name: ansible_password
      prompt: Input F5 TMUI Password
      private: yes
      unsafe: yes

  pre_tasks:
  - name: Obtaining HA Information
    bigip_command:
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
      warn: no
      match: "any"
      commands:
        - bash -c "cat /var/prompt/cmiSyncStatus"
      wait_for:
        - result[0] contains Standalone
        - result[0] contains In Sync
    register: result
    any_errors_fatal: true
    delegate_to: localhost

  - name: Setting facts
    set_fact:
      ha_info: "{{ result }}"
      cacheable: yes

  - name: Obtaining HA State
    bigip_command:
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
      warn: no
      match: "any"
      commands:
        - bash -c "cat /var/prompt/ps1"
      wait_for:
        - result[0] contains Active
        - result[0] contains Standby
    register: result  
    any_errors_fatal: true
    delegate_to: localhost

  - name: Setting facts
    set_fact:
      ha_state: "{{ result }}"
      cacheable: yes

  roles:
    - bigip_download_image
    - bigip_ucs_backup
    - bigip_license_reactivate_standby
    - bigip_software_activation_standby
    - bigip_traffic_failover
    - bigip_license_reactivate_active
    - bigip_software_activation_active
    - bigip_cleanup_image

1. ISOイメージのダウンロード

まずはISOイメージをF5にアップロードする所からです。
Ansibleモジュールでコピー専門のものがありますが、ローカルシステムからのみの転送となるので、スケーラブルなものを書くことが難しかったです。
https://docs.ansible.com/ansible/latest/collections/f5networks/f5_modules/bigip_file_copy_module.html
また、delegate_toによる踏み台サーバの使用を考えると、そこら中にISOファイルを散蒔く必要があったので、これはボツにしました。

結局F5のbash上のcurlを使ってパブリッククラウドに存在するファイルを落すで完結してます。

  - name: Downloading the image file
    bigip_command:
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
      warn: no
      match: "any"
      commands:
        - bash -c 'cd /shared/images && curl -L --proxy {{ upstream_proxy }}:{{ upstream_proxy_port }} -o {{ image }} {{ azure_sas_image }}'
      retries: 1
    ignore_errors: true
    delegate_to: {{ jumphost }}

問題点:SASのURI形式上エスケープ文字が発生する為、URIをvarとして使用する場合、埋め込みが必要になります。
tmshでのテストでは?が問題になったのですが、Ansibleからのvarハンドリングはまたそれと異り、結論プレイブック上では&のみ埋め込む事をワークアラウンドにしてます。

サンプルSAS:https://hogehoge.blob.core.windows.net/others/BIGIP-14.1.4.5-0.0.7.iso?sp=r&st=2022-02-01T01:18:59Z&se=2022-02-01T09:18:59Z&spr=https&sv=2020-08-04&sr=b&sig=xOxO1ST1%2FP3w48DpsytcKQmpLhYDNEFMuZ3Mf37PgLE%3D

プレイブック上のSAS:https://hogehoge.blob.core.windows.net/others/BIGIP-14.1.4.5-0.0.7.iso?sp=r\&st=2022-02-01T01:18:59Z\&se=2022-02-01T09:18:59Z\&spr=https\&sv=2020-08-04\&sr=b\&sig=xOxO1ST1%2FP3w48DpsytcKQmpLhYDNEFMuZ3Mf37PgLE%3D

2. 冗長ステータスの棲み分け

次に、冗長ステータスの確認です。
極力ダウンタイム無しでアップグレードを実行したい(また、通常の追加作業などでも、HA間でシンキングしているのであれば、Activeのみへの変更でOK)ので、これはどうしても必要でした。
あいにく、既存のfactで利用できるものがなかったので、またbigip_commandに活躍してもらいました。

ここでは、/var/prompt/ps1と、cmiSyncStatusでのアウトプットをカスタムのfactとして登録し、それを利用してます。
ちなみに、シンキングが壊れている場合はfailします。

  - name: Obtaining HA Information
    bigip_command:
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
      warn: no
      match: "any"
      commands:
        - bash -c "cat /var/prompt/cmiSyncStatus"
      wait_for:
        - result[0] contains Standalone
        - result[0] contains In Sync
    register: result
    any_errors_fatal: true
    delegate_to: localhost

  - name: Setting facts
    set_fact:
      ha_info: "{{ result }}"
      cacheable: yes

  - name: Obtaining HA State
    bigip_command:
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
      warn: no
      match: "any"
      commands:
        - bash -c "cat /var/prompt/ps1"
      wait_for:
        - result[0] contains Active
        - result[0] contains Standby
    register: result  
    any_errors_fatal: true
    delegate_to: localhost

  - name: Setting facts
    set_fact:
      ha_state: "{{ result }}"
      cacheable: yes

スタンドアローン機は必然的にActiveになるので、どっちでも適用できるようになります。
例えばスタンバイ機のみでの実行タスク:

  - name: test cmd
    bigip_command:
    provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
    warn: no
    match: "any"
    commands:
        - bash -c "echo $HOSTNAME"
    delegate_to: localhost
    when: ansible_facts['ha_state']['stdout'][0] == "Standby"

3. UCSコンフィグのバックアップ

これはAnsibleのオフィシャルモジュールで簡単にできました。

  - name: Preflight work - backing up to UCS
    bigip_ucs_fetch:
      create_on_missing: yes
      src: "{{ chg }}_{{ private_ip }}_pre-upgrade.ucs"
      dest: "{{ backup_loc }}/{{ chg }}_{{ private_ip }}_pre-upgrade.ucs"
      provider:
        server: "{{ private_ip }}"
        user: "{{ ansible_user }}"
        password: "{{ ansible_password }}"
        validate_certs: no
    delegate_to: "{{ jumphost }}"

4. ライセンスのリアクティベーション

コレ用:https://support.f5.com/csp/article/K7727

- name: Obtaining license information
  bigip_command:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
    warn: no
    match: "any"
    commands:
      - show sys license
  register: result  
  any_errors_fatal: true
  delegate_to: localhost

- name: Setting facts
  set_fact:
    license: "{{ result.stdout_lines[0][2][20:] }}"

- name: Reactivate bigip license using proxy on standby unit
  bigip_command:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
    commands:
      - bash -c "/usr/local/bin/SOAPLicenseClient --proxy {{ upstream_proxy }} --basekey {{ license }} --certupdatecheck"
    wait_for:
      - result[0] contains New license installed
  delegate_to: localhost
  when: ansible_facts['ha_state']['stdout'][0] == "Standby" and upstream_proxy is defined

- name: Reactivate bigip license on standby unit
  bigip_command:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
    commands:
      - bash -c "/usr/local/bin/SOAPLicenseClient --basekey {{ license }} --certupdatecheck"
    wait_for:
      - result[0] contains New license installed
  delegate_to: localhost
  when: ansible_facts['ha_state']['stdout'][0] == "Standby" and upstream_proxy is not defined

- name: Sleeping
  pause: 
    minutes: 5

5. イメージのインストールとアクティべーション

こちらもオフィシャルで。。。
Active機とStandby機でrolesに分けてます。
下記はStandby用:

- name: Installing and activating new image on standby unit 
  bigip_software_install:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
      timeout: 3200
    image: "{{ image }}"
    volume: "{{ volume }}"
    state: activated
  delegate_to: "{{ jumphost }}"
  when: ansible_facts['ha_state']['stdout'][0] == "Standby"
  any_errors_fatal: true

- name: Sleeping
  pause:
    minutes: 10

- name: Confirming device status of standby unit before continuing
  bigip_command:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
    warn: no
    match: "any"
    commands:
      - bash -c "cat /var/prompt/ps1"
    wait_for:
      - result[0] contains Standby
    retries: 12
    interval: 10
  register: result  
  any_errors_fatal: true
  delegate_to: "{{ jumphost }}"
  when: ansible_facts['ha_state']['stdout'][0] == "Standby"

- name: Sleeping
  pause:
    minutes: 5

6. 後片付け

bash -c 'rm -f /shared/image/{{ image }}でアップロードしたイメージを消去します。
個人的にこれは今後オフィシャルのを使ってabsentフラグでもいいかな。

---
- name: Cleaning up image after upgrade
  bigip_command:
    provider:
      server: "{{ private_ip }}"
      user: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      validate_certs: no
    warn: no
    match: "any"
    commands:
      - bash -c 'rm -f /shared/images/{{ image }}'
      - bash -c 'rm -f /shared/images/{{ image }}.md5'
  any_errors_fatal: true
  delegate_to: "{{ jumphost }}"

今後の課題

■APIでSASのURIを取得する際に、Ansibleでそのまま利用できる形式にテイラーする必要性がある。他の外部ストレージもテスト要。
■Curl経由でのISOダウンロードがどうしても不可能(ファイアーウォール等の影響で)な場合、ローカルからISOをアップロードするプレイも必要。
■オフィシャルのモジュールを使った際、イメージのインストールとアクティべーションを別で対応するのが不可能(?)みたい。カスタムコマンドで対応すべきか。
■アップグレード前後のステートチェックにどうインタグレートするか。

Discussion