diff --git a/.github/workflows/main.bicep b/.github/workflows/main.bicep new file mode 100644 index 0000000..34a6993 --- /dev/null +++ b/.github/workflows/main.bicep @@ -0,0 +1,31 @@ +// =========== main.bicep =========== + +param buildId string +param environment string +param location string = resourceGroup().location + +module apim '../../apim.bicep' = { + name: 'unit-test-${buildId}-apim' + params: { + application: 'chenette' + environment: environment + location: location + } +} + +// module vnet '../../vnet.bicep' = { +// name: 'unit-test-${buildId}-vnet' +// params: { +// environment: environment +// location: location +// } +// } + +// module snet '../../snet.bicep' = { +// name: 'unit-test-${buildId}-snet' +// params: { +// vnetName: vnet.outputs.name +// environment: environment +// location: location +// } +// } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18b71cb..339c614 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,21 +1,41 @@ -on: [push, workflow_dispatch] +on: [push, workflow_dispatch] name: Azure ARM + +permissions: + id-token: write + contents: read + jobs: - build-and-deploy: + deploy: environment: Azure runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@main + - uses: actions/checkout@v3 - - name: Log into Azure - uses: azure/login@v1.4.3 + # https://github.com/marketplace/actions/azure-login#login-with-openid-connect-oidc-recommended + - name: Log in to Azure using OIDC + uses: azure/login@v1 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Deploy Bicep file - uses: azure/arm-deploy@main + - name: Deploy resources to Azure + uses: azure/arm-deploy@v1 with: + resourceGroupName: rg-application-dev-001 + template: .github/workflows/main.bicep + parameters: buildId=${{ github.run_attempt }} environment=dev + + + - name: Remove deployed resources + uses: azure/arm-deploy@v1 + with: + scope: subscription deploymentName: ${{ github.run_number }} - resourceGroupName: ${{ secrets.RESOURCE_GROUP }} + region: southcentralus template: demo.bicep + deploymentMode: Complete + parameters: delete=true + + diff --git a/.ps-rule/GitHub.Community.Rule.ps1 b/.ps-rule/GitHub.Community.Rule.ps1 new file mode 100644 index 0000000..66df67a --- /dev/null +++ b/.ps-rule/GitHub.Community.Rule.ps1 @@ -0,0 +1,11 @@ +# Example .ps-rule/GitHub.Community.Rule.ps1 + +# Synopsis: Check for recommended community files +Rule 'GitHub.Community' -Type 'PSRule.Data.RepositoryInfo' { + $Assert.FilePath($TargetObject, 'FullName', @('LICENSE')); + $Assert.FilePath($TargetObject, 'FullName', @('CODE_OF_CONDUCT.md')); + $Assert.FilePath($TargetObject, 'FullName', @('CONTRIBUTING.md')); + $Assert.FilePath($TargetObject, 'FullName', @('README.md')); + $Assert.FilePath($TargetObject, 'FullName', @('.github/CODEOWNERS')); + $Assert.FilePath($TargetObject, 'FullName', @('.github/PULL_REQUEST_TEMPLATE.md')); +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c179722 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Matthew Chenette + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1272a28 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +## Best Practices +- [Bicep Parameters](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices#parameters) +- [ARM Templates](https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/best-practices) + +- It is recommended to give a descriptive and unique [deploymentName](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-resource-manager-template-deployment-v3?view=azure-pipelines#inputs) for both the modules used and for the Azure Pipelines task itself. This allows for quicker and easier debugging of potential errors. + +- https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/best-practices#parameters + +- The idea is to be able to delete the entire application in Azure and be able to redeploy it with one click of a button + +- One thing that IaC does not address is data. + + +- The principal behind these templates: + - They can be deployed with only 1-2 parameters required from the consumer + - Further configuration/customization possible, but not necessary + +- The idea is also to have these templates have all minimum and recommended security settings enabled by default (i.e., https/tls 1.2, ...) \ No newline at end of file diff --git a/agw.bicep b/agw.bicep new file mode 100644 index 0000000..636bc10 --- /dev/null +++ b/agw.bicep @@ -0,0 +1,62 @@ +// =========== agw.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' +param vnetName string +param snetName string + +@allowed([443, 8080]) +param httpSettingPort int = 443 +param httpSettingProtocol string = 'Http' + + +// PROPERTIES PARAMETERS +param sku object = { + name: 'Standard_Small' + tier: 'Standard' + capacity: 2 +} +param backendPools array = [ + { name: 'appGatewayBackendPool', properties: { backendAddresses: [ { IpAddress: '10.0.0.4' }, { IpAddress: '10.0.0.5' } ] } } +] +param backendHttpSettings array = [ + { name: 'appGatewayBackendHttpSettings', properties: { port: httpSettingPort, protocol: httpSettingProtocol, cookieBasedAffinity: 'Disabled' } } +] +param httpListeners array = [ + { name: 'appGatewayHttpListener', properties: { frontendIPConfiguration: { id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', name, 'appGatewayFrontendIP') }, frontendPort: { id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', name, 'appGatewayFrontendPort') }, protocol: 'Http' } } +] +param requestRoutingRules array = [ + { name: 'rule1', properties: { ruleType: 'Basic', httpListener: { id: resourceId('Microsoft.Network/applicationGateways/httpListeners', name, 'appGatewayHttpListener') }, backendAddressPool: { id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', name, 'appGatewayBackendPool') }, backendHttpSettings: { id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', name, 'appGatewayBackendHttpSettings') } } } +] +param frontendPorts array = [ + { name: 'appGatewayFrontendPort', properties: { port: 80 } } +] + +// BASE PARAMETERS +param name string = 'agw-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location + +// DEPENDENCIES +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName } +// resource snet 'Microsoft.Network/virtualNetworks/subnets@2022-11-01' existing = { name: snetName } +var subnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, snetName) + + +// RESOURCE +resource applicationGateway 'Microsoft.Network/applicationGateways@2022-11-01' = { + name: name + location: location + properties: { + sku: sku + gatewayIPConfigurations: [ { name: 'appGatewayIpConfig', properties: { subnet: { id: subnetRef } } } ] + frontendIPConfigurations: [ { name: 'appGatewayFrontendIP', properties: { subnet: { id: subnetRef } } } ] + frontendPorts: [for frontendPort in frontendPorts: { name: frontendPort.name, properties: frontendPort.properties }] + backendAddressPools: [for backendPool in backendPools: { name: backendPool.name, properties: backendPool.properties }] + backendHttpSettingsCollection: [for backendHttpSetting in backendHttpSettings: { name: backendHttpSetting.name, properties: backendHttpSetting.properties }] + httpListeners: [for httpListener in httpListeners: { name: httpListener.name, properties: httpListener.properties }] + requestRoutingRules: [for requestRoutingRule in requestRoutingRules: { name: requestRoutingRule.name, properties: requestRoutingRule.properties }] + } + dependsOn: [ vnet ] +} diff --git a/apim.bicep b/apim.bicep new file mode 100644 index 0000000..f713396 --- /dev/null +++ b/apim.bicep @@ -0,0 +1,26 @@ +param application string +param environment string +param instance string = '1' + +// PROPERTIES PARAMETERS + +// BASE PARAMETERS +param name string = 'apim-${application}-${environment}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param sku object = { + name: 'Developer' + capacity: 1 +} +param identity object = { type: 'None' } +param properties object = { + publisherEmail: 'iron3bromate@gmail.com' + publisherName: 'chenette' +} + +resource apim 'Microsoft.ApiManagement/service@2023-03-01-preview' = { + name: name + location: location + sku: sku + identity: identity + properties: properties +} diff --git a/app.bicep b/app.bicep new file mode 100644 index 0000000..2a6c7e8 --- /dev/null +++ b/app.bicep @@ -0,0 +1,33 @@ +// =========== app.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' +param aspId string + +// PROPERTIES PARAMETERS +param httpsOnly bool = true +param isLinux bool = true + +// BASE PARAMETERS +param name string = 'app-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param kind string = 'app,linux' // 'app,linux,container' +param properties object = { + httpsOnly: httpsOnly + reserved: isLinux + serverFarmId: aspId + siteConfig: { + alwaysOn: true + linuxFxVersion: 'DOTNETCORE|7.0' // 'DOCKER|crchenetteprod001.azurecr.io/dotnetwebapp:latest' + } +} + +// RESOURCE +resource app 'Microsoft.Web/sites@2022-09-01' = { + name: name + location: location + kind: kind + properties: properties +} diff --git a/asp.bicep b/asp.bicep new file mode 100644 index 0000000..73657ab --- /dev/null +++ b/asp.bicep @@ -0,0 +1,41 @@ +// =========== asp.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' + +// SKU PARAMETERS +param skuCapacity int = 1 +param skuFamily string = 'B' +param skuName string = 'B1' +param skuSize string = 'B1' +param skuTier string = 'Basic' + +// PROPERTIES PARAMETERS +param isLinux bool = true + +// BASE PARAMETERS +param name string = 'asp-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param sku object = { + capacity: skuCapacity + family: skuFamily + name: skuName + size: skuSize + tier: skuTier +} +param properties object = { + reserved: isLinux +} + +// RESOURCE +resource asp 'Microsoft.Web/serverfarms@2022-09-01' = { + name: name + location: location + sku: sku + properties: properties +} + +// OUTPUTS +output resourceId string = asp.id diff --git a/cr.bicep b/cr.bicep new file mode 100644 index 0000000..88b434c --- /dev/null +++ b/cr.bicep @@ -0,0 +1,50 @@ +// =========== cr.bicep =========== + +param name string +param location string +param sku string + +resource cr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { + name: name + location: location + sku: { + name: sku + } + properties: { + adminUserEnabled: true + } +} + +resource kv 'Microsoft.KeyVault/vaults@2023-02-01' = { + name: name + location: location + properties: { + enabledForTemplateDeployment: true + tenantId: tenant().tenantId + accessPolicies: [] + sku: { + name: 'standard' + family: 'A' + } + } + resource crUsername 'secrets' = { + name: 'crUsername' + properties: { + value: cr.listCredentials().username + } + } + resource crPassword1 'secrets' = { + name: 'crPassword1' + properties: { + value: cr.listCredentials().passwords[0].value + } + } + resource crPassword2 'secrets' = { + name: 'crPassword2' + properties: { + value: cr.listCredentials().passwords[1].value + } + } +} + +// output resource resource = cr diff --git a/demo.bicep b/demo.bicep deleted file mode 100644 index 8a218fd..0000000 --- a/demo.bicep +++ /dev/null @@ -1,37 +0,0 @@ -param webAppName string = uniqueString(resourceGroup().id) // Generate unique String for web app name -param sku string = 'S1' // The SKU of App Service Plan -param linuxFxVersion string = 'node|14-lts' // The runtime stack of web app -param location string = resourceGroup().location // Location for all resources -param repositoryUrl string = 'https://github.com/Azure-Samples/nodejs-docs-hello-world' -param branch string = 'master' -var appServicePlanName = toLower('AppServicePlan-${webAppName}') -var webSiteName = toLower('wapp-${webAppName}') -resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { - name: appServicePlanName - location: location - properties: { - reserved: true - } - sku: { - name: sku - } - kind: 'linux' -} -resource appService 'Microsoft.Web/sites@2020-06-01' = { - name: webSiteName - location: location - properties: { - serverFarmId: appServicePlan.id - siteConfig: { - linuxFxVersion: linuxFxVersion - } - } -} -resource srcControls 'Microsoft.Web/sites/sourcecontrols@2021-01-01' = { - name: '${appService.name}/web' - properties: { - repoUrl: repositoryUrl - branch: branch - isManualIntegration: true - } -} diff --git a/kv.bicep b/kv.bicep new file mode 100644 index 0000000..84e6b18 --- /dev/null +++ b/kv.bicep @@ -0,0 +1,35 @@ +// =========== kv.bicep =========== + +@minLength(3) +@maxLength(24) +param name string + +param location string = resourceGroup().location +param tags object = {} + +@allowed([ + 'standard' + 'premium' +]) +param skuName string = 'standard' + +resource kv 'Microsoft.KeyVault/vaults@2023-02-01' = { + name: name + location: location + tags: tags + properties: { + accessPolicies: [] + enabledForDeployment: false + enabledForDiskEncryption: false + enabledForTemplateDeployment: true + enablePurgeProtection: false + enableRbacAuthorization: false + enableSoftDelete: false + sku: { + name: skuName + family: 'A' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} diff --git a/main.bicep b/main.bicep deleted file mode 100644 index 2dfa645..0000000 --- a/main.bicep +++ /dev/null @@ -1,112 +0,0 @@ -// @minLength(3) -// @maxLength(11) -// param storagePrefix string - -// @allowed([ -// 'Standard_LRS' -// 'Standard_GRS' -// 'Standard_RAGRS' -// 'Standard_ZRS' -// 'Premium_LRS' -// 'Premium_ZRS' -// 'Standard_GZRS' -// 'Standard_RAGZRS' -// ]) -// param storageSKU string = 'Standard_LRS' - -// param location string = resourceGroup().location - -// var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}' - -// resource stg 'Microsoft.Storage/storageAccounts@2021-04-01' = { -// name: uniqueStorageName -// location: location -// sku: { -// name: storageSKU -// } -// kind: 'StorageV2' -// properties: { -// supportsHttpsTrafficOnly: true -// } -// } - -// output storageEndpoint object = stg.properties.primaryEndpoints - -@description('The Azure region into which the resources should be deployed.') -param location string = resourceGroup().location - -@description('The type of environment. This must be nonprod or prod.') -@allowed([ - 'nonprod' - 'prod' -]) -param environmentType string - -@description('A unique suffix to add to resource names that need to be globally unique.') -@maxLength(13) -param resourceNameSuffix string = uniqueString(resourceGroup().id) - -var appServiceAppName = 'toy-website-${resourceNameSuffix}' -var appServicePlanName = 'toy-website-plan' -var toyManualsStorageAccountName = 'toyweb${resourceNameSuffix}' - -// Define the SKUs for each component based on the environment type. -var environmentConfigurationMap = { - nonprod: { - appServicePlan: { - sku: { - name: 'F1' - capacity: 1 - } - } - toyManualsStorageAccount: { - sku: { - name: 'Standard_LRS' - } - } - } - prod: { - appServicePlan: { - sku: { - name: 'S1' - capacity: 2 - } - } - toyManualsStorageAccount: { - sku: { - name: 'Standard_ZRS' - } - } - } -} -var toyManualsStorageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${toyManualsStorageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${toyManualsStorageAccount.listKeys().keys[0].value}' - -resource appServicePlan 'Microsoft.Web/serverFarms@2020-06-01' = { - name: appServicePlanName - location: location - sku: environmentConfigurationMap[environmentType].appServicePlan.sku -} - -resource appServiceApp 'Microsoft.Web/sites@2020-06-01' = { - name: appServiceAppName - location: location - properties: { - serverFarmId: appServicePlan.id - httpsOnly: true - siteConfig: { - appSettings: [ - { - name: 'ToyManualsStorageAccountConnectionString' - value: toyManualsStorageAccountConnectionString - } - ] - } - } -} - -resource toyManualsStorageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { - name: toyManualsStorageAccountName - location: location - kind: 'StorageV2' - sku: environmentConfigurationMap[environmentType].toyManualsStorageAccount.sku -} diff --git a/snet.bicep b/snet.bicep new file mode 100644 index 0000000..30fd9f1 --- /dev/null +++ b/snet.bicep @@ -0,0 +1,24 @@ +// =========== snet.bicep =========== + +// USER-PROVIDED PARAMETERS +param environment string +param instance string = '1' +param vnetName string + +// PROPERTIES PARAMETERS +param subnetAddressPrefix string = '10.0.0.0/24' +param location string = resourceGroup().location + +// BASE PARAMETERS +param name string = 'snet-${environment}-${location}-${padLeft(instance, 3, '0')}' +resource parent 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName } +param properties object = { addressPrefix: subnetAddressPrefix } + +// RESOURCE +resource snet 'Microsoft.Network/virtualNetworks/subnets@2022-11-01' = { + name: name + parent: parent + properties: properties +} + +output name string = name diff --git a/sql.bicep b/sql.bicep new file mode 100644 index 0000000..4077ca9 --- /dev/null +++ b/sql.bicep @@ -0,0 +1,35 @@ +// =========== sql.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' + +// PROPERTIES PARAMETERS +param azureADOnlyAuthentication bool = true +param minimalTlsVersion string = '1.2' +param publicNetworkAccess string = 'Disabled' + +// BASE PARAMETERS +param name string = 'sql-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param properties object = { + administrators: { + azureADOnlyAuthentication: azureADOnlyAuthentication + login: 'matthew@chenette.com' //'Global DBAs' + principalType: 'User' //'Group' + sid: '84574441-38cc-4302-be53-903f57446fdb' //'16f797d0-d11e-436c-a20e-1415561cfe93' + } + minimalTlsVersion: minimalTlsVersion + publicNetworkAccess: publicNetworkAccess +} + +// RESOURCE +resource sql 'Microsoft.Sql/servers@2022-08-01-preview' = { + name: name + location: location + properties: properties +} + +// OUTPUTS +output resourceId string = sql.id diff --git a/sqldb.bicep b/sqldb.bicep new file mode 100644 index 0000000..ab1623f --- /dev/null +++ b/sqldb.bicep @@ -0,0 +1,27 @@ +// =========== sqldb.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' + +// PROPERTIES PARAMETERS +param azureADOnlyAuthentication bool = true +param minimalTlsVersion string = '1.2' +param publicNetworkAccess string = 'Disabled' + +// BASE PARAMETERS +param name string = 'sql-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param sku object = { + capacity: 1 + family: 'Gen5' + name: 'GP_S_Gen5' + tier: 'GeneralPurpose' +} +// RESOURCE +resource sqldb 'Microsoft.Sql/servers/databases@2022-08-01-preview' = { + name: name + location: location + sku: sku +} diff --git a/st.bicep b/st.bicep new file mode 100644 index 0000000..6bbde41 --- /dev/null +++ b/st.bicep @@ -0,0 +1,24 @@ +// =========== st.bicep =========== + +// USER-PROVIDED PARAMETERS +param application string +param environment string +param instance string = '1' + +// BASE PARAMETERS +param name string = 'asp-${application}-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param sku object = { + name: 'Standard_LRS' +} +param kind string = 'StorageV2' + +// RESOURCE +resource st 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: name + location: location + sku: sku + kind: kind +} + +// OUTPUTS diff --git a/vnet.bicep b/vnet.bicep new file mode 100644 index 0000000..658a705 --- /dev/null +++ b/vnet.bicep @@ -0,0 +1,23 @@ +// =========== vnet.bicep =========== + +// USER-PROVIDED PARAMETERS +param environment string +param instance string = '1' + +// PROPERTIES PARAMETERS +param vnetAddressPrefixes array = [ '10.0.0.0/16' ] +param addressSpace object = { addressPrefixes: vnetAddressPrefixes } + +// BASE PARAMETERS +param name string = 'vnet-${environment}-${location}-${padLeft(instance, 3, '0')}' +param location string = resourceGroup().location +param properties object = { addressSpace: addressSpace } + +// RESOURCE +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { + name: name + location: location + properties: properties +} + +output name string = name