【IaC】Azure×Terraform×AnsibleでDify環境を完全自動構築してみた|GitHub Actions連携
「LLMアプリ開発を効率化したい」「話題のDifyをすぐに試せる環境がほしい」
この記事は、そんなクラウドやIaCに興味のあるインフラエンジニアや開発者に向けて書いています。
手作業での環境構築は時間がかかり、手順も複雑になりがちですよね。そこで今回は、オープンソースのLLMアプリ開発プラットフォームであるDifyの環境を、IaC (Infrastructure as Code) を用いてAzure上に自動で構築する方法をご紹介します。
バージョンアップの自動化についてはこちらの記事をご覧ください。
はじめに:この記事でやること
この記事では、以下のゴールを目指します。
ゴール: 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) をトリガーに、一連のタスクを実行します。
主な処理の流れ:
- Azure Login: GitHub Secretsに保存した認証情報を使ってAzureにログインします。
- Terraform Apply: terraformディレクトリに移動し、terraform applyコマンドでAzureインフラを構築します。
- IPアドレスの一時許可: AnsibleがVMに接続できるよう、GitHub Actions Runner自身のグローバルIPアドレスをAzure Key VaultとNetwork Security Group (NSG) の許可リストに一時的に追加します。これにより、セキュアなプロビジョニングを実現します。
- Run Ansible Playbook: ansibleディレクトリに移動し、Playbookを実行してVMのセットアップを行います。
- 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
)
このファイルに、実行したいタスクを上から順に記述します。
主な処理内容:
- パッケージのアップデート (apt)
- DockerのGPGキーを追加し、リポジトリを設定
- Docker EngineとDocker Compose V2プラグインをインストール
- Difyの公式リポジトリを
/opt/dify
にクローン - 環境設定ファイル(
.env
)をサンプルからコピー 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などにデプロイする構成に発展させることも可能です。
この記事を書いた人

What is BEMA!?
Be Engineer, More Agile
Advent Calendar!
Advent Calendar 2024