APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

【GitLab】ブランチ戦略にGitLab Flowを採用して、GitLab CI/CDでIaCの自動デプロイを実現する

こんにちは、クラウド事業部 CI/CD導入支援サービスチームの西野です。

Ansible PlaybookなどのIaCのコードをGitで管理する際に、どんなブランチ戦略を採用していますか?
ブランチ戦略とは、ソフトウェア開発において、Gitのブランチ機能をどのように使い、管理するかを定めたルールのことです。
代表的なものとしては、以下の3種類があります。

  • Git Flow
  • GitHub Flow
  • GitLab Flow

GitLab Flowについて、詳細は後述しますが、環境に対応するブランチを保持するという考え方のブランチ戦略なので、IaCのコード管理と相性が良いのでは、と考えました。
今回はAnsible Playbookのコード管理にGitLab Flowをブランチ戦略として採用し、GitLab CI/CDを利用してIaCの自動デプロイを実現する検証を行いました。

GitLab Flowとは

GitLab Flow とは、GitLab社が提唱しているGitブランチ戦略です。
GitHub FlowとGit Flowの中間的な位置づけで、以下のブランチを主軸に運用します。

  • main (master)
  • pre-production
  • production
  • feature/hotfix

大きな特徴としては、環境に対応したブランチが存在していることです。

GitLab Flowを採用した場合、以下のようなリリースフローになります。

  1. mainブランチからfeatureブランチを作成
  2. featureブランチでコードを更新
  3. featureブランチからmainブランチにマージ
    • featureブランチはマージ後に削除
  4. mainブランチから開発環境にデプロイ
  5. 開発環境でのテスト完了後、mainブランチからpre-productionブランチにマージ
  6. pre-productionブランチのコードをステージング環境にデプロイ
  7. ステージング環境でのテスト完了後、pre-productionブランチからproductionブランチにマージ
  8. productionブランチのコードを本番環境にデプロイ

上記説明では、「開発環境」「ステージング環境」「本番環境」という環境名を用いていますが、 現場によって環境の呼称は異なる場合が多いです。適宜読み替えてイメージしてください。
原則、マージは一方向のみという制約があり、誤って逆方向にマージすると修正が大変になるので、ルールを徹底する必要があります。
緊急対応時も、基本的にhotfix -> main -> pre-production -> productionの順で マージするようにしましょう。

docs.gitlab.co.jp

※本記事ではGit FlowとGitHub Flowの説明は割愛します。

構成

本検証で利用する環境は、以下のサービス・ノードで構成します。

  • GitLab
    • GitLab.com(SaaS) Ultimate
  • GitLab Runner/Ansibleコントロールノード
    • AWS EC2インスタンス
    • Ubuntu 24.04
    • GitLab Runner 18.5.0
    • Ansible 2.19.3
  • ステージング環境用Ansibleターゲットノード
    • AWS EC2インスタンス
    • Ubuntu 24.04
    • NGINX
  • 本番環境用Ansibleターゲットノード
    • AWS EC2インスタンス
    • Ubuntu 24.04
    • NGINX

前提事項

  • 本検証では開発環境は省略する
  • 一般的な現場では、環境毎にVPCが分かれているが、本検証では、同一VPC上で、ステージング環境用Ansibleターゲットノード・本番環境用Ansibleターゲットノードを構築する
  • Ansibleコントロールノードも通信の関係上、環境毎に構築しているケースが多いが、本検証では、ステージング環境・本番環境で共有する

環境構築

Ansible Playbook準備

GitLab Duo Agent Platformの機能を使って作成しました。
以下のようなプロンプトを実行しました。

Hello Ansible!が表示されるnginxサーバーを構築するplaybookを作成してください。 フォルダー構造はrolesを利用した構成にしてください。 IPアドレスは自分で置き換えるので、置き換え対象とわかる書き方をしてください。

結果、少し手を入れることにはなりましたが、効率的に期待するPlaybookを作成することができました。
GitLabにプロジェクト作成し、このPlaybookをコミットします。

ansible-gitlab-flow/
├── site.yml                        # メインPlaybook
├── inventories/                    # インベントリディレクトリ
│   ├── production/                 # 本番環境設定
│   │   ├── hosts.yml              # 本番環境ホスト定義
│   │   └── group_vars/
│   │       └── all.yml            # 本番環境変数
│   └── staging/                   # ステージング環境設定
│       ├── hosts.yml              # ステージング環境ホスト定義
│       └── group_vars/
│           └── all.yml            # ステージング環境変数
└── roles/                         # Ansibleロールディレクトリ
    └── nginx/                     # nginxロール
        ├── defaults/
        │   └── main.yml           # デフォルト変数
        ├── handlers/
        │   └── main.yml           # ハンドラー定義
        ├── tasks/
        │   └── main.yml           # メインタスク
        ├── templates/
        │   ├── index.html.j2      # HTMLテンプレート
        │   └── nginx-site.conf.j2 # nginx設定テンプレート
        └── vars/
            └── main.yml           # ロール変数

ファイル一覧

site.yml

---
# メインのPlaybook - nginxサーバーをデプロイします
- name: Deploy nginx server with "Hello Ansible!" message
  hosts: webservers
  become: yes
  roles:
    - nginx

inventories/production/hosts.yml

all:
  children:
    webservers:
      hosts:
        nginx-prod-01:
          ansible_host: <本番環境のAnsibleターゲットノードのIP>
          ansible_user: ubuntu
          ansible_ssh_private_key_file: ~/.ssh/id_ed25519

inventories/staging/hosts.ymlansible_hostの値がステージング環境のAnsibleターゲットノードのIPに置き換わる

inventories/production/group_vars/all.yml

---
message: "Hello Ansible! (Production Environment)"

inventories/staging/group_vars/all.ymlmessageの値が"Hello Ansible! (Staging Environment)"に置き換わる

roles/nginx/defaults/main.yml

---
# nginxロールのデフォルト変数
nginx_port: 80
nginx_user: www-data
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_document_root: /var/www/html
nginx_server_name: localhost

roles/nginx/handlers/main.yml

---
# nginxサービスの再起動ハンドラー
- name: restart nginx
  service:
    name: "{{ nginx_service_name }}"
    state: restarted

- name: reload nginx
  service:
    name: "{{ nginx_service_name }}"
    state: reloaded

roles/nginx/tasks/main.yml

---
# nginxのインストールと設定タスク
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Install nginx
  package:
    name: "{{ nginx_package_name }}"
    state: present

- name: Create document root directory
  file:
    path: "{{ nginx_document_root }}"
    state: directory
    owner: "{{ nginx_user }}"
    group: "{{ nginx_user }}"
    mode: '0755'

- name: Create Hello Ansible! HTML file
  template:
    src: index.html.j2
    dest: "{{ nginx_document_root }}/index.html"
    owner: "{{ nginx_user }}"
    group: "{{ nginx_user }}"
    mode: '0644'
  notify: restart nginx

- name: Create nginx site configuration
  template:
    src: nginx-site.conf.j2
    dest: "{{ nginx_sites_available }}/default"
    owner: root
    group: root
    mode: '0644'
  notify: restart nginx

- name: Enable nginx site
  file:
    src: "{{ nginx_sites_available }}/default"
    dest: "{{ nginx_sites_enabled }}/default"
    state: link
  notify: restart nginx

- name: Remove default nginx configuration if it exists
  file:
    path: "{{ nginx_sites_enabled }}/default"
    state: absent
  when: false  # このタスクは通常は実行しない

- name: Start and enable nginx service
  service:
    name: "{{ nginx_service_name }}"
    state: started
    enabled: yes

