Break A Loop Additional Resource
Break a loop with an additional resource
This example demonstrates how to break a loop where two resources have to both depend on each others outputs.
How the example works
There is often a mutual loop assigning a principal to a Kubernetes service account to enable Workload Identity. The Kubernetes service account often needs to be annotated with the principal and the principal needs some policy to allow it to be used by the Kubernetes Service Account.
In this example, we will simulate the graph using a k8s-service-account
and a fake aws-role
resource both implemented with the
humanitec/template
driver.
graph LR
workload --> k8s-service-account
k8s-service-account --> aws-role
aws-role --> k8s-service-account
The loop arises because it is necessary to generate both the Kubernetes service account name and the aws-role dynamically. This is because these need to be unique for each Workload in the Application. Essentially, both resources require the same two pieces of information.
There are two ways to break the loop:
-
Convention
Decide that each resource “knows” how to generate both pieces of information. This can be achieved by using the context to provide the unique element.
This has the downside that it is inflexible and limiting. For example, if a 3rd party system is used to issue principals, then this technique will not work.
-
Add an additional resource
Both the
k8s-service-account
andaws-role
resources get both the service account name and role ID from a 3rd resource.This approach ensures consistency, does not rely on convention and allows for complex scenarios like getting IDs from a 3rd party system.
graph LR
workload --> k8s-service-account
k8s-service-account --> aws-role
k8s-service-account --> config
aws-role --> config
score.yaml
(
view on GitHub
)
:
apiVersion: score.dev/v1b1
metadata:
name: example-workload
containers:
busybox:
image: busybox:latest
variables:
BUCKET_NAME: ${resources.my-s3.bucket}
command:
- /bin/sh
args:
- "-c"
# This will output all of the environment variables in the container to
# STDOUT every 15 seconds. This can be seen in the container logs in the
# Humanitec UI.
- "while true; do set; sleep 15; done"
resources:
my-s3:
type: s3
Resource Definitions
def-aws-role.yaml
(
view on GitHub
)
:
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: example-aws-role
entity:
criteria:
- app_id: example-break-a-loop-additional-resource
driver_inputs:
values:
role_arn: ${resources['config.sa-name-role-id'].outputs.role_arn}
sa_name: ${resources['config.sa-name-role-id'].outputs.sa_name}
templates:
outputs: |
arn: {{ .drivers.role_arn }}
driver_type: humanitec/template
name: example-aws-role
type: aws-role
def-config-name-id.yaml
(
view on GitHub
)
:
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: example-config-name-id
entity:
criteria:
# We only match to class and not res_id because the res_id changes for
# each workload
- class: sa-name-role-id
app_id: example-break-a-loop-additional-resource
driver_inputs:
values:
res_id: ${context.res.id}
app_id: ${context.app.id}
templates:
init: |
workload_id: {{ .driver.values.res_id | splitList "." | last }}
outputs: |
role_arn: "arn:aws:iam::123456789012:role/{{ .driver.values.app_id }}/sa-role-{{ .init.workload_id }}"
sa_name: {{ .init.workload_id }}-sa
driver_type: humanitec/template
name: example-config-name-id
type: config
def-k8s-service-account.yaml
(
view on GitHub
)
:
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: example-k8s-service-account
entity:
criteria:
- app_id: example-break-a-loop-additional-resource
driver_inputs:
values:
role_arn: ${resources['config.sa-name-role-id'].outputs.role_arn}
sa_name: ${resources['config.sa-name-role-id'].outputs.sa_name}
templates:
outputs: |
name: {{ .driver.values.sa_name }}
manifests: |
service-account.yaml:
location: namespace
data: |
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .driver.values.sa_name }}
annotations:
eks.amazonaws.com/role-arn: {{ .driver.values.role_arn }}
driver_type: humanitec/template
name: example-k8s-service-account
type: k8s-service-account
def-workload.yaml
(
view on GitHub
)
:
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: example-workload
entity:
criteria:
- app_id: example-break-a-loop-additional-resource
driver_inputs:
values:
templates:
outputs: |
update:
- op: add
path: /spec/serviceAccountName
{{/*
The resource reference does not specify ID or class so the ID and
class of the workload being provisioned will be used.
*//}
value: ${resources.k8s-service-account.outputs.name}
driver_type: humanitec/template
name: example-workload
type: workload
def-aws-role.tf
(
view on GitHub
)
:
resource "humanitec_resource_definition" "example-aws-role" {
driver_type = "humanitec/template"
id = "example-aws-role"
name = "example-aws-role"
type = "aws-role"
driver_inputs = {
values_string = jsonencode({
"role_arn" = "$${resources['config.sa-name-role-id'].outputs.role_arn}"
"sa_name" = "$${resources['config.sa-name-role-id'].outputs.sa_name}"
"templates" = {
"outputs" = "arn: {{ .drivers.role_arn }}\n"
}
})
}
}
resource "humanitec_resource_definition_criteria" "example-aws-role_criteria_0" {
resource_definition_id = resource.humanitec_resource_definition.example-aws-role.id
app_id = "example-break-a-loop-additional-resource"
}
def-config-name-id.tf
(
view on GitHub
)
:
resource "humanitec_resource_definition" "example-config-name-id" {
driver_type = "humanitec/template"
id = "example-config-name-id"
name = "example-config-name-id"
type = "config"
driver_inputs = {
values_string = jsonencode({
"res_id" = "$${context.res.id}"
"app_id" = "$${context.app.id}"
"templates" = {
"init" = "workload_id: {{ .driver.values.res_id | splitList \".\" | last }}\n"
"outputs" = <<END_OF_TEXT
role_arn: "arn:aws:iam::123456789012:role/{{ .driver.values.app_id }}/sa-role-{{ .init.workload_id }}"
sa_name: {{ .init.workload_id }}-sa
END_OF_TEXT
}
})
}
}
resource "humanitec_resource_definition_criteria" "example-config-name-id_criteria_0" {
resource_definition_id = resource.humanitec_resource_definition.example-config-name-id.id
class = "sa-name-role-id"
app_id = "example-break-a-loop-additional-resource"
}
def-k8s-service-account.tf
(
view on GitHub
)
:
resource "humanitec_resource_definition" "example-k8s-service-account" {
driver_type = "humanitec/template"
id = "example-k8s-service-account"
name = "example-k8s-service-account"
type = "k8s-service-account"
driver_inputs = {
values_string = jsonencode({
"role_arn" = "$${resources['config.sa-name-role-id'].outputs.role_arn}"
"sa_name" = "$${resources['config.sa-name-role-id'].outputs.sa_name}"
"templates" = {
"outputs" = "name: {{ .driver.values.sa_name }}\n"
"manifests" = <<END_OF_TEXT
service-account.yaml:
location: namespace
data: |
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .driver.values.sa_name }}
annotations:
eks.amazonaws.com/role-arn: {{ .driver.values.role_arn }}
END_OF_TEXT
}
})
}
}
resource "humanitec_resource_definition_criteria" "example-k8s-service-account_criteria_0" {
resource_definition_id = resource.humanitec_resource_definition.example-k8s-service-account.id
app_id = "example-break-a-loop-additional-resource"
}
def-workload.tf
(
view on GitHub
)
:
resource "humanitec_resource_definition" "example-workload" {
driver_type = "humanitec/template"
id = "example-workload"
name = "example-workload"
type = "workload"
driver_inputs = {
values_string = jsonencode({
"templates" = {
"outputs" = <<END_OF_TEXT
update:
- op: add
path: /spec/serviceAccountName
{{/*
The resource reference does not specify ID or class so the ID and
class of the workload being provisioned will be used.
*//}
value: $${resources.k8s-service-account.outputs.name}
END_OF_TEXT
}
})
}
}
resource "humanitec_resource_definition_criteria" "example-workload_criteria_0" {
resource_definition_id = resource.humanitec_resource_definition.example-workload.id
app_id = "example-break-a-loop-additional-resource"
}