Skip to content

Commit

Permalink
feat(azure-devops): create projects and respective service connection…
Browse files Browse the repository at this point in the history
…s to ARM resources
  • Loading branch information
julie-ng committed Nov 3, 2020
1 parent 455a5df commit 7c1f8d9
Show file tree
Hide file tree
Showing 19 changed files with 445 additions and 90 deletions.
168 changes: 96 additions & 72 deletions main.tf
Original file line number Diff line number Diff line change
@@ -1,98 +1,122 @@
# Note: all variables included here for easier understanding/learning
# Azure AD Groups
# ---------------

resource "azuread_group" "groups" {
for_each = var.groups
name = "demo-${each.value}-${local.suffix}"
prevent_duplicate_names = true
}


# Suffix
# ------
# Some Azure resources, e.g. storage accounts must have globally
# unique names. Use a suffix to avoid automation errors.
# Azure DevOps
# ------------

resource "random_string" "suffix" {
length = 4
special = false
upper = false
resource "azuredevops_project" "team_projects" {
for_each = var.projects
project_name = each.value.name
description = each.value.description
visibility = "private"
version_control = "Git"

features = {
repositories = "enabled"
pipelines = "enabled"
artifacts = "disabled"
boards = "disabled"
testplans = "disabled"
}
}

locals {
suffix = random_string.suffix.result
module "ado_standard_permissions" {
for_each = var.projects
source = "./modules/azure-devops-permissions"
ado_project_id = azuredevops_project.team_projects["proj_${each.value.team}"].id
team_aad_id = azuread_group.groups["${each.value.team}"].id
admin_aad_id = azuread_group.groups["${each.value.team}_admins"].id
}

# Supermarket Project

# Azure AD Groups
# ---------------
# Workspaces generally have 2 groups of actors, general
# team members who are granted "Contributor" permissions
# and admins who are granted "Owner" permissions.

variable "teams" {
type = map(string)
default = {
fruits = "fruits"
fruits_admins = "fruits-admins"
veggies_admins = "veggies-admins"
veggies = "veggies"
infra = "infra"
infra_admins = "infra"
resource "azuredevops_project" "supermarket" {
project_name = "supermarket"
description = "Example: 1 project, many teams, many repos"
visibility = "private"
version_control = "Git"

features = {
boards = "enabled"
repositories = "enabled"
pipelines = "enabled"
artifacts = "disabled"
testplans = "disabled"
}
}

resource "azuread_group" "groups" {
for_each = var.teams
name = "demo-${each.value}-${local.suffix}"
prevent_duplicate_names = true
module "supermarket_permissions_fruits" {
source = "./modules/azure-devops-permissions"
ado_project_id = azuredevops_project.supermarket.id
team_aad_id = azuread_group.groups["fruits"].id
admin_aad_id = azuread_group.groups["fruits_admins"].id
}

module "supermarket_permissions_veggies" {
source = "./modules/azure-devops-permissions"
ado_project_id = azuredevops_project.supermarket.id
team_aad_id = azuread_group.groups["veggies"].id
admin_aad_id = azuread_group.groups["veggies_admins"].id
}

# Workspaces
# ----------
# This map defines our workspaces. The keys can be referenced in outputs,
# e.g. module.workspace["gov_shared"]. Suffixes are appended later.

variable "environments" {
type = map(map(string))

default = {
fru_dev = {
env = "dev"
team = "fruits"
}

fru_prod = {
env = "prod"
team = "fruits"
}

veg_dev = {
env = "dev"
team = "veggies"
}

veg_prod = {
env = "prod"
team = "veggies"
}

shared = {
env = "shared"
team = "infra"
}
# Shared Collaboration

resource "azuredevops_project" "collaboration" {
project_name = "shared-collaboration"
description = "Example: what if separate teams should talk to each other? (Disadvantage: cannot link external project commits to work items in this project)"
visibility = "private"
version_control = "Git"

features = {
boards = "enabled"
repositories = "disabled"
pipelines = "disabled"
artifacts = "disabled"
testplans = "disabled"
}
}

module "collaboration_permissions_fruits" {
source = "./modules/azure-devops-permissions"
ado_project_id = azuredevops_project.collaboration.id
team_aad_id = azuread_group.groups["fruits"].id
admin_aad_id = azuread_group.groups["fruits_admins"].id
}

module "collaboration_permissions_veggies" {
source = "./modules/azure-devops-permissions"
ado_project_id = azuredevops_project.collaboration.id
team_aad_id = azuread_group.groups["veggies"].id
admin_aad_id = azuread_group.groups["veggies_admins"].id
}


# Workspaces
# ----------

module "workspace" {
for_each = var.environments
source = "./modules/workspace"
source = "./modules/azure-resources"
name = "${each.value.team}-${each.value.env}-${local.suffix}"
team_group_id = azuread_group.groups["${each.value.team}"].id
admin_group_id = azuread_group.groups["${each.value.team}_admins"].id
}


# Outputs
# -------
# Service Connections for ADO
# ---------------------------

output "workspaces" {
value = module.workspace[*]
module "service_connections" {
for_each = module.workspace
source = "./modules/azure-devops-service-connection"
service_principal_id = each.value.service_principals[0].application_id
key_vault_name = each.value.key_vault
resource_group_name = each.value.resource_group_name
}

output "aad_groups" {
value = azuread_group.groups[*]
}
48 changes: 48 additions & 0 deletions modules/azure-devops-permissions/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Azure DevOps Groups
# -------------------
# To add AAD groups to existing project roles:
#
# 1) Create new ADO group
# 2) Get Reference to existing group, e.g. Contribtors in ADO Project
# 3) Add ADO group (which references AAD group) to "Contributors" from above

locals {
team_role = "Contributors"
admin_role = "Project Administrators"
}

# Contributors

resource "azuredevops_group" "team_group" {
origin_id = var.team_aad_id
}

data "azuredevops_group" "contributors" {
project_id = var.ado_project_id
name = local.team_role
}

resource "azuredevops_group_membership" "team" {
group = data.azuredevops_group.contributors.descriptor
members = [
azuredevops_group.team_group.descriptor
]
}

# Administrators

resource "azuredevops_group" "admins_group" {
origin_id = var.admin_aad_id
}

data "azuredevops_group" "admins" {
project_id = var.ado_project_id
name = local.admin_role
}

resource "azuredevops_group_membership" "admins" {
group = data.azuredevops_group.admins.descriptor
members = [
azuredevops_group.team_group.descriptor
]
}
18 changes: 18 additions & 0 deletions modules/azure-devops-permissions/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
output "azure_devops_groups" {
value = {
contributors = {
origin = azuredevops_group.team_group.origin
principal_name = azuredevops_group.team_group.principal_name
project_role = data.azuredevops_group.contributors.name
project_id = data.azuredevops_group.contributors.project_id
subject_kind = azuredevops_group.team_group.subject_kind
}
admins = {
origin = azuredevops_group.admins_group.origin
principal_name = azuredevops_group.admins_group.principal_name
project_role = data.azuredevops_group.admins.name
project_id = data.azuredevops_group.admins.project_id
subject_kind = azuredevops_group.admins_group.subject_kind
}
}
}
14 changes: 14 additions & 0 deletions modules/azure-devops-permissions/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
variable "ado_project_id" {
description = "Azure DevOps Project ID"
type = string
}

variable "team_aad_id" {
description = "AAD Group ID to receive 'Contributor' permissions"
type = string
}

variable "admin_aad_id" {
description = "AAD Group ID to receive 'Owner' permissions"
type = string
}
8 changes: 8 additions & 0 deletions modules/azure-devops-permissions/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
azuredevops = {
source = "terraform-providers/azuredevops"
}
}
required_version = ">= 0.13"
}
39 changes: 39 additions & 0 deletions modules/azure-devops-service-connection/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 1 - get Service Principal secret from Key Vault

data "azurerm_key_vault" "workspace" {
name = var.key_vault_name
resource_group_name = var.resource_group_name
}

data "azurerm_key_vault_secret" "sp_secret" {
name = local.sp_secret_name
key_vault_id = data.azurerm_key_vault.workspace.id
}

# 2 - get reference to ADO Project

data "azuredevops_project" "team" {
project_name = local.project_name
}

# 3 -get Subscription Info

data "azurerm_subscription" "current" {
}

data "azurerm_client_config" "current" {
}

# 4 - finally create Service Connection in ADO project

resource "azuredevops_serviceendpoint_azurerm" "workspace_endpoint" {
project_id = data.azuredevops_project.team.id
service_endpoint_name = local.connection_name
credentials {
serviceprincipalid = var.service_principal_id
serviceprincipalkey = data.azurerm_key_vault_secret.sp_secret.value
}
azurerm_spn_tenantid = data.azurerm_client_config.current.tenant_id
azurerm_subscription_id = data.azurerm_client_config.current.subscription_id
azurerm_subscription_name = data.azurerm_subscription.current.display_name
}
3 changes: 3 additions & 0 deletions modules/azure-devops-service-connection/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "service_connection" {
value = azuredevops_serviceendpoint_azurerm.workspace_endpoint
}
26 changes: 26 additions & 0 deletions modules/azure-devops-service-connection/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
variable "service_principal_id" {
type = string
description = "ID of Service Principal scoped to workspace/environment. The display name of this service principal uses `fruits-dev-XXXX-rg-sp` format, where `X` is a random character."
}

variable "key_vault_name" {
type = string
description = "Name of Key Vault of this workspace, e.g. `fruits-dev-XXXX-kv`, where `X` is a random character."
}

variable "resource_group_name" {
type = string
description = "Name of resource group of this workspace, e.g. `fruits-dev-XXXX-rg`, where `X` is a random character."
}

locals {
sp_secret_name = "workspace-sp-secret"
connection_name = "${var.resource_group_name}-connection"
project_name = split("-", var.resource_group_name)[0] == "infra" ? "central-it" : "project-${split("-", var.resource_group_name)[0]}"
}

# Note: ADO project names are determined based on Resource Group name patterns:
#
# - fruits-dev-u6t7-rg
# - veggies-prod-u6t7-rg
# - infra-shared-u6t7-rg (breaks convetion)
12 changes: 12 additions & 0 deletions modules/azure-devops-service-connection/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
terraform {
required_providers {
azuredevops = {
source = "terraform-providers/azuredevops"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 2.30.0"
}
}
required_version = ">= 0.13"
}
Loading

0 comments on commit 7c1f8d9

Please sign in to comment.