From 7a51bf1f3485d9cbb6ebe116a78e192a6be5c1f3 Mon Sep 17 00:00:00 2001 From: shaarron Date: Tue, 26 May 2026 16:05:37 +0300 Subject: [PATCH 01/11] cloudformation initial commit --- deploy/aws/README.md | 84 +++++++ deploy/aws/template.yaml | 468 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 552 insertions(+) create mode 100644 deploy/aws/README.md create mode 100644 deploy/aws/template.yaml diff --git a/deploy/aws/README.md b/deploy/aws/README.md new file mode 100644 index 000000000..694b7493e --- /dev/null +++ b/deploy/aws/README.md @@ -0,0 +1,84 @@ +# Open Design AWS Deployment + +This directory contains an AWS CloudFormation template (`template.yaml`) to deploy Open Design into your AWS environment using Amazon Elastic Container Service (ECS) with AWS Fargate. + +## Architecture Overview + +The template provisions a robust, highly-available, and secure architecture for Open Design: + +* **Networking:** A new Virtual Private Cloud (VPC) spanning two Availability Zones, with both Public and Private subnets. A NAT Gateway allows outbound internet access. +* **Load Balancing:** An internet-facing Application Load Balancer (ALB) routes incoming traffic. It optionally supports HTTPS if a custom domain and ACM certificate are provided. +* **Compute:** AWS ECS running on serverless Fargate instances in the private subnets. The task definition includes: + * The **Open Design** app container. + * An **Nginx Auth Proxy** sidecar container that securely attaches the Open Design API Token to incoming `/api/` requests. +* **Storage:** Amazon Elastic File System (EFS) is mounted to the Fargate containers to durably store the Open Design `.od` SQLite database and file artifacts. It is configured with deletion protection (`Retain`) to prevent accidental data loss. +* **Security:** + * **Secrets Manager:** Securely stores the Open Design API Token, preventing it from being exposed in plain text. + * **Security Groups:** Restrict traffic flow (e.g., the ALB only accepts traffic from a specified IP range; Fargate only accepts traffic from the ALB; EFS only accepts traffic from Fargate). +* **Logging:** Amazon CloudWatch Log Group captures container logs for easy debugging. + +## Prerequisites + +* An AWS Account. +* [AWS CLI](https://aws.amazon.com/cli/) installed and configured with appropriate permissions. +* (Optional) An ACM Certificate ARN if you want to use a custom domain with HTTPS. + +## Parameters + +When deploying the CloudFormation stack, you can customize the following parameters: + +| Parameter | Description | Default | +| :--- | :--- | :--- | +| `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | +| `ApiToken` | **(Required)** The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager. | | +| `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | +| `DockerImage` | The Open Design Docker image to deploy. | `vanjayak/open-design:latest` | +| `VpcCidr` | The CIDR block for the VPC. | `10.42.0.0/16` | +| `PublicSubnet1Cidr` | The CIDR block for Public Subnet 1 (AZ1). | `10.42.1.0/24` | +| `PublicSubnet2Cidr` | The CIDR block for Public Subnet 2 (AZ2). | `10.42.3.0/24` | +| `PrivateSubnetCidr` | The CIDR block for the Private Subnet. | `10.42.2.0/24` | +| `FargateCpu` | Fargate task CPU units (1024 = 1 vCPU). Allowed values: 256, 512, 1024, 2048, 4096. | `512` | +| `FargateMemory` | Fargate task memory in MiB. | `1024` | +| `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | +| `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | +| `ContainerPort` | The internal port the Open Design container listens on. | `7456` | +| `AppStoragePath` | The container path where the `.od` SQLite directory is mounted via EFS. | `/app/.od` | + +## Deployment + +You can deploy this stack via the AWS Management Console or the AWS CLI. + +### Using AWS Management Console + +1. Log in to the AWS Management Console and navigate to the **CloudFormation** service. +2. Click **Create stack** and select **With new resources (standard)**. +3. Under **Prerequisite - Prepare template**, select **Template is ready**. +4. Under **Specify template**, select **Upload a template file**, click **Choose file**, and select the `template.yaml` file from this directory. +5. Click **Next**. +6. Enter a **Stack name** (e.g., `open-design-stack`). +7. Fill in the **Parameters** according to your requirements. Note that `ApiToken` and `AllowedSourceIp` are required. +8. Click **Next**. Configure any stack options if desired, then click **Next** again. +9. Scroll to the bottom of the review page, check the box that says **I acknowledge that AWS CloudFormation might create IAM resources**, and click **Submit**. + +### Using AWS CLI + +1. Open your terminal and navigate to this directory. +2. Run the `aws cloudformation deploy` command, passing in the required parameters: + +```bash +aws cloudformation deploy \ + --template-file template.yaml \ + --stack-name open-design-stack \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides \ + ApiToken="YOUR_SECURE_API_TOKEN" \ + AllowedSourceIp="YOUR_IP_ADDRESS/32" +``` + +*Note: If you want to use a custom domain with HTTPS, include the `CustomDomainName` and `AcmCertificateArn` parameters in the `--parameter-overrides` list.* + +## Accessing the Application + +Once the CloudFormation stack creation is complete, go to the **Outputs** tab of the stack in the AWS CloudFormation Console. + +You will find the `AppUrl` output, which contains the HTTP link to the Application Load Balancer (or your custom domain if configured). Use this URL to access Open Design. diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml new file mode 100644 index 000000000..21ca00dd0 --- /dev/null +++ b/deploy/aws/template.yaml @@ -0,0 +1,468 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Open Design ECS/Fargate Deployment (VPC, ALB, EFS, Private Access)' + +Parameters: + CustomDomainName: + Type: String + Description: '(Optional) Your custom domain name (e.g., design.yourcompany.com). Leave blank to use the default ALB URL.' + Default: '' + AcmCertificateArn: + Type: String + Description: '(Optional) The ARN of your AWS Certificate Manager (ACM) certificate. Required if CustomDomainName is provided.' + Default: '' + DockerImage: + Type: String + Default: 'vanjayak/open-design:latest' + AllowedSourceIp: + Type: String + Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' + ContainerPort: + Type: Number + Default: 7456 + AppStoragePath: + Type: String + Description: 'Container path where the .od SQLite directory is stored.' + Default: '/app/.od' + ApiToken: + Type: String + NoEcho: true + Description: 'REQUIRED: The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager.' + VpcCidr: + Type: String + Default: '10.42.0.0/16' + Description: 'The CIDR block for the VPC' + PublicSubnet1Cidr: + Type: String + Default: '10.42.1.0/24' + Description: 'The CIDR block for Public Subnet 1 (AZ1)' + PublicSubnet2Cidr: + Type: String + Default: '10.42.3.0/24' + Description: 'The CIDR block for Public Subnet 2 (AZ2)' + PrivateSubnetCidr: + Type: String + Default: '10.42.2.0/24' + Description: 'The CIDR block for the Private Subnet' + FargateCpu: + Type: String + Default: '512' + AllowedValues: ['256', '512', '1024', '2048', '4096'] + Description: 'Fargate task CPU units (1024 = 1 vCPU)' + FargateMemory: + Type: String + Default: '1024' + Description: 'Fargate task memory in MiB' + +Conditions: + UseCustomDomain: !Not [!Equals [!Ref CustomDomainName, '']] + +Resources: + # NETWORKING (VPC, Subnets, NAT) + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCidr + EnableDnsSupport: true + EnableDnsHostnames: true + + InternetGateway: + Type: AWS::EC2::InternetGateway + AttachGateway: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + PublicSubnet: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Ref PublicSubnet1Cidr + MapPublicIpOnLaunch: true + AvailabilityZone: !Select [ 0, !GetAZs '' ] + + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Ref PublicSubnet2Cidr + MapPublicIpOnLaunch: true + AvailabilityZone: !Select [ 1, !GetAZs '' ] + + PublicSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet2 + RouteTableId: !Ref PublicRouteTable + + PrivateSubnet: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Ref PrivateSubnetCidr + AvailabilityZone: !Select [ 0, !GetAZs '' ] + + NatGatewayEIP: + Type: AWS::EC2::EIP + Properties: + Domain: vpc + NatGateway: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGatewayEIP.AllocationId + SubnetId: !Ref PublicSubnet + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + PublicRoute: + Type: AWS::EC2::Route + DependsOn: AttachGateway + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + PublicSubnetRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet + RouteTableId: !Ref PublicRouteTable + + PrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + PrivateRoute: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway + PrivateSubnetRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet + RouteTableId: !Ref PrivateRouteTable + + # SECURITY GROUPS + AlbSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allow restricted inbound traffic to ALB + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: !Ref AllowedSourceIp + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !Ref AllowedSourceIp + + FargateSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allow traffic from ALB to Fargate + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref AlbSecurityGroup + EfsSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allow NFS traffic from Fargate to EFS + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref FargateSecurityGroup + + # 3. STORAGE (EFS for SQLite .od directory) + FileSystem: + Type: AWS::EFS::FileSystem + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + Encrypted: true + PerformanceMode: generalPurpose + + MountTarget: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet + SecurityGroups: + - !Ref EfsSecurityGroup + + EfsAccessPoint: + Type: AWS::EFS::AccessPoint + Properties: + FileSystemId: !Ref FileSystem + PosixUser: + Uid: "1001" + Gid: "1001" + RootDirectory: + Path: "/od-data" + CreationInfo: + OwnerUid: "1001" + OwnerGid: "1001" + Permissions: "0755" + + # LOAD BALANCER + LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Subnets: + - !Ref PublicSubnet + - !Ref PublicSubnet2 + SecurityGroups: + - !Ref AlbSecurityGroup + Scheme: internet-facing + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + VpcId: !Ref VPC + Port: 8080 + Protocol: HTTP + TargetType: ip + HealthCheckPath: /nginx-health + + Listener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref LoadBalancer + Port: 80 + Protocol: HTTP + DefaultActions: !If + - UseCustomDomain + - - Type: redirect + RedirectConfig: + Protocol: HTTPS + Port: "443" + StatusCode: HTTP_301 + - - Type: forward + TargetGroupArn: !Ref TargetGroup + + HttpsListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: UseCustomDomain + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref TargetGroup + LoadBalancerArn: !Ref LoadBalancer + Port: 443 + Protocol: HTTPS + Certificates: + - CertificateArn: !Ref AcmCertificateArn + + # COMPUTE (ECS Fargate) + EcsCluster: + Type: AWS::ECS::Cluster + + TaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: ReadSecrets + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref ApiTokenSecret + - PolicyName: CloudWatchLogs + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt LogGroup.Arn + + TaskRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: EfsMountAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - elasticfilesystem:ClientMount + - elasticfilesystem:ClientWrite + Resource: !GetAtt FileSystem.Arn + Condition: + StringEquals: + elasticfilesystem:AccessPointArn: !GetAtt EfsAccessPoint.Arn + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: opendesign-app + RequiresCompatibilities: + - FARGATE + NetworkMode: awsvpc + Cpu: !Ref FargateCpu + Memory: !Ref FargateMemory + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !GetAtt TaskRole.Arn + RuntimePlatform: + CpuArchitecture: ARM64 + OperatingSystemFamily: LINUX + Volumes: + - Name: efs-storage + EFSVolumeConfiguration: + FilesystemId: !Ref FileSystem + TransitEncryption: ENABLED + AuthorizationConfig: + AccessPointId: !Ref EfsAccessPoint + IAM: ENABLED + ContainerDefinitions: + - Name: app + Image: !Ref DockerImage + Environment: + - Name: OD_ALLOWED_ORIGINS + Value: !If + - UseCustomDomain + - !Sub 'https://${CustomDomainName}' + - !Sub 'http://${LoadBalancer.DNSName}' + - Name: OD_BIND_HOST + Value: "127.0.0.1" + - Name: OD_PORT + Value: "7456" + Secrets: + - Name: OD_API_TOKEN + ValueFrom: !Ref ApiTokenSecret + MountPoints: + - SourceVolume: efs-storage + ContainerPath: !Ref AppStoragePath + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + - Name: auth-proxy + Image: nginxinc/nginx-unprivileged:1.25-alpine-slim + PortMappings: + - ContainerPort: 8080 + Environment: + - Name: PROXY_PORT + Value: "8080" + - Name: OD_BIND_HOST + Value: "127.0.0.1" + - Name: OD_WEB_PORT + Value: "7456" + Secrets: + - Name: PROXY_API_TOKEN + ValueFrom: !Ref ApiTokenSecret + EntryPoint: + - /bin/sh + - -c + Command: + - | + cat << 'EOF' > /tmp/default.conf.template + server { + listen ${PROXY_PORT}; + + location /nginx-health { + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + location / { + proxy_pass http://${OD_BIND_HOST}:${OD_WEB_PORT}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Host $http_x_forwarded_host; + } + + location /api/ { + proxy_pass http://${OD_BIND_HOST}:${OD_WEB_PORT}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Host $http_x_forwarded_host; + + proxy_set_header Authorization "Bearer ${PROXY_API_TOKEN}"; + + proxy_buffering off; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + proxy_set_header Connection ''; + http2_push_preload on; + } + } + EOF + envsubst '$$PROXY_PORT $$OD_BIND_HOST $$OD_WEB_PORT $$PROXY_API_TOKEN' < /tmp/default.conf.template > /etc/nginx/conf.d/default.conf + exec nginx -g "daemon off;" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs-proxy + EcsService: + Type: AWS::ECS::Service + DependsOn: Listener + Properties: + Cluster: !Ref EcsCluster + TaskDefinition: !Ref TaskDefinition + LaunchType: FARGATE + DesiredCount: 1 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: DISABLED + Subnets: + - !Ref PrivateSubnet + SecurityGroups: + - !Ref FargateSecurityGroup + LoadBalancers: + - ContainerName: auth-proxy + ContainerPort: 8080 + TargetGroupArn: !Ref TargetGroup + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /ecs/opendesign-app + RetentionInDays: 7 + + # SECRETS (Secrets Manager) + ApiTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Description: "API Token for Open Design" + SecretString: !Ref ApiToken + +Outputs: + AppUrl: + Description: URL of the Application Load Balancer + Value: !Sub 'http://${LoadBalancer.DNSName}' \ No newline at end of file From 7fc505674416816646023a58b798b70a6f3f7b2a Mon Sep 17 00:00:00 2001 From: shaarron Date: Tue, 26 May 2026 23:17:51 +0300 Subject: [PATCH 02/11] correct dynamic AppUrl logic and activate hardcoded port parameter --- deploy/aws/README.md | 2 +- deploy/aws/template.yaml | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 694b7493e..09b782a1b 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -41,7 +41,7 @@ When deploying the CloudFormation stack, you can customize the following paramet | `FargateMemory` | Fargate task memory in MiB. | `1024` | | `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | | `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | -| `ContainerPort` | The internal port the Open Design container listens on. | `7456` | +| `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. | `8080` | | `AppStoragePath` | The container path where the `.od` SQLite directory is mounted via EFS. | `/app/.od` | ## Deployment diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 21ca00dd0..798a90b7e 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -16,9 +16,10 @@ Parameters: AllowedSourceIp: Type: String Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' - ContainerPort: + ProxyPort: Type: Number - Default: 7456 + Default: 8080 + Description: 'The dynamic port used by the Nginx proxy and exposed to the Load Balancer.' AppStoragePath: Type: String Description: 'Container path where the .od SQLite directory is stored.' @@ -168,8 +169,8 @@ Resources: VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp - FromPort: 8080 - ToPort: 8080 + FromPort: !Ref ProxyPort + ToPort: !Ref ProxyPort SourceSecurityGroupId: !Ref AlbSecurityGroup EfsSecurityGroup: Type: AWS::EC2::SecurityGroup @@ -228,7 +229,7 @@ Resources: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: VpcId: !Ref VPC - Port: 8080 + Port: !Ref ProxyPort Protocol: HTTP TargetType: ip HealthCheckPath: /nginx-health @@ -369,10 +370,10 @@ Resources: - Name: auth-proxy Image: nginxinc/nginx-unprivileged:1.25-alpine-slim PortMappings: - - ContainerPort: 8080 + - ContainerPort: !Ref ProxyPort Environment: - Name: PROXY_PORT - Value: "8080" + Value: !Ref ProxyPort - Name: OD_BIND_HOST Value: "127.0.0.1" - Name: OD_WEB_PORT @@ -446,7 +447,7 @@ Resources: - !Ref FargateSecurityGroup LoadBalancers: - ContainerName: auth-proxy - ContainerPort: 8080 + ContainerPort: !Ref ProxyPort TargetGroupArn: !Ref TargetGroup LogGroup: @@ -464,5 +465,8 @@ Resources: Outputs: AppUrl: - Description: URL of the Application Load Balancer - Value: !Sub 'http://${LoadBalancer.DNSName}' \ No newline at end of file + Description: 'URL of the Load Balancer/Custom Domain' + Value: !If + - UseCustomDomain + - !Sub 'https://${CustomDomainName}' + - !Sub 'http://${LoadBalancer.DNSName}' \ No newline at end of file From 481814864105bfd59cb08cd540c2f374960f4b1c Mon Sep 17 00:00:00 2001 From: shaarron Date: Wed, 27 May 2026 13:41:23 +0300 Subject: [PATCH 03/11] resolve proxy port limit and log group resource to use stack name --- deploy/aws/README.md | 2 +- deploy/aws/template.yaml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 09b782a1b..1665a6ffd 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -41,7 +41,7 @@ When deploying the CloudFormation stack, you can customize the following paramet | `FargateMemory` | Fargate task memory in MiB. | `1024` | | `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | | `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | -| `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. | `8080` | +| `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. Must be >= 1024 (unprivileged container). | `8080` | | `AppStoragePath` | The container path where the `.od` SQLite directory is mounted via EFS. | `/app/.od` | ## Deployment diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 798a90b7e..95af8f5c3 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -19,7 +19,9 @@ Parameters: ProxyPort: Type: Number Default: 8080 - Description: 'The dynamic port used by the Nginx proxy and exposed to the Load Balancer.' + MinValue: 1024 + MaxValue: 65535 + Description: 'The dynamic port used by the Nginx proxy (must be >= 1024 for unprivileged container).' AppStoragePath: Type: String Description: 'Container path where the .od SQLite directory is stored.' @@ -453,7 +455,7 @@ Resources: LogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: /ecs/opendesign-app + LogGroupName: !Sub '/ecs/${AWS::StackName}/opendesign-app' RetentionInDays: 7 # SECRETS (Secrets Manager) From d5c1c9a2da74312e5a54b67e64bc230abdabf300 Mon Sep 17 00:00:00 2001 From: shaarron Date: Wed, 27 May 2026 14:42:59 +0300 Subject: [PATCH 04/11] fixes following a review --- deploy/aws/README.md | 2 +- deploy/aws/template.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 1665a6ffd..7d7527a15 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -31,12 +31,12 @@ When deploying the CloudFormation stack, you can customize the following paramet | :--- | :--- | :--- | | `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | | `ApiToken` | **(Required)** The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager. | | -| `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | | `DockerImage` | The Open Design Docker image to deploy. | `vanjayak/open-design:latest` | | `VpcCidr` | The CIDR block for the VPC. | `10.42.0.0/16` | | `PublicSubnet1Cidr` | The CIDR block for Public Subnet 1 (AZ1). | `10.42.1.0/24` | | `PublicSubnet2Cidr` | The CIDR block for Public Subnet 2 (AZ2). | `10.42.3.0/24` | | `PrivateSubnetCidr` | The CIDR block for the Private Subnet. | `10.42.2.0/24` | +| `PrivateSubnet2Cidr` | The CIDR block for Private Subnet 2 (AZ2). | `10.42.4.0/24` | | `FargateCpu` | Fargate task CPU units (1024 = 1 vCPU). Allowed values: 256, 512, 1024, 2048, 4096. | `512` | | `FargateMemory` | Fargate task memory in MiB. | `1024` | | `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 95af8f5c3..19f90e663 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -46,6 +46,10 @@ Parameters: Type: String Default: '10.42.2.0/24' Description: 'The CIDR block for the Private Subnet' + PrivateSubnet2Cidr: + Type: String + Default: '10.42.4.0/24' + Description: 'The CIDR block for Private Subnet 2 (AZ2)' FargateCpu: Type: String Default: '512' @@ -59,6 +63,13 @@ Parameters: Conditions: UseCustomDomain: !Not [!Equals [!Ref CustomDomainName, '']] +Rules: + RequireCertificateWithDomain: + RuleCondition: !Not [!Equals [!Ref CustomDomainName, '']] + Assertions: + - Assert: !Not [!Equals [!Ref AcmCertificateArn, '']] + AssertDescription: 'You must provide an AcmCertificateArn when specifying a CustomDomainName.' + Resources: # NETWORKING (VPC, Subnets, NAT) VPC: @@ -105,6 +116,13 @@ Resources: CidrBlock: !Ref PrivateSubnetCidr AvailabilityZone: !Select [ 0, !GetAZs '' ] + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: !Ref PrivateSubnet2Cidr + AvailabilityZone: !Select [ 1, !GetAZs '' ] + NatGatewayEIP: Type: AWS::EC2::EIP Properties: @@ -148,6 +166,12 @@ Resources: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable + PrivateSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet2 + RouteTableId: !Ref PrivateRouteTable + # SECURITY GROUPS AlbSecurityGroup: Type: AWS::EC2::SecurityGroup @@ -202,6 +226,14 @@ Resources: SecurityGroups: - !Ref EfsSecurityGroup + MountTarget2: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet2 + SecurityGroups: + - !Ref EfsSecurityGroup + EfsAccessPoint: Type: AWS::EFS::AccessPoint Properties: @@ -445,6 +477,7 @@ Resources: AssignPublicIp: DISABLED Subnets: - !Ref PrivateSubnet + - !Ref PrivateSubnet2 SecurityGroups: - !Ref FargateSecurityGroup LoadBalancers: From 56ba01af48a378f132e4d035066b5c84b248724e Mon Sep 17 00:00:00 2001 From: shaarron Date: Thu, 28 May 2026 09:16:06 +0300 Subject: [PATCH 05/11] address PR feedback by enforcing deployment parameter validations, implementing true multi-AZ backend routing, and correcting proxy configuration syntax --- deploy/aws/README.md | 5 ++-- deploy/aws/template.yaml | 60 ++++++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 7d7527a15..14c9600ec 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -6,7 +6,7 @@ This directory contains an AWS CloudFormation template (`template.yaml`) to depl The template provisions a robust, highly-available, and secure architecture for Open Design: -* **Networking:** A new Virtual Private Cloud (VPC) spanning two Availability Zones, with both Public and Private subnets. A NAT Gateway allows outbound internet access. +* **Networking:** A new Virtual Private Cloud (VPC) spanning two Availability Zones, with both Public and Private subnets. Two independent NAT Gateways (one in each AZ) provide highly available outbound internet access. * **Load Balancing:** An internet-facing Application Load Balancer (ALB) routes incoming traffic. It optionally supports HTTPS if a custom domain and ACM certificate are provided. * **Compute:** AWS ECS running on serverless Fargate instances in the private subnets. The task definition includes: * The **Open Design** app container. @@ -37,8 +37,7 @@ When deploying the CloudFormation stack, you can customize the following paramet | `PublicSubnet2Cidr` | The CIDR block for Public Subnet 2 (AZ2). | `10.42.3.0/24` | | `PrivateSubnetCidr` | The CIDR block for the Private Subnet. | `10.42.2.0/24` | | `PrivateSubnet2Cidr` | The CIDR block for Private Subnet 2 (AZ2). | `10.42.4.0/24` | -| `FargateCpu` | Fargate task CPU units (1024 = 1 vCPU). Allowed values: 256, 512, 1024, 2048, 4096. | `512` | -| `FargateMemory` | Fargate task memory in MiB. | `1024` | +| `TaskSize` | The compute size for the Open Design application. Allowed values: `small` (256 CPU, 1024 MiB), `medium` (512 CPU, 2048 MiB), `large` (1024 CPU, 4096 MiB). | `small` | | `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | | `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | | `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. Must be >= 1024 (unprivileged container). | `8080` | diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 19f90e663..a82a951d6 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -50,15 +50,23 @@ Parameters: Type: String Default: '10.42.4.0/24' Description: 'The CIDR block for Private Subnet 2 (AZ2)' - FargateCpu: + TaskSize: Type: String - Default: '512' - AllowedValues: ['256', '512', '1024', '2048', '4096'] - Description: 'Fargate task CPU units (1024 = 1 vCPU)' - FargateMemory: - Type: String - Default: '1024' - Description: 'Fargate task memory in MiB' + Default: small + AllowedValues: [small, medium, large] + Description: 'The compute size for the Open Design application.' + +Mappings: + TaskSizes: + small: + Cpu: '256' + Memory: '1024' + medium: + Cpu: '512' + Memory: '2048' + large: + Cpu: '1024' + Memory: '4096' Conditions: UseCustomDomain: !Not [!Equals [!Ref CustomDomainName, '']] @@ -133,6 +141,16 @@ Resources: AllocationId: !GetAtt NatGatewayEIP.AllocationId SubnetId: !Ref PublicSubnet + NatGateway2EIP: + Type: AWS::EC2::EIP + Properties: + Domain: vpc + NatGateway2: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGateway2EIP.AllocationId + SubnetId: !Ref PublicSubnet2 + PublicRouteTable: Type: AWS::EC2::RouteTable Properties: @@ -166,11 +184,22 @@ Resources: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable + PrivateRouteTable2: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + PrivateRoute2: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable2 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway2 + PrivateSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet2 - RouteTableId: !Ref PrivateRouteTable + RouteTableId: !Ref PrivateRouteTable2 # SECURITY GROUPS AlbSecurityGroup: @@ -266,7 +295,7 @@ Resources: Port: !Ref ProxyPort Protocol: HTTP TargetType: ip - HealthCheckPath: /nginx-health + HealthCheckPath: /api/health Listener: Type: AWS::ElasticLoadBalancingV2::Listener @@ -361,8 +390,8 @@ Resources: RequiresCompatibilities: - FARGATE NetworkMode: awsvpc - Cpu: !Ref FargateCpu - Memory: !Ref FargateMemory + Cpu: !FindInMap [TaskSizes, !Ref TaskSize, Cpu] + Memory: !FindInMap [TaskSizes, !Ref TaskSize, Memory] ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn TaskRoleArn: !GetAtt TaskRole.Arn RuntimePlatform: @@ -424,11 +453,6 @@ Resources: server { listen ${PROXY_PORT}; - location /nginx-health { - return 200 "healthy\n"; - add_header Content-Type text/plain; - } - location / { proxy_pass http://${OD_BIND_HOST}:${OD_WEB_PORT}; proxy_set_header Host $host; @@ -456,7 +480,7 @@ Resources: } } EOF - envsubst '$$PROXY_PORT $$OD_BIND_HOST $$OD_WEB_PORT $$PROXY_API_TOKEN' < /tmp/default.conf.template > /etc/nginx/conf.d/default.conf + envsubst '$PROXY_PORT $OD_BIND_HOST $OD_WEB_PORT $PROXY_API_TOKEN' < /tmp/default.conf.template > /etc/nginx/conf.d/default.conf exec nginx -g "daemon off;" LogConfiguration: LogDriver: awslogs From b9a5d9202438fdb87103231cbdd1a7eae9b24011 Mon Sep 17 00:00:00 2001 From: shaarron Date: Thu, 28 May 2026 14:26:22 +0300 Subject: [PATCH 06/11] correct rules and validations --- deploy/aws/README.md | 2 +- deploy/aws/template.yaml | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 14c9600ec..f1867e16d 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -31,7 +31,7 @@ When deploying the CloudFormation stack, you can customize the following paramet | :--- | :--- | :--- | | `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | | `ApiToken` | **(Required)** The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager. | | -| `DockerImage` | The Open Design Docker image to deploy. | `vanjayak/open-design:latest` | +| `DockerImage` | **(Required)** The full repository URI and tag for the Open Design Docker image. You must provide an explicit image as the public Docker Hub baseline is currently unmaintained. | *None* | | `VpcCidr` | The CIDR block for the VPC. | `10.42.0.0/16` | | `PublicSubnet1Cidr` | The CIDR block for Public Subnet 1 (AZ1). | `10.42.1.0/24` | | `PublicSubnet2Cidr` | The CIDR block for Public Subnet 2 (AZ2). | `10.42.3.0/24` | diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index a82a951d6..96a79c7bc 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -12,9 +12,13 @@ Parameters: Default: '' DockerImage: Type: String - Default: 'vanjayak/open-design:latest' + MinLength: 1 + Description: 'REQUIRED: The full repository URI and tag for the Open Design Docker image (e.g., your-registry/open-design:latest). You must provide an explicit image as the public Docker Hub baseline is currently unmaintained.' AllowedSourceIp: Type: String + AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]{1,2})$' + ConstraintDescription: 'Must be a valid IPv4 CIDR range (e.g., 192.168.1.1/32).' + MinLength: 1 Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' ProxyPort: Type: Number @@ -29,6 +33,7 @@ Parameters: ApiToken: Type: String NoEcho: true + MinLength: 1 Description: 'REQUIRED: The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager.' VpcCidr: Type: String @@ -77,6 +82,10 @@ Rules: Assertions: - Assert: !Not [!Equals [!Ref AcmCertificateArn, '']] AssertDescription: 'You must provide an AcmCertificateArn when specifying a CustomDomainName.' + PreventPublicExposure: + Assertions: + - Assert: !Not [!Equals [!Ref AllowedSourceIp, '0.0.0.0/0']] + AssertDescription: 'Public exposure (0.0.0.0/0) is strictly forbidden by this baseline. Please provide a specific private IP or VPN CIDR block.' Resources: # NETWORKING (VPC, Subnets, NAT) From f21acf61f2ba4297b5e241cad4f4e062f5c753c6 Mon Sep 17 00:00:00 2001 From: shaarron Date: Thu, 28 May 2026 16:29:30 +0300 Subject: [PATCH 07/11] fix readme and regex validation --- deploy/aws/README.md | 7 ++++--- deploy/aws/template.yaml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index f1867e16d..a22786a9a 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -55,14 +55,14 @@ You can deploy this stack via the AWS Management Console or the AWS CLI. 4. Under **Specify template**, select **Upload a template file**, click **Choose file**, and select the `template.yaml` file from this directory. 5. Click **Next**. 6. Enter a **Stack name** (e.g., `open-design-stack`). -7. Fill in the **Parameters** according to your requirements. Note that `ApiToken` and `AllowedSourceIp` are required. +7. Fill in the **Parameters** according to your requirements. Note that `ApiToken`, `AllowedSourceIp`, and `DockerImage` are required. 8. Click **Next**. Configure any stack options if desired, then click **Next** again. 9. Scroll to the bottom of the review page, check the box that says **I acknowledge that AWS CloudFormation might create IAM resources**, and click **Submit**. ### Using AWS CLI 1. Open your terminal and navigate to this directory. -2. Run the `aws cloudformation deploy` command, passing in the required parameters: +2. Run the `aws cloudformation deploy` command, passing in the required parameters (`ApiToken`, `AllowedSourceIp`, and `DockerImage`): ```bash aws cloudformation deploy \ @@ -71,7 +71,8 @@ aws cloudformation deploy \ --capabilities CAPABILITY_IAM \ --parameter-overrides \ ApiToken="YOUR_SECURE_API_TOKEN" \ - AllowedSourceIp="YOUR_IP_ADDRESS/32" + AllowedSourceIp="YOUR_IP_ADDRESS/32" \ + DockerImage="your-registry/open-design:latest" ``` *Note: If you want to use a custom domain with HTTPS, include the `CustomDomainName` and `AcmCertificateArn` parameters in the `--parameter-overrides` list.* diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 96a79c7bc..a10a6a05d 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -16,7 +16,7 @@ Parameters: Description: 'REQUIRED: The full repository URI and tag for the Open Design Docker image (e.g., your-registry/open-design:latest). You must provide an explicit image as the public Docker Hub baseline is currently unmaintained.' AllowedSourceIp: Type: String - AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]{1,2})$' + AllowedPattern: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/([0-2]?[0-9]|3[0-2])$' ConstraintDescription: 'Must be a valid IPv4 CIDR range (e.g., 192.168.1.1/32).' MinLength: 1 Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' From 0a48ac575493ad4916c0116cf14f520b7fc055ec Mon Sep 17 00:00:00 2001 From: shaarron Date: Thu, 28 May 2026 16:54:27 +0300 Subject: [PATCH 08/11] restrict allowed source IP ranges to private CIDR masks and clarify single-instance compute constraints in documentation --- deploy/aws/README.md | 4 ++-- deploy/aws/template.yaml | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index a22786a9a..9742ff042 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -4,11 +4,11 @@ This directory contains an AWS CloudFormation template (`template.yaml`) to depl ## Architecture Overview -The template provisions a robust, highly-available, and secure architecture for Open Design: +The template provisions a robust, fault-tolerant, and secure architecture for Open Design: * **Networking:** A new Virtual Private Cloud (VPC) spanning two Availability Zones, with both Public and Private subnets. Two independent NAT Gateways (one in each AZ) provide highly available outbound internet access. * **Load Balancing:** An internet-facing Application Load Balancer (ALB) routes incoming traffic. It optionally supports HTTPS if a custom domain and ACM certificate are provided. -* **Compute:** AWS ECS running on serverless Fargate instances in the private subnets. The task definition includes: +* **Compute:** AWS ECS running on serverless Fargate instances in the private subnets. To protect the file-based SQLite database from concurrent network write corruption, the service hard-codes a single-instance baseline (DesiredCount: 1). However, it leverages the multi-AZ networking primitives for Active-Passive fault tolerance: if a task or zone fails, ECS automatically reschedules the container in the healthy AZ. The task definition includes: * The **Open Design** app container. * An **Nginx Auth Proxy** sidecar container that securely attaches the Open Design API Token to incoming `/api/` requests. * **Storage:** Amazon Elastic File System (EFS) is mounted to the Fargate containers to durably store the Open Design `.od` SQLite database and file artifacts. It is configured with deletion protection (`Retain`) to prevent accidental data loss. diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index a10a6a05d..70ad3efa3 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -16,8 +16,8 @@ Parameters: Description: 'REQUIRED: The full repository URI and tag for the Open Design Docker image (e.g., your-registry/open-design:latest). You must provide an explicit image as the public Docker Hub baseline is currently unmaintained.' AllowedSourceIp: Type: String - AllowedPattern: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/([0-2]?[0-9]|3[0-2])$' - ConstraintDescription: 'Must be a valid IPv4 CIDR range (e.g., 192.168.1.1/32).' + AllowedPattern: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/(1[6-9]|2[0-9]|3[0-2])$' + ConstraintDescription: 'Must be a valid IPv4 CIDR range with a subnet mask between /16 and /32 (e.g., 192.168.1.0/24 or 10.0.0.1/32).' MinLength: 1 Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' ProxyPort: @@ -82,10 +82,7 @@ Rules: Assertions: - Assert: !Not [!Equals [!Ref AcmCertificateArn, '']] AssertDescription: 'You must provide an AcmCertificateArn when specifying a CustomDomainName.' - PreventPublicExposure: - Assertions: - - Assert: !Not [!Equals [!Ref AllowedSourceIp, '0.0.0.0/0']] - AssertDescription: 'Public exposure (0.0.0.0/0) is strictly forbidden by this baseline. Please provide a specific private IP or VPN CIDR block.' + Resources: # NETWORKING (VPC, Subnets, NAT) From 8c8541ec34da0937cd93dfc5709cfd6c06108f17 Mon Sep 17 00:00:00 2001 From: shaarron Date: Fri, 29 May 2026 14:29:50 +0300 Subject: [PATCH 09/11] addressing review points --- deploy/aws/README.md | 7 ++++--- deploy/aws/template.yaml | 24 +++++++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 9742ff042..2a13ea37d 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -14,7 +14,7 @@ The template provisions a robust, fault-tolerant, and secure architecture for Op * **Storage:** Amazon Elastic File System (EFS) is mounted to the Fargate containers to durably store the Open Design `.od` SQLite database and file artifacts. It is configured with deletion protection (`Retain`) to prevent accidental data loss. * **Security:** * **Secrets Manager:** Securely stores the Open Design API Token, preventing it from being exposed in plain text. - * **Security Groups:** Restrict traffic flow (e.g., the ALB only accepts traffic from a specified IP range; Fargate only accepts traffic from the ALB; EFS only accepts traffic from Fargate). + * **Security Groups:** Restrict traffic flow. The ALB requires an explicitly configured CIDR — ensure this is your VPN or corporate range to avoid unintended public exposure. Fargate only accepts traffic from the ALB; EFS only accepts traffic from Fargate. * **Logging:** Amazon CloudWatch Log Group captures container logs for easy debugging. ## Prerequisites @@ -29,15 +29,16 @@ When deploying the CloudFormation stack, you can customize the following paramet | Parameter | Description | Default | | :--- | :--- | :--- | -| `AllowedSourceIp` | **(Required)** The CIDR block allowed to access the Load Balancer (e.g., your office VPN IP). Must end in `/32` for a single IP. | *None* | +| `AllowedSourceIp` | **(Required)** The specific IPv4 CIDR block allowlisted to access the Load Balancer. The ALB requires an explicitly configured CIDR — ensure this is your VPN or corporate range to avoid unintended public exposure. Accepts any valid IPv4 range with a subnet mask between /16 and /32. | *None* | | `ApiToken` | **(Required)** The secure API token used to authenticate requests to the Open Design backend. It is stored securely in AWS Secrets Manager. | | | `DockerImage` | **(Required)** The full repository URI and tag for the Open Design Docker image. You must provide an explicit image as the public Docker Hub baseline is currently unmaintained. | *None* | | `VpcCidr` | The CIDR block for the VPC. | `10.42.0.0/16` | | `PublicSubnet1Cidr` | The CIDR block for Public Subnet 1 (AZ1). | `10.42.1.0/24` | | `PublicSubnet2Cidr` | The CIDR block for Public Subnet 2 (AZ2). | `10.42.3.0/24` | -| `PrivateSubnetCidr` | The CIDR block for the Private Subnet. | `10.42.2.0/24` | +| `PrivateSubnet1Cidr` | The CIDR block for Private Subnet 1 (AZ1). | `10.42.2.0/24` | | `PrivateSubnet2Cidr` | The CIDR block for Private Subnet 2 (AZ2). | `10.42.4.0/24` | | `TaskSize` | The compute size for the Open Design application. Allowed values: `small` (256 CPU, 1024 MiB), `medium` (512 CPU, 2048 MiB), `large` (1024 CPU, 4096 MiB). | `small` | +| `TaskCpuArchitecture` | The CPU architecture for the ECS task. Must match the architecture of your Docker image. Allowed values (available as a dropdown): `X86_64`, `ARM64`. | `X86_64` | | `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | | `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | | `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. Must be >= 1024 (unprivileged container). | `8080` | diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index 70ad3efa3..b5ec9084e 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -19,7 +19,7 @@ Parameters: AllowedPattern: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/(1[6-9]|2[0-9]|3[0-2])$' ConstraintDescription: 'Must be a valid IPv4 CIDR range with a subnet mask between /16 and /32 (e.g., 192.168.1.0/24 or 10.0.0.1/32).' MinLength: 1 - Description: 'REQUIRED: The CIDR block allowed to access the ALB (e.g., your VPN or office IP. Must end in /32 for a single IP).' + Description: 'REQUIRED: The specific IPv4 CIDR block allowlisted to access the ALB. Ensure this is your VPN or corporate range to avoid unintended public exposure. Accepts any valid IPv4 range with a subnet mask between /16 and /32.' ProxyPort: Type: Number Default: 8080 @@ -47,10 +47,10 @@ Parameters: Type: String Default: '10.42.3.0/24' Description: 'The CIDR block for Public Subnet 2 (AZ2)' - PrivateSubnetCidr: + PrivateSubnet1Cidr: Type: String Default: '10.42.2.0/24' - Description: 'The CIDR block for the Private Subnet' + Description: 'The CIDR block for Private Subnet 1 (AZ1)' PrivateSubnet2Cidr: Type: String Default: '10.42.4.0/24' @@ -60,6 +60,11 @@ Parameters: Default: small AllowedValues: [small, medium, large] Description: 'The compute size for the Open Design application.' + TaskCpuArchitecture: + Type: String + Default: X86_64 + AllowedValues: [ARM64, X86_64] + Description: 'The CPU architecture for the Fargate task. Must match how your DockerImage was built (e.g., use X86_64 for standard linux/amd64 images).' Mappings: TaskSizes: @@ -123,11 +128,11 @@ Resources: SubnetId: !Ref PublicSubnet2 RouteTableId: !Ref PublicRouteTable - PrivateSubnet: + PrivateSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC - CidrBlock: !Ref PrivateSubnetCidr + CidrBlock: !Ref PrivateSubnet1Cidr AvailabilityZone: !Select [ 0, !GetAZs '' ] PrivateSubnet2: @@ -187,7 +192,7 @@ Resources: PrivateSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: - SubnetId: !Ref PrivateSubnet + SubnetId: !Ref PrivateSubnet1 RouteTableId: !Ref PrivateRouteTable PrivateRouteTable2: @@ -257,7 +262,7 @@ Resources: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref FileSystem - SubnetId: !Ref PrivateSubnet + SubnetId: !Ref PrivateSubnet1 SecurityGroups: - !Ref EfsSecurityGroup @@ -401,7 +406,7 @@ Resources: ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn TaskRoleArn: !GetAtt TaskRole.Arn RuntimePlatform: - CpuArchitecture: ARM64 + CpuArchitecture: !Ref TaskCpuArchitecture OperatingSystemFamily: LINUX Volumes: - Name: efs-storage @@ -502,11 +507,12 @@ Resources: TaskDefinition: !Ref TaskDefinition LaunchType: FARGATE DesiredCount: 1 + HealthCheckGracePeriodSeconds: 60 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED Subnets: - - !Ref PrivateSubnet + - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 SecurityGroups: - !Ref FargateSecurityGroup From 920d71516a8b43dbe91a73451b9a68c17d22bcc6 Mon Sep 17 00:00:00 2001 From: shaarron Date: Fri, 29 May 2026 14:44:35 +0300 Subject: [PATCH 10/11] README update --- deploy/aws/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/deploy/aws/README.md b/deploy/aws/README.md index 2a13ea37d..ec0d4da36 100644 --- a/deploy/aws/README.md +++ b/deploy/aws/README.md @@ -39,7 +39,7 @@ When deploying the CloudFormation stack, you can customize the following paramet | `PrivateSubnet2Cidr` | The CIDR block for Private Subnet 2 (AZ2). | `10.42.4.0/24` | | `TaskSize` | The compute size for the Open Design application. Allowed values: `small` (256 CPU, 1024 MiB), `medium` (512 CPU, 2048 MiB), `large` (1024 CPU, 4096 MiB). | `small` | | `TaskCpuArchitecture` | The CPU architecture for the ECS task. Must match the architecture of your Docker image. Allowed values (available as a dropdown): `X86_64`, `ARM64`. | `X86_64` | -| `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If blank, the default ALB DNS name is used over HTTP. | *None* | +| `CustomDomainName` | *(Optional)* Your custom domain name (e.g., `design.yourcompany.com`). If provided, you must manually create a DNS CNAME/Alias record pointing to the ALB after deployment. If blank, the default ALB DNS name is used over HTTP. | *None* | | `AcmCertificateArn` | *(Optional)* The ARN of your AWS Certificate Manager (ACM) certificate. **Required** if `CustomDomainName` is provided. | *None* | | `ProxyPort` | The dynamic port used by the Nginx proxy and exposed to the Load Balancer. Must be >= 1024 (unprivileged container). | `8080` | | `AppStoragePath` | The container path where the `.od` SQLite directory is mounted via EFS. | `/app/.od` | @@ -80,6 +80,10 @@ aws cloudformation deploy \ ## Accessing the Application -Once the CloudFormation stack creation is complete, go to the **Outputs** tab of the stack in the AWS CloudFormation Console. +Once the CloudFormation stack creation is complete, go to the **Outputs** tab of the stack in the AWS CloudFormation Console to find the `AlbDnsName` and `AppUrl`. -You will find the `AppUrl` output, which contains the HTTP link to the Application Load Balancer (or your custom domain if configured). Use this URL to access Open Design. +**If you did NOT use a custom domain:** +Access Open Design directly using the HTTP URL provided in `AppUrl`. + +**If you used a Custom Domain (HTTPS):** +You must create a DNS record to route traffic to your new load balancer. Go to your DNS provider (e.g., AWS Route53, Cloudflare) and create a CNAME or Alias (A) record that points your `CustomDomainName` to the `AlbDnsName` output value. Once DNS propagates, you can access Open Design securely via your custom HTTPS domain. From fd641590115a73c1fb5af4d904a581a0c0524497 Mon Sep 17 00:00:00 2001 From: shaarron Date: Fri, 29 May 2026 14:54:18 +0300 Subject: [PATCH 11/11] fixed missing output --- deploy/aws/template.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy/aws/template.yaml b/deploy/aws/template.yaml index b5ec9084e..e59bae160 100644 --- a/deploy/aws/template.yaml +++ b/deploy/aws/template.yaml @@ -540,4 +540,7 @@ Outputs: Value: !If - UseCustomDomain - !Sub 'https://${CustomDomainName}' - - !Sub 'http://${LoadBalancer.DNSName}' \ No newline at end of file + - !Sub 'http://${LoadBalancer.DNSName}' + AlbDnsName: + Description: 'The raw DNS name of the Application Load Balancer (use as the target for custom domain CNAME/Alias records).' + Value: !GetAtt LoadBalancer.DNSName \ No newline at end of file