gobridge

Production stack — complete CDK stack

Overview

The following stack assembles all production components:

The consumer owns the registry image and its optional embedded initial config. BridgeConfig drives validation and grants, not an overwrite of the target. An image without an initial document needs existing config or operator creation; missing config leaves the data plane idle. See initial configuration.

package main

import (
    "github.com/aws/aws-cdk-go/awscdk/v2"
    "github.com/aws/aws-cdk-go/awscdk/v2/awscloudwatch"
    "github.com/aws/aws-cdk-go/awscdk/v2/awscloudwatchactions"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsecs"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
    "github.com/aws/aws-cdk-go/awscdk/v2/awskms"
    "github.com/aws/aws-cdk-go/awscdk/v2/awslogs"
    "github.com/aws/aws-cdk-go/awscdk/v2/awssns"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsxray"
    "github.com/aws/constructs-go/constructs/v10"
    "github.com/aws/jsii-runtime-go"

    "github.com/mariotoffia/gobridge/deployment/aws/cdk/gobridge"
)

func NewProductionStack(scope constructs.Construct, id string, props *awscdk.StackProps) awscdk.Stack {
    stack := awscdk.NewStack(scope, &id, props)

    // --- Networking (no NAT -- VPC endpoints instead) ---
    vpc := awsec2.NewVpc(stack, jsii.String("Vpc"), &awsec2.VpcProps{
        MaxAzs: jsii.Number(2), NatGateways: jsii.Number(0),
    })
    for _, ep := range []struct {
        ID  string
        Svc awsec2.InterfaceVpcEndpointAwsService
    }{
        {"SSM", awsec2.InterfaceVpcEndpointAwsService_SSM()},
        {"SQS", awsec2.InterfaceVpcEndpointAwsService_SQS()},
        {"ECR", awsec2.InterfaceVpcEndpointAwsService_ECR()},
        {"ECRDocker", awsec2.InterfaceVpcEndpointAwsService_ECR_DOCKER()},
        {"CWLogs", awsec2.InterfaceVpcEndpointAwsService_CLOUDWATCH_LOGS()},
        {"CWMetrics", awsec2.InterfaceVpcEndpointAwsService_CLOUDWATCH()},
    } {
        vpc.AddInterfaceEndpoint(jsii.String(ep.ID),
            &awsec2.InterfaceVpcEndpointOptions{
                Service: ep.Svc, PrivateDnsEnabled: jsii.Bool(true),
            },
        )
    }
    vpc.AddGatewayEndpoint(jsii.String("S3"), &awsec2.GatewayVpcEndpointOptions{
        Service: awsec2.GatewayVpcEndpointAwsService_S3(),
    })

    // --- KMS for SSM SecureString ---
    kmsKey := awskms.NewKey(stack, jsii.String("Key"), &awskms.KeyProps{
        Description:       jsii.String("GoBridge SSM encryption"),
        EnableKeyRotation: jsii.Bool(true),
    })

    // --- GoBridge cluster (control + autoscaled workers, EFS, log retention built in) ---
    workers := float64(2)
    src := gobridge.ConfigFile("bridge.yaml")
    bridge := gobridge.NewCluster(stack, "Bridge",
        &gobridge.ClusterProps{
            Vpc: vpc,
            Image: gobridge.ImageFromRegistry(
                "123456789012.dkr.ecr.eu-west-1.amazonaws.com/gobridge@sha256:<digest>"),
            // NewCluster forces topology filesystem_replicated.
            Bootstrap: gobridge.Bootstrap{
                BridgeID: "gobridge-prod", ConfigFilePath: "/var/lib/gobridge/bridge.yaml",
                PollInterval: "5s", AdminAPIKeyParam: "/gobridge/prod/admin-api-key",
                MonitorAPIKeyParam: "/gobridge/prod/monitor-api-key",
                // Publish runtime metrics to CloudWatch (grants PutMetricData
                // scoped to the namespace).
                MetricsExporter: "cloudwatch",
            },
            BridgeConfig:       src,
            CPU:                jsii.Number(512),
            MemoryMiB:          jsii.Number(1024),
            WorkerDesiredCount: &workers,
            AutoScaling: &gobridge.AutoScaling{
                Min: 2, Max: 8, TargetCPU: 70,
            },
            LogRetention: awslogs.RetentionDays_ONE_MONTH,
        },
    )

    // --- IAM: KMS decrypt + X-Ray on BOTH task roles ---
    for _, td := range []awsecs.FargateTaskDefinition{
        bridge.ControlTaskDefinition(), bridge.WorkerTaskDefinition(),
    } {
        td.TaskRole().AddToPrincipalPolicy(
            awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
                Actions:   &[]*string{jsii.String("kms:Decrypt")},
                Resources: &[]*string{kmsKey.KeyArn()},
            }),
        )
        td.TaskRole().AddToPrincipalPolicy(
            awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
                Actions: &[]*string{
                    jsii.String("xray:PutTraceSegments"), jsii.String("xray:PutTelemetryRecords"),
                    jsii.String("xray:GetSamplingRules"), jsii.String("xray:GetSamplingTargets"),
                },
                Resources: &[]*string{jsii.String("*")},
            }),
        )
    }

    // --- X-Ray sampling rule ---
    awsxray.NewCfnSamplingRule(stack, jsii.String("Sampling"),
        &awsxray.CfnSamplingRuleProps{
            SamplingRule: &awsxray.CfnSamplingRule_SamplingRuleProperty{
                RuleName: jsii.String("GoBridge-Prod"), Priority: jsii.Number(100),
                FixedRate: jsii.Number(0.1), ReservoirSize: jsii.Number(5),
                ServiceName: jsii.String("gobridge"), ServiceType: jsii.String("*"),
                Host: jsii.String("*"), HttpMethod: jsii.String("*"),
                UrlPath: jsii.String("*"), ResourceArn: jsii.String("*"),
            },
        },
    )

    // --- Alarms + SNS ---
    topic := awssns.NewTopic(stack, jsii.String("Alerts"),
        &awssns.TopicProps{TopicName: jsii.String("gobridge-prod-alerts")},
    )
    action := awscloudwatchactions.NewSnsAction(topic)

    alarms := []awscloudwatch.Alarm{
        awscloudwatch.NewAlarm(stack, jsii.String("DLQ"), &awscloudwatch.AlarmProps{
            AlarmName: jsii.String("GoBridge-DLQ"),
            Metric: awscloudwatch.NewMetric(&awscloudwatch.MetricProps{
                Namespace: jsii.String("GoBridge/Runtime"), MetricName: jsii.String("DLQEntries"),
                Statistic: jsii.String("Sum"), Period: awscdk.Duration_Minutes(jsii.Number(5)),
            }),
            Threshold: jsii.Number(0), EvaluationPeriods: jsii.Number(1),
            ComparisonOperator: awscloudwatch.ComparisonOperator_GREATER_THAN_THRESHOLD,
            TreatMissingData:   awscloudwatch.TreatMissingData_NOT_BREACHING,
        }),
        awscloudwatch.NewAlarm(stack, jsii.String("CPU"), &awscloudwatch.AlarmProps{
            AlarmName: jsii.String("GoBridge-HighCPU"),
            Metric: bridge.WorkerService().(awsecs.BaseService).MetricCpuUtilization(nil),
            Threshold: jsii.Number(80), EvaluationPeriods: jsii.Number(3),
            ComparisonOperator: awscloudwatch.ComparisonOperator_GREATER_THAN_THRESHOLD,
        }),
    }
    for _, a := range alarms {
        a.AddAlarmAction(action)
        a.AddOkAction(action)
    }

    // --- Dashboard ---
    dash := awscloudwatch.NewDashboard(stack, jsii.String("Dash"),
        &awscloudwatch.DashboardProps{DashboardName: jsii.String("GoBridge-Production")},
    )
    dash.AddWidgets(
        awscloudwatch.NewGraphWidget(&awscloudwatch.GraphWidgetProps{
            Title: jsii.String("Throughput"), Width: jsii.Number(12),
            Left: &[]awscloudwatch.IMetric{
                awscloudwatch.NewMetric(&awscloudwatch.MetricProps{
                    Namespace: jsii.String("GoBridge/Runtime"),
                    MetricName: jsii.String("MessagesReceived"), Statistic: jsii.String("Sum"),
                }),
                awscloudwatch.NewMetric(&awscloudwatch.MetricProps{
                    Namespace: jsii.String("GoBridge/Runtime"),
                    MetricName: jsii.String("MessagesSent"), Statistic: jsii.String("Sum"),
                }),
            },
        }),
        awscloudwatch.NewGraphWidget(&awscloudwatch.GraphWidgetProps{
            Title: jsii.String("ECS Health"), Width: jsii.Number(12),
            Left: &[]awscloudwatch.IMetric{
                bridge.WorkerService().(awsecs.BaseService).MetricCpuUtilization(nil),
                bridge.WorkerService().(awsecs.BaseService).MetricMemoryUtilization(nil),
            },
        }),
    )

    return stack
}

func main() {
    app := awscdk.NewApp(nil)
    NewProductionStack(app, "GoBridgeProd", &awscdk.StackProps{
        Env: &awscdk.Environment{
            Account: jsii.String("123456789012"), Region: jsii.String("eu-west-1"),
        },
    })
    app.Synth(nil)
}

Deploy:

cdk deploy GoBridgeProd