BEMAロゴ

エンジニアの
成長を支援する
技術メディア

【IaC】Azure×Terraform×AnsibleでDify環境を完全自動構築してみた|GitHub Actions連携

「LLMアプリ開発を効率化したい」「話題のDifyをすぐに試せる環境がほしい」
この記事は、そんなクラウドやIaCに興味のあるインフラエンジニアや開発者に向けて書いています。

手作業での環境構築は時間がかかり、手順も複雑になりがちですよね。そこで今回は、オープンソースのLLMアプリ開発プラットフォームであるDifyの環境を、IaC (Infrastructure as Code) を用いてAzure上に自動で構築する方法をご紹介します。

バージョンアップの自動化についてはこちらの記事Open in new tabをご覧ください。

はじめに:この記事でやること

この記事では、以下のゴールを目指します。

ゴール: GitHub Actionsをトリガーにして、Azure VM上にDifyが稼働する環境を自動で構築する。

▼利用技術:

  • Dify: 直感的なUIでLLMアプリケーションを開発・運用できるプラットフォーム。
  • Azure: クラウドプラットフォームとして、仮想マシン(VM)などのインフラを提供。
  • Terraform: Azureのインフラ(VM、ネットワークなど)をコードで定義し、構築・管理。
  • Ansible: 構築したVMにSSH接続し、DockerのインストールやDifyのセットアップといったプロビジョニングを自動化。
  • GitHub Actions: TerraformとAnsibleの実行を連携させ、CI/CDパイプラインを構築。

あくまで、VM上にDifyを構築するシンプルな一例として、IaCによる自動化の勘所を掴んでいただくことを目的としています。

システム構成

今回構築するシステムの全体像は以下の通りです。

開発者がGitHub Actionsのワークフローを実行すると、TerraformがAzureに必要なインフラリソース(仮想マシン、ネットワークなど)を作成します。その後、Ansibleが作成されたVMにDifyアプリケーションをセットアップする、という流れです。

ディレクトリ構成

プロジェクト全体のディレクトリ構成は以下のようになります。difyディレクトリ配下にterraformとansibleのコードを配置しています。

Plaintext
./
├── .github
│   └── workflows
│       └── create_dify.yml  # GitHub Actions ワークフロー定義
└── dify
    ├── ansible              # Ansible関連ファイル
    │   ├── ansible.cfg
    │   └── playbook.yml
    └── terraform            # Terraform関連ファイル
        ├── keyvault.tf
        ├── main.tf
        ├── network.tf
        ├── output.tf
        ├── terraform.tfvars
        ├── variables.tf
        └── vm.tf

IaCコード解説

ここからは、自動化を実現するための主要なコードをポイントを絞って解説します。

1. GitHub Actions:すべてを繋ぐ自動化の司令塔

このワークフロー (create_dify.yml) が全体の処理を制御します。手動実行 (workflow_dispatch) をトリガーに、一連のタスクを実行します。

主な処理の流れ:

  1. Azure Login: GitHub Secretsに保存した認証情報を使ってAzureにログインします。
  2. Terraform Apply: terraformディレクトリに移動し、terraform applyコマンドでAzureインフラを構築します。
  3. IPアドレスの一時許可: AnsibleがVMに接続できるよう、GitHub Actions Runner自身のグローバルIPアドレスをAzure Key VaultとNetwork Security Group (NSG) の許可リストに一時的に追加します。これにより、セキュアなプロビジョニングを実現します。
  4. Run Ansible Playbook: ansibleディレクトリに移動し、Playbookを実行してVMのセットアップを行います。
  5. IPアドレスの削除: if: always() を指定することで、処理の成功・失敗にかかわらず、3で追加したIPアドレスを必ず削除し、不要なアクセス許可が残らないようにします。

コード スニペット

name: Dify環境自動構築

on:
  workflow_dispatch:
    inputs:
      manage_resource_group_name:
        description: 'シークレットを管理するリソースグループ名'
        required: true
      individual_resource_group_name:
          description: '環境を設置する対象のリソースグループ名'
          required: true
      vault_name:
        description: 'Azure Key Vaultのリソース名'
        required: true
      nsg_name:
        description: 'NSG名'
        required: true

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      TF_WORKING_DIR: ./dify/terraform
      ANSIBLE_WORKING_DIR: ./dify/ansible

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Azure Login
        uses: azure/login@v1

        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Terraform Init
        run: terraform init
        working-directory: ${{ env.TF_WORKING_DIR }}

      - name: Terraform Apply
        id: tf-apply
        run: |
          terraform apply -auto-approve \
            -var="tenant_id=${{ secrets.AZURE_TENANT_ID }}" \
            -var="subscription_id=${{ secrets.AZURE_SUBSCRIPTION_ID }}" \
            -var="client_id=${{ secrets.AZURE_CLIENT_ID }}" \
            -var="client_secret=${{ secrets.AZURE_CLIENT_SECRET }}"
        working-directory: ${{ env.TF_WORKING_DIR }}

      - name: Add IP Address For Key Vault And NSG
        uses: azure/CLI@v1
        id: get_ip
        with:
          inlineScript: |
            IP=$(curl -s https://checkip.amazonaws.com/)
            echo "IP=$IP" >> $GITHUB_OUTPUT
            az keyvault network-rule add \ 
--resource-group "${{ github.event.inputs.manage_resource_group_name }}"--name "${{ github.event.inputs.vault_name }}"--ip-address "${IP}/32"
            
 az network nsg rule create \
 --resource-group "${{ github.event.inputs.individual_resource_group_name }}" \
 --nsg-name "${{ github.event.inputs.nsg_name }}" \
 --name SSH-Temp \
 --priority 1000 \
 --source-address-prefixes "${IP}" \
 --destination-port-ranges 22 \
 --access Allow \
 --protocol Tcp \
 --direction Inbound

      - name: Setup Ansible
        run: |
          sudo apt-get update
          sudo apt-get install -y ansible python3-pip
          pip3 install ansible-core
          ansible-galaxy collection install 'community.docker:<4.0.0'

      - name: Run Ansible Playbook
        run: |
          ansible-playbook -i hosts.ini playbook.yml \
          --ssh-common-args='-o StrictHostKeyChecking=no'
        working-directory: ${{ env.ANSIBLE_WORKING_DIR }}

      - name: Remove GitHub Actions Runner IP from NSG
        if: always()
        uses: azure/CLI@v1
        with:
          inlineScript: |
            IP="${{ steps.get_ip.outputs.IP }}"
            az keyvault network-rule remove \
 --resource-group "${{ github.event.inputs.manage_resource_group_name }}" \
 --name "${{ github.event.inputs.vault_name }}" \
 --ip-address "${IP}/32"

            az network nsg rule delete \
 --resource-group "${{ github.event.inputs.individual_resource_group_name }}" \
 --nsg-name "${{ github.event.inputs.nsg_name }}" \
 --name SSH-Temp

2. Terraform:Azureインフラの設計図

Terraformは、VMやネットワークといったAzureリソースをコードで管理します。複数の.tfファイルに分割することで、役割ごとにコードを整理し、見通しを良くしています。

main.tf: プロバイダと基本設定

このファイルはTerraformの「エントリーポイント」です。使用するプロバイダ(今回はazurermなど)の定義や、Azureへの接続情報を設定します。また、既存のリソースグループをdataソースとして参照し、共通で利用するタグをlocalsで定義しています。

コード スニペット

# TerraformとAzureプロバイダーの設定
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "4.23.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.1"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.1"
    }
  }
}

provider "azurerm" {  features {}  skip_provider_registration = true  tenant_id                  = var.tenant_id  subscription_id            = var.subscription_id  client_id                  = var.client_id  client_secret              = var.client_secret}

# ----------------------------------------------------------------
# データソース定義
# ----------------------------------------------------------------
data "azurerm_client_config" "current" {}

data "azurerm_resource_group" "manage_rg" {
  name = var.manage_resource_group_name
}

data "azurerm_resource_group" "app_rg" {
  name = var.app_resource_group_name
}

# ----------------------------------------------------------------
# ローカル変数定義
# ----------------------------------------------------------------
locals {
  common_tags = {
    System      = "Dify"
    Environment = "Prod"
  }
}

network.tf: ネットワークリソース

