Skip to content

feat(hosting): Add secret() support for self-hosted/pipeline deployments #3228

Description

@adrianjoshua-strutt

Problem

When using @aws-amplify/hosting with self-hosted deployments (ampx deploy --pipeline or standalone ampx deploy), there is no way to reference secrets (API keys, connection ARNs, domain names, etc.) without hardcoding them in source code.

Current state of secrets in Amplify Gen2

Deployment Mode Secret mechanism How secrets are set
Amplify Managed Hosting secret() from @aws-amplify/backend Amplify Console UI
Cloud Sandbox secret() from @aws-amplify/backend npx ampx sandbox secret set <key>
Standalone (ampx deploy) ❌ None Must manually create SSM params at undocumented paths
Custom Pipeline (definePipeline) ❌ None No mechanism exists

The existing secret() from @aws-amplify/backend:

  • Resolves from SSM at paths like /amplify/shared/<app-id>/<key> or /amplify/<app-id>/<branch>-branch-<hash>/<key>
  • Is tightly coupled to the Amplify managed service (app-id, branch model)
  • Cannot be used in hosting.ts or pipeline.ts
  • Has no CLI support for standalone/pipeline deployments

The gap

Users of defineHosting() and definePipeline() have no way to:

  1. Reference secrets in hosting.ts (domains, API keys for Lambda env vars, third-party credentials)
  2. Reference secrets in pipeline.ts (CodeConnection ARNs, deployment config)
  3. Set secrets via CLI for standalone/pipeline mode

Proposed Solution

New secret() function in @aws-amplify/hosting

// amplify/hosting.ts
import { defineHosting, secret } from '@aws-amplify/hosting';

defineHosting({
  framework: 'nextjs',
  domain: secret('DOMAIN_PROD'),
  compute: {
    environment: {
      STRIPE_KEY: secret('STRIPE_KEY'),
    },
  },
});
// amplify/pipeline.ts
import { definePipeline, secret } from '@aws-amplify/hosting/pipeline';

definePipeline({
  source: {
    repo: 'my-org/my-app',
    connectionArn: secret('CONNECTION_ARN'),
  },
  branches: [{
    branch: 'main',
    stages: [
      { name: 'beta', config: { domain: secret('DOMAIN_BETA') } },
      { name: 'prod', config: { domain: secret('DOMAIN_PROD') } },
    ],
  }],
});

CLI for setting secrets

ampx secret set DOMAIN_BETA "beta.example.com"
ampx secret set DOMAIN_PROD "prod.example.com"
ampx secret set CONNECTION_ARN "arn:aws:codeconnections:..."
ampx secret set STRIPE_KEY "sk_live_..."

Design Principles

  1. Flat namespace — No magic stage-scoping. secret('DOMAIN_BETA') always resolves to the same SSM parameter regardless of which stage is deploying. Customer picks the key name explicitly.

  2. No confusion with Gen2 secret() — This is a SEPARATE function exported from @aws-amplify/hosting, not @aws-amplify/backend. Different deployment model (self-hosted vs managed), different package, different SSM path convention.

  3. Works in both hosting.ts and pipeline.ts — Same function, re-exported from @aws-amplify/hosting/pipeline.

  4. Explicit and debuggable — No implicit scoping, no hidden behavior. The key you set is the key you reference. 1:1 mapping.

  5. SSM-backed — Stored in AWS Systems Manager Parameter Store at a simple, documented path (e.g., /amplify/hosting/<identifier>/<key>).

Technical Considerations

Resolution timing differs by context

Context When secret resolves Mechanism
hosting.ts (Phase 1, CDK stage) CloudFormation deploy time SSM dynamic reference
hosting.ts (Phase 2, CodeBuild) CodeBuild runtime BuildEnvironmentVariableType.PARAMETER_STORE
pipeline.ts (source config) CloudFormation deploy time SSM dynamic reference (source action supports it)
pipeline.ts (stage config) Synth time OR CodeBuild runtime SDK call or PARAMETER_STORE env var

Implementation approach

  1. secret('KEY') returns a marker object (not a raw string)
  2. definePipeline() / defineHosting() processes markers:
    • For CDK construct props: converts to SSM dynamic reference (CFN resolves at deploy time)
    • For Phase 2 CodeBuild env vars: uses BuildEnvironmentVariableType.PARAMETER_STORE
  3. ampx secret set creates SSM SecureString at the conventional path
  4. Path convention: /amplify/hosting/<stack-identifier>/<key> (or simpler: /amplify/self-hosted/<key>)

Why not reuse secret() from @aws-amplify/backend?

  1. Wrong package@aws-amplify/backend is for backend resources (auth, data, storage). Hosting is separate.
  2. Wrong path convention — Backend secrets use /amplify/<app-id>/<branch>-branch-<hash>/<key> which requires an Amplify app-id (doesn't exist in self-hosted mode).
  3. Wrong resolution — Backend secret() returns BackendSecret type designed for backend construct integration, not hosting/pipeline props.
  4. Confusion — Having two different secret() functions with same name but different behavior from different packages would be worse than having clearly named, separate mechanisms.

Acceptance Criteria

  • secret() function exported from @aws-amplify/hosting
  • secret() re-exported from @aws-amplify/hosting/pipeline
  • ampx secret set <key> <value> CLI command for self-hosted mode
  • ampx secret list CLI command
  • ampx secret remove <key> CLI command
  • Works in defineHosting() props (domain, compute.environment)
  • Works in definePipeline() props (connectionArn, stage config)
  • Phase 2 CodeBuild step resolves secrets via PARAMETER_STORE env vars
  • Unit tests for secret marker resolution in both hosting and pipeline
  • Documentation for the new secret management workflow

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions