Skip to content

Use Station to create secure and automated environments for your workloads in Azure

License

Notifications You must be signed in to change notification settings

blinqas/station

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Station Terraform Module

Station is a Terraform module that lets you quickly spin up new workload environments in Azure and Terraform Cloud. Station gives you a high level of automation for workload environment provisioning.

  • Station is maintained by the DevOps team at blinQ (https://blinq.no).
  • See the terraform-docs.md file for terraform-docs generated documentation.
  • Check out our Design Decision document here.

Why does Station exist?

To quickly enable users to deploy workload environments in Azure. Isolating Entra ID and Azure Subscription interactions from the actual workload environment. Station consists of three parts; bootstrap, deployments and workload environment.

  • Bootstrap: setting up a Station for your Azure subscription(s) and tenant. (Administrator with permissions on Subscription and Entra ID/Azure AD)
  • Deployments: Where workload environments are defined and deployed. (Application Team/DevOps/SRE/Platform Engineer/Cloud Engineer)
  • Workload Environment: The workload environment where infrastructure is deployed to. (Application Team)

Station was designed with isolation in mind. We want our environments to work with least-privilege principle. That's why your workload identity is restricted to permissions inside its own resource group(s). The module is highly flexible and also support Cloud Adoption Framework-like modularization. See our COMING! examples folder for more!

Who uses Station?

Station is used primarily in context of application development and hosting; DevOps, GitOps, SRE's or Platform Engineers. There is nothing wrong with using Station in operations; we encourage it!


Requirements

  • Terraform Cloud account
    • Permission to create Team Token
  • Azure- Tenant and Subscription
    • Global Administrator on Azure AD
    • Owner on Subscription

Usage

The following example deploys a workload environment for common resources, in this environment we would deploy Container Registries for example.

Consider the following file structure:

common.tf
github_repositories.tf
tags.tf
variables.tf
# filename: common.tf
module "common" {
  source              = "git::https://github.com/blinqas/station.git?ref=1.3.0"
  tenant_id           = var.tenant_id
  subscription_id     = var.subscription_id
  environment_name    = "prod"
  resource_group_name = "common"
  tags                = local.tags.common
  tfe = {
    organization_name     = "my-tfc-organization"
    project_name          = "Azure"
    workspace_name        = "common"
    workspace_description = "Common resources which are shared between workloads."
    vcs_repo = {
      identifier     = github_repository.repos["common"].full_name
      branch         = "trunk"
      oauth_token_id = var.vcs_repo_oauth_token_id
    }
  }
}

This file would provision the following:

  • Resource Group
  • Managed Identity
    • Service Principal is assigned Owner on Resource Group
  • Federated Credential (OIDC to authenticate Terraform Cloud runners)
  • Terraform Cloud (TFC) Workspace
    • Configured to run on commits to trunk branch
    • Configured to authenticate to VCS with token already in Terraform Cloud
  • TFC Environment Variables for OIDC authentication with Managed Identity

Contact

License

MIT

Inputs

Name Description Type Default Required
applications Map of applications to create. The body of each object is more or less identical to azuread_application
with the exception of map usage instead of blocks (as blocks are impossible to define with HCL)
Example:
applications = {
example_client = {
display_name = "Example app"
owners = local.users["admin_user"]
sign_in_audience = "AzureADMyOrg"
identifier_uris = ["api://station-test"]
group_membership_claims = ["All"]
prevent_duplicate_names = true
fallback_public_client_enabled = true
notes = "This is an example application"
logo_image = filebase64("./assets/application_logos/example.png")

required_resource_access = {
graph = {
resource_app_id = azuread_service_principal.MicrosoftGraph.client_id
resource_object_id = azuread_service_principal.MicrosoftGraph.object_id
resource_access = {
application_user_read_all = {
id = "df021288-bdef-4463-88db-98f22de89214"
type = "Role" //Application
}
delegated_user_read = {
id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
type = "Scope" //Delegated
},
}
},
exchange_online = {
auto_admin_consent = true //This will require admin consent after the deployment
resource_app_id = azuread_service_principal.Office365ExchangeOnline.client_id
resource_object_id = azuread_service_principal.Office365ExchangeOnline.object_id
resource_access = {
delegated_ews_accessasuser_all = {
id = "3b5f3d61-589b-4a3c-a359-5dd4b5ee5bd5"
type = "Scope" //Application
},
application_ews_accessasuser_all = {
id = "dc890d15-9560-4a4c-9b7f-a736ec74ec40"
type = "Role" //Delegated
}
}
}
}

web = {
implicit_grant = {
access_token_issuance_enabled = true
}
}

service_principal = {
account_enabled = true
app_role_assignment_required = true
description = "Service Principal for example app"
owners = local.users["admin_user"]
use_existing = false
}
}
map(object({
display_name = string
owners = optional(list(string))
logo_image = optional(string) #Base64 encoded image
sign_in_audience = optional(string)
group_membership_claims = optional(list(string))
identifier_uris = optional(list(string))
prevent_duplicate_names = optional(bool)
fallback_public_client_enabled = optional(bool)
notes = optional(string) #This can be used as description for the application. 1024 character limit.
use_existing = optional(bool)

single_page_application = optional(object({
redirect_uris = optional(list(string))
}))

api = optional(object({
known_client_applications = optional(list(string))
mapped_claims_enabled = optional(bool)
requested_access_token_version = optional(number)

oauth2_permission_scope = optional(list(object({
admin_consent_description = string
admin_consent_display_name = string
id = string
enabled = optional(bool)
type = optional(string)
user_consent_description = optional(string)
user_consent_display_name = optional(string)
value = string
})))
}))

public_client = optional(object({
redirect_uris = optional(set(string))
}))

required_resource_access = optional(map(object({
auto_admin_consent = optional(bool, true)
resource_app_id = string
resource_object_id = string
resource_access = map(object({
id = string
type = string
}))
})))

optional_claims = optional(object({
access_token = optional(set(object({
name = string
source = optional(string)
essential = optional(bool)
additional_properties = optional(list(string))
})))
id_token = optional(set(object({
name = string
source = optional(string)
essential = optional(bool)
additional_properties = optional(list(string))
})))
saml2_token = optional(set(object({
name = string
source = optional(string)
essential = optional(bool)
additional_properties = optional(list(string))
})))
}))

web = optional(object({
homepage_url = optional(string)
logout_url = optional(string)
redirect_uris = optional(set(string))
implicit_grant = optional(object({
access_token_issuance_enabled = optional(bool)
id_token_issuance_enabled = optional(bool)
}))
}))

service_principal = optional(object({
account_enabled = optional(bool, true)
alternative_names = optional(list(string))
app_role_assignment_required = optional(bool, false)
description = optional(string)
login_url = optional(string)
notes = optional(string)
notification_email_addresses = optional(list(string))
owners = optional(list(string))
preferred_single_sign_on_mode = optional(string)
tags = optional(list(string))
use_existing = optional(bool, false)

feature_tags = optional(object({
custom_single_sign_on = optional(bool, false)
enterprise = optional(bool, false)
gallery = optional(bool, false)
hide = optional(bool, false)
}))

saml_single_sign_on = optional(object({
relay_state = optional(string)
}))
}))
}))
{} no
connectivity Use this block to configure connectivity of this Landing Zone. Connectivity can be virtual networks, subnets, and even peerings to other virtual networks.

Limitations:
- Connecting Virtual Networks in different resource groups managed by this landing zone is currently unavailable. Configure this manually in the landing zone configuration.
- The key used for a peering object must be unique across all connectivity objects
map(object({
virtual_network_name = string
tags = optional(map(string), {})
address_space = set(string)
resource_group_name = optional(string)
location = optional(string)
bgp_community = optional(string)
ddos_protection_plan = optional(object({
id = string
enable = string
}))
encryption = optional(object({
enforcement = string
}))
dns_servers = optional(set(string))
edge_zone = optional(string)
flow_timeout_in_minutes = optional(string)
private_endpoint_vnet_policies = optional(string, "Disabled")
subnets = map(object({
name = string
address_prefixes = list(string)
security_group = optional(string)
delegation = optional(map(object({
name = string
service_delegation = object({
name = string
actions = optional(set(string))
})
})))
default_outbound_access_enabled = optional(bool, true)
private_endpoint_network_policies = optional(string, "Disabled")
private_link_service_network_policies_enabled = optional(bool, true)
service_endpoints = optional(set(string))
service_endpoint_policy_ids = optional(set(string))
route_table_id = optional(string)
}))
peerings = optional(map(object({
name = string
remote_virtual_network_id = string
allow_virtual_network_access = optional(bool, true)
allow_forwarded_traffic = optional(bool, false)
allow_gateway_transit = optional(bool, false)
local_subnet_names = optional(list(string), [])
only_ipv6_peering_enabled = optional(bool)
peer_complete_virtual_networks_enabled = optional(bool, true)
remote_subnet_names = optional(list(string), [])
use_remote_gateways = optional(bool, false)
triggers = optional(object({
remote_address_space = string
}))
})))
})
)
{} no
default_location The name of the default location to deploy workload resources to. string "norwayeast" no
environment_name The name of the deployment environment for the workload. Ex: dev/staging/production string "dev" no
groups (Optional) Map of Entra ID (Azure AD) groups to create
Note: The workload identity is automatically assigned the App Role "User.ReadBasic.All" and "Group.Read.All"
because being "Owner" of the group is not sufficient to add principals and then list them after an add or delete operation.
map(object({
display_name = string
description = optional(string)
owners = optional(list(string))
members = optional(set(string))
security_enabled = optional(bool)
mail_enabled = optional(bool)
types = optional(set(string))
dynamic_membership = optional(object({
enabled = bool
rule = string
}))
role_assignments = optional(map(object({
name = optional(string)
scope = optional(string)
role_definition_id = optional(string)
role_definition_name = optional(string)
condition = optional(string)
condition_version = optional(string)
description = optional(string)
skip_service_principal_aad_check = optional(bool)
})))
}))
{} no
identity Configuration for the workload identity. This is the identity that is used to perform the Terraform plan and apply operations.

Example:
identity = {
name = "workload-prod" #Name will be prefixed with mi-

role_assignments = {
key_vault_admin = {
scope = null # Defaults to the resource groups created by the workload
role_definition_name = "Key Vault Administrator"
description = "Needed to manage key vaults"
}
}

app_role_assignments = {
"User.ReadBasic.All" = {
app_role_id = data.azuread_service_principals.well_known["MicrosoftGraph"].app_role_ids["User.ReadBasic.All"]
resource_object_id = data.azuread_service_principals.well_known["MicrosoftGraph"].object_id
}
}

group_memberships = {
"A group" = "ad-group-object-id"
}

directory_role_assignments = {
Reader = {
role_name = "Directory Readers"
}
}
}
object({
name = optional(string)
role_assignments = optional(map(object({
name = optional(string)
scope = optional(string)
role_definition_id = optional(string)
role_definition_name = optional(string)
condition = optional(string)
condition_version = optional(string)
delegated_managed_identity_resource_id = optional(string)
description = optional(string)
skip_service_principal_aad_check = optional(bool)
})), {})
group_memberships = optional(map(string), {})
app_role_assignments = optional(map(object({
app_role_id = string
resource_object_id = string
})), {})
directory_role_assignments = optional(map(object({
role_name = optional(string)
role_id = optional(string)
app_scope_id = optional(string)
directory_scope_id = optional(string)
})), {})
})
{} no
resource_group_name The name of the workload resource group. The final name is prefixed with rg-.

If a value is not provided, Station will set the name to rg-var.tfe.workspace_name-var.environment_name
string null no
resource_groups Map of resource groups to create
map(object({
name = string
location = optional(string)
tags = optional(map(string))
}))
{} no
role_assignments Map of role_assignments to create. Be careful of who is allowed to provision role_assignments, you might want to
consider Sentinel policies in TFC.
map(object({
name = optional(string)
scope = string
role_definition_id = optional(string)
role_definition_name = optional(string)
principal_id = optional(string)
condition = optional(string)
condition_version = optional(string)
delegated_managed_identity_resource_id = optional(string)
description = optional(string)
skip_service_principal_aad_check = optional(bool, false)
}))
{} no
subscription_id (Required) The Azure subscription ID used by the caller. string n/a yes
tags Tags to merge with the default tags configured by Station.

Station configures the following map in tags.tf:
{
"station-id" = random_id.workload.hex
"environment" = var.environment_name
}
map(string) {} no
tenant_id (Required) The Entra ID tenant ID used by the caller. string n/a yes
tfe Terraform Cloud configuration for the workload environment

- Either of tfe.vcs_repo.(oauth_token_id|github_app_installation_id) must be provided, both can not be used at the same time.
- tfe.workspace_env_vars lets you configure Environment Variables for the Terraform Cloud runtime environment
- tfe.workspace_vars lets you configure Terraform variables
- tfe.module_outputs_to_workspace_var.(groups|applications|user_assigned_identities) sets output from the respective
resource into respective Terraform variables on the Terraform Cloud workspace. Useful when you need group object ids
for the groups Station Deployments provisioned in your workload environment.
- tfe.workspace_settings lets you configure the workspace settings like agent_pool_id and execution_mode. If agent_pool_id is provided, execution_mode must be set to "agent".
object({
organization_name = string
project = object({
id = string
name = string
})
workspace_name = string
workspace_description = string
workspace_settings = optional(object({
agent_pool_id = optional(string)
execution_mode = optional(string)
}))
file_triggers_enabled = optional(bool)
vcs_repo = optional(object({
identifier = string
branch = optional(string)
ingress_submodules = optional(string)
oauth_token_id = optional(string)
github_app_installation_id = optional(string)
tags_regex = optional(string)
}))
workspace_env_vars = optional(map(object({
value = string
category = string
description = string
sensitive = optional(bool, false)
})))
workspace_vars = optional(map(object({
value = any
category = string
description = string
hcl = optional(bool, false)
sensitive = optional(bool, false)
})))
module_outputs_to_workspace_var = optional(object({
groups = optional(bool)
applications = optional(bool)
user_assigned_identities = optional(bool)
resource_groups = optional(bool)
role_definitions = optional(bool)
}))
})
null no
user_assigned_identities User Assigned Identities to create.

Example:

user_assigned_identities = {
my_app = {
name = "uai-my-identity"
resource_group_name = "rg-name"
location = "norwayeast"
app_role_assignments = {
Application.ReadWrite.OwnedBy = {
app_role_id = "18a4783c-866b-4cc7-a460-3d5e5662c884"
resource_object_id = "microsoft-graph-enterprise-app-object-id"
}
}
group_memberships = {
"Kubernetes Administrators" = azuread_group.k8s_admins.object_id
}
directory_role_assignments = {
role_name = "Application Administrator"
}
}
}
map(object({
name = string
resource_group_name = optional(string)
location = optional(string)
app_role_assignments = optional(map(object({
app_role_id = string
resource_object_id = string
})), {})
role_assignments = optional(map(object({
name = optional(string)
scope = string
role_definition_id = optional(string)
role_definition_name = optional(string)
principal_id = optional(string)
assign_to_workload_principal = optional(bool)
condition = optional(string)
condition_version = optional(string)
delegated_managed_identity_resource_id = optional(string)
description = optional(string)
skip_service_principal_aad_check = optional(bool)
})), {})
group_memberships = optional(map(string), {})
directory_role_assignments = optional(map(object({
role_name = optional(string)
app_scope_id = optional(string)
directory_scope_id = optional(string)
})), {})
}))
{} no

Outputs

Name Description
applications n/a
client_id n/a
groups n/a
landing_zone_identity n/a
peerings n/a
resource_group n/a
resource_groups_user_specified n/a
role_assignments Map of role assignments.

- lz_owner: Owner role assignment on the default landing zone resource group
- lz_identity: Role assignments created through var.identity.role_assignment
- others: Role assignments created through var.role_assignments
subscription_id n/a
tenant_id n/a
tfe n/a
user_assigned_identities n/a
virtual_networks n/a
workload_resource_group_name n/a
workload_service_principal_object_id n/a

About

Use Station to create secure and automated environments for your workloads in Azure

Resources

License

Stars

Watchers

Forks

Packages

No packages published