Skip to content

Commit 05a5990

Browse files
authored
feat(aws-marketplace): gate Enterprise on the AWS Marketplace container license and add the EKS install profile (#9917)
* feat(aws-marketplace): gate Enterprise on the AWS Marketplace container license and add the EKS install profile * fix(aws-marketplace): read the license instead of checking a unit out, scope the IAM role to it, and bump the chart * docs(aws-marketplace): say where the profile's images resolve, keep the RDS username tied to the template, and note why the license is read in the pod's Region
1 parent d026560 commit 05a5990

8 files changed

Lines changed: 452 additions & 4 deletions

File tree

‎apps/sim/bootstrap.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
/**
2-
* Container entrypoint. Hydrates `process.env` from the runtime secret and drops Next's route
3-
* cache when an earlier build wrote it, before loading the Next.js standalone server, so
4-
* application modules that read env at import time see the full configuration. See
2+
* Container entrypoint. Hydrates `process.env` from the runtime secret, resolves an AWS
3+
* Marketplace Enterprise license into `ENTERPRISE_ENABLED` on Marketplace installs, and drops
4+
* Next's route cache when an earlier build wrote it, before loading the Next.js standalone server,
5+
* so application modules that read env at import time see the full configuration. See
56
* `@sim/runtime-secrets`.
67
*/
78
import { fileURLToPath } from 'node:url'
89
import { loadRuntimeSecrets } from '@sim/runtime-secrets'
10+
import { applyAwsMarketplaceContainerLicense } from '@/lib/billing/aws-marketplace/container-license'
911
import { clearRouteCacheFromOtherBuild } from '@/lib/core/utils/route-cache'
1012

1113
await loadRuntimeSecrets()
14+
await applyAwsMarketplaceContainerLicense()
1215
await clearRouteCacheFromOtherBuild(fileURLToPath(new URL('./.next', import.meta.url)))
1316
// `server.js` is the Next standalone build artifact, a sibling of this file in
1417
// the image; it does not exist at type-check time, so the specifier is held in a
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
type GrantedLicense,
3+
LicenseManagerClient,
4+
ListReceivedLicensesCommand,
5+
} from '@aws-sdk/client-license-manager'
6+
import { createLogger } from '@sim/logger'
7+
import { getErrorMessage } from '@sim/utils/errors'
8+
import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server'
9+
10+
/**
11+
* Enterprise entitlement for Sim installed from the AWS Marketplace container listing.
12+
*
13+
* The listing sells an annual contract whose one dimension is `enterprise_license`. AWS Marketplace
14+
* grants the buyer a license in AWS License Manager, and the running product reads it. While the
15+
* license is active, the Enterprise suite is on (`ENTERPRISE_ENABLED`). Without one, Sim runs as the
16+
* open-source core, so a lapsed renewal or an unreachable License Manager never locks a buyer out
17+
* of their deployment.
18+
*
19+
* The check reads the license rather than checking a unit out of it. A contract usually covers the
20+
* whole deployment with one unit, so checking it out from every replica would make them compete
21+
* for it, and a unit left checked out (a failed check-in holds it for an hour) would read exactly
22+
* like an expired license.
23+
*
24+
* Opt-in: nothing here runs unless {@link PRODUCT_SKU_ENV} is set, which only the Marketplace
25+
* Helm profile does. Enterprise flags are read once at startup by both the server and the
26+
* browser, so instead of flipping them live, a re-check that sees the answer change asks the
27+
* process to shut down and the orchestrator starts it again in the right mode.
28+
*/
29+
30+
const logger = createLogger('AwsMarketplaceContainerLicense')
31+
32+
/** Set by the Marketplace Helm profile to the listing's product ID (the License Manager SKU). */
33+
const PRODUCT_SKU_ENV = 'AWS_MARKETPLACE_LICENSE_PRODUCT_SKU'
34+
35+
/**
36+
* Optional override for where the license is read. License Manager replicates a granted license to
37+
* other Regions, so the default is the pod's own Region, the same chain AWS's EKS walkthrough uses.
38+
*/
39+
const LICENSE_REGION_ENV = 'AWS_MARKETPLACE_LICENSE_REGION'
40+
41+
/** The issuer every AWS Marketplace license is signed by (a fixed value, not a placeholder). */
42+
const MARKETPLACE_KEY_FINGERPRINT = 'aws:294406891311:AWS/Marketplace:issuer-fingerprint'
43+
44+
/** The listing's contract dimension API name. */
45+
const ENTERPRISE_ENTITLEMENT = 'enterprise_license'
46+
47+
/** AWS recommends re-checking every 15 minutes to pick up renewals, upgrades, and cancellations. */
48+
const RECHECK_INTERVAL_MS = 15 * 60 * 1000
49+
50+
const ENTERPRISE_FLAGS = ['ENTERPRISE_ENABLED', 'NEXT_PUBLIC_ENTERPRISE_ENABLED'] as const
51+
52+
/** `unknown` is a failed check (network, permissions, throttling), never a verdict on the license. */
53+
type LicenseState = 'entitled' | 'not-entitled' | 'unknown'
54+
55+
function createClient(): LicenseManagerClient {
56+
const region =
57+
process.env[LICENSE_REGION_ENV] ||
58+
process.env.AWS_REGION ||
59+
process.env.AWS_DEFAULT_REGION ||
60+
'us-east-1'
61+
return new LicenseManagerClient({
62+
region,
63+
requestHandler: awsRequestHandler({ connectionTimeout: 3_000, requestTimeout: 10_000 }),
64+
})
65+
}
66+
67+
/**
68+
* An AWS Marketplace license this product's buyer holds is current when License Manager reports it
69+
* `AVAILABLE`, its issuer is AWS Marketplace, and it carries the Enterprise entitlement. License
70+
* Manager moves an ended, cancelled, or suspended license out of `AVAILABLE` itself.
71+
*/
72+
function isActiveEnterpriseLicense(license: GrantedLicense): boolean {
73+
return (
74+
license.Status === 'AVAILABLE' &&
75+
license.Issuer?.KeyFingerprint === MARKETPLACE_KEY_FINGERPRINT &&
76+
(license.Entitlements ?? []).some((entitlement) => entitlement.Name === ENTERPRISE_ENTITLEMENT)
77+
)
78+
}
79+
80+
async function checkLicense(
81+
client: LicenseManagerClient,
82+
productSku: string
83+
): Promise<LicenseState> {
84+
try {
85+
let nextToken: string | undefined
86+
do {
87+
const page = await client.send(
88+
new ListReceivedLicensesCommand({
89+
Filters: [
90+
{ Name: 'ProductSKU', Values: [productSku] },
91+
{ Name: 'Fingerprint', Values: [MARKETPLACE_KEY_FINGERPRINT] },
92+
],
93+
NextToken: nextToken,
94+
})
95+
)
96+
if ((page.Licenses ?? []).some(isActiveEnterpriseLicense)) return 'entitled'
97+
nextToken = page.NextToken
98+
} while (nextToken)
99+
return 'not-entitled'
100+
} catch (error) {
101+
logger.warn('Could not check the AWS Marketplace Enterprise license', {
102+
message: getErrorMessage(error, 'Unknown error'),
103+
})
104+
return 'unknown'
105+
}
106+
}
107+
108+
function setEnterpriseFlags(enabled: boolean): void {
109+
for (const flag of ENTERPRISE_FLAGS) process.env[flag] = enabled ? 'true' : 'false'
110+
}
111+
112+
/**
113+
* Resolves the Enterprise license before the server loads, so every Enterprise gate reads the
114+
* answer through `ENTERPRISE_ENABLED`, then keeps re-checking. Returns without doing anything on a
115+
* deployment that is not a Marketplace install.
116+
*/
117+
export async function applyAwsMarketplaceContainerLicense(): Promise<void> {
118+
const productSku = process.env[PRODUCT_SKU_ENV]
119+
if (!productSku) return
120+
121+
const client = createClient()
122+
const bootState = await checkLicense(client, productSku)
123+
const enterpriseAtBoot = bootState === 'entitled'
124+
setEnterpriseFlags(enterpriseAtBoot)
125+
logger.info(
126+
enterpriseAtBoot
127+
? 'AWS Marketplace Enterprise license active; Enterprise features on'
128+
: 'No active AWS Marketplace Enterprise license; running without Enterprise features',
129+
{ state: bootState }
130+
)
131+
132+
const recheck = setInterval(async () => {
133+
const state = await checkLicense(client, productSku)
134+
const entitledNow = state === 'entitled'
135+
if (state === 'unknown' || entitledNow === enterpriseAtBoot) return
136+
logger.info('AWS Marketplace Enterprise license changed; restarting to apply it', { state })
137+
clearInterval(recheck)
138+
process.kill(process.pid, 'SIGTERM')
139+
}, RECHECK_INTERVAL_MS)
140+
recheck.unref()
141+
}

