Deploy-Time Intelligence in AWS CDK: Custom Resources in Action
Source: Dev.to
Introduction
In real‑world AWS platforms, a single CDK codebase is often deployed across multiple AWS accounts, each representing a different environment such as development, staging, or production.
While AWS CDK excels at defining infrastructure, it has a limitation:
It cannot make decisions at deploy time based on values stored inside the account, such as values stored in AWS Systems Manager (SSM) Parameter Store.
In this blog we solve a practical platform‑engineering problem using a Lambda‑backed Custom Resource to make environment‑aware decisions when installing an EKS Helm add‑on.
The Real‑Life Problem
You are a platform engineer managing EKS clusters across multiple AWS accounts:
| Environment | Expectation |
|---|---|
| Development | Low cost, minimal redundancy |
| Staging | Production‑like but smaller |
| Production | High availability |
Your organization already stores the environment type centrally as a SSM parameter:
/platform/account/env
with values like:
developmentstagingproduction
Now you want to install ingress‑nginx on every EKS cluster, but configure it differently:
development→replicaCount = 1staging/production→replicaCount = 2
Why Not Use CDK Context? (Short Version)
At first glance, CDK context variables may seem like a simpler solution for environment‑based configuration. However, context values are resolved at synthesis time, not during deployment. This means they must be provided externally (via cdk.json or CI/CD pipelines) and are unaware of account‑level metadata such as values stored in SSM Parameter Store.
In multi‑account platforms this often leads to:
- Manual coordination
- Configuration drift
- Governance issues
Since the environment classification already lives inside the AWS account and should be owned by the platform, using a deploy‑time Custom Resource ensures the configuration is accurate, consistent, and centrally controlled.
Why CDK Alone Is Not Enough
AWS CDK evaluates logic during synthesis, but the SSM parameter value is only reliably available during deployment. Consequently you cannot:
- Use CDK
ifstatements for the value - Hard‑code environment values in the stack
- Reliably rely on CDK context
What you need is deploy‑time logic.
The Solution: Lambda‑Backed Custom Resource
A Custom Resource allows CloudFormation to:
- Invoke a Lambda function during stack creation or update
- Wait for the result
- Consume returned attributes as inputs for other resources
In this case, the Custom Resource:
- Reads the environment value from SSM
- Computes the correct Helm value
- Returns it to CDK
- CDK passes it into the Helm chart
Architecture Overview
Deployment flow
- CDK creates
- EKS cluster
- SSM parameter
/platform/account/env - Lambda function
- Custom Resource triggers the Lambda
- Lambda computes Helm values
- Helm chart is installed using the returned values
This keeps:
- One CDK codebase
- Zero manual steps
- Environment‑aware behavior
CDK Stack Code (Python)
Below is the CDK stack that creates:
- EKS cluster
- SSM parameter
- Lambda function
- Custom Resource
- Helm chart with dynamic values
from aws_cdk import (
Stack,
aws_eks as eks,
aws_ec2 as ec2,
aws_iam as iam,
aws_ssm as ssm,
aws_signer as signer,
aws_lambda as _lambda,
custom_resources as cr,
CustomResource,
Token,
)
from aws_cdk.lambda_layer_kubectl_v34 import KubectlV34Layer
from constructs import Construct
import os
class TestStack(Stack):
def __init__(self, scope: Construct, construct_id: str, *, vpc: ec2.IVpc = None, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
aws_account_id = self.node.try_get_context("aws-account-id")
# -------------------------------------------------
# EKS Cluster
# -------------------------------------------------
cluster = eks.Cluster(
self,
"MyEKS",
version=eks.KubernetesVersion.V1_34,
endpoint_access=eks.EndpointAccess.PUBLIC_AND_PRIVATE,
default_capacity=0,
default_capacity_instance=ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MEDIUM,
),
kubectl_layer=KubectlV34Layer(self, "kubectl"),
vpc=vpc,
vpc_subnets=[
ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
)
],
cluster_name="MyEKS",
tags={"Name": "MyEKS", "Purpose": "Swisscom-Interview"},
)
# -------------------------------------------------
# EKS Admin Access
# -------------------------------------------------
admin_user = iam.User(self, "EKSAdmin")
cluster.aws_auth.add_user_mapping(admin_user, groups=["system:masters"])
# -------------------------------------------------
# Store environment in SSM
# -------------------------------------------------
ssm.StringParameter(
self,
"MyEnvParam",
parameter_name="/platform/account/env",
string_value="development",
description="Environment Name",
)
# -------------------------------------------------
# Lambda code signing
# -------------------------------------------------
signing_profile = signer.SigningProfile(
self,
"SigningProfile",
platform=signer.Platform.AWS_LAMBDA_SHA384_ECDSA,
)
code_signing_config = _lambda.CodeSigningConfig(
self,
"CodeSigningConfig",
signing_profiles=[signing_profile],
)
# -------------------------------------------------
# Lambda Function (reads the SSM parameter)
# -------------------------------------------------
fn = _lambda.Function(
self,
"MySSMParamLambda",
runtime=_lambda.Runtime.PYTHON_3_13,
handler="index.lambda_handler",
code=_lambda.Code.from_asset(
os.path.join(os.path.dirname(__file__), "lambda_functions")
),
environment={"SSM_PARAM_NAME": "/platform/account/env"},
code_signing_config=code_signing_config,
)
fn.add_to_role_policy(
iam.PolicyStatement(
actions=["ssm:GetParameter"],
resources=["*"], # Adjust to the specific parameter ARN in production
)
)
# -------------------------------------------------
# Custom Resource that invokes the Lambda
# -------------------------------------------------
custom_resource = cr.AwsCustomResource(
self,
"EnvLookupCustomResource",
on_create=cr.AwsSdkCall(
service="Lambda",
action="invoke",
parameters={
"FunctionName": fn.function_name,
"Payload": Token.as_string(
{
"RequestType": "Create",
"ResourceProperties": {"SSM_PARAM_NAME": "/platform/account/env"},
}
),
},
physical_resource_id=cr.PhysicalResourceId.of("EnvLookup"),
),
policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
),
)
# -------------------------------------------------
# Use the value returned by the Custom Resource in the Helm chart
# -------------------------------------------------
replica_count = custom_resource.get_response_field("Payload.body.replicaCount")
eks.HelmChart(
self,
"IngressNginx",
cluster=cluster,
chart="ingress-nginx",
repository="https://kubernetes.github.io/ingress-nginx",
release="ingress-nginx",
namespace="ingress-nginx",
values={"controller": {"replicaCount": replica_count}},
)
The Lambda code (not shown) reads /platform/account/env, maps the value to the appropriate replicaCount, and returns it in the response payload.
CDK Code (Infrastructure)
resources = [
f"arn:aws:ssm:{self.region}:{self.account}:parameter/platform/account/env"
]
# Custom Resource Provider
provider = cr.Provider(
self,
"EnvToHelmProvider",
on_event_handler=fn,
)
env_cr = CustomResource(
self,
"EnvToHelmValues",
service_token=provider.service_token,
)
replica_count = Token.as_number(
env_cr.get_att("ReplicaCount")
)
# Install ingress‑nginx Helm chart
cluster.add_helm_chart(
"nginx-ingress",
chart="nginx-ingress",
repository="https://helm.nginx.com/stable",
namespace="kube-system",
values={
"controller": {
"replicaCount": replica_count
}
},
)
Lambda Code (Custom Resource Logic)
This Lambda:
- Reads the environment from SSM
- Computes the correct replica count
- Returns values back to CloudFormation
import boto3
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ssm = boto3.client("ssm")
PARA_NAME = os.environ["SSM_PARAM_NAME"]
def get_parameter(para_name):
parameter = ssm.get_parameter(Name=para_name)
env = parameter["Parameter"]["Value"].strip().lower()
if env == "development":
value = 1
elif env in ["staging", "production"]:
value = 2
else:
raise ValueError(
f"Invalid environment {env} in SSM Parameter {para_name}"
)
logger.info(f"Computed replicaCount={value} for env={env}")
return {
"Environment": env,
"ReplicaCount": value,
}
def lambda_handler(event, context):
if event.get("RequestType") == "Delete":
return {
"PhysicalResourceId": event.get("PhysicalResourceId", "env"),
"Data": {},
}
data = get_parameter(PARA_NAME)
return {
"PhysicalResourceId": f"{PARA_NAME}:{data['Environment']}",
"Data": data,
}
Why This Pattern Works Well
This approach provides:
- Single CDK codebase
- Environment‑aware behavior
- No manual Helm overrides
- Deploy‑time decision making
- Testable business logic
It scales well as you add more add‑ons such as:
- cert‑manager
- external‑dns
- cluster‑autoscaler
- logging agents
Conclusion
AWS CDK is declarative by nature, but real platforms require deploy‑time intelligence. By combining Lambda‑backed Custom Resources with CDK, you can make infrastructure decisions based on real account metadata, not hard‑coded assumptions.
This pattern is a powerful tool for platform teams aiming for consistency, safety, and automation across environments.