Difyが稼働するVMのためのネットワーク環境を定義します。
仮想ネットワーク(VNet)、サブネット、そして外部からのアクセスを制御するネットワークセキュリティグループ(NSG)を作成します。NSGでは、SSH (ポート22) とWebアクセス (ポート80, 443) の通信を許可するルールを設定しています。

コード スニペット

# ネットワークリソース
# 仮想ネットワーク
resource "azurerm_virtual_network" "vnet" {
  name                = "${var.prefix}-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = data.azurerm_resource_group.app_rg.location
  resource_group_name = data.azurerm_resource_group.app_rg.name
  tags                = local.common_tags
}

# サブネット
resource "azurerm_subnet" "subnet" {
  name                 = "${var.prefix}-subnet"
  resource_group_name  = data.azurerm_resource_group.app_rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

# ネットワークセキュリティグループ (NSG)
resource "azurerm_network_security_group" "nsg" {
  name                = "${var.prefix}-nsg"
  location            = data.azurerm_resource_group.app_rg.location
  resource_group_name = data.azurerm_resource_group.app_rg.name

  security_rule {
    name                       = "AllowSSH"
    priority                   = 300
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefixes    = var.allowed_ip_addresses
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowHTTP"
    priority                   = 310
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefixes    = var.allowed_ip_addresses
    destination_address_prefix = "*"
  }
  
  # (AllowHTTPS ruleは省略)

  tags = local.common_tags
}

# サブネットとNSGを紐付け
resource "azurerm_subnet_network_security_group_association" "nsg_association" {
  subnet_id                 = azurerm_subnet.subnet.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

keyvault.tf: SSHキーと機密情報の管理

VMにログインするためのSSHキーペアを生成し、その秘密キーを安全に管理するためにAzure Key Vaultに格納します。コードに直接秘密キーを記述するのを避け、セキュリティを高めるための重要なステップです。

コード スニペット

# SSHキーとKey Vault
# Key Vault名の一意性を確保するためのランダムIDresource "random_id" "kv_suffix" {  byte_length = 4}# Key Vaultリソース作成 (管理用RG内)resource "azurerm_key_vault" "kv" {
  name                       = "kv-${var.prefix}-${random_id.kv_suffix.hex}"
  location                   = data.azurerm_resource_group.manage_rg.location
  resource_group_name        = data.azurerm_resource_group.manage_rg.name
  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "standard"
  soft_delete_retention_days = 7
  purge_protection_enabled   = false

  # Terraformを実行するサービスプリンシパル/ユーザーにアクセス権を付与
  access_policy {
    tenant_id = data.azurerm_client_config.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id
    secret_permissions = [
      "Get", "List", "Set", "Delete", "Purge", "Recover"
    ]
  }
  tags = local.common_tags
}

# SSHキーペアの生成 (ED25519)
resource "tls_private_key" "ssh" {
  algorithm = "ED25519"
}

# SSH公開鍵をKey Vaultにシークレットとして格納
resource "azurerm_key_vault_secret" "ssh_public_key" {
  name         = "vm-ssh-public-key"
  value        = tls_private_key.ssh.public_key_openssh
  key_vault_id = azurerm_key_vault.kv.id
}

# SSH秘密鍵をKey Vaultにシークレットとして格納
resource "azurerm_key_vault_secret" "ssh_private_key" {
  name         = "vm-ssh-private-key"
  value        = tls_private_key.ssh.private_key_pem
  key_vault_id = azurerm_key_vault.kv.id
}

# 生成した秘密鍵をAnsible用にローカルファイルとして保存
resource "local_file" "private_key_pem" {
  content         = tls_private_key.ssh.private_key_openssh
  filename        = "../ansible/vm_ssh_key.pem"
  file_permission = "0600"
}

vm.tf: 仮想マシンの構築

Difyアプリケーションが稼働する本体であるLinux仮想マシンを定義します。パブリックIP、ネットワークインターフェース(NIC)を作成し、VMにアタッチします。 注目すべきは、null_resourceブロックです。local-execプロビジョナーを使い、Terraformが作成したVMのパブリックIPアドレスを動的に取得して、Ansibleの接続情報ファイル(../ansible/hosts.ini)を自動生成しています。これにより、TerraformとAnsibleがスムーズに連携できます。

コード スニペット

# パブリックIPアドレス
resource "azurerm_public_ip" "pip" {
  name                = "${var.prefix}-pip"
  location            = data.azurerm_resource_group.app_rg.location
  resource_group_name = data.azurerm_resource_group.app_rg.name
  allocation_method   = "Static"
  sku                 = "Standard"
  tags                = local.common_tags
}
# ネットワークインターフェース (NIC)
resource "azurerm_network_interface" "nic" {
  name                = "${var.prefix}-nic"
  location            = data.azurerm_resource_group.app_rg.location
  resource_group_name = data.azurerm_resource_group.app_rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.pip.id
  }
  tags = local.common_tags
}