‎apps/sim/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"@aws-sdk/client-iam": "3.1162.0",
8181
"@aws-sdk/client-identitystore": "3.1162.0",
8282
"@aws-sdk/client-lambda": "3.1162.0",
83+
"@aws-sdk/client-license-manager": "3.1162.0",
8384
"@aws-sdk/client-marketplace-entitlement-service": "3.1162.0",
8485
"@aws-sdk/client-marketplace-metering": "3.1162.0",
8586
"@aws-sdk/client-organizations": "3.1162.0",

‎bun.lock‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎helm/sim/Chart.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: sim
33
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
44
type: application
5-
version: 1.11.14
5+
version: 1.11.15
66
appVersion: "v0.8.26"
77
kubeVersion: ">=1.25.0-0"
88
home: https://sim.ai

‎helm/sim/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ Pre-built values files for common scenarios live in [`helm/sim/examples/`](https
261261
| `values-development.yaml` | Local dev / `kind` / `minikube`. Minimal resources, no TLS. |
262262
| `values-production.yaml` | Generic production: HA, network policy, autoscaling, monitoring. |
263263
| `values-aws.yaml` | EKS — EBS GP3 storage, ALB ingress, IRSA-friendly. |
264+
| `values-aws-marketplace.yaml` | EKS from the AWS Marketplace listing — Sim's images only, RDS + ElastiCache, Enterprise from the Marketplace license. |
264265
| `values-gcp.yaml` | GKE — Persistent Disk storage, GCP managed certs, Workload Identity. |
265266
| `values-azure.yaml` | AKS — managed-csi storage, NGINX ingress, GPU node pools. |
266267
| `values-external-db.yaml` | Production with a managed Postgres (RDS, Cloud SQL, Azure DB). |
@@ -285,6 +286,15 @@ helm install sim oci://ghcr.io/simstudioai/charts/sim \
285286
--set postgresql.auth.password="$POSTGRES_PASSWORD"
286287
```
287288

289+
### AWS Marketplace
290+
291+
The AWS Marketplace listing ships this chart with `values-aws-marketplace.yaml` as its defaults
292+
and every image pointed at the AWS Marketplace registry. It needs your own Amazon RDS for
293+
PostgreSQL 17 and Amazon ElastiCache: [`aws-marketplace-prerequisites.yaml`](https://github.com/simstudioai/sim/tree/main/helm/sim/examples/aws-marketplace-prerequisites.yaml)
294+
is a CloudFormation template that creates both, plus the IAM role the chart's ServiceAccount
295+
uses to check your Marketplace license in AWS License Manager. While that license is active the
296+
Enterprise suite is on; without it Sim runs as the open-source core.
297+
288298
---
289299

290300
## Parameters

0 commit comments

Comments
 (0)