3793 words
19 min read

EKS Validation on a Real AWS Account: Twenty Cents a Run, Seven Failures No Mock Saw

By · Docker Captain · CNCF Ambassador · IBM Champion
EKS cluster modules weighed on a steel balance scale

Twenty cents. That is what it cost on 15 October, by the run’s own list-price estimate, to prove an EKS reference platform end to end on a real AWS account: 101 resources applied, all 19 Argo CD Applications Synced and Healthy on EKS 1.37, 14 checks passed, everything destroyed and nothing left behind. The seven runs before it, the same night, failed on seven things that the platform’s mocked Terraform tests and its daily suite on kind had passed for weeks. Every one of the seven is now a check.

In October I wrote that most of a Terraform posture can be tested with no AWS account at all, and that what a plan cannot see belongs in a separate, slower suite against a sandbox account. This is that suite’s first night. It is the lesson of planting bugs in infrastructure tests, one layer further out: a test proves what it can see, and a mocked provider cannot see AWS. A plan is a forecast. An apply is a measurement.

What a mocked provider cannot see#

The platform under test is a public reference platform for Amazon EKS: Terraform for the network, EKS and its add-ons, Argo CD for everything inside the cluster, Kyverno for admission, Karpenter for nodes, Velero for backups. Its Terraform tests run on every change against a mocked provider, and the whole platform runs on kind on every change and every day. The night of 15 October was the first time any of it met an AWS account. Each run reports its own length and estimates its cost from resource-hours at list prices, before data transfer, logs and load balancer capacity units.

RunLengthEstimateWhat stopped it
10.04 h$0.00The role refused the run at the first AWS call
20.02 h$0.00A rule name past an AWS limit, and a guardrail that denied the run’s role its own iam:GetRole
31.19 h$0.31Argo CD’s root Application stuck in Unknown for a reason its log does not name, and a residue search that failed a clean teardown
40.40 h$0.08Anonymous chart pulls from ECR Public refused
50.55 h$0.12Argo CD started before the cluster’s metrics API answered
60.59 h$0.13The same symptom, traced to the metrics-server add-on
71.72 h$0.4518 of 19 Applications healthy, then a load balancer outlived the cluster
80.81 h$0.20Passed every check and left nothing

The seven failures cost a dollar and nine cents by the same estimates, more than five times the run that passed. What they printed is what an engineer pastes into a search box, so the post quotes it verbatim.

# the seven failed validation runs, 15 October 2026, verbatim, account ID removed
Not authorized to perform sts:AssumeRoleWithWebIdentity
"name" cannot be longer than 64 characters: "reference-platform-38666856654-karpenter-capacity-reservation-interruption"
is not authorized to perform: iam:GetRole on resource: role platform-validation-create with an explicit deny in an identity-based policy
response status code 429: toomanyrequests: Data limit exceeded
error getting openapi resources: the server is currently unable to handle the request
DependencyViolation: The subnet 'subnet-0b2546018e28dfddd' has dependencies and cannot be deleted.

Three of the seven came from outside the repository, and nothing in it had changed. GitHub moved repositories created, renamed or transferred after 15 July 2026 to an immutable OIDC subject that carries the owner’s and the repository’s IDs, so a trust policy written for the old owner-and-name format refused every run before it started. ECR Public gives an anonymous client 500 gigabytes a month and one pull a second, and a hosted runner fetching Karpenter’s chart without logging in was told the data limit was exceeded. And the default metrics-server add-on for EKS 1.37, in October 2026, is built with Go 1.27, where the new JSON implementation is the default.

That last one took two runs. The cluster’s OpenAPI document still answered as JSON, which is how a person checks it, and failed as protobuf, which is how Argo CD reads it. Argo CD could not build its cluster cache, and the root Application sat in Unknown behind an error about a server that could not handle the request. The upstream issue has been open since 1 September, and on 15 October I added the EKS builds and the pin that works to it. For three days the platform held the add-on on its last Go 1.26 build. On 16 October the add-on’s thirteenth EKS build was reported fixed on the same issue: the same Go 1.27, with the new JSON implementation switched off. The platform moved to it on 18 October, and two checks keep the move honest. Before it creates anything, the weekly run asks EKS whether that build is offered for the cluster’s Kubernetes version. Before it installs anything, the bootstrap asks for the document the way Argo CD does.

Terminal window
# bootstrap/bootstrap.sh in aws-eks-reference-platform at v1.3.0, shortened: read the cluster's OpenAPI document the way Argo CD reads it, as protobuf
proxy_dir="$(mktemp -d)"
kubectl proxy --unix-socket="$proxy_dir/api.sock" >/dev/null 2>&1 &
proxy=$!
trap 'kill "$proxy" 2>/dev/null || true; rm -rf "$proxy_dir"' EXIT
code=000
for _ in $(seq 1 30); do
kill -0 "$proxy" 2>/dev/null || { echo "kubectl proxy exited; cannot ask for the OpenAPI document" >&2; exit 1; }
if [ -S "$proxy_dir/api.sock" ]; then
code="$(curl -s --max-time 30 --unix-socket "$proxy_dir/api.sock" -o /dev/null -w '%{http_code}' \
-H 'Accept: application/[email protected]+protobuf' http://localhost/openapi/v2 || true)"
[ "$code" = 200 ] && break
fi
sleep 10
done
# ... the proxy is stopped and its directory removed
if [ "$code" != 200 ]; then
echo "/openapi/v2 as protobuf answers HTTP $code; Argo CD cannot build its cluster cache on this cluster." >&2
# ... the upstream issue is named and the aggregated APIs are listed
exit 1
fi

The pin carries its own history, so the next person to lift it knows what it is guarding against.

# terraform/addons.tf in aws-eks-reference-platform at v1.3.0, shortened: the pin and the reason for it
# metrics-server is pinned to v0.9.0-eksbuild.13. eksbuild.12, the default
# for Kubernetes 1.37 in October 2026, is built with Go 1.27, whose default
# jsonv2 experiment makes kube-openapi leak field names into the schema it
# publishes; the API server then answers /openapi/v2 as protobuf with 503,
# and Argo CD cannot build its cluster cache ("error getting openapi
# resources: the server is currently unable to handle the request"). The
# first EKS validation runs found it, and the platform held eksbuild.11, the
# last build on Go 1.26. eksbuild.13 is Go 1.27 built with
# GOEXPERIMENT=nojsonv2, the fix AWS reported on
# https://github.com/kubernetes-sigs/metrics-server/issues/1868. Two checks
# keep it honest: the weekly run asks EKS before applying anything whether
# this build is offered for the cluster's Kubernetes version, and bootstrap
# reads /openapi/v2 as protobuf, so a build that brings the 503 back fails
# there.
resource "aws_eks_addon" "after_nodes" {
for_each = {
coredns = { service_account = null, role_arn = null, version = null }
metrics-server = { service_account = null, role_arn = null, version = "v0.9.0-eksbuild.13" }
aws-ebs-csi-driver = { service_account = "ebs-csi-controller-sa", role_arn = aws_iam_role.ebs_csi.arn, version = null }
}
# ...

The first scheduled run, on 21 October, put both checks to work on a real cluster. EKS offered the thirteenth build for Kubernetes 1.37, the document answered as protobuf, and every Application came up Synced and Healthy.

The other four were the platform’s own, and only the real API could judge them. An EventBridge rule name reached 74 characters once the cluster name carried the run’s ID, and AWS stops at 64. The guardrail that keeps workflow roles away from the bootstrap’s own IAM denied reads as well as writes, so Terraform could not look up the role it was running as. A residue search failed a teardown that had left nothing, because the tagging API kept listing a deleted NAT gateway, subnet, security group and flow log, and three keys that were only scheduled for deletion. And deleting Argo CD’s root Application cascaded through the load balancer controller before the Gateway’s Service was gone, so a network load balancer outlived the cluster and held the network open.

In the production environment I run, the first apply of anything new has always been where the account’s own rules showed up, never the plan.

Why this keeps happening#

A mocked provider answers with what the test’s author believed about AWS on the day the test was written. It has no quotas, no name limits, no identity formats and no default add-on builds, and it never changes on its own. Everything around it does. Three of the seven failures were changes and limits outside the repository. A fourth trap sits in AWS’s own defaults: Budgets counts credits unless told otherwise, so in an account running on promotional credits a budget alert waits for the credits to run out.

The kind suite has the opposite blind spot. It runs the Kubernetes the platform deploys, every day, and catches what admission, GitOps and backups do inside a cluster. It has no cloud controllers, no load balancers made by AWS, no IAM and no add-ons that someone else builds. Each layer proved what it could reach. None of them reached the account.

A long-lived staging cluster looks like the answer and is a different test. It proves the platform keeps running. It does not prove the platform can be built from nothing, which is the case that matters on the day a region, an account or a customer’s environment has to be created. It is the gap behind the restore drill on a clean machine, one layer up, and its bill runs whether anyone reads it or not.

Proof that never meets the account is a forecast.

Risk and blast radius#

Direct exposure is the first real apply happening when someone needs it: a rebuild after an incident, a new region, a customer’s account during onboarding. Every failure on this list would have arrived then, one at a time, with the clock running. For a solutions architect or a forward deployed engineer, that first apply happens in a customer’s account, in front of the customer. The night of 15 October found all seven in under five hours, for about a dollar.

Systemic exposure is the copy. A reference platform exists to be copied into other accounts, and every copy inherits the trust policy that no longer matches GitHub’s subject, the add-on build that blinds Argo CD and the teardown that leaves a load balancer billing. For a CISO, two of the seven sat in identity: a security improvement at GitHub that refuses every trust policy written the old way, and a guardrail strict enough to deny the role it protects. For a CTO or a finance lead, the credits default means a company living on credits can have budget alerts configured and still never receive one.

Three of the seven, and the credits default, are four questions any account can answer today, before anything is built.

Terminal window
# four questions for an account of your own. The first, the first half of the second and the fourth are adapted from
# aws-eks-reference-platform at v1.3.0 (terraform/bootstrap-aws/variables.tf and .github/workflows/eks-validation.yml).
# The third reads the setting that repository's budget turns off (terraform/bootstrap-aws/main.tf, include_credit = false).
# 1. The subject prefix GitHub writes into this repository's OIDC token, and what the role trusts. The role's sub must begin with it.
gh api repos/OWNER/NAME/actions/oidc/customization/sub --jq .sub_claim_prefix
aws iam get-role --role-name ROLE --query 'Role.AssumeRolePolicyDocument.Statement[].Condition' --output json
# 2. The metrics-server builds EKS offers for a Kubernetes version (here 1.37), and the build a cluster runs.
aws eks describe-addon-versions --kubernetes-version 1.37 --addon-name metrics-server \
--query 'addons[0].addonVersions[].addonVersion' --output text
aws eks describe-addon --cluster-name CLUSTER --addon-name metrics-server --query addon.addonVersion --output text
# 3. Whether each budget counts credits. True means the budget waits for the credits to run out.
aws budgets describe-budgets --account-id "$(aws sts get-caller-identity --query Account --output text)" \
--query 'Budgets[].[BudgetName,CostTypes.IncludeCredit]' --output text
# 4. Log in to ECR Public before pulling a chart from it, so the pull is the account's and not an anonymous one.
aws ecr-public get-login-password --region us-east-1 | helm registry login --username AWS --password-stdin public.ecr.aws

Ways to prove an EKS platform, compared#

Way to prove itWhat it costsWhat it catchesWhat it cannot catch
terraform test with a mocked provider, on every changenothing in AWS, minutes of runner timeposture and configuration drift, on every pushlimits, identity formats, defaults and add-on builds that AWS and others decide
e2e on kind, dailyrunner minutesadmission, GitOps sync, backups, the platform’s own behaviourcloud controllers, AWS load balancers, IAM, managed add-ons
a real account, weekly, the network, cluster and platform destroyed after each runabout 0.20arun,under0.20 a run, under 1 a monthfirst-apply failures, teardown residue, changes outside the repositoryscale, load, drift over months
a long-lived staging clusterabout $235 a month before workloads, at the list prices in the platform’s READMEdrift over time, upgrades in placea clean build from nothing, a clean teardown

The table is not a ranking. The first two run on every change because they are free and fast, and they stay. The third answers the one question the first two cannot, for less than a dollar a month.

None of the four is new: ephemeral environments and Terratest have applied real infrastructure from CI for years. What this night adds is a price and a list.

Framework: a ceiling, a clean teardown, a weekly run#

Layer 1: an account that can only spend up to a ceiling (week 1)#

Give the runs a member account of their own with nothing else in it, because the workflow roles can delete what they did not make. Put the ceiling in AWS Budgets, measured before credits, with an action that attaches a deny-all policy to the role that creates resources.

# kill-switch.tf: a standalone version for an account of your own, adapted from terraform/bootstrap-aws/main.tf in
# aws-eks-reference-platform, where the limits are variables ($10, $20 and $30 by default) and the role is the module's own.
# Alerts on usage before credits, then deny-all at the ceiling.
variable "creation_role_name" {
type = string
}
variable "alert_email" {
type = string
}
data "aws_caller_identity" "current" {}
resource "aws_budgets_budget" "validation" {
name = "platform-validation-monthly"
budget_type = "COST"
limit_amount = "30.00"
limit_unit = "USD"
time_unit = "MONTHLY"
cost_types {
include_credit = false # Budgets counts credits by default: on promotional credits nothing would ever fire.
}
dynamic "notification" {
for_each = [10, 20]
content {
comparison_operator = "GREATER_THAN"
threshold = notification.value
threshold_type = "ABSOLUTE_VALUE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.alert_email]
}
}
}
resource "aws_iam_policy" "deny_all" {
name = "platform-validation-deny-all"
policy = jsonencode({ Version = "2012-10-17", Statement = [{ Effect = "Deny", Action = "*", Resource = "*" }] })
}
resource "aws_iam_role" "budget_action" {
name = "platform-validation-budget-action"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "budgets.amazonaws.com" }
Condition = { StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id } }
}]
})
}
resource "aws_iam_role_policy" "budget_action" {
name = "attach-deny-all-to-the-creation-role"
role = aws_iam_role.budget_action.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["iam:AttachRolePolicy", "iam:DetachRolePolicy"]
Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.creation_role_name}"
Condition = { ArnEquals = { "iam:PolicyARN" = aws_iam_policy.deny_all.arn } }
}]
})
}
# At the ceiling, Budgets attaches deny-all to the creation role. The sweeper's role is not touched.
resource "aws_budgets_budget_action" "kill_switch" {
budget_name = aws_budgets_budget.validation.name
action_type = "APPLY_IAM_POLICY"
approval_model = "AUTOMATIC"
notification_type = "ACTUAL"
execution_role_arn = aws_iam_role.budget_action.arn
action_threshold {
action_threshold_type = "ABSOLUTE_VALUE"
action_threshold_value = 30
}
definition {
iam_action_definition {
policy_arn = aws_iam_policy.deny_all.arn
roles = [var.creation_role_name]
}
}
subscriber {
address = var.alert_email
subscription_type = "EMAIL"
}
}

