Ansible 作為常用的自動化工具,能有效簡化應用程式佈署流程。本文首先介紹 ansible-playbook 的基本執行方式,包括輸出解讀、遠端使用者設定、sudo 選項,以及其他常用選項如自訂清單檔案、詳細模式、變數定義、平行執行數量、連線型別和檢查模式。接著,以 Rocky Linux 系統佈署 Node.js 應用為例,示範如何撰寫 playbook 安裝 EPEL 和 Remi 儲存函式庫、匯入 GPG key、安裝 Node.js 和 npm,並組態防火牆開放 HTTP 服務。此外,文章也涵蓋了自動化佈署與持續整合(CI)的實踐,提供一個包含建置、測試和佈署階段的 CI Pipeline範例,並使用 Mermaid 語法圖示模擬節點間的通訊架構。最後,文章更進一步示範如何使用 Ansible 建立 Docker 環境,包含執行 Flask 應用程式的容器、MySQL 資料函式庫容器以及資料容器,並詳細說明 Vagrantfile 和 Ansible playbook 的撰寫、Docker image 建置、容器啟動與組態等步驟,提供一個完整且可擴充套件的 Docker 環境建置方案。

Ansible 實戰:自動化佈署 Node.js 應用

使用 ansible-playbook 執行 Playbook

執行 ansible-playbook 命令時,會顯示類別似以下的輸出:

playbook: playbook.yml
play #1 (all): host count=4
127.0.0.1
192.168.24.2
foo.example.com
bar.example.com

其中 count 是根據清單中定義的伺服器數量,後面跟著所有在清單中定義的主機列表。

設定遠端使用者與 Sudo 選項

若在 Playbook 中未明確定義 remote_user,Ansible 會假設使用清單檔案中特定主機的使用者名稱進行連線,如果沒有則回退到本地使用者名稱。可以使用 --user-u 選項明確定義遠端使用者:

$ ansible-playbook playbook.yml --user=johndoe

在某些情況下,需要將 sudo 密碼傳遞到遠端伺服器以執行 sudo 命令。這時需要使用 --ask-become-pass-K 選項。也可以使用 --become-b 選項強制所有任務都以 sudo 執行。此外,可以使用 --become-user 選項定義 sudo 使用者:

$ ansible-playbook playbook.yml --become --become-user=janedoe --ask-become-pass

如果未使用金鑰驗證來連線伺服器,可以使用 --ask-pass 選項。

其他常用的 ansible-playbook 選項

  • --inventory=PATH-i PATH:定義自訂的清單檔案路徑(預設是 /etc/ansible/hosts)。
  • --verbose-v:啟用詳細模式(顯示所有輸出,包括成功的選項)。可以使用 -vvvv 取得更多細節。
  • --extra-vars=VARS-e VARS:定義在 Playbook 中使用的變數,格式為 "key=value,key=value"
  • --forks=NUM-f NUM:設定並發執行的伺服器數量(整數)。設定為高於5的數字以增加並發任務的伺服器數量。
  • --connection=TYPE-c TYPE:指定連線型別(預設為 ssh;有時可能需要使用 local 在本地機器或遠端伺服器上執行 Playbook)。
  • --check: 在檢查模式(“Dry Run”)下執行 Playbook;所有在 Playbook 中定義的任務都會被檢查,但不會實際執行。

這些選項應該足夠開始在自己伺服器或虛擬機器上執行 Playbook。本章剩餘部分將介紹更真實的 Ansible Playbook。

案例研究:Rocky Linux Node.js 應用伺服器

安裝與組態 Node.js 應用

以下是一個簡單的 YAML Playbook 檔案(playbook.yml),用於組態 Rocky Linux 伺服器來執行 Node.js 應用:

---
- hosts: all
  become: yes

  vars:
    node_apps_location: /usr/local/opt/node

  tasks:
    # 加入 EPEL 和 Remi 儲存函式庫
    - name: Install EPEL repository
      yum:
        name: epel-release
        state: present

    - name: Import Remi GPG key
      rpm_key:
        key: https://rpms.remirepo.net/RPM-GPG-KEY-remi2018
        state: present

    - name: Add Remi repository
      yum_repository:
        name: remi
        description: Remi's RPM repository for Enterprise Linux 8 - $basearch
        baseurl: https://rpms.remirepo.net/enterprise/8/$basearch/
        gpgcheck: yes
        enabled: yes

    - name: Install Node.js (npm and all its dependencies)
      yum:
        name: npm
        state: present
        enablerepo: epel

    # 安裝 Node.js 應用並啟動服務(具體步驟省略)
