Skip to content

nanohype/cdk-constructs

cdk-constructs

AWS CDK constructs for building production AI systems on AWS. Part of the nanohype ecosystem alongside the template catalog and the protohype subsystems built from it.

Every construct here encodes a decision — fail-secure defaults, prod-vs-staging shape, how ingress is scoped, what gets retained on teardown. Consume them when you want the decision already made; reach for raw aws-cdk-lib when you need a different one.

Install

npm install github:nanohype/cdk-constructs#v0.1.0

Peer deps: aws-cdk-lib ^2.250.0, constructs ^10.6.0, Node ≥24. The prepare hook runs tsc automatically on install — you get built dist/ with type declarations. Pin to a tag for reproducible deploys; use #main during active development.

What's inside

EnvelopeKey             KMS customer-managed key for envelope encryption
AppSecrets              Secrets Manager with seed-placeholders + preserve-on-update
BedrockLoggingDisabled  Deploy-time assert: Bedrock invocation logging off

PgvectorDatabase        RDS Postgres 16 with prod-vs-staging defaults
DynamoTable             PAY_PER_REQUEST, TTL, PITR-in-prod, GSI/LSI support
ArchiveBucket           S3 audit archive with lifecycle and block-public-access
RedisCluster            ElastiCache Redis, single-node dev / multi-AZ prod

WorkerService           ECS Fargate long-running worker, no ALB, optional ECS Exec
AlbWithTls              ALB with three env-driven TLS shapes
CronSchedule            EventBridge Scheduler cron, not legacy Rules

SqsWithDlq              Primary + DLQ + depth alarm, FIFO-aware
OtelSidecar             ADOT collector as Fargate sidecar

containerFromAsset      fromAsset with Platform.LINUX_AMD64 pinned
grantEcsExec            SSM-messages grants for ECS Exec
namePrefix              <project>-<env> helper

Quick starts

RDS pgvector + Redis + S3 audit archive

import {
  PgvectorDatabase,
  RedisCluster,
  ArchiveBucket,
  EnvelopeKey,
} from "@nanohype/cdk-constructs";

const auditKey = new EnvelopeKey(this, "AuditKey", {
  purpose: `${project} audit envelope`,
});

const db = new PgvectorDatabase(this, "Corpus", {
  vpc,
  computeSecurityGroup: worker.securityGroup,
  databaseName: project,
  isProd,
});

const redis = new RedisCluster(this, "Ratelimit", {
  vpc,
  computeSecurityGroup: worker.securityGroup,
  replicationGroupId: `${project}-rl-${env}`,
  isProd,
});

const audit = new ArchiveBucket(this, "AuditArchive", {
  bucketName: `${project}-audit-${env}-${this.account}`,
  isProd,
  encryptionKey: auditKey.key,
});

Your app runs CREATE EXTENSION IF NOT EXISTS vector; on boot against db.credentialSecret — the idempotent pattern. No custom resources, no schema gymnastics.

Worker service with OTel + Bedrock

import {
  WorkerService,
  OtelSidecar,
  BedrockLoggingDisabled,
  containerFromAsset,
} from "@nanohype/cdk-constructs";
import * as path from "node:path";

new BedrockLoggingDisabled(this, "DisableBedrockLogging", {
  identifier: project,
});

const worker = new WorkerService(this, "Classifier", {
  cluster,
  image: containerFromAsset(path.join(__dirname, "../..")),
  environment: { AWS_REGION: this.region, OTEL_SERVICE_NAME: project },
  secrets: {
    DB_PASSWORD: ecs.Secret.fromSecretsManager(db.credentialSecret, "password"),
  },
  enableExecute: true,
});

new OtelSidecar(this, "Otel", {
  taskDefinition: worker.taskDefinition,
  serviceName: project,
  environment: env,
});

EventBridge Scheduler + SQS

import { CronSchedule, SqsWithDlq } from "@nanohype/cdk-constructs";

const crawlQueue = new SqsWithDlq(this, "CrawlQueue", {
  queueName: `${project}-crawl-${env}`,
});

new CronSchedule(this, "CrawlSecEdgar", {
  scheduleName: `${project}-crawl-sec-edgar-${env}`,
  scheduleExpression: "cron(0 * * * ? *)",
  target: {
    arn: crawlQueue.queue.queueArn,
    input: JSON.stringify({ source: "sec-edgar" }),
    actions: ["sqs:SendMessage"],
  },
});

