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
app_role_assignments (Optional) A set of azuread_app_role_assignment resources to assign to the workload identity. Only built-in application roles are supported.

Example:
hcl
app_role_assignments = [
"IdentityRiskEvent.ReadWrite.All",
"IdentityRiskEvent.Read.All"
]
set(string) [] no
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)
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.

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(set(object({
resource_app_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
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
federated_identity_credential_config Map of Federated Credentials to create on the workload identity
map(object({
display_name = string
description = optional(string)
audiences = list(string)
issuer = string
subject = string
}))
{} no
group_membership Map of group object ids the workload identity should be member of.

Example:

group_membership = {
"Kubernetes Administrators" = azuread_group.k8s_admins.object_id
}
map(string) {} 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
managed_identity_name The name of the managed identity (identity provided to the workload) that is created. The final name is prefixed with mi-.

If a value is not provided, Station will set the name to mi-var.tfe.workspace_name-var.environment_name
string null 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_assignment 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.

- assign_to_workload_principal assigns the role to the workload identity. Can not be used with principal_id.
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)
}))
{} no
role_definition_name_on_workload_rg The name of an in-built role to assign the workload identity on the workload resource group string "Owner" 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

- tfe.create_federated_identity_credential configures Federated Credentials on the workload identity for plan and apply phases.
- 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)
}))
create_federated_identity_credential = optional(bool)
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 = ["IdentityRiskEvent.ReadWrite.All"]
group_memberships = {
"Kubernetes Administrators" = azuread_group.k8s_admins.object_id
}
}
}
map(object({
name = string
resource_group_name = optional(string)
location = optional(string)
app_role_assignments = optional(set(string))
role_assignments = optional(map(object({
name = optional(string)
scope = string
role_definition_id = optional(string)
role_definition_name = 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))
}))
{} no

Outputs

Name Description
applications n/a
client_id n/a
groups n/a
resource_group n/a
resource_groups_user_specified n/a
subscription_id n/a
tenant_id n/a
tfe n/a
user_assigned_identities 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