The sweeper that cleans up behind the runs uses a second, delete-only role, which the switch does not touch, so cleaning goes on after creation has stopped. The switch is a ceiling for the month, not a brake on a run: AWS refreshes the billing data Budgets reads at least once a day, and a deny on the role stops new resources, not the ones already running. The brakes on a single run are its own timeout, the destroy job that runs whatever happened to the run, and the daily sweeper. Alerts at ten and twenty dollars and the switch at thirty make the ceiling a month of weekly runs with room for a bad night.

Owner: the platform team, with the alert address agreed with whoever owns the bill.

Layer 2: a teardown that proves it left nothing (week 2)#

Order decides whether a teardown finishes. Stop the GitOps controller first, by scaling it to zero rather than deleting its Applications, or the deletion cascades through the platform and takes the load balancer controller with it. Delete what makes AWS resources while the controllers that made them still run: Gateways, the remaining load balancer Services, Ingresses, Karpenter’s node pools and node claims, the volume claims. Then delete from AWS’s side, by the controllers’ tags, whatever they left, and only then let Terraform take the network. Then prove the account is clean, and do not trust the tagging API’s first answer.

Terminal window
# residue.sh: a standalone version for one run ID, adapted from scripts/aws-residue.sh in aws-eks-reference-platform,
# not that file's text. The tagging API lists what was just deleted, so ask each ARN
set -uo pipefail
run_id="${1:?run-id}"
left=0
exists() { # exists <arn>: 0 while the resource is there and not on its way out
local arn="$1" id="${1##*/}"
case "$arn" in
*:natgateway/*) aws ec2 describe-nat-gateways --nat-gateway-ids "$id" \
--query 'NatGateways[].State' --output text 2>/dev/null | grep -qvE '^(deleted)?$' ;;
*:subnet/*) aws ec2 describe-subnets --subnet-ids "$id" >/dev/null 2>&1 ;;
*:security-group/*) aws ec2 describe-security-groups --group-ids "$id" >/dev/null 2>&1 ;;
*:vpc-flow-log/*) aws ec2 describe-flow-logs --flow-log-ids "$id" \
--query 'FlowLogs[].FlowLogId' --output text 2>/dev/null | grep -q . ;;
*:volume/*) aws ec2 describe-volumes --volume-ids "$id" >/dev/null 2>&1 ;;
*:key/*) aws kms describe-key --key-id "$id" --query 'KeyMetadata.KeyState' \
--output text 2>/dev/null | grep -qvE '^(PendingDeletion|PendingReplicaDeletion)?$' ;;
*) return 0 ;; # a type this script cannot ask about counts as left, never as gone
esac
}
for arn in $(aws resourcegroupstaggingapi get-resources --tag-filters "Key=run-id,Values=${run_id}" \
--query 'ResourceTagMappingList[].ResourceARN' --output text); do
if exists "$arn"; then
echo "left behind: $arn"
left=1
fi
done
[ "$left" -eq 0 ] && echo "nothing left from run ${run_id}"
exit "$left"

The platform’s own version also searches what the cluster’s controllers made outside Terraform, by the cluster’s tags: instances, volumes, network interfaces, security groups and load balancers.

Owner: platform engineer.

Layer 3: the run on a schedule, reconciled with the bill (week 3 onward)#

A run nobody schedules is a run nobody makes. This one runs every Wednesday, after Monday’s dependency updates have merged, and by hand only when someone types the word that says they mean to spend. A repository variable stops the schedule without a commit. The first scheduled run, on 21 October, passed fourteen of fourteen checks against AWS in 0.62 hours, for about fourteen cents by the teardown’s own list-price estimate. The runs of 28 October and 4 November passed the same fourteen, at about sixteen cents each by the same estimate. GitHub started each of them between nearly five and more than six hours after its cron time, still on the Wednesday, which is how scheduled workflows behave under load, so nothing downstream should expect it on the minute.

# .github/workflows/eks-validation.yml in aws-eks-reference-platform at v1.3.0, shortened: weekly, by hand only with a typed word, stoppable without a commit
on:
schedule:
# Wednesday: after Monday's Dependabot updates have merged, so the week's
# pins reach AWS in days, not a week later.
- cron: "17 1 * * 3"
workflow_dispatch:
inputs:
confirm:
description: "Type SPEND to create billed AWS resources (about $0.20 a passing run, up to about $0.50)"
required: true
type: string
# ...
jobs:
up:
name: Apply, install, check
if: github.ref == 'refs/heads/main' && (inputs.confirm == 'SPEND' || (github.event_name == 'schedule' && vars.AWS_VALIDATION_ENABLED == 'true'))
runs-on: ubuntu-latest
environment: aws-validation
timeout-minutes: 100
# ... apply, install and check; a second job destroys, searches for residue and estimates the cost

The run’s own estimate is a forecast too. The daily sweeper reads yesterday’s bill from Cost Explorer, before credits and refunds, so the estimate and the bill sit side by side.

Terminal window
# .github/workflows/eks-sweeper.yml in aws-eks-reference-platform at v1.3.0, shortened: what the account used yesterday, by service, before credits and refunds
start="$(date -u -d yesterday +%F)"; end="$(date -u +%F)"
# ... a table header for the run's summary
aws ce get-cost-and-usage --time-period "Start=$start,End=$end" --granularity DAILY \
--metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE \
--filter '{"Not":{"Dimensions":{"Key":"RECORD_TYPE","Values":["Credit","Refund"]}}}' \
--query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' --output text |
awk -F'\t' '$2 > 0.005 {printf "| %s | $%.2f |\n", $1, $2; t += $2} END {printf "| **Total** | **$%.2f** |\n", t}'

Owner: SRE, with a failed weekly run treated as a finding, not a flaky job.

Tradeoffs#

A passing run takes under an hour of runner time across its two jobs and costs between fourteen and twenty cents in AWS by its own list-price estimate, so a weekly run costs under a dollar a month. Failures cost more than success: the seven failed runs cost more than five times the one that passed, and the most expensive of them was the one that got furthest. The account has to exist inside an AWS Organization and hold nothing else. And the run proves a build from nothing with two system nodes, demo tenants and a Let’s Encrypt staging certificate. It says nothing about scale or load, and the platform’s README says so in its known limitations.

Skipping it costs nothing on every day before the first real apply. On that day the seven failures arrive in someone else’s account.

The closing argument#

Mocked tests and a kind suite are the right defaults, and they keep running on every change. They prove the platform agrees with itself. Only an apply proves it agrees with AWS, and GitHub and EKS moved the ground under this one twice since July without a commit in the repository. Twenty cents a week buys the answer before anyone needs it.

Sources#

Discussion#

If you prove infrastructure on a real account on a schedule, or you have a reason to trust the plan alone, drop a comment below. Counterarguments are welcome and the comment thread is where I respond first. For longer back-and-forth with senior practitioners, join the discussion on Discord.


Vladimir Mikhalev

Docker Captain  ·  CNCF Ambassador  ·  IBM Champion

The Verdict — production-tested analysis on YouTube.

The Verdict

Inconvenient truths about shipping in the AI era

Container security, platform engineering, and the agentic shift — tested in production, argued without the hype. The verdict reaches your inbox the moment there's one worth sending.

Related Posts

Same category
  1. 1
    A Partial Read Is Not a Smaller Fleet: A GitHub API Rate Limit and the Numbers That Lied for an Hour
    DevOps & Cloud · A partial read published as the whole. For an hour the profile said 24 restore scripts across 15 repositories, and the fleet had 73 across 48. Three guards now.
  2. 2
    Restore Drills on a Clean Machine — Measured RTO for 47 Stacks, 19 Seconds to 7 Minutes
    DevOps & Cloud · Restore drills that measure RTO on a machine that never ran the stack, 47 templates weekly. Every same-host restore passed. The first clean one answered 500.
  3. 3
    Mutation Testing for Infrastructure Code — 153 Planted Bugs, Four Tests That Could Not Fail
    DevOps & Cloud · Mutation testing for infrastructure code, done by hand across fifteen repositories. 153 planted bugs, and four green tests that could never have failed.
  4. 4
    Docker Health Checks That Cannot Fail — Seventeen Hours Green
    DevOps & Cloud · A Docker health check whose pattern matches the shell running it can never go red. How to find the checks in your fleet that never failed, and prove each one.

Random Posts

Random
  1. 1
    Install eksctl on macOS with Homebrew, from the AWS Tap
    DevOps & Cloud · Install eksctl on macOS in three commands with Homebrew and the aws/tap AWS maintains, then check the version. The old weaveworks tap is gone from the docs.
  2. 2
    Machine Learning and Deep Learning Courses on YouTube
    AI & MLOps · Explore the best free YouTube courses in machine learning and deep learning—from beginner-friendly foundations to advanced topics like NLP, CV, and MLOps.
  3. 3
    AI Didn't Fix Productivity. Measurement Did.
    Opinion & Culture · AI adoption is easy. Proving productivity isn't. A Docker Captain's view on why AI impact is hard to measure—and how teams can finally prove real value.
  4. 4
    Terminal, Shell, Command Line, Prompt: Four Words, Four Things
    SysAdmin & IT Pro · The terminal is the window, the shell runs your commands, the prompt says it is ready, the command line is the idea. Plain definitions, examples, a cheat sheet.
EKS Validation on a Real AWS Account: Twenty Cents a Run, Seven Failures No Mock Saw
https://heyvaldemar.com/eks-validation-real-aws-account-cost/
Author
Vladimir Mikhalev
Published
2026-11-10
License
CC BY-NC-SA 4.0