Merge work from Global Hackathon 2023 (#25)
* Changes 0825-0333pm * added KV Secrets loops. * Vishal's Changes 0831-0257 * Prepare .gitignore for customer param files * Fix vnet and web app max lengths * Create sample bicepparam file * Create deployment PS script * Support for App Service Plan and App Insights * Naming cleanup: Create resource group names that match namingConvention Add parameter descriptions Remove storage FQDN hardcoding Update module names Add TODO comments * Add TODO comments * Fixes DBHostName Web App setting is incorrect kalalvishal/azure-redcap-paas #13 * Remove unnecessary default param value * Fixes #20 * Add TODO comment * Add comments * Fixes Network address parameter #24 * Add `vnetAddressSpace` param to sample param file * fixed keyvault role assignment deployment issues * feedback update * Add TODO comments * Fixes Key Vault reference in App Service #15 * Specify MySQL credentials as parameters Create Generate-Password PS module to create a strong password * Fixes #19 * Add support for UAMI and deployment scripts * Set sql_generate_invisible_primary_key OFF using deployment script * Updated WebApp and created a new module for monitoring. * parameterize redcapZipUri, redcapCommunityUserName&Password (#30) * parameterize redcapZipUri, redcapCommunityUserName&Password * store redcap credentials in kv and reference from web app settings * feedback update * Updates performed as per the comments. * Updates performed as per the comments. * Update law.bicep * Conflict fixed. * Use JSON file for deploy to support inline param Update sample param file * Update param descriptions * Create structured and unique deployment names * Update sample param file with ref to param val * github workflow added. * fixed the changes required for issue #37 * fixed the changes required for issue #36 * update Bicep-build.yml based on the comments. * Add clarifying comments to sample param file * Change webApp to app to align with recommendations * Reference MySQL username from KV secret * Fix Bicep linting * Deploy.sh support & fixes (#47) * Author: Seokwon Yang <seyan@microsoft.com> Date: Fri Sep 15 07:44:52 2023 -0700 deployment enhancement & fixes * feedback upate * Perform root folder cleanup; fixes #39 Rename azDeploySecureSub to main * Update GH action from Vishal --------- Co-authored-by: Sven Aelterman <17446043+SvenAelterman@users.noreply.github.com> * Remove location list in deploy.ps1, main.bicep * Update README Add information about deploy.ps1 Remove or comment out outdated text * Exclude `/` from password characters Fixes #57 * Add additional storage-related app settings Fixes #58 * Cleanup * Fixes #55 and #56 * Addresses #63 but needs more work to ensure reliability of Key Vault refs * changed based on the last test. * added manual.md and configuration.md * Update env var names * Remove @secret attribute from KV reference params --------- Co-authored-by: Vishal Kalal <vishal.kalal@outlook.com> Co-authored-by: kalalvishal <vishal.rajasthan@gmail.com> Co-authored-by: Sven Aelterman <17446043+SvenAelterman@users.noreply.github.com> Co-authored-by: Seokwon Yang <seyan@microsoft.com> Co-authored-by: sjyang18 <41694933+sjyang18@users.noreply.github.com>
This commit is contained in:
Родитель
859988e173
Коммит
df8ee32663
|
@ -1,3 +1,3 @@
|
|||
[config]
|
||||
command = bash deploy.sh
|
||||
SCM_COMMAND_IDLE_TIMEOUT=600
|
||||
command = bash scripts/bash/deploy.sh
|
||||
SCM_COMMAND_IDLE_TIMEOUT=1200
|
|
@ -0,0 +1,76 @@
|
|||
## deploy azDeploySecureSub.bicep
|
||||
|
||||
name: Azure REDCap Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
azCliVersion: 2.30.0
|
||||
environment: 'env-redcap'
|
||||
region: 'eastus'
|
||||
|
||||
jobs:
|
||||
# Validate the Bicep templates
|
||||
validateDeployment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
name: Checkout
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Azure Login
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
# Deploy Bicep file
|
||||
- name: validateTemplates
|
||||
uses: azure/arm-deploy@v1
|
||||
with:
|
||||
scope: 'subscription'
|
||||
template: ./main.bicep
|
||||
deploymentMode: 'Validate'
|
||||
region: ${{ env.region }}
|
||||
|
||||
- name: planDeployment
|
||||
uses: azure/arm-deploy@v1
|
||||
with:
|
||||
scope: 'subscription'
|
||||
template: ./main.bicep
|
||||
additionalArguments: "--what-if"
|
||||
region: ${{ env.region }}
|
||||
|
||||
# Deploy the resources
|
||||
deployResources:
|
||||
if: ( github.ref == 'refs/heads/main' )
|
||||
runs-on: ubuntu-latest
|
||||
environment: 'nonProduction' ## Replce with your environment name
|
||||
needs: [
|
||||
validateDeployment
|
||||
]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
name: Checkout
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Azure Login
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
# Deploy Bicep file
|
||||
- name: deploy
|
||||
uses: azure/arm-deploy@v1
|
||||
with:
|
||||
scope: 'subscription'
|
||||
template: ./main.bicep
|
||||
region: ${{ env.region }}
|
|
@ -330,3 +330,10 @@ ASALocalRun/
|
|||
.mfractor/
|
||||
*.sln
|
||||
*.deployproj
|
||||
|
||||
/*.json
|
||||
|
||||
# Exclude Bicep parameter files
|
||||
*.bicepparam
|
||||
# Except for the sample file
|
||||
!/*-sample.bicepparam
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"dotnetAcquisitionExtension.existingDotnetPath": [
|
||||
"/usr/local/dotnet/current/dotnet"
|
||||
]
|
||||
],
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
$startTime=Get-Date
|
||||
Write-Host "Beginning deployment at $starttime"
|
||||
|
||||
Import-Module Azure -ErrorAction SilentlyContinue
|
||||
$version = 0;
|
||||
|
||||
#DEPLOYMENT OPTIONS
|
||||
#Please review the azuredeploy.json file for available options
|
||||
$RGName = "<YOUR RESOURCE GROUP>"
|
||||
$DeployRegion = "<SELECT AZURE REGION>"
|
||||
$AssetLocation = "https://github.com/vanderbilt-redcap/redcap-azure/blob/master/azuredeploy.json"
|
||||
|
||||
$parms = @{
|
||||
|
||||
#Alternative to the zip file above, you can use REDCap Community credentials to download the zip file.
|
||||
"redcapCommunityUsername" = "<REDCap Community site username>";
|
||||
"redcapCommunityPassword" = "<REDCap Community site password>";
|
||||
"redcapAppZipVersion" = "<REDCap version";
|
||||
|
||||
#Mail settings
|
||||
"fromEmailAddress" = "<email address listed as sender for outbound emails>";
|
||||
"smtpFQDN" = "<what it says>"
|
||||
"smtpUser" = "<login name for smtp auth>"
|
||||
"smtpPassword" = "<password for smtp auth>"
|
||||
|
||||
#Azure Web App
|
||||
"siteName" = "<WEB SITE NAME, like 'redcap'>";
|
||||
"skuName" = "S1";
|
||||
"skuCapacity" = 1;
|
||||
|
||||
#MySQL
|
||||
"administratorLogin" = "<MySQL admin account name>";
|
||||
"administratorLoginPassword" = "<MySQL admin login password>";
|
||||
|
||||
"databaseForMySqlCores" = 2;
|
||||
"databaseForMySqlFamily" = "Gen5";
|
||||
"databaseSkuSizeMB" = 5120;
|
||||
"databaseForMySqlTier" = "GeneralPurpose";
|
||||
"mysqlVersion" = "5.7";
|
||||
|
||||
#Azure Storage
|
||||
"storageType" = "Standard_LRS";
|
||||
"storageContainerName" = "redcap";
|
||||
|
||||
#GitHub
|
||||
"repoURL" = "https://github.com/vanderbilt-redcap/redcap-azure.git";
|
||||
"branch" = "master";
|
||||
}
|
||||
#END DEPLOYMENT OPTIONS
|
||||
|
||||
#Dot-sourced variable override (optional, comment out if not using)
|
||||
$dotsourceSettings = "$($env:PSH_Settings_Files)redcap-azure.ps1"
|
||||
if (Test-Path $dotsourceSettings) {
|
||||
. $dotsourceSettings
|
||||
}
|
||||
|
||||
#ensure we're logged in
|
||||
Get-AzureRmContext -ErrorAction Stop
|
||||
|
||||
#deploy
|
||||
$TemplateFile = "$($AssetLocation)?x=$version"
|
||||
|
||||
try {
|
||||
Get-AzureRmResourceGroup -Name $RGName -ErrorAction Stop
|
||||
Write-Host "Resource group $RGName exists, updating deployment"
|
||||
}
|
||||
catch {
|
||||
$RG = New-AzureRmResourceGroup -Name $RGName -Location $DeployRegion
|
||||
Write-Host "Created new resource group $RGName."
|
||||
}
|
||||
$version ++
|
||||
$deployment = New-AzureRmResourceGroupDeployment -ResourceGroupName $RGName -TemplateParameterObject $parms -TemplateFile $TemplateFile -Name "RedCAPDeploy$version" -Force -Verbose
|
||||
|
||||
if ($deployment.ProvisioningState -eq "Succeeded") {
|
||||
$siteName = $deployment.Outputs.webSiteFQDN.Value
|
||||
start "https://$($siteName)/AzDeployStatus.php"
|
||||
Write-Host "---------"
|
||||
$deployment.Outputs | ConvertTo-Json
|
||||
|
||||
} else {
|
||||
$deperr = Get-AzureRmResourceGroupDeploymentOperation -DeploymentName "RedCAPDeploy$version" -ResourceGroupName $RGName
|
||||
$deperr | ConvertTo-Json
|
||||
}
|
||||
|
||||
$endTime=Get-Date
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Total Deployment time:"
|
||||
New-TimeSpan -Start $startTime -End $endTime | Select Hours, Minutes, Seconds
|
|
@ -4,7 +4,7 @@ upload_max_filesize = 32M
|
|||
post_max_size = 32M
|
||||
|
||||
; Mail settings
|
||||
SMTP = 'replace_smtp_server_name'
|
||||
smpt_port = replace_smtp_port
|
||||
sendmail_from = 'replace_sendmail_from'
|
||||
sendmail_path='replace_sendmail_path'
|
||||
SMTP = ''
|
||||
smtp_port =
|
||||
sendmail_from = ''
|
||||
sendmail_path = ''
|
||||
|
|
61
README.md
61
README.md
|
@ -1,11 +1,24 @@
|
|||
# ARM Template for REDCap automated deployment in Azure
|
||||
# REDCap Deployment on Azure
|
||||
|
||||
## Quick Start
|
||||
### Overview
|
||||
This repository provides you with the necessary resources and guidance to deploy the REDCap application on Microsoft’s Azure cloud platform. This allows you to leverage the power of cloud computing for your research data management needs.
|
||||
|
||||
| Description | Link | Azure US Gov Link |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - |
|
||||
| Deploy with your SMTP Relay | [![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fazure-redcap-paas%2Fmain%2Fazuredeploy.json) | [![Deploy To Azure US Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fazure-redcap-paas%2Fmain%2Fazuredeploy.json) |
|
||||
| Deploy using SendGrid | [![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fazure-redcap-paas%2Fmain%2Fazuredeploy_with_SendGrid.json) | [![Deploy To Azure US Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fazure-redcap-paas%2Fmain%2Fazuredeploy_with_SendGrid.json) |
|
||||
This template automates the deployment of the REDCap solution into Azure using managed PaaS resources. The template assumes you are deploying a version of REDCap that supports direct connection to Azure Blob Storage. If you deploy an older version, deployment will succeed but you will need to manually provision NFS storage in Azure, and delete the new storage account. For NFS, consider:
|
||||
|
||||
|
||||
|
||||
### Deployment Options
|
||||
- ### Manual deployment
|
||||
|
||||
- For manual deployment process, please navigate [***here***](manual.md)
|
||||
|
||||
- ### CI/CD Deployment with GitHub
|
||||
|
||||
- Information pending
|
||||
|
||||
- ### CI/CD Deployment with Azure DevOps
|
||||
|
||||
- Information pending
|
||||
|
||||
### Details
|
||||
|
||||
|
@ -15,33 +28,37 @@ This template automates the deployment of the REDCap solution into Azure using m
|
|||
- <https://azuremarketplace.microsoft.com/marketplace/apps/softnas.buurst_nas>
|
||||
- <https://learn.microsoft.com/samples/azure/azure-quickstart-templates/nfs-ha-cluster-ubuntu/>
|
||||
|
||||
To deploy REDCap source to Azure App Service, you must supply your REDCap Community site credentials which the deployment automation will use to pull your copy of the REDCap source directly from the community site.
|
||||
To deploy the REDCap source to Azure App Service, you must supply your REDCap Community site credentials. The deployment automation will use them to pull the REDCap source directly from the community site.
|
||||
|
||||
> NOTE: These values will be stored within the Azure App Service as configuration settings. Once your deployment has succeeded, you should navigate to your Azure App Service resource and delete or empty out the values so that they aren't stored here.
|
||||
> NOTE: These values will be stored within the Azure App Service as configuration settings. Once your deployment has succeeded, you should navigate to your Azure App Service resource and delete or clear the values so that they aren't stored here.
|
||||
|
||||
![Azure App Service](/images/app-settings.png)
|
||||
|
||||
<https://projectredcap.org/wp-content/resources/REDCapTechnicalOverview.pdf>
|
||||
|
||||
- ARM template deploys the following:
|
||||
- The template deploys the following:
|
||||
- Azure Web App
|
||||
- Azure DB for MySQL (1)
|
||||
- Azure Storage Account
|
||||
- (optional) SendGrid 3rd Party Email service (2)
|
||||
- Key Vault
|
||||
- Private DNS zones
|
||||
- Virtual Network
|
||||
- Application Insights
|
||||
<!-- - (optional) SendGrid 3rd Party Email service (2) -->
|
||||
|
||||
(1) Review <https://learn.microsoft.com/azure/mysql/flexible-server/concepts-service-tiers-storage> for details on available features, regions, and pricing models for Azure DB for MySQL.
|
||||
|
||||
(2) SendGrid is a paid service with a free tier offering 25k messages per month, with additional paid tiers offering more volume, whitelisting, custom domains, etc. There is a limit of two instances per subscription using the free tier. For more information see <https://docs.microsoft.com/en-us/azure/store-sendgrid-php-how-to-send-email#create-a-sendgrid-account>. The service will be accessed initially using the password you enter in the deployment template. You can click "Manage" on the SendGrid service after deployment to administrate the service in their portal, including options to create an API key that can be used for access instead of the password.
|
||||
<!--(2) SendGrid is a paid service with a free tier offering 25k messages per month, with additional paid tiers offering more volume, whitelisting, custom domains, etc. There is a limit of two instances per subscription using the free tier. For more information see <https://docs.microsoft.com/en-us/azure/store-sendgrid-php-how-to-send-email#create-a-sendgrid-account>. The service will be accessed initially using the password you enter in the deployment template. You can click "Manage" on the SendGrid service after deployment to administrate the service in their portal, including options to create an API key that can be used for access instead of the password.
|
||||
|
||||
If after deployment, you would instead like to use a different SMTP relay, edit the values "smtp_fqdn_name", "smtp_port", "smtp_user_name", and "smtp_password" to point to your preferred endpoint. You can then delete the SendGrid service from this resource group.
|
||||
|
||||
If you use Exchange Online (part of the Microsoft 365 Suite), you can follow these steps to set it up and use it as an SMTP relay for this service: <https://learn.microsoft.com/Exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365>
|
||||
If you use Exchange Online (part of the Microsoft 365 Suite), you can follow these steps to set it up and use it as an SMTP relay for this service: <https://learn.microsoft.com/Exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365> -->
|
||||
|
||||
### Setup
|
||||
|
||||
This template will automatically deploy the resources necessary to run REDCap in Azure using PaaS (Platform-as-a-Service) features.
|
||||
|
||||
**IMPORTANT**: _The "Site Name" you choose will be re-used as part of the storage, website, and MySql database name. Make sure you don't use characters that will be rejected by MySql._
|
||||
**IMPORTANT**: _The "Workload Name" you choose will be re-used as part of the storage, website, and MySQL database name. Make sure you don't use characters that will be rejected by MySQL._
|
||||
|
||||
After the template is deployed, deployment automation will download the REDCap ZIP file you specify, and install it in your web app. It will then automatically update the database connection information in the app.
|
||||
|
||||
|
@ -49,17 +66,9 @@ After the template is deployed, deployment automation will download the REDCap Z
|
|||
|
||||
With the download and unzipping of REDCap application, the entire operation will take between 12-16 minutes.
|
||||
|
||||
If you need to connect to the MySQL database using the MySQL client, you will need to open the firewall to your managed MySQL instance and allow connections from the location where you will run the client. Here are the instructions:
|
||||
<https://docs.microsoft.com/en-us/azure/mysql/quickstart-create-mysql-server-database-using-azure-portal#configure-a-server-level-firewall-rule>
|
||||
If you need to connect to the MySQL database using the MySQL client, you will need to deploy a Virtual Machine with Bastion or AVD to the virtual network to run the client.
|
||||
|
||||
(Add your current IP address by clicking "+ Add My IP")
|
||||
|
||||
Once you've opened the firewall, you will need your database name. The credentials are those you supplied in this template. The name is available from the portal where you updated the firewall rules:
|
||||
|
||||
![alt text][mysql]
|
||||
|
||||
Please also review:
|
||||
<https://learn.microsoft.com/azure/mysql/flexible-server/how-to-connect-tls-ssl>
|
||||
The database user name defaults to `sqladmin` and the password is a random string of 25 characters. The password is stored in Key Vault.
|
||||
|
||||
### Post-Setup
|
||||
|
||||
|
@ -79,13 +88,13 @@ bash install.sh
|
|||
|
||||
It will take a few minutes to execute the SQL.
|
||||
|
||||
Once you regain access to the console, you can navigate to the root of your app service and confirm everything shows green on the REDCap Configuration Check page - with the exception of CronJob status which you may have to manually invoke. If anything displays on that page in red or yellow, it is recommended that you perform a "Restart" of the Azure "App Service". This needs to be done due to the fact that some necessary server environment settings get changed after the initial deployment, but restarting the App Service will load the service with the intended settings. Everything should be fine after that initial restart though.
|
||||
Once you regain access to the console, you can navigate to the root of your app service and confirm everything shows green on the REDCap Configuration Check page - with the exception of CronJob status which you may have to manually invoke. If anything displays on that page in red or yellow, it is recommended that you perform a "Restart" of the Azure "App Service". This needs to be done due to the fact that some necessary server environment settings get changed after the initial deployment, but restarting the App Service will load the service with the intended settings.
|
||||
|
||||
### Note about REDCap "Easy Upgade"
|
||||
## Note about REDCap "Easy Upgade"
|
||||
|
||||
The "Easy Upgrade" feature in REDCap 8.11.0 and later is currently _not_ supported when deploying a REDCap instance on Azure. Support for "Easy Upgrade" on Azure is expected to come at a later time in a future REDCap release.
|
||||
|
||||
### Resources
|
||||
## Resources
|
||||
|
||||
- App Services overview
|
||||
<https://learn.microsoft.com/azure/app-service/overview>
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,72 @@
|
|||
# PowerShell script to deploy the main.bicep template with parameter values
|
||||
|
||||
#Requires -Modules "Az"
|
||||
#Requires -PSEdition Core
|
||||
|
||||
# Use these parameters to customize the deployment instead of modifying the default parameter values
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Position = 1)]
|
||||
[string]$Location,
|
||||
[Parameter(Position = 2)]
|
||||
[string]$TemplateParameterFile = "./main-sample.bicepparam",
|
||||
[Parameter(Position = 3)]
|
||||
[string]$SubscriptionId
|
||||
)
|
||||
|
||||
# Define common parameters for the New-AzDeployment cmdlet
|
||||
[hashtable]$CmdLetParameters = @{
|
||||
Location = $Location
|
||||
TemplateFile = '.\main.bicep'
|
||||
}
|
||||
|
||||
# Convert the .bicepparam file to JSON to read values that will be used to construct the deployment name
|
||||
$JsonParamFile = [System.IO.Path]::ChangeExtension($TemplateParameterFile, 'json')
|
||||
Write-Verbose $JsonParamFile
|
||||
bicep build-params $TemplateParameterFile --outfile $JsonParamFile
|
||||
|
||||
<# HACK: 2023-09-14: At this time, .bicepparam cannot be combined with inline parameters,
|
||||
which is needed to supply a new random database password. So we're using the JSON file here too. #>
|
||||
$CmdLetParameters.Add('TemplateParameterFile', $JsonParamFile)
|
||||
|
||||
# Read the values from the parameters file, to use when generating the $DeploymentName value
|
||||
$ParameterFileContents = (Get-Content $JsonParamFile | ConvertFrom-Json)
|
||||
[string]$WorkloadName = $ParameterFileContents.parameters.workloadName.value
|
||||
[string]$Environment = $ParameterFileContents.parameters.environment.value
|
||||
|
||||
# Generate a unique name for the deployment
|
||||
[string]$DeploymentName = "$WorkloadName-$Environment-$(Get-Date -Format 'yyyyMMddThhmmssZ' -AsUTC)"
|
||||
$CmdLetParameters.Add('Name', $DeploymentName)
|
||||
|
||||
# Determine if a cloud context switch is required
|
||||
$AzContext = Get-AzContext
|
||||
|
||||
if ($SubscriptionId -ne $AzContext.Subscription.Id) {
|
||||
Write-Verbose "Current subscription: '$($AzContext.Subscription.Id)'. Switching subscription."
|
||||
Select-AzSubscription $SubscriptionId
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Current Subscription: '$($AzContext.Subscription.Name)'. No switch needed."
|
||||
}
|
||||
|
||||
# Import the Generate-Password module
|
||||
Import-Module .\scripts\PowerShell\Generate-Password.psm1
|
||||
|
||||
# Generate a 25 character random password for the MySQL admin user
|
||||
[securestring]$SqlPassword = New-RandomPassword 25
|
||||
|
||||
# Remove the Generate-Password module from the session
|
||||
Remove-module Generate-Password
|
||||
|
||||
$CmdLetParameters.Add('sqlPassword', $SqlPassword)
|
||||
|
||||
# Execute the deployment
|
||||
$DeploymentResult = New-AzDeployment @CmdLetParameters
|
||||
|
||||
# Evaluate the deployment results
|
||||
if ($DeploymentResult.ProvisioningState -eq 'Succeeded') {
|
||||
Write-Host "🔥 Deployment succeeded."
|
||||
}
|
||||
else {
|
||||
$DeploymentResult
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using './main.bicep'
|
||||
|
||||
// These parameters might have acceptable defaults.
|
||||
param location = 'eastus'
|
||||
param environment = 'demo'
|
||||
param workloadName = 'redcap'
|
||||
param namingConvention = '{workloadName}-{env}-{rtype}-{loc}-{seq}'
|
||||
param sequence = 1
|
||||
|
||||
// These parameters should be modified for your environment
|
||||
param identityObjectId = '<Valid Entra ID object ID for permissions assignment>'
|
||||
param vnetAddressSpace = '10.0.0.0/24'
|
||||
|
||||
// If providing redcapZipUrl, you do not need to provide REDCap community username and password.
|
||||
// redcapZipUrl should not require authentication.
|
||||
param redcapZipUrl = '<Valid Redcap Zip URL>'
|
||||
param redcapCommunityUsername = '<Valid Redcap Community Username>'
|
||||
param redcapCommunityPassword = '<Valid Redcap Community Password>'
|
||||
|
||||
param scmRepoUrl = '<Valid Scm Repo URL where build scripts are downloaded from>'
|
||||
param scmRepoBranch = '<Valid Scm Repo Branch where build scripts are downloaded from>'
|
||||
|
||||
// ** Do not specify anything here! **
|
||||
// This parameter is required to be here but should be blank so the password doesn't leak.
|
||||
// A password is generated for each deployment.
|
||||
param sqlPassword = ''
|
|
@ -0,0 +1,405 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
@description('The Azure region to target for the deployment. Replaces {loc} in namingConvention.')
|
||||
param location string = 'eastus'
|
||||
|
||||
@description('The environment designator for the deployment. Replaces {env} in namingConvention.')
|
||||
@allowed([
|
||||
'test'
|
||||
'demo'
|
||||
'prod'
|
||||
])
|
||||
param environment string = 'demo'
|
||||
@description('The workload name. Replaces {workloadName} in namingConvention.')
|
||||
param workloadName string = 'redcap'
|
||||
@description('The Azure resource naming convention. Include the following placeholders (case-sensitive): {workloadName}, {env}, {rtype}, {loc}, {seq}.')
|
||||
param namingConvention string = '{workloadName}-{env}-{rtype}-{loc}-{seq}'
|
||||
@description('A sequence number for the deployment. Used to distinguish multiple deployed versions of the same workload. Replaces {seq} in namingConvention.')
|
||||
@minValue(1)
|
||||
@maxValue(99)
|
||||
param sequence int = 1
|
||||
|
||||
@description('A valid Entra ID object ID, which will be assigned RBAC permissions on the deployed resources.')
|
||||
param identityObjectId string
|
||||
|
||||
@description('The address space for the virtual network. Subnets will be carved out. Minimum IPv4 size: /24.')
|
||||
param vnetAddressSpace string
|
||||
@description('If available, the public URL to download the REDCap zip file from. Used for debugging purposes. Does not need to be specified when downloading from the REDCap community using a username and password.')
|
||||
@secure()
|
||||
param redcapZipUrl string = ''
|
||||
@description('REDCap Community site username for downloading the REDCap zip file.')
|
||||
@secure()
|
||||
param redcapCommunityUsername string
|
||||
|
||||
@description('REDCap Community site password for downloading the REDCap zip file.')
|
||||
@secure()
|
||||
param redcapCommunityPassword string
|
||||
@description('Github Repo URL where build scripts are downloaded from')
|
||||
param scmRepoUrl string = 'https://github.com/microsoft/azure-redcap-paas'
|
||||
@description('Github Repo Branch where build scripts are downloaded from')
|
||||
param scmRepoBranch string = 'main'
|
||||
@description('The prerequsites command before build to be run on the web app with an elevated privilege. This is used to install the required packages for REDCap.')
|
||||
param preRequsitesCommand string = 'apt-get install unzip -y && apt-get install -y python3 python3-pip'
|
||||
|
||||
param deploymentTime string = utcNow()
|
||||
|
||||
@description('The password to use for the MySQL Flexible Server admin account \'sqladmin\'.')
|
||||
@secure()
|
||||
param sqlPassword string
|
||||
|
||||
param sqlAdmin string = 'sqladmin'
|
||||
|
||||
var sequenceFormatted = format('{0:00}', sequence)
|
||||
var rgNamingStructure = replace(replace(replace(replace(replace(namingConvention, '{rtype}', 'rg'), '{workloadName}', '${workloadName}-{rgName}'), '{loc}', location), '{seq}', sequenceFormatted), '{env}', environment)
|
||||
var vnetName = nameModule[0].outputs.shortName
|
||||
var strgName = nameModule[1].outputs.shortName
|
||||
var webAppName = nameModule[2].outputs.shortName
|
||||
var kvName = nameModule[3].outputs.shortName
|
||||
var sqlName = nameModule[4].outputs.shortName
|
||||
var planName = nameModule[5].outputs.shortName
|
||||
var uamiName = nameModule[6].outputs.shortName
|
||||
var dplscrName = nameModule[7].outputs.shortName
|
||||
var lawName = nameModule[8].outputs.shortName
|
||||
|
||||
var deploymentNameStructure = '${workloadName}-${environment}-${sequenceFormatted}-{rtype}-${deploymentTime}'
|
||||
|
||||
var subnets = {
|
||||
// TODO: Define securityRules
|
||||
PrivateLinkSubnet: {
|
||||
addressPrefix: cidrSubnet(vnetAddressSpace, 27, 0)
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.KeyVault'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Storage'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
ComputeSubnet: {
|
||||
addressPrefix: cidrSubnet(vnetAddressSpace, 27, 1)
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.KeyVault'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Storage'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Web'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
IntegrationSubnet: {
|
||||
// Two /27 have already been created, which add up to a /26. This the second /26 (index = 1).
|
||||
addressPrefix: cidrSubnet(vnetAddressSpace, 26, 1)
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.KeyVault'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Storage'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Web'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
]
|
||||
delegation: 'Microsoft.Web/serverFarms'
|
||||
}
|
||||
MySQLFlexSubnet: {
|
||||
// TODO: /29 seems very small
|
||||
// Two /26 have been allocated; that's equivalent to sixteen /29s.
|
||||
addressPrefix: cidrSubnet(vnetAddressSpace, 29, 16)
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.KeyVault'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
{
|
||||
service: 'Microsoft.Storage'
|
||||
locations: [
|
||||
location
|
||||
]
|
||||
}
|
||||
]
|
||||
delegation: 'Microsoft.DBforMySQL/flexibleServers'
|
||||
}
|
||||
}
|
||||
|
||||
var tags = {
|
||||
workload: workloadName
|
||||
environment: environment
|
||||
}
|
||||
|
||||
var secrets = {
|
||||
sqlAdminName: mySqlModule.outputs.sqlAdmin
|
||||
sqlPassword: sqlPassword
|
||||
redcapCommunityUsername: redcapCommunityUsername
|
||||
redcapCommunityPassword: redcapCommunityPassword
|
||||
}
|
||||
|
||||
var resourceTypes = [
|
||||
'vnet'
|
||||
'st'
|
||||
'app'
|
||||
'kv'
|
||||
'mysql'
|
||||
'plan'
|
||||
'uami'
|
||||
'dplscr'
|
||||
'law'
|
||||
]
|
||||
|
||||
@batchSize(1)
|
||||
module nameModule 'modules/common/createValidAzResourceName.bicep' = [for workload in resourceTypes: {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'nameGen-${workload}'), 64)
|
||||
params: {
|
||||
location: location
|
||||
environment: environment
|
||||
namingConvention: namingConvention
|
||||
resourceType: workload
|
||||
sequence: sequence
|
||||
workloadName: workloadName
|
||||
addRandomChars: 4
|
||||
}
|
||||
}]
|
||||
|
||||
module rolesModule './modules/common/roles.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'roles'), 64)
|
||||
}
|
||||
|
||||
var storageAccountKeySecretName = 'storageKey'
|
||||
// The secrets object is converted to an array using the items() function, which alphabetically sorts it
|
||||
var defaultSecretNames = map(items(secrets), s => s.key)
|
||||
var additionalSecretNames = [ storageAccountKeySecretName ]
|
||||
var secretNames = concat(defaultSecretNames, additionalSecretNames)
|
||||
|
||||
// The output will be in alphabetical order
|
||||
// LATER: Output an object instead
|
||||
module kvSecretReferencesModule './modules/common/appSvcKeyVaultRefs.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'kv-secrets'), 64)
|
||||
params: {
|
||||
keyVaultName: kvName
|
||||
secretNames: secretNames
|
||||
}
|
||||
}
|
||||
|
||||
module virtualNetworkModule './modules/networking/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'network'), 64)
|
||||
params: {
|
||||
resourceGroupName: replace(rgNamingStructure, '{rgName}', 'network')
|
||||
virtualNetworkName: vnetName
|
||||
vnetAddressPrefix: vnetAddressSpace
|
||||
location: location
|
||||
subnets: subnets
|
||||
customDnsIPs: []
|
||||
tags: tags
|
||||
customTags: {
|
||||
workloadType: 'networking'
|
||||
}
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
module monitoring './modules/monitoring/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'monitoring'), 64)
|
||||
params: {
|
||||
resourceGroupName: replace(rgNamingStructure, '{rgName}', 'monitoring')
|
||||
appInsightsName: 'appInsights-${webAppName}'
|
||||
logAnalyticsWorkspaceName: lawName
|
||||
logAnalyticsWorkspaceSku: 'PerGB2018'
|
||||
retentionInDays: 30
|
||||
location: location
|
||||
tags: tags
|
||||
customTags: {
|
||||
workloadType: 'monitoring'
|
||||
}
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
module storageAccountModule './modules/storage/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'storage'), 64)
|
||||
params: {
|
||||
resourceGroupName: replace(rgNamingStructure, '{rgName}', 'storage')
|
||||
location: location
|
||||
storageAccountName: strgName
|
||||
peSubnetId: virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id
|
||||
storageContainerName: 'redcap'
|
||||
kind: 'StorageV2'
|
||||
storageAccountSku: 'Standard_LRS'
|
||||
virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId
|
||||
privateDnsZoneName: 'privatelink.blob.${az.environment().suffixes.storage}'
|
||||
tags: tags
|
||||
customTags: {
|
||||
workloadType: 'storageAccount'
|
||||
}
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
|
||||
keyVaultSecretName: storageAccountKeySecretName
|
||||
keyVaultId: keyVaultModule.outputs.id
|
||||
}
|
||||
}
|
||||
|
||||
module keyVaultModule './modules/kv/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'keyVault'), 64)
|
||||
params: {
|
||||
resourceGroupName: replace(rgNamingStructure, '{rgName}', 'keyVault')
|
||||
keyVaultName: kvName
|
||||
location: location
|
||||
tags: tags
|
||||
customTags: {
|
||||
workloadType: 'keyVault'
|
||||
}
|
||||
peSubnetId: virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id
|
||||
virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId
|
||||
roleAssignments: [
|
||||
{
|
||||
RoleDefinitionId: rolesModule.outputs.roles['Key Vault Administrator']
|
||||
objectId: identityObjectId
|
||||
}
|
||||
{
|
||||
RoleDefinitionId: rolesModule.outputs.roles['Key Vault Secrets User']
|
||||
objectId: uamiModule.outputs.principalId
|
||||
}
|
||||
]
|
||||
privateDnsZoneName: 'privatelink.vaultcore.azure.net'
|
||||
secrets: secrets
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
module mySqlModule './modules/sql/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'mysql'), 64)
|
||||
params: {
|
||||
resourceGroupName: replace(rgNamingStructure, '{rgName}', 'database')
|
||||
flexibleSqlServerName: sqlName
|
||||
location: location
|
||||
tags: tags
|
||||
|
||||
customTags: {
|
||||
workloadType: 'mySqlFlexibleServer'
|
||||
}
|
||||
skuName: 'Standard_B1s'
|
||||
SkuTier: 'Burstable'
|
||||
StorageSizeGB: 20
|
||||
StorageIops: 396
|
||||
peSubnetId: virtualNetworkModule.outputs.subnets.MySQLFlexSubnet.id
|
||||
privateDnsZoneName: 'privatelink.mysql.database.azure.com'
|
||||
sqlAdminUser: sqlAdmin
|
||||
sqlAdminPasword: sqlPassword
|
||||
mysqlVersion: '8.0.21'
|
||||
// TODO: Consider using workloadname + 'db'
|
||||
databaseName: 'redcapdb'
|
||||
|
||||
roles: rolesModule.outputs.roles
|
||||
|
||||
uamiId: uamiModule.outputs.id
|
||||
uamiPrincipalId: uamiModule.outputs.principalId
|
||||
|
||||
deploymentScriptName: dplscrName
|
||||
|
||||
// Required charset and collation for REDCap
|
||||
database_charset: 'utf8'
|
||||
database_collation: 'utf8_general_ci'
|
||||
|
||||
virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
resource webAppResourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
|
||||
name: replace(rgNamingStructure, '{rgName}', 'web')
|
||||
location: location
|
||||
}
|
||||
|
||||
module webAppModule './modules/webapp/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'appService'), 64)
|
||||
scope: webAppResourceGroup
|
||||
params: {
|
||||
webAppName: webAppName
|
||||
appServicePlanName: planName
|
||||
location: location
|
||||
// TODO: Consider deploying as P0V3 to ensure the deployment runs on a scale unit that supports P_v3 for future upgrades. GH issue #50
|
||||
skuName: 'S1'
|
||||
skuTier: 'Standard'
|
||||
peSubnetId: virtualNetworkModule.outputs.subnets.ComputeSubnet.id
|
||||
appInsights_connectionString: monitoring.outputs.appInsightsResourceId
|
||||
appInsights_instrumentationKey: monitoring.outputs.appInsightsInstrumentationKey
|
||||
linuxFxVersion: 'php|8.2'
|
||||
tags: tags
|
||||
customTags: {
|
||||
workloadType: 'webApp'
|
||||
}
|
||||
privateDnsZoneName: 'privatelink.azurewebsites.net'
|
||||
virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId
|
||||
redcapZipUrl: redcapZipUrl
|
||||
dbHostName: mySqlModule.outputs.fqdn
|
||||
dbName: mySqlModule.outputs.databaseName
|
||||
|
||||
dbUserNameSecretRef: kvSecretReferencesModule.outputs.keyVaultRefs[2]
|
||||
dbPasswordSecretRef: kvSecretReferencesModule.outputs.keyVaultRefs[3]
|
||||
|
||||
redcapCommunityUsernameSecretRef: kvSecretReferencesModule.outputs.keyVaultRefs[1]
|
||||
redcapCommunityPasswordSecretRef: kvSecretReferencesModule.outputs.keyVaultRefs[0]
|
||||
|
||||
storageAccountKeySecretRef: kvSecretReferencesModule.outputs.keyVaultRefs[4]
|
||||
storageAccountContainerName: storageAccountModule.outputs.containerName
|
||||
storageAccountName: storageAccountModule.outputs.name
|
||||
|
||||
// Enable VNet integration
|
||||
integrationSubnetId: virtualNetworkModule.outputs.subnets.IntegrationSubnet.id
|
||||
|
||||
scmRepoUrl: scmRepoUrl
|
||||
scmRepoBranch: scmRepoBranch
|
||||
preRequsitesCommand: preRequsitesCommand
|
||||
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
|
||||
uamiId: uamiModule.outputs.id
|
||||
}
|
||||
}
|
||||
|
||||
module uamiModule 'modules/uami/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'uami'), 64)
|
||||
scope: webAppResourceGroup
|
||||
params: {
|
||||
tags: tags
|
||||
location: location
|
||||
uamiName: uamiName
|
||||
}
|
||||
}
|
||||
|
||||
// The web app URL
|
||||
output webAppUrl string = webAppModule.outputs.webAppUrl
|
|
@ -0,0 +1,47 @@
|
|||
# Manually deploy Redcap using PowerShell
|
||||
|
||||
### Prerequisites:
|
||||
|
||||
Install the following prerequisites on your local machine:
|
||||
- **[PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell?view=powershell-7.3)**
|
||||
- **[Az PowerShell module](https://learn.microsoft.com/powershell/azure/new-azureps-module-az?view=azps-10.3.0)**
|
||||
- **[Bicep tools](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/install)**
|
||||
- **[Git](https://git-scm.com/downloads)**
|
||||
- **[Visual Studio Code](https://code.visualstudio.com/download)**
|
||||
|
||||
### Deployment Steps:
|
||||
Perform the following steps to deploy the solution using PowerShell:
|
||||
|
||||
- Fork this repository and clone it to your administrative workstation or alternatively you can just clone the repository and work with it directly:
|
||||
- To clone the repository and work with it directly, run the following command:
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/kalalvishal/azure-redcap-paas.git
|
||||
```
|
||||
- Open the `azure-redcap-paas` folder in VSCode
|
||||
|
||||
- Copy `main-sample.bicepparam` to a new file with a descriptive name, such as `main-*yourorg*.bicepparam`
|
||||
|
||||
- Review and modify the parameter values in the `main-*yourorg*.bicepparam` file as needed. Here is the summary of parameters:
|
||||
- ***location***: The region where the resources will be deployed. The example of this parameter is `eastus`
|
||||
- ***environment***: The name of the enviorment for this deployed value. Allowed values are `test`, `demo`, `prod`. The example of this parameter is `test`
|
||||
- ***workloadName***: The name of the workload. The example of this parameter is `redcap`
|
||||
- ***sequenceNumber***: The sequence number of the deployment. The example of this parameter is `1`. If you are deploying the same workload multiple times, you need to increment this number for each deployment.
|
||||
- ***identityObjectId***: Valid Entra ID object ID for permissions assignment. This identity object will be assigned admin access. The example of this parameter is `00000000-0000-0000-0000-000000000000`
|
||||
- ***vnetAddressPrefix***: The address prefix for the virtual network. The example of this parameter is `192.168.1.0/24`
|
||||
- ***redcapZipUrl***: The URL to the Redcap zip file.
|
||||
- ***redcapCommunityUsername***: This is not required if redcapZipUrl is provided. Else The username for the Redcap community site.
|
||||
- ***redcapCommunityPassword***: This is not required if redcapZipUrl is provided. Else The password for the Redcap community site.
|
||||
- ***scmRepoUrl***: If you have fork the repo, provide the URL to your forked repo. Else provide the URL to the original repo.
|
||||
- ***scmRepoBranch***: The branch of the repo to deploy from. The example of this parameter is `main`
|
||||
- Execute `deploy.ps1` as shown below.
|
||||
|
||||
```PowerShell
|
||||
./deploy.ps1 -Location 'eastus' -TemplateParameterFile 'main-yourorg.bicepparam' -SubscriptionId 'subscription-id'
|
||||
```
|
||||
|
||||
- You may omit the parameter names and use them in the order `Location`, `TemplateParameterFile`, and `SubscriptionId`
|
||||
|
||||
```PowerShell
|
||||
./deploy.ps1 'eastus' 'main-yourorg.bicepparam' 'subscription-id'
|
||||
```
|
|
@ -0,0 +1,121 @@
|
|||
param location string
|
||||
|
||||
// @allowed([
|
||||
// 'eastus'
|
||||
// 'westus'
|
||||
// 'westeurope'
|
||||
// 'northeurope'
|
||||
// 'uksouth'
|
||||
// ])
|
||||
// param workspaceLocation string
|
||||
|
||||
@description('If true Host Pool, App Group and Workspace will be created. Default is to join Session Hosts to existing AVD environment')
|
||||
param newBuild bool = false
|
||||
|
||||
// @description('Expiration time for the HostPool registration token. This must be up to 30 days from todays date.')
|
||||
// param tokenExpirationTime string
|
||||
|
||||
@allowed([
|
||||
'Personal'
|
||||
'Pooled'
|
||||
])
|
||||
param hostPoolType string = 'Pooled'
|
||||
param hostPoolName string
|
||||
|
||||
// @allowed([
|
||||
// 'Automatic'
|
||||
// 'Direct'
|
||||
// ])
|
||||
// param personalDesktopAssignmentType string = 'Direct'
|
||||
param maxSessionLimit int = 5
|
||||
|
||||
@allowed([
|
||||
'BreadthFirst'
|
||||
'DepthFirst'
|
||||
'Persistent'
|
||||
])
|
||||
param loadBalancerType string = 'BreadthFirst'
|
||||
|
||||
@description('Custom RDP properties to be applied to the AVD Host Pool.')
|
||||
param customRdpProperty string
|
||||
|
||||
@description('Friendly Name of the Host Pool, this is visible via the AVD client')
|
||||
param hostPoolFriendlyName string
|
||||
|
||||
@description('Name of the AVD Workspace to used for this deployment')
|
||||
param workspaceName string = 'AVD-PROD'
|
||||
param appGroupFriendlyName string
|
||||
param tags object
|
||||
param appGroupName string
|
||||
|
||||
// @description('Log Analytics workspace ID to join AVD to.')
|
||||
// param logworkspaceID string
|
||||
// param logworkspaceSub string
|
||||
// param logworkspaceResourceGroup string
|
||||
// param logworkspaceName string
|
||||
|
||||
// @description('List of application group resource IDs to be added to Workspace. MUST add existing ones!')
|
||||
// param applicationGroupReferences string
|
||||
|
||||
// var appGroupResourceID = array(resourceId('Microsoft.DesktopVirtualization/applicationgroups/', appGroupName))
|
||||
// var applicationGroupReferencesArr = applicationGroupReferences == '' ? appGroupResourceID : concat(split(applicationGroupReferences, ','), appGroupResourceID)
|
||||
|
||||
resource hostPool 'Microsoft.DesktopVirtualization/hostPools@2022-10-14-preview' = if (newBuild) {
|
||||
name: hostPoolName
|
||||
location: location
|
||||
properties: {
|
||||
friendlyName: hostPoolFriendlyName
|
||||
hostPoolType: hostPoolType
|
||||
loadBalancerType: loadBalancerType
|
||||
customRdpProperty: customRdpProperty
|
||||
preferredAppGroupType: 'Desktop'
|
||||
maxSessionLimit: maxSessionLimit
|
||||
validationEnvironment: false
|
||||
registrationInfo: {
|
||||
expirationTime: null
|
||||
token: null
|
||||
registrationTokenOperation: 'none'
|
||||
}
|
||||
}
|
||||
tags: tags
|
||||
}
|
||||
|
||||
resource applicationGroup 'Microsoft.DesktopVirtualization/applicationGroups@2019-12-10-preview' = if (newBuild) {
|
||||
name: appGroupName
|
||||
location: location
|
||||
properties: {
|
||||
friendlyName: appGroupFriendlyName
|
||||
applicationGroupType: 'Desktop'
|
||||
description: 'Deskop Application Group created through Abri Deploy process.'
|
||||
hostPoolArmPath: resourceId('Microsoft.DesktopVirtualization/hostpools', hostPoolName)
|
||||
}
|
||||
dependsOn: [
|
||||
hostPool
|
||||
]
|
||||
}
|
||||
|
||||
resource workspace 'Microsoft.DesktopVirtualization/workspaces@2019-12-10-preview' = if (newBuild) {
|
||||
name: workspaceName
|
||||
location: location
|
||||
properties: {
|
||||
applicationGroupReferences: [ applicationGroup.id ]
|
||||
}
|
||||
}
|
||||
|
||||
// module Monitoring './Monitoring.bicep' = if (newBuild) {
|
||||
// name: 'Monitoring'
|
||||
// params: {
|
||||
// hostpoolName: hostPoolName
|
||||
// workspaceName: workspaceName
|
||||
// appgroupName: appGroupName
|
||||
// logworkspaceSub: logworkspaceSub
|
||||
// logworkspaceResourceGroup: logworkspaceResourceGroup
|
||||
// logworkspaceName: logworkspaceName
|
||||
// }
|
||||
// dependsOn: [
|
||||
// workspace
|
||||
// hostPool
|
||||
// ]
|
||||
// }
|
||||
|
||||
output appGroupName string = appGroupName
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
/*
|
||||
* This module creates a value for an App Service or Function App setting that references a Key Vault secret.
|
||||
*/
|
||||
|
||||
@description('The names of the Key Vault secrets to create references for.')
|
||||
param secretNames array
|
||||
@description('The name of the Key Vault where the secrets are stored.')
|
||||
param keyVaultName string
|
||||
|
||||
output keyVaultRefs array = [for secretName in secretNames: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=${secretName})']
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Creates a short name for the given structure and values that is no longer than the maximum specified length
|
||||
* How this is shorter than the standard naming convention
|
||||
* - Saves usually 1 character on the sequence (01 vs. 1)
|
||||
* - Saves a few characters in the location name (eastus vs. eus)
|
||||
* - Takes only the first character of the environment (prod = p, demo or dev = d, test = t)
|
||||
* - Ensures the max length does not exceed the specified value
|
||||
*/
|
||||
targetScope = 'subscription'
|
||||
|
||||
param namingConvention string
|
||||
param location string
|
||||
@allowed([
|
||||
'vnet' // Virtual Network
|
||||
'kv' // Key Vault
|
||||
'st' // Storage Account
|
||||
'cr' // Container Registry
|
||||
'pg' // PostgreSQL Flexible Server
|
||||
'ci' // Container Instance
|
||||
'mysql' // MySQL Flexible Server
|
||||
'app' // Web App
|
||||
'plan' // App Service Plan
|
||||
'appi' // Application Insights
|
||||
'uami' // User-assigned Managed Identity
|
||||
'dplscr' // Deployment Script
|
||||
'law' // Log Analytics Workspace
|
||||
])
|
||||
param resourceType string
|
||||
param environment string
|
||||
param workloadName string
|
||||
param sequence int
|
||||
|
||||
@description('If true, the name will always use short versions of placeholders. If false, it will only be shortened when needed to fit in the maxLength.')
|
||||
param requireShorten bool = false
|
||||
@description('If true, hyphens will be removed from the name. If false, they will only be removed if required by the resource type.')
|
||||
param removeSegmentSeparator bool = false
|
||||
|
||||
@allowed([
|
||||
'-'
|
||||
'_'
|
||||
])
|
||||
param segmentSeparator string = '-'
|
||||
|
||||
@description('If true, when creating a short name, vowels will be removed first from the workload name.')
|
||||
param useRemoveVowelStrategy bool = false
|
||||
|
||||
@maxValue(13)
|
||||
param addRandomChars int = 0
|
||||
@description('When using addRandomChars > 0, generated resource names will be idempotent for the same resource group, workload, resource location, environment, sequence, and resource type. If an additional discrimnator is required, provide the value here.')
|
||||
param additionalRandomInitializer string = ''
|
||||
|
||||
// Define the behavior of this module for each supported resource type
|
||||
var Defs = {
|
||||
vnet: {
|
||||
lowerCase: false
|
||||
maxLength: 64
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
plan: {
|
||||
lowerCase: false
|
||||
maxLength: 60
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
app: {
|
||||
lowerCase: false
|
||||
maxLength: 60
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
kv: {
|
||||
lowerCase: false
|
||||
maxLength: 24
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
st: {
|
||||
lowerCase: true
|
||||
maxLength: 24
|
||||
alwaysRemoveSegmentSeparator: true
|
||||
}
|
||||
cr: {
|
||||
lowerCase: false
|
||||
maxLength: 50
|
||||
alwaysRemoveSegmentSeparator: true
|
||||
}
|
||||
pg: {
|
||||
lowerCase: true
|
||||
maxLength: 63
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
ci: {
|
||||
lowerCase: true
|
||||
maxLength: 63
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
mysql: {
|
||||
lowerCase: true
|
||||
maxLength: 63
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
appi: {
|
||||
lowerCase: false
|
||||
maxLength: 260
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
uami: {
|
||||
lowerCase: false
|
||||
maxLength: 128
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
dplscr: {
|
||||
lowerCase: false
|
||||
maxLength: 63 // Guess, not documented
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
law: {
|
||||
lowerCase: false
|
||||
maxLength: 63
|
||||
alwaysRemoveSegmentSeparator: false
|
||||
}
|
||||
}
|
||||
|
||||
var shortLocations = {
|
||||
eastus: 'eus'
|
||||
eastus2: 'eus2'
|
||||
}
|
||||
|
||||
var maxLength = Defs[resourceType].maxLength
|
||||
var lowerCase = Defs[resourceType].lowerCase
|
||||
// Hyphens (default segment separator) must be removed for certain resource types (storage accounts)
|
||||
// and might be removed based on parameter input for others
|
||||
var doRemoveSegmentSeparator = (Defs[resourceType].alwaysRemoveSegmentSeparator || removeSegmentSeparator)
|
||||
|
||||
// Translate the regular location value to a shorter value
|
||||
var shortLocationValue = shortLocations[location]
|
||||
// Create a two-digit sequence string
|
||||
var sequenceFormatted = format('{0:00}', sequence)
|
||||
|
||||
// Just in case we need them
|
||||
// For idempotency, deployments of the same type, workload, environment, sequence, and resource group will yield the same resource name
|
||||
var randomChars = addRandomChars > 0 ? take(uniqueString(subscription().subscriptionId, workloadName, location, environment, string(sequence), resourceType, additionalRandomInitializer), addRandomChars) : ''
|
||||
|
||||
// Remove hyphens from the naming convention if needed
|
||||
var namingConventionSegmentSeparatorProcessed = doRemoveSegmentSeparator ? replace(namingConvention, segmentSeparator, '') : namingConvention
|
||||
|
||||
var workloadNameSegmentSeparatorProcessed = doRemoveSegmentSeparator ? replace(workloadName, segmentSeparator, '') : workloadName
|
||||
var randomizedWorkloadName = '${workloadNameSegmentSeparatorProcessed}${randomChars}'
|
||||
|
||||
// Use the naming convention to create two names: one shortened, one regular
|
||||
var regularName = replace(replace(replace(replace(replace(namingConventionSegmentSeparatorProcessed, '{env}', toLower(environment)), '{loc}', location), '{seq}', sequenceFormatted), '{workloadName}', randomizedWorkloadName), '{rtype}', resourceType)
|
||||
// The short name uses one character for the environment, a shorter location name, and the minimum number of digits for the sequence
|
||||
var shortName = replace(replace(replace(replace(replace(namingConventionSegmentSeparatorProcessed, '{env}', toLower(take(environment, 1))), '{loc}', shortLocationValue), '{seq}', string(sequence)), '{workloadName}', randomizedWorkloadName), '{rtype}', resourceType)
|
||||
|
||||
// Based on the length of the workload name, the short name could still be too long
|
||||
var mustTryVowelRemoval = length(shortName) > maxLength
|
||||
// How many vowels would need to be removed to be effective without further shortening
|
||||
var minEffectiveVowelRemovalCount = length(shortName) - maxLength
|
||||
|
||||
// If allowed, try removing vowels
|
||||
var workloadNameVowelsProcessed = mustTryVowelRemoval && useRemoveVowelStrategy ? replace(replace(replace(replace(replace(workloadNameSegmentSeparatorProcessed, 'a', ''), 'e', ''), 'i', ''), 'o', ''), 'u', '') : workloadNameSegmentSeparatorProcessed
|
||||
|
||||
var mustShortenWorkloadName = (length(randomizedWorkloadName) - length('${workloadNameVowelsProcessed}${randomChars}')) < minEffectiveVowelRemovalCount
|
||||
|
||||
// Determine how many characters must be kept from the workload name
|
||||
var workloadNameCharsToKeep = mustShortenWorkloadName ? length(workloadNameVowelsProcessed) - length(shortName) + maxLength : length(workloadName)
|
||||
|
||||
// Create a shortened workload name by removing characters from the end
|
||||
var shortWorkloadName = '${take(workloadNameVowelsProcessed, workloadNameCharsToKeep)}${randomChars}'
|
||||
|
||||
// Recreate a proposed short name for the resource
|
||||
var actualShortName = replace(replace(replace(replace(replace(namingConventionSegmentSeparatorProcessed, '{env}', toLower(take(environment, 1))), '{loc}', shortLocationValue), '{seq}', string(sequence)), '{workloadName}', shortWorkloadName), '{rtype}', resourceType)
|
||||
|
||||
// The actual name of the resource depends on whether shortening is required or the length of the regular name exceeds the maximum length allowed for the resource type
|
||||
var actualName = (requireShorten || length(regularName) > maxLength) ? actualShortName : regularName
|
||||
|
||||
var actualNameCased = lowerCase ? toLower(actualName) : actualName
|
||||
|
||||
// This take() function shouldn't actually remove any characters, just here for safety
|
||||
output shortName string = take(actualNameCased, maxLength)
|
|
@ -0,0 +1,16 @@
|
|||
param mySqlFlexServerName string
|
||||
param principalId string
|
||||
param roleDefinitionId string
|
||||
|
||||
resource server 'Microsoft.DBforMySQL/flexibleServers@2022-09-30-preview' existing = {
|
||||
name: mySqlFlexServerName
|
||||
}
|
||||
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
|
||||
name: guid(server.id, principalId, roleDefinitionId)
|
||||
scope: server
|
||||
properties: {
|
||||
roleDefinitionId: roleDefinitionId
|
||||
principalId: principalId
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
var roles = {
|
||||
// Storage Account data plane roles
|
||||
'Storage Blob Data Owner': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')
|
||||
'Storage Blob Data Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
||||
'Storage Blob Data Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')
|
||||
'Storage File Data SMB Share Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')
|
||||
|
||||
// Storage account management plane roles
|
||||
'Storage Account Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')
|
||||
|
||||
// Key Vault data plane roles
|
||||
'Key Vault Crypto User': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424')
|
||||
'Key Vault Certificates Officer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a4417e6f-fecd-4de8-b567-7b0420556985')
|
||||
'Key Vault Secrets User': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
|
||||
'Key Vault Secrets Officer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7')
|
||||
'Key Vault Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2')
|
||||
'Key Vault Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')
|
||||
'Key Vault Crypto Service Encryption User': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e147488a-f6f5-4113-8e2d-b22465e65bf6')
|
||||
'Key Vault Crypto Officer': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '14b46e9e-c2b7-41b4-b07b-48a6ebf60603')
|
||||
|
||||
// Azure Container Registry roles
|
||||
AcrPull: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
|
||||
AcrPush: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec')
|
||||
|
||||
// Generic roles
|
||||
Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
|
||||
Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
|
||||
'User Access Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')
|
||||
|
||||
// Managed Identity roles
|
||||
'Managed Identity Operator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830')
|
||||
|
||||
// Data Factory roles
|
||||
'Data Factory Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '673868aa-7521-48a0-acc6-0f60742d39f5')
|
||||
|
||||
// Virtual Machine roles
|
||||
'Virtual Machine User Login': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb879df8-f326-4884-b1cf-06f3ad86be52')
|
||||
'Virtual Machine Administrator Login': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1c0163c0-47e6-4577-8991-ea5c82e286e4')
|
||||
|
||||
// AVD roles
|
||||
'Desktop Virtualization User': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63')
|
||||
|
||||
// Azure Cache for Redis roles
|
||||
'Redis Cache Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e0f68234-74aa-48ed-b826-c38b57376e17')
|
||||
}
|
||||
|
||||
output roles object = roles
|
|
@ -0,0 +1,109 @@
|
|||
param keyVaultName string
|
||||
param location string
|
||||
param tags object
|
||||
param enabledForDeployment bool = true
|
||||
param enabledForDiskEncryption bool = false
|
||||
param enabledForTemplateDeployment bool = true
|
||||
param enableSoftDelete bool = true
|
||||
param enableRbacAuthorization bool = true
|
||||
param enablePurgeProtection bool = true
|
||||
param peSubnetId string
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
param roleAssignments array = [ {
|
||||
RoleDefinitionId: ''
|
||||
objectId: ''
|
||||
} ]
|
||||
param privateDnsZoneId string
|
||||
|
||||
@secure()
|
||||
param secrets object
|
||||
|
||||
@allowed([
|
||||
'disabled'
|
||||
'enabled'
|
||||
])
|
||||
param publicNetworkAccess string = 'disabled'
|
||||
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
|
||||
name: keyVaultName
|
||||
location: location
|
||||
tags: tags
|
||||
properties: {
|
||||
createMode: 'default'
|
||||
enabledForDeployment: enabledForDeployment
|
||||
enabledForDiskEncryption: enabledForDiskEncryption
|
||||
enabledForTemplateDeployment: enabledForTemplateDeployment
|
||||
enableSoftDelete: enableSoftDelete
|
||||
enableRbacAuthorization: enableRbacAuthorization
|
||||
enablePurgeProtection: enablePurgeProtection
|
||||
networkAcls: {
|
||||
bypass: 'AzureServices'
|
||||
defaultAction: 'Deny'
|
||||
}
|
||||
sku: {
|
||||
family: 'A'
|
||||
name: 'standard'
|
||||
}
|
||||
softDeleteRetentionInDays: 7
|
||||
tenantId: subscription().tenantId
|
||||
publicNetworkAccess: publicNetworkAccess
|
||||
}
|
||||
}
|
||||
|
||||
module keyVaultSecretsModule 'kvSecrets.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'kv-secrets'), 64)
|
||||
params: {
|
||||
keyVaultName: keyVault.name
|
||||
secrets: secrets
|
||||
}
|
||||
}
|
||||
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for roleAssignment in roleAssignments: {
|
||||
scope: keyVault
|
||||
name: guid(keyVault.id, roleAssignment.objectId, roleAssignment.RoleDefinitionId)
|
||||
properties: {
|
||||
roleDefinitionId: roleAssignment.RoleDefinitionId
|
||||
principalId: roleAssignment.objectId
|
||||
}
|
||||
}]
|
||||
|
||||
resource pekeyVault 'Microsoft.Network/privateEndpoints@2022-07-01' = {
|
||||
name: 'pe-${keyVaultName}'
|
||||
location: location
|
||||
properties: {
|
||||
subnet: {
|
||||
id: peSubnetId
|
||||
}
|
||||
privateLinkServiceConnections: [
|
||||
{
|
||||
name: 'pe-${keyVaultName}'
|
||||
properties: {
|
||||
privateLinkServiceId: keyVault.id
|
||||
groupIds: [
|
||||
'vault'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource privateDnsZoneGroupsKeyVault 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = {
|
||||
name: 'privatednszonegroup'
|
||||
parent: pekeyVault
|
||||
properties: {
|
||||
privateDnsZoneConfigs: [
|
||||
{
|
||||
name: 'pe-${keyVaultName}'
|
||||
properties: {
|
||||
privateDnsZoneId: privateDnsZoneId
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
output keyVaultName string = keyVault.name
|
||||
output id string = keyVault.id
|
|
@ -0,0 +1,15 @@
|
|||
param keyVaultName string
|
||||
@secure()
|
||||
param secrets object
|
||||
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
|
||||
name: keyVaultName
|
||||
}
|
||||
|
||||
resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = [for secret in items(secrets): {
|
||||
parent: keyVault
|
||||
name: secret.key
|
||||
properties: {
|
||||
value: secret.value
|
||||
}
|
||||
}]
|
|
@ -0,0 +1,54 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
param resourceGroupName string
|
||||
param location string
|
||||
param tags object
|
||||
param customTags object
|
||||
param keyVaultName string
|
||||
param peSubnetId string
|
||||
param roleAssignments array = [ {
|
||||
RoleDefinitionId: ''
|
||||
objectId: ''
|
||||
} ]
|
||||
@secure()
|
||||
param secrets object
|
||||
param privateDnsZoneName string
|
||||
param virtualNetworkId string
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
|
||||
name: resourceGroupName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
module keyVaultModule './kv.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'kv'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
keyVaultName: keyVaultName
|
||||
location: location
|
||||
tags: tags
|
||||
peSubnetId: peSubnetId
|
||||
privateDnsZoneId: keyVaultPrivateDnsModule.outputs.privateDnsId
|
||||
secrets: secrets
|
||||
roleAssignments: roleAssignments
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
module keyVaultPrivateDnsModule '../pdns/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'kv-dns'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
privateDnsZoneName: privateDnsZoneName
|
||||
virtualNetworkId: virtualNetworkId
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
|
||||
output keyVaultName string = keyVaultModule.outputs.keyVaultName
|
||||
output id string = keyVaultModule.outputs.id
|
|
@ -0,0 +1,19 @@
|
|||
param appInsightsName string
|
||||
param location string
|
||||
param tags object
|
||||
param logAnalyticsWorkspaceId string
|
||||
|
||||
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: appInsightsName
|
||||
location: location
|
||||
tags: tags
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
Flow_Type: 'Bluefield'
|
||||
WorkspaceResourceId: logAnalyticsWorkspaceId
|
||||
}
|
||||
}
|
||||
|
||||
output appInsightsResourceId string = appInsights.id
|
||||
output appInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey
|
|
@ -0,0 +1,24 @@
|
|||
param logAnalyticsWorkspaceName string
|
||||
param location string
|
||||
param tags object
|
||||
param retentionInDays int = 30
|
||||
|
||||
@allowed([
|
||||
'PerGB2018'
|
||||
])
|
||||
param logAnalyticsWorkspaceSku string = 'PerGB2018'
|
||||
|
||||
resource logAnalyticsWorkSpace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
|
||||
name: logAnalyticsWorkspaceName
|
||||
location: location
|
||||
tags: tags
|
||||
properties: {
|
||||
retentionInDays: retentionInDays
|
||||
sku: {
|
||||
name: logAnalyticsWorkspaceSku
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output logAnalyticsWorkspaceId string = logAnalyticsWorkSpace.id
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
targetScope = 'subscription'
|
||||
param resourceGroupName string
|
||||
param location string
|
||||
param tags object
|
||||
param customTags object
|
||||
param logAnalyticsWorkspaceName string
|
||||
param logAnalyticsWorkspaceSku string
|
||||
param retentionInDays int
|
||||
param appInsightsName string
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
|
||||
name: resourceGroupName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
module logAnalyticsWorkspace 'law.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'log'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
|
||||
logAnalyticsWorkspaceSku: logAnalyticsWorkspaceSku
|
||||
retentionInDays: retentionInDays
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
}
|
||||
|
||||
module appInsights 'appInsights.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'appi'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
appInsightsName: appInsightsName
|
||||
logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
}
|
||||
|
||||
output appInsightsResourceId string = appInsights.outputs.appInsightsResourceId
|
||||
output appInsightsInstrumentationKey string = appInsights.outputs.appInsightsInstrumentationKey
|
||||
output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
|
|
@ -0,0 +1,36 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
param resourceGroupName string
|
||||
param location string
|
||||
param virtualNetworkName string
|
||||
param vnetAddressPrefix string
|
||||
param subnets object
|
||||
param customDnsIPs array
|
||||
param tags object
|
||||
param customTags object
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
|
||||
name: resourceGroupName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
module vNetModule 'vnet.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'vnet'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
virtualNetworkName: virtualNetworkName
|
||||
vnetAddressPrefix: vnetAddressPrefix
|
||||
location: location
|
||||
subnets: subnets
|
||||
tags: mergeTags
|
||||
customDnsIPs: customDnsIPs
|
||||
}
|
||||
}
|
||||
|
||||
output virtualNetworkId string = vNetModule.outputs.virtualNetworkId
|
||||
output subnets object = reduce(vNetModule.outputs.subnets, {}, (cur, next) => union(cur, next))
|
|
@ -0,0 +1,71 @@
|
|||
targetScope = 'resourceGroup'
|
||||
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('virtualNetworkName')
|
||||
param virtualNetworkName string
|
||||
|
||||
@description('vnetAddressSpace')
|
||||
param vnetAddressPrefix string
|
||||
|
||||
@description('subnetsDetails')
|
||||
param subnets object
|
||||
param tags object
|
||||
|
||||
param customDnsIPs array
|
||||
|
||||
var subnetDefsArray = items(subnets)
|
||||
|
||||
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-05-01' = {
|
||||
name: virtualNetworkName
|
||||
location: location
|
||||
properties: {
|
||||
addressSpace: {
|
||||
addressPrefixes: [
|
||||
vnetAddressPrefix
|
||||
]
|
||||
}
|
||||
subnets: [for (subnet, i) in subnetDefsArray: {
|
||||
name: subnet.key
|
||||
properties: {
|
||||
addressPrefix: subnet.value.addressPrefix
|
||||
serviceEndpoints: contains(subnet.value, 'serviceEndpoints') ? subnet.value.serviceEndpoints : null
|
||||
delegations: contains(subnet.value, 'delegation') && !empty(subnet.value.delegation) ? [
|
||||
{
|
||||
name: 'delegation'
|
||||
properties: {
|
||||
serviceName: subnet.value.delegation
|
||||
}
|
||||
}
|
||||
] : null
|
||||
}
|
||||
}]
|
||||
|
||||
dhcpOptions: {
|
||||
dnsServers: customDnsIPs
|
||||
}
|
||||
}
|
||||
tags: tags
|
||||
}
|
||||
|
||||
output virtualNetworkId string = virtualNetwork.id
|
||||
|
||||
// Retrieve the subnets as an array of existing resources
|
||||
// This is important because we need to ensure subnet return value is matched to the name of the subnet correctly - order matters
|
||||
// This works because the parent property is set to the virtual network, which means this won't be attempted until the VNet is created
|
||||
resource subnetRes 'Microsoft.Network/virtualNetworks/subnets@2022-05-01' existing = [for subnet in subnetDefsArray: {
|
||||
name: subnet.key
|
||||
parent: virtualNetwork
|
||||
}]
|
||||
|
||||
output subnets array = [for i in range(0, length((subnetDefsArray))): {
|
||||
'${subnetRes[i].name}': {
|
||||
id: subnetRes[i].id
|
||||
addressPrefix: subnetRes[i].properties.addressPrefix
|
||||
// routeTableId: contains(subnetRes[i].properties, 'routeTable') ? subnetRes[i].properties.routeTable.id : null
|
||||
// routeTableName: contains(subnetRes[i].properties, 'routeTable') ? routeTables[subnetRes[i].name].name : null
|
||||
// networkSecurityGroupId: contains(subnetRes[i].properties, 'networkSecurityGroup') ? subnetRes[i].properties.networkSecurityGroup.id : null
|
||||
// networkSecurityGroupName: contains(subnetRes[i].properties, 'networkSecurityGroup') ? networkSecurityGroups[subnetRes[i].name].name : null
|
||||
// Add as many additional subnet properties as needed downstream
|
||||
}
|
||||
}]
|
|
@ -0,0 +1,32 @@
|
|||
@description('privateDnsZone Name')
|
||||
param privateDnsZoneName string
|
||||
|
||||
@description('virtualNetworkId')
|
||||
param virtualNetworkId string
|
||||
|
||||
param tags object
|
||||
|
||||
var mergeTags = union(tags, {
|
||||
workloadType: 'privateDns'
|
||||
})
|
||||
|
||||
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
|
||||
name: privateDnsZoneName
|
||||
location: 'global'
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
resource privateDnsvnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
|
||||
name: 'vnetlink'
|
||||
location: 'global'
|
||||
parent: privateDnsZone
|
||||
tags: mergeTags
|
||||
properties: {
|
||||
registrationEnabled: false
|
||||
virtualNetwork: {
|
||||
id: virtualNetworkId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output privateDnsId string = privateDnsZone.id
|
|
@ -0,0 +1,101 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
param resourceGroupName string
|
||||
param location string
|
||||
param tags object
|
||||
param customTags object
|
||||
param flexibleSqlServerName string
|
||||
param peSubnetId string
|
||||
param privateDnsZoneName string
|
||||
param sqlAdminUser string
|
||||
param virtualNetworkId string
|
||||
|
||||
param roles object
|
||||
param deploymentScriptName string
|
||||
|
||||
@description('MySQL version')
|
||||
@allowed([
|
||||
'5.7'
|
||||
'8.0.21'
|
||||
//'8.0.32'
|
||||
])
|
||||
param mysqlVersion string = '8.0.21'
|
||||
|
||||
@secure()
|
||||
param sqlAdminPasword string
|
||||
|
||||
@description('Azure database for MySQL sku name ')
|
||||
param skuName string = 'Standard_B1s'
|
||||
|
||||
@description('Azure database for MySQL pricing tier')
|
||||
@allowed([
|
||||
'GeneralPurpose'
|
||||
'MemoryOptimized'
|
||||
'Burstable'
|
||||
])
|
||||
param SkuTier string
|
||||
|
||||
@description('Azure database for MySQL storage Size ')
|
||||
param StorageSizeGB int = 20
|
||||
|
||||
@description('Azure database for MySQL storage Iops')
|
||||
param StorageIops int = 360
|
||||
|
||||
param databaseName string
|
||||
param database_charset string = 'utf8'
|
||||
param database_collation string = 'utf8_general_ci'
|
||||
|
||||
param uamiId string
|
||||
param uamiPrincipalId string
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
|
||||
name: resourceGroupName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
module mysqlDbserver './sql.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'mysql'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
flexibleSqlServerName: flexibleSqlServerName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
skuName: skuName
|
||||
SkuTier: SkuTier
|
||||
StorageSizeGB: StorageSizeGB
|
||||
StorageIops: StorageIops
|
||||
peSubnetId: peSubnetId
|
||||
privateDnsZoneId: privateDns.outputs.privateDnsId
|
||||
adminUserName: sqlAdminUser
|
||||
adminPassword: sqlAdminPasword
|
||||
mysqlVersion: mysqlVersion
|
||||
databaseName: databaseName
|
||||
database_charset: database_charset
|
||||
database_collation: database_collation
|
||||
|
||||
roles: roles
|
||||
uamiId: uamiId
|
||||
uamiPrincipalId: uamiPrincipalId
|
||||
deploymentScriptName: deploymentScriptName
|
||||
}
|
||||
}
|
||||
|
||||
module privateDns '../pdns/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'mysql-dns'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
privateDnsZoneName: privateDnsZoneName
|
||||
virtualNetworkId: virtualNetworkId
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
|
||||
output mySqlServerName string = mysqlDbserver.outputs.mySqlServerName
|
||||
output databaseName string = mysqlDbserver.outputs.databaseName
|
||||
output sqlAdmin string = mysqlDbserver.outputs.sqlAdmin
|
||||
output fqdn string = mysqlDbserver.outputs.fqdn
|
|
@ -0,0 +1,150 @@
|
|||
@description('Server Name for Azure database for MySQL')
|
||||
param flexibleSqlServerName string
|
||||
|
||||
param location string
|
||||
param tags object
|
||||
|
||||
// TODO: skuName and SkuTier are related; should be specified as a single object param, IMHO
|
||||
@description('Azure database for MySQL sku name ')
|
||||
param skuName string = 'Standard_B1s'
|
||||
|
||||
@description('Azure database for MySQL pricing tier')
|
||||
@allowed([
|
||||
'GeneralPurpose'
|
||||
'MemoryOptimized'
|
||||
'Burstable'
|
||||
])
|
||||
param SkuTier string = 'Burstable'
|
||||
|
||||
@description('Azure database for MySQL storage Size ')
|
||||
param StorageSizeGB int = 20
|
||||
|
||||
@description('Azure database for MySQL storage Iops')
|
||||
param StorageIops int = 360
|
||||
|
||||
param peSubnetId string
|
||||
param privateDnsZoneId string
|
||||
|
||||
param adminUserName string
|
||||
|
||||
param roles object
|
||||
param uamiId string
|
||||
param uamiPrincipalId string
|
||||
param deploymentScriptName string
|
||||
|
||||
@description('Database administrator password')
|
||||
@minLength(8)
|
||||
@secure()
|
||||
param adminPassword string
|
||||
|
||||
@description('MySQL version')
|
||||
@allowed([
|
||||
'5.7'
|
||||
'8.0.21'
|
||||
])
|
||||
param mysqlVersion string = '8.0.21'
|
||||
|
||||
@allowed([
|
||||
'Enabled'
|
||||
'Disabled'
|
||||
])
|
||||
@description('Whether or not geo redundant backup is enabled.')
|
||||
param geoRedundantBackup string = 'Disabled'
|
||||
|
||||
param backupRetentionDays int = 7
|
||||
|
||||
@allowed([
|
||||
'Enabled'
|
||||
'Disabled'
|
||||
])
|
||||
param highAvailability string = 'Disabled'
|
||||
|
||||
@allowed([
|
||||
'Enabled'
|
||||
'Disabled'
|
||||
])
|
||||
param publicNetworkAccess string = 'Disabled'
|
||||
|
||||
param databaseName string
|
||||
param database_charset string = 'utf8'
|
||||
param database_collation string = 'utf8_general_ci'
|
||||
|
||||
param currentTime string = utcNow()
|
||||
|
||||
resource server 'Microsoft.DBforMySQL/flexibleServers@2022-09-30-preview' = {
|
||||
name: flexibleSqlServerName
|
||||
location: location
|
||||
tags: tags
|
||||
sku: {
|
||||
name: skuName
|
||||
tier: SkuTier
|
||||
}
|
||||
properties: {
|
||||
administratorLogin: adminUserName
|
||||
administratorLoginPassword: adminPassword
|
||||
version: mysqlVersion
|
||||
replicationRole: 'None'
|
||||
createMode: 'Default'
|
||||
backup: {
|
||||
backupRetentionDays: backupRetentionDays
|
||||
geoRedundantBackup: geoRedundantBackup
|
||||
}
|
||||
highAvailability: {
|
||||
mode: highAvailability
|
||||
}
|
||||
network: {
|
||||
delegatedSubnetResourceId: peSubnetId
|
||||
privateDnsZoneResourceId: privateDnsZoneId
|
||||
publicNetworkAccess: publicNetworkAccess
|
||||
}
|
||||
storage: {
|
||||
autoGrow: 'Enabled'
|
||||
iops: StorageIops
|
||||
storageSizeGB: StorageSizeGB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource database 'Microsoft.DBforMySQL/flexibleServers/databases@2021-12-01-preview' = {
|
||||
parent: server
|
||||
name: databaseName
|
||||
properties: {
|
||||
charset: database_charset
|
||||
collation: database_collation
|
||||
}
|
||||
}
|
||||
|
||||
module uamiMySqlRoleAssignmentModule '../common/roleAssignment-mySql.bicep' = {
|
||||
name: 'mySqlRole'
|
||||
params: {
|
||||
mySqlFlexServerName: server.name
|
||||
principalId: uamiPrincipalId
|
||||
roleDefinitionId: roles.Contributor
|
||||
}
|
||||
}
|
||||
|
||||
resource dbConfigDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
|
||||
name: deploymentScriptName
|
||||
location: location
|
||||
identity: {
|
||||
type: 'UserAssigned'
|
||||
userAssignedIdentities: {
|
||||
'${uamiId}': {}
|
||||
}
|
||||
}
|
||||
kind: 'AzureCLI'
|
||||
properties: {
|
||||
azCliVersion: '2.50.0'
|
||||
retentionInterval: 'P1D'
|
||||
cleanupPreference: 'OnSuccess'
|
||||
forceUpdateTag: currentTime
|
||||
scriptContent: 'az mysql flexible-server parameter set -g ${resourceGroup().name} --server-name ${server.name} --name sql_generate_invisible_primary_key --value OFF'
|
||||
}
|
||||
tags: tags
|
||||
dependsOn: [ uamiMySqlRoleAssignmentModule ]
|
||||
}
|
||||
|
||||
output mySqlServerName string = server.name
|
||||
output databaseName string = database.name
|
||||
output sqlAdmin string = server.properties.administratorLogin
|
||||
output fqdn string = server.properties.fullyQualifiedDomainName
|
|
@ -0,0 +1,63 @@
|
|||
targetScope = 'subscription'
|
||||
|
||||
param resourceGroupName string
|
||||
param location string
|
||||
param storageAccountName string
|
||||
param storageContainerName string
|
||||
param kind string
|
||||
param storageAccountSku string
|
||||
param privateDnsZoneName string
|
||||
param peSubnetId string
|
||||
param virtualNetworkId string
|
||||
param tags object
|
||||
param customTags object
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
@description('Resource ID of the Key Vault where the storage key secret should be created.')
|
||||
param keyVaultId string
|
||||
@description('Name of the secret in Key Vault.')
|
||||
param keyVaultSecretName string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
|
||||
name: resourceGroupName
|
||||
location: location
|
||||
tags: mergeTags
|
||||
}
|
||||
|
||||
module storageAccount './storage.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'st'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
location: location
|
||||
tags: mergeTags
|
||||
storageAccountName: storageAccountName
|
||||
peSubnetId: peSubnetId
|
||||
storageContainerName: storageContainerName
|
||||
kind: kind
|
||||
storageAccountSku: storageAccountSku
|
||||
privateDnsZoneId: privateDns.outputs.privateDnsId
|
||||
keyVaultId: keyVaultId
|
||||
keyVaultSecretName: keyVaultSecretName
|
||||
deploymentNameStructure: deploymentNameStructure
|
||||
}
|
||||
}
|
||||
|
||||
module privateDns '../pdns/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'st-dns'), 64)
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
privateDnsZoneName: privateDnsZoneName
|
||||
virtualNetworkId: virtualNetworkId
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add lock to storage account to avoid accidental deletion
|
||||
|
||||
output id string = storageAccount.outputs.id
|
||||
output name string = storageAccount.outputs.name
|
||||
output resourceGroupName string = resourceGroup.name
|
||||
output containerName string = storageAccount.outputs.containerName
|
|
@ -0,0 +1,120 @@
|
|||
param location string = resourceGroup().location
|
||||
param storageContainerName string = ''
|
||||
param peSubnetId string
|
||||
param storageAccountName string
|
||||
|
||||
@description('privateDnsZone Details')
|
||||
param privateDnsZoneId string
|
||||
|
||||
@description('storageAccountSku')
|
||||
param storageAccountSku string
|
||||
|
||||
@description('Resource ID of the Key Vault where the storage key secret should be created.')
|
||||
param keyVaultId string
|
||||
@description('Name of the secret in Key Vault.')
|
||||
param keyVaultSecretName string
|
||||
|
||||
param tags object
|
||||
param deploymentNameStructure string
|
||||
param kind string
|
||||
|
||||
var storageType = kind == 'FileStorage' ? 'file' : 'blob'
|
||||
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
|
||||
name: storageAccountName
|
||||
location: location
|
||||
sku: {
|
||||
name: storageAccountSku
|
||||
}
|
||||
tags: tags
|
||||
kind: kind
|
||||
properties: {
|
||||
allowBlobPublicAccess: false
|
||||
publicNetworkAccess: 'Disabled'
|
||||
}
|
||||
}
|
||||
|
||||
resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2021-09-01' = if (kind == 'StorageV2') {
|
||||
name: 'default'
|
||||
parent: storageAccount
|
||||
}
|
||||
|
||||
resource storageContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-09-01' = if (kind == 'StorageV2') {
|
||||
name: storageContainerName
|
||||
parent: blobServices
|
||||
}
|
||||
|
||||
resource fileServices 'Microsoft.Storage/storageAccounts/fileServices@2022-09-01' = if (kind == 'FileStorage') {
|
||||
name: 'default'
|
||||
parent: storageAccount
|
||||
}
|
||||
|
||||
resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = if (kind == 'FileStorage') {
|
||||
name: storageContainerName
|
||||
parent: fileServices
|
||||
properties: {
|
||||
accessTier: 'Premium'
|
||||
shareQuota: 100
|
||||
}
|
||||
}
|
||||
|
||||
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2022-07-01' = {
|
||||
name: 'pe-${storageAccountName}'
|
||||
location: location
|
||||
properties: {
|
||||
subnet: {
|
||||
id: peSubnetId
|
||||
}
|
||||
privateLinkServiceConnections: [
|
||||
{
|
||||
name: 'pe-${storageAccountName}'
|
||||
properties: {
|
||||
privateLinkServiceId: storageAccount.id
|
||||
groupIds: [
|
||||
storageType
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource privateDnsZoneGroupsStorage 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = {
|
||||
name: 'default'
|
||||
parent: privateEndpoint
|
||||
properties: {
|
||||
privateDnsZoneConfigs: [
|
||||
{
|
||||
name: 'pe-${storageAccountName}'
|
||||
properties: {
|
||||
privateDnsZoneId: privateDnsZoneId
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Create a secret with the storage account's primary key in the specified Key Vault
|
||||
var keyVaultIdSplit = split(keyVaultId, '/')
|
||||
var keyVaultResourceGroupName = keyVaultIdSplit[4]
|
||||
var keyVaultName = keyVaultIdSplit[8]
|
||||
|
||||
resource keyVaultResourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' existing = {
|
||||
name: keyVaultResourceGroupName
|
||||
scope: subscription()
|
||||
}
|
||||
|
||||
module keyVaultSecretsModule '../kv/kvSecrets.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'kv-st-secret'), 64)
|
||||
scope: keyVaultResourceGroup
|
||||
params: {
|
||||
keyVaultName: keyVaultName
|
||||
secrets: {
|
||||
'${keyVaultSecretName}': storageAccount.listKeys().keys[0].value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output name string = storageAccount.name
|
||||
output id string = storageAccount.id
|
||||
output containerName string = storageContainer.name
|
|
@ -0,0 +1,12 @@
|
|||
param uamiName string
|
||||
param location string = resourceGroup().location
|
||||
param tags object
|
||||
|
||||
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
|
||||
name: uamiName
|
||||
location: location
|
||||
tags: tags
|
||||
}
|
||||
|
||||
output id string = managedIdentity.id
|
||||
output principalId string = managedIdentity.properties.principalId
|
|
@ -0,0 +1,93 @@
|
|||
param location string = resourceGroup().location
|
||||
|
||||
param webAppName string
|
||||
param appServicePlanName string
|
||||
param skuName string
|
||||
param skuTier string
|
||||
param linuxFxVersion string = 'php|8.2'
|
||||
param dbHostName string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param dbUserNameSecretRef string
|
||||
param tags object
|
||||
param customTags object
|
||||
param dbName string
|
||||
param peSubnetId string
|
||||
param privateDnsZoneName string
|
||||
param virtualNetworkId string
|
||||
param integrationSubnetId string
|
||||
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param storageAccountKeySecretRef string
|
||||
param storageAccountName string
|
||||
param storageAccountContainerName string
|
||||
|
||||
param appInsights_connectionString string
|
||||
param appInsights_instrumentationKey string
|
||||
|
||||
param scmRepoUrl string
|
||||
param scmRepoBranch string
|
||||
@secure()
|
||||
param redcapZipUrl string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param redcapCommunityUsernameSecretRef string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param redcapCommunityPasswordSecretRef string
|
||||
param preRequsitesCommand string
|
||||
|
||||
param uamiId string
|
||||
|
||||
// Disabling this check because this is no longer a secret; it's a reference to Key Vault
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param dbPasswordSecretRef string
|
||||
|
||||
param deploymentNameStructure string
|
||||
|
||||
var mergeTags = union(tags, customTags)
|
||||
|
||||
module appService 'webapp.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'planAndApp'), 64)
|
||||
params: {
|
||||
webAppName: webAppName
|
||||
appServicePlanName: appServicePlanName
|
||||
location: location
|
||||
skuName: skuName
|
||||
skuTier: skuTier
|
||||
linuxFxVersion: linuxFxVersion
|
||||
tags: mergeTags
|
||||
dbHostName: dbHostName
|
||||
dbName: dbName
|
||||
dbPasswordSecretRef: dbPasswordSecretRef
|
||||
dbUserNameSecretRef: dbUserNameSecretRef
|
||||
peSubnetId: peSubnetId
|
||||
privateDnsZoneId: privateDns.outputs.privateDnsId
|
||||
integrationSubnetId: integrationSubnetId
|
||||
|
||||
appInsights_connectionString: appInsights_connectionString
|
||||
appInsights_instrumentationKey: appInsights_instrumentationKey
|
||||
|
||||
redcapZipUrl: redcapZipUrl
|
||||
redcapCommunityUsernameSecretRef: redcapCommunityUsernameSecretRef
|
||||
redcapCommunityPasswordSecretRef: redcapCommunityPasswordSecretRef
|
||||
|
||||
scmRepoUrl: scmRepoUrl
|
||||
scmRepoBranch: scmRepoBranch
|
||||
preRequsitesCommand: preRequsitesCommand
|
||||
|
||||
storageAccountContainerName: storageAccountContainerName
|
||||
storageAccountKeySecretRef: storageAccountKeySecretRef
|
||||
storageAccountName: storageAccountName
|
||||
|
||||
uamiId: uamiId
|
||||
}
|
||||
}
|
||||
|
||||
module privateDns '../pdns/main.bicep' = {
|
||||
name: take(replace(deploymentNameStructure, '{rtype}', 'app-dns'), 64)
|
||||
params: {
|
||||
privateDnsZoneName: privateDnsZoneName
|
||||
virtualNetworkId: virtualNetworkId
|
||||
tags: mergeTags
|
||||
}
|
||||
}
|
||||
|
||||
output webAppUrl string = appService.outputs.webAppUrl
|
|
@ -0,0 +1,198 @@
|
|||
param webAppName string
|
||||
param appServicePlanName string
|
||||
param location string
|
||||
param skuName string
|
||||
param skuTier string
|
||||
param tags object
|
||||
param linuxFxVersion string
|
||||
|
||||
param dbHostName string
|
||||
param dbName string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param dbUserNameSecretRef string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param dbPasswordSecretRef string
|
||||
|
||||
param peSubnetId string
|
||||
param privateDnsZoneId string
|
||||
param integrationSubnetId string
|
||||
@secure()
|
||||
param redcapZipUrl string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param redcapCommunityUsernameSecretRef string
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param redcapCommunityPasswordSecretRef string
|
||||
param scmRepoUrl string
|
||||
param scmRepoBranch string = 'main'
|
||||
param preRequsitesCommand string
|
||||
|
||||
param appInsights_connectionString string
|
||||
param appInsights_instrumentationKey string
|
||||
|
||||
#disable-next-line secure-secrets-in-params
|
||||
param storageAccountKeySecretRef string
|
||||
param storageAccountName string
|
||||
param storageAccountContainerName string
|
||||
|
||||
param uamiId string
|
||||
|
||||
resource appSrvcPlan 'Microsoft.Web/serverfarms@2022-03-01' = {
|
||||
name: appServicePlanName
|
||||
location: location
|
||||
tags: tags
|
||||
sku: {
|
||||
name: skuName
|
||||
tier: skuTier
|
||||
}
|
||||
kind: 'linux'
|
||||
properties: {
|
||||
reserved: true
|
||||
}
|
||||
}
|
||||
|
||||
var DBSslCa = '/home/site/wwwroot/DigiCertGlobalRootCA.crt.pem'
|
||||
|
||||
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
|
||||
name: webAppName
|
||||
location: location
|
||||
tags: tags
|
||||
properties: {
|
||||
httpsOnly: true
|
||||
serverFarmId: appSrvcPlan.id
|
||||
virtualNetworkSubnetId: integrationSubnetId
|
||||
keyVaultReferenceIdentity: uamiId
|
||||
siteConfig: {
|
||||
alwaysOn: true
|
||||
http20Enabled: true
|
||||
|
||||
linuxFxVersion: linuxFxVersion
|
||||
minTlsVersion: '1.2'
|
||||
ftpsState: 'FtpsOnly'
|
||||
appCommandLine: preRequsitesCommand
|
||||
appSettings: [
|
||||
{
|
||||
name: 'redcapAppZip'
|
||||
value: redcapZipUrl
|
||||
}
|
||||
{
|
||||
name: 'DBHostName'
|
||||
value: dbHostName
|
||||
}
|
||||
{
|
||||
name: 'DBName'
|
||||
value: dbName
|
||||
}
|
||||
{
|
||||
name: 'DBUserName'
|
||||
value: dbUserNameSecretRef
|
||||
}
|
||||
{
|
||||
name: 'DBPassword'
|
||||
value: dbPasswordSecretRef
|
||||
}
|
||||
{
|
||||
name: 'redcapCommunityUsername'
|
||||
value: redcapCommunityUsernameSecretRef
|
||||
}
|
||||
{
|
||||
name: 'redcapCommunityPassword'
|
||||
value: redcapCommunityPasswordSecretRef
|
||||
}
|
||||
{
|
||||
name: 'DBSslCa'
|
||||
value: DBSslCa
|
||||
}
|
||||
{
|
||||
name: 'smtpFQDN'
|
||||
value: ''
|
||||
}
|
||||
{
|
||||
name: 'smtpPort'
|
||||
value: ''
|
||||
}
|
||||
{
|
||||
name: 'fromEmailAddress'
|
||||
value: ''
|
||||
}
|
||||
{
|
||||
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
|
||||
value: appInsights_instrumentationKey
|
||||
}
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: appInsights_connectionString
|
||||
}
|
||||
{
|
||||
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
|
||||
value: '1'
|
||||
}
|
||||
{
|
||||
name: 'storageKey'
|
||||
value: storageAccountKeySecretRef
|
||||
}
|
||||
{
|
||||
name: 'storageAccount'
|
||||
value: storageAccountName
|
||||
}
|
||||
{
|
||||
name: 'storageContainerName'
|
||||
value: storageAccountContainerName
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
identity: {
|
||||
type: 'UserAssigned'
|
||||
userAssignedIdentities: {
|
||||
'${uamiId}': {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource webSiteName_web 'Microsoft.Web/sites/sourcecontrols@2022-09-01' = {
|
||||
parent: webApp
|
||||
name: 'web'
|
||||
properties: {
|
||||
repoUrl: scmRepoUrl
|
||||
branch: scmRepoBranch
|
||||
isManualIntegration: true
|
||||
}
|
||||
}
|
||||
|
||||
resource peWebApp 'Microsoft.Network/privateEndpoints@2022-07-01' = {
|
||||
name: 'pe-${webApp.name}'
|
||||
location: location
|
||||
properties: {
|
||||
subnet: {
|
||||
id: peSubnetId
|
||||
}
|
||||
privateLinkServiceConnections: [
|
||||
{
|
||||
name: 'pe-${webApp.name}'
|
||||
properties: {
|
||||
privateLinkServiceId: webApp.id
|
||||
groupIds: [
|
||||
'sites'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource privateDnsZoneGroupsWebApp 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = {
|
||||
name: 'privatednszonegroup'
|
||||
parent: peWebApp
|
||||
properties: {
|
||||
privateDnsZoneConfigs: [
|
||||
{
|
||||
name: 'pe-${webAppName}'
|
||||
properties: {
|
||||
privateDnsZoneId: privateDnsZoneId
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
output webAppUrl string = webApp.properties.defaultHostName
|
|
@ -0,0 +1,23 @@
|
|||
function New-RandomPassword {
|
||||
param (
|
||||
[Parameter(Mandatory, Position = 1)]
|
||||
[int]$Length
|
||||
)
|
||||
|
||||
$charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.ToCharArray()
|
||||
|
||||
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
|
||||
$bytes = New-Object byte[]($length)
|
||||
|
||||
$rng.GetBytes($bytes)
|
||||
|
||||
$result = New-Object char[]($length)
|
||||
|
||||
for ($i = 0 ; $i -lt $length ; $i++) {
|
||||
$result[$i] = $charSet[$bytes[$i] % $charSet.Length]
|
||||
}
|
||||
|
||||
return ConvertTo-SecureString (-Join $result) -AsPlainText -Force
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function New-RandomPassword
|
|
@ -10,7 +10,6 @@
|
|||
# Timestamp for log file
|
||||
#
|
||||
####################################################################################
|
||||
|
||||
stamp=$(date +%Y-%m-%d-%H-%M)
|
||||
|
||||
####################################################################################
|
||||
|
@ -36,12 +35,12 @@ cd /tmp
|
|||
if [ -z "$APPSETTING_redcapAppZip" ]; then
|
||||
echo "Downloading REDCap zip file from REDCap Community site" >> /home/site/log-$stamp.txt
|
||||
|
||||
if [ -z "$APPSETTING_zipUsername" ]; then
|
||||
if [ -z "$APPSETTING_redcapCommunityUsername" ]; then
|
||||
echo "Missing REDCap Community site username." >> /home/site/log-$stamp.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$APPSETTING_zipPassword" ]; then
|
||||
if [ -z "$APPSETTING_redcapCommunityPassword" ]; then
|
||||
echo "Missing REDCap Community site password." >> /home/site/log-$stamp.txt
|
||||
exit 1
|
||||
fi
|
||||
|
@ -51,7 +50,7 @@ if [ -z "$APPSETTING_redcapAppZip" ]; then
|
|||
export APPSETTING_zipVersion="latest"
|
||||
fi
|
||||
|
||||
wget --method=post -O redcap.zip -q --body-data="username=$APPSETTING_zipUsername&password=$APPSETTING_zipPassword&version=$APPSETTING_zipVersion&install=1" --header=Content-Type:application/x-www-form-urlencoded https://redcap.vanderbilt.edu/plugins/redcap_consortium/versions.php
|
||||
wget --method=post -O /tmp/redcap.zip -q --body-data="username=$APPSETTING_redcapCommunityUsername&password=$APPSETTING_redcapCommunityPassword&version=$APPSETTING_zipVersion&install=1" --header=Content-Type:application/x-www-form-urlencoded https://redcap.vanderbilt.edu/plugins/redcap_consortium/versions.php
|
||||
|
||||
# check to see if the redcap.zip file contains the word error
|
||||
if [ -z "$(grep -i error redcap.zip)" ]; then
|
||||
|
@ -63,13 +62,14 @@ if [ -z "$APPSETTING_redcapAppZip" ]; then
|
|||
|
||||
else
|
||||
echo "Downloading REDCap zip file from storage" >> /home/site/log-$stamp.txt
|
||||
wget -q -O redcap.zip $APPSETTING_redcapAppZip
|
||||
wget -q -O /tmp/redcap.zip $APPSETTING_redcapAppZip
|
||||
fi
|
||||
|
||||
rm /home/site/wwwroot/hostingstart.html
|
||||
unzip -oq redcap.zip -d /home/site/wwwroot
|
||||
mv /home/site/wwwroot/redcap/* /home/site/wwwroot/
|
||||
rm -Rf /home/site/wwwroot/redcap
|
||||
rm -f /home/site/wwwroot/hostingstart.html
|
||||
unzip -oq /tmp/redcap.zip -d /tmp/wwwroot
|
||||
mv /tmp/wwwroot/redcap/* /home/site/wwwroot/
|
||||
rm -rf /tmp/wwwroot
|
||||
rm /tmp/redcap.zip
|
||||
|
||||
####################################################################################
|
||||
#
|
||||
|
@ -83,10 +83,10 @@ cd /home/site/wwwroot
|
|||
|
||||
wget --no-check-certificate https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem
|
||||
|
||||
sed -i "s/'your_mysql_host_name'/'$APPSETTING_DBHostName'/" database.php
|
||||
sed -i "s/'your_mysql_db_name'/'$APPSETTING_DBName'/" database.php
|
||||
sed -i "s/'your_mysql_db_username'/'$APPSETTING_DBUserName'/" database.php
|
||||
sed -i "s/'your_mysql_db_password'/'$APPSETTING_DBPassword'/" database.php
|
||||
sed -i "s|hostname[[:space:]]*= '';|hostname = '$APPSETTING_DBHostName';|" database.php
|
||||
sed -i "s|db[[:space:]]*= '';|db = '$APPSETTING_DBName';|" database.php
|
||||
sed -i "s|username[[:space:]]*= '';|username = '$APPSETTING_DBUserName';|" database.php
|
||||
sed -i "s|password[[:space:]]*= '';|password = '$APPSETTING_DBPassword';|" database.php
|
||||
sed -i "s|db_ssl_ca[[:space:]]*= '';|db_ssl_ca = '$APPSETTING_DBSslCa';|" database.php
|
||||
|
||||
sed -i "s/db_ssl_verify_server_cert = false;/db_ssl_verify_server_cert = true;/" database.php
|
||||
|
@ -99,10 +99,10 @@ sed -i "s/$salt = '';/$salt = '$(echo $RANDOM | md5sum | head -c 20; echo;)';/"
|
|||
####################################################################################
|
||||
|
||||
echo "Configuring REDCap recommended settings" >> /home/site/log-$stamp.txt
|
||||
sed -i "s/replace_smtp_server_name/$APPSETTING_smtpFQDN/" /home/site/repository/Files/settings.ini
|
||||
sed -i "s/replace_smtp_port/$APPSETTING_smtpPort/" /home/site/repository/Files/settings.ini
|
||||
sed -i "s/replace_sendmail_from/$APPSETTING_fromEmailAddress/" /home/site/repository/Files/settings.ini
|
||||
sed -i "s:replace_sendmail_path:/usr/sbin/sendmail -t -i:" /home/site/repository/Files/settings.ini
|
||||
sed -i "s|SMTP[[:space:]]*= ''|SMTP = '$APPSETTING_smtpFQDN'|" /home/site/repository/Files/settings.ini
|
||||
sed -i "s|smtp_port[[:space:]]*= |smtp_port = $APPSETTING_smtpPort|" /home/site/repository/Files/settings.ini
|
||||
sed -i "s|sendmail_from[[:space:]]*= ''|sendmail_from = '$APPSETTING_fromEmailAddress'|" /home/site/repository/Files/settings.ini
|
||||
sed -i "s|sendmail_path[[:space:]]*= ''|sendmail_path = '/usr/sbin/sendmail -t -i'|" /home/site/repository/Files/settings.ini
|
||||
cp /home/site/repository/Files/settings.ini /home/site/redcap.ini
|
||||
|
||||
####################################################################################
|
||||
|
@ -122,7 +122,7 @@ echo "session.cookie_secure = On" >> /home/site/redcap.ini
|
|||
####################################################################################
|
||||
|
||||
mkdir -p /home/site/deployments/tools/PostDeploymentActions
|
||||
cp /home/site/repository/postbuild.sh /home/site/deployments/tools/PostDeploymentActions/postbuild.sh
|
||||
cp /home/site/repository/scripts/bash/postbuild.sh /home/site/deployments/tools/PostDeploymentActions/postbuild.sh
|
||||
|
||||
####################################################################################
|
||||
#
|
||||
|
@ -130,4 +130,4 @@ cp /home/site/repository/postbuild.sh /home/site/deployments/tools/PostDeploymen
|
|||
#
|
||||
####################################################################################
|
||||
|
||||
cp /home/site/repository/startup.sh /home/startup.sh
|
||||
cp /home/site/repository/scripts/bash/startup.sh /home/startup.sh
|
|
@ -13,17 +13,16 @@ echo "hello from postbuild.sh"
|
|||
#
|
||||
####################################################################################
|
||||
|
||||
apt-get install -y python3 python3-pip
|
||||
|
||||
|
||||
####################################################################################
|
||||
#
|
||||
# Install Python3 modules used to scrape REDCap installation SQL script
|
||||
#
|
||||
####################################################################################
|
||||
|
||||
curl -sS https://bootstrap.pypa.io/get-pip.py | python3
|
||||
python3 -m pip install beautifulsoup4
|
||||
python3 -m pip install requests
|
||||
|
||||
####################################################################################
|
||||
#
|
||||
# Scrape the install.php page for SQL commands to execute
|
||||
|
@ -44,11 +43,11 @@ with open("/home/install.sql", "w") as out:
|
|||
1+1
|
||||
EOF
|
||||
python3 scraper.py
|
||||
|
||||
echo "completed running scraper.py with $?"
|
||||
####################################################################################
|
||||
#
|
||||
# Copy the install.sh file to the /home directory
|
||||
#
|
||||
####################################################################################
|
||||
|
||||
cp /home/site/repository/install.sh /home/install.sh
|
||||
cp /home/site/repository/scripts/bash/install.sh /home/install.sh
|
|
@ -0,0 +1,390 @@
|
|||
|
||||
param location string = resourceGroup().location
|
||||
|
||||
var prefix = 'Redcap'
|
||||
var myObjectId = 'd9608212-09d1-440a-a543-585ee85fcdf2'
|
||||
|
||||
var tags = {
|
||||
workload: prefix
|
||||
}
|
||||
|
||||
|
||||
|
||||
module virtualNetwork './modules/networking/main.bicep' = {
|
||||
name: 'vnetDeploy'
|
||||
params: {
|
||||
|
||||
virtualNetworkName: 'VNET-REDCAP'
|
||||
vnetAddressPrefix: '10.230.0.0/24'
|
||||
location: location
|
||||
subnets: subnets
|
||||
customDnsIPs: [
|
||||
//'192.160.0.4'
|
||||
]
|
||||
privateDNSZones: [
|
||||
'privatelink.blob.core.windows.net'
|
||||
'privatelink.file.core.windows.net'
|
||||
'privatelink.mysql.database.azure.com'
|
||||
'privatelink.vaultcore.azure.net'
|
||||
]
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
|
||||
module storageAccounts './modules/storage/main.bicep' = {
|
||||
name: 'strgDeploy1'
|
||||
dependsOn: [ virtualNetwork ]
|
||||
params: {
|
||||
strgConfig: [
|
||||
{
|
||||
location: location
|
||||
storageAccountName: 'redcap${uniqueString(resourceGroup().id)}'
|
||||
peSubnetId: virtualNetwork.outputs.subnets.PrivateLinkSubnet.id
|
||||
storageContainerName: 'redcap'
|
||||
kind: 'StorageV2'
|
||||
storageAccountSku: 'Standard_LRS'
|
||||
//accessTier: 'Hot'
|
||||
privateDNSZones: [
|
||||
'privatelink.blob.core.windows.net'
|
||||
]
|
||||
tags: tags
|
||||
}
|
||||
{
|
||||
location: location
|
||||
storageAccountName: 'fsrc${uniqueString(resourceGroup().id)}'
|
||||
peSubnetId: virtualNetwork.outputs.subnets.PrivateLinkSubnet.id
|
||||
storageContainerName: 'redcap'
|
||||
kind: 'FileStorage'
|
||||
storageAccountSku: 'Premium_LRS'
|
||||
//accessTier: 'Premium'
|
||||
privateDNSZones: [
|
||||
'privatelink.file.core.windows.net'
|
||||
]
|
||||
tags: tags
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var webAppName = 'webApp${uniqueString(resourceGroup().id)}'
|
||||
|
||||
module webApp './modules/webapp/main.bicep' = {
|
||||
name: 'webAppDeploy'
|
||||
params: {
|
||||
webAppName: webAppName
|
||||
appServicePlan: 'ASP-${webAppName}'
|
||||
location: location
|
||||
skuName: 'S1'
|
||||
skuTier: 'Standard'
|
||||
subnetId: virtualNetwork.outputs.subnets.IntegrationSubnet.id
|
||||
linuxFxVersion: 'php|7.4'
|
||||
tags: tags
|
||||
dbHostName: mysqlDbserver.outputs.dbServerName
|
||||
dbName: mysqlDbserver.outputs.dbName
|
||||
dbPassword: sqlPassword
|
||||
dbUserName: sqlUserName
|
||||
}
|
||||
}
|
||||
|
||||
var avdPrefix = '${prefix}-AVD'
|
||||
var customRdpProperty = 'audiocapturemode:i:1;camerastoredirect:s:*;audiomode:i:0;drivestoredirect:s:;redirectclipboard:i:1;redirectcomports:i:0;redirectprinters:i:1;redirectsmartcards:i:1;screen mode id:i:2;devicestoredirect:s:*'
|
||||
|
||||
module avd './modules/avd/avd.bicep' = {
|
||||
scope: resourceGroup()
|
||||
name: 'DeployAVD'
|
||||
params: {
|
||||
location: location
|
||||
// logworkspaceSub: logworkspaceSub
|
||||
// logworkspaceResourceGroup: logworkspaceResourceGroup
|
||||
// logworkspaceName: logworkspaceName
|
||||
hostPoolName: '${avdPrefix}-HP'
|
||||
hostPoolFriendlyName: '${avdPrefix} Host Pool'
|
||||
hostPoolType: 'Pooled'
|
||||
appGroupName: '${avdPrefix}-AG'
|
||||
appGroupFriendlyName: '${avdPrefix} AppGrp'
|
||||
loadBalancerType: 'DepthFirst'
|
||||
workspaceName: '${avdPrefix}-WS'
|
||||
customRdpProperty: customRdpProperty
|
||||
// tokenExpirationTime:
|
||||
maxSessionLimit: 5
|
||||
newBuild: true
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
|
||||
var flexibleServerName = toLower(substring('${prefix}${uniqueString(resourceGroup().id)}', 0, 8))
|
||||
var dbName = '${prefix}db'
|
||||
var sqlPassword = 'P@ssw0rd' // this should be linked to keyvault secret.
|
||||
var sqlUserName = '${flexibleServerName}admin'
|
||||
|
||||
module mysqlDbserver './modules/sql/sql.bicep' = {
|
||||
name: 'DeploymysqlDbserver'
|
||||
params: {
|
||||
flexibleServerName: toLower(flexibleServerName)
|
||||
location: location
|
||||
tags: tags
|
||||
skuName: 'Standard_B1s'
|
||||
SkuTier: 'Burstable'
|
||||
StorageSizeGB: 20
|
||||
StorageIops: 396
|
||||
subnetId: virtualNetwork.outputs.subnets.MySQLFlexSubnet.id
|
||||
privateDnsZone: 'privatelink.mysql.database.azure.com'
|
||||
adminUserName: '${flexibleServerName}admin'
|
||||
adminPassword: sqlPassword
|
||||
mysqlVersion: '8.0.21'
|
||||
dbName: dbName
|
||||
}
|
||||
}
|
||||
|
||||
var keyVaultName = toLower(substring('${prefix}${uniqueString(resourceGroup().id)}', 0, 12))
|
||||
module keyvault './modules/kv/kv.bicep' = {
|
||||
name: 'kvDeploy'
|
||||
params: {
|
||||
keyVaultName: keyVaultName
|
||||
location: location
|
||||
tags: tags
|
||||
objectIds: [
|
||||
webApp.outputs.webAppIdentity
|
||||
myObjectId
|
||||
]
|
||||
subnetIds: [
|
||||
virtualNetwork.outputs.subnets.PrivateLinkSubnet.id
|
||||
]
|
||||
privateDnsZone: 'privatelink.vaultcore.azure.net'
|
||||
secrets: [
|
||||
{
|
||||
name: 'sqlUserName'
|
||||
value: sqlUserName
|
||||
}
|
||||
{
|
||||
name: 'sqlPassword'
|
||||
value: sqlPassword
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// // Azure Virtual Desktop and Session Hosts region
|
||||
|
||||
// resource hostPool 'Microsoft.DesktopVirtualization/hostPools@2022-10-14-preview' = {
|
||||
// name: 'hp-${siteNameCleaned}'
|
||||
// location: location
|
||||
// identity: {
|
||||
// type: 'SystemAssigned'
|
||||
// }
|
||||
// managedBy: 'string'
|
||||
// properties: {
|
||||
// preferredAppGroupType: 'Desktop'
|
||||
// description: 'REDCap AVD host pool for remote app and remote desktop services'
|
||||
// friendlyName: 'REDCap Host Pool'
|
||||
// hostPoolType: 'Pooled'
|
||||
// loadBalancerType: 'BreadthFirst'
|
||||
// maxSessionLimit: 999999
|
||||
// registrationInfo: {
|
||||
// expirationTime: avdRegistrationExpiriationDate
|
||||
// }
|
||||
// validationEnvironment: false
|
||||
// }
|
||||
// }
|
||||
|
||||
// resource applicationGroup 'Microsoft.DesktopVirtualization/applicationGroups@2022-10-14-preview' = {
|
||||
// name: 'dag-${siteNameCleaned}'
|
||||
// location: location
|
||||
// properties: {
|
||||
// applicationGroupType: 'Desktop'
|
||||
// description: 'Windpws 10 Desktops'
|
||||
// friendlyName: 'REDCap Workstation'
|
||||
// hostPoolArmPath: hostPool.id
|
||||
// }
|
||||
// }
|
||||
|
||||
// resource avdWorkspace 'Microsoft.DesktopVirtualization/workspaces@2022-10-14-preview' = {
|
||||
// name: 'ws-${siteNameCleaned}'
|
||||
// location: location
|
||||
// properties: {
|
||||
// applicationGroupReferences: [
|
||||
// applicationGroup.id
|
||||
// ]
|
||||
// description: 'Session desktops'
|
||||
// friendlyName: 'REDCAP Workspace'
|
||||
// }
|
||||
// }
|
||||
|
||||
// resource nic 'Microsoft.Network/networkInterfaces@2020-06-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'nic-redcap-${i}'
|
||||
// location: location
|
||||
// properties: {
|
||||
// ipConfigurations: [
|
||||
// {
|
||||
// name: 'ipconfig'
|
||||
// properties: {
|
||||
// privateIPAllocationMethod: 'Dynamic'
|
||||
// subnet: {
|
||||
// id: redcapComputeSubnet.id
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }]
|
||||
|
||||
// resource vm 'Microsoft.Compute/virtualMachines@2023-03-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'vm-redcap-${i}'
|
||||
// location: location
|
||||
// properties: {
|
||||
// licenseType: 'Windows_Client'
|
||||
// hardwareProfile: {
|
||||
// vmSize: vmSku
|
||||
// }
|
||||
// osProfile: {
|
||||
// computerName: 'vm-redcap-${i}'
|
||||
// adminUsername: vmAdminUserName
|
||||
// adminPassword: vmAdminPassword
|
||||
// windowsConfiguration: {
|
||||
// enableAutomaticUpdates: false
|
||||
// patchSettings: {
|
||||
// patchMode: 'Manual'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// storageProfile: {
|
||||
// osDisk: {
|
||||
// name: 'vm-OS-${i}'
|
||||
// caching: vmDiskCachingType
|
||||
// managedDisk: {
|
||||
// storageAccountType: vmDiskType
|
||||
// }
|
||||
// osType: 'Windows'
|
||||
// createOption: 'FromImage'
|
||||
// }
|
||||
// // TODO Turn into params
|
||||
// imageReference: {
|
||||
// publisher: 'microsoftwindowsdesktop'
|
||||
// offer: 'office-365'
|
||||
// sku: '20h2-evd-o365pp'
|
||||
// version: 'latest'
|
||||
// }
|
||||
// dataDisks: []
|
||||
// }
|
||||
// networkProfile: {
|
||||
// networkInterfaces: [
|
||||
// {
|
||||
// id: nic[i].id
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// dependsOn: [
|
||||
// nic[i]
|
||||
// ]
|
||||
// }]
|
||||
|
||||
// // Reference https://github.com/Azure/avdaccelerator/blob/e247ec5d1ba5fac0c6e9f822c4198c6b41cb77b4/workload/bicep/modules/avdSessionHosts/deploy.bicep#L162
|
||||
// // Needed to get the hostpool in order to pass registration info token, else it comes as null when usiung
|
||||
// // registrationInfoToken: hostPool.properties.registrationInfo.token
|
||||
// // Workaround: reference https://github.com/Azure/bicep/issues/6105
|
||||
// // registrationInfoToken: reference(getHostPool.id, '2021-01-14-preview').registrationInfo.token - also does not work
|
||||
// resource getHostPool 'Microsoft.DesktopVirtualization/hostPools@2019-12-10-preview' existing = {
|
||||
// name: hostPool.name
|
||||
// }
|
||||
|
||||
// // Deploy the AVD agents to each session host
|
||||
// resource avdAgentDscExtension 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'AvdAgentDSC'
|
||||
// parent: vm[i]
|
||||
// location: location
|
||||
// properties: {
|
||||
// publisher: 'Microsoft.Powershell'
|
||||
// type: 'DSC'
|
||||
// typeHandlerVersion: '2.73'
|
||||
// autoUpgradeMinorVersion: true
|
||||
// settings: {
|
||||
// modulesUrl: artifactsLocation
|
||||
// configurationFunction: 'Configuration.ps1\\AddSessionHost'
|
||||
// properties: {
|
||||
// hostPoolName: hostPool.name
|
||||
// registrationInfoToken: getHostPool.properties.registrationInfo.token
|
||||
// aadJoin: false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// dependsOn: [
|
||||
// getHostPool
|
||||
// ]
|
||||
// }]
|
||||
|
||||
// resource domainJoinExtension 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'DomainJoin'
|
||||
// parent: vm[i]
|
||||
// location: location
|
||||
// properties: {
|
||||
// publisher: 'Microsoft.Compute'
|
||||
// type: 'JsonADDomainExtension'
|
||||
// typeHandlerVersion: '1.3'
|
||||
// autoUpgradeMinorVersion: true
|
||||
// settings: {
|
||||
// name: adDomainFqdn
|
||||
// ouPath: adOuPath
|
||||
// user: domainJoinUsername
|
||||
// restart: 'true'
|
||||
// options: '3'
|
||||
// }
|
||||
// protectedSettings: {
|
||||
// password: domainJoinPassword
|
||||
// }
|
||||
// }
|
||||
// dependsOn: [
|
||||
// avdAgentDscExtension[i]
|
||||
// ]
|
||||
// }]
|
||||
|
||||
// resource dependencyAgentExtension 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'DAExtension'
|
||||
// parent: vm[i]
|
||||
// location: location
|
||||
// properties: {
|
||||
// publisher: 'Microsoft.Azure.Monitoring.DependencyAgent'
|
||||
// type: 'DependencyAgentWindows'
|
||||
// typeHandlerVersion: '9.5'
|
||||
// autoUpgradeMinorVersion: true
|
||||
// }
|
||||
// }]
|
||||
|
||||
// resource antiMalwareExtension 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'IaaSAntiMalware'
|
||||
// parent: vm[i]
|
||||
// location: location
|
||||
// properties: {
|
||||
// publisher: 'Microsoft.Azure.Security'
|
||||
// type: 'IaaSAntimalware'
|
||||
// typeHandlerVersion: '1.5'
|
||||
// autoUpgradeMinorVersion: true
|
||||
// settings: {
|
||||
// AntimalwareEnabled: true
|
||||
// }
|
||||
// }
|
||||
// }]
|
||||
|
||||
// resource ansibleExtension 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = [for i in range(0, AVDnumberOfInstances): {
|
||||
// name: 'AnsibleWinRM'
|
||||
// parent: vm[i]
|
||||
// location: location
|
||||
// properties: {
|
||||
// publisher: 'Microsoft.Compute'
|
||||
// type: 'CustomScriptExtension'
|
||||
// typeHandlerVersion: '1.10'
|
||||
// autoUpgradeMinorVersion: true
|
||||
// settings: {
|
||||
// fileUris: [ 'https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1' ]
|
||||
// }
|
||||
// protectedSettings: {
|
||||
// commandToExecute: 'powershell.exe -Command \'./ConfigureRemotingForAnsible.ps1; exit 0;\''
|
||||
// }
|
||||
// }
|
||||
// }]
|
||||
|
||||
// output MySQLHostName string = '${uniqueServerName}.mysql.database.azure.com'
|
||||
// output MySqlUserName string = '${administratorLogin}@${uniqueServerName}'
|
||||
// output webSiteFQDN string = '${uniqueWebSiteName}.azurewebsites.net'
|
||||
// output storageAccountName string = uniqueStorageName
|
||||
// output storageContainerName string = storageContainerName
|
|
@ -0,0 +1,2 @@
|
|||
using './azuredeploysecure.bicep'
|
||||
|
|
@ -6,55 +6,53 @@ $version = 0;
|
|||
|
||||
#DEPLOYMENT OPTIONS
|
||||
#Please review the azuredeploy.bicep file for available options
|
||||
$RGName = "<YOUR RESOURCE GROUP>"
|
||||
$DeployRegion = "<SELECT AZURE REGION>"
|
||||
$RGName = "RG-Redcap"
|
||||
$DeployRegion = "eastus"
|
||||
|
||||
$parms = @{
|
||||
|
||||
#Alternative to the zip file above, you can use REDCap Community credentials to download the zip file.
|
||||
"redcapCommunityUsername" = "<REDCap Community site username>";
|
||||
"redcapCommunityPassword" = "<REDCap Community site password>";
|
||||
"redcapCommunityUsername" = "vishalkalal@thevktech.com";
|
||||
"redcapCommunityPassword" = "abc@1234";
|
||||
"redcapAppZipVersion" = "<REDCap version";
|
||||
|
||||
#Mail settings
|
||||
"fromEmailAddress" = "<email address listed as sender for outbound emails>";
|
||||
"smtpFQDN" = "<what it says>"
|
||||
"smtpUser" = "<login name for smtp auth>"
|
||||
"smtpPassword" = "<password for smtp auth>"
|
||||
"fromEmailAddress" = "vishalkalal@thevktech.com";
|
||||
"smtpFQDN" = "smtp.thevktech.com"
|
||||
"smtpUser" = "smtpuser"
|
||||
"smtpPassword" = "password@123"
|
||||
|
||||
#Azure Web App
|
||||
"siteName" = "<WEB SITE NAME, like 'redcap'>";
|
||||
"siteName" = "vkdemoredcap";
|
||||
"skuName" = "S1";
|
||||
"skuCapacity" = 1;
|
||||
|
||||
#MySQL
|
||||
"administratorLogin" = "<MySQL admin account name>";
|
||||
"administratorLoginPassword" = "<MySQL admin login password>";
|
||||
"administratorLogin" = "vishalkalal";
|
||||
"administratorLoginPassword" = "P@ssw0rd@123";
|
||||
|
||||
# "databaseForMySqlCores" = 2;
|
||||
# "databaseForMySqlFamily" = "Gen5";
|
||||
# "databaseSkuSizeMB" = 5120;
|
||||
# "databaseForMySqlTier" = "GeneralPurpose";
|
||||
"mysqlVersion" = "5.7";
|
||||
|
||||
|
||||
#Azure Storage
|
||||
"storageType" = "Standard_LRS";
|
||||
"storageContainerName" = "redcap";
|
||||
|
||||
#GitHub
|
||||
"repoURL" = "https://github.com/vanderbilt-redcap/redcap-azure.git";
|
||||
"repoURL" = "https://github.com/microsoft/azure-redcap-paas.git";
|
||||
"branch" = "master";
|
||||
|
||||
#AVD session hosts
|
||||
"vmAdminUserName" = "<vm admin user name>"
|
||||
"vmAdminPassword" = "<vm admin password>"
|
||||
"vmAdminUserName" = "vishalkalal"
|
||||
"vmAdminPassword" = "P@ssw0rd@123"
|
||||
|
||||
#Domain join
|
||||
"domainJoinUsername" = "<domain join user name>"
|
||||
"domainJoinPassword" = "<domain join password>"
|
||||
"adDomainFqdn" = "<AD Domain FQDN>"
|
||||
|
||||
|
||||
"domainJoinUsername" = "ADDC01-admin"
|
||||
"domainJoinPassword" = "Info$world"
|
||||
"adDomainFqdn" = "thevktech.local"
|
||||
}
|
||||
#END DEPLOYMENT OPTIONS
|
||||
|
Загрузка…
Ссылка в новой задаче