@@ -2,11 +2,12 @@ import * as cdk from "aws-cdk-lib";
22import * as ec2 from "aws-cdk-lib/aws-ec2" ;
33import * as ecs from "aws-cdk-lib/aws-ecs" ;
44import * as efs from "aws-cdk-lib/aws-efs" ;
5- import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2 " ;
5+ import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2 " ;
66import * as cloudfront from "aws-cdk-lib/aws-cloudfront" ;
77import * as origins from "aws-cdk-lib/aws-cloudfront-origins" ;
88import * as logs from "aws-cdk-lib/aws-logs" ;
99import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager" ;
10+ import * as servicediscovery from "aws-cdk-lib/aws-servicediscovery" ;
1011import { Construct } from "constructs" ;
1112import * as path from "path" ;
1213
@@ -15,8 +16,6 @@ export class CommunityDashboardStack extends cdk.Stack {
1516 super ( scope , id , props ) ;
1617
1718 // ── Secrets Manager ──────────────────────────────────────────────────
18- // The GitHub PAT must already exist in Secrets Manager as a plain-text
19- // secret. Pass the ARN via the GITHUB_SECRET_ARN env var or CDK context.
2019 const secretArn =
2120 process . env . GITHUB_SECRET_ARN ??
2221 this . node . tryGetContext ( "githubSecretArn" ) ;
@@ -25,7 +24,7 @@ export class CommunityDashboardStack extends cdk.Stack {
2524 throw new Error (
2625 "GITHUB_SECRET_ARN environment variable or 'githubSecretArn' CDK context must be set.\n" +
2726 "Create the secret first:\n" +
28- ' aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-west-2 '
27+ ' aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-east-1 '
2928 ) ;
3029 }
3130
@@ -63,7 +62,16 @@ export class CommunityDashboardStack extends cdk.Stack {
6362 } ,
6463 } ) ;
6564
66- // ── ECS Cluster ─────────────────────────────────────────────────────
65+ // ── ECS Cluster + Cloud Map namespace ───────────────────────────────
66+ const namespace = new servicediscovery . PrivateDnsNamespace (
67+ this ,
68+ "Namespace" ,
69+ {
70+ name : "community-dashboard.local" ,
71+ vpc,
72+ }
73+ ) ;
74+
6775 const cluster = new ecs . Cluster ( this , "Cluster" , {
6876 vpc,
6977 containerInsights : true ,
@@ -75,7 +83,6 @@ export class CommunityDashboardStack extends cdk.Stack {
7583 memoryLimitMiB : 1024 ,
7684 } ) ;
7785
78- // Mount EFS volume
7986 taskDef . addVolume ( {
8087 name : "metrics-data" ,
8188 efsVolumeConfiguration : {
@@ -88,23 +95,18 @@ export class CommunityDashboardStack extends cdk.Stack {
8895 } ,
8996 } ) ;
9097
91- // Grant EFS access to the task role
9298 fileSystem . grant (
9399 taskDef . taskRole ,
94100 "elasticfilesystem:ClientMount" ,
95101 "elasticfilesystem:ClientWrite" ,
96102 "elasticfilesystem:ClientRootAccess"
97103 ) ;
98104
99- // Container definition — built from the unified Dockerfile
100105 const container = taskDef . addContainer ( "grafana" , {
101- image : ecs . ContainerImage . fromAsset (
102- path . join ( __dirname , "../../" ) ,
103- {
104- file : "docker/Dockerfile" ,
105- platform : cdk . aws_ecr_assets . Platform . LINUX_AMD64 ,
106- }
107- ) ,
106+ image : ecs . ContainerImage . fromAsset ( path . join ( __dirname , "../../" ) , {
107+ file : "docker/Dockerfile" ,
108+ platform : cdk . aws_ecr_assets . Platform . LINUX_AMD64 ,
109+ } ) ,
108110 logging : ecs . LogDrivers . awsLogs ( {
109111 streamPrefix : "community-dashboard" ,
110112 logRetention : logs . RetentionDays . TWO_WEEKS ,
@@ -131,63 +133,97 @@ export class CommunityDashboardStack extends cdk.Stack {
131133 readOnly : false ,
132134 } ) ;
133135
134- // ── Fargate Service + ALB ─────────────────────────── ────────────────
136+ // ── Fargate Service with Cloud Map service discovery ────────────────
135137 const service = new ecs . FargateService ( this , "Service" , {
136138 cluster,
137139 taskDefinition : taskDef ,
138140 desiredCount : 1 ,
139141 assignPublicIp : false ,
140142 platformVersion : ecs . FargatePlatformVersion . LATEST ,
143+ enableExecuteCommand : true ,
144+ cloudMapOptions : {
145+ cloudMapNamespace : namespace ,
146+ name : "grafana" ,
147+ containerPort : 3000 ,
148+ dnsRecordType : servicediscovery . DnsRecordType . SRV ,
149+ } ,
141150 } ) ;
142151
143- // Allow the service to reach EFS
144152 service . connections . allowTo ( fileSystem , ec2 . Port . tcp ( 2049 ) , "EFS access" ) ;
145153
146- // Application Load Balancer
147- const alb = new elbv2 . ApplicationLoadBalancer ( this , "Alb" , {
148- vpc,
149- internetFacing : true ,
154+ // ── API Gateway HTTP API + VPC Link ─────────────────────────────────
155+ // No ALB needed — API Gateway connects directly to ECS via VPC Link.
156+ // This avoids Epoxy/Riddler flagging a public-facing Grafana instance.
157+ const vpcLink = new apigwv2 . CfnVpcLink ( this , "VpcLink" , {
158+ name : "community-dashboard-vpc-link" ,
159+ subnetIds : vpc . privateSubnets . map ( ( s ) => s . subnetId ) ,
160+ securityGroupIds : [ service . connections . securityGroups [ 0 ] . securityGroupId ] ,
150161 } ) ;
151162
152- const listener = alb . addListener ( "HttpListener" , {
153- port : 80 ,
163+ const httpApi = new apigwv2 . CfnApi ( this , "HttpApi" , {
164+ name : "community-dashboard-api" ,
165+ protocolType : "HTTP" ,
166+ description : "API Gateway for Community Dashboard (Grafana)" ,
154167 } ) ;
155168
156- listener . addTargets ( "GrafanaTarget" , {
157- port : 3000 ,
158- protocol : elbv2 . ApplicationProtocol . HTTP ,
159- targets : [ service ] ,
160- healthCheck : {
161- path : "/api/health" ,
162- interval : cdk . Duration . seconds ( 30 ) ,
163- healthyThresholdCount : 2 ,
164- unhealthyThresholdCount : 3 ,
165- } ,
166- deregistrationDelay : cdk . Duration . seconds ( 30 ) ,
169+ // Integration: forward all requests to the Cloud Map service via VPC Link
170+ const integration = new apigwv2 . CfnIntegration ( this , "Integration" , {
171+ apiId : httpApi . ref ,
172+ integrationType : "HTTP_PROXY" ,
173+ integrationMethod : "ANY" ,
174+ connectionType : "VPC_LINK" ,
175+ connectionId : vpcLink . ref ,
176+ integrationUri : service . cloudMapService ! . serviceArn ,
177+ payloadFormatVersion : "1.0" ,
167178 } ) ;
168179
169- // ── Outputs ─────────────────────────────────────────────────────────
170- // CloudFront distribution — provides HTTPS on *.cloudfront.net
180+ new apigwv2 . CfnRoute ( this , "DefaultRoute" , {
181+ apiId : httpApi . ref ,
182+ routeKey : "$default" ,
183+ target : `integrations/${ integration . ref } ` ,
184+ } ) ;
185+
186+ const stage = new apigwv2 . CfnStage ( this , "DefaultStage" , {
187+ apiId : httpApi . ref ,
188+ stageName : "$default" ,
189+ autoDeploy : true ,
190+ } ) ;
191+
192+ // Allow API Gateway VPC Link to reach the ECS service
193+ service . connections . allowFrom (
194+ ec2 . Peer . ipv4 ( vpc . vpcCidrBlock ) ,
195+ ec2 . Port . tcp ( 3000 ) ,
196+ "Allow API Gateway VPC Link"
197+ ) ;
198+
199+ // ── CloudFront ──────────────────────────────────────────────────────
200+ // Extract the API Gateway domain from the endpoint URL
201+ const apiDomain = cdk . Fn . select (
202+ 2 ,
203+ cdk . Fn . split ( "/" , httpApi . attrApiEndpoint )
204+ ) ;
205+
171206 const distribution = new cloudfront . Distribution ( this , "Distribution" , {
172207 defaultBehavior : {
173- origin : new origins . HttpOrigin ( alb . loadBalancerDnsName , {
174- protocolPolicy : cloudfront . OriginProtocolPolicy . HTTP_ONLY ,
208+ origin : new origins . HttpOrigin ( apiDomain , {
209+ protocolPolicy : cloudfront . OriginProtocolPolicy . HTTPS_ONLY ,
175210 } ) ,
176211 viewerProtocolPolicy : cloudfront . ViewerProtocolPolicy . REDIRECT_TO_HTTPS ,
177212 cachePolicy : cloudfront . CachePolicy . CACHING_DISABLED ,
178- originRequestPolicy : cloudfront . OriginRequestPolicy . ALL_VIEWER ,
213+ originRequestPolicy : cloudfront . OriginRequestPolicy . ALL_VIEWER_EXCEPT_HOST_HEADER ,
179214 allowedMethods : cloudfront . AllowedMethods . ALLOW_ALL ,
180215 } ,
181216 } ) ;
182217
218+ // ── Outputs ─────────────────────────────────────────────────────────
183219 new cdk . CfnOutput ( this , "GrafanaUrl" , {
184220 value : `https://${ distribution . distributionDomainName } ` ,
185221 description : "Grafana dashboard URL (HTTPS via CloudFront)" ,
186222 } ) ;
187223
188- new cdk . CfnOutput ( this , "AlbUrl " , {
189- value : `http:// ${ alb . loadBalancerDnsName } ` ,
190- description : "Grafana dashboard URL (ALB, HTTP only) " ,
224+ new cdk . CfnOutput ( this , "ApiGatewayUrl " , {
225+ value : httpApi . attrApiEndpoint ,
226+ description : "API Gateway endpoint URL " ,
191227 } ) ;
192228
193229 new cdk . CfnOutput ( this , "EfsFileSystemId" , {
@@ -202,7 +238,7 @@ export class CommunityDashboardStack extends cdk.Stack {
202238
203239 new cdk . CfnOutput ( this , "CreateSecretCommand" , {
204240 value :
205- 'aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-west-2 ' ,
241+ 'aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-east-1 ' ,
206242 description : "Command to create the GitHub token secret (one-time)" ,
207243 } ) ;
208244 }
0 commit comments