🦐

【Azure/Bicep】Logic Apps × PrivateEndpointをBicepからデプロイする方法

に公開

記事の内容

下記の構成でLogic Appsをデプロイする。
※下記のBicepでは、SubnetとNSGを余分に作成しています。用途に合わせて削ってください。

※StorageAccountは診断設定に用いられるものですが、今回は診断設定は除いてデプロイします。
↓ 本来の想定

Logic Appsについて


今回はStandardプランのWS1でデプロイします。PrivateEndpointを付けてデプロイするにはLogic AppsはStandardプラン(ワークフロー or App Service Environment)である必要があります。

また、Logic AppsとPrivateEndpointを組み合わせる場合、AppServicePlanのデプロイも必要です。従量課金プランではMicrosoftがホストするため、AppServicePlanを作成する必要はありません。Standardプランでは固定の支払いとなり、ユーザーがホストする必要があるため、AppServicePlanの構築が必要です。
https://zenn.dev/headwaters/articles/2aa981ac24fd0a

ディレクトリ構成

bicep-project/
├── deployments/
│   ├── main.bicep
│   └── rg.bicep
└── modules/
     ├── resourceGroup.bicep
     ├── vnet.bicep
     ├── NSG.bicep
     ├── logicapps.bicep
     ├── appserviceplan.bicep
     ├── blobstorage.bicep
     └── privateEndpoint.bicep

Bicep

rg.bicep/resourceGroup.bicep

rg.bicep

targetScope = 'subscription'
param rgName string = 'myProjectRG'
param location string = 'japanwest'

var tags = {
  Environment: 'dev'
}

module rgModule '../modules/resourceGroup.bicep' = {
  name: 'resourceGroup'
  params: {
    rgName: rgName
    location: location
    tags: tags
  }
}

resourceGroup.bicep

targetScope = 'subscription'
param rgName string
param location string
param tags object

resource RG 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: rgName
  location: location
  tags: tags
}

output rgName string = RG.name
vnet.bicep

vnet.bicep

param vnetName string
param location string
param tags object
param vnetAddressPrefix string
param subnets array

resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
  name: vnetName
  location: location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        vnetAddressPrefix
      ]
    }
    subnets: [
      for (subnet, index) in subnets: {
        name: subnet.name
        properties: union(
          {
            addressPrefix: subnet.addressPrefix
            privateEndpointNetworkPolicies: 'Enabled'
            networkSecurityGroup: {
              id: resourceId('Microsoft.Network/networkSecurityGroups', subnet.nsgName)
            }
          },
          index == 2
            ? {
                delegations: [
                  {
                    name: 'delegation'
                    properties: {
                      serviceName: 'Microsoft.Web/serverFarms'
                    }
                  }
                ]
              }
            : {}
        )
      }
    ]
  }
}

output vnetId string = vnet.id
output vnetName string = vnet.name
output agwsubnetId01 string = vnet.properties.subnets[0].id
output pesubnetId01 string = vnet.properties.subnets[1].id
output vnetIntegrationSubnetId string = vnet.properties.subnets[2].id
output pesubnetId02 string = vnet.properties.subnets[3].id
NSG.bicep

NSG.bicep

param location string = 'japanwest'
param tags object = {
  environment: 'sample'
}
param subnets array = [
  {
    name: 'subnet-app'
    nsgName: 'nsg-app'
  }
  {
    name: 'subnet-pe'
    nsgName: 'nsg-pe'
  }
]

var nsgRules = {
  '${subnets[0].nsgName}': [
    {
      name: 'AllowAppSubnetInbound'
      priority: 100
      direction: 'Inbound'
      access: 'Allow'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '10.1.0.0/24' // sample private range
      destinationAddressPrefix: '*'
    }
    {
      name: 'DenyAllOutbound'
      priority: 4096
      direction: 'Outbound'
      access: 'Deny'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: '*'
    }
  ]
  '${subnets[1].nsgName}': [
    {
      name: 'AllowHttpsOutbound'
      priority: 100
      direction: 'Outbound'
      access: 'Allow'
      protocol: 'Tcp'
      sourcePortRange: '*'
      destinationPortRange: '443'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: 'Internet'
    }
    {
      name: 'DenyAllOutbound'
      priority: 4096
      direction: 'Outbound'
      access: 'Deny'
      protocol: '*'
      sourcePortRange: '*'
      destinationPortRange: '*'
      sourceAddressPrefix: '*'
      destinationAddressPrefix: '*'
    }
  ]
}

resource nsgs 'Microsoft.Network/networkSecurityGroups@2023-05-01' = [
  for subnet in subnets: {
    name: subnet.nsgName
    location: location
    tags: tags
    properties: {
      securityRules: [
        for rule in nsgRules[subnet.nsgName]: {
          name: rule.name
          properties: {
            priority: rule.priority
            direction: rule.direction
            access: rule.access
            protocol: rule.protocol
            sourcePortRange: rule.sourcePortRange
            destinationPortRange: rule.destinationPortRange
            sourceAddressPrefix: rule.sourceAddressPrefix
            destinationAddressPrefix: rule.destinationAddressPrefix
          }
        }
      ]
    }
  }
]

output appNsgId string = nsgs[0].id
output peNsgId string = nsgs[1].id

main.bicep

// 共通パラメータ (sample)
targetScope = 'subscription'
param rgName string = 'sampleRG'
param location string = 'japanwest'
param environment string = 'dev'
param projectCode string = 'sample'

param tags object = {
  Environment: 'dev'
}

// ネットワーク関連 (sample ranges)
var subnets = [
  {
    name: 'sample-agw-subnet01'
    addressPrefix: '10.1.0.0/24'
    nsgName: 'sample-agw-nsg01'
  }
  {
    name: 'sample-pe-subnet01'
    addressPrefix: '10.1.1.0/24'
    nsgName: 'sample-pe-nsg01'
  }
  {
    name: 'sample-integration-subnet01'
    addressPrefix: '10.1.2.0/24'
    nsgName: 'sample-integration-nsg01'
  }
  {
    name: 'sample-pe-subnet02'
    addressPrefix: '10.1.3.0/24'
    nsgName: 'sample-pe-nsg02'
  }
]

module nsgModule '../modules/NSG.bicep' = {
  name: 'NetworkSecurityGroup'
  scope: resourceGroup(rgName)
  params: {
    location: location
    tags: tags
    subnets: subnets
  }
}

module vnetModule '../modules/vnet.bicep' = {
  name: 'VirtualNetwork'
  scope: resourceGroup(rgName)
  params: {
    vnetName: 'sampleVnet01'
    location: location
    tags: tags
    vnetAddressPrefix: '10.1.0.0/16'
    subnets: subnets
  }
  dependsOn: [
    nsgModule
  ]
}

// App Service Plan
param appPlanSku string = 'WS1'
param appPlanTier string = 'WorkflowStandard'

module appServicePlanModule '../modules/appserviceplan.bicep' = {
  name: 'AppsServicePlan'
  scope: resourceGroup(rgName)
  params: {
    appServicePlanName: '${development}-${case}appplan001'
    location: location
    tags: tags
    appPlanSku: appPlanSku
    appPlanTier: appPlanTier

    // 診断設定をオンにするときは下記を追加+診断設定モジュールを追加
    // logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId
    // storageAccountId: logAnalyticsModule.outputs.logstorageAccountId
  }
  dependsOn: [
    //logAnalyticsModule
  ]
}

//blob
// Logic Apps Standard用Storage Account
param logicAppStorageName string = '${toLower(development)}${toLower(case)}logicst01'

module logicAppStorage '../modules/blobstorage.bicep' = {
  name: 'logicAppStorage'
  scope: resourceGroup(rgName)
  params: {
    storageAccountName: logicAppStorageName
    location: location
    tags: tags
    sku: 'Standard_LRS'
    kind: 'StorageV2'
    accessTier: 'Hot'
    publicNetworkAccess: 'Enabled'
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}

//LogicApps
param logicAppName string = 'son-logicapp001'

module logicAppsModule '../modules/logicapps.bicep' = {
  name: 'logicApps'
  scope: resourceGroup(rgName)
  params: {
    location: location
    tags: tags
    logicAppName: logicAppName
    appServicePlanId: appServicePlanModule.outputs.appServicePlanId
    storageAccountName: logicAppStorage.outputs.storageAccountName
    storageAccountId: logicAppStorage.outputs.storageAccountId
    vnetIntegrationSubnetId: vnetModule.outputs.vnetIntegrationSubnetId
  }
  dependsOn: [
    appServicePlanModule
    vnetModule
  ]
}

module logicAppsPrivateEndpoint '../modules/privateEndpoint.bicep' = {
  name: 'logicapps-pe'
  scope: resourceGroup(rgName)
  params: {
    privateEndpointName: '${development}-${case}-logicapps-pe01'
    location: location
    tags: tags
    subnetId: vnetModule.outputs.pesubnetId01
    targetResourceId: logicAppsModule.outputs.logicAppsId
    groupIds: ['sites']
    privateDnsZoneNames: [
      'privatelink.azurewebsites.net'
    ]
    vnetId: vnetModule.outputs.vnetId
  }
  dependsOn: [
    vnetModule
    logicAppsModule
  ]
}

logicapps.bicep

param location string
param tags object
param logicAppName string
param appServicePlanId string
param storageAccountName string
param storageAccountId string
param vnetIntegrationSubnetId string

// Logic Apps Standard
resource logicAppStandard 'Microsoft.Web/sites@2023-01-01' = {
  name: logicAppName
  location: location
  tags: tags
  kind: 'functionapp,workflowapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlanId
    httpsOnly: true
    virtualNetworkSubnetId: vnetIntegrationSubnetId
    siteConfig: {
      netFrameworkVersion: 'v6.0'
      use32BitWorkerProcess: false
      alwaysOn: true
      vnetRouteAllEnabled: true
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccountId, '2023-01-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccountId, '2023-01-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(logicAppName)
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'node'
        }
        {
          name: 'APP_KIND'
          value: 'workflowApp'
        }
      ]
    }
  }
}

output logicAppsId string = logicAppStandard.id
output logicAppsName string = logicAppStandard.name
output principalId string = logicAppStandard.identity.principalId

appserviceplan.bicep

param location string
param tags object
param appServicePlanName string
param appPlanSku string
param appPlanTier string
// param logAnalyticsWorkspaceId string
// param storageAccountId string

resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: appServicePlanName
  location: location
  tags: tags
  sku: {
    name: appPlanSku
    tier: appPlanTier
  }
  kind: 'linux'
  properties: {
    reserved: true
    perSiteScaling: false
    maximumElasticWorkerCount: 1
  }
}

//診断設定
// resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
//   name: '${appServicePlanName}-diagnostic'
//   scope: appServicePlan
//   properties: {
//     workspaceId: logAnalyticsWorkspaceId
//     storageAccountId: storageAccountId
//     logs: []
//     metrics: [
//       {
//         category: 'AllMetrics'
//         enabled: true
//         retentionPolicy: {
//           enabled: false
//           days: 0
//         }
//       }
//     ]
//   }
// }

output appServicePlanId string = appServicePlan.id
output appServicePlanName string = appServicePlan.name

blobstorage.bicep

param storageAccountName string
param location string
param tags object
param sku string
param kind string
param accessTier string
param publicNetworkAccess string
param supportsHttpsTrafficOnly bool
param minimumTlsVersion string

// Storage Account リソース
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  tags: tags
  sku: {
    name: sku
  }
  kind: kind
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    accessTier: accessTier
    publicNetworkAccess: publicNetworkAccess
    supportsHttpsTrafficOnly: supportsHttpsTrafficOnly
    minimumTlsVersion: minimumTlsVersion
    allowBlobPublicAccess: false
    allowSharedKeyAccess: true
    networkAcls: {
      defaultAction: publicNetworkAccess == 'Enabled' ? 'Allow' : 'Deny'
      bypass: 'AzureServices'
      ipRules: []
      virtualNetworkRules: []
    }
    encryption: {
      services: {
        blob: {
          enabled: true
        }
        file: {
          enabled: true
        }
      }
      keySource: 'Microsoft.Storage'
    }
  }
}