# 仮想マシン (Linux)
resource "azurerm_linux_virtual_machine" "vm" {
  name                = var.prefix
  resource_group_name = data.azurerm_resource_group.app_rg.name
  location            = data.azurerm_resource_group.app_rg.location
  size                = "Standard_D2s_v3"
  admin_ssh_key {
    username   = "azureuser"
    public_key = tls_private_key.ssh.public_key_openssh
  }
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = local.common_tags
}

# Ansibleインベントリファイルを生成
resource "null_resource" "generate_inventory" {
 triggers = {
    vm_public_ip = azurerm_public_ip.pip.ip_address
  }
  provisioner "local-exec" {
    command = <<EOF
      echo "[azure_vms]" > ../ansible/hosts.ini
      echo "${azurerm_public_ip.pip.ip_address} ansible_user=${azurerm_linux_virtual_machine.vm.admin_username} ansible_ssh_private_key_file=./vm_ssh_key.pem" >> ../ansible/hosts.ini
    EOF
  }

  depends_on = [
    azurerm_linux_virtual_machine.vm,
    local_file.private_key_pem
  ]
}

注目すべきは、null_resourceブロックです。ここではlocal-execプロビジョナーを使い、Terraformが作成したVMのパブリックIPアドレスSSHキーの情報(ローカルに保存された秘密鍵のパス)を動的に取得して、Ansibleの接続情報ファイル(hosts.ini)を自動生成しています。これにより、TerraformとAnsibleがスムーズに連携できます。

output.tf: 結果の出力

terraform applyの実行後、作成されたリソースの重要な情報(VMのパブリックIPアドレスなど)をターミナルに表示します。これにより、インフラが正常に作成されたことを確認し、VMに接続するための情報を簡単に取得できます。

コード スニペット

output "vm_public_ip" {
  value       = azurerm_public_ip.pip.ip_address
  description = "仮想マシンのパブリックIPアドレス"
}

output "key_vault_name" {
  value       = azurerm_key_vault.kv.name
  description = "作成されたKey Vaultの名前"
}


output "nsg_name" {
  value       = azurerm_network_security_group.nsg.name
  description = "作成されたNSGの名前"
}


output "ssh_command" {
  value       = "ssh -i ../ansible/vm_ssh_key.pem azureuser@${azurerm_public_ip.pip.ip_address}"
  description = "VMにSSH接続するためのコマンド"
  sensitive   = true
}

変数管理 (variables.tf & terraform.tfvars)

Azureの認証情報やリソース名の接頭辞などを変数として定義しています。これにより、コード本体を変更することなく、terraform.tfvarsファイルや実行時オプションで値を柔軟に変更でき、再利用性が高まります。

コード スニペット

# Azure 認証情報
variable "tenant_id" {
  type        = string
  description = "Azure Tenant ID"
  sensitive   = true
}

variable "subscription_id" {
  type        = string
  description = "Azure Subscription ID"
}

variable "client_id" {
  type        = string
  description = "Azure Service Principal Client ID"
}

variable "client_secret" {
  type        = string
  description = "Azure Service Principal Client Secret"
  sensitive   = true
}

# リソース設定
variable "manage_resource_group_name" {
  type        = string
  description = "管理リソースを配置する既存のリソースグループ名"
  default     = "${リソースグループ名}"
}

variable "app_resource_group_name" {
  type        = string
  description = "アプリケーションリソースを配置する既存のリソースグループ名"
  default     = "${リソースグループ名}"
}

variable "prefix" {
  type        = string
  description = "作成するリソース名の接頭辞"
  default     = "${VM名}"
}

variable "allowed_ip_addresses" {
  type        = list(string)
  description = "NSGで許可するIPアドレスのリスト"
  default     = ["xx.xx.xx.xx/xx"]
}

3. Ansible:VMセットアップの自動化職人

Ansibleは、Terraformによって作成されたVMにログインし、必要なソフトウェアのインストールや設定を行います。
Playbook (playbook.yml)

このファイルに、実行したいタスクを上から順に記述します。

主な処理内容:

  1. パッケージのアップデート (apt)
  2. DockerのGPGキーを追加し、リポジトリを設定
  3. Docker EngineDocker Compose V2プラグインをインストール
  4. Difyの公式リポジトリを/opt/difyにクローン
  5. 環境設定ファイル(.env)をサンプルからコピー
  6. docker_compose_v2モジュールを使い、Difyのコンテナ群を起動

コード スニペット

---
- name: Install Docker and Setup Dify with Compose V2
  hosts: azure_vms
  become: yes
  tasks:
    - name: Update and upgrade apt packages
      ansible.builtin.apt:
        update_cache: yes
        upgrade: dist

    - name: Install prerequisite packages
      ansible.builtin.apt:
        name:
          - ca-certificates
          - curl
          - gnupg
          - lsb-release
        state: present

    - name: Install pip for Python
      ansible.builtin.apt:
        name: python3-pip
        state: present
        update_cache: yes

    - name: Create directory for Docker GPG key
      ansible.builtin.file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    - name: Add Docker’s official GPG key
      ansible.builtin.shell:
        cmd: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
        creates: /etc/apt/keyrings/docker.gpg

    - name: Set up Docker repository
      ansible.builtin.shell:
        cmd: >
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
        creates: /etc/apt/sources.list.d/docker.list

    - name: Update apt package index again
      ansible.builtin.apt:
        update_cache: yes
    
    - name: Install Docker Engine and Compose Plugin
      ansible.builtin.apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin # V2プラグインをインストール
        state: present

    - name: Add azureuser to docker group
      ansible.builtin.user:
        name: azureuser
        groups: docker
        append: yes

    - name: Reset SSH connection to apply group membership
      ansible.builtin.meta: reset_connection

    - name: Enable and start Docker service
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: yes

    - name: Create directory for Dify
      ansible.builtin.file:
        path: /opt/dify
        state: directory
        owner: azureuser
        group: azureuser
        mode: '0755'

    - name: Clone Dify repository
      ansible.builtin.git:
        repo: https://github.com/langgenius/dify.git
        dest: /opt/dify
        version: main
      become_user: azureuser

    - name: Copy sample environment file
      ansible.builtin.copy:
        src: /opt/dify/docker/.env.example
        dest: /opt/dify/docker/.env
        remote_src: yes
        owner: azureuser
        group: azureuser

    - name: Start Dify containers
      community.docker.docker_compose_v2:
        project_src: /opt/dify/docker
        state: present
      become_user: azureuser

Docker Compose V2 (docker composeコマンド) に対応したcommunity.docker.docker_compose_v2モジュールを使用しているのがポイントです。

まとめ

今回は、Terraform、Ansible、GitHub Actionsを組み合わせることで、Difyの実行環境をAzure上に自動構築する方法を紹介しました。

IaCを活用することで、以下のようなメリットが得られます。

  • 迅速性: コマンド一つで、いつでも環境を構築・破棄できる。
  • 再現性: 誰が実行しても同じ構成の環境を正確に再現できる。
  • 可視性: インフラ構成がコードとして可視化され、レビューやバージョン管理が容易になる。

この記事で紹介した構成はシンプルなものですが、これをベースにDBを外部サービス(Azure Database for PostgreSQLなど)に切り出したり、Azure Container Appsなどにデプロイする構成に発展させることも可能です。

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

山岸 玄斉
山岸 玄斉
2024年に株式会社メンバーズに中途入社。デブオプスリードカンパニーにて開発支援を行う傍ら、クラウドエンジニアとしてクラウド技術を学習し、生成AIを用いたPoCにも従事。
詳しく見る
ページトップへ戻る