內容解密:
  1. Install EPEL repository:此任務安裝 EPEL 儲存函式庫,這是 Red Hat Enterprise Linux 的額外軟體儲存函式庫。
  2. Import Remi GPG key:此任務匯入 Remi 儲存函式庫的 GPG 鑰匙,這是確保軟體包完整性和來源可靠性的一種方式。
  3. Add Remi repository:此任務新增 Remi 儲存函式庫,這個儲存函式庫提供了更新版本的 Node.js 和其他軟體包。
  4. Install Node.js (npm and all its dependencies):此任務安裝 npm 和其所有依賴專案。

這些步驟確保了系統上有最新版本的 Node.js,並且可以透過 npm 安裝和管理其他 JavaScript 模組。

其他組態與最佳實踐

在實際應用中,可能需要進一步組態防火牆、安全組、日誌記錄等方面。此外,還需要考慮到自動化佈署和持續整合(CI)流程。這些都是自動化維運中的重要環節。

防火牆組態

在安裝完 Node.js 應用後,通常需要組態防火牆以允許外部存取。以下是一個簡單的防火牆組態範例:

---
- hosts: all
  become: yes

  tasks:
    - name: Open port for Node.js application
      firewalld:
        service: http
        permanent: yes
        state: enabled

    - name: Reload firewalld to apply changes
      service:
        name: firewalld
        state: reloaded
內容解密:
  1. Open port for Node.js application:此任務開啟 HTTP 專案來允許外部存取。
  2. Reload firewalld to apply changes:此任務重新載入防火牆以應用變更。

這樣可以確保 Node.js 應用可以被外部存取。

自動化佈署與持續整合(CI)

在現代 DevOps 工作流程中,自動化佈署和持續整合(CI)是不可或缺的一部分。以下是一個簡單的 CI Pipeline範例:

stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - echo "Building the application..."
    - npm install

test_job:
  stage: test
  script:
    - echo "Running tests..."
    - npm test

deploy_job:
  stage: deploy
  script:
    - echo "Deploying the application..."
    - ansible-playbook deploy.yml --inventory inventory.ini --user ubuntu --private-key ~/.ssh/id_rsa --extra-vars "node_apps_location=/usr/local/opt/node"
內容解密:
  1. build_job:此階段負責構建應用程式,通常包括安裝依賴項。
  2. test_job:此階段負責執行測試來確保應用程式執行正常。
  3. deploy_job:此階段負責將應用程式佈署到生產環境。

這樣的Pipeline可以確保每次提交程式碼後都能自動構建、測試和佈署應用程式。

模擬節點間通訊圖表示意圖

  graph TD;
    A[Client] --> B[Load Balancer]
    B --> C[Node Server]
    B --> D[Node Server]
    C --> E[Database]
    D --> E[Database]

內容解密:

此圖示展示了客戶端透過負載平衡器存取兩個節點伺服器,這些節點伺服器再與分享資料函式庫進行互動。

建立 Flask 應用程式的 Docker 環境

接下來,玄貓將帶領讀者建立一個更實用的 Docker 環境。這個環境包含一個執行應用程式的容器(使用 Flask,一個輕量級的 Python 網頁框架),以及一個執行資料函式庫(MySQL)的容器,還有資料容器用來持久化 MySQL 資料函式庫。因為 MySQL 容器內部的資料會在容器停止時消失,所以需要一個獨立的資料容器來持久化這些資料。

Docker 環境的基本構建

首先,我們需要準備好 Vagrantfile 和 Ansible playbook。這些檔案將幫助我們在任何能夠執行 Ansible 和 Vagrant 的機器上進行測試。

Vagrantfile

首先,建立一個 docker 資料夾,並在其中建立 Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "geerlingguy/ubuntu2004"
  config.vm.network :private_network, ip: "192.168.33.39"
  config.ssh.insert_key = false

  config.vm.hostname = "docker-flask.test"
  config.vm.provider :virtualbox do |v|
    v.name = "docker-flask.test"
    v.memory = 1024
    v.cpus = 2
    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    v.customize ["modifyvm", :id, "--ioapic", "on"]
  end

  # Enable provisioning with Ansible.
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "provisioning/main.yml"
  end
end

此處我們使用了 Ubuntu 作為主機系統,並指定了一個 Ansible playbook(provisioning/main.yml)來進行設定。

Ansible Playbook

接下來,我們需要在 provisioning 資料夾中建立 main.yml

---
- hosts: all
  become: true

  vars:
    build_root: /vagrant/provisioning

  pre_tasks:
    - name: Update apt cache if needed.
      apt: update_cache=yes cache_valid_time=3600

  roles:
    - role: geerlingguy.docker

  tasks:
    - import_tasks: setup.yml
    - import_tasks: docker.yml

在此 Playbook 中,我們使用了 become 標籤來確保所有任務都以 root 許可權執行。這是因為 Docker 需要 root 許可權或使用者必須在 docker 裝置群組中才能執行命令。我們還設定了一個 build_root 論據變數,這將用於告知 Docker 在虛擬機器內建構容器。

我們使用的是 geerlingguy.docker role,這個 role 不需要額外設定或組態,但必須使用 ansible-galaxy 安裝。建立 requirements.yml

roles:
  - name: geerlingguy.docker

安裝 role:

$ ansible-galaxy install -r requirements.yml

安裝必要的工具

接下來,我們需要安裝一些必要的工具,包括 pip 和 Docker Python 函式庫。在 provisioning 資料夾中建立 setup.yml

---
- name: Install Pip.
  apt: name=python3-pip state=present

- name: Install Docker Python library.
  pip: name=docker state=present

Ansible 需要 Docker Python 函式庫來透過 Python 控制 Docker。

建構和啟動 Docker 容器

這是 Playbook 的核心部分:在 provisioning 資料夾中建立 docker.yml

---
- name: Build Docker images from Dockerfiles.
  docker_image:
    name: "{{ item.name }}"
    tag: "{{ item.tag }}"
    source: build
    build:
      path: "{{ build_root }}/{{ item.directory }}"
    pull: false
    state: present
  with_items:
    - { name: data, tag: latest, directory: data }
    - { name: flask, tag: latest, directory: www }
    - { name: db, tag: latest, directory: db }

內容解密:

  1. Docker images 架構:我們使用 docker_image 模組來從 Dockerfiles 建構映像檔。每個映像檔都有其名稱、標籤和來源路徑。
  2. 路徑設定:由於我們是在虛擬機器內建構映像檔,所以使用了 /vagrant/provisioning/[directory] 作為路徑。
  3. 標籤的作用:標籤類別似於 Git 的標籤,允許未來的 Docker 命令使用這些特定版本的映像檔。

啟動容器

- name: Run a Data container.
  docker_container:
    image: data:latest
    name: data
    state: present

- name: Run a Flask container.
  docker_container:
    image: www:latest
    name: www
    state: started
    command: python /opt/www/index.py
    ports: "80:80"

- name: Run a MySQL container.
  docker_container:
    image: db:latest
    name: db
    state: started
    volumes_from: data
    ports: "3306:3306"
    env:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: flask
      MYSQL_USER: flask
      MYSQL_PASSWORD: flask

內容解密:

  1. 資料容器:確保資料容器存在即可。
  2. Flask 應用程式容器:確保應用程式不僅執行,還能持續執行。這裡我們提供了一個明確的命令來執行應用程式。
    command: python /opt/www/index.py
    
    呼叫指令碼會使應用程式在前景執行並將所有日誌記錄到 stdout。
  3. MySQL 資料函式庫容器:設定 MySQL 資料函式庫的基本組態,包括環境變數和連線埠。
內容解密:
  1. Flask 應用程式MySQL 資料函式庫之間有直接連線。
  2. Data Container 用來持久化 MySQL 資料函式庫中的資料。
  3. 每個服務都在其各自的 Docker 容器中執行。

透過這些步驟,玄貓成功地建立了一個完整且可擴充套件的 Flask 應用程式與 MySQL 資料函式庫的 Docker 環境。這樣的架構不僅能夠在不同機器上進行測試,還能確保資料的一致性和永續性。