// Blob Service の設定
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: storageAccount
  name: 'default'
  properties: {
    cors: {
      corsRules: []
    }
    deleteRetentionPolicy: {
      enabled: true
      days: 7
    }
    containerDeleteRetentionPolicy: {
      enabled: true
      days: 7
    }
    changeFeed: {
      enabled: false
    }
    restorePolicy: {
      enabled: false
    }
    isVersioningEnabled: false
  }
}

// デフォルトのBlobコンテナ
resource defaultContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
  parent: blobService
  name: 'default-container'
  properties: {
    publicAccess: 'None'
  }
}

// Outputs
output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
output storageAccountPrincipalId string = storageAccount.identity.principalId

privateEndpoint.bicep

param privateEndpointName string
param location string
param tags object
param subnetId string
param targetResourceId string
param groupIds array
param privateDnsZoneNames array
param vnetId string

resource privateDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [
  for zoneName in privateDnsZoneNames: {
    name: zoneName
    location: 'global'
    tags: tags
  }
]

resource privateDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [
  for (zoneName, index) in privateDnsZoneNames: {
    parent: privateDnsZones[index]
    name: 'vnet-link-${index}'
    location: 'global'
    tags: tags
    properties: {
      registrationEnabled: false
      virtualNetwork: {
        id: vnetId
      }
    }
  }
]

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
  name: privateEndpointName
  location: location
  tags: tags
  properties: {
    subnet: {
      id: subnetId
    }
    privateLinkServiceConnections: [
      {
        name: '${privateEndpointName}-connection'
        properties: {
          privateLinkServiceId: targetResourceId
          groupIds: groupIds
        }
      }
    ]
  }
}

resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = {
  parent: privateEndpoint
  name: 'default'
  properties: {
    privateDnsZoneConfigs: [
      for (zoneName, index) in privateDnsZoneNames: {
        name: replace(zoneName, '.', '-')
        properties: {
          privateDnsZoneId: privateDnsZones[index].id
        }
      }
    ]
  }
  dependsOn: [
    privateDnsZoneLinks
  ]
}

output privateEndpointId string = privateEndpoint.id
output privateEndpointName string = privateEndpoint.name
output privateDnsZoneIds array = [for (zoneName, index) in privateDnsZoneNames: privateDnsZones[index].id]

デプロイ手順

1. Azure CLIへのログイン

az login

2. リソースグループのデプロイ

az deployment sub create `
  --location japanwest `
  --template-file .\deployments\rg.bicep

3. メインリソースのデプロイ

az deployment sub create `
  --location japanwest `
  --template-file .\deployments\main.bicep

デプロイ時に起きた課題と解決案

  • AppServicePlanの価格(SKU)に関して
    • P0V3でデプロイしようとしたところ失敗
    • 正しくは、WS1, WS2, WS3のグレードで設定可能

main.bicep

// App Service Plan
param appPlanSku string = 'WS1'
param appPlanTier string = 'WorkflowStandard'

module appServicePlanModule '../modules/appserviceplan.bicep' = {
  name: 'AppsServicePlan'
  scope: resourceGroup(rgName)
  params: {
    appServicePlanName: '${development}-${case}appplan001'
    location: location
    tags: tags
    appPlanSku: appPlanSku
    appPlanTier: appPlanTier

    // 診断設定をオンにするときは下記を追加+診断設定モジュールを追加
    // logAnalyticsWorkspaceId: logAnalyticsModule.outputs.logAnalyticsWorkspaceId
    // storageAccountId: logAnalyticsModule.outputs.logstorageAccountId
  }
  dependsOn: [
    //logAnalyticsModule
  ]
}
ヘッドウォータース

Discussion