FAST CI/CD Azure Devops support via project template (#3616)

* azd wip

* azd wip

* azd wip

* wip

* wip

* wip

* plan/apply pipelines

* wip

* wip

* cross-repo auth, README improvements

* README

* README

* README

* module sources

* self hosted agents

* tfdoc

* tested

* test

* anonymize examples

* boilerplate

* yamllint

* yamllint
This commit is contained in:
Ludovico Magnocavallo
2026-01-03 12:59:24 +01:00
committed by GitHub
parent 3c2adbec2d
commit 04cf0c9d95
12 changed files with 1242 additions and 4 deletions

View File

@@ -0,0 +1,260 @@
# Azure Devops Pipelines via Workload Identity Federation
<!-- BEGIN TOC -->
- [Project Configuration](#project-configuration)
- [Organization Policies](#organization-policies)
- [Data Access Logs](#data-access-logs)
- [Hosted vs Managed Agents](#hosted-vs-managed-agents)
- [Workload Identity Federation](#workload-identity-federation)
- [Azure Devops Service Connections](#azure-devops-service-connections)
- [GCP Workload Identity Federation Pool and Providers](#gcp-workload-identity-federation-pool-and-providers)
- [IAM principals](#iam-principals)
- [Agent Configuration](#agent-configuration)
- [Pipeline Configuration](#pipeline-configuration)
- [Included Examples](#included-examples)
- [Branch Policies and Permissions](#branch-policies-and-permissions)
- [Module Authentication (Optional)](#module-authentication-optional)
<!-- END TOC -->
## Project Configuration
The provided `project.yaml` file provides an initial configuration, and makes a few assumptions that are explained in this section. This setup will only cover direct requirements for the Azure Devops integration, and assume that environment-specific customizations (project id, parent folder reference, specific IAM settings, region, etc.) will be implemented by the user.
### Organization Policies
A big part of the configuration outlined here involves setting up a Workload Identity Federation pool and provider. Depending on the organizational setup, this might require relaxing organization policies like shown in the `project.yaml` file contained in this example.
Comment this out if this constraint is enforced in the parent hierarchy.
```yaml
org_policies:
iam.workloadIdentityPoolProviders:
rules:
- allow:
all: true
```
### Data Access Logs
Workload Identity Federation often requires some amount of troubleshooting to make sure the IAM principals match the assertions in the tokens provided by the external IdP (Azure Devops in this case). This is made easier by turning on data access logs for specific services, like in this example, and also allows logging token exchanges.
Comment this out if you don't need to troubleshoot, or don't want to track token exchanges.
```yaml
data_access_logs:
iam.googleapis.com:
ADMIN_READ: {}
DATA_READ: {}
DATA_WRITE: {}
sts.googleapis.com:
ADMIN_READ: {}
DATA_READ: {}
DATA_WRITE: {}
```
### Hosted vs Managed Agents
The services enabled at the project level only include those required for Microsoft hosted agents. If you plan on running self hosted agents on Compute instances, additional configuration needs to be uncommented.
```yaml
services:
# TODO: uncomment for self hosted agent on GCP
# - artifactregistry.googleapis.com
# - compute.googleapis.com
```
The same applies to the `vm-default` service account, which is only needed for self hosted agents and needs to be uncommented. Keep the rest of the definitions in both `iam_principals` and `service_accounts`, which are not shown here.
```yaml
iam_by_principals:
# TODO: uncomment for self hosted agent on GCP
# $iam_principals:service_accounts/_self_/vm-default:
# - roles/artifactregistry.reader
# - roles/logging.logWriter
# - roles/monitoring.metricWriter
service_accounts:
# TODO: uncomment for self hosted agent on GCP
# vm-default:
# display_name: VM default service account.
```
And for self hosted agents instances a network is required, if using Shared VPC also edit and uncomment the following.
```yaml
# TODO: uncomment for self hosted agent on GCP
# shared_vpc_service_config:
# host_project: $project_ids:dev-spoke-0
```
## Workload Identity Federation
The pattern implemented here is the one we typically follow for infrastructure-level CI/CD, where two separate principals are used for each Terraform root module / state: a read-only one for PR checks, and a read-write one for merges.
This allows running potentially unsafe code in PRs which have not yet been reviewed in a sort of sandbox, where a read-only principal is used to run checks and Terraform plan, thus preventing any change to resources.
On the Azure Devops side, this requires setting up one Service Connection per principal (read-only and read-write), and then mapping to a dedicated Workload Identity provider. This is required, since the claims in the Azure Devops JWT token do not contain any information about the branch or job used for the pipeline context, and only provide the Service Connection id as a usable attribute.
This also forces using two separate pipelines for each of the principals, as the Service Connection access grants are done at the pipeline level.
### Azure Devops Service Connections
On the Azure Devops side, configure two Service Connections as explained in the ["Prepare your external IdP"](https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines#prepare) section of the Workload Identity documentation.
Once the service connections are configured, copy the "Issuer" and "Subject identifier" attributes displayed in the Service Connection's "Workload Identity federation details", which will be used to configure the WIF providers and associated IAM principals.
Your pipelines will need to be authorized to use these service connections, but this is a simple step that is shown in the UI when the pipelines is run for the first time. Remember to allow each pipeline usage of their respective Service Connection, so as to prevent use of the read-write principal from the PR pipeline.
### GCP Workload Identity Federation Pool and Providers
Other than the issuers coming from the Service Connections, one additional source of information is also needed before we can complete the WIF providers configurations.
The providers allow defining an attribute condition, which is used to restrict the set of supported tokens. This is entirely optional, but it's good practice to define it as a preventive control, to avoid the risk of accepting tokens originating from other customers' Azure Devops organizations.
The attribute condition is a CEL expression that checks assertions in the JWT token. The only usable information presented in tokens generated by Azure Devops are the object ID of the Azure Devops enterprise application (`assertion.oid`) and the Azure tenant ID (`assertion.tid`). These IDs can be found in your Azure AD tenant, typically under 'Enterprise applications' for your Azure DevOps organization's application (for `oid`), and 'Properties' for the Tenant ID (`tid`). The condition in the example below only uses `oid`, but you can mix and match depending on specific needs.
The last bit of information needed for the WIF providers configurations is the set of allowed audiences, which in this case is entirely static and contains a single object id for the AAD Token Exchange Endpoint.
Find the following definitions in the `project.yaml` file and edit them to reflect your desired values. As explained above, when multiple pipelines need to be mapped to different IAM principals on the GCP side, one Service Connection and one WIF provider are needed for each of them. The WIF pool though can stay the same.
For convenience, we also copy/paste the second part of the subject identifiers shown in the Service Connections details (everything after `/sc/`) in a comment, as we'll need them later to configure IAM.
```yaml
workload_identity_pools:
# pool name on GCP
cicd-0:
display_name: CI/CD pool.
providers:
# provider name on GCP, multiple providers are supported here
az-test-0-ro:
# TODO: copy everything after `/sc` in the service connection sub
# sub: 5d2face9-4998-4294-8d24-763e98b6af3e/ddf48e36-d2cc-4aed-b863-abcdefghi
display_name: Azure Devops test (read-only).
# TODO: use the AZD enterprise application object id in your Entra
# the Azure tenant id (assertion.tid) can also be used
attribute_condition: assertion.oid=="6f90190a-864b-4915-a9b2-abcdefghi"
attribute_mapping:
google.subject: assertion.sub.split("/sc/")[1]
identity_provider:
oidc:
# TODO: use the issuer displayed in the service connection details
issuer_uri: https://login.microsoftonline.com/a659ec42-b896-4739-824b-abcdefghi/v2.0
# you do not need to change this
allowed_audiences:
- fb60f99c-7a34-4190-8149-302f77469936
# provider name on GCP, multiple providers are supported here
az-test-0-rw:
# TODO: copy everything after `/sc` in the service connection sub
# sub: 5d2face9-4998-4294-8d24-763e98b6af3e/20cef207-7699-4013-b4bf-704cebeb0037
display_name: Azure Devops test (read-write).
# TODO: use the same condition defined for the ro provider above
attribute_condition: assertion.oid=="6f90190a-864b-4915-a9b2-abcdefghi"
# TODO: use the same mapping defined for the ro provider above
attribute_mapping:
google.subject: assertion.sub.split("/sc/")[1]
identity_provider:
oidc:
# TODO: use the issuer displayed in the service connection details
# it should be identical to the one defined for the ro provider
issuer_uri: https://login.microsoftonline.com/a659ec42-b896-4739-824b-abcdefghi/v2.0
# you do not need to change this
allowed_audiences:
- fb60f99c-7a34-4190-8149-302f77469936
```
### IAM principals
As explained above, IAM principals for Azure Devops tokens will only be able to use the assertion subject identifier as the only defining claim. Azure Devops does not populate project, repository, or pipeline information in the token so each Service Connection is only defined by its subject identifier, and can then only be mapped to a single principal on the GCP side.
Using the subject identifier also poses a different problem, as its length often exceeds the number of characters supported in Workload Identity Federation mappings. To work around this problem, the mapping is defined as `assertion.sub.split("/sc/")[1]`: the subject identifier is split into two parts, and only the second part (which contains the service connection id) is kept.
So for a Service Connection that defines this subject (the read-only one in the example above):
```txt
/eid1/c/pub/t/QuxZppa4OUeCSx113vjOOg/a/rISbSSETf0KqFyZ8ppdXmA/sc/5d2face9-4998-4294-8d24-abcdefghi/ddf48e36-d2cc-4aed-b863-abcdefghi
```
The `google.subject` mapping that identifies a IAM principal will only use the part after `/sc/`, which is composed of your Azure Devops [organization id](https://stackoverflow.com/a/67871296) and the id of the Service Connection:
```txt
5d2face9-4998-4294-8d24-abcdefghi/ddf48e36-d2cc-4aed-b863-abcdefghi
```
So we finally get to the IAM principal, which for the read-only service connection above has this form:
```txt
principal://iam.googleapis.com/projects/[project number]/locations/global/workloadIdentityPools/[pool name]/subject/5d2face9-4998-4294-8d24-abcdefghi/ddf48e36-d2cc-4aed-b863-abcdefghi
```
The principals are of course different for the read-only and read-write pipelines, allowing us to grant them impersonation to the respective service accounts on the GCP side.
## Agent Configuration
To run pipelines one or more agent pools are needed, using either [Microsoft-hosted](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=windows-images%2Cyaml) or [self-hosted](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/linux-agent?view=azure-devops&tabs=IP-V4) agents.
Using Microsoft-hosted agents is simpler as there's no need to manage actual instances, but allows less control over the environment and tools used by the pipelines (`gcloud`, `terraform`, etc.). If a tool is needed, it needs to be fetched at runtime by the pipeline either via a preconfigured [Task](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/?view=azure-pipelines) or a dedicated step.
Self-hosted agents require a lot more configuration to be set up, but allow for more customizations at the instance level. They can also be run via Docker images, which allow even simpler packaging of third-party tools.
This example uses Microsoft-hosted agents, but also provides some basic starting points for hosted agents running Docker images on GCP via Container Optimized OS. Refer to the [self-hosted-agents](./self-hosted-agents) folder documentation for more information on those.
## Pipeline Configuration
### Included Examples
Three sample pipelines are provided as examples:
- `sample-pipeline.yaml`: a very simple pipeline that can be used to verify the credentials exchange flow
- `pr-pipeline.yaml`: a "PR pipeline" that runs Terraform init, validate, and plan on pull requests. It posts the plan output as a comment to the PR and updates the PR status.
- `merge-pipeline.yaml`: a "merge pipeline" that runs Terraform init, validate, and apply on merges to the main branch.
Each of the above pipelines needs to be edited to match your project id and resource names. Once that has been done, the code can be copy/pasted on a new pipeline in Azure Devops. On first run, you might be asked to grant permissions to the pipeline on the service connection. Refer to the Azure Devops [Pipelines Schema Reference](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/view=azure-pipelines) can be used for further customizations.
### Branch Policies and Permissions
To enable the PR pipeline to function correctly, specifically to trigger on PR creation and post comments/status checks back to the PR, two key configurations are required in Azure DevOps:
1. **Branch Policies (Build Validation):**
- Navigate to **Project Settings** -> **Repositories**.
- Select your repository and go to the **Policies** tab.
- Select the target branch (e.g., `main`).
- Under **Build Validation**, add a new policy.
- Select your pipeline and ensure the "Trigger" is set to "Automatic".
- *Note:* The `pr:` trigger in the YAML file is effectively ignored unless this policy is in place.
2. **Build Service Permissions:**
- Navigate to **Project Settings** -> **Repositories**.
- Select your repository and go to the **Security** tab.
- Locate the **Build Service** accounts (e.g., "Project Collection Build Service" and "[Project Name] Build Service").
- Set the **"Contribute to pull requests"** permission to **Allow**.
- *Reason:* This permission is required for the pipeline to post the Terraform Plan output as a comment and to update the PR status check using the System Access Token.
3. **Status Check Policy (Optional but Recommended):**
- Under **Branch Policies** -> **Status Checks**, add a new check.
- Status to check: `Terraform Plan` (this name must match what the pipeline posts).
- Policy requirement: "Required".
- *Reason:* This ensures the PR cannot be merged unless the Terraform Plan step in the pipeline explicitly reports "succeeded".
### Module Authentication (Optional)
If the Terraform code references modules from a private Azure repository (instead of local files, or a public repository), some further configuration is needed. The following assumes that the modules repository is in the same Azure Devops project, if the repository is in a different project some additional configuration steps are needed, they are not outlined here as this is beyond the scope of this example.
The example pipelines include a step to configure Git to use the `System.AccessToken`.
```bash
git config --global url."https://$SYSTEM_ACCESSTOKEN@dev.azure.com".insteadOf "https://dev.azure.com"
```
**Permissions:**
To allow the pipeline to fetch these modules, you must ensure the **Build Service** account has **Read** access to the target repository, and the pipeline itself is explicitly granted access:
- Go to **Project Settings** -> **Repositories**.
- Select the repository hosting the modules.
- Go to **Security**.
- Add/Select "Project Collection Build Service" (and/or "[Project Name] Build Service").
- Set **Read** to **Allow**.
- In the same **Security** dialog, locate the "Pipelines" section. Explicitly add your pipeline(s) (e.g., `pr-pipeline`, `merge-pipeline`) and grant them **Read** access to this repository. This provides an additional layer of authorization for specific pipeline runs.
**Important:** You must also disable a restrictive default setting that limits token scope:
- Go to **Project Settings** -> **Pipelines** -> **Settings**.
- Ensure **"Protect access to repositories in YAML pipelines"** (or "Limit job authorization scope to referenced Azure DevOps repositories") is **Disabled (Unchecked)**. If enabled, this setting prevents the System Access Token from accessing repositories even if permissions are granted.

View File

@@ -0,0 +1,90 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
trigger:
branches:
include:
- main
resources:
repositories:
- repository: modules
type: git
name: fast-test-ms-hosted/modules
pr: none
variables:
- name: Azure.WorkloadIdentity.Connection
# TODO: edit to match the name of the rw service connection
value: az-test-0-rw
- name: AZURE_TOKEN
value: $(Pipeline.Workspace)/token.txt
- name: FAST_SERVICE_ACCOUNT
# TODO: edit to match the rw service account email
value: az-test-0-rw@tf-playground-svpc-azd-0.iam.gserviceaccount.com
- name: FAST_WIF_PROVIDER
# TODO: edit to match your WIF provider name
value: projects/592545000114/locations/global/workloadIdentityPools/cicd-0/providers/az-test-0-rw
- name: GOOGLE_CREDENTIALS
value: cicd-sa-credentials.json
pool:
name: Azure Pipelines
vmImage: 'ubuntu-latest'
jobs:
- job: gcp
displayName: Interact with GCP resources.
steps:
- checkout: self
- task: TerraformInstaller@1
displayName: Install Terraform 1.14.3
inputs:
terraformVersion: 1.14.3
- task: AzureCLI@2
displayName: 'Run Terraform Apply'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
connectedServiceNameARM: $(Azure.WorkloadIdentity.Connection)
addSpnToEnvironment: true
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
set -u
set -e
set -o pipefail
# Configure Git to use the System Access Token via Authorization header
git config --global http.https://dev.azure.com.extraHeader "AUTHORIZATION: bearer $SYSTEM_ACCESSTOKEN"
# Setup GCP Auth
sudo snap install google-cloud-sdk --classic
echo $idToken > $AZURE_TOKEN
gcloud iam workload-identity-pools create-cred-config \
$FAST_WIF_PROVIDER \
--service-account=$FAST_SERVICE_ACCOUNT \
--service-account-token-lifetime-seconds=900 \
--output-file=$GOOGLE_CREDENTIALS \
--credential-source-file=$AZURE_TOKEN
gcloud config set auth/credential_file_override $GOOGLE_CREDENTIALS
# Terraform Execution
echo "Initializing Terraform..."
terraform init -no-color
echo "Validating Terraform configuration..."
terraform validate -no-color
echo "Applying Terraform configuration..."
terraform apply -auto-approve -no-color

View File

@@ -0,0 +1,205 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
trigger: none
resources:
repositories:
- repository: modules
type: git
name: fast-test-ms-hosted/modules
pr:
branches:
include:
- main
variables:
- name: Azure.WorkloadIdentity.Connection
# TODO: edit to match the name of the ro service connection
value: az-test-0-ro
- name: AZURE_TOKEN
value: $(Pipeline.Workspace)/token.txt
- name: FAST_SERVICE_ACCOUNT
# TODO: edit to match the ro service account email
value: az-test-0-ro@tf-playground-svpc-azd-0.iam.gserviceaccount.com
- name: FAST_WIF_PROVIDER
# TODO: edit to match your WIF provider name
value: projects/592545000114/locations/global/workloadIdentityPools/cicd-0/providers/az-test-0-ro
- name: GOOGLE_CREDENTIALS
value: cicd-sa-credentials.json
pool:
name: Azure Pipelines
vmImage: 'ubuntu-latest'
jobs:
- job: gcp
displayName: Interact with GCP resources.
steps:
- checkout: self
- task: TerraformInstaller@1
displayName: Install Terraform 1.14.3
inputs:
terraformVersion: 1.14.3
- task: AzureCLI@2
displayName: 'Run Terraform and Post to PR'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
connectedServiceNameARM: $(Azure.WorkloadIdentity.Connection)
addSpnToEnvironment: true
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
set -u
set -o pipefail
LOG_FILE="terraform_execution.log"
touch "$LOG_FILE"
# Configure Git to use the System Access Token via Authorization header
git config --global http.https://dev.azure.com.extraHeader "AUTHORIZATION: bearer $SYSTEM_ACCESSTOKEN"
post_to_pr() {
local exit_code=$1
local content_file=$2
if [[ "$BUILD_REASON" == "PullRequest" ]]; then
echo "Posting results to Pull Request..."
# Read the log output
RAW_PLAN_OUTPUT=$(cat "$content_file")
# Construct API URLs
BASE_URL="${SYSTEM_COLLECTIONURI}${SYSTEM_TEAMPROJECT}/_apis/git/repositories/${BUILD_REPOSITORY_NAME}/pullRequests/${SYSTEM_PULLREQUEST_PULLREQUESTID}"
THREADS_URL="${BASE_URL}/threads?api-version=7.1-preview.1"
STATUS_URL="${BASE_URL}/statuses?api-version=7.1-preview.1"
# Determine status and header
if [ $exit_code -eq 0 ]; then
HEADER="## Terraform Success 🚀"
STATUS_STATE="succeeded"
STATUS_DESC="Terraform plan completed successfully."
else
HEADER="## Terraform Failed ❌"
STATUS_STATE="failed"
STATUS_DESC="Terraform plan failed."
fi
# --- 1. Post Comment ---
# Create comment payload with collapsible section, passing RAW_PLAN_OUTPUT directly
COMMENT_PAYLOAD=$(jq -n \
--arg header "$HEADER" \
--arg raw_plan "$RAW_PLAN_OUTPUT" \
'{
"comments": [
{
"parentCommentId": 0,
"content": "\($header)\n\n<details>\n<summary>Click to view full output</summary>\n\n```text\n\($raw_plan)\n```\n</details>",
"commentType": 1
}
],
"status": 1
}')
echo "Posting comment..."
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
-d "$COMMENT_PAYLOAD" \
"$THREADS_URL"
# --- 2. Post Status Check ---
BUILD_URL="${SYSTEM_COLLECTIONURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}"
STATUS_PAYLOAD=$(jq -n \
--arg state "$STATUS_STATE" \
--arg desc "$STATUS_DESC" \
--arg url "$BUILD_URL" \
'{
"state": $state,
"description": $desc,
"context": {
"name": "Terraform Plan",
"genre": "terraform"
},
"targetUrl": $url
}')
echo "Posting status check..."
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
-d "$STATUS_PAYLOAD" \
"$STATUS_URL"
else
echo "Not a Pull Request, skipping comment and status posting."
fi
}
run_tf_command() {
local cmd="$1"
echo "Running: $cmd"
echo "--------------------------------------------------" >> "$LOG_FILE"
echo "Running: $cmd" >> "$LOG_FILE"
echo "--------------------------------------------------" >> "$LOG_FILE"
# Run command, capture stdout and stderr to temp file, then append to log
# We use a pipe to tee to stdout so we can see progress in the pipeline logs too
if eval "$cmd" 2>&1 | tee -a "$LOG_FILE"; then
echo "" >> "$LOG_FILE"
return 0
else
echo "" >> "$LOG_FILE"
echo "Command failed." >> "$LOG_FILE"
return 1
fi
}
# Setup GCP Auth
sudo snap install google-cloud-sdk --classic
echo $idToken > $AZURE_TOKEN
gcloud iam workload-identity-pools create-cred-config \
$FAST_WIF_PROVIDER \
--service-account=$FAST_SERVICE_ACCOUNT \
--service-account-token-lifetime-seconds=900 \
--output-file=$GOOGLE_CREDENTIALS \
--credential-source-file=$AZURE_TOKEN
gcloud config set auth/credential_file_override $GOOGLE_CREDENTIALS
# Terraform Execution
EXIT_CODE=0
run_tf_command "terraform init -no-color" || EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
run_tf_command "terraform validate -no-color" || EXIT_CODE=$?
fi
if [ $EXIT_CODE -eq 0 ]; then
run_tf_command "terraform plan -out=tfplan -no-color -lock=false" || EXIT_CODE=$?
# If plan succeeded, append the readable plan to the log
if [ $EXIT_CODE -eq 0 ]; then
echo "--------------------------------------------------" >> "$LOG_FILE"
echo "Plan Details:" >> "$LOG_FILE"
echo "--------------------------------------------------" >> "$LOG_FILE"
terraform show -no-color tfplan >> "$LOG_FILE" 2>&1
fi
fi
# Post results
post_to_pr $EXIT_CODE "$LOG_FILE"
# Exit with the captured code
exit $EXIT_CODE

View File

@@ -0,0 +1,63 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
trigger:
branches:
include:
- main
variables:
- name: Azure.WorkloadIdentity.Connection
# TODO: edit to match the name of the ro service connection
value: az-test-0-ro
- name: AZURE_TOKEN
value: $(Pipeline.Workspace)/token.txt
- name: FAST_SERVICE_ACCOUNT
# TODO: edit to match the rw service account email
value: az-test-0-ro@myprj.iam.gserviceaccount.com
- name: FAST_WIF_PROVIDER
# TODO: edit to match your WIF provider name
value: projects/592545000114/locations/global/workloadIdentityPools/azd-0/providers/az-test-0-ro
- name: GOOGLE_CREDENTIALS
value: cicd-sa-credentials.json
pool:
name: Azure Pipelines
vmImage: 'ubuntu-latest'
jobs:
- job: gcp
displayName: Interact with GCP resources.
steps:
- task: TerraformInstaller@1
displayName: Install Terraform 1.14.3
inputs:
terraformVersion: 1.14.3
- task: AzureCLI@2
inputs:
connectedServiceNameARM: $(Azure.WorkloadIdentity.Connection)
addSpnToEnvironment: true
scriptType: 'bash'
scriptLocation: 'inlineScript'
# # failOnStandardError: true
inlineScript: |
sudo snap install google-cloud-sdk --classic
echo $idToken > $AZURE_TOKEN
gcloud iam workload-identity-pools create-cred-config \
$FAST_WIF_PROVIDER \
--service-account=$FAST_SERVICE_ACCOUNT \
--service-account-token-lifetime-seconds=900 \
--output-file=$GOOGLE_CREDENTIALS \
--credential-source-file=$AZURE_TOKEN
gcloud config set auth/credential_file_override $GOOGLE_CREDENTIALS
# gcloud projects list
terraform init
terraform apply -auto-approve -no-color

View File

@@ -0,0 +1,109 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# yaml-language-server: $schema=https://cdn.jsdelivr.net/gh/GoogleCloudPlatform/cloud-foundation-fabric@fast-dev/modules/project-factory/schemas/project.schema.json
# TODO: set a parent folder if needed
# parent: $folder_ids:shared
iam_by_principals:
$iam_principals:service_accounts/_self_/az-test-0-ro:
- roles/viewer
$iam_principals:service_accounts/_self_/az-test-0-rw:
- roles/editor
# TODO: uncomment for self hosted agent on GCP
# $iam_principals:service_accounts/_self_/vm-default:
# - roles/artifactregistry.reader
# - roles/logging.logWriter
# - roles/monitoring.metricWriter
data_access_logs:
iam.googleapis.com:
ADMIN_READ: {}
DATA_READ: {}
DATA_WRITE: {}
sts.googleapis.com:
ADMIN_READ: {}
DATA_READ: {}
DATA_WRITE: {}
services:
# TODO: uncomment for self hosted agent on GCP
# - artifactregistry.googleapis.com
# - compute.googleapis.com
- iam.googleapis.com
- logging.googleapis.com
- monitoring.googleapis.com
- sts.googleapis.com
org_policies:
iam.workloadIdentityPoolProviders:
rules:
- allow:
all: true
service_accounts:
az-test-0-ro:
display_name: Azure Devops test pipeline (read-only).
# TODO: change the project number to match yours and uncomment
# iam:
# roles/iam.workloadIdentityUser:
# - principal://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/cicd-0/subject/5d2face9-4998-4294-8d24-763e98b6af3e/ddf48e36-d2cc-4aed-b863-1234567890
az-test-0-rw:
display_name: Azure Devops test pipeline (read-write).
# TODO: change the project number to match yours and uncomment
# iam:
# roles/iam.workloadIdentityUser:
# - principal://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/cicd-0/subject/5d2face9-4998-4294-8d24-763e98b6af3e/20cef207-7699-4013-b4bf-1234567890
# TODO: uncomment for self hosted agent on GCP
# vm-default:
# display_name: VM default service account.
# TODO: uncomment for self hosted agent on GCP
# shared_vpc_service_config:
# host_project: $project_ids:dev-spoke-0
workload_identity_pools:
# pool name on GCP
cicd-0:
display_name: CI/CD pool.
providers:
# provider name on GCP, multiple providers are supported here
az-test-0-ro:
# TODO: copy everything after `/sc` in the service connection sub
# sub: 5d2face9-4998-4294-8d24-763e98b6af3e/ddf48e36-d2cc-4aed-b863-1234567890
display_name: Azure Devops test (read-only).
# TODO: use the AZD enterprise application object id in your Entra
# the Azure tenant id (assertion.tid) can also be used
attribute_condition: assertion.oid=="6f90190a-864b-4915-a9b2-abcdefghi"
attribute_mapping:
google.subject: assertion.sub.split("/sc/")[1]
identity_provider:
oidc:
# TODO: use the issuer displayed in the service connection details
issuer_uri: https://login.microsoftonline.com/a659ec42-b896-4739-824b-abcdefghi/v2.0
# you do not need to change this
allowed_audiences:
- fb60f99c-7a34-4190-8149-302f77469936
# provider name on GCP, multiple providers are supported here
az-test-0-rw:
# TODO: copy everything after `/sc` in the service connection sub
# sub: 5d2face9-4998-4294-8d24-763e98b6af3e/20cef207-7699-4013-b4bf-1234567890
display_name: Azure Devops test (read-write).
# TODO: use the same condition defined for the ro provider above
attribute_condition: assertion.oid=="6f90190a-864b-4915-a9b2-abcdefghi"
# TODO: use the same mapping defined for the ro provider above
attribute_mapping:
google.subject: assertion.sub.split("/sc/")[1]
identity_provider:
oidc:
# TODO: use the issuer displayed in the service connection details
# it should be identical to the one defined for the ro provider
issuer_uri: https://login.microsoftonline.com/a659ec42-b896-4739-824b-abcdefghi/v2.0
# you do not need to change this
allowed_audiences:
- fb60f99c-7a34-4190-8149-302f77469936

View File

@@ -0,0 +1 @@
token.txt

View File

@@ -0,0 +1,217 @@
# Self-Hosted Agents
If self-hosted agents are required, a sample Container Optimized OS based agent is provided as part of this example.
<!-- BEGIN TOC -->
- [Project-level Requirements](#project-level-requirements)
- [Azure Devops Requirements](#azure-devops-requirements)
- [First Terraform Apply: Docker Registry and Secret](#first-terraform-apply-docker-registry-and-secret)
- [Docker Image](#docker-image)
- [Agent Instance](#agent-instance)
- [Extending this Example](#extending-this-example)
- [Docker Image Customizations](#docker-image-customizations)
- [GCP Infrastructure Scale-up](#gcp-infrastructure-scale-up)
- [Variables](#variables)
- [Outputs](#outputs)
- [Test](#test)
<!-- END TOC -->
## Project-level Requirements
Some requirements are needed at the project level for this example to work. If you are creating the project with the [project file provided in the parent folder](../project.yaml), simply [follow the instructions in the parent README](../README.md#hosted-vs-managed-agents), uncomment the relevant lines, and run the project factory to update the project.
If you are using a pre-existing project or you created one by hand, go through the requirements described above and mirror them in your project configuration.
One last requirement for running self-hosted agents is Internet connectivity. Check the Azure Devops documentation for details on which hosts and ports are needed.
## Azure Devops Requirements
Some additional requirements are needed on the Azure Devops side:
- [create an agent token on Azure Devops](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/personal-access-token-agent-registration?view=azure-devops) and save it to a local `token.txt` file
- define an agent pool in the organization and not down its name
## First Terraform Apply: Docker Registry and Secret
The first Terraform apply is used to create an Artifact Registry to host the custom Docker image for the agent, and the secret that contains the agent token.
Create a `terraform.tfvars` file and configure the variables needed at this stage, as in the following example. You probably should also configure a backend to persist state remotely, which is a common enough task when using Terraform and not explicitly covered here.
```hcl
agent_config = {
# TODO: Azure Devops instance (organization)
instance = "myorg"
# TODO: Azure Devops agent pool name
pool_name = "hosted agent"
# TODO: make sure token file exists at first apply
token = {
file = "token.txt"
}
}
# TODO: set GCP resource location, defaults to "europe-west8"
location = "europe-west1"
# TODO: GCP project id
project_id = "my-prj"
```
Some additional variables can be customized if their defaults don't match the desired configuration, or if the Azure Devops token changes:
- `agent_config.agent_name` defaults to "Test Agent on GCP"
- `agent_config.token.version` needs to be changed whenever a new token needs to be saved in the secret, this defaults to `1` so just bump the number if needed
- `name` name used for GCP resources, defaults to "azd"
The Azure Devops agent token in the `token.txt` file is stored in the secret using a Terraform write-only attribute: it will not be persisted in state, and the `token.txt` file is only needed on first apply and can then be removed.
If you need to change the token, for example to update expiration, simply put the new token in a `token.txt` file and increment the number in `agent_config.token.version` so the secret is updated.
Once the Terraform configuration has been saved, run `terraform init` and `terraform apply`.
## Docker Image
This example bootstraps a [self-hosted agent in Docker](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/docker?view=azure-devops#linux), so a Docker image is needed. Follow the [instructions in the Azure Devops documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/docker?view=azure-devops#create-and-build-the-dockerfile-1) to prepare and build the image.
Once the image has been built, tag it and push it to the Artifact Registry. The registry URL is provided in the Terraform output, so either copy it from the `apply` run in the previous step, or run `terraform output`. The principal pushing the image needs the `roles/artifactregistry.writer` on the registry: check your project configuration and set it if it's missing.
The image can of course be customized to include the tools required by the pipelines, like for example `gcloud` or `terraform`, so as to save time at each job run.
## Agent Instance
Once the image has been pushed, edit your `terraform.tfvars` and add the instance-level configuration like in the following example.
```hcl
instance_config = {
# TODO: full path of the Docker image
docker_image = "europe-west8-docker.pkg.dev/my-prj/azd-docker/azp-agent:latest"
# TODO: service account for the instance
service_account = "vm-default@my-prj.iam.gserviceaccount.com"
# TODO: network configuration
vpc_config = {
network = "projects/my-net-prj/global/networks/dev-spoke-0"
subnetwork = "projects/my-net-prj/regions/europe-west8/subnetworks/gce"
}
}
```
The instance service account needs the `roles/artifactregistry.reader` role on the registry to be able to pull the image. This module automatically grants this role to the configured service account.
Once the Terraform configuration has been saved, run `terraform init` and `terraform apply`, then check that the instance is up and the agent is connected to your Azure Devops pool.
## Extending this Example
This example provides a minimal working solution, whose main goal is bringing up a working agent without changes to the assets provided in the official documentation. Once the example has been verified to be working, it's fairly easy to use it as a basis for further customizations.
### Docker Image Customizations
One simple area of improvement is the Docker image, where tools needed by the pipelines can be embedded in the image to simplify jobs and reduce run time. The main drawback of this approach is deviating from the providing agent Dockerfile, and the need to keep it sync with updates to the code provided in the documentation.
As a simple example, the following changes to the Dockerfile allow embedding gcloud and terraform in the container image by using a different base image, and slightly tweaking the original Dockerfile.
```dockerfile
# FROM python:3-alpine
FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
ARG TERRAFORM_VERSION=1.14.0
ENV TARGETARCH="linux-musl-x64"
# Another option:
# FROM arm64v8/alpine
# ENV TARGETARCH="linux-musl-arm64"
# Alpine packages
RUN apk update && \
apk upgrade && \
apk add \
bash curl gcc git icu-libs jq musl-dev python3-dev libffi-dev \
openssl-dev cargo make py-pip unzip
# Terraform
RUN curl -LO https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
mv terraform /usr/local/bin/ && \
rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip
# Azure CLI
RUN pip install --break-system-packages azure-cli
WORKDIR /azp/
COPY ./start.sh ./
RUN chmod 755 ./start.sh
RUN adduser -D agent
RUN chown agent ./
USER agent
# Another option is to run the agent as root.
# ENV AGENT_ALLOW_RUNASROOT="true"
ENTRYPOINT [ "./start.sh" ]
```
Another possible change is fetching the secret from within the container. This has minimal security benefits (the token would still be available via container `exec`), but removes the need of an additional systemd unit. The following snippet in the container's `start.sh` script can be used with the above Dockerfile to fetch the secret at runtime.
```bash
# Azure token from GCP secret
if [[ -n "${GCP_PROJECT}" && -n "${GCP_SECRET}" ]]; then
gcloud secrets versions access latest \
--secret $GCP_SECRET --project $GCP_PROJECT >/azp/token.txt
AZP_TOKEN_FILE="/azp/token.txt"
echo "Token retrieved from GCP secret"
else
echo "Not retrieving token from GCP secret"
fi
```
### GCP Infrastructure Scale-up
Another customization area is on the GCP infrastructure side, where the simple instance code in this example can be easily changed to an instance template, and then wrapped into a Managed Instance Group to provide autohealing, easier scaling, or even autoscaling. This is very easy to do by leveraging the examples in the [`compute-vm`](../../../../modules/compute-vm/) and [`compute-mig`](../../../../modules/compute-mig/) modules.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [agent_config](variables.tf#L17) | Agent configuration. | <code title="object&#40;&#123;&#10; agent_name &#61; optional&#40;string, &#34;Test Agent on GCP&#34;&#41;&#10; instance &#61; string&#10; pool_name &#61; string&#10; token &#61; object&#40;&#123;&#10; file &#61; string&#10; version &#61; optional&#40;number, 1&#41;&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [project_id](variables.tf#L58) | Project id where resources will be created. | <code>string</code> | ✓ | |
| [instance_config](variables.tf#L30) | Instance configuration. | <code title="object&#40;&#123;&#10; docker_image &#61; string&#10; service_account &#61; string&#10; zone &#61; optional&#40;string, &#34;b&#34;&#41;&#10; vpc_config &#61; object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10; &#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [location](variables.tf#L45) | Location used for regional resources. | <code>string</code> | | <code>&#34;europe-west8&#34;</code> |
| [name](variables.tf#L51) | Prefix used for resource names. | <code>string</code> | | <code>&#34;azd&#34;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [docker_registry](outputs.tf#L17) | Docker registry URL. | |
| [secret](outputs.tf#L22) | Azure token secret. | |
| [ssh_command](outputs.tf#L27) | Command to SSH to the agent instance. | |
| [vpcsc_command](outputs.tf#L35) | Command to allow egress to remotes from inside a perimeter. | |
<!-- END TFDOC -->
## Test
```hcl
module "test-agent" {
source = "./fabric/fast/project-templates/devops-azure-wif/self-hosted-agents"
agent_config = {
instance = "myorg"
pool_name = "Self Hosted"
token = {
file = "token.txt"
}
}
instance_config = {
docker_image = "europe-west8-docker.pkg.dev/my-prj/azd-docker/azd-agent:orig"
service_account = "vm-default@my-prj.iam.gserviceaccount.com"
vpc_config = {
network = "projects/my-net-prj/global/networks/dev-spoke-0"
subnetwork = "projects/my-net-prj/regions/europe-west8/subnetworks/gce"
}
}
project_id = "my-prj"
}
# tftest modules=4 resources=6
```

View File

@@ -0,0 +1,93 @@
#cloud-config
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
write_files:
# generate root docker auth configuration
- path: /run/azp-agent/.docker/config.json
permissions: "0644"
owner: root
content: |
{
"auths": {},
"credHelpers": {
"asia.gcr.io": "gcr",
"eu.gcr.io": "gcr",
"${location}-docker.pkg.dev": "gcr",
"gcr.io": "gcr",
"marketplace.gcr.io": "gcr",
"us.gcr.io": "gcr"
}
}
# limit docker log size
- path: /var/lib/docker/daemon.json
permissions: "0644"
owner: root
content: |
{
"live-restore": true,
"storage-driver": "overlay2",
"log-opts": {
"max-size": "1024m"
}
}
# agent token service
- path: /etc/systemd/system/azp-token.service
permissions: "0644"
owner: root
content: |
[Unit]
Description=Azure Devops Agent Token
After=gcr-online.target docker.socket
Before=azp-agent.service
Wants=gcr-online.target docker.socket docker-events-collector.service
[Service]
Type=oneshot
RemainAfterExit=true
Environment="HOME=/run/azp-agent"
ExecStart=/usr/bin/docker run --rm \
-v /run/azp-agent:/azp-agent \
gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine \
gcloud secrets versions access latest \
--secret ${name} --project ${project_id} --out-file=/azp-agent/token.txt
ExecStart=chgrp 1000 /run/azp-agent/token.txt
ExecStart=chmod 640 /run/azp-agent/token.txt
ExecStop=rm -f /run/azp-agent/token.txt
# agent container service
- path: /etc/systemd/system/azp-agent.service
permissions: "0644"
owner: root
content: |
[Unit]
Description=Start Azure Pipelines Agent container
After=gcr-online.target docker.socket azp-token.service
Wants=gcr-online.target docker.socket docker-events-collector.service azp-token.service
[Service]
Environment="HOME=/run/azp-agent"
ExecStart=/usr/bin/docker run --rm --name=azp-agent \
-v /run/azp-agent/token.txt:/token.txt:ro \
-e AZP_TOKEN_FILE=/token.txt \
-e AZP_URL="https://dev.azure.com/${instance}" \
-e AZP_POOL="${pool_name}" \
-e AZP_AGENT_NAME="${agent_name}" \
${image}
ExecStop=/usr/bin/docker stop azp-agent
bootcmd:
- systemctl start node-problem-detector
runcmd:
# - iptables -I INPUT 1 -p tcp -m tcp --dport 8080 -m state --state NEW,ESTABLISHED -j ACCEPT
- systemctl daemon-reload
- systemctl start azp-token
- systemctl start azp-agent

View File

@@ -0,0 +1,95 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
cloud_config = !local.create_instance ? null : templatefile(
"${path.module}/cloud-config.yaml", merge(var.agent_config, {
image = var.instance_config.docker_image
location = var.location
name = var.name
project_id = var.project_id
})
)
create_instance = (
try(var.instance_config.docker_image, null) == null ? false : true
)
}
module "registry" {
source = "../../../../modules/artifact-registry"
project_id = var.project_id
location = var.location
name = "${var.name}-docker"
format = {
docker = {
standard = {}
}
}
iam = var.instance_config == null ? {} : {
"roles/artifactregistry.reader" = [
"serviceAccount:${var.instance_config.service_account}"
]
}
}
module "secret" {
source = "../../../../modules/secret-manager"
project_id = var.project_id
secrets = {
(var.name) = {
iam = var.instance_config == null ? {} : {
"roles/secretmanager.secretAccessor" = [
"serviceAccount:${var.instance_config.service_account}"
]
}
versions = {
"v-${var.agent_config.token.version}" = {
data = try(file(var.agent_config.token.file), null)
data_config = {
write_only_version = var.agent_config.token.version
}
}
}
}
}
}
module "instance" {
source = "../../../../modules/compute-vm"
count = local.create_instance ? 1 : 0
project_id = var.project_id
zone = "${var.location}-${var.instance_config.zone}"
name = "${var.name}-agent"
instance_type = "e2-micro"
boot_disk = {
auto_delete = false
initialize_params = {
image = "projects/cos-cloud/global/images/family/cos-117-lts"
size = 10
}
}
network_interfaces = [{
network = var.instance_config.vpc_config.network
subnetwork = var.instance_config.vpc_config.subnetwork
}]
metadata = {
user-data = local.cloud_config
}
service_account = {
email = var.instance_config.service_account
}
depends_on = [module.secret]
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "docker_registry" {
description = "Docker registry URL."
value = module.registry.url
}
output "secret" {
description = "Azure token secret."
value = module.secret.secrets[var.name].id
}
output "ssh_command" {
description = "Command to SSH to the agent instance."
value = nonsensitive(try(
"gcloud compute ssh ${module.instance[0].instance.name} --zone ${module.instance[0].instance.zone} --project ${var.project_id}",
null
))
}
output "vpcsc_command" {
description = "Command to allow egress to remotes from inside a perimeter."
value = (
"gcloud artifacts vpcsc-config allow --project=${var.project_id} --location=${var.location}"
)
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "agent_config" {
description = "Agent configuration."
type = object({
agent_name = optional(string, "Test Agent on GCP")
instance = string
pool_name = string
token = object({
file = string
version = optional(number, 1)
})
})
}
variable "instance_config" {
description = "Instance configuration."
type = object({
docker_image = string
service_account = string
zone = optional(string, "b")
vpc_config = object({
network = string
subnetwork = string
})
})
nullable = true
default = null
}
variable "location" {
description = "Location used for regional resources."
type = string
default = "europe-west8"
}
variable "name" {
description = "Prefix used for resource names."
type = string
nullable = true
default = "azd"
}
variable "project_id" {
description = "Project id where resources will be created."
type = string
}

View File

@@ -204,16 +204,19 @@ org_policies_imports = [
Once org policies have been imported, the variable definition can be removed from the tfvars file.
#### Importing existing organization level IAM bindings
For brownfield implementations you may need to import existing organization IAM policies. These snippets can help you add existing settings into the YAML file.
Scripts below require [yq](https://github.com/mikefarah/yq/) in at least version 4. It was tested using yq `v4.47.2`.
To create `iam:` part of the `/organization/.config.yaml` file, you can use following snippet:
```shell
gcloud <resource> get-iam-policy <resource name> | yq '.bindings | map({"key": .role, "value": .members}) | from_entries'
```
To create `iam_by_principals:` part of the factory YAML file, you can use following snippet:
```shell
gcloud <resource> get-iam-policy <resource name> | yq '
[.bindings | .[] | .members[] as $member | { "member": $member, "role": .role}] |
@@ -321,7 +324,7 @@ This dataset implements a design where internal tenants are given control over p
### Enabling Optional Features
The "Classic FAST" dataset is designed to be more lightweight than the "Hardened FAST" dataset regarding controls and policies.
The "Classic FAST" dataset is designed to be more lightweight than the "Hardened FAST" dataset regarding controls and policies.
But, it fully supports more advanced features like SCC Custom SHA modules and Observability factories if needed.
Note that the configuration described below is already implemented when using the "Hardened FAST" dataset.
@@ -347,7 +350,6 @@ To configure and provision observability resources such as log-based metrics and
1. Create a folder `datasets/classic/observability`.
2. Place your monitoring alerts and log-based metrics in this folder. Sample of existing alerts and log-based metrics can be found in the [hardened dataset](./datasets/hardened/observability).
## Detailed configuration
The following sections explain how to configure and run this stage, and should be read in sequence when using it for the first time.
@@ -655,7 +657,7 @@ The provided project configurations also create several key resources for the st
### CI/CD configuration
CI/CD support is implemented via two different sets of connfigurations:
CI/CD support is implemented via two different sets of configurations:
- [Workload Identity](https://docs.cloud.google.com/iam/docs/workload-identity-federation) providers are defined in project configurations
- CI/CD service accounts and templated workflow generation are defined in a dedicated configuration (`var.factories_config.cicd_workflows`).
@@ -685,7 +687,7 @@ workload_identity_pools:
The above configuration can be easily extended to support multiple pools and providers, and is not limited to OpenId Connect but can also leverage other provider types. Check the project module or project schema for the full interface.
Once one or more providers have been defined they can be referenced in the CI/CD cofniguration file. The following example defines a workflow configuration for this stage.
Once one or more providers have been defined they can be referenced in the CI/CD configuration file. The following example defines a workflow configuration for this stage.
```yaml
# cicd-workflows.yaml
@@ -713,6 +715,8 @@ org-setup:
The configuration prepares a sample workflow file for the target repository, and configures IAM on the service accounts referenced in the configuration, so that repository tokens can impersonate them via the Workload Identity provider.
The above setup supports GitHub and Gitlab providers out of the box. Azure Devops is also supported via a separate, more complex configuration [defined in a dedicated project template](../../project-templates/devops-azure-wif/).
#### Read-write and read-only impersonation
The access pattern implemented above allows impersonation from any branch to the read-only (`-ro`) service account, and impersonation from explicitly mentioned branches to the read-write (`rw`) service account. This ensures that PR-related actions run with limited privileges, and higher level privileges are only used for merges after PR checks and approvals. If a more relaxed approach where any branch can access the read-write service account, simply omit the `repository.apply_branches` block.