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:
Vishal Kalal 2023-11-08 14:06:38 -06:00 коммит произвёл GitHub
Родитель 859988e173
Коммит df8ee32663
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
48 изменённых файлов: 2650 добавлений и 1387 удалений

Просмотреть файл

@ -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

76
.github/workflows/bicep-build.yml поставляемый Normal file
Просмотреть файл

@ -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 }}

7
.gitignore поставляемый
Просмотреть файл

@ -330,3 +330,10 @@ ASALocalRun/
.mfractor/
*.sln
*.deployproj
/*.json
# Exclude Bicep parameter files
*.bicepparam
# Except for the sample file
!/*-sample.bicepparam

2
.vscode/settings.json поставляемый
Просмотреть файл

@ -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 = ''

Просмотреть файл

@ -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 Microsofts 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
configuration.md Normal file
Просмотреть файл

72
deploy.ps1 Normal file
Просмотреть файл

@ -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
}

26
main-sample.bicepparam Normal file
Просмотреть файл

@ -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 = ''

405
main.bicep Normal file
Просмотреть файл

@ -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

47
manual.md Normal file
Просмотреть файл

@ -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'
```

121
modules/avd/avd.bicep Normal file
Просмотреть файл

@ -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

1
modules/avd/main.bicep Normal file
Просмотреть файл

@ -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

109
modules/kv/kv.bicep Normal file
Просмотреть файл

@ -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
}
}]

54
modules/kv/main.bicep Normal file
Просмотреть файл

@ -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
}
}]

32
modules/pdns/main.bicep Normal file
Просмотреть файл

@ -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

101
modules/sql/main.bicep Normal file
Просмотреть файл

@ -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

150
modules/sql/sql.bicep Normal file
Просмотреть файл

@ -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

12
modules/uami/main.bicep Normal file
Просмотреть файл

@ -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

93
modules/webapp/main.bicep Normal file
Просмотреть файл

@ -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

198
modules/webapp/webapp.bicep Normal file
Просмотреть файл

@ -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