🦐
【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の構築が必要です。
ディレクトリ構成
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