AWS
This page shows how to run each deployment pattern on AWS, in your own accounts, with managed AWS services supplying what the runtime leaves to you: secrets, encryption keys, network isolation, logging and metrics.
Reference examples, not shipped artifacts
The CloudFormation template, task definition, manifests and scripts on this page are examples for you to adapt. Helixor does not publish AWS images, templates or modules for the runtime. Every path here works the same way: install the runtime wheel into an image you build, store it in Amazon ECR, and bring your own license file. Account IDs, Regions, names and sizes are placeholders.
Reference architecture#
Choose a pattern#
| Pattern | AWS services | Use when |
|---|---|---|
| (a) In-process | ECS on Fargate or EC2, EKS, EC2 | Python services that need decisions on the hot path. |
| (b) Sidecar in an ECS task | ECS on Fargate or EC2 | Callers in any language. Rule changes ship without touching the application image. |
| (c) Sidecar in an EKS pod | EKS, Secrets Store CSI driver, IRSA or EKS Pod Identity | You run on Kubernetes. |
| (d) In-process in Lambda | Lambda (container image) | Event-driven or spiky workloads that should scale to zero. |
| (e) Isolated VPC | Private subnets, VPC endpoints | No internet route is allowed. |
| (f) Shared in-VPC service | Internal ALB with mutual TLS, VPC Lattice, or API Gateway private integration | Many services share one set of policies. |
The generic versions of these patterns are in Deployment patterns.
Build the images#
Start from the hardened sidecar image in Deployment patterns, built from the runtime wheel and pushed to ECR. For AWS, build one more layer per pack version. Your pipeline copies the approved pack from the S3 pack bucket into the image, and adds an entrypoint that turns the license secret into a file.
The pack is encrypted and opens only with its license, so baking it into a versioned image is safe. The image tag then becomes the unit you promote and roll back. The license never goes into an image.
FROM 111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-base:0.2.1 # Approved pack, copied from the S3 pack bucket by the pipeline COPY --chown=10001:10001 guard.hxpack /opt/helixor/pack/guard.hxpack COPY --chmod=0755 aws-entrypoint.sh /opt/helixor/aws-entrypoint.sh USER 10001 ENTRYPOINT ["/opt/helixor/aws-entrypoint.sh"]
The runtime reads the license from a file path. ECS can inject a Secrets Manager secret only as an environment variable, so the entrypoint writes the secret to an in-memory file with mode 600. It then removes the variable and hands over to the service:
#!/bin/sh
set -eu
umask 077 # new files are mode 600
: "${HELIXOR_LICENSE_JSON:?license secret was not injected}"
mkdir -p /dev/shm/helixor
printf '%s' "$HELIXOR_LICENSE_JSON" > /dev/shm/helixor/helixor.lic
unset HELIXOR_LICENSE_JSON
exec helixor-pack serve \
--pack /opt/helixor/pack/guard.hxpack \
--license /dev/shm/helixor/helixor.lic \
--host 127.0.0.1 --port 18734
/dev/shmis the in-memory file system that containers get by default, and it stays writable with a read-only root file system. Confirm it is present on your platform version. If it is not, use a task volume. On Fargate, task storage is disk-backed ephemeral storage. On the EC2 launch type you can add atmpfsmount inlinuxParameters.- Leave
torchout of decision images. The runtime imports it at start-up when it is installed, which adds start time and memory, and evaluation never uses it. - Build for arm64 if you run on Graviton-based instances or Fargate on ARM64. The runtime is pure Python and runs on both architectures. Benchmark each on your workload.
(a) In-process on ECS, EKS or EC2#
Install the wheel into your Python service's image, and mount the pack and license as in (b). Create one engine per worker process at start-up, and run one warm-up evaluation before the process reports healthy. Scale with tasks, pods or processes, about one vCPU each. See Performance efficiency.
from helixor_runtime import HelixorEngine
engine = HelixorEngine.load_pack("/opt/helixor/pack/guard.hxpack", # one per worker process
license_file="/dev/shm/helixor/helixor.lic")
engine.evaluate("warm-up")
def guard(text: str) -> str:
result = engine.evaluate(text)
if result.action.startswith("block_") or any(t.severity == "FATAL" for t in result.triggers):
raise PermissionError(result.reason)
return result.remedy.clean_text
Check engine.pack_id at start-up; see Reliability. (In 0.2.0, load_pack could not load sealed packs, so in-process deployments ran the built-in pack; 0.2.1 fixes that.)
(b) Sidecar decision service in an ECS task#
With awsvpc networking, the containers in one task share a network namespace, so the application reaches the sidecar on 127.0.0.1. Nothing outside the task can reach a port bound to loopback, and that bind is your access control: the runtime has no authentication. Do not add a port mapping for the sidecar.
{
"family": "claims-api-prod",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "1024",
"memory": "2048",
"runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "ARM64" },
"executionRoleArn": "arn:aws:iam::111122223333:role/claims-api-execution",
"taskRoleArn": "arn:aws:iam::111122223333:role/claims-api-task",
"containerDefinitions": [
{
"name": "app",
"image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/claims-api:2.4.0",
"essential": true,
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"environment": [{ "name": "DECISION_URL", "value": "http://127.0.0.1:18734" }],
"dependsOn": [{ "containerName": "helixor-decision", "condition": "HEALTHY" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/helixor/prod/decision",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "app"
}
}
},
{
"name": "helixor-decision",
"image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-claims-guard:1.4.0",
"essential": true,
"user": "10001",
"readonlyRootFilesystem": true,
"secrets": [
{
"name": "HELIXOR_LICENSE_JSON",
"valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/helixor/license-2026-AbCdEf"
}
],
"healthCheck": {
"command": ["CMD", "python", "/opt/helixor/probe.py"],
"interval": 10,
"timeout": 3,
"retries": 3,
"startPeriod": 20
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/helixor/prod/decision",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "decision"
}
}
}
]
}
- Execution role:
secretsmanager:GetSecretValueon the license secret andkms:Decrypton its key, plus ECR pull and log writes. The decision container needs no task-role permissions. - Container health check: an exec probe that runs a real evaluation (
probe.pyfrom Reliability). It checks the pack ID and a real verdict;GET /v1/healthshows only that the process is up and which pack it loaded. - Ordering: the application container depends on the sidecar being
HEALTHY, so it never starts without a working decision service. - Deployment: enable the deployment circuit breaker with rollback. A task whose license and pack do not match fails its health check, and the deployment rolls back.
CloudFormation example#
This template creates the pieces around the Fargate sidecar service:
- a customer managed KMS key
- a versioned, SSE-KMS pack bucket that denies non-TLS access
- an encrypted log group
- least-privilege roles
- a security group that allows HTTPS only to VPC endpoints
- the task definition and service
Create the license secret outside the template, so the license contents never appear in a template or stack parameter. The template passes cfn-lint.
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Reference example (not a shipped artifact): Helixor decision sidecar on
AWS Fargate with Secrets Manager, KMS, CloudWatch Logs and an S3 pack bucket.
Parameters:
Environment:
Type: String
AllowedValues: [dev, stage, prod]
CostCenter:
Type: String
VpcId:
Type: AWS::EC2::VPC::Id
VpcCidr:
Type: String
Description: CIDR of the VPC that hosts the interface endpoints
S3PrefixListId:
Type: String
Description: Prefix list of the S3 gateway endpoint (image layers come from S3)
PrivateSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
ClusterName:
Type: String
AppImage:
Type: String
DecisionImage:
Type: String
Description: Your decision image with the pack for this license baked in
LicenseSecretArn:
Type: String
Description: Secrets Manager secret holding the .hxlic contents, created out of band
LicenseSecretKmsKeyArn:
Type: String
Description: Customer managed KMS key that encrypts the license secret
Resources:
DataKey:
Type: AWS::KMS::Key
Properties:
Description: Helixor pack bucket and decision log group
EnableKeyRotation: true
KeyPolicy:
Version: "2012-10-17"
Statement:
- Sid: AccountAdministration
Effect: Allow
Principal:
AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root
Action: kms:*
Resource: "*"
- Sid: CloudWatchLogs
Effect: Allow
Principal:
Service: !Sub logs.${AWS::Region}.amazonaws.com
Action:
- kms:Encrypt*
- kms:Decrypt*
- kms:ReEncrypt*
- kms:GenerateDataKey*
- kms:Describe*
Resource: "*"
Condition:
ArnLike:
kms:EncryptionContext:aws:logs:arn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/helixor/*
Tags:
- { Key: Application, Value: helixor-decision }
- { Key: Environment, Value: !Ref Environment }
- { Key: CostCenter, Value: !Ref CostCenter }
PackBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !GetAtt DataKey.Arn
BucketKeyEnabled: true
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
Tags:
- { Key: Application, Value: helixor-decision }
- { Key: Environment, Value: !Ref Environment }
- { Key: CostCenter, Value: !Ref CostCenter }
PackBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref PackBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: DenyInsecureTransport
Effect: Deny
Principal: "*"
Action: s3:*
Resource:
- !GetAtt PackBucket.Arn
- !Sub ${PackBucket.Arn}/*
Condition:
Bool:
aws:SecureTransport: "false"
DecisionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /helixor/${Environment}/decision
RetentionInDays: 365
KmsKeyId: !GetAtt DataKey.Arn
Tags:
- { Key: Application, Value: helixor-decision }
- { Key: Environment, Value: !Ref Environment }
- { Key: CostCenter, Value: !Ref CostCenter }
ExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: read-helixor-license
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref LicenseSecretArn
- Effect: Allow
Action: kms:Decrypt
Resource: !Ref LicenseSecretKmsKeyArn
TaskRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ServiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Decision tasks - no inbound, HTTPS to VPC endpoints only
VpcId: !Ref VpcId
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Ref VpcCidr
Description: Interface endpoints (ECR, Secrets Manager, CloudWatch Logs, KMS)
- IpProtocol: tcp
FromPort: 443
ToPort: 443
DestinationPrefixListId: !Ref S3PrefixListId
Description: S3 gateway endpoint
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub helixor-decision-${Environment}
RequiresCompatibilities: [FARGATE]
NetworkMode: awsvpc
Cpu: "1024"
Memory: "2048"
RuntimePlatform:
OperatingSystemFamily: LINUX
CpuArchitecture: ARM64
ExecutionRoleArn: !GetAtt ExecutionRole.Arn
TaskRoleArn: !GetAtt TaskRole.Arn
ContainerDefinitions:
- Name: app
Image: !Ref AppImage
Essential: true
Environment:
- { Name: DECISION_URL, Value: "http://127.0.0.1:18734" }
DependsOn:
- { ContainerName: helixor-decision, Condition: HEALTHY }
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref DecisionLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: app
- Name: helixor-decision
Image: !Ref DecisionImage
Essential: true
User: "10001"
ReadonlyRootFilesystem: true
Secrets:
- { Name: HELIXOR_LICENSE_JSON, ValueFrom: !Ref LicenseSecretArn }
HealthCheck:
Command: ["CMD", "python", "/opt/helixor/probe.py"]
Interval: 10
Timeout: 3
Retries: 3
StartPeriod: 20
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref DecisionLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: decision
Tags:
- { Key: Application, Value: helixor-decision }
- { Key: Environment, Value: !Ref Environment }
- { Key: CostCenter, Value: !Ref CostCenter }
Service:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref ClusterName
LaunchType: FARGATE
DesiredCount: 2
TaskDefinition: !Ref TaskDefinition
PropagateTags: TASK_DEFINITION
EnableECSManagedTags: true
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
DeploymentCircuitBreaker:
Enable: true
Rollback: true
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets: !Ref PrivateSubnetIds
SecurityGroups:
- !Ref ServiceSecurityGroup
Tags:
- { Key: Application, Value: helixor-decision }
- { Key: Environment, Value: !Ref Environment }
- { Key: CostCenter, Value: !Ref CostCenter }
Outputs:
PackBucketName:
Value: !Ref PackBucket
DataKeyArn:
Value: !GetAtt DataKey.Arn
(c) Sidecar in an EKS pod#
On EKS, the Secrets Store CSI driver with the AWS provider mounts the license from Secrets Manager as a file. That is exactly what the runtime needs, so no entrypoint script is required. The pod reads the secret with its own identity: IAM roles for service accounts (IRSA), or EKS Pod Identity.
apiVersion: v1
kind: ServiceAccount
metadata:
name: claims-api
namespace: claims
annotations:
# IRSA: role allowed to read only this secret and decrypt with its KMS key
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/claims-api-helixor-license
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: helixor-license-2026
namespace: claims
spec:
provider: aws
parameters:
# With EKS Pod Identity instead of IRSA, add: usePodIdentity: "true"
objects: |
- objectName: "prod/helixor/license-2026"
objectType: "secretsmanager"
objectAlias: "helixor.lic"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: claims-api
namespace: claims
spec:
replicas: 3
selector:
matchLabels: { app: claims-api }
template:
metadata:
labels: { app: claims-api }
spec:
serviceAccountName: claims-api
securityContext:
runAsNonRoot: true
fsGroup: 10001
containers:
- name: app
image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/claims-api:2.4.0
env:
- { name: DECISION_URL, value: "http://127.0.0.1:18734" }
- name: helixor-decision
image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/helixor-decision-claims-guard:1.4.0
command: ["helixor-pack", "serve",
"--pack", "/opt/helixor/pack/guard.hxpack",
"--license", "/run/secrets/helixor/helixor.lic",
"--host", "127.0.0.1", "--port", "18734"]
securityContext:
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: license, mountPath: /run/secrets/helixor, readOnly: true }
readinessProbe:
exec: { command: ["python", "/opt/helixor/probe.py"] }
periodSeconds: 10
livenessProbe:
exec: { command: ["python", "/opt/helixor/probe.py"] }
periodSeconds: 30
volumes:
- name: license
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: helixor-license-2026
- Scope the pod's IAM role to
secretsmanager:GetSecretValueon this one secret, andkms:Decrypton its key. - Mount the license volume only into the decision container. Check the file mode your provider version applies to mounted objects.
- Kubernetes NetworkPolicy applies to the whole pod, and is enforced only when your cluster runs a policy engine, such as the VPC CNI with network policy enabled. A deny-all-egress policy on a sidecar pod also cuts off the application. For a decision workload with egress fully denied, use pattern (f). Security groups for pods are an alternative control at the ENI level.
- Use exec probes for readiness, for the reason given in (b). An HTTP liveness probe on
GET /v1/healthis fine.
(d) In-process in AWS Lambda#
Package the function as a container image. The runtime's dependencies include numpy, cryptography, pydantic, fastapi and uvicorn, and a container image avoids the size limits of zip packages.
FROM public.ecr.aws/lambda/python:3.11
COPY helixor_runtime-0.2.1-py3-none-any.whl /tmp/
RUN pip install --no-cache-dir /tmp/helixor_runtime-0.2.1-py3-none-any.whl \
&& rm -f /tmp/*.whl
COPY handler.py ${LAMBDA_TASK_ROOT}/
CMD ["handler.handler"]
Create the engine during initialization, outside the handler, so each execution environment builds it once and reuses it for every invocation:
from helixor_runtime import HelixorEngine
ENGINE = HelixorEngine() # once per execution environment
ENGINE.evaluate("warm-up") # compile patterns during init, not on the first request
def handler(event, context):
result = ENGINE.evaluate(event["text"])
blocked = result.action.startswith("block_") or any(
t.severity == "FATAL" for t in result.triggers)
print({"action": result.action, "receipt_hash": result.receipt_hash}) # never the payload
return {"blocked": blocked,
"text": None if blocked else result.remedy.clean_text,
"receipt_hash": result.receipt_hash}
- Cold start: importing the runtime and its dependencies, plus the warm-up evaluation, happens on every new execution environment. Measure it. Use provisioned concurrency if your latency budget cannot absorb it.
- Memory and CPU: Lambda allocates CPU in proportion to memory. Measure duration at a few memory sizes and choose the cheapest one that meets your latency.
- Concurrency: each execution environment handles one invocation at a time, so one engine per environment is naturally safe.
For sealed packs, write the license to /tmp during initialization and pass that path to HelixorEngine.load_pack(). Read it with an SDK call, or through the AWS Parameters and Secrets Lambda Extension:
import os, boto3
def write_license(secret_id: str, path: str = "/tmp/helixor/helixor.lic") -> str:
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
value = boto3.client("secretsmanager").get_secret_value(SecretId=secret_id)["SecretString"]
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
f.write(value)
return path
(e) Isolated VPC#
Run the workload in private subnets with no internet gateway and no NAT gateway. Everything the tasks need at start-up is reached through VPC endpoints:
| Endpoint | Type | Why |
|---|---|---|
| ECR API and ECR Docker registry | Interface | Pull images |
| Amazon S3 | Gateway | Image layers, and pack objects if you fetch them at start-up |
| Secrets Manager | Interface | The license secret |
| CloudWatch Logs | Interface | Container logs, receipts, EMF metrics |
| AWS KMS | Interface | Decrypting the secret and S3 objects |
| AWS STS, and your orchestrator's endpoints | Interface | Only if your platform needs them, for example IRSA on EKS or ECS on the EC2 launch type |
- Attach endpoint policies that allow only your accounts' resources, and an S3 gateway endpoint policy that allows only your pack and log buckets.
- Compile packs on a build host that can reach nothing outside the VPC: use
helixor-pack compilewithout--remote. Commands that contact Helixor (compile --remote,catalog,download-pack) fail without a route, so bring catalog packs in through your artifact process. - Hosts use the Amazon Time Sync Service, which is reachable without internet access. License validity uses the local clock.
(f) Shared in-VPC decision service#
The runtime's own authentication is one shared bearer token over plain HTTP, so the AWS front door provides TLS and per-caller authentication. Choose one:
| Front door | Authentication | Notes |
|---|---|---|
| Internal Application Load Balancer | Mutual TLS in verify mode, with a trust store of your client CAs | Target type ip to the task. |
| VPC Lattice service | IAM auth policy; callers sign requests with SigV4 | Identity-based access across VPCs and accounts. |
| API Gateway private REST API | IAM authorization and a resource policy; VPC link to a Network Load Balancer | Adds throttling and request validation. |
- In this pattern the service must accept traffic from the front door, so bind
helixor-pack serveto the task address. The service refuses a non-loopback address without a token, so injectHELIXOR_SERVICE_TOKENfrom Secrets Manager and have the front door send it (or terminate authentication at a proxy in the task and keep the service on loopback). Set the task security group to allow ingress on that port only from the load balancer or Lattice security group. Without that rule, the service is open to the VPC. - Give the task security group no egress rules except to the VPC endpoints it needs. The runtime makes no outbound calls.
- Load balancer health checks can only send
GET. Point the target health check atGET /v1/health, which needs no token and shows that the process is up. Rely on the ECS container health check (the exec evaluation probe) to replace tasks that are up but wrong. - At the front door, set a maximum body size and an idle timeout, and never allow browser origins.
Cross-cutting guidance#
Pack artifacts in S3#
- Keep one pack bucket per account. Enable versioning, SSE-KMS with a customer managed key, Block Public Access and a TLS-only bucket policy.
- Lay objects out as
packs/<pack_id>/<version>/<license>/guard.hxpackwith the SHA-256 digest beside each object. - Promote by copying the approved object from the build account's bucket to the target account's bucket. Grant the target account read access and
kms:Decrypton the source key. Never recompile after approval. Remember that a pack opens only with the license it was compiled for. - Use a service control policy so that only the pipeline role can write to production pack buckets.
Receipts and audit logs#
Receipts are unkeyed fingerprints, so tamper evidence has to come from where you store them:
- Log
Write one structured line per decision to CloudWatch Logs, in a KMS-encrypted log group. Use the field set from Operational excellence, never payloads or
matched_items. - Export
Stream the log group with a subscription filter to Amazon Data Firehose, delivering to an S3 audit bucket.
- Lock
Create the audit bucket with S3 Object Lock in compliance mode and a default retention that matches your regulation. Nobody, including the account root user, can delete or overwrite a locked object before its retention ends.
- Sign (optional)
For per-record tamper evidence, sign each record with a KMS asymmetric key or an HMAC key before you write it; see Security.
KMS keys#
Use customer managed keys for the license secret, the pack bucket, the log group and the audit bucket. Grant kms:Decrypt on the license key only to the roles that start decision workloads. Enable automatic key rotation. KMS protects these artifacts at rest outside the runtime. The runtime itself has no KMS integration, and wrapping pack keys with a key you control is Planned.
Egress control#
- Security groups with no
0.0.0.0/0egress. Allow HTTPS only to the endpoints' security group or CIDR, and to the S3 prefix list. - VPC endpoints for every AWS service the task uses, so no NAT gateway is needed.
- AWS Network Firewall, optionally, with a domain allow-list for any workload that must reach the internet. The decision service itself never needs to.
- VPC Flow Logs on decision subnets. Any rejected outbound flow from a decision task is worth an alert.
Metrics with CloudWatch embedded metric format#
The runtime emits no metrics. Emit them from your application in CloudWatch embedded metric format (EMF). In Lambda, print EMF lines to standard output. On ECS or EKS, send them through the CloudWatch agent. Use the metric names from Operational excellence:
{
"_aws": {
"Timestamp": 1790000000000,
"CloudWatchMetrics": [{
"Namespace": "Helixor/Decisions",
"Dimensions": [["PackId", "Action"]],
"Metrics": [{ "Name": "Decisions", "Unit": "Count" },
{ "Name": "CallerLatency", "Unit": "Milliseconds" }]
}]
},
"PackId": "custom.claims_guard.v1",
"Action": "redact_and_permit_contact_pii",
"Decisions": 1,
"CallerLatency": 0.4,
"receipt_hash": "hx_proof_ddaca3a134e0b6e6599f3ecd"
}
Keep dimensions low-cardinality: pack ID and action, never request IDs. Receipt hashes belong in the record's properties, not in dimensions. Publish helixor_license_days_remaining from a daily EventBridge Scheduler job that runs the expiry check in Reliability. That job is a scheduled check, not a polling loop in the service.
Health checks#
ECS container health checks, EKS readiness probes and Lambda initialization should all run a real evaluation of a harmless payload and check the pack ID. Load balancers that can only GET check /v1/health for liveness, as described in (f).
Multi-account layout#
- Keep development, staging and production in separate member accounts, and keep security tooling and log archives in their own accounts.
- Use one license per environment where your agreement allows. Each environment's license lives only in that account's Secrets Manager. Because a pack opens only with its license, the build account compiles one pack per environment license from the same reviewed commit.
- Enforce guardrails with service control policies: no public S3, no deletion of audit buckets, and pipeline-only writes to pack buckets and ECR repositories.
Tagging and cost allocation#
Tag every resource with Application, Environment and CostCenter, and activate them as cost allocation tags. For ECS, set PropagateTags to TASK_DEFINITION and enable ECS managed tags, so task-level cost lands on the right owner. Add a PackId tag to shared decision services if you charge teams back by policy.
Next steps#
- Map this deployment to the six AWS pillars in AWS Well-Architected alignment.
- Sign off with the Production checklist.