Skip to content

Commit

Permalink
Merge pull request #5 from Hekku2/feat/key-vault
Browse files Browse the repository at this point in the history
Add Key Vault and User Assigned Managed Identity support
  • Loading branch information
Hekku2 authored Jun 25, 2024
2 parents abb995a + 9f49d22 commit 89f9aa8
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 52 deletions.
5 changes: 5 additions & 0 deletions doc/technical-reasoning.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Azure Function App with dotnet-isolated runtime was chosen.
* Supports Managed Identity
* Can be activated periodically (`TimerTrigger`)
* Supports containers for local development.
* User Assigned Identity
* User Assigned identity was chosen because it solves the chicken-egg
-problem related to function app creation. If System Managed Identity would
be used, we would need to do multi step initialization with function app,
storage RBAC, function app site settings, etc.
* Isolated worker model
* Chosen instead of In-process model, because In-Process support is ending.
* Consumption mode for Function App
Expand Down
57 changes: 45 additions & 12 deletions infra/functions.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ param baseName string
@description('The name of the application insights to be used')
param applicationInsightsName string

@description('Settings for Discord bot.')
param discordSettings DiscordSettings

@description('Settings for image storage.')
param imageStorageSettings ImageStorageSettings

Expand All @@ -21,6 +18,12 @@ param webSitePackageLocation string = ''
@description('If true, messages are not sent to Discord. This should only be used when testing.')
param disableDiscordSending bool = false

@description('The name of the key vault that contains the secrets used by function app.')
param keyVaultName string

@description('The name of the identity that will be used by the function app.')
param identityName string

var hostingPlanName = 'asp-${baseName}'
var functionAppName = 'func-${baseName}'
var storageBlobDataOwnerRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
Expand All @@ -30,6 +33,16 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing
scope: resourceGroup()
}

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: keyVaultName
scope: resourceGroup()
}

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = {
name: identityName
scope: resourceGroup()
}

resource functionStorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: 'st${uniqueString(resourceGroup().id)}'
location: location
Expand Down Expand Up @@ -68,37 +81,50 @@ var discordSettingsKey = 'DiscordConfiguration'
var blobStorageKey = 'BlobStorageImageSourceOptions'
var imageIndexStorageKey = 'ImageIndexOptions'

resource functionApp 'Microsoft.Web/sites@2021-02-01' = {
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
kind: 'linux,functionapp'
name: functionAppName
location: location
tags: {
displayName: 'Function app'
}
identity: {
type: 'SystemAssigned'
type: 'UserAssigned'
userAssignedIdentities: {
'${identity.id}': {}
}
}
properties: {
reserved: true
serverFarmId: hostingPlan.id
httpsOnly: true
keyVaultReferenceIdentity: identity.id
siteConfig: {
defaultDocuments: []
linuxFxVersion: 'DOTNET-ISOLATED|8.0'
phpVersion: null
use32BitWorkerProcess: false
ftpsState: 'FtpsOnly'
ftpsState: 'Disabled'
minTlsVersion: '1.2'
cors: {
allowedOrigins: [
'https://portal.azure.com'
]
}
keyVaultReferenceIdentity: identity.id
appSettings: [
{
name: 'AzureWebJobsStorage__accountName'
value: functionStorageAccount.name
}
{
name: 'AzureWebJobsStorage__clientId'
value: identity.properties.clientId
}
{
name: 'AzureWebJobsStorage__credential'
value: 'managedidentity'
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
Expand All @@ -111,6 +137,14 @@ resource functionApp 'Microsoft.Web/sites@2021-02-01' = {
name: 'WEBSITE_RUN_FROM_PACKAGE'
value: webSitePackageLocation
}
{
name: 'WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID'
value: identity.id
}
{
name: 'AZURE_CLIENT_ID'
value: identity.properties.clientId
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
Expand All @@ -125,15 +159,15 @@ resource functionApp 'Microsoft.Web/sites@2021-02-01' = {
}
{
name: '${discordSettingsKey}__Token'
value: discordSettings.token
value: '@Microsoft.KeyVault(VaultName=${keyVault.name};SecretName=DiscordToken)'
}
{
name: '${discordSettingsKey}__GuildId'
value: '${discordSettings.guildId}'
value: '@Microsoft.KeyVault(VaultName=${keyVault.name};SecretName=DiscordGuildId)'
}
{
name: '${discordSettingsKey}__ChannelId'
value: '${discordSettings.channelId}'
value: '@Microsoft.KeyVault(VaultName=${keyVault.name};SecretName=DiscordChannelId)'
}
{
name: '${blobStorageKey}__BlobContainerUri'
Expand All @@ -154,9 +188,9 @@ resource functionApp 'Microsoft.Web/sites@2021-02-01' = {

resource functionAppFunctionBlobStorageAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: functionStorageAccount
name: guid(functionApp.id, storageBlobDataOwnerRoleDefinitionId, functionStorageAccount.id)
name: guid(identity.id, storageBlobDataOwnerRoleDefinitionId, functionStorageAccount.id)
properties: {
principalId: functionApp.identity.principalId
principalId: identity.properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
Expand All @@ -165,5 +199,4 @@ resource functionAppFunctionBlobStorageAccess 'Microsoft.Authorization/roleAssig
}
}

output functionAppPrincipalId string = functionApp.identity.principalId
output functionAppResourceId string = functionApp.id
65 changes: 65 additions & 0 deletions infra/image-storage.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@minLength(5)
param baseName string

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Names of the container to create in the storage account.')
param imageContainerName string

@description('Identities of which are assigned with Blob Data Reader to the containers.')
param imageReaderIdentities string[]

resource identities 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = [
for identityName in imageReaderIdentities: {
name: identityName
scope: resourceGroup()
}
]

resource imageStorage 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: substring(replace('stimages${baseName}', '-', ''), 0, 24)
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
tags: {
displayName: 'Storage for function app'
}
properties: {
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
isHnsEnabled: true
}
}

resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: imageStorage
name: 'default'
}

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobServices
name: imageContainerName
}

var storageBlobDataReaderRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'
)

resource imageRearderAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for (identityName, i) in imageReaderIdentities: {
scope: container
name: guid(identities[i].id, storageBlobDataReaderRoleDefinitionId, container.id)
properties: {
principalId: identities[i].properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: storageBlobDataReaderRoleDefinitionId
}
}
]

output blobContainerUri string = '${imageStorage.properties.primaryEndpoints.blob}${imageContainerName}'
72 changes: 72 additions & 0 deletions infra/key-vault.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { SecretKeyValue } from 'types.bicep'

@description('Base of the name for all resources.')
param baseName string

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Secrets to store in the key vault.')
param secrets SecretKeyValue[]

@description('Secret users to grant access to the key vault.')
param secretUserRoleIdentities string[]

var secretUserRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6'
)

resource identities 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = [
for identityName in secretUserRoleIdentities: {
name: identityName
scope: resourceGroup()
}
]

var keyVaultName = replace('kv${baseName}', '-', '')
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
enableRbacAuthorization: true
enabledForDeployment: false
enabledForDiskEncryption: false
enabledForTemplateDeployment: false
tenantId: tenant().tenantId
enableSoftDelete: false
accessPolicies: []
sku: {
name: 'standard'
family: 'A'
}
networkAcls: {
defaultAction: 'Allow'
bypass: 'AzureServices'
}
}
}

resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [
for secret in secrets: {
parent: keyVault
name: secret.key
properties: {
value: secret.value
}
}
]

resource secretUserRoleAssingment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for (identityName, i) in secretUserRoleIdentities: {
scope: keyVault
name: guid(identities[i].id, secretUserRoleDefinitionId, keyVault.id)
properties: {
principalId: identities[i].properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: secretUserRoleDefinitionId
}
}
]

output keyVaultName string = keyVaultName
78 changes: 38 additions & 40 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,50 @@ module appInsights 'app-insights.bicep' = {
}
}

// Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only
resource imageStorage 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: substring(replace('stimages${baseName}', '-', ''), 0, 24)
resource functionAppIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
name: 'id-${baseName}'
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
tags: {
displayName: 'Storage for function app'
}
properties: {
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
isHnsEnabled: true
}
}

var imageContainerName = 'images'
resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: imageStorage
name: 'default'
module keyVaultModule 'key-vault.bicep' = {
name: 'key-vault'
params: {
location: location
baseName: baseName
secretUserRoleIdentities: [
functionAppIdentity.name
]
secrets: [
{
key: 'DiscordToken'
value: discordSettings.token
}
{
key: 'DiscordGuildId'
value: '${discordSettings.guildId}'
}
{
key: 'DiscordChannelId'
value: '${discordSettings.channelId}'
}
]
}
}

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobServices
name: imageContainerName
module imageStorageModule 'image-storage.bicep' = {
name: 'image-storage'
params: {
location: location
baseName: baseName
imageContainerName: 'images'
imageReaderIdentities: [
functionAppIdentity.name
]
}
}

var imageSettings = {
blobContainerUri: '${imageStorage.properties.primaryEndpoints.blob}${imageContainerName}'
blobContainerUri: imageStorageModule.outputs.blobContainerUri
folderPath: 'root'
}

Expand All @@ -64,24 +76,10 @@ module functions 'functions.bicep' = {
applicationInsightsName: appInsights.outputs.applicationInsightsName
location: location
baseName: baseName
discordSettings: discordSettings
keyVaultName: keyVaultModule.outputs.keyVaultName
imageStorageSettings: imageSettings
webSitePackageLocation: webSitePackageLocation
disableDiscordSending: disableDiscordSending
}
}

// TODO Also this assignment could probably be a separate module etc.
var storageBlobDataReaderRoleDefinitionId = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'
resource functionAppFunctionBlobStorageAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: container
name: guid(functions.name, storageBlobDataReaderRoleDefinitionId, container.id)
properties: {
principalId: functions.outputs.functionAppPrincipalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
storageBlobDataReaderRoleDefinitionId
)
identityName: functionAppIdentity.name
}
}
Loading

0 comments on commit 89f9aa8

Please sign in to comment.