こんにちは、クラウド事業部 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を採用した場合、以下のようなリリースフローになります。
- mainブランチからfeatureブランチを作成
- featureブランチでコードを更新
- featureブランチからmainブランチにマージ
- featureブランチはマージ後に削除
- mainブランチから開発環境にデプロイ
- 開発環境でのテスト完了後、mainブランチからpre-productionブランチにマージ
- pre-productionブランチのコードをステージング環境にデプロイ
- ステージング環境でのテスト完了後、pre-productionブランチからproductionブランチにマージ
- productionブランチのコードを本番環境にデプロイ
上記説明では、「開発環境」「ステージング環境」「本番環境」という環境名を用いていますが、 現場によって環境の呼称は異なる場合が多いです。適宜読み替えてイメージしてください。
原則、マージは一方向のみという制約があり、誤って逆方向にマージすると修正が大変になるので、ルールを徹底する必要があります。
緊急対応時も、基本的にhotfix -> main -> pre-production -> productionの順で マージするようにしましょう。
※本記事では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.ymlはansible_hostの値がステージング環境のAnsibleターゲットノードのIPに置き換わる
inventories/production/group_vars/all.yml
--- message: "Hello Ansible! (Production Environment)"
※inventories/staging/group_vars/all.ymlはmessageの値が"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.ymlとinvenroty/stagng/group_vars/all.ymlに環境固有の値を設定します。
各環境のブランチに、対応する環境以外の情報が存在する状態にはなりますが、GitLab CI/CDパイプラインでトリガーされるデプロイコマンドをブランチ毎に変えることで、デプロイ時に対応する環境の値が適用されます。
GitLab Runner/Ansible実行環境 セットアップ
GitLab Runner/AnsibleコントロールノードにAnsibleとGitLab Runnerをインストールし、GitLab.comからRunnerとして認識させます。
また、AnsibleターゲットノードへSSH接続が可能な状態にセットアップします。
- GitLab.comにRunner追加する
- GitLab.comのプロジェクト画面を開く
- 左メニュー
設定->CI/CDをクリックする
RunnerのメニューからプロジェクトRunnerを作成するをクリックする
- 以下の値を入力して、
Runnerを作成をクリックする- タグ:
ansible - Runnerの説明:
ansible実行用Runner
- タグ:
- 以下の値を選択する
- プラットフォーム:Linux

- プラットフォーム:Linux
GitLab Runner/AnsibleコントロールノードにGitLab Runnerをインストールする
以下のコマンドを実行する
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をインストールするにはどうすればいいですかをクリックすると表示される- 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 executorはshellを選択する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"
以下のコマンドを実行する
sudo gitlab-runner start- GitLab.comの左メニュー
設定->CI/CDのRunnerのメニューから、新規登録したRunnerがオンラインになっていることを確認
GitLab Runner/AnsibleコントロールノードにAnsibleをインストールする
以下のコマンドを実行する
sudo apt update sudo apt install software-properties-common sudo apt-add-repository --yes --update ppa:ansible/ansible sudo apt install ansible
- GitLab Runner/AnsibleコントロールノードでSSH接続用のキーペアを作成する
以下のコマンドを実行する
ssh-keygen -t ed25519~/.sshディレクトリに秘密鍵(id_ed25519)、公開鍵(id_ed25519.pub)が生成されていることを確認する
- 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ブランチは保護ブランチに設定します。
マージ・プッシュが可能なユーザーを限定することで、意図しないデプロイを防止することができます。
設定方法は以下の公式ドキュメントをご参照ください。
リリースフローの実践
Playbook・.gitlab-ci.ymlをコミットしたmainブランチから、pre-productionブランチ・productionブランチを作成し、初回デプロイを実行済みの状態から、リリースフローを実践します。
本番環境の初期状態は以下の通りです。
※ステージング環境はProductionの文字がStagingに置き換わります。
GitLab Flowで想定して、以下の手順でフローを実践します。
- mainブランチから、feature-testブランチを作成する
- feature-testブランチでコードを変更する
index.htmlの<title>の値を修正する- 変数
messageを変更する
- feature-testブランチからmainブランチにマージリクエストを作成し、マージする
- マージリクエストの
変更画面
- マージリクエスト作成後/マージ後に、
validate_ansibleのジョブが成功することを確認する
- マージリクエストの
- (開発環境へのデプロイ確認はスキップ)
- mainブランチからpre-productionブランチにマージリクエストを作成し、マージする
- マージリクエスト作成後に、
validate_ansibleのジョブが成功することを確認する - マージ後に、
validate_ansible・deploy_stagingのジョブが成功することを確認する
- マージリクエスト作成後に、
- ステージング環境にpre-productionブランチの内容が反映されていることを確認する

- pre-productionブランチからproductionブランチにマージリクエストを作成し、マージする
- マージリクエスト作成後/マージ後に、
validate_ansibleのジョブが成功することを確認する
- マージリクエスト作成後/マージ後に、
- パイプラインの画面から
deploy_productionのジョブを手動で実行(上記スクリーンショットの▶アイコンを押下)するdeploy_productionのジョブが成功することを確認する
- 本番環境にproductionブランチの内容が反映されていることを確認する

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

※.gitlab-ci.ymlのenvironmentの定義が必須です。
おわりに
ブランチ戦略にGitLab Flowを採用して、Ansible PlaybookをGitLab CI/CDで自動デプロイする例をご紹介しました。
「環境の再現性」と「設定の確実な管理」を重視するIaCにおいては、環境に対応するブランチを保持するというGitLab Flowの設計思想は高い親和性を持つと感じました。
GitLabのEnvironment(環境)の機能を活用し、「どのコードが、どの環境に適用されているか」をGitLabのダッシュボードに可視化することも、安定的なIaCの自動デプロイの運用に有効だと思います。
弊社はGitLabオープンパートナー認定を受けております。 また以下のようにCI/CDの導入を支援するサービスも行っているので、何かご相談したいことがあればお気軽にご連絡ください。