From 8fbce5bd4c3fa56dc226383c668fa3ea04cfef0e Mon Sep 17 00:00:00 2001 From: Tom Groves Date: Sat, 14 Mar 2026 18:03:07 +0000 Subject: [PATCH 1/3] feat(aws): add configurable tagging API resource resolver via stack config Add `resource-resolver: tagging` stack config option that skips SSM parameter creation and tells the runtime to use the Resource Groups Tagging API for resource discovery. This avoids the SSM 4KB/8KB size limit that breaks large stacks. Default behaviour (SSM) is unchanged. --- cloud/aws/common/config.go | 6 ++++++ cloud/aws/deploy/batch.go | 7 +++++++ cloud/aws/deploy/resources.go | 4 ++++ cloud/aws/deploy/service.go | 4 ++++ cloud/aws/deploytf/resources.go | 4 ++++ cloud/aws/deploytf/service.go | 4 ++++ 6 files changed, 29 insertions(+) diff --git a/cloud/aws/common/config.go b/cloud/aws/common/config.go index 9ea3b0597..b9e906305 100644 --- a/cloud/aws/common/config.go +++ b/cloud/aws/common/config.go @@ -64,6 +64,7 @@ type AuroraRdsClusterConfig struct { type AwsConfig struct { ScheduleTimezone string `mapstructure:"schedule-timezone,omitempty"` + ResourceResolver string `mapstructure:"resource-resolver,omitempty"` Import AwsImports Refresh bool Apis map[string]*AwsApiConfig @@ -139,6 +140,11 @@ func ConfigFromAttributes(attributes map[string]interface{}) (*AwsConfig, error) awsConfig.ScheduleTimezone = "UTC" } + // Default resource resolver to SSM if not specified + if awsConfig.ResourceResolver == "" { + awsConfig.ResourceResolver = "ssm" + } + if awsConfig.Apis == nil { awsConfig.Apis = map[string]*AwsApiConfig{} } diff --git a/cloud/aws/deploy/batch.go b/cloud/aws/deploy/batch.go index a42b9f1fa..203947830 100644 --- a/cloud/aws/deploy/batch.go +++ b/cloud/aws/deploy/batch.go @@ -364,6 +364,13 @@ func (p *NitricAwsPulumiProvider) Batch(ctx *pulumi.Context, parent pulumi.Resou }) } + if p.AwsConfig.ResourceResolver == "tagging" { + jobDefinitionContainerProperties.Environment = append(jobDefinitionContainerProperties.Environment, EnvironmentVariable{ + Name: "NITRIC_AWS_RESOURCE_RESOLVER", + Value: "tagging", + }) + } + if job.Requirements.Gpus > 0 { jobDefinitionContainerProperties.ResourceRequirements = append(jobDefinitionContainerProperties.ResourceRequirements, ResourceRequirement{ Type: "GPU", diff --git a/cloud/aws/deploy/resources.go b/cloud/aws/deploy/resources.go index 5267c11bd..84eaa88df 100644 --- a/cloud/aws/deploy/resources.go +++ b/cloud/aws/deploy/resources.go @@ -24,6 +24,10 @@ import ( ) func (a *NitricAwsPulumiProvider) resourcesStore(ctx *pulumi.Context) error { + if a.AwsConfig.ResourceResolver == "tagging" { + return nil + } + // Build the AWS resource index from the provider information // This will be used to store the ARNs/Identifiers of all resources created by the stack bucketArnMap := pulumi.StringMap{} diff --git a/cloud/aws/deploy/service.go b/cloud/aws/deploy/service.go index ec89b41d0..24d7b15e8 100644 --- a/cloud/aws/deploy/service.go +++ b/cloud/aws/deploy/service.go @@ -181,6 +181,10 @@ func (a *NitricAwsPulumiProvider) Service(ctx *pulumi.Context, parent pulumi.Res envVars[k] = v } + if a.AwsConfig.ResourceResolver == "tagging" { + envVars["NITRIC_AWS_RESOURCE_RESOLVER"] = pulumi.String("tagging") + } + if a.JobQueue != nil { envVars["NITRIC_JOB_QUEUE_ARN"] = a.JobQueue.Arn } diff --git a/cloud/aws/deploytf/resources.go b/cloud/aws/deploytf/resources.go index e12355b38..2b9a19d8d 100644 --- a/cloud/aws/deploytf/resources.go +++ b/cloud/aws/deploytf/resources.go @@ -25,6 +25,10 @@ import ( ) func (a *NitricAwsTerraformProvider) ResourcesStore(stack cdktf.TerraformStack, accessRoleNames []string) error { + if a.AwsConfig.ResourceResolver == "tagging" { + return nil + } + index := common.NewResourceIndex() for name, bucket := range a.Buckets { diff --git a/cloud/aws/deploytf/service.go b/cloud/aws/deploytf/service.go index 13104d917..e90371718 100644 --- a/cloud/aws/deploytf/service.go +++ b/cloud/aws/deploytf/service.go @@ -53,6 +53,10 @@ func (a *NitricAwsTerraformProvider) Service(stack cdktf.TerraformStack, name st "NITRIC_HTTP_PROXY_PORT": jsii.String(fmt.Sprint(3000)), } + if a.AwsConfig.ResourceResolver == "tagging" { + jsiiEnv["NITRIC_AWS_RESOURCE_RESOLVER"] = jsii.String("tagging") + } + // TODO: Only apply to requesting services if a.Rds != nil { jsiiEnv["NITRIC_DATABASE_BASE_URL"] = jsii.Sprintf("postgres://%s:%s@%s:%s", *a.Rds.ClusterUsernameOutput(), *a.Rds.ClusterPasswordOutput(), From 8facf40d274cba8f2616737a2608a28ef91c88d4 Mon Sep 17 00:00:00 2001 From: Tom Groves Date: Sat, 14 Mar 2026 18:38:10 +0000 Subject: [PATCH 2/3] docs(aws): document resource-resolver config option Add resource-resolver option to both Pulumi and Terraform AWS provider stack configuration reference. Explains the 'ssm' (default) vs 'tagging' modes and the SSM size limit motivation. --- docs/docs/providers/pulumi/aws.mdx | 7 +++++++ docs/docs/providers/terraform/aws.mdx | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/docs/providers/pulumi/aws.mdx b/docs/docs/providers/pulumi/aws.mdx index a0f6e5b15..3c536059b 100644 --- a/docs/docs/providers/pulumi/aws.mdx +++ b/docs/docs/providers/pulumi/aws.mdx @@ -166,6 +166,13 @@ region: my-aws-stack-region # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones schedule-timezone: Australia/Sydney # Available since v0.27.0 +# Configure how the runtime discovers deployed resources +# 'ssm' (default) stores a resource index in SSM Parameter Store +# 'tagging' uses the AWS Resource Groups Tagging API for runtime resource +# discovery, avoiding the SSM parameter size limits (4KB standard / 8KB advanced) +# which can be exceeded in stacks with many resources +resource-resolver: tagging + # Import existing AWS Resources # Available since v0.28.0 import: diff --git a/docs/docs/providers/terraform/aws.mdx b/docs/docs/providers/terraform/aws.mdx index 82ddf2fd8..340964bc8 100644 --- a/docs/docs/providers/terraform/aws.mdx +++ b/docs/docs/providers/terraform/aws.mdx @@ -146,6 +146,13 @@ import: # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones schedule-timezone: Australia/Sydney # Available since v0.27.0 +# Configure how the runtime discovers deployed resources +# 'ssm' (default) stores a resource index in SSM Parameter Store +# 'tagging' uses the AWS Resource Groups Tagging API for runtime resource +# discovery, avoiding the SSM parameter size limits (4KB standard / 8KB advanced) +# which can be exceeded in stacks with many resources +resource-resolver: tagging + # Apply configuration to nitric APIs apis: # The nitric name of the API to configure From 7616e7ad39207f671b8d6eefe17fb07156932652 Mon Sep 17 00:00:00 2001 From: Tom Groves Date: Sat, 14 Mar 2026 18:47:24 +0000 Subject: [PATCH 3/3] fix(aws): validate resource-resolver config and use named constants Address code review feedback: - Validate resource-resolver accepts only "ssm" or "tagging", rejecting typos with a clear error at deploy time - Extract ResourceResolverSSM and ResourceResolverTagging constants to eliminate magic string duplication across 7 call sites - Add unit tests for config validation (default, explicit ssm, tagging, and invalid values) --- cloud/aws/common/config.go | 15 +++++- cloud/aws/common/config_test.go | 96 +++++++++++++++++++++++++++++++++ cloud/aws/deploy/batch.go | 5 +- cloud/aws/deploy/resources.go | 2 +- cloud/aws/deploy/service.go | 5 +- cloud/aws/deploytf/resources.go | 2 +- cloud/aws/deploytf/service.go | 5 +- 7 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 cloud/aws/common/config_test.go diff --git a/cloud/aws/common/config.go b/cloud/aws/common/config.go index b9e906305..dc64e02a5 100644 --- a/cloud/aws/common/config.go +++ b/cloud/aws/common/config.go @@ -17,11 +17,20 @@ package common import ( + "fmt" + "github.com/imdario/mergo" "github.com/mitchellh/mapstructure" "github.com/nitrictech/nitric/cloud/common/deploy/config" ) +const ( + // ResourceResolverSSM uses SSM Parameter Store for runtime resource discovery (default). + ResourceResolverSSM = "ssm" + // ResourceResolverTagging uses the AWS Resource Groups Tagging API for runtime resource discovery. + ResourceResolverTagging = "tagging" +) + type AwsApiConfig struct { Description string Domains []string @@ -142,7 +151,11 @@ func ConfigFromAttributes(attributes map[string]interface{}) (*AwsConfig, error) // Default resource resolver to SSM if not specified if awsConfig.ResourceResolver == "" { - awsConfig.ResourceResolver = "ssm" + awsConfig.ResourceResolver = ResourceResolverSSM + } + + if awsConfig.ResourceResolver != ResourceResolverSSM && awsConfig.ResourceResolver != ResourceResolverTagging { + return nil, fmt.Errorf("invalid resource-resolver value %q: must be %q or %q", awsConfig.ResourceResolver, ResourceResolverSSM, ResourceResolverTagging) } if awsConfig.Apis == nil { diff --git a/cloud/aws/common/config_test.go b/cloud/aws/common/config_test.go new file mode 100644 index 000000000..325d88036 --- /dev/null +++ b/cloud/aws/common/config_test.go @@ -0,0 +1,96 @@ +// Copyright 2021 Nitric Technologies Pty Ltd. +// +// 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. + +package common + +import ( + "testing" +) + +func TestConfigFromAttributes_ResourceResolver(t *testing.T) { + tests := []struct { + name string + attrs map[string]interface{} + wantValue string + wantErr bool + errContains string + }{ + { + name: "defaults to ssm when not specified", + attrs: map[string]interface{}{}, + wantValue: ResourceResolverSSM, + }, + { + name: "accepts ssm explicitly", + attrs: map[string]interface{}{"resource-resolver": "ssm"}, + wantValue: ResourceResolverSSM, + }, + { + name: "accepts tagging", + attrs: map[string]interface{}{"resource-resolver": "tagging"}, + wantValue: ResourceResolverTagging, + }, + { + name: "rejects invalid value", + attrs: map[string]interface{}{"resource-resolver": "taging"}, + wantErr: true, + errContains: "invalid resource-resolver", + }, + { + name: "rejects arbitrary string", + attrs: map[string]interface{}{"resource-resolver": "something-else"}, + wantErr: true, + errContains: "invalid resource-resolver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := ConfigFromAttributes(tt.attrs) + + if tt.wantErr { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tt.errContains) + } + if tt.errContains != "" { + if !containsString(err.Error(), tt.errContains) { + t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error()) + } + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if cfg.ResourceResolver != tt.wantValue { + t.Errorf("ResourceResolver = %q, want %q", cfg.ResourceResolver, tt.wantValue) + } + }) + } +} + +func containsString(s, substr string) bool { + return len(s) >= len(substr) && searchString(s, substr) +} + +func searchString(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/cloud/aws/deploy/batch.go b/cloud/aws/deploy/batch.go index 203947830..6f8317b36 100644 --- a/cloud/aws/deploy/batch.go +++ b/cloud/aws/deploy/batch.go @@ -19,6 +19,7 @@ import ( "fmt" "strconv" + "github.com/nitrictech/nitric/cloud/aws/common" "github.com/nitrictech/nitric/cloud/common/deploy/image" "github.com/nitrictech/nitric/cloud/common/deploy/provider" "github.com/nitrictech/nitric/cloud/common/deploy/tags" @@ -364,10 +365,10 @@ func (p *NitricAwsPulumiProvider) Batch(ctx *pulumi.Context, parent pulumi.Resou }) } - if p.AwsConfig.ResourceResolver == "tagging" { + if p.AwsConfig.ResourceResolver == common.ResourceResolverTagging { jobDefinitionContainerProperties.Environment = append(jobDefinitionContainerProperties.Environment, EnvironmentVariable{ Name: "NITRIC_AWS_RESOURCE_RESOLVER", - Value: "tagging", + Value: common.ResourceResolverTagging, }) } diff --git a/cloud/aws/deploy/resources.go b/cloud/aws/deploy/resources.go index 84eaa88df..1af600f55 100644 --- a/cloud/aws/deploy/resources.go +++ b/cloud/aws/deploy/resources.go @@ -24,7 +24,7 @@ import ( ) func (a *NitricAwsPulumiProvider) resourcesStore(ctx *pulumi.Context) error { - if a.AwsConfig.ResourceResolver == "tagging" { + if a.AwsConfig.ResourceResolver == common.ResourceResolverTagging { return nil } diff --git a/cloud/aws/deploy/service.go b/cloud/aws/deploy/service.go index 24d7b15e8..89fad6e21 100644 --- a/cloud/aws/deploy/service.go +++ b/cloud/aws/deploy/service.go @@ -23,6 +23,7 @@ import ( "github.com/avast/retry-go" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lambda" + "github.com/nitrictech/nitric/cloud/aws/common" "github.com/nitrictech/nitric/cloud/common/deploy/image" "github.com/nitrictech/nitric/cloud/common/deploy/provider" "github.com/nitrictech/nitric/cloud/common/deploy/pulumix" @@ -181,8 +182,8 @@ func (a *NitricAwsPulumiProvider) Service(ctx *pulumi.Context, parent pulumi.Res envVars[k] = v } - if a.AwsConfig.ResourceResolver == "tagging" { - envVars["NITRIC_AWS_RESOURCE_RESOLVER"] = pulumi.String("tagging") + if a.AwsConfig.ResourceResolver == common.ResourceResolverTagging { + envVars["NITRIC_AWS_RESOURCE_RESOLVER"] = pulumi.String(common.ResourceResolverTagging) } if a.JobQueue != nil { diff --git a/cloud/aws/deploytf/resources.go b/cloud/aws/deploytf/resources.go index 2b9a19d8d..3a4b88927 100644 --- a/cloud/aws/deploytf/resources.go +++ b/cloud/aws/deploytf/resources.go @@ -25,7 +25,7 @@ import ( ) func (a *NitricAwsTerraformProvider) ResourcesStore(stack cdktf.TerraformStack, accessRoleNames []string) error { - if a.AwsConfig.ResourceResolver == "tagging" { + if a.AwsConfig.ResourceResolver == common.ResourceResolverTagging { return nil } diff --git a/cloud/aws/deploytf/service.go b/cloud/aws/deploytf/service.go index e90371718..377c52a2b 100644 --- a/cloud/aws/deploytf/service.go +++ b/cloud/aws/deploytf/service.go @@ -19,6 +19,7 @@ import ( "github.com/aws/jsii-runtime-go" "github.com/hashicorp/terraform-cdk-go/cdktf" + "github.com/nitrictech/nitric/cloud/aws/common" "github.com/nitrictech/nitric/cloud/aws/deploytf/generated/service" "github.com/nitrictech/nitric/cloud/common/deploy/image" "github.com/nitrictech/nitric/cloud/common/deploy/provider" @@ -53,8 +54,8 @@ func (a *NitricAwsTerraformProvider) Service(stack cdktf.TerraformStack, name st "NITRIC_HTTP_PROXY_PORT": jsii.String(fmt.Sprint(3000)), } - if a.AwsConfig.ResourceResolver == "tagging" { - jsiiEnv["NITRIC_AWS_RESOURCE_RESOLVER"] = jsii.String("tagging") + if a.AwsConfig.ResourceResolver == common.ResourceResolverTagging { + jsiiEnv["NITRIC_AWS_RESOURCE_RESOLVER"] = jsii.String(common.ResourceResolverTagging) } // TODO: Only apply to requesting services