ALB with env-driven TLS

import { AlbWithTls } from "@nanohype/cdk-constructs";

const alb = new AlbWithTls(this, "Front", {
  vpc,
  tls: process.env.CERT_ARN
    ? {
        mode: "byo-cert",
        certArn: process.env.CERT_ARN,
        domainName: process.env.DOMAIN_NAME!,
      }
    : process.env.HOSTED_ZONE_ID
      ? {
          mode: "managed-cert",
          hostedZoneId: process.env.HOSTED_ZONE_ID,
          hostedZoneName: process.env.HOSTED_ZONE_NAME!,
          domainName: process.env.DOMAIN_NAME!,
        }
      : { mode: "http-only" },
});

Three shapes. CDK provisions the ACM cert + Route 53 alias for managed-cert. Caller owns DNS for byo-cert. http-only is smoke mode — OAuth callbacks won't work, by design.

Secrets Manager, seed-and-preserve

import { AppSecrets } from "@nanohype/cdk-constructs";

const appSecrets = new AppSecrets(this, "AppSecrets", {
  secretName: `${project}/${env}/app-secrets`,
  manualKeys: ["SLACK_BOT_TOKEN", "NOTION_OAUTH_CLIENT_SECRET", "SENTRY_DSN"],
  generatedKeys: { STATE_SIGNING_SECRET: { length: 64 } },
});

CREATE seeds placeholders (REPLACE_ME_SLACK_BOT_TOKEN) so ECS tasks start clean. Operators run aws secretsmanager put-secret-value out of band. Subsequent cdk deploy calls don't overwrite — the seed template is only applied on CREATE.

Design rules

Opinionated, not configurable. Every prop that's always the same default in practice IS the default. Props exist only for settings projects actually diverge on — FIFO vs standard, prod vs staging, table key shape, TLS mode. If a knob has one reasonable value in every context we've seen, the knob isn't there.

One construct = one pattern. If two projects use a construct differently, they fork in the consumer stack — the construct doesn't grow a conditional. A construct that serves two patterns eventually serves neither well.

Fail-secure. RETAIN on destructive paths. enableKeyRotation: true on KMS. blockPublicAccess.BLOCK_ALL on S3. deletionProtection: true on prod RDS. publiclyAccessible: false on every data plane. The default is always the paranoid one.

Env-driven prod posture. An isProd prop flips a bundle of defaults (multi-AZ / single-AZ, longer backups, RETAIN vs DESTROY, TLS vs no TLS on Redis). Consumers set it once from their stage config; constructs don't probe env vars themselves.

No runtime side effects at import. Constructs are pure CDK constructs — no file I/O, no network calls, no registry shenanigans.

Versioning

We're at 0.1.0 for the foundation. APIs may shift within 0.1.x as we add constructs and discover which shapes generalize. Pin to a specific tag; treat main as "nightly" until we cut 0.2.0.

Roadmap

What production stacks still hand-roll:

  • Lambda-backed SQS→S3 archive consumer (composes SqsWithDlq + a Lambda + ArchiveBucket)
  • CDK-side pgvector CREATE EXTENSION bootstrap (requires bundling pg in a custom resource — v0.1.0 documents app-side bootstrap as the default)
  • CloudWatch dashboard helper that accepts widget specs — today stacks build dashboards by hand
  • Resource-scoped IAM grant helpers (grantBedrock, grantDynamoRead, etc.) — today stacks write the PolicyStatement inline
  • Canary / smoke-test helpers that read stack outputs and probe endpoints post-deploy

All observed in almanac, marshal, palisade, watchtower. Extract when two subsystems converge on the same shape.

Development

git clone git@github.com:nanohype/cdk-constructs.git
cd cdk-constructs
npm install
npm run typecheck
npm test
npm run build

Tests use aws-cdk-lib/assertions to check resource properties, counts, and IAM policy shapes — not snapshots. Snapshot churn-to-signal is too low for a construct library.

License

Apache-2.0

About

Opinionated AWS CDK constructs for nanohype-derived projects — pgvector, SQS+DLQ, Bedrock logging disable, ADOT sidecar, DynamoDB

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors