├── .env ├── .env.powershell ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── Readme.md ├── oidc ├── .env ├── .env.powershell ├── Readme.md ├── federated_credential.json ├── resources.bicep ├── up.ps1 └── up.sh ├── resources.bicep ├── up.ps1 └── up.sh /.env: -------------------------------------------------------------------------------- 1 | # change values as needed 2 | name="tfscaffold" 3 | suffix="dev" 4 | location="northeurope" 5 | 6 | spName="sp-$name-$suffix" 7 | rg="rg-$name-$suffix" 8 | tag="$suffix" 9 | saName="stac0${name}0${suffix}" 10 | scName="blob0${name}0${suffix}" 11 | vaultName="akv-$name-$suffix" 12 | 13 | saSku="Standard_ZRS" 14 | vaultSku="standard" 15 | -------------------------------------------------------------------------------- /.env.powershell: -------------------------------------------------------------------------------- 1 | # change values as needed 2 | name=tfscaffold 3 | suffix=dev 4 | location=northeurope 5 | saSku=Standard_ZRS 6 | vaultSku=standard 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: nmeisenzahl 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 2. ... 17 | 18 | ## Expected behavior 19 | A clear and concise description of what you expected to happen. 20 | 21 | ## Screenshots/Logs 22 | If applicable, add screenshots and console output to help explain your problem. 23 | 24 | ## Additional context 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: nmeisenzahl 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? 11 | Please describe. A clear and concise description of what the problem is. 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or code snippets about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Describe your Pull Request 2 | A clear and concise description of what the Pull Request is about. 3 | 4 | Fixes # (issue) 5 | 6 | Please select relevant options: 7 | 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 11 | 12 | ## Checklist 13 | 14 | - [ ] My code follows the style guidelines of this project 15 | - [ ] I have made corresponding changes to the documentation 16 | - [ ] I have added tests that prove my fix is effective or that my feature works 17 | - [ ] I have checked my code and corrected any misspellings 18 | 19 | Reviewer: @nmeisenzahl 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 white duck Gesellschaft für Softwareentwicklung mbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Terraform scaffold for Azure 2 | 3 | This repo contains everything to get started with Terraform on Azure. 4 | 5 | ## What you will get 6 | 7 | After executing the below steps you will get: 8 | 9 | - a service principal used to run Terraform on behalf 10 | - a Storage Container used to store the Terraform state file 11 | - a Key Vault containing all secrets to allow easy and secure access 12 | 13 | ## Requirements 14 | 15 | This project requires the following: 16 | 17 | - Bash or PowerShell (you can use [Azure Cloud Shell](http://shell.azure.com/)) 18 | - for Bash you need to have [jq](https://stedolan.github.io/jq/) installed 19 | - Azure CLI (authenticated) 20 | - the executing user needs Subscription owner access (to give owner access to the Service Principal for creating managed identities and assigning roles) as well as the Application Developer role in AAD (to create the Service Principal) 21 | 22 | ## Get started with Bash 23 | 24 | Execute the following steps to get started: 25 | 26 | 1. Authenticate against Azure by executing `az login` 27 | 2. Optional: Export your Tenant (`tenantId`) and Subscription ID (`subscriptionId`) if you don't like to deploy with your `az` defaults. 28 | 3. Customize `.env` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). 29 | 4. Execute `up.sh` to deploy everything needed 30 | 5. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Azure AD). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). 31 | 32 | ## Get started with PowerShell 33 | 34 | Execute the following steps to get started: 35 | 36 | 1. Authenticate against Azure by executing `az login` 37 | 2. Optional: Create environment variables for Tenant (`tenantId`) and Subscription ID (`subscriptionId`) or call the script with the parameters `-tenantId` and `-subscriptionId` if you don't like to deploy with your `az` defaults. 38 | 3. Customize `.env.powershell` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). 39 | 4. Execute `up.ps1` to deploy everything needed 40 | 5. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Azure AD). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). 41 | 42 | ## Scaffold a Terraform project 43 | 44 | You will need to tell Terraform where to store its state file. To do so, you need to customize your `main.tf` file based on the below example: 45 | 46 | ``` 47 | terraform { 48 | required_providers { 49 | azurerm = { 50 | source = "hashicorp/azurerm" 51 | version = "3.77.0" 52 | } 53 | } 54 | backend "azurerm" { 55 | key = "azure.tfstate" 56 | } 57 | } 58 | 59 | provider "azurerm" { 60 | # Configuration options 61 | } 62 | ``` 63 | 64 | [Terraform Backend Docs for azurerm](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm) 65 | 66 | [Azure Provider Docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) 67 | 68 | We do not recommend to store any secrets and credentials in code. Therefore everything needed will be requested from Key Vault as needed. To init you project run the following script: 69 | 70 | ```Bash 71 | #!/bin/bash 72 | 73 | # customize your subscription id and resource group name 74 | export subscriptionId="00000000-0000-0000-0000-000000000000" 75 | export rg="my-rg" 76 | 77 | # sets subscription; 78 | az account set --subscription $subscriptionId 79 | 80 | # get vault 81 | export vaultName=$(az keyvault list --subscription=$subscriptionId -g $rg --query '[0].{name:name}' -o tsv) 82 | 83 | ## extracts and exports secrets 84 | export saKey=$(az keyvault secret show --subscription=$subscriptionId --vault-name="$vaultName" --name sa-key --query value -o tsv) 85 | export saName=$(az keyvault secret show --subscription=$subscriptionId --vault-name="$vaultName" --name sa-name --query value -o tsv) 86 | export scName=$(az keyvault secret show --subscription=$subscriptionId --vault-name="$vaultName" --name sc-name --query value -o tsv) 87 | export spSecret=$(az keyvault secret show --subscription=$subscriptionId --vault-name="$vaultName" --name sp-secret --query value -o tsv) 88 | export spId=$(az keyvault secret show --subscription=$subscriptionId --vault-name="$vaultName" --name sp-id --query value -o tsv) 89 | 90 | # exports secrets 91 | export ARM_SUBSCRIPTION_ID=$subscriptionId 92 | export ARM_TENANT_ID=$tenantId 93 | export ARM_CLIENT_ID=$spId 94 | export ARM_CLIENT_SECRET=$spSecret 95 | 96 | # runs Terraform init 97 | terraform init -input=false \ 98 | -backend-config="access_key=$saKey" \ 99 | -backend-config="storage_account_name=$saName" \ 100 | -backend-config="container_name=$scName" 101 | ``` 102 | 103 | ## Azuread provider configuration 104 | 105 | ``` 106 | terraform { 107 | required_providers { 108 | azuread = { 109 | source = "hashicorp/azuread" 110 | version = "2.44.1" 111 | } 112 | } 113 | } 114 | 115 | provider "azuread" { 116 | # Configuration options 117 | } 118 | ``` 119 | 120 | [Azure Active Directory Provider Docs](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) 121 | 122 | ## Disclaimer 123 | 124 | The `up.sh` script asks you whether you would like to map our Partner ID to the created Service Principal. Feel free to opt-out or remove the marked lines if you don't like to support us. 125 | 126 | > 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. 127 | -------------------------------------------------------------------------------- /oidc/.env: -------------------------------------------------------------------------------- 1 | # change values as needed 2 | name="test" 3 | env="dev" 4 | scope="tfstate" 5 | location="westeurope" 6 | 7 | spName="sp-$scope-$name-$env" 8 | rg="rg-$scope-$name-$env" 9 | tag="$env" 10 | saName="st${scope}${name}${env}" 11 | scName="blob-$scope-$name-$env" 12 | 13 | saSku="Standard_ZRS" -------------------------------------------------------------------------------- /oidc/.env.powershell: -------------------------------------------------------------------------------- 1 | # change values as needed 2 | name=tfscaffold 3 | suffix=dev 4 | location=northeurope 5 | saSku=Standard_ZRS 6 | -------------------------------------------------------------------------------- /oidc/Readme.md: -------------------------------------------------------------------------------- 1 | # Terraform scaffold for Azure 2 | 3 | This repo contains everything to get started with Terraform on Azure. It sets you up to use the `azurerm` backend with Service Principal authentication via OIDC. 4 | 5 | [Terraform Backend Docs for azurerm](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm#backend-azure-ad-service-principal-or-user-assigned-managed-identity-via-oidc-workload-identity-federation) 6 | 7 | ## What you will get 8 | After executing the below steps you will get: 9 | 10 | - a service principal used to run Terraform on behalf 11 | - a Storage Container used to store the Terraform state file 12 | 13 | ## Requirements 14 | 15 | This project requires the following: 16 | 17 | - Bash or PowerShell (you can use [Azure Cloud Shell](http://shell.azure.com/)) 18 | - for Bash you need to have [jq](https://stedolan.github.io/jq/) installed 19 | - Azure CLI (authenticated) 20 | - the executing user needs Subscription owner access (to give owner access to the Service Principal for creating managed identities and assigning roles) as well as the Application Developer role in Entra ID (to create the Service Principal) 21 | 22 | ## Get started with Bash 23 | 24 | Execute the following steps to get started: 25 | 26 | 1. Authenticate against Azure by executing `az login` 27 | 1. Optional: Export your Tenant (`tenantId`) and Subscription ID (`subscriptionId`) if you don't like to deploy with your `az` defaults. 28 | 1. Customize `.env` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). 29 | 1. Update the \ in `federated_credential.json`. 30 | 1. Execute `up.sh` to deploy everything needed 31 | 1. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Entra ID). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). 32 | 33 | #TODO 34 | ## Get started with PowerShell 35 | 36 | Execute the following steps to get started: 37 | 38 | 1. Authenticate against Azure by executing `az login` 39 | 1. Optional: Create environment variables for Tenant (`tenantId`) and Subscription ID (`subscriptionId`) or call the script with the parameters `-tenantId` and `-subscriptionId` if you don't like to deploy with your `az` defaults. 40 | 1. Customize `.env.powershell` based on your needs and naming conventions (Make sure you met all [Azure naming rules and restrictions](https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules)). 41 | 1. Update the \ in `federated_credential.json`. 42 | 1. Execute `up.ps1` to deploy everything needed 43 | 1. Grant admin consent for the created app registrations (Terraform will then be allowed to create app registrations and groups in Entra ID). This needs Azure Active Directory global admin access. Find more details on how to grant consent [here](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent). 44 | 45 | 46 | ## Disclaimer 47 | 48 | The `up.sh` script asks you whether you would like to map our Partner ID to the created Service Principal. Feel free to opt-out or remove the marked lines if you don't like to support us. 49 | 50 | > 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. 51 | -------------------------------------------------------------------------------- /oidc/federated_credential.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "-service-connection", 3 | "issuer": "https://vstoken.dev.azure.com/", 4 | "subject": "sc:////", 5 | "description": "Terraform pipeline", 6 | "audiences": [ 7 | "api://AzureADTokenExchange" 8 | ] 9 | } -------------------------------------------------------------------------------- /oidc/resources.bicep: -------------------------------------------------------------------------------- 1 | param sa_name string 2 | param sa_sku string 3 | param sc_name string 4 | param tag string 5 | param location string 6 | 7 | resource tf_sa 'Microsoft.Storage/storageAccounts@2023-01-01' = { 8 | name: sa_name 9 | location: location 10 | tags: { 11 | environment: tag 12 | managedBy: 'tfScaffolding' 13 | } 14 | sku: { 15 | name: sa_sku 16 | } 17 | kind: 'StorageV2' 18 | properties: { 19 | networkAcls: { 20 | bypass: 'AzureServices' 21 | virtualNetworkRules: [] 22 | ipRules: [] 23 | defaultAction: 'Allow' 24 | } 25 | supportsHttpsTrafficOnly: true 26 | minimumTlsVersion: 'TLS1_2' 27 | encryption: { 28 | services: { 29 | file: { 30 | enabled: true 31 | } 32 | blob: { 33 | enabled: true 34 | } 35 | } 36 | keySource: 'Microsoft.Storage' 37 | } 38 | } 39 | } 40 | 41 | resource tf_sb 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { 42 | parent: tf_sa 43 | name: 'default' 44 | properties: { 45 | deleteRetentionPolicy: { 46 | enabled: true 47 | days: 30 48 | allowPermanentDelete: false 49 | } 50 | containerDeleteRetentionPolicy: { 51 | enabled: true 52 | days: 30 53 | allowPermanentDelete: false 54 | } 55 | } 56 | } 57 | 58 | resource tf_sc 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { 59 | parent: tf_sb 60 | name: sc_name 61 | } 62 | -------------------------------------------------------------------------------- /oidc/up.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [string] 5 | $subscriptionId = $env:subscriptionId, 6 | 7 | [Parameter()] 8 | [string] 9 | $tenantId = $env:tenantId 10 | ) 11 | 12 | # Error trapping 13 | trap { 14 | Write-Host "Error on line $($($_.InvocationInfo.ScriptLineNumber)): $($_.Exception.Message)" 15 | exit 1 16 | } 17 | 18 | # If $subscriptionId is not set, try to set it using the az CLI 19 | # If $subscriptionId is still not set after that, throw an error 20 | if (-not $subscriptionId) { 21 | $subscriptionId = az account show --query id -o tsv 22 | if (-not $subscriptionId) { 23 | throw "Failed to obtain subscription ID" 24 | } 25 | } 26 | Write-Host "Subscription ID set to $subscriptionId" 27 | 28 | # If $tenantId is not set, try to set it using the az CLI 29 | # If $tenantId is still not set after that, throw an error 30 | if (-not $tenantId) { 31 | $tenantId = az account show --query homeTenantId -o tsv 32 | if (-not $tenantId) { 33 | throw "Failed to obtain tenant ID" 34 | } 35 | } 36 | Write-Host "Tenant ID set to $tenantId" 37 | 38 | # Load independent variables from .env.powershell file 39 | $envVars = Get-Content .env.powershell | Out-String | ConvertFrom-StringData 40 | 41 | # Declare dependent variables 42 | $spName = "sp-$($envVars['name'])-$($envVars['suffix'])" 43 | $rg = "rg-$($envVars['name'])-$($envVars['suffix'])" 44 | $tag = $envVars['suffix'] 45 | $saName = "stac0$($envVars['name'])0$($envVars['suffix'])" 46 | $scName = "blob0$($envVars['name'])0$($envVars['suffix'])" 47 | 48 | # Set subscription 49 | az account set --subscription "$subscriptionId" 50 | 51 | # Creates resource group 52 | az group create ` 53 | --name $rg ` 54 | --location "$($envVars['location'])" ` 55 | --tags environment="$tag" ` 56 | --subscription "$subscriptionId" 57 | if (-not $?) { 58 | throw "Failed to create resource group" 59 | } 60 | Write-Host "Resource group created..." 61 | 62 | # Creates a service principal if it doesn't exist 63 | # Needs to be owner to create managed identities and assign roles 64 | $sp = az ad sp list --display-name $spName --query "[].displayName" -o tsv 65 | if ($sp -eq $spName) { 66 | Write-Host "Service principal already exists..." 67 | $spId = az ad sp list --display-name $spName --query "[].appId" -o tsv 68 | } 69 | else { 70 | $sp = az ad sp create-for-rbac ` 71 | --name $spName ` 72 | --role "Owner" ` 73 | --scopes "/subscriptions/$subscriptionId" ` 74 | --years 99 | ConvertFrom-Json 75 | Write-Host "Service principal created..." 76 | # Set service principal id variable 77 | $spId = $sp.appId 78 | $parametersPath = "./federated_credential.json" 79 | az ad app federated-credential create --id $spId --parameters $parametersPath 80 | Write-Host "Federated credential created..." 81 | } 82 | 83 | # Add ADD API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All 84 | az ad app permission add ` 85 | --id "$spId" ` 86 | --api 00000003-0000-0000-c000-000000000000 ` 87 | --api-permissions ` 88 | 62a82d76-70ea-41e2-9197-370581804d09=Role ` 89 | dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role ` 90 | df021288-bdef-4463-88db-98f22de89214=Role 91 | if (-not $?) { 92 | throw "Failed to add ADD API permissions" 93 | } 94 | Write-Host "ADD API permissions added..." 95 | 96 | # Update roles 97 | az role assignment create ` 98 | --assignee "$spId" ` 99 | --scope "/subscriptions/$subscriptionId" ` 100 | --role "Monitoring Metrics Publisher" 101 | if (-not $?) { 102 | throw "Failed to update roles" 103 | } 104 | Write-Host "Roles updated..." 105 | 106 | # Get local user 107 | $userId = az ad signed-in-user show --query id -o tsv 108 | if (-not $?) { 109 | throw "Failed to get local user" 110 | } 111 | Write-Host "Local user fetched..." 112 | 113 | # Creates resources 114 | az deployment group create ` 115 | --name "$($envVars['name'])" ` 116 | --resource-group "$rg" ` 117 | --template-file ./resources.bicep ` 118 | --subscription "$subscriptionId" ` 119 | --mode Incremental ` 120 | --parameters ` 121 | sa_name="$saName" ` 122 | sa_sku="$($envVars['saSku'])" ` 123 | sc_name="$scName" ` 124 | tag="$tag" ` 125 | location="$($envVars['location'])" 126 | if (-not $?) { 127 | throw "Failed to create deployment" 128 | } 129 | Write-Host "Deployment created..." 130 | 131 | # Update roles 132 | az role assignment create ` 133 | --assignee "$spId" ` 134 | --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$saName" ` 135 | --role "Storage Blob Data Owner" 136 | if (-not $?) { 137 | throw "Failed to update roles" 138 | } 139 | Write-Host "Roles updated..." 140 | 141 | # Map Partner ID (optional) 142 | Write-Host "---" 143 | $response = Read-Host "Do you like to map our Partner ID? [y/N]" 144 | if ($response -imatch "^(y|yes)$") { 145 | az extension add --name managementpartner 146 | az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" 147 | az managementpartner create --partner-id 3699617 148 | az logout 149 | Write-Host "---" 150 | Write-Host "Please login." 151 | az login 152 | } 153 | -------------------------------------------------------------------------------- /oidc/up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Used to bootstrap infrastructure required by Terraform 3 | 4 | set -e # Exit on error 5 | set -o pipefail # Exit on pipeline failure 6 | 7 | # Check for jq installation 8 | if ! command -v jq >/dev/null; then 9 | echo "Error: jq is not installed." 10 | exit 1 11 | fi 12 | 13 | # Central error handling 14 | error_handler() { 15 | echo "Error on line $1" 16 | exit 1 17 | } 18 | 19 | trap 'error_handler $LINENO' ERR 20 | 21 | # Check and export subscription/tenant if needed 22 | if [[ -z "$subscriptionId" ]]; then 23 | export subscriptionId=$(az account show --query id -o tsv) 24 | [[ -n "$subscriptionId" ]] && echo "Subscription exported..." || exit 1 25 | else 26 | echo "Subscription details are set..." 27 | fi 28 | 29 | if [[ -z "$tenantId" ]]; then 30 | export tenantId=$(az account show --query homeTenantId -o tsv) 31 | [[ -n "$tenantId" ]] && echo "Tenant exported..." || exit 1 32 | else 33 | echo "Tenant details are set..." 34 | fi 35 | 36 | # Sources variables 37 | if [[ -f ".env" ]]; then 38 | source .env 39 | fi 40 | 41 | # Set subscription 42 | az account set --subscription "$subscriptionId" 43 | 44 | # Creates resource group 45 | az group create --name "$rg" \ 46 | --location "$location" \ 47 | --tags environment="$tag" \ 48 | --subscription "$subscriptionId" 49 | echo "Resources group created..." 50 | 51 | # create service principal if not exists already 52 | # Needs to be owner to create managed identities and assign roles 53 | if [[ $(az ad sp list --display-name $spName --query "[].displayName" -o tsv) = "$spName" ]]; then 54 | echo "Service principal already exists..." 55 | export spId=$(az ad sp list --display-name $spName --query "[].appId" -o tsv) 56 | else 57 | export sp=$(az ad sp create-for-rbac \ 58 | --name "$spName" \ 59 | --role="Owner" \ 60 | --scopes="/subscriptions/$subscriptionId") 61 | echo "Service principal created..." 62 | export spSecret=$(echo "$sp" | jq -r '.password') 63 | export spId=$(echo "$sp" | jq -r '.appId') 64 | # Create federated credential 65 | az ad app federated-credential create --id "$spId" --parameters ./federated_credential.json 66 | echo "Federated credential created..." 67 | fi 68 | 69 | # Add API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All 70 | az ad app permission add \ 71 | --id "$spId" \ 72 | --api 00000003-0000-0000-c000-000000000000 \ 73 | --api-permissions \ 74 | 62a82d76-70ea-41e2-9197-370581804d09=Role \ 75 | dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role \ 76 | df021288-bdef-4463-88db-98f22de89214=Role 77 | echo "Service principal authorized..." 78 | 79 | # Update roles 80 | az role assignment create \ 81 | --assignee "$spId" \ 82 | --scope "/subscriptions/$subscriptionId" \ 83 | --role "Monitoring Metrics Publisher" 84 | echo "Service principal role updated..." 85 | 86 | # Creates resources 87 | az deployment group create \ 88 | --name "$name" \ 89 | --resource-group "$rg" \ 90 | --template-file ./resources.bicep \ 91 | --subscription "$subscriptionId" \ 92 | --mode Incremental \ 93 | --parameters "sa_name=$saName" \ 94 | "sa_sku=$saSku" \ 95 | "sc_name=$scName" \ 96 | "tag=$tag" \ 97 | "location=$location" 98 | echo "Deployment created..." 99 | 100 | # Add Storage Blob Data Owner role assignment 101 | az role assignment create \ 102 | --assignee "$spId" \ 103 | --role "Storage Blob Data Owner" \ 104 | --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$saName" 105 | echo "Role for Service Principal created..." 106 | 107 | # Map Partner ID (optional) 108 | echo "---" 109 | read -r -p "Do you like to map our Partner ID? [y/N] " response 110 | if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then 111 | az extension add --name managementpartner 112 | az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" 113 | az managementpartner create --partner-id 3699617 114 | az logout 115 | echo "---" 116 | echo "Please login." 117 | az login 118 | fi -------------------------------------------------------------------------------- /resources.bicep: -------------------------------------------------------------------------------- 1 | param vault_name string 2 | param vault_sku string 3 | param sa_name string 4 | param sa_sku string 5 | param sc_name string 6 | param tenant_id string 7 | param user_id string 8 | param tag string 9 | param location string 10 | param roleId string = 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' // Key Vault Officer 11 | 12 | 13 | resource tf_akv 'Microsoft.KeyVault/vaults@2022-07-01' = { 14 | name: vault_name 15 | location: location 16 | tags: { 17 | environment: tag 18 | } 19 | properties: { 20 | sku: { 21 | family: 'A' 22 | name: vault_sku 23 | } 24 | tenantId: tenant_id 25 | enableRbacAuthorization: true 26 | enabledForDeployment: false 27 | enableSoftDelete: true 28 | softDeleteRetentionInDays: 30 29 | } 30 | } 31 | 32 | resource tf_sa 'Microsoft.Storage/storageAccounts@2022-09-01' = { 33 | name: sa_name 34 | location: location 35 | tags: { 36 | environment: tag 37 | } 38 | sku: { 39 | name: sa_sku 40 | } 41 | kind: 'StorageV2' 42 | properties: { 43 | networkAcls: { 44 | bypass: 'AzureServices' 45 | virtualNetworkRules: [] 46 | ipRules: [] 47 | defaultAction: 'Allow' 48 | } 49 | supportsHttpsTrafficOnly: true 50 | minimumTlsVersion: 'TLS1_2' 51 | encryption: { 52 | services: { 53 | file: { 54 | enabled: true 55 | } 56 | blob: { 57 | enabled: true 58 | } 59 | } 60 | keySource: 'Microsoft.Storage' 61 | } 62 | } 63 | } 64 | 65 | resource tf_sb 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' existing = { 66 | parent: tf_sa 67 | name: 'default' 68 | } 69 | 70 | resource tf_sc 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { 71 | parent: tf_sb 72 | name: sc_name 73 | } 74 | 75 | // Assign Role Key Vault Officer to User 76 | resource keyVaultAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 77 | name: guid(subscription().subscriptionId, tf_akv.name, user_id) 78 | scope: tf_akv 79 | properties: { 80 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) 81 | principalId: user_id 82 | principalType: 'User' 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /up.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [string] 5 | $subscriptionId = $env:subscriptionId, 6 | 7 | [Parameter()] 8 | [string] 9 | $tenantId = $env:tenantId 10 | ) 11 | 12 | # Error trapping 13 | trap { 14 | Write-Host "Error on line $($($_.InvocationInfo.ScriptLineNumber)): $($_.Exception.Message)" 15 | exit 1 16 | } 17 | 18 | # If $subscriptionId is not set, try to set it using the az CLI 19 | # If $subscriptionId is still not set after that, throw an error 20 | if (-not $subscriptionId) { 21 | $subscriptionId = az account show --query id -o tsv 22 | if (-not $subscriptionId) { 23 | throw "Failed to obtain subscription ID" 24 | } 25 | } 26 | Write-Host "Subscription ID set to $subscriptionId" 27 | 28 | # If $tenantId is not set, try to set it using the az CLI 29 | # If $tenantId is still not set after that, throw an error 30 | if (-not $tenantId) { 31 | $tenantId = az account show --query homeTenantId -o tsv 32 | if (-not $tenantId) { 33 | throw "Failed to obtain tenant ID" 34 | } 35 | } 36 | Write-Host "Tenant ID set to $tenantId" 37 | 38 | # Load independent variables from .env.powershell file 39 | $envVars = Get-Content .env.powershell | Out-String | ConvertFrom-StringData 40 | 41 | # Declare dependent variables 42 | $spName = "sp-$($envVars['name'])-$($envVars['suffix'])" 43 | $rg = "rg-$($envVars['name'])-$($envVars['suffix'])" 44 | $tag = $envVars['suffix'] 45 | $saName = "stac0$($envVars['name'])0$($envVars['suffix'])" 46 | $scName = "blob0$($envVars['name'])0$($envVars['suffix'])" 47 | $vaultName = "akv-$($envVars['name'])-$($envVars['suffix'])" 48 | 49 | # Set subscription 50 | az account set --subscription "$subscriptionId" 51 | 52 | # Creates resource group 53 | az group create ` 54 | --name $rg ` 55 | --location "$($envVars['location'])" ` 56 | --tags environment="$tag" ` 57 | --subscription "$subscriptionId" 58 | if (-not $?) { 59 | throw "Failed to create resource group" 60 | } 61 | Write-Host "Resource group created..." 62 | 63 | # Creates a service principal if it doesn't exist 64 | # Needs to be owner to create managed identities and assign roles 65 | $sp = az ad sp list --display-name $spName --query "[].displayName" -o tsv 66 | if ($sp -eq $spName) { 67 | Write-Host "Service principal already exists..." 68 | $spId = az ad sp list --display-name $spName --query "[].appId" -o tsv 69 | } 70 | else { 71 | $sp = az ad sp create-for-rbac ` 72 | --name $spName ` 73 | --role "Owner" ` 74 | --scopes "/subscriptions/$subscriptionId" ` 75 | --years 99 | ConvertFrom-Json 76 | Write-Host "Service principal created..." 77 | # Set service principal id and secret variables 78 | $spSecret = $sp.password 79 | $spId = $sp.appId 80 | } 81 | 82 | # Add ADD API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All 83 | az ad app permission add ` 84 | --id "$spId" ` 85 | --api 00000003-0000-0000-c000-000000000000 ` 86 | --api-permissions ` 87 | 62a82d76-70ea-41e2-9197-370581804d09=Role ` 88 | dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role ` 89 | df021288-bdef-4463-88db-98f22de89214=Role 90 | if (-not $?) { 91 | throw "Failed to add ADD API permissions" 92 | } 93 | Write-Host "ADD API permissions added..." 94 | 95 | # Update roles 96 | az role assignment create ` 97 | --assignee "$spId" ` 98 | --scope "/subscriptions/$subscriptionId" ` 99 | --role "Monitoring Metrics Publisher" 100 | if (-not $?) { 101 | throw "Failed to update roles" 102 | } 103 | Write-Host "Roles updated..." 104 | 105 | # Get local user 106 | $userId = az ad signed-in-user show --query id -o tsv 107 | if (-not $?) { 108 | throw "Failed to get local user" 109 | } 110 | Write-Host "Local user fetched..." 111 | 112 | # Creates resources 113 | az deployment group create ` 114 | --name "$($envVars['name'])" ` 115 | --resource-group "$rg" ` 116 | --template-file ./resources.bicep ` 117 | --subscription "$subscriptionId" ` 118 | --mode Incremental ` 119 | --parameters ` 120 | vault_name="$vaultName" ` 121 | vault_sku="$($envVars['vaultSku'])" ` 122 | sa_name="$saName" ` 123 | sa_sku="$($envVars['saSku'])" ` 124 | sc_name="$scName" ` 125 | tenant_id="$tenantId" ` 126 | user_id="$userId" ` 127 | tag="$tag" ` 128 | location="$($envVars['location'])" 129 | if (-not $?) { 130 | throw "Failed to create deployment" 131 | } 132 | Write-Host "Deployment created..." 133 | 134 | # Gets storage account key 135 | $saKey = az storage account keys list ` 136 | --subscription="$subscriptionId" ` 137 | --resource-group "$rg" ` 138 | --account-name "$saName" ` 139 | --query [0].value ` 140 | -o tsv 141 | if (-not $?) { 142 | throw "Failed to get storage account key" 143 | } 144 | Write-Host "Storage account key fetched..." 145 | 146 | # Save storage account details to vault 147 | az keyvault secret set --vault-name "$vaultName" ` 148 | --name "sa-key" ` 149 | --value "$saKey" 150 | az keyvault secret set --vault-name "$vaultName" ` 151 | --name "sa-name" ` 152 | --value "$saName" 153 | az keyvault secret set --vault-name "$vaultName" ` 154 | --name "sc-name" ` 155 | --value "$scName" 156 | if (-not $?) { 157 | throw "Failed to save storage account details to vault" 158 | } 159 | Write-Host "Storage account details saved to vault..." 160 | 161 | # Save service principal details to vault 162 | az keyvault secret set --vault-name "$vaultName" ` 163 | --name "sp-id" ` 164 | --value "$spId" 165 | 166 | # Save subscription id to vault 167 | az keyvault secret set --vault-name "$vaultName" ` 168 | --name "subscription-id" ` 169 | --value "$subscriptionId" 170 | 171 | # Check if secret already exists and if not, set it 172 | $secretList = az keyvault secret list --vault-name $vaultName --query "[].name" -o tsv 173 | if ($secretList -match "sp-secret") { 174 | Write-Host "SP secret already exists..." 175 | } 176 | elseif ([string]::IsNullOrEmpty($spSecret)) { 177 | Write-Host "spSecret is not set. Please enter the value:" 178 | $spSecret = Read-Host 179 | az keyvault secret set --vault-name $vaultName ` 180 | --name "sp-secret" ` 181 | --value $spSecret 182 | Write-Host "Secrets are saved in vault..." 183 | } 184 | else { 185 | az keyvault secret set --vault-name $vaultName ` 186 | --name "sp-secret" ` 187 | --value $spSecret 188 | Write-Host "Secrets are saved in vault..." 189 | } 190 | Write-Host "Service principal details saved to vault..." 191 | 192 | # Add vault access 193 | az role assignment create ` 194 | --assignee "$spId" ` 195 | --role "Key Vault Secrets Officer" ` 196 | --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.KeyVault/vaults/$vaultName" 197 | if (-not $?) { 198 | throw "Failed to create role assignment" 199 | } 200 | Write-Host "Role assignment created" 201 | 202 | # Map Partner ID (optional) 203 | Write-Host "---" 204 | $response = Read-Host "Do you like to map our Partner ID? [y/N]" 205 | if ($response -imatch "^(y|yes)$") { 206 | az extension add --name managementpartner 207 | az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" 208 | az managementpartner create --partner-id 3699617 209 | az logout 210 | Write-Host "---" 211 | Write-Host "Please login." 212 | az login 213 | } 214 | -------------------------------------------------------------------------------- /up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Used to bootstrap infrastructure required by Terraform 3 | 4 | set -e # Exit on error 5 | set -o pipefail # Exit on pipeline failure 6 | 7 | # Check for jq installation 8 | if ! command -v jq >/dev/null; then 9 | echo "Error: jq is not installed." 10 | exit 1 11 | fi 12 | 13 | # Central error handling 14 | error_handler() { 15 | echo "Error on line $1" 16 | exit 1 17 | } 18 | 19 | trap 'error_handler $LINENO' ERR 20 | 21 | # Check and export subscription/tenant if needed 22 | if [[ -z "$subscriptionId" ]]; then 23 | export subscriptionId=$(az account show --query id -o tsv) 24 | [[ -n "$subscriptionId" ]] && echo "Subscription exported..." || exit 1 25 | else 26 | echo "Subscription details are set..." 27 | fi 28 | 29 | if [[ -z "$tenantId" ]]; then 30 | export tenantId=$(az account show --query homeTenantId -o tsv) 31 | [[ -n "$tenantId" ]] && echo "Tenant exported..." || exit 1 32 | else 33 | echo "Tenant details are set..." 34 | fi 35 | 36 | # Sources variables 37 | if [[ -f ".env" ]]; then 38 | source .env 39 | fi 40 | 41 | # Set subscription 42 | az account set --subscription "$subscriptionId" 43 | 44 | # Creates resource group 45 | az group create --name "$rg" \ 46 | --location "$location" \ 47 | --tags environment="$tag" \ 48 | --subscription "$subscriptionId" 49 | echo "Resources group created..." 50 | 51 | # create service principal if not exists already 52 | # Needs to be owner to create managed identities and assign roles 53 | if [[ $(az ad sp list --display-name $spName --query "[].displayName" -o tsv) = "$spName" ]]; then 54 | echo "Service principal already exists..." 55 | export spId=$(az ad sp list --display-name $spName --query "[].appId" -o tsv) 56 | else 57 | export sp=$(az ad sp create-for-rbac \ 58 | --name "$spName" \ 59 | --role="Owner" \ 60 | --scopes="/subscriptions/$subscriptionId" \ 61 | --years 99) 62 | echo "Service principal created..." 63 | # Set service principal id and secret variables 64 | export spSecret=$(echo "$sp" | jq -r '.password') 65 | export spId=$(echo "$sp" | jq -r '.appId') 66 | fi 67 | 68 | # Add ADD API permissions - Group.ReadWrite.All, GroupMember.ReadWrite.All, User.Read.All 69 | az ad app permission add \ 70 | --id "$spId" \ 71 | --api 00000003-0000-0000-c000-000000000000 \ 72 | --api-permissions \ 73 | 62a82d76-70ea-41e2-9197-370581804d09=Role \ 74 | dbaae8cf-10b5-4b86-a4a1-f871c94c6695=Role \ 75 | df021288-bdef-4463-88db-98f22de89214=Role 76 | echo "Service principal authorized..." 77 | 78 | # Update roles 79 | az role assignment create \ 80 | --assignee "$spId" \ 81 | --scope "/subscriptions/$subscriptionId" \ 82 | --role "Monitoring Metrics Publisher" 83 | echo "Service principal role updated..." 84 | 85 | # Get local user 86 | export userId=$(az ad signed-in-user show --query id -o tsv) 87 | echo "Local user fetched..." 88 | 89 | # Creates resources 90 | az deployment group create \ 91 | --name "$name" \ 92 | --resource-group "$rg" \ 93 | --template-file ./resources.bicep \ 94 | --subscription "$subscriptionId" \ 95 | --mode Incremental \ 96 | --parameters "vault_name=$vaultName" \ 97 | "vault_sku=$vaultSku" \ 98 | "sa_name=$saName" \ 99 | "sa_sku=$saSku" \ 100 | "sc_name=$scName" \ 101 | "tenant_id=$tenantId" \ 102 | "user_id=$userId" \ 103 | "tag=$tag" \ 104 | "location=$location" 105 | echo "Deployment created..." 106 | 107 | # Gets storage account key 108 | export saKey=$(az storage account keys list \ 109 | --subscription="$subscriptionId" \ 110 | --resource-group "$rg" \ 111 | --account-name "$saName" \ 112 | --query '[0].value' -o tsv) 113 | echo "Storage container created..." 114 | 115 | # Saves storage account details to vault 116 | az keyvault secret set --vault-name "$vaultName" \ 117 | --name "sa-key" \ 118 | --value "$saKey" 119 | az keyvault secret set --vault-name "$vaultName" \ 120 | --name "sa-name" \ 121 | --value "$saName" 122 | az keyvault secret set --vault-name "$vaultName" \ 123 | --name "sc-name" \ 124 | --value "$scName" 125 | echo "Secrets are saved in vault..." 126 | 127 | # Save service principal details to vault 128 | az keyvault secret set --vault-name "$vaultName" \ 129 | --name "sp-id" \ 130 | --value "$spId" 131 | 132 | # Save subscription id to vault 133 | az keyvault secret set --vault-name "$vaultName" \ 134 | --name "subscription-id" \ 135 | --value "$subscriptionId" 136 | 137 | # checks if a password secret already exists and only sets secret value if password doesn't exist 138 | if [ -n "$(az keyvault secret list --vault-name "$vaultName" --query "[].name" | grep "sp-secret")" ] 139 | then 140 | echo "SP secret already exists..." 141 | elif [ -z "$spSecret" ]; then 142 | echo "spSecret is not set. Please enter the value:" 143 | read spSecret 144 | az keyvault secret set --vault-name "$vaultName" \ 145 | --name "sp-secret" \ 146 | --value "$spSecret" 147 | echo "Secrets are saved in vault..." 148 | elif [ -n "$spSecret" ]; then 149 | az keyvault secret set --vault-name "$vaultName" \ 150 | --name "sp-secret" \ 151 | --value "$spSecret" 152 | echo "Secrets are saved in vault..." 153 | fi 154 | 155 | # Add vault access 156 | az role assignment create \ 157 | --assignee "$spId" \ 158 | --role "Key Vault Secrets Officer" \ 159 | --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.KeyVault/vaults/$vaultName" 160 | echo "Role for Service Principal set" 161 | 162 | # Map Partner ID (optional) 163 | echo "---" 164 | read -r -p "Do you like to map our Partner ID? [y/N] " response 165 | if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then 166 | az extension add --name managementpartner 167 | az login --tenant "$tenantId" --service-principal -u "$spId" -p "$spSecret" 168 | az managementpartner create --partner-id 3699617 169 | az logout 170 | echo "---" 171 | echo "Please login." 172 | az login 173 | fi 174 | --------------------------------------------------------------------------------