[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": "# Local .terraform directories\n**/.terraform/*\n\n# .tfstate files\n*.tfstate\n*.tfstate.*\n\n# Crash log files\ncrash.log\n\n# Ignore any .tfvars files that are generated automatically for each Terraform run. Most\n# .tfvars files are managed as part of configuration and so should be included in\n# version control.\n#\n# example.tfvars\n\n# Ignore override files as they are usually used to override resources locally and so\n# are not checked in\noverride.tf\noverride.tf.json\n*_override.tf\n*_override.tf.json\n\n# Include override files you do wish to add to version control using negated pattern\n#\n# !example_override.tf\n\n# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan\n# example: *tfplan*\n"
  },
  {
    "path": "101-terraform-job/README.md",
    "content": "# Basic Terraform job\n\n## About this template\n\nThis template includes a multi-stage pipeline that deploys \nan environment from Terraform configuration, and run\na subsequent job configured from Terraform outputs.\n\n[![Watch the video](/docs/images/terraform_starter/201-video.png)](https://youtu.be/GpKdCuv2icY)\n\nThe Terraform definition only deploys an empty resource group.\nYou can extend the definition with your custom infrastructure, such as Web Apps.\n\n## Walkthrough\n\n### Using the template\n\nTo use the template, follow the section\n[How to use the templates](/README.md#how-to-use-the-templates)\nin the main README file.\n\n## Next steps\n\n* The next template, [201-plan-apply-stages](../201-plan-apply-stages) demonstrates\n  how to manually review and approve changes before they are applied on an environment.\n  It also shows you can structure your project to develop and test locally without an Azure\n  backend.\n"
  },
  {
    "path": "101-terraform-job/azure-pipelines.yml",
    "content": "pr: none\ntrigger:\n  branches:\n    include:\n    - master\n  paths:\n    include:\n    - 101-terraform-job/\n\nvariables:\n- group: terraform-secrets\n\nstages:\n\n- template: terraform-stages-template.yml\n  parameters:\n    environment: test\n    environmentDisplayName: Test\n    # Pass variables as environment variables.\n    # Terraform recognizes TF_VAR prefixed environment variables.\n    TerraformEnvVariables:\n      TF_VAR_department: engineering\n"
  },
  {
    "path": "101-terraform-job/terraform/backend.tf",
    "content": "#Set the terraform backend\nterraform {\n  # Backend variables are initialized by Azure DevOps\n  backend \"azurerm\" {}\n}\n"
  },
  {
    "path": "101-terraform-job/terraform/main.tf",
    "content": "# Deploy a Resource Group with Azure resources.\n#\n# For suggested naming conventions, refer to:\n#   https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/naming-and-tagging\n\n# Sample Resource Group\n\nresource \"azurerm_resource_group\" \"main\" {\n  name     = \"rg-${var.appname}-${var.environment}-main\"\n  location = var.location\n  tags     = {\n    department = var.department\n  }\n}\n\n# Add additional modules...\n"
  },
  {
    "path": "101-terraform-job/terraform/outputs.tf",
    "content": "output \"subscription_id\" {\n  value = data.azurerm_client_config.current.subscription_id\n}\n\noutput \"resource_group_name\" {\n  value = azurerm_resource_group.main.name\n}\n"
  },
  {
    "path": "101-terraform-job/terraform/provider.tf",
    "content": "#Set the terraform required version\nterraform {\n  required_version = \">= 0.12.6\"\n}\n\n# Configure the Azure Provider\nprovider \"azurerm\" {\n  # It is recommended to pin to a given version of the Provider\n  version = \"=1.44.0\"\n}\n\n# Data\n\n# Make client_id, tenant_id, subscription_id and object_id variables\ndata \"azurerm_client_config\" \"current\" {}\n"
  },
  {
    "path": "101-terraform-job/terraform/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n  description = \"Application name. Use only lowercase letters and numbers\"\n  default = \"starterterraform\"\n}\n\nvariable \"environment\" {\n  type    = string\n  description = \"Environment name, e.g. 'dev' or 'stage'\"\n  default = \"dev\"\n}\n\nvariable \"location\" {\n  type    = string\n  description = \"Azure region where to create resources.\"\n  default = \"North Europe\"\n}\n\nvariable \"department\" {\n  type    = string\n  description = \"A sample variable passed from the build pipeline and used to tag resources.\"\n  default = \"Engineering\"\n}\n"
  },
  {
    "path": "101-terraform-job/terraform-stages-template.yml",
    "content": "parameters:\n  environment: test\n  environmentDisplayName: Test\n  TerraformArguments: ''\n  TerraformEnvVariables:\n\nstages:\n- stage: Terraform_${{ parameters.environment }}\n  displayName: Terraform ${{ parameters.environmentDisplayName }}\n  pool:\n    vmImage: ubuntu-latest\n  jobs:\n\n  - job: Terraform\n    displayName: Terraform\n    # Avoid concurrent Terraform runs on PRs, which would result in failures due to exclusive lock on remote state file.\n    condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), variables['RUN_FLAG_TERRAFORM']))\n    steps:\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        provisionStorage: true\n        TerraformDirectory: 101-terraform-job/terraform\n        environment: ${{ parameters.environment }}\n\n    # Using bash instead of Terraform extension because of following issues:\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/748\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/725\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/747\n    - bash: |\n        set -eu\n        export ARM_CLIENT_SECRET=$(ARM_CLIENT_SECRET)\n        terraform apply -input=false -auto-approve -var environment=${{ parameters.environment }} ${{ parameters.TerraformArguments }}\n      displayName: Terraform apply\n      workingDirectory: 101-terraform-job/terraform\n      env:\n        ${{ parameters.TerraformEnvVariables }}\n\n- stage: PostTerraform_${{ parameters.environment }}\n  displayName: PostTerraform ${{ parameters.environmentDisplayName }}\n  pool:\n    vmImage: ubuntu-latest\n  jobs:\n  - job: ReadTerraform\n    displayName: Use Terraform outputs\n    steps:\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        TerraformDirectory: 101-terraform-job/terraform\n        environment: ${{ parameters.environment }}\n\n    - template: ../infrastructure/terraform-outputs-template.yml\n      parameters:\n        TerraformDirectory: 101-terraform-job/terraform\n\n    - bash: |\n        # Dummy job showing how to consume Terraform outputs\n        echo Subscription ID: $(subscription_id)\n        echo Resource group: $(resource_group_name)\n      displayName: Sample script\n"
  },
  {
    "path": "201-plan-apply-stages/README.md",
    "content": "# Separate Plan and Apply stages\n\n## About this template\n\nThis template includes a multi-stage pipeline allowing to manually review and approve infrastructure\nchanges before they are deployed.\n\n[![Watch the video](/docs/images/terraform_starter/201-video.png)](https://youtu.be/PvXonOWOWaM)\n\nThe Terraform definition only deploys a resource group and two empty SQL Server instances\n(to illustrate two different approaches to managing secrets, in this case the SQL Server\npassword).\nYou can extend the definition with your custom infrastructure, such as Web Apps.\n\nThe project can be used in local development without a remote Terraform state backend.\nThis allows quickly iterating while developing the Terraform configuration, and \ngood security practices.\n\nWhen the project is run in Azure DevOps, however, the pipeline adds the\n`infrastructure/terraform_backend/backend.tf` to the `infrastructure/terraform` \ndirectory to enable the Azure Storage shared backend for additional resiliency.\nSee the Terraform documentation to understand [why a state store is needed](https://www.terraform.io/docs/state/purpose.html).\n\n## Walkthrough\n\n### Using the template\n\nTo use the template, follow the section\n[How to use the templates](/README.md#how-to-use-the-templates)\nin the main README file.\n\n### Manual approvals\n\nAs of December 2019, there is no support for stage gates in Azure DevOps multi-stage pipelines, but\n*deployment environments* provide a basic mechanism for stage approvals.\n\nCreate an environment with no resources. Name it `Staging`.\n\n![create environment](/docs/images/terraform_starter/create_environment.png)\n\nDefine environment approvals. If you want to allow anyone out of a group a people to be able to individually approve, add a group.\n\n![create environment_approval1](/docs/images/terraform_starter/create_environment_approval1.png)\n\n![create environment approval2](/docs/images/terraform_starter/create_environment_approval2.png)\n\n![create environment approval3](/docs/images/terraform_starter/create_environment_approval3.png)\n\n![environment approval](/docs/images/terraform_starter/environment_approval.png)\n\nRepeat those steps for an environment named `QA`.\n\nUnder Library, create a Variable Group named `terraform-secrets`. Create a secret\nnamed `SQL_PASSWORD` and give it a unique value (e.g. `Strong_Passw0rd!`). Make\nthe variable secret using the padlock icon.\n\n![environment approval](/docs/images/terraform_starter/variable_group.png)\n\n### Running the pipeline\n\nAs you run the pipeline, after running `terraform plan`, the next stage will be waiting for your approval.\n\n![pipeline stage waiting](/docs/images/terraform_starter/pipeline_stage_waiting.png)\n\nReview the detailed plan to ensure no critical resources or data will be lost.\n\n![terraform plan output](/docs/images/terraform_starter/terraform_plan_output.png)\n\nYou can also review the plan and terraform configuration files by navigating to Pipeline Artifacts (rightmost column in the table below).\n\n![pipeline artifacts](/docs/images/terraform_starter/pipeline_artifacts.png)\n\n![pipeline artifacts detail](/docs/images/terraform_starter/pipeline_artifacts_detail.png)\n\nApprove or reject the deployment.\n\n![stage approval waiting](/docs/images/terraform_starter/stage_approval_waiting.png)\n\nThe pipeline will proceed to `terraform apply`.\n\nAt this stage you will have a new resource group deployed named `rg-starterterraform-stage-main`. \n\nThe pipeline will then proceed in the same manner for the `QA` environment.\n\n![pipeline completed](/docs/images/terraform_starter/pipeline_completed.png)\n\nIf any changes have been performed on the infrastructure between the Plan and Apply stages, the pipeline will fail.\nYou can rerun the Plan stage directly in the pipeline view to produce an updated plan.\n\n![plan changed](/docs/images/terraform_starter/plan_changed.png)\n\n## Next steps\n\n* It's not currently possible to skip approval and deployment if there are no\n  changes in the Terraform plan, because of limitations in multi-stage\n  pipelines (stages cannot be conditioned on the outputs of previous stages).\n  You could cancel the pipeline (through the REST API) in that case, but that\n  would prevent extending the pipeline to include activities beyond Terraform.\n* The next template, [301-deploy-agent-vms](../301-deploy-agent-vms) demonstrates\n  how you can use Terraform to manage infrastructure used for the build itself,\n  such as build agent VMs.\n"
  },
  {
    "path": "201-plan-apply-stages/azure-pipelines.yml",
    "content": "pr: none\ntrigger:\n  branches:\n    include:\n    - master\n  paths:\n    include:\n    - 201-plan-apply-stages/\n\nvariables:\n- group: terraform-secrets\n\nstages:\n- template: terraform-stages-template.yml\n  parameters:\n    environment: stage\n    environmentDisplayName: Staging\n    TerraformArguments: >-\n      -var department=Engineering\n    # For additional security, pass secret through environment instead of command line.\n    # Terraform recognizes TF_VAR prefixed environment variables.\n    TerraformEnvVariables:\n      TF_VAR_sql2password: $(SQL_PASSWORD)\n\n- template: terraform-stages-template.yml\n  parameters:\n    environment: qa\n    environmentDisplayName: QA\n    TerraformArguments: >-\n      -var department=QA\n    TerraformEnvVariables:\n      TF_VAR_sql2password: $(SQL_PASSWORD)\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/main.tf",
    "content": "# Deploy a Resource Group with Azure resources.\n#\n# For suggested naming conventions, refer to:\n#   https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/naming-and-tagging\n\n# Sample Resource Group\n\nresource \"azurerm_resource_group\" \"main\" {\n  name     = \"rg-${var.appname}-${var.environment}-main\"\n  location = var.location\n  tags     = {\n    department = var.department\n  }\n}\n\n# Sample Resources\n\nmodule \"sqlserver1_generated_password\" {\n  source = \"./sqlserver1_generated_password\"\n  appname = var.appname\n  environment = var.environment\n  resource_group = azurerm_resource_group.main.name\n  location = azurerm_resource_group.main.location\n}\n\nmodule \"sqlserver2_assigned_password\" {\n  source = \"./sqlserver2_assigned_password\"\n  appname = var.appname\n  environment = var.environment\n  resource_group = azurerm_resource_group.main.name\n  location = azurerm_resource_group.main.location\n  sql_password = var.sql2password\n}\n\n# Add additional modules...\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/outputs.tf",
    "content": "output \"subscription_id\" {\n  value = data.azurerm_client_config.current.subscription_id\n}\n\noutput \"sqlserver1_host\" {\n  value = module.sqlserver1_generated_password.fully_qualified_domain_name\n}\n\noutput \"sqlserver1_user\" {\n  value = module.sqlserver1_generated_password.user\n}\n\noutput \"sqlserver1_password\" {\n  value = module.sqlserver1_generated_password.password\n  sensitive = true\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/provider.tf",
    "content": "#Set the terraform required version\nterraform {\n  required_version = \">= 0.12.6\"\n}\n\n# Configure the Azure Provider\nprovider \"azurerm\" {\n  # It is recommended to pin to a given version of the Provider\n  version = \"=1.44.0\"\n}\n\nprovider \"random\" {\n  version = \"~> 2.2\"\n}\n\n# Data\n\n# Make client_id, tenant_id, subscription_id and object_id variables\ndata \"azurerm_client_config\" \"current\" {}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/sqlserver1_generated_password/main.tf",
    "content": "resource \"random_password\" \"sql\" {\n  length = 16\n  special = true\n  override_special = \"!@#$%&*()-_=+[]:?\"\n  min_upper = 1\n  min_lower = 1\n  min_numeric = 1\n  min_special = 1\n}\n\nresource \"azurerm_sql_server\" \"example\" {\n  name                         = \"sqldb-${var.appname}-${var.environment}\"\n  resource_group_name          = var.resource_group\n  location                     = var.location\n  version                      = \"12.0\"\n  administrator_login          = \"sqladm\"\n  administrator_login_password = random_password.sql.result\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/sqlserver1_generated_password/outputs.tf",
    "content": "output \"fully_qualified_domain_name\" {\n  value = azurerm_sql_server.example.fully_qualified_domain_name\n}\n\noutput \"user\" {\n  value = azurerm_sql_server.example.administrator_login\n}\n\noutput \"password\" {\n  value = azurerm_sql_server.example.administrator_login_password\n  sensitive = true\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/sqlserver1_generated_password/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type    = string\n}\n\nvariable \"resource_group\" {\n  type    = string\n}\n\nvariable \"location\" {\n  type    = string\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/sqlserver2_assigned_password/main.tf",
    "content": "resource \"azurerm_sql_server\" \"example\" {\n  name                         = \"sqldb-${var.appname}-2-${var.environment}\"\n  resource_group_name          = var.resource_group\n  location                     = var.location\n  version                      = \"12.0\"\n  administrator_login          = \"sqladm\"\n  administrator_login_password = var.sql_password\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/sqlserver2_assigned_password/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type    = string\n}\n\nvariable \"resource_group\" {\n  type    = string\n}\n\nvariable \"location\" {\n  type    = string\n}\n\nvariable \"sql_password\" {\n  type    = string\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n  description = \"Application name. Use only lowercase letters and numbers\"\n  default = \"starterterraform\"\n}\n\nvariable \"environment\" {\n  type    = string\n  description = \"Environment name, e.g. 'dev' or 'stage'\"\n  default = \"dev\"\n}\n\nvariable \"location\" {\n  type    = string\n  description = \"Azure region where to create resources.\"\n  default = \"North Europe\"\n}\n\nvariable \"department\" {\n  type    = string\n  description = \"A sample variable passed from the build pipeline and used to tag resources.\"\n  default = \"Engineering\"\n}\n\nvariable \"sql2password\" {\n  type    = string\n  description = \"A password for SQL Server #2\"\n}\n"
  },
  {
    "path": "201-plan-apply-stages/terraform-stages-template.yml",
    "content": "parameters:\n  environment: stage\n  environmentDisplayName: Staging\n  TerraformArguments: ''\n  TerraformEnvVariables:\n\nstages:\n- stage: Terraform_Plan_${{ parameters.environment }}\n  displayName: Plan ${{ parameters.environmentDisplayName }}\n  jobs:\n  - job: Terraform_Plan\n    displayName: Plan Terraform\n    # Avoid concurrent Terraform runs on PRs, which would result in failures due to exclusive lock on remote state file.\n    condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), variables['RUN_FLAG_TERRAFORM']))\n    pool:\n      vmImage: ubuntu-latest\n    steps:\n\n    - bash: |\n        cp terraform_backend/* terraform\n      displayName: Configure backend\n      workingDirectory: 201-plan-apply-stages\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        provisionStorage: true\n        TerraformDirectory: 201-plan-apply-stages/terraform\n        environment: ${{ parameters.environment }}\n\n    # Using bash instead of Terraform extension because of following issues:\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/748\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/725\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/747\n    - bash: |\n        set -eu\n        export ARM_CLIENT_SECRET=$(ARM_CLIENT_SECRET)\n        terraform plan -input=false -out=tfplan -var environment=${{ parameters.environment }} ${{ parameters.TerraformArguments }}\n      displayName: Terraform plan\n      workingDirectory: 201-plan-apply-stages/terraform\n      env:\n        ${{ parameters.TerraformEnvVariables }}\n\n    - bash: |\n        # Save a human-friendly version of the plan with passwords hidden\n        terraform show -no-color tfplan > plan.txt\n        # Remove terraform plan from published artifacts, as it contains clear-text secrets\n        rm tfplan\n        # Resource providers can be > 100MB large, we don't want them in the published artifacts.\n        rm -r .terraform\n      displayName: Save plan text\n      workingDirectory: 201-plan-apply-stages/terraform\n\n    - task: PublishPipelineArtifact@1\n      displayName: Publish plan artifact\n      inputs:\n        targetPath: 201-plan-apply-stages/terraform\n        artifact: terraform_resources_${{ parameters.environment }}\n\n- stage: Terraform_Apply_${{ parameters.environment }}\n  displayName: Apply ${{ parameters.environmentDisplayName }}\n  jobs:\n  - deployment: Apply\n    environment: ${{ parameters.environmentDisplayName }}\n    displayName: Apply Terraform\n    condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), variables['RUN_FLAG_TERRAFORM']))\n    pool:\n      vmImage: ubuntu-latest\n    strategy:\n      runOnce:\n        deploy:\n          steps:\n\n          - task: DownloadPipelineArtifact@2\n            displayName: Download plan\n            inputs:\n              artifactName: terraform_resources_${{ parameters.environment }}\n              targetPath: terraform_resources\n\n          - template: ../infrastructure/terraform-init-template.yml\n            parameters:\n              TerraformDirectory: terraform_resources\n              environment: ${{ parameters.environment }}\n\n          # As the Terraform extension plan task doesn't support -detailed-exitcode\n          # (to check if any changes are present), we define an equivalent bash\n          # task instead.\n          - bash: |\n              set -eu\n              export ARM_CLIENT_SECRET=$(ARM_CLIENT_SECRET)\n              # terraform plan -detailed-exitcode exit codes:\n              # 0 - Succeeded, diff is empty (no changes)\n              # 1 - Errored\n              # 2 - Succeeded, there is a diff\n              # >2 - unexpected, crash or bug\n              if terraform plan -detailed-exitcode -input=false -out=tfplan -var environment=${{ parameters.environment }} ${{ parameters.TerraformArguments }}; then\n                echo \"Terraform succeeded with no changes\"\n                # NB terraform apply should still be run, e.g. if new outputs have been created\n              else\n                terraform_exitcode=$?\n                if [ $terraform_exitcode -eq 2 ]; then\n                  echo \"Terraform succeeded with updates\"\n                else\n                  echo \"ERROR: terraform exited with code $terraform_exitcode\"\n                  exit 1\n                fi\n              fi\n            displayName: Terraform plan\n            workingDirectory: terraform_resources\n            env:\n              ${{ parameters.TerraformEnvVariables }}\n\n          - bash: |\n              set -eux  # ensure pipeline stops if terraform fails or diff reports a difference\n              terraform show -no-color tfplan > newplan.txt\n              diff -u plan.txt newplan.txt\n            workingDirectory: terraform_resources\n            displayName: Check unchanged plan\n\n          - bash: |\n              set -eu\n              terraform apply -input=false -auto-approve tfplan\n            displayName: Terraform apply\n            workingDirectory: terraform_resources\n            env:\n              ARM_CLIENT_SECRET: $(ARM_CLIENT_SECRET)\n\n  - job: ReadTerraform\n    dependsOn: Apply\n    condition: always()\n    displayName: Read outputs\n    pool:\n      vmImage: ubuntu-latest\n    steps:\n\n    - bash: |\n        cp terraform_backend/* terraform\n      displayName: Configure backend\n      workingDirectory: 201-plan-apply-stages\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        TerraformDirectory: 201-plan-apply-stages/terraform\n        environment: ${{ parameters.environment }}\n\n    - template: ../infrastructure/terraform-outputs-template.yml\n      parameters:\n        TerraformDirectory: 201-plan-apply-stages/terraform\n \n  - job: DummySampleJob\n    displayName: Use Terraform outputs\n    dependsOn: ReadTerraform\n    variables:\n      sqlserver1_host: $[ dependencies.ReadTerraform.outputs['Outputs.sqlserver1_host'] ]\n      sqlserver1_user: $[ dependencies.ReadTerraform.outputs['Outputs.sqlserver1_user'] ]\n      sqlserver1_password: $[ dependencies.ReadTerraform.outputs['Outputs.sqlserver1_password'] ]\n    steps:\n      - bash: |\n          # Dummy job showing how to consume Terraform outputs\n          echo DB_CONN_STRING=\"User ID=$(sqlserver1_user);Password=$(sqlserver1_password)\"\n        displayName: Sample script\n"
  },
  {
    "path": "201-plan-apply-stages/terraform_backend/backend.tf",
    "content": "#Set the terraform backend\nterraform {\n  # Backend variables are initialized by Azure DevOps\n  backend \"azurerm\" {}\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/README.md",
    "content": "# Deploy hosted agent VMs\n\n## About this template\n\nThis template shows how to use Terraform to deploy a pool of agent VMs on which a subsequent job is run.\n\n[![Watch the video](/docs/images/terraform_starter/301-video.png)](https://youtu.be/TdXmqqJE-gw)\n\nThe Terraform definition does not contain any other resources.\nYou can extend the definition with your custom infrastructure, such as Web Apps.\n\n### Agent deployment scripts\n\nThe template deploys agents at a high density, with (by default) 2 agent VMs with 4 Azure DevOps build agents per VM.\nThis is efficient when running pipelines that do not require much local computing power, or only sporadically.\n\n## Walkthrough\n\n### Creating an agent pool\n\nIn your Azure DevOps project settings, [create an Agent pool](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues).\nName the pool `starterpool` (if you want to use a different name, change the value in [azure-pipelines.yml](azure-pipelines.yml)).\n\n### Creating a PAT token\n\nIn Azure DevOps, [create a PAT token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page).\nClick on *Show all scopes* and grant the token *Read and Manage* permissions on *Agent Pools*.\n\n![PAT token](/docs/images/terraform_starter/301-pat-token.png)\n\nUnder Library, create a Variable Group named `terraform-secrets`. Create a secret\nnamed `AGENT_POOL_MANAGE_PAT_TOKEN` and paste the token value.\nMake the variable secret using the padlock icon.\n\n### Using the template\n\nTo use the template, follow the section\n[How to use the templates](/README.md#how-to-use-the-templates)\nin the main README file.\n\n### Automatic shutdown of agents\n\nThe pipeline configures the agent VMs to automatically shutdown daily at 23:00 UTC.\nTo use a different schedule, change `TF_VAR_az_devops_agent_vm_shutdown_time`\nin [azure-pipelines.yml](azure-pipelines.yml),\nor remove that line completely to disable automatic shutdown.\n\nThe pipeline contains a task to start up the agent VMs again before running the agent job.\n"
  },
  {
    "path": "301-deploy-agent-vms/azure-pipelines.yml",
    "content": "pr: none\ntrigger:\n  branches:\n    include:\n    - master\n  paths:\n    include:\n    - 301-deploy-agent-vms/\n\nvariables:\n- group: terraform-secrets\n\nstages:\n\n- template: terraform-stages-template.yml\n  parameters:\n    environment: dev\n    environmentDisplayName: Dev\n    # Pass variables as environment variables.\n    # Terraform recognizes TF_VAR prefixed environment variables.\n    TerraformEnvVariables:\n      TF_VAR_az_devops_url: $(System.TeamFoundationCollectionUri)\n      TF_VAR_az_devops_pat: $(AGENT_POOL_MANAGE_PAT_TOKEN)\n      TF_VAR_az_devops_agent_pool: starterpool\n      TF_VAR_az_devops_agent_vm_shutdown_time: 2300\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/backend.tf",
    "content": "#Set the terraform backend\nterraform {\n  # Backend variables are initialized by Azure DevOps\n  backend \"azurerm\" {}\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/devops-agent/devops_agent_init.sh",
    "content": "#!/bin/sh\n\n\ntest -n \"$1\" || { echo \"The argument az_devops_url must be provided\"; exit 1; }\naz_devops_url=\"$1\"\n[[ \"$az_devops_url\" == */ ]] || { echo \"The argument az_devops_url must end with /\"; exit 1; }\ntest -n \"$2\" || { echo \"The argument az_devops_pat must be provided\"; exit 1; }\naz_devops_pat=\"$2\"\ntest -n \"$3\" || { echo \"The argument az_devops_agent_pool must be provided\"; exit 1; }\naz_devops_agent_pool=\"$3\"\ntest -n \"$4\" || { echo \"The argument az_devops_agents_per_vm must be provided\"; exit 1; }\naz_devops_agents_per_vm=\"$4\"\n\n\n#strict mode, fail on error\nset -euo pipefail\n\n\necho \"start\"\n\necho \"install Ubuntu packages\"\n\n# To make it easier for build and release pipelines to run apt-get,\n# configure apt to not require confirmation (assume the -y argument by default)\nexport DEBIAN_FRONTEND=noninteractive\necho 'APT::Get::Assume-Yes \"true\";' > /etc/apt/apt.conf.d/90assumeyes\necho 'Dpkg::Use-Pty \"0\";' > /etc/apt/apt.conf.d/00usepty\n\n\napt-get update\napt-get install -y --no-install-recommends \\\n        ca-certificates \\\n        jq \\\n        apt-transport-https \\\n        docker.io\n\n\necho \"Allowing agent to run docker\"\n\nusermod -aG docker azuredevopsuser\n\necho \"Installing Azure CLI\"\n\ncurl -sL https://aka.ms/InstallAzureCLIDeb | bash\n\necho \"install VSTS Agent\"\n\ncd /home/azuredevopsuser\nmkdir -p agent\ncd agent\n\nAGENTRELEASE=\"$(curl -s https://api.github.com/repos/Microsoft/azure-pipelines-agent/releases/latest | grep -oP '\"tag_name\": \"v\\K(.*)(?=\")')\"\nAGENTURL=\"https://vstsagentpackage.azureedge.net/agent/${AGENTRELEASE}/vsts-agent-linux-x64-${AGENTRELEASE}.tar.gz\"\necho \"Release \"${AGENTRELEASE}\" appears to be latest\" \necho \"Downloading...\"\nwget -q -O agent_package.tar.gz ${AGENTURL} \n\n# Generate random prefix for agent names\nif ! test -e \"host_uuid.txt\"; then\n  uuidgen > host_uuid.txt.tmp\n  mv host_uuid.txt.tmp host_uuid.txt\nfi\nhost_id=$(cat host_uuid.txt)\n\n\nfor agent_num in $(seq 1 $az_devops_agents_per_vm); do\n  agent_dir=\"agent-$agent_num\"\n  mkdir -p \"$agent_dir\"\n  pushd \"$agent_dir\"\n    agent_id=\"${agent_num}_${host_id}\"\n    echo \"installing agent $agent_id\"\n    tar zxf ../agent_package.tar.gz\n    chmod -R 777 .\n    echo \"extracted\"\n    ./bin/installdependencies.sh\n    echo \"dependencies installed\"\n\n    if test -e .agent; then\n      echo \"attempting to uninstall agent\"\n      ./svc.sh stop || true\n      ./svc.sh uninstall || true\n      sudo -u azuredevopsuser ./config.sh remove --unattended --auth pat --token \"$az_devops_pat\" || true\n    fi\n\n    echo \"running installation\"\n    sudo -u azuredevopsuser ./config.sh --unattended --url \"$az_devops_url\" --auth pat --token \"$az_devops_pat\" --pool \"$az_devops_agent_pool\" --agent \"$agent_id\" --acceptTeeEula --work ./_work --runAsService\n    echo \"configuration done\"\n    ./svc.sh install\n    echo \"service installed\"\n    ./svc.sh start\n    echo \"service started\"\n    echo \"config done\"\n  popd\ndone\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/devops-agent/main.tf",
    "content": "resource \"azurerm_resource_group\" \"devops\" {\n  name   = \"rg-${var.appname}-${var.environment}-devops\"\n  location = var.location\n}\n\n# Create virtual network\n\nresource \"azurerm_virtual_network\" \"devops\" {\n  name                = \"vnet-${var.appname}-devops-${var.environment}\"\n  address_space       = [\"10.100.0.0/16\"]\n  location            = var.location\n  resource_group_name = azurerm_resource_group.devops.name\n}\n\nresource \"azurerm_subnet\" \"devops\" {\n  name                 = \"agents-subnet\"\n  resource_group_name  = azurerm_resource_group.devops.name\n  virtual_network_name = azurerm_virtual_network.devops.name\n  address_prefix       = \"10.100.1.0/24\"\n}\n\nresource \"azurerm_storage_account\" \"devops\" {\n  name                     = \"stado${var.appname}${var.environment}\"\n  resource_group_name      = azurerm_resource_group.devops.name\n  location                 = azurerm_resource_group.devops.location\n  account_tier             = \"Standard\"\n  account_replication_type = \"LRS\"\n}\n\nresource \"azurerm_storage_container\" \"devops\" {\n  name                  = \"content\"\n  storage_account_name  = azurerm_storage_account.devops.name\n  container_access_type = \"private\"\n}\n\nresource \"azurerm_storage_blob\" \"devops\" {\n  name                   = \"devops_agent_init-${md5(file(\"${path.module}/devops_agent_init.sh\"))}.sh\"\n  storage_account_name   = azurerm_storage_account.devops.name\n  storage_container_name = azurerm_storage_container.devops.name\n  type                   = \"Block\"\n  source                 = \"${path.module}/devops_agent_init.sh\"\n}\n\ndata \"azurerm_storage_account_blob_container_sas\" \"devops_agent_init\" {\n  connection_string = azurerm_storage_account.devops.primary_connection_string\n  container_name    = azurerm_storage_container.devops.name\n  https_only        = true\n\n  start  = \"2000-01-01\"\n  expiry = \"2099-01-01\"\n\n  permissions {\n    read   = true\n    add    = false\n    create = false\n    write  = false\n    delete = false\n    list   = false\n  }\n}\n\n\n# Create public IPs\nresource \"azurerm_public_ip\" \"devops\" {\n  name                = \"pip-${var.appname}-devops-${var.environment}-${format(\"%03d\", count.index + 1)}\"\n  location            = var.location\n  resource_group_name = azurerm_resource_group.devops.name\n  allocation_method   = \"Dynamic\"\n  count               = var.az_devops_agent_vm_count\n}\n\n# Create network interface\nresource \"azurerm_network_interface\" \"devops\" {\n  name                      = \"nic-${var.appname}-devops-${var.environment}-${format(\"%03d\", count.index + 1)}\"\n  location                  = var.location\n  resource_group_name       = azurerm_resource_group.devops.name\n\n  ip_configuration {\n    name                          = \"AzureDevOpsNicConfiguration\"\n    subnet_id                     = azurerm_subnet.devops.id\n    private_ip_address_allocation = \"dynamic\"\n    public_ip_address_id          = azurerm_public_ip.devops[count.index].id\n  }\n\n  count                     = var.az_devops_agent_vm_count\n}\n\n# Create virtual machine\n\nresource \"random_password\" \"agent_vms\" {\n  length = 24\n  special = true\n  override_special = \"!@#$%&*()-_=+[]:?\"\n  min_upper = 1\n  min_lower = 1\n  min_numeric = 1\n  min_special = 1\n}\n\nresource \"azurerm_virtual_machine\" \"devops\" {\n  name                  = \"vm${var.appname}devops${var.environment}-${format(\"%03d\", count.index + 1)}\"\n  location              = var.location\n  resource_group_name   = azurerm_resource_group.devops.name\n  network_interface_ids = [azurerm_network_interface.devops[count.index].id]\n  vm_size               = var.az_devops_agent_vm_size\n\n  storage_os_disk {\n    name              = \"osdisk${var.appname}devops${var.environment}${format(\"%03d\", count.index + 1)}\"\n    caching           = \"ReadWrite\"\n    create_option     = \"FromImage\"\n    managed_disk_type = \"Premium_LRS\"\n  }\n\n  storage_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"UbuntuServer\"\n    sku       = \"16.04.0-LTS\"\n    version   = \"latest\"\n  }\n\n  os_profile {\n    computer_name  = \"AzureDevOps\"\n    admin_username = \"azuredevopsuser\"\n    admin_password = random_password.agent_vms.result\n  }\n\n  os_profile_linux_config {\n    disable_password_authentication = false\n\n    dynamic \"ssh_keys\" {\n      for_each = var.az_devops_agent_sshkeys\n      content {\n        key_data = each.key\n        path = \"/home/azuredevopsuser/.ssh/authorized_keys\"\n      }\n    }\n  }\n\n  boot_diagnostics {\n    enabled     = \"true\"\n    storage_uri = azurerm_storage_account.devops.primary_blob_endpoint\n  }\n\n  count = var.az_devops_agent_vm_count\n}\n\nresource \"azurerm_virtual_machine_extension\" \"devops\" {\n  name                 = format(\"install_azure_devops_agent-%03d\", count.index + 1)\n  virtual_machine_id   = azurerm_virtual_machine.devops[count.index].id\n  publisher            = \"Microsoft.Azure.Extensions\"\n  type                 = \"CustomScript\"\n  type_handler_version = \"2.0\"\n\n  #timestamp: use this field only to trigger a re-run of the script by changing value of this field.\n  #           Any integer value is acceptable; it must only be different than the previous value.\n  settings = jsonencode({\n    \"timestamp\" : 1\n  })\n  protected_settings = jsonencode({\n  \"fileUris\": [\"${azurerm_storage_blob.devops.url}${data.azurerm_storage_account_blob_container_sas.devops_agent_init.sas}\"],\n  \"commandToExecute\": \"bash ${azurerm_storage_blob.devops.name} '${var.az_devops_url}' '${var.az_devops_pat}' '${var.az_devops_agent_pool}' '${var.az_devops_agents_per_vm}'\"\n  })\n  count = var.az_devops_agent_vm_count\n}\n\nresource \"azurerm_template_deployment\" \"devops_shutdown\" {\n  name = format(\"shutdown-vm-%03d\", count.index + 1)\n  resource_group_name = azurerm_resource_group.devops.name\n\n  template_body = file(\"${path.module}/shutdown_schedule_arm_template.json\")\n\n  parameters = {\n    name = \"shutdown-computevm-${azurerm_virtual_machine.devops[count.index].name}\"\n    shutdown_enabled = var.az_devops_agent_vm_shutdown_time != null ? \"Enabled\" : \"Disabled\"\n    shutdown_time = coalesce(var.az_devops_agent_vm_shutdown_time, \"0000\")\n    vm_id = azurerm_virtual_machine.devops[count.index].id\n  }\n\n  depends_on = [\n    azurerm_virtual_machine.devops\n  ]\n\n  deployment_mode = \"Incremental\"\n\n  count = var.az_devops_agent_vm_count\n}\n\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/devops-agent/outputs.tf",
    "content": "output \"agent_vm_ids\" {\n  value = azurerm_virtual_machine.devops.*.id\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/devops-agent/shutdown_schedule_arm_template.json",
    "content": "{\n  \"$schema\": \"http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#\",\n  \"contentVersion\": \"1.0.0.0\",\n  \"parameters\": {\n    \"name\": {\n      \"type\": \"string\"\n    },\n    \"shutdown_enabled\": {\n      \"type\": \"string\"\n    },\n    \"shutdown_time\": {\n      \"type\": \"string\"\n    },\n    \"vm_id\": {\n      \"type\": \"string\"\n    }\n  },\n  \"resources\": [\n    {\n      \"type\": \"microsoft.devtestlab/schedules\",\n      \"apiVersion\": \"2018-09-15\",\n      \"name\": \"[parameters('name')]\",\n      \"location\": \"northeurope\",\n      \"properties\": {\n        \"status\": \"[parameters('shutdown_enabled')]\",\n        \"taskType\": \"ComputeVmShutdownTask\",\n        \"dailyRecurrence\": {\n          \"time\": \"[parameters('shutdown_time')]\"\n        },\n        \"timeZoneId\": \"UTC\",\n        \"notificationSettings\": {\n          \"status\": \"Disabled\",\n          \"timeInMinutes\": 30,\n          \"notificationLocale\": \"en\"\n        },\n        \"targetResourceId\": \"[parameters('vm_id')]\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/devops-agent/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type = string\n}\n\nvariable \"location\" {\n  type = string\n}\n\nvariable \"az_devops_url\" {\n  type = string\n  description = \"Specify the Azure DevOps url e.g. https://dev.azure.com/myorg\"\n}\n\nvariable \"az_devops_pat\" {\n  type = string\n  description = \"Provide a Personal Access Token (PAT) for Azure DevOps. Create it at https://dev.azure.com/[Organization]/_usersSettings/tokens with permission Agent Pools > Read & manage\"\n}\n\nvariable \"az_devops_agent_pool\" {\n  type = string\n  description = \"Specify the name of the agent pool - must exist before. Create it at https://dev.azure.com/[Organization]/_settings/agentpools\"\n  default = \"pool001\"\n}\n\nvariable \"az_devops_agent_sshkeys\" {\n  type        = list(string)\n  description = \"Optionally provide ssh public key(s) to logon to the VM\"\n}\n\nvariable \"az_devops_agent_vm_size\" {\n  type    = string\n  description = \"Specify the size of the VM\"\n  default = \"Standard_D2s_v3\"\n}\n\nvariable \"az_devops_agent_vm_count\" {\n  type    = number\n  description = \"Number of Azure DevOps agent VMs\"\n  default = 1\n}\n\nvariable \"az_devops_agent_vm_shutdown_time\" {\n  type    = string\n  description = \"UTC Time at which to shutdown the agent VMs daily, for example '2000' for 8 PM\"\n  default = null\n}\n\nvariable \"az_devops_agents_per_vm\" {\n  type = number\n  description = \"Number of Azure DevOps agents spawned per VM. Agents will be named with a random prefix.\"\n  default = 4\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/main.tf",
    "content": "# Azure DevOps agent VMs\n\nmodule \"devops-agent\" {\n  source = \"./devops-agent\"\n  appname = var.appname\n  environment = var.environment\n  location = var.location\n  az_devops_url = var.az_devops_url\n  az_devops_pat = var.az_devops_pat\n  az_devops_agent_pool = var.az_devops_agent_pool\n  az_devops_agents_per_vm = var.az_devops_agents_per_vm\n  az_devops_agent_sshkeys = var.az_devops_agent_sshkeys\n  az_devops_agent_vm_size = var.az_devops_agent_vm_size\n  az_devops_agent_vm_count = var.az_devops_agent_vm_count\n  az_devops_agent_vm_shutdown_time = var.az_devops_agent_vm_shutdown_time\n}\n\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/outputs.tf",
    "content": "output \"pool_name\" {\n  value = var.az_devops_agent_pool\n}\n\noutput \"agent_vm_ids\" {\n  value = module.devops-agent.agent_vm_ids\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/provider.tf",
    "content": "#Set the terraform required version\nterraform {\n  required_version = \">= 0.12.6\"\n}\n\n# Configure the Azure Provider\nprovider \"azurerm\" {\n  # It is recommended to pin to a given version of the Provider\n  version = \"=1.44.0\"\n}\n\nprovider \"random\" {\n  version = \"~> 2.2\"\n}\n\n# Data\n\n# Make client_id, tenant_id, subscription_id and object_id variables\ndata \"azurerm_client_config\" \"current\" {}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform/variables.tf",
    "content": "variable \"appname\" {\n  type = string\n  description = \"Application name. Use only lowercase letters and numbers\"\n  default = \"starterterraform\"\n}\n\nvariable \"environment\" {\n  type    = string\n  description = \"Environment name, e.g. 'dev' or 'stage'\"\n  default = \"dev\"\n}\n\nvariable \"location\" {\n  type    = string\n  description = \"Azure region where to create resources.\"\n  default = \"North Europe\"\n}\n\nvariable \"department\" {\n  type    = string\n  description = \"A sample variable passed from the build pipeline and used to tag resources.\"\n  default = \"Engineering\"\n}\n\nvariable \"az_devops_url\" {\n  type = string\n  description = \"Specify the Azure DevOps url e.g. https://dev.azure.com/myorg\"\n}\n\nvariable \"az_devops_pat\" {\n  type = string\n  description = \"Provide a Personal Access Token (PAT) for Azure DevOps. Create it at https://dev.azure.com/[Organization]/_usersSettings/tokens with permission Agent Pools > Read & manage\"\n}\n\nvariable \"az_devops_agent_pool\" {\n  type = string\n  description = \"Specify the name of the agent pool - must exist before. Create it at https://dev.azure.com/[Organization]/_settings/agentpools\"\n  default = \"pool001\"\n}\n\nvariable \"az_devops_agent_sshkeys\" {\n  type        = list(string)\n  description = \"Optionally provide ssh public key(s) to logon to the VM\"\n  default     = []\n}\n\nvariable \"az_devops_agent_vm_size\" {\n  type    = string\n  description = \"Specify the size of the VM\"\n  default = \"Standard_D2s_v3\"\n}\n\nvariable \"az_devops_agent_vm_count\" {\n  type    = number\n  description = \"Number of Azure DevOps agent VMs\"\n  default = 2\n}\n\nvariable \"az_devops_agents_per_vm\" {\n  type = number\n  description = \"Number of Azure DevOps agents spawned per VM. Agents will be named with a random prefix.\"\n  default = 4\n}\n\nvariable \"az_devops_agent_vm_shutdown_time\" {\n  type    = string\n  description = \"UTC Time at which to shutdown the agent VMs daily, for example '2000' for 8 PM. If null, no shutdown will configured.\"\n  default = null\n}\n"
  },
  {
    "path": "301-deploy-agent-vms/terraform-stages-template.yml",
    "content": "parameters:\n  environment: agents\n  TerraformArguments: ''\n  TerraformEnvVariables:\n\nstages:\n- stage: Terraform_${{ parameters.environment }}\n  displayName: Terraform ${{ parameters.environmentDisplayName }}\n  pool:\n    vmImage: ubuntu-latest\n  jobs:\n\n  - job: Terraform\n    displayName: Terraform\n    # Avoid concurrent Terraform runs on PRs, which would result in failures due to exclusive lock on remote state file.\n    condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), variables['RUN_FLAG_TERRAFORM']))\n    steps:\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        provisionStorage: true\n        TerraformDirectory: 301-deploy-agent-vms/terraform\n        environment: ${{ parameters.environment }}\n\n    # Using bash instead of Terraform extension because of following issues:\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/748\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/725\n    # - https://github.com/microsoft/azure-pipelines-extensions/issues/747\n    - bash: |\n        set -eu\n        export ARM_CLIENT_SECRET=$(ARM_CLIENT_SECRET)\n        terraform apply -input=false -auto-approve -var environment=${{ parameters.environment }} ${{ parameters.TerraformArguments }}\n      displayName: Terraform apply\n      workingDirectory: 301-deploy-agent-vms/terraform\n      env:\n        ${{ parameters.TerraformEnvVariables }}\n\n- stage: PostTerraform_${{ parameters.environment }}\n  displayName: PostTerraform ${{ parameters.environmentDisplayName }}\n  pool:\n    vmImage: ubuntu-latest\n  jobs:\n  - job: ReadTerraform\n    displayName: Read Terraform outputs\n    steps:\n\n    - template: ../infrastructure/terraform-init-template.yml\n      parameters:\n        TerraformDirectory: 301-deploy-agent-vms/terraform\n        environment: ${{ parameters.environment }}\n\n    - template: ../infrastructure/terraform-outputs-template.yml\n      parameters:\n        TerraformDirectory: 301-deploy-agent-vms/terraform\n\n    - bash: env\n\n    - task: AzureCLI@1\n      displayName: Start agents\n      inputs:\n        azureSubscription: Terraform\n        scriptLocation: inlineScript\n        inlineScript: |\n          set -eux  # fail on error\n          az vm start --ids $(echo $AGENT_VM_IDS | jq -r '.[]') -o none\n\n  - job: DummySampleJob\n    displayName: Run Agent job\n    dependsOn: ReadTerraform\n    variables:\n      pool_name: $[ dependencies.ReadTerraform.outputs['Outputs.pool_name'] ]\n    pool: $(pool_name)\n    steps:\n    - bash: |\n        echo This is running on agent\n        hostname\n      displayName: Sample script\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Microsoft Open Source Code of Conduct\r\n\r\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\r\n\r\nResources:\r\n\r\n- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)\r\n- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)\r\n- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns\r\n"
  },
  {
    "path": "LICENSE",
    "content": "    MIT License\r\n\r\n    Copyright (c) Microsoft Corporation.\r\n\r\n    Permission is hereby granted, free of charge, to any person obtaining a copy\r\n    of this software and associated documentation files (the \"Software\"), to deal\r\n    in the Software without restriction, including without limitation the rights\r\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n    copies of the Software, and to permit persons to whom the Software is\r\n    furnished to do so, subject to the following conditions:\r\n\r\n    The above copyright notice and this permission notice shall be included in all\r\n    copies or substantial portions of the Software.\r\n\r\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\n    SOFTWARE\r\n"
  },
  {
    "path": "README.md",
    "content": "---\npage_type: sample\nproducts:\n- devops\ndescription: \"Starter project for Azure Pipelines deploying resources on Terraform\"\n---\n\n# Terraform starter project for Azure Pipelines\n\n<!-- \nGuidelines on README format: https://review.docs.microsoft.com/help/onboard/admin/samples/concepts/readme-template?branch=master\n\nGuidance on onboarding samples to docs.microsoft.com/samples: https://review.docs.microsoft.com/help/onboard/admin/samples/process/onboarding?branch=master\n\nTaxonomies for products and languages: https://review.docs.microsoft.com/new-hope/information-architecture/metadata/taxonomies?branch=master\n-->\n\nThis project can be used as a starter for Azure Pipelines deploying resources on Terraform.\n\n![pipeline jobs](/docs/images/terraform_starter/pipeline_jobs.png)\n\n## Contents\n\n| File/folder             | Description                                                  |\n|-------------------------|--------------------------------------------------------------|\n| `infrastructure`        | YAML pipeline templates shared across the samples.           |\n| `101-terraform-job`     | Sample YAML pipeline for a simple Terraform job.             |\n| `201-plan-apply-stages` | Sample YAML pipeline for manually approving plans.           |\n| `301-deploy-agent-vms`  | Sample YAML pipeline for deploying build agent VMs.          |\n| `docs`                  | Resources related to documentation.                          |\n| `CODE_OF_CONDUCT.md`    | Microsoft Open Source Code of Conduct.                       |\n| `LICENSE`               | The license for the sample.                                  |\n| `README.md`             | This README file.                                            |\n| `SECURITY.md`           | Reporting security issues.                                   |\n\n# Templates\n\n## 101 Basic Terraform job\n\nThe first template shows how to build an environment from Terraform configuration, and run\na subsequent job configured from Terraform outputs.\n\n[101-terraform-job: Basic Terraform job](101-terraform-job)\n\n![pipeline job](/docs/images/terraform_starter/101-terraform-job.png)\n\n## 201 Separate Plan and Apply stages\n\nThe next template shows how to build a multi-stage pipeline\nallowing to manually review and approve infrastructure changes before they are deployed.\n\n[201-plan-apply-stages: Separate Plan and Apply stages](201-plan-apply-stages)\n\n![pipeline jobs](docs/images/terraform_starter/pipeline_stage_waiting.png)\n\n## 301 Deploy hosted agent VMs\n\nThe next template shows how to use Terraform to deploy a pool of agent VMs on which to run\nsubsequent jobs.\n\n[301-deploy-agent-vms: Deploy hosted agent VMs](301-deploy-agent-vms)\n\n![agent job](/docs/images/terraform_starter/301-agent-job.png)\n\n# How to use the templates\n\n## Variables and state management\n\nVariables can be injected using `TF_VAR_` syntax in the `TerraformEnvVariables` parameter or the\n`-var key=value` syntax in the `TerraformArguments` parameter.\nThe pipelines demonstrates this by adding a custom tag named `department` to the\ncreated resource group, with distinct values in staging and QA.\n\nRather than passing a Terraform plan between stages (which would contain clear-text secrets),\nthe pipeline in the\n[201-plan-apply-stages](201-plan-apply-stages) sample\nperforms `terraform plan` again before applying changes and verifies that\na textual representation of the plan (not including secrets values) is unchanged.\n\nThe Terraform state is managed in a Azure Storage backend. Note that this backend contains\nsecrets in cleartext.\n\n## Secrets management\n\n### Generate secrets with Terraform\n\nTo demonstrate one approach to secrets management, the Terraform configuration\ngenerates a random password (per stage) for the SQL Server 1 instance, stored in\nTerraform state.\nYou can adapt this to suit your lifecycle.\n\n### Manage secrets with Azure DevOps\n\nYou might want to read credentials from an externally managed Key Vault\nor inject them via pipeline variables. This approach is demonstrated\nby defining a password for the SQL Server 2 instance and passing\nit to Terraform via an environment variable.\n\n## Getting started\n\nIn `infrastructure/terraform/variables.tf`, change the `appname` default value from\n`starterterraform` to a globally unique name.\n\n## Azure DevOps pipeline\n\nInstall the [Terraform extension for Azure DevOps](https://marketplace.visualstudio.com/items?itemName=ms-devlabs.custom-terraform-tasks).\n\nCreate a Service Connection of type Azure Resource Manager at subscription scope. Name the Service Connection `Terraform`.\nAllow all pipelines to use the connection.\n\nIn `infrastructure/terraform-init-template.yml`, update the `TerraformBackendStorageAccount` name to a globally unique storage account name.\nThe pipeline will create the storage account.\n\nCreate a build pipeline referencing `101-terraform-job/azure-pipelines.yml`.\n\n## Usage on non-master branch\n\nTo avoid issues with concurrent access to the Terraform state file, the jobs running Terraform `plan` and `apply` commands\nrun by default only on the `master` branch. On other branches, they are skipped by default:\n\n![run on non-master branch](/docs/images/terraform_starter/non_master_branch.png)\n\nYou can set the `RUN_FLAG_TERRAFORM` variable (to any non-empty value)\nwhen running the pipeline, to trigger Terraform application on a non-`master` branch.\n\n## Local development\n\nIn local development, no backend is configured so a local backend is used.\n\nInstall Azure CLI and login. Terraform will use your Azure CLI credentials.\n\n```\n$ az login -o table\nYou have logged in. Now let us find all the subscriptions to which you have access...\nCloudName    IsDefault    Name                                                  State    TenantId\n-----------  -----------  ----------------------------------------------------  -------  ------------------------------------\nAzureCloud   True         My Azure subscription                                 Enabled  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\nAzureCloud   False        My other Azure subscription                           Enabled  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\nRun `terraform init`.\n\n```\n$ terraform init\n\nInitializing the backend...\n\nInitializing provider plugins...\n- Checking for available provider plugins...\n- Downloading plugin for provider \"azurerm\" (hashicorp/azurerm) 1.38.0...\n\nTerraform has been successfully initialized!\n\nYou may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.\n```\n\nRun `terraform plan`.\n\n```\n$ terraform plan -out tfplan\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\ndata.azurerm_client_config.current: Refreshing state...\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n  # azurerm_resource_group.main will be created\n  + resource \"azurerm_resource_group\" \"main\" {\n      + id       = (known after apply)\n      + location = \"northeurope\"\n      + name     = \"rg-starterterraform-dev-main\"\n      + tags     = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n\nRun `terraform apply tfplan`.\n\n```\n$ terraform apply tfplan\ndata.azurerm_client_config.current: Refreshing state...\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n  # azurerm_resource_group.main will be created\n  + resource \"azurerm_resource_group\" \"main\" {\n      + id       = (known after apply)\n      + location = \"northeurope\"\n      + name     = \"rg-starterterraform-dev-main\"\n      + tags     = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nDo you want to perform these actions?\n  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: yes\n\nazurerm_resource_group.main: Creating...\nazurerm_resource_group.main: Creation complete after 1s [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-starterterraform-dev-main]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nsubscription_id = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\nAt this stage you will have a new resource group deployed named `rg-starterterraform-dev-main`. \n\n# Using Terraform outputs\n\nThe pipeline automatically exports Terraform outputs into pipeline variables.\n\nThe pipelines contain a sample job that consumes those variables:\n\n![output variables](/docs/images/terraform_starter/output_variables.png)\n\n- For example, in the template [301-deploy-agent-vms](301-deploy-agent-vms), the Terraform config has an output named [agent_vm_ids](301-deploy-agent-vms/terraform/outputs.tf). In the subsequent task used\nwe use the bash variable [AGENT_VM_IDS](301-deploy-agent-vms/terraform-stages-template.yml) to pass the list of agent VMs to the `az start` command.\n\nThis mechanism is useful for using generated resource names, access keys,\nand even [entire kube_config files](https://www.terraform.io/docs/providers/azurerm/r/kubernetes_cluster.html#kube_config_raw) (for Azure Kubernetes Service)\nin downstream testing or continuous delivery jobs.\n\n# Next steps\n\n* You can of course adapt the pipeline to other environments, such as Production.\n\n## Contributing\n\nThis project welcomes contributions and suggestions.  Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.\n\nWhen you submit a pull request, a CLA bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->\r\n\r\n## Security\r\n\r\nMicrosoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).\r\n\r\nIf you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.\r\n\r\n## Reporting Security Issues\r\n\r\n**Please do not report security vulnerabilities through public GitHub issues.**\r\n\r\nInstead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).\r\n\r\nIf you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).\r\n\r\nYou should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).\r\n\r\nPlease include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:\r\n\r\n  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)\r\n  * Full paths of source file(s) related to the manifestation of the issue\r\n  * The location of the affected source code (tag/branch/commit or direct URL)\r\n  * Any special configuration required to reproduce the issue\r\n  * Step-by-step instructions to reproduce the issue\r\n  * Proof-of-concept or exploit code (if possible)\r\n  * Impact of the issue, including how an attacker might exploit the issue\r\n\r\nThis information will help us triage your report more quickly.\r\n\r\nIf you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.\r\n\r\n## Preferred Languages\r\n\r\nWe prefer all communications to be in English.\r\n\r\n## Policy\r\n\r\nMicrosoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).\r\n\r\n<!-- END MICROSOFT SECURITY.MD BLOCK -->\r\n"
  },
  {
    "path": "infrastructure/terraform-init-template.yml",
    "content": "parameters:\n  environment: stage\n  provisionStorage: false\n  TerraformVersion: 0.12.21\n  TerraformDirectory:\n  TerraformBackendServiceConnection: Terraform\n  TerraformEnvironmentServiceConnection: Terraform\n  TerraformBackendResourceGroup: terraform\n  TerraformBackendStorageAccount: terraformstarterstate\n  TerraformBackendStorageContainer: terraformstate\n  TerraformBackendLocation: North Europe\n\nsteps:\n\n- task: AzureCLI@1\n  displayName: Set Terraform backend\n  condition: and(succeeded(), ${{ parameters.provisionStorage }})\n  inputs:\n    azureSubscription: ${{ parameters.TerraformBackendServiceConnection }}\n    scriptLocation: inlineScript\n    inlineScript: |\n      set -eu  # fail on error\n      RG='${{ parameters.TerraformBackendResourceGroup }}'\n      export AZURE_STORAGE_ACCOUNT='${{ parameters.TerraformBackendStorageAccount }}'\n      export AZURE_STORAGE_KEY=\"$(az storage account keys list -g \"$RG\" -n \"$AZURE_STORAGE_ACCOUNT\" --query '[0].value' -o tsv)\"\n      if test -z \"$AZURE_STORAGE_KEY\"; then\n        az configure --defaults group=\"$RG\" location='${{ parameters.TerraformBackendLocation }}'\n        az group create -n \"$RG\" -o none\n        az storage account create -n \"$AZURE_STORAGE_ACCOUNT\" -o none\n        export AZURE_STORAGE_KEY=\"$(az storage account keys list -g \"$RG\" -n \"$AZURE_STORAGE_ACCOUNT\" --query '[0].value' -o tsv)\"\n      fi\n\n      container='${{ parameters.TerraformBackendStorageContainer }}'\n      if ! az storage container show -n \"$container\" -o none 2>/dev/null; then\n        az storage container create -n \"$container\" -o none\n      fi\n      blob='${{ parameters.environment }}.tfstate'\n      if [[ $(az storage blob exists -c \"$container\" -n \"$blob\" --query exists) = \"true\" ]]; then\n        if [[ $(az storage blob show -c \"$container\" -n \"$blob\" --query \"properties.lease.status=='locked'\") = \"true\" ]]; then\n          echo \"State is leased\"\n          lock_jwt=$(az storage blob show -c \"$container\" -n \"$blob\" --query metadata.terraformlockid -o tsv)\n          if [ \"$lock_jwt\" != \"\" ]; then\n            lock_json=$(base64 -d <<< \"$lock_jwt\")\n            echo \"State is locked\"\n            jq . <<< \"$lock_json\"\n          fi\n          if [ \"${TERRAFORM_BREAK_LEASE:-}\" != \"\" ]; then\n            az storage blob lease break -c \"$container\" -b \"$blob\"\n          else\n            echo \"If you're really sure you want to break the lease, rerun the pipeline with variable TERRAFORM_BREAK_LEASE set to 1.\"\n            exit 1\n          fi\n        fi\n      fi\n    addSpnToEnvironment: true\n\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0\n  displayName: Install Terraform\n  inputs:\n    terraformVersion: ${{ parameters.TerraformVersion }}\n\n- task: AzureCLI@1\n  displayName: Terraform credentials\n  inputs:\n    azureSubscription: ${{ parameters.TerraformEnvironmentServiceConnection }}\n    scriptLocation: inlineScript\n    inlineScript: |\n      set -eu\n      subscriptionId=$(az account show --query id -o tsv)\n      echo \"##vso[task.setvariable variable=ARM_CLIENT_ID]$servicePrincipalId\"\n      echo \"##vso[task.setvariable variable=ARM_CLIENT_SECRET;issecret=true]$servicePrincipalKey\"\n      echo \"##vso[task.setvariable variable=ARM_SUBSCRIPTION_ID]$subscriptionId\"\n      echo \"##vso[task.setvariable variable=ARM_TENANT_ID]$tenantId\"\n    addSpnToEnvironment: true\n\n# Using bash instead of Terraform extension because of following issue:\n# - https://github.com/microsoft/azure-pipelines-extensions/issues/738\n- task: AzureCLI@1\n  displayName: Terraform init\n  inputs:\n    azureSubscription: ${{ parameters.TerraformBackendServiceConnection }}\n    scriptLocation: inlineScript\n    inlineScript: |\n      set -eux  # fail on error\n      subscriptionId=$(az account show --query id -o tsv)\n      terraform init \\\n        -backend-config=storage_account_name=${{ parameters.TerraformBackendStorageAccount }} \\\n        -backend-config=container_name=${{ parameters.TerraformBackendStorageContainer }} \\\n        -backend-config=key=${{ parameters.environment }}.tfstate \\\n        -backend-config=resource_group_name=${{ parameters.TerraformBackendResourceGroup }} \\\n        -backend-config=subscription_id=$subscriptionId \\\n        -backend-config=tenant_id=$tenantId \\\n        -backend-config=client_id=$servicePrincipalId \\\n        -backend-config=client_secret=\"$servicePrincipalKey\"\n    workingDirectory: ${{ parameters.TerraformDirectory }}\n    addSpnToEnvironment: true\n"
  },
  {
    "path": "infrastructure/terraform-outputs-template.yml",
    "content": "parameters:\n  TerraformDirectory:\n\nsteps:\n- bash: |\n    set -eu\n\n    echo \"Setting job variables from Terraform outputs:\"\n    terraform output -json | jq -r '\n      . as $in\n      | keys[]\n      | [\"- \" + .]\n      | @tsv'\n\n    terraform output -json | jq -r '\n      . as $in\n      | keys[]\n      | ($in[.].value | tostring) as $value\n      | ($in[.].sensitive | tostring) as $sensitive\n      | [\n        \"- \" + . + \": \" + if $in[.].sensitive then \"(sensitive)\" else $value end,  # output name to console\n        \"##vso[task.setvariable variable=\" + . + \";isSecret=\" + $sensitive + \"]\" + $value,  # set as ADO task variable\n        \"##vso[task.setvariable variable=\" + . + \";isOutput=true;isSecret=\" + $sensitive + \"]\" + $value  # also set as ADO job variable\n        ]\n      | .[]'\n\n  name: Outputs\n  displayName: Read Terraform outputs\n  workingDirectory: ${{ parameters.TerraformDirectory }}\n"
  }
]