Azure Bicep による Infrastructure as Code 実践ガイド

はじめに

Azure Bicep は、Azure リソースをデプロイするための宣言型のドメイン固有言語(DSL)です。ARM テンプレートよりもシンプルで読みやすい構文を提供し、Infrastructure as Code(IaC)の実践を容易にします。本記事では、Bicep を使用した実践的なインフラ管理手法を解説します。

Bicep の基本概念

1. なぜ Bicep を使うのか

// ARM テンプレート(JSON)での記述
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageAccountName": {
      "type": "string",
      "minLength": 3,
      "maxLength": 24
    }
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2021-04-01",
      "name": "[parameters('storageAccountName')]",
      "location": "[resourceGroup().location]",
      "sku": {
        "name": "Standard_LRS"
      },
      "kind": "StorageV2"
    }
  ]
}

// Bicep での同じリソース定義
param storageAccountName string

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

2. 基本構文と型システム

// パラメータ定義
@description('環境名(dev, staging, prod)')
@allowed([
  'dev'
  'staging'
  'prod'
])
param environment string

@description('リージョン名')
param location string = resourceGroup().location

@secure()
@description('SQL Server 管理者パスワード')
param sqlAdminPassword string

// 変数定義
var storageAccountName = 'st${uniqueString(resourceGroup().id)}${environment}'
var appServicePlanName = 'asp-${environment}-${location}'

// リソース定義
resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: environment == 'prod' ? 'P2v3' : 'B1'
    tier: environment == 'prod' ? 'PremiumV3' : 'Basic'
  }
  properties: {
    reserved: true // Linux
  }
}

// 出力定義
output appServicePlanId string = appServicePlan.id
output appServicePlanName string = appServicePlan.name

実践的な Bicep モジュール設計

1. モジュール構造

infrastructure/
├── main.bicep              # メインテンプレート
├── modules/                # モジュールディレクトリ
│   ├── networking/
│   │   ├── vnet.bicep
│   │   └── nsg.bicep
│   ├── compute/
│   │   ├── vm.bicep
│   │   └── aks.bicep
│   ├── data/
│   │   ├── sql.bicep
│   │   └── cosmos.bicep
│   └── security/
│       ├── keyvault.bicep
│       └── identity.bicep
├── environments/           # 環境別パラメータ
│   ├── dev.parameters.json
│   ├── staging.parameters.json
│   └── prod.parameters.json
└── scripts/               # デプロイスクリプト
    └── deploy.ps1

2. ネットワークモジュール

// modules/networking/vnet.bicep
@description('仮想ネットワーク名')
param vnetName string

@description('アドレス空間')
param addressPrefix string = '10.0.0.0/16'

@description('サブネット構成')
param subnets array = [
  {
    name: 'web-subnet'
    addressPrefix: '10.0.1.0/24'
    nsgId: ''
  }
  {
    name: 'app-subnet'
    addressPrefix: '10.0.2.0/24'
    nsgId: ''
  }
  {
    name: 'db-subnet'
    addressPrefix: '10.0.3.0/24'
    nsgId: ''
  }
]

@description('タグ')
param tags object = {}

resource vnet 'Microsoft.Network/virtualNetworks@2021-05-01' = {
  name: vnetName
  location: resourceGroup().location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [for subnet in subnets: {
      name: subnet.name
      properties: {
        addressPrefix: subnet.addressPrefix
        networkSecurityGroup: empty(subnet.nsgId) ? null : {
          id: subnet.nsgId
        }
        privateEndpointNetworkPolicies: 'Disabled'
        privateLinkServiceNetworkPolicies: 'Disabled'
      }
    }]
  }
}

output vnetId string = vnet.id
output vnetName string = vnet.name
output subnets array = [for (subnet, i) in subnets: {
  name: subnet.name
  id: vnet.properties.subnets[i].id
}]

3. AKS クラスタモジュール

// modules/compute/aks.bicep
@description('AKS クラスタ名')
param clusterName string

@description('Kubernetes バージョン')
param kubernetesVersion string = '1.27.7'

@description('ノードプール設定')
param systemNodePool object = {
  name: 'system'
  count: 3
  vmSize: 'Standard_D4s_v3'
  osDiskSizeGB: 128
  maxPods: 30
}

@description('ユーザーノードプール設定')
param userNodePools array = []

@description('ネットワーク設定')
param networkProfile object = {
  networkPlugin: 'azure'
  networkPolicy: 'calico'
  serviceCidr: '10.0.0.0/16'
  dnsServiceIP: '10.0.0.10'
  dockerBridgeCidr: '172.17.0.1/16'
}

@description('サブネット ID')
param subnetId string

@description('Log Analytics ワークスペース ID')
param logAnalyticsWorkspaceId string = ''

resource aksIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: '${clusterName}-identity'
  location: resourceGroup().location
}

resource aks 'Microsoft.ContainerService/managedClusters@2023-01-01' = {
  name: clusterName
  location: resourceGroup().location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${aksIdentity.id}': {}
    }
  }
  properties: {
    kubernetesVersion: kubernetesVersion
    dnsPrefix: clusterName
    
    agentPoolProfiles: concat([
      {
        name: systemNodePool.name
        count: systemNodePool.count
        vmSize: systemNodePool.vmSize
        osDiskSizeGB: systemNodePool.osDiskSizeGB
        maxPods: systemNodePool.maxPods
        type: 'VirtualMachineScaleSets'
        mode: 'System'
        osType: 'Linux'
        vnetSubnetID: subnetId
        enableAutoScaling: true
        minCount: 3
        maxCount: 5
      }
    ], [for nodePool in userNodePools: {
      name: nodePool.name
      count: nodePool.count
      vmSize: nodePool.vmSize
      osDiskSizeGB: nodePool.osDiskSizeGB
      maxPods: nodePool.maxPods
      type: 'VirtualMachineScaleSets'
      mode: 'User'
      osType: 'Linux'
      vnetSubnetID: subnetId
      enableAutoScaling: nodePool.enableAutoScaling
      minCount: nodePool.minCount
      maxCount: nodePool.maxCount
      nodeTaints: nodePool.?nodeTaints ?? []
      nodeLabels: nodePool.?nodeLabels ?? {}
    }])
    
    networkProfile: {
      networkPlugin: networkProfile.networkPlugin
      networkPolicy: networkProfile.networkPolicy
      serviceCidr: networkProfile.serviceCidr
      dnsServiceIP: networkProfile.dnsServiceIP
      dockerBridgeCidr: networkProfile.dockerBridgeCidr
      loadBalancerSku: 'standard'
    }
    
    addonProfiles: {
      azureKeyvaultSecretsProvider: {
        enabled: true
        config: {
          enableSecretRotation: 'true'
          rotationPollInterval: '2m'
        }
      }
      omsagent: empty(logAnalyticsWorkspaceId) ? {} : {
        enabled: true
        config: {
          logAnalyticsWorkspaceResourceID: logAnalyticsWorkspaceId
        }
      }
    }
    
    enableRBAC: true
    aadProfile: {
      managed: true
      enableAzureRBAC: true
    }
  }
}

output clusterName string = aks.name
output clusterFqdn string = aks.properties.fqdn
output identityPrincipalId string = aksIdentity.properties.principalId

4. メインテンプレート

// main.bicep
targetScope = 'subscription'

@description('環境名')
@allowed(['dev', 'staging', 'prod'])
param environment string

@description('リージョン')
param location string

@description('プロジェクト名')
param projectName string

// リソースグループ
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: 'rg-${projectName}-${environment}'
  location: location
  tags: {
    Environment: environment
    Project: projectName
    ManagedBy: 'Bicep'
  }
}

// ネットワーク
module networking './modules/networking/vnet.bicep' = {
  scope: rg
  name: 'networking'
  params: {
    vnetName: 'vnet-${projectName}-${environment}'
    addressPrefix: environment == 'prod' ? '10.0.0.0/16' : '10.1.0.0/16'
    tags: {
      Environment: environment
      Project: projectName
    }
  }
}

// AKS クラスタ
module aks './modules/compute/aks.bicep' = if (environment != 'dev') {
  scope: rg
  name: 'aks'
  params: {
    clusterName: 'aks-${projectName}-${environment}'
    subnetId: networking.outputs.subnets[1].id
    systemNodePool: {
      name: 'system'
      count: environment == 'prod' ? 3 : 1
      vmSize: environment == 'prod' ? 'Standard_D4s_v3' : 'Standard_D2s_v3'
      osDiskSizeGB: 128
      maxPods: 30
    }
    userNodePools: environment == 'prod' ? [
      {
        name: 'user'
        count: 3
        vmSize: 'Standard_D4s_v3'
        osDiskSizeGB: 128
        maxPods: 30
        enableAutoScaling: true
        minCount: 3
        maxCount: 10
      }
    ] : []
  }
}

// Key Vault
module keyVault './modules/security/keyvault.bicep' = {
  scope: rg
  name: 'keyvault'
  params: {
    keyVaultName: 'kv-${projectName}-${environment}'
    enableSoftDelete: environment == 'prod'
    enablePurgeProtection: environment == 'prod'
    accessPolicies: environment != 'dev' ? [
      {
        tenantId: subscription().tenantId
        objectId: aks.outputs.identityPrincipalId
        permissions: {
          secrets: ['get', 'list']
        }
      }
    ] : []
  }
}

CI/CD パイプライン統合

1. Azure DevOps Pipeline

# azure-pipelines.yml
trigger:
  branches:
    include:
    - main
    - develop
  paths:
    include:
    - infrastructure/*

variables:
  - group: azure-credentials
  - name: environment
    ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
      value: 'prod'
    ${{ else }}:
      value: 'dev'

stages:
- stage: Validate
  jobs:
  - job: ValidateBicep
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: AzureCLI@2
      displayName: 'Validate Bicep templates'
      inputs:
        azureSubscription: 'Azure Service Connection'
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          # Bicep のビルドと検証
          az bicep build --file infrastructure/main.bicep
          
          # What-if 実行
          az deployment sub what-if \
            --location japaneast \
            --template-file infrastructure/main.bicep \
            --parameters infrastructure/environments/$(environment).parameters.json

- stage: Deploy
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - deployment: DeployInfrastructure
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureCLI@2
            displayName: 'Deploy Bicep templates'
            inputs:
              azureSubscription: 'Azure Service Connection'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                # デプロイ実行
                az deployment sub create \
                  --location japaneast \
                  --template-file infrastructure/main.bicep \
                  --parameters infrastructure/environments/$(environment).parameters.json \
                  --name "deploy-$(Build.BuildId)"

2. GitHub Actions

# .github/workflows/deploy-infrastructure.yml
name: Deploy Infrastructure

on:
  push:
    branches: [ main, develop ]
    paths:
      - 'infrastructure/**'
  pull_request:
    branches: [ main ]
    paths:
      - 'infrastructure/**'

env:
  AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
  AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    - name: Validate Bicep
      run: |
        az bicep build --file infrastructure/main.bicep
        
    - name: Run What-If
      run: |
        ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
        az deployment sub what-if \
          --location japaneast \
          --template-file infrastructure/main.bicep \
          --parameters infrastructure/environments/${ENVIRONMENT}.parameters.json

  deploy:
    needs: validate
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    - name: Deploy Infrastructure
      run: |
        az deployment sub create \
          --location japaneast \
          --template-file infrastructure/main.bicep \
          --parameters infrastructure/environments/prod.parameters.json \
          --name "deploy-${{ github.run_id }}"

高度な Bicep パターン

1. 条件付きデプロイとループ

// 条件付きリソース
param deployRedis bool = true
param deployCosmosDB bool = false

resource redis 'Microsoft.Cache/redis@2021-06-01' = if (deployRedis) {
  name: 'redis-${uniqueString(resourceGroup().id)}'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'Standard'
      family: 'C'
      capacity: 1
    }
  }
}

// 配列からのリソース作成
param webApps array = [
  {
    name: 'api'
    runtime: 'DOTNET|6.0'
  }
  {
    name: 'web'
    runtime: 'NODE|18-lts'
  }
]

resource appServices 'Microsoft.Web/sites@2021-02-01' = [for app in webApps: {
  name: 'app-${app.name}-${uniqueString(resourceGroup().id)}'
  location: resourceGroup().location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: app.runtime
    }
  }
}]

// 既存リソースの参照
resource existingKeyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: 'kv-shared-prod'
  scope: resourceGroup('rg-shared-prod')
}

// シークレットの取得と使用
module sqlServer './modules/data/sql.bicep' = {
  name: 'sql'
  params: {
    administratorLogin: 'sqladmin'
    administratorPassword: existingKeyVault.getSecret('sql-admin-password')
  }
}

2. User Defined Types (プレビュー機能)

// types.bicep
@export()
type environmentConfigType = {
  name: ('dev' | 'staging' | 'prod')
  settings: {
    sku: string
    capacity: int
    enableHA: bool
  }
}

@export()
type networkConfigType = {
  vnet: {
    addressSpace: string
    subnets: {
      name: string
      addressPrefix: string
      delegations: string[]?
    }[]
  }
}

// main.bicep
import * as types from './types.bicep'

param environmentConfig types.environmentConfigType
param networkConfig types.networkConfigType

トラブルシューティングとベストプラクティス

1. デバッグテクニック

// デバッグ用の出力
output debugInfo object = {
  resourceGroupId: resourceGroup().id
  deploymentName: deployment().name
  parametersUsed: {
    environment: environment
    location: location
  }
}

// 検証用のアサーション
resource assertValidConfig 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'validate-config'
  location: location
  kind: 'AzurePowerShell'
  properties: {
    azPowerShellVersion: '7.0'
    scriptContent: '''
      $config = $DeploymentScriptOutputs
      if ($config.environment -eq 'prod' -and $config.replicaCount -lt 3) {
        throw "Production environment requires at least 3 replicas"
      }
    '''
    cleanupPreference: 'OnSuccess'
    retentionInterval: 'P1D'
  }
}

2. セキュリティベストプラクティス

// セキュアなパラメータ処理
@secure()
param databasePassword string

// Key Vault 統合
resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = {
  name: keyVaultName
  location: location
  properties: {
    enableRbacAuthorization: true
    enableSoftDelete: true
    enablePurgeProtection: true
    softDeleteRetentionInDays: 90
    tenantId: subscription().tenantId
    sku: {
      family: 'A'
      name: 'standard'
    }
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
      ipRules: [
        {
          value: '203.0.113.0/24'
        }
      ]
    }
  }
}

// シークレットの保存
resource secret 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = {
  parent: keyVault
  name: 'database-connection-string'
  properties: {
    value: 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${databaseName};User ID=${adminLogin};Password=${databasePassword};'
  }
}

まとめ

Azure Bicep は、Infrastructure as Code を実践する上で強力なツールです。主な利点:

  1. 読みやすさ: JSON より簡潔で理解しやすい構文
  2. 型安全性: コンパイル時の型チェック
  3. モジュール性: 再利用可能なコンポーネント
  4. 統合性: Azure エコシステムとの完全な統合

エンハンスド株式会社では、Bicep を活用した Azure インフラストラクチャの設計・構築・運用を支援しています。IaC の導入により、インフラ管理の効率化と品質向上を実現します。