roles/nginx/templates/index.html.j2

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello Ansible!</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            color: white;
        }
        .container {
            text-align: center;
            background: rgba(255, 255, 255, 0.1);
            padding: 50px;
            border-radius: 20px;
            backdrop-filter: blur(10px);
            box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
            border: 1px solid rgba(255, 255, 255, 0.18);
        }
        h1 {
            font-size: 3em;
            margin-bottom: 20px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
        }
        p {
            font-size: 1.2em;
            margin-bottom: 10px;
        }
        .info {
            margin-top: 30px;
            font-size: 0.9em;
            opacity: 0.8;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎉 {{ message }} 🎉</h1>
        <p>nginxサーバーが正常にデプロイされました!</p>
        <p>Ansibleによる自動化が成功しています。</p>
        <div class="info">
            <p>サーバー: {{ ansible_hostname }}</p>
            <p>IPアドレス: {{ ansible_default_ipv4.address }}</p>
            <p>デプロイ日時: {{ ansible_date_time.iso8601 }}</p>
        </div>
    </div>
</body>
</html>

roles/nginx/templates/nginx-site.conf.j2

server {
    listen {{ nginx_port }};
    listen [::]:{{ nginx_port }};

    server_name {{ nginx_server_name }};
    root {{ nginx_document_root }};
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    # ログ設定
    access_log /var/log/nginx/{{ nginx_server_name }}_access.log;
    error_log /var/log/nginx/{{ nginx_server_name }}_error.log;

    # セキュリティヘッダー
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

    # gzip圧縮
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
}

roles/nginx/vars/main.yml

---
# nginxロールの変数
nginx_package_name: nginx
nginx_service_name: nginx
nginx_config_path: /etc/nginx
nginx_sites_available: "{{ nginx_config_path }}/sites-available"
nginx_sites_enabled: "{{ nginx_config_path }}/sites-enabled"

環境固有の値の管理方法について

アプリケーションコード(例:Webアプリケーション)では、環境固有の値(データベース接続情報、APIキーなど)を外部から実行時に注入することが一般的です。
一方IaCの場合、環境固有の値はデプロイ時に適用される設定の一部であるため、コード管理において、環境固有の値をどのように管理するかを検討する必要があります。
本検証ではinvenroty/production/group_vars/all.ymlinvenroty/stagng/group_vars/all.ymlに環境固有の値を設定します。
各環境のブランチに、対応する環境以外の情報が存在する状態にはなりますが、GitLab CI/CDパイプラインでトリガーされるデプロイコマンドをブランチ毎に変えることで、デプロイ時に対応する環境の値が適用されます。

GitLab Runner/Ansible実行環境 セットアップ

GitLab Runner/AnsibleコントロールノードにAnsibleとGitLab Runnerをインストールし、GitLab.comからRunnerとして認識させます。
また、AnsibleターゲットノードへSSH接続が可能な状態にセットアップします。

  1. GitLab.comにRunner追加する
    1. GitLab.comのプロジェクト画面を開く
    2. 左メニュー設定 -> CI/CDをクリックする
    3. RunnerのメニューからプロジェクトRunnerを作成するをクリックする
    4. 以下の値を入力して、Runnerを作成をクリックする
      • タグ:ansible
      • Runnerの説明:ansible実行用Runner
    5. 以下の値を選択する
      • プラットフォーム:Linux
  2. GitLab Runner/AnsibleコントロールノードにGitLab Runnerをインストールする

    1. 以下のコマンドを実行する

       sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
       sudo chmod +x /usr/local/bin/gitlab-runner
       sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
       sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
      

      ※上記コマンドはGitLab.comのRunner登録画面でGitLab Runnerをインストールするにはどうすればいいですかをクリックすると表示される

    2. GitLab.comのRunner登録画面(項番1-5)のステップ1に表示されているgitlab-runner registerから始まるコマンドを実行する
      • Enter the GitLab instance URLはGitLabのURLを入力する(本検証はGitLab.comを利用するので、デフォルトのままEnterを押下)
      • Enter a name for the runner. This is stored only in the local config.toml fileはRunnerに付与する任意の名前を設定する(本検証ではデフォルトのままEnterを押下)
      • Enter an executorshellを選択する
      • Runnerの登録が完了すると以下が出力される

          Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
        
          Configuration (with the authentication token) was saved in "/home/ubuntu/.gitlab-runner/config.toml"
        
    3. 以下のコマンドを実行する

       sudo gitlab-runner start
      
    4. GitLab.comの左メニュー設定 -> CI/CDのRunnerのメニューから、新規登録したRunnerがオンラインになっていることを確認
  3. GitLab Runner/AnsibleコントロールノードにAnsibleをインストールする

    1. 以下のコマンドを実行する

       sudo apt update
       sudo apt install software-properties-common
       sudo apt-add-repository --yes --update ppa:ansible/ansible
       sudo apt install ansible
      

      参考:Ansible のインストール — Ansible Documentation

  4. GitLab Runner/AnsibleコントロールノードでSSH接続用のキーペアを作成する
    1. 以下のコマンドを実行する

       ssh-keygen -t ed25519
      
    2. ~/.sshディレクトリに秘密鍵(id_ed25519)、公開鍵(id_ed25519.pub)が生成されていることを確認する
  5. Ansibleターゲットノードの~/.ssh/authorized_keysに項番4で作成した公開鍵(id_ed25519.pub)の内容を追記する

GitLab CI/CDパイプライン実装

GitLab CI/CDパイプラインの定義ファイルである.gitlab-ci.ymlもGitLab Duo Agent Platformの機能を使って作成しました。 以下の機能を持つGitLab CI/CDパイプラインを作成しました。

  • すべてのブランチへのコミット・マージリクエスト作成をトリガーに構文チェックを実施
  • pre-productionブランチへのコミットをトリガーにステージング環境へのデプロイを実施
  • productionブランチへのコミットをトリガーにステージング環境へのデプロイを実施

Playbookを格納したGitLabプロジェクトにコミットします。

---
# GitLab CI/CD Pipeline for Ansible Deployment
# ブランチベースの自動デプロイメント設定

stages:
  - validate
  - deploy

variables:
  # Ansible設定
  ANSIBLE_HOST_KEY_CHECKING: "False"
  ANSIBLE_STDOUT_CALLBACK: "yaml"
  ANSIBLE_FORCE_COLOR: "true"

# Ansible構文チェック(全ブランチ対象)
validate_ansible:
  stage: validate
  tags:
    - ansible
  script:
    - echo "🔍 Ansible構文チェックを実行中..."
    - ansible-playbook --syntax-check site.yml
    - echo "✅ 構文チェック完了"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH

# ステージング環境デプロイ(pre-productionブランチ)
deploy_staging:
  stage: deploy
  tags:
    - ansible
  variables:
    DEPLOY_ENVIRONMENT: "staging"
    INVENTORY_PATH: "inventories/staging"
  script:
    - echo "ステージング環境へのデプロイを開始..."
    
    # インベントリファイルの存在確認
    - |
      if [ ! -f "$INVENTORY_PATH/hosts.yml" ]; then
        echo "❌ エラー: インベントリファイルが見つかりません: $INVENTORY_PATH/hosts.yml"
        exit 1
      fi
    
    # 接続テスト
    - echo "🔗 ターゲットサーバーへの接続テスト..."
    - ansible -i $INVENTORY_PATH all -m ping
    
    # Playbookの実行
    - echo "📦 nginx サーバーのデプロイを実行..."
    - ansible-playbook -i $INVENTORY_PATH site.yml
    
    - echo "✅ ステージング環境へのデプロイが完了しました!"
  rules:
    - if: $CI_COMMIT_BRANCH == "pre-production"
      when: always
  environment:
    name: staging
    url: http://<ステージング環境用AnsibleターゲットノードのパブリックIP>

# 本番環境デプロイ(productionブランチ)
deploy_production:
  stage: deploy
  tags:
    - ansible
  variables:
    DEPLOY_ENVIRONMENT: "production"
    INVENTORY_PATH: "inventories/production"
  script:
    - echo "🚀 本番環境へのデプロイを開始..."
    
    # インベントリファイルの存在確認
    - |
      if [ ! -f "$INVENTORY_PATH/hosts.yml" ]; then
        echo "❌ エラー: インベントリファイルが見つかりません: $INVENTORY_PATH/hosts.yml"
        exit 1
      fi
    
    # 接続テスト
    - echo "🔗 ターゲットサーバーへの接続テスト..."
    - ansible -i $INVENTORY_PATH all -m ping
    
    # Playbookの実行
    - echo "📦 nginx サーバーのデプロイを実行..."
    - ansible-playbook -i $INVENTORY_PATH site.yml
    
    - echo "✅ 本番環境へのデプロイが完了しました!"
  rules:
    - if: $CI_COMMIT_BRANCH == "production"
      when: manual  # 本番環境は手動実行
  environment:
    name: production
    url: http://<本番環境用AnsibleターゲットノードのパブリックIP>

保護ブランチの設定

mainブランチ、pre-productionブランチ、productionブランチは保護ブランチに設定します。
マージ・プッシュが可能なユーザーを限定することで、意図しないデプロイを防止することができます。
設定方法は以下の公式ドキュメントをご参照ください。

gitlab-docs.creationline.com

リリースフローの実践

Playbook・.gitlab-ci.ymlをコミットしたmainブランチから、pre-productionブランチ・productionブランチを作成し、初回デプロイを実行済みの状態から、リリースフローを実践します。
本番環境の初期状態は以下の通りです。

※ステージング環境はProductionの文字がStagingに置き換わります。

GitLab Flowで想定して、以下の手順でフローを実践します。

  1. mainブランチから、feature-testブランチを作成する
  2. feature-testブランチでコードを変更する
    • index.html<title>の値を修正する
    • 変数messageを変更する
  3. feature-testブランチからmainブランチにマージリクエストを作成し、マージする
    • マージリクエストの変更画面
    • マージリクエスト作成後/マージ後に、validate_ansibleのジョブが成功することを確認する
  4. (開発環境へのデプロイ確認はスキップ)
  5. mainブランチからpre-productionブランチにマージリクエストを作成し、マージする
    • マージリクエスト作成後に、validate_ansibleのジョブが成功することを確認する
    • マージ後に、validate_ansibledeploy_stagingのジョブが成功することを確認する
  6. ステージング環境にpre-productionブランチの内容が反映されていることを確認する
  7. pre-productionブランチからproductionブランチにマージリクエストを作成し、マージする
    • マージリクエスト作成後/マージ後に、validate_ansibleのジョブが成功することを確認する
  8. パイプラインの画面からdeploy_productionのジョブを手動で実行(上記スクリーンショットのアイコンを押下)する
    • deploy_productionのジョブが成功することを確認する
  9. 本番環境にproductionブランチの内容が反映されていることを確認する

GitLabの環境メニューから、どのコミットがデプロイされている状況か確認することができます。

※.gitlab-ci.ymlenvironmentの定義が必須です。

gitlab-docs.creationline.com

おわりに

ブランチ戦略にGitLab Flowを採用して、Ansible PlaybookをGitLab CI/CDで自動デプロイする例をご紹介しました。 「環境の再現性」と「設定の確実な管理」を重視するIaCにおいては、環境に対応するブランチを保持するというGitLab Flowの設計思想は高い親和性を持つと感じました。
GitLabのEnvironment(環境)の機能を活用し、「どのコードが、どの環境に適用されているか」をGitLabのダッシュボードに可視化することも、安定的なIaCの自動デプロイの運用に有効だと思います。

弊社はGitLabオープンパートナー認定を受けております。 また以下のようにCI/CDの導入を支援するサービスも行っているので、何かご相談したいことがあればお気軽にご連絡ください。

www.ap-com.co.jp