Provide and initial ADO bootstrap template to help users get stared with IaC pipelines (#386)

* init - details follow:
- pulled in @nmiodice example
- began docs / README.md
- refactored example to follow other temp's more closely (break out ins & outs), and began massaging terminology to be consistent with other temps, AZ, AzDO, and the README.md

* further alignment with existing doc & use of .env

* ins, outs, and in-betweens defined

* more polish and clean-up

* more in inputs and resources

* Attention to creating the AzDO project (and parameterizing it).

* ignore .env

* align var groups with the latest requirements from Cobalt

* document outputs

* document inputs

* finish detailing in- & out-puts

* Initial commit.

* migrate git ignores into root-most level

* remove 'fixme' and add optional bits to create AzDO projects, as desired

* remove 'fixme' and add optional bits to create AzDO projects, as desired

* merge to upstream repo

* point to 'new' repo and add secret VGs

* lint all the things!

* stub new instructions to get the pipeline running the first time

Co-authored-by: Nicholas M. Iodice <niiodice@microsoft.com>
This commit is contained in:
Stephen Henderson 2020-04-22 13:04:55 -05:00 коммит произвёл GitHub
Родитель c6b2ea15d2
Коммит e6162db608
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 449 добавлений и 8 удалений

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

@ -13,10 +13,20 @@ _vendor-*/
# Terraform
.terraform/
**/.terraform/
**/terraform.tfstate.*
**/terraform.tfstate
**/.terraform.tfstate.lock.info
**/*.plan
*.tfstate
*.tfstate.*
# Local .terraform directories
**/.terraform/*
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# User-specific files
*.suo
@ -349,15 +359,12 @@ ASALocalRun/
# ENV files
**/.env
.envrc
**/.envrc
# Exclude local build directory
build/
# For Mac OS user files
**/.DS_Store
# For Mac OS user files
**/.DS_Store
# Dep files
*.toml
*.lock

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

@ -0,0 +1,6 @@
AZDO_PERSONAL_ACCESS_TOKEN=
AZDO_ORG_SERVICE_URL=
ARM_CLIENT_ID=
ARM_CLIENT_SECRET=
ARM_SUBSCRIPTION_ID=
ARM_TENANT_ID=

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

@ -0,0 +1,210 @@
# Bootstrap for Cobalt IaC Pipelines on Azure DevOps
## Overview
This provides a starting point for anyone interested in having an Azure DevOps
Project that will 'run' a target Cobalt template whenever that template changes.
## Intended audience
This article is for an infrastructure worker that is brand new to Cobalt templating, and it's *Cobalt Infrastructure
Template* (CIT) developer workflow, who wants to stand-up Azure DevOps and Azure resources to enact a CI/CD process
over their infrastructure as code work.
### An example
Imagine that you are building a new set of web applications which are destined
to run on Azure. Those applications are going to use a few runtime resources, and you've
wisely decided to use a Cobalt template to satisfy and centralize your Infrastructure as Code ("IaC")
needs across all of those applications.
To get started, you want a central Git repository to store your IaC in,
and following [solid engineering fundamentals](https://github.com/microsoft/code-with-engineering-playbook/tree/master/continuous-integration#continuous-integration),
you've decided to wrap CI/CD, and staged deployments around your IaC to help
protect the quality of your infrastructure changes.
This bootstrap (itself a Cobalt template) can be used to quickly, safely, and easily create
the ADO resources (a Project, Git Repository, Build Definitions, Variable Groups,
Service Connections, etc) that you'll need in that example.
### Terminology
Given that we're using a Cobalt template to create resources that will then operate other
Cobalt templates, discussing matters can become a bit confusing. To help with that, and
to get specific about a few other terms we'll use, let's define a few terms and then use
them consistently, within this document and this bootstrap template itself.
|Term|Meaning|
|----|----|
|ADO|Azure DevOps, AKA "AzDO," elsewhere|
|Application(s)|The application that will rely on the resources managed by the Infrastructure Template|
|Bootstrap Template|This Cobalt template, as described in the example, above|
|IaC|Infrastructure as Code|
|Infrastructure Template|Whichever Cobalt template that will manage the Application(s) runtime resources. _By default, the Bootstrap Template points to the 'Hello World' example template in the official Cobalt GitHub repository_|
|Project|The ADO Project that the Bootstrap Template will add pipeline resources to|
|IaC Repository|(empty) Git Repository in the Project where the Infrastructure Template is expected to be kept|
## What the Bootstrap Template needs, provisions, and outputs
To work properly, the Bootstrap Template needs some information about how you want things named, how many environments
are desired (and their details), and little else. The template also creates many kinds of resources
### Decide with Project to use
Visit your ADO Organization site and select or create a project. This project will house the pipelines that
will operate your IaC CI/CD pipelines. Note that project's name. We'll need to use it as an input, in the next section.
### Inputs
There are two places from where the template consumes input information.
1. The environment variables that are set before the template is used-- usually these are `export` shell environment variables.
2. The other is a `terraform.tfvars` file.
Let's quickly explore each of these.
#### Environment Variable
You'll need to define a `.env` file in the root of the project.
You can use our environment template file to start with: `cp .env.template .env`
Provide values for the environment values in `.env` which are required to allow
Terraform to provision resources within your subscription, and your Azure DevOps Organization.
```bash
ARM_SUBSCRIPTION_ID="<az-service-principal-subscription-id>"
ARM_CLIENT_ID="<az-service-principal-client-id>"
ARM_CLIENT_SECRET="<az-service-principal-auth-secret>"
ARM_TENANT_ID="<az-service-principal-tenant>"
ARM_ACCESS_KEY="<remote-state-storage-account-primary-key>"
AZDO_PERSONAL_ACCESS_TOKEN="<see, below>"
AZDO_ORG_SERVICE_URL="<typically: https://dev.azure.com/__YOUR_ORG__/"
```
_Note: you can find out more information about the Azure DevOps personal access token [here](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page)._
#### Terraform Variables
The Environments and Azure Subscription details, for each, need to be provided in the `terraform.tfvars` file.
It should take the form of:
```hcl-terraform
environments = [{
environment: "dev1",
az_sub_id: "1aaaaaa1-bbbb-cccc-dddd-eeeeee"
},{
environment: "dev2",
az_sub_id: "4aaaaaa4-bbbb-cccc-dddd-eeeeee"
},{
environment: "prod",
az_sub_id: "1aaaaaa1-bbbb-cccc-dddd-eeeeee"
}]
```
In the structure above, you can indicate as many environments as needed. Each should have a unique `environment` value,
and each will need an accurate `az_sub_id`. For each environment, `az_sub_id` is the Azure Subscription ID that the
Infrastructure Template will eventually provision resources into.
You'll also need to specify the name of the Project (_The ADO Project that the Bootstrap Template will add pipeline resources to_).
The Bootstrap Template can be used with an existing Azure DevOps Project ("the Project"), or it can create
a new project. Take a look at the `azdo.tf` file, around lines 12-34 to see examples of how to use the template
for either choice.
In either case, you'll need to provide the name of the Project to use with a line like this:
```hcl-terraform
project_name = "My CICD Project Name"
```
### Provisioned Resources
The Bootstrap Template creates a few resources in the Azure Subscription(s) and the Azure DevOps Organization that you
pointed to with the inputs, above. These resources establish the CI/CD pipelines in ADO, and map those pipelines to
the appropriate Azure Subscriptions.
Here's a list of the resources that are provisioned including what a typical name might be, what kind of resource it is,
and some extra detail about the purpose of the resource.
| Name (typical) | Resource Type | Description |
| --- | --- | --- |
| Infrastructure Pipeline Variables | azuredevops_variable_group | Common values that all pipelines share |
| Infrastructure Pipeline Variables - *prod* | azuredevops_variable_group | Environment-specific values, per pipeline |
| Infrastructure Pipeline Secrets - *prod* | azuredevops_variable_group | Environment-specific secret values, per pipeline |
| Infrastructure Repository | azuredevops_git_repository | The Git repository where Infrastructure templates are stored and watched for changes |
| Infrastructure CICD | azuredevops_build_definition | The Build Definition that watches for IaC Template changes, and then triggers the Infrastructure pipelines |
| Infrastructure Deployment Service Connection | azuredevops_serviceendpoint_azurerm | The service connection that the Infrastructure Template will run under |
| bootstrap-deploy-app | azuread_application | app |
| n/a | azuread_service_principal | The Azure Service Principal that the Infrastructure Template will run under |
| n/a | azurerm_role_assignment | The Azure Service Principal role assignment that the Infrastructure Template will run under |
| bootstrap-iac-tf-workspaces | azurerm_resource_group | The Azure Resource Group which houses the remote state containers for Terraform |
| iactf*prod* | azurerm_storage_account | An Azure Storage Account to house remote state containers, for a given environment |
| tfstate | azurerm_storage_container | The Azure Blob Storage Container, within an environment-specific Azure Storage Account that will house remote state containers for Terraform |
| *project_name* | The ADO Project that the Bootstrap Template will add pipeline resources to |
### Outputs
After the Bootstrap Template has been applied (via `terraforma apply`) you will see some output reflecting the
| name | description |
| ---- | ----------- |
| project_id | The ID of the Project that was provisioned |
| project_name | The name of the Project that was provisioned |
| repo_clone_url | The *https* (not the *ssh*) URL of the IaC Repository that was created |
## Running the Bootstrap Template
### Set-up
For the purposes of this Bootstrap Template, you'll need a few tools to be ready on your workstation.
Please see the quick start guide's list of prerequisites: *[quick-start guide prerequisites](https://github.com/microsoft/cobalt/blob/master/docs/2_QUICK_START_GUIDE.md#2.3-prerequisites).*
## Example Usage
For this example, we'll be using `bash` running on a unix, such as OSX, Ubuntu or Windows Subsystem for Linux.
1. Navigate to the directory where the `ado-bootstrap-iac-pipeline` template (where this `README.md` file is at).
1. Execute the following commands to set the environment variables that the template will require
for Terraform, Azure and Azure DevOps. This will take the values in an .env file and export them in the local
shell:
```bash
# these commands `export` the environment variables needed to run this template into the local shell
DOT_ENV=<path to your .env file -- see .env.template for more information>
export $(cat $DOT_ENV | xargs)
```
1. Execute the following commands to set up your terraform workspace.
_Note: the Bootstrap Template creates the Azure resources needed for handling Terraform remote state, but
we're not using remote state configuration for this._
```bash
# This command configures terraform to use a local workspace unique to you.
terraform init
```
5. Execute the following commands to orchestrate a deployment.
```bash
# See what terraform will try to deploy without actually deploying
terraform plan
```
Review the output from the `plan` and make sure everything looks right.
For example, you might want the names of environments, or Resource Groups to be slightly different.
Also, if you happen to be running the Bootstrap Template
a second time (after a previous `apply` operation), then you'll want to be careful that Terraform isn't removing or
replacing any resources that you didn't intend it to.
When everything looks good, you can let Terraform create everything in the plan:
```bash
# Execute a deployment
terraform apply
```
6. Contribute your Cobalt IaC repo to the newly provisioned Infrastructure Repository.
7. Trigger the Infrastructure CICD pipeline that was provisioned into the Project.
8. Authorize the Infrastructure CICD pipeline to access the variable groups that were provisioned by the into the Project.

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

@ -0,0 +1,129 @@
# Make sure to set the following environment variables:
# AZDO_PERSONAL_ACCESS_TOKEN
# AZDO_ORG_SERVICE_URL
provider "azuredevops" {
version = ">= 0.0.1"
}
provider "null" {
version = "2.1.2"
}
// use the following two block (a `resource` and a `local` block) to
// use an existing Azure DevOps Project.
//
//data "azuredevops_projects" "p" {
// project_name = var.project_name
//}
//
//locals {
// project_name = data.azuredevops_projects.p.projects.*.name[0]
// project_id = data.azuredevops_projects.p.projects.*.project_id[0]
//}
// use the following two block (a `resource` and a `local` block) to
// create a new Azure DevOps Project.
resource "azuredevops_project" "p" {
project_name = var.project_name
}
locals {
project_name = azuredevops_project.p.project_name
project_id = azuredevops_project.p.id
}
resource "azuredevops_variable_group" "core_vg" {
project_id = local.project_id
name = "Infrastructure Pipeline Variables"
description = "Managed by Terraform"
allow_access = true
variable {
name = "AGENT_POOL"
value = "Hosted Ubuntu 1604"
}
variable {
name = "BUILD_ARTIFACT_NAME"
value = "drop"
}
variable {
name = "FORCE_RUN"
value = "False"
}
variable {
name = "SERVICE_CONNECTION_NAME"
value = azuredevops_serviceendpoint_azurerm.endpointazure.service_endpoint_name
}
}
resource "azuredevops_variable_group" "stage_vg" {
project_id = local.project_id
count = length(var.environments)
name = format("Infrastructure Pipeline Variables - %s", var.environments[count.index].environment)
description = "Managed by Terraform"
allow_access = false
variable {
name = "ARM_SUBSCRIPTION_ID"
value = var.environments[count.index].az_sub_id
}
variable {
name = "REMOTE_STATE_ACCOUNT"
value = format("tf%s", var.environments[count.index].environment)
}
variable {
name = "REMOTE_STATE_CONTAINER"
value = local.tf_state_container_name
}
}
resource "azuredevops_variable_group" "secrets_vg" {
project_id = local.project_id
count = length(var.environments)
name = format("Infrastructure Pipeline Secrets - %s", var.environments[count.index].environment)
description = "Managed by Terraform"
allow_access = false
variable {
name = "PLACEHOLDER"
value = "placeholder"
}
}
resource "azuredevops_git_repository" "repo" {
project_id = local.project_id
name = "Infrastructure Repository"
initialization {
init_type = "Clean"
}
}
resource "azuredevops_build_definition" "build" {
project_id = local.project_id
name = "Infrastructure CICD"
repository {
repo_type = "TfsGit"
repo_name = azuredevops_git_repository.repo.name
branch_name = azuredevops_git_repository.repo.default_branch
yml_path = "devops/providers/azure-devops/templates/azure-pipelines.yml"
}
variable_groups = concat(
[azuredevops_variable_group.core_vg.id],
azuredevops_variable_group.stage_vg.*.id,
azuredevops_variable_group.secrets_vg.*.id
)
}
resource "azuredevops_serviceendpoint_azurerm" "endpointazure" {
project_id = local.project_id
service_endpoint_name = "Infrastructure Deployment Service Connection"
azurerm_spn_clientid = azuread_service_principal.sp.application_id
azurerm_spn_clientsecret = random_string.random.result
azurerm_spn_tenantid = data.azurerm_subscription.sub.tenant_id
azurerm_subscription_id = data.azurerm_subscription.sub.subscription_id
azurerm_subscription_name = data.azurerm_subscription.sub.display_name
azurerm_scope = data.azurerm_subscription.sub.id
}

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

@ -0,0 +1,74 @@
provider "azuread" {
version = 0.8
}
provider "random" {
version = 2.2
}
provider "azurerm" {
version = "=2.0.0"
features {}
}
data "azurerm_subscription" "sub" {
}
data "azurerm_client_config" "example" {
}
resource "azuread_application" "app" {
name = "bootstrap-deploy-app"
}
resource "azuread_service_principal" "sp" {
application_id = azuread_application.app.application_id
}
resource "azurerm_role_assignment" "rbac" {
scope = data.azurerm_subscription.sub.id
role_definition_name = "Contributor"
principal_id = azuread_service_principal.sp.object_id
}
resource "random_string" "random" {
length = 16
special = true
}
resource "azuread_service_principal_password" "passwd" {
service_principal_id = azuread_service_principal.sp.id
value = random_string.random.result
end_date = "2099-01-01T01:02:03Z"
}
resource "azurerm_resource_group" "rg" {
name = "bootstrap-iac-tf-workspaces"
location = "Central US"
tags = {
bootstrap = "bootstrap"
}
}
locals {
tf_state_container_name = "bootstraptfstate"
}
resource "azurerm_storage_account" "acct" {
count = length(var.environments)
name = format("iactf%s", var.environments[count.index].environment)
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
environment = var.environments[count.index].environment
bootstrap = "bootstrap"
}
}
resource "azurerm_storage_container" "container" {
count = length(var.environments)
name = local.tf_state_container_name
storage_account_name = azurerm_storage_account.acct[count.index].name
container_access_type = "private"
}

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

@ -0,0 +1,4 @@
output "repo_clone_url" {
description = "The Git repository where IaC templates are stored and watched for changes"
value = azuredevops_git_repository.repo.web_url
}

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

@ -0,0 +1,11 @@
variable "environments" {
description = "The environments and Az Subcriptions that IaC CI/CD will provision"
type = list(object({
environment = string,
az_sub_id = string
}))
}
variable "project_name" {
description = "The name of an existing proejct that will be provisioned to run the IaC CI/CD pipelines"
}