Chuyển đến nội dung chính

レッスン 26: Ansible による自動化

PostgreSQL HA クラスターのデプロイ、構成管理、自動テスト、CI/CD 統合のための Ansible プレイブックを作成します。

🔒 DevSecOps — レッスン 26 レッスン 26: Ansible による自動化

Patroni と PostgreSQL の高可用性etcd

パート 6: 制作とケーススタディ_

xdev.asia_

目標

このレッスンの後、次のことを行います:

  • Ansible を使用して Patroni クラスターのデプロイメントを自動化する
  • プレイブックで構成を管理する_
  • 自動化を実装するテスト_
  • データベースの変更をCI/CDに統合_
  • コードとしてのインフラストラクチャの原則を使用_

1。 PostgreSQL の Ansible の基本

1.1。 Ansible_

# Install Ansible
sudo apt-get update
sudo apt-get install -y ansible

Or via pip

pip3 install ansible

Verify

ansible --version

ansible [core 2.15.5]

1.2 をインストールします。インベントリ ファイル_

# inventory.ini
[postgres_cluster]
pg-node1 ansible_host=10.0.1.11 ansible_user=ubuntu
pg-node2 ansible_host=10.0.1.12 ansible_user=ubuntu
pg-node3 ansible_host=10.0.1.13 ansible_user=ubuntu

[etcd_cluster] etcd-node1 ansible_host=10.0.1.11 ansible_user=ubuntu etcd-node2 ansible_host=10.0.1.12 ansible_user=ubuntu etcd-node3 ansible_host=10.0.1.13 ansible_user=ubuntu

[all:vars] ansible_python_interpreter=/usr/bin/python3 ansible_ssh_private_key_file=~/.ssh/id_rsa

1.3。 Ansible 構成

# ansible.cfg
[defaults]
inventory = inventory.ini
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400

[ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s pipelining = True

2。 Patroni 導入プレイブック

2.1 を完了します。メイン プレイブック

# site.yml
  • name: Deploy PostgreSQL HA Cluster with Patroni hosts: all become: yes vars_files:
    • vars/main.yml roles:
    • common
    • etcd
    • postgresql
    • patroni
    • haproxy
    • monitoring

2.2。変数_

# vars/main.yml




PostgreSQL

postgresql_version: 18 postgresql_data_dir: /var/lib/postgresql/{{ postgresql_version }}/data postgresql_bin_dir: /usr/lib/postgresql/{{ postgresql_version }}/bin

Patroni

patroni_scope: postgres-cluster patroni_namespace: /service/

etcd

etcd_version: 3.5.11 etcd_data_dir: /var/lib/etcd etcd_initial_cluster_token: etcd-cluster-token

Cluster

cluster_nodes:

  • { name: pg-node1, ip: 10.0.1.11 }
  • { name: pg-node2, ip: 10.0.1.12 }
  • { name: pg-node3, ip: 10.0.1.13 }

Passwords (use Ansible Vault in production!)

postgres_password: !vault | $ANSIBLE_VAULT;1.1;AES256 ...encrypted... replicator_password: !vault | $ANSIBLE_VAULT;1.1;AES256 ...encrypted...

2.3。共通の役割

# roles/common/tasks/main.yml
  • name: Update apt cache apt: update_cache: yes cache_valid_time: 3600

  • name: Install common packages apt: name: - curl - wget - vim - git - htop - net-tools - python3 - python3-pip state: present

  • name: Set timezone timezone: name: Asia/Ho_Chi_Minh

  • name: Configure sysctl for PostgreSQL sysctl: name: "{{ item.name }}" value: "{{ item.value }}" state: present reload: yes loop:

    • { name: 'vm.swappiness', value: '1' }
    • { name: 'vm.overcommit_memory', value: '2' }
    • { name: 'vm.dirty_ratio', value: '10' }
    • { name: 'vm.dirty_background_ratio', value: '3' }
    • { name: 'net.ipv4.tcp_keepalive_time', value: '60' }
    • { name: 'net.ipv4.tcp_keepalive_intvl', value: '10' }
    • { name: 'net.ipv4.tcp_keepalive_probes', value: '6' }
  • name: Set system limits pam_limits: domain: postgres limit_type: "{{ item.type }}" limit_item: "{{ item.item }}" value: "{{ item.value }}" loop:

    • { type: 'soft', item: 'nofile', value: '65536' }
    • { type: 'hard', item: 'nofile', value: '65536' }
    • { type: 'soft', item: 'nproc', value: '8192' }
    • { type: 'hard', item: 'nproc', value: '8192' }

2.4。 etcd ロール

# roles/etcd/tasks/main.yml




  • name: Create etcd user user: name: etcd shell: /bin/false system: yes home: "{{ etcd_data_dir }}"

  • name: Download etcd get_url: url: "https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz" dest: /tmp/etcd.tar.gz

  • name: Extract etcd unarchive: src: /tmp/etcd.tar.gz dest: /tmp remote_src: yes

  • name: Install etcd binaries copy: src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}" dest: /usr/local/bin/{{ item }} mode: '0755' remote_src: yes loop:

    • etcd
    • etcdctl
  • name: Create etcd data directory file: path: "{{ etcd_data_dir }}" state: directory owner: etcd group: etcd mode: '0755'

  • name: Template etcd systemd service template: src: etcd.service.j2 dest: /etc/systemd/system/etcd.service notify: restart etcd

  • name: Start and enable etcd systemd: name: etcd state: started enabled: yes daemon_reload: yes

{# roles/etcd/templates/etcd.service.j2 #}
[Unit]
Description=etcd key-value store
Documentation=https://github.com/etcd-io/etcd
After=network.target

[Service] Type=notify User=etcd ExecStart=/usr/local/bin/etcd
--name {{ ansible_hostname }}
--data-dir {{ etcd_data_dir }}
--initial-advertise-peer-urls http://{{ ansible_default_ipv4.address }}:2380
--listen-peer-urls http://{{ ansible_default_ipv4.address }}:2380
--listen-client-urls http://{{ ansible_default_ipv4.address }}:2379,http://127.0.0.1:2379
--advertise-client-urls http://{{ ansible_default_ipv4.address }}:2379
--initial-cluster-token {{ etcd_initial_cluster_token }}
--initial-cluster {% for node in cluster_nodes %}{{ node.name }}=http://{{ node.ip }}:2380{% if not loop.last %},{% endif %}{% endfor %}
--initial-cluster-state new Restart=on-failure RestartSec=5

[Install] WantedBy=multi-user.target

2.5。 PostgreSQL ロール_

# roles/postgresql/tasks/main.yml
  • name: Add PostgreSQL apt key apt_key: url: https://www.postgresql.org/media/keys/ACCC4CF8.asc state: present

  • name: Add PostgreSQL repository apt_repository: repo: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main" state: present

  • name: Install PostgreSQL apt: name: - "postgresql-{{ postgresql_version }}" - "postgresql-contrib-{{ postgresql_version }}" - "postgresql-server-dev-{{ postgresql_version }}" state: present update_cache: yes

  • name: Stop and disable PostgreSQL (managed by Patroni) systemd: name: "postgresql@{{ postgresql_version }}-main" state: stopped enabled: no ignore_errors: yes

  • name: Create PostgreSQL directories file: path: "{{ item }}" state: directory owner: postgres group: postgres mode: '0700' loop:

    • "{{ postgresql_data_dir }}"
    • /var/lib/postgresql/wal_archive
    • /var/lib/postgresql/backups

2.6。パトローニの役割_

# roles/patroni/tasks/main.yml




  • name: Install Python dependencies pip: name: - patroni[etcd] - psycopg2-binary - python-etcd state: present executable: pip3

  • name: Create Patroni configuration directory file: path: /etc/patroni state: directory owner: postgres group: postgres mode: '0755'

  • name: Template Patroni configuration template: src: patroni.yml.j2 dest: /etc/patroni/patroni.yml owner: postgres group: postgres mode: '0600' notify: restart patroni

  • name: Template Patroni systemd service template: src: patroni.service.j2 dest: /etc/systemd/system/patroni.service notify: restart patroni

  • name: Start and enable Patroni systemd: name: patroni state: started enabled: yes daemon_reload: yes

  • name: Wait for Patroni to be ready wait_for: port: 8008 timeout: 60

{# roles/patroni/templates/patroni.yml.j2 #}
scope: {{ patroni_scope }}
name: {{ ansible_hostname }}

restapi: listen: {{ ansible_default_ipv4.address }}:8008 connect_address: {{ ansible_default_ipv4.address }}:8008

etcd: hosts: {% for node in cluster_nodes %}{{ node.ip }}:2379{% if not loop.last %},{% endif %}{% endfor %}

bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 postgresql: use_pg_rewind: true use_slots: true parameters: max_connections: 100 shared_buffers: 256MB effective_cache_size: 1GB maintenance_work_mem: 64MB checkpoint_completion_target: 0.9 wal_buffers: 16MB default_statistics_target: 100 random_page_cost: 1.1 effective_io_concurrency: 200 work_mem: 2621kB min_wal_size: 1GB max_wal_size: 4GB max_worker_processes: 4 max_parallel_workers_per_gather: 2 max_parallel_workers: 4 max_parallel_maintenance_workers: 2 wal_level: replica max_wal_senders: 10 max_replication_slots: 10 hot_standby: on archive_mode: on archive_command: 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'

initdb: - encoding: UTF8 - data-checksums

pg_hba: - host replication replicator 0.0.0.0/0 scram-sha-256 - host all all 0.0.0.0/0 scram-sha-256

postgresql: listen: 0.0.0.0:5432 connect_address: {{ ansible_default_ipv4.address }}:5432 data_dir: {{ postgresql_data_dir }} bin_dir: {{ postgresql_bin_dir }} authentication: replication: username: replicator password: {{ replicator_password }} superuser: username: postgres password: {{ postgres_password }}

tags: nofailover: false noloadbalance: false clonefrom: false nosync: false

3。導入_

3.1。プレイブック_

# Dry run (check mode)
ansible-playbook site.yml --check

Execute

ansible-playbook site.yml

With verbose output

ansible-playbook site.yml -vvv

Specific tags

ansible-playbook site.yml --tags "postgresql,patroni"

3.2 を実行します。導入を確認

# verify.yml
  • name: Verify Patroni cluster hosts: postgres_cluster tasks:
    • name: Check Patroni service systemd: name: patroni state: started register: patroni_status

    • name: Get cluster status command: patronictl -c /etc/patroni/patroni.yml list register: cluster_status changed_when: false

    • name: Display cluster status debug: var: cluster_status.stdout_lines

    • name: Check PostgreSQL connectivity postgresql_ping: db: postgres login_host: localhost login_user: postgres login_password: "{{ postgres_password }}" become_user: postgres

ansible-playbook verify.yml

4。構成管理

4.1。動的構成の更新

# update_config.yml


  • name: Update Patroni configuration hosts: postgres_cluster become: yes vars: new_max_connections: 200 tasks:
    • name: Update DCS configuration shell: | patronictl -c /etc/patroni/patroni.yml edit-config --apply - <<EOF postgresql: parameters: max_connections: {{ new_max_connections }} EOF run_once: true register: config_update

    • name: Restart nodes if needed shell: patronictl -c /etc/patroni/patroni.yml restart {{ patroni_scope }} {{ ansible_hostname }} --force when: "'Pending restart' in config_update.stdout"

4.2。バックアップの自動化_

# backup.yml


  • name: Perform PostgreSQL backup hosts: postgres_cluster[0] # Only on leader become: yes become_user: postgres vars: backup_dir: /var/lib/postgresql/backups backup_retention_days: 7 tasks:
    • name: Create backup directory file: path: "{{ backup_dir }}" state: directory mode: '0700'

    • name: Run pg_basebackup shell: | pg_basebackup -D {{ backup_dir }}/backup_$(date +%Y%m%d_%H%M%S)
      -Ft -z -Xs -P args: creates: "{{ backup_dir }}/backup_*"

    • name: Remove old backups find: paths: "{{ backup_dir }}" age: "{{ backup_retention_days }}d" recurse: yes register: old_backups

    • name: Delete old backups file: path: "{{ item.path }}" state: absent loop: "{{ old_backups.files }}"

5。テストの自動化

5.1。テスト用の分子_

# Install Molecule
pip3 install molecule molecule-plugins[docker]

Initialize Molecule scenario

cd roles/patroni molecule init scenario --driver-name docker

# roles/patroni/molecule/default/molecule.yml

dependency: name: galaxy driver: name: docker platforms:

  • name: pg-node1 image: ubuntu:22.04 pre_build_image: true
  • name: pg-node2 image: ubuntu:22.04 pre_build_image: true
  • name: pg-node3 image: ubuntu:22.04 pre_build_image: true provisioner: name: ansible verifier: name: ansible
# roles/patroni/molecule/default/verify.yml


  • name: Verify hosts: all tasks:
    • name: Check Patroni is running systemd: name: patroni state: started register: result failed_when: result.status.ActiveState != 'active'

    • name: Check cluster has leader shell: patronictl -c /etc/patroni/patroni.yml list | grep Leader register: leader_check failed_when: leader_check.rc != 0

# Run tests
molecule test

5.2。検証のためのテスト

pip3 install testinfra
# tests/test_patroni.py
import testinfra

def test_patroni_service(host): """Test Patroni service is running""" service = host.service("patroni") assert service.is_running assert service.is_enabled

def test_postgresql_port(host): """Test PostgreSQL port is listening""" assert host.socket("tcp://0.0.0.0:5432").is_listening

def test_patroni_rest_api(host): """Test Patroni REST API""" assert host.socket("tcp://0.0.0.0:8008").is_listening

def test_etcd_connectivity(host): """Test etcd cluster health""" cmd = host.run("etcdctl endpoint health") assert cmd.rc == 0 assert "healthy" in cmd.stdout

def test_cluster_has_leader(host): """Test cluster has exactly one leader""" cmd = host.run("patronictl -c /etc/patroni/patroni.yml list") assert cmd.rc == 0 assert cmd.stdout.count("Leader") == 1

def test_replication_lag(host): """Test replication lag is low""" cmd = host.run("sudo -u postgres psql -Atc "SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) FROM pg_stat_replication;"") if cmd.rc == 0 and cmd.stdout: lag = int(cmd.stdout.strip()) assert lag < 1048576 # < 1MB lag

# Run tests
pytest tests/test_patroni.py -v

6。 CI/CD の統合

6.1。 GitLab CI の例

# .gitlab-ci.yml
stages:

  • lint
  • test
  • deploy_staging
  • deploy_production

variables: ANSIBLE_FORCE_COLOR: "true"

lint: stage: lint image: python:3.11 before_script: - pip install ansible-lint yamllint script: - ansible-lint site.yml - yamllint . only: - merge_requests - main

test: stage: test image: python:3.11 before_script: - pip install molecule molecule-plugins[docker] testinfra script: - molecule test only: - merge_requests - main

deploy_staging: stage: deploy_staging image: python:3.11 before_script: - pip install ansible - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - script: - ansible-playbook -i inventory/staging.ini site.yml only: - main environment: name: staging

deploy_production: stage: deploy_production image: python:3.11 before_script: - pip install ansible - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - script: - ansible-playbook -i inventory/production.ini site.yml only: - tags when: manual environment: name: production

6.2。 GitHub アクションの例_

# .github/workflows/deploy.yml
name: Deploy Patroni Cluster

on: push: branches: [main] pull_request: branches: [main]

jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | pip install ansible-lint yamllint - name: Run linters run: | ansible-lint site.yml yamllint .

test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Molecule run: | pip install molecule molecule-plugins[docker] - name: Run Molecule tests run: | molecule test

deploy_staging: needs: [lint, test] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Ansible run: pip install ansible - name: Deploy to staging env: ANSIBLE_HOST_KEY_CHECKING: False run: | echo "${{ secrets.SSH_PRIVATE_KEY }}" > private_key chmod 600 private_key ansible-playbook -i inventory/staging.ini site.yml --private-key=private_key

7。ベスト プラクティス

✅ DO

  1. Ansible Vault を使用 - シークレットを暗号化
  2. べき等プレイブック - 複数回実行可能
  3. 分子でのテスト - 本番前
  4. バージョン管理 - すべての人にGitプレイブック_
  5. ドキュメント変数 - README をクリア
  6. ロールを使用 -組織
  7. タグタスク - 選択的実行
  8. CI/CD統合 - 自動化テスト
  9. ドライラン - 常に最初に確認
  10. 変更前のバックアップ - 安全性net

❌ しないでください

  1. ハードコーディングしないでください秘密_ - Vault を使用
  2. テストを省略しない - 最初にステージング_
  3. モジュールが存在する場合はシェルを使用しない - PostgreSQL モジュールを使用する_
  4. 失敗したタスクを無視しない - エラーを処理する_
  5. バックアップなしで実行しない - 常にバックアップする最初_

8。ラボ演習

ラボ 1: Ansible を使用してクラスターをデプロイ

タスク:

  1. 3 のインベントリをセットアップするノード_
  2. ロールを含むプレイブックの作成
  3. etcd クラスターのデプロイ_
  4. PostgreSQL + Patroni のデプロイ_
  5. クラスターの検証健康_

ラボ 2: 構成管理

タスク:

  1. max_connections を更新プレイブック_
  2. 夜間バックアップの自動化_
  3. DR フェイルオーバー用のプレイブックの作成_
  4. 構成ロールバックのテスト_
  5. すべてを文書化するプレイブック

ラボ 3: CI/CD パイプライン

タスク:

  1. GitLab/GitHub のセットアップアクション_
  2. リンティングステージを追加
  3. 分子テストを追加_
  4. ステージングに自動的にデプロイ_
  5. 本番環境の手動承認_

ラボ4: Molecule を使用したテスト

タスク:

  1. Molecule シナリオの初期化
  2. 検証テストの作成_
  3. テストの役割Docker コンテナ内_
  4. クラスター機能の検証_
  5. CI パイプラインへの統合_

9。概要_

自動化のメリット_

Manual vs Automated:

  • Deployment time: 4 hours → 15 minutes
  • Error rate: 30% → < 1%
  • Consistency: Variable → 100%
  • Documentation: Outdated → Self-documenting
  • Repeatability: Difficult → Trivial

Ansible の主要な概念____HTMLTAG_276__CODEBLOCK_26___

次のステップ

レッスン 27 では、表紙 災害復旧訓練:

  • DR計画手順
  • テスト方法
  • インシデント対応ワークフロー_
  • 事後分析
  • 完全な DR シミュレーション ラボ