Description: Code Ocean VPC deployment stack
Metadata:
  CodeOcean::VersionInfo:
    Version: v4.4.3
    Commit: 2b5b83e9e58f23876127fa1533bf0095d164c74e
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Domain Configuration
        Parameters:
          - pDnsName
          - pDnsRootDomain
          - pHostedZoneId
      - Label:
          default: TLS Certificate Configuration
        Parameters:
          - pCertificateArn
          - pPrivateCA
      - Label:
          default: VPC Configuration (Use Existing)
        Parameters:
          - pVpcId
          - pAvailabilityZone1
          - pAvailabilityZone2
          - pPrivateSubnet1Id
          - pPrivateSubnet2Id
          - pPublicSubnet1Id
          - pPublicSubnet2Id
      - Label:
          default: VPC Configuration (Create New)
        Parameters:
          - pNewVpcAvailabilityZone1
          - pNewVpcAvailabilityZone2
          - pVpcCIDR
          - pPrivateSubnet1CIDR
          - pPrivateSubnet2CIDR
          - pPublicSubnet1CIDR
          - pPublicSubnet2CIDR
          - pVpcFlowLogs
      - Label:
          default: Custom Upstream Docker Registry Configuration
        Parameters:
          - pCustomUpstreamDockerRegistryEndpoint
          - pCustomUpstreamDockerRegistrySecret
      - Label:
          default: Deployment Configuration
        Parameters:
          - pSystemCapacityTier
          - pDeploymentType
      - Label:
          default: Custom AMI Configuration
        Parameters:
          - pCustomAmi
      - Label:
          default: Worker Configuration
        Parameters:
          - pWorkerInstanceType
          - pWorkersAsgMaxSize
          - pMinWorkersAvailable
          - pGPUWorkerInstanceType
          - pGpuWorkersAsgMaxSize
          - pMinGpuWorkersAvailable
          - pAutoScalingIdleTimeout
      - Label:
          default: Analytics RDS Configuration
        Parameters:
          - pRdsAllocatedStorage
          - pRdsMaxAllocatedStorage
      - Label:
          default: IAM Configuration
        Parameters:
          - pAssumableRoles
      - Label:
          default: Pipeline Configuration
        Parameters:
          - pBatchMaxvCpus
          - BatchInstanceTypes
          - BatcGpuInstanceTypes
          - pBatchVolumeSize
          - pBatchVolumeIops
          - pBatchVolumeThroughput
      - Label:
          default: (Optional) Network Extension Configurations for Existing VPC
        Parameters:
          - pAvailabilityZone3
          - pPrivateSubnet3Id
          - pPublicSubnet3Id
          - pAvailabilityZone4
          - pPrivateSubnet4Id
          - pPublicSubnet4Id
          - pAvailabilityZone5
          - pPrivateSubnet5Id
          - pPublicSubnet5Id
          - pAvailabilityZone6
          - pPrivateSubnet6Id
          - pPublicSubnet6Id
      - Label:
          default: (Optional) Network Extension Configurations for New VPC
        Parameters:
          - pNewVpcAvailabilityZone3
          - pPrivateSubnet3CIDR
          - pPublicSubnet3CIDR
          - pNewVpcAvailabilityZone4
          - pPrivateSubnet4CIDR
          - pPublicSubnet4CIDR
          - pNewVpcAvailabilityZone5
          - pPrivateSubnet5CIDR
          - pPublicSubnet5CIDR
          - pNewVpcAvailabilityZone6
          - pPrivateSubnet6CIDR
          - pPublicSubnet6CIDR
      - Label:
          default: (Optional) Backup Configuration
        Parameters:
          - pBackupSchedule
          - pBackupRetentionPeriod
          - pDestinationBackupVaultArn
          - pDestinationBackupVaultCrossRegion
          - pDestinationBackupRetentionPeriod
          - pDestinationBackupS3KmsKey
          - pDestinationBackupS3StorageClass
          - pDestinationBackupCapsulesBucketArn
          - pDestinationBackupDatasetsBucketArn
          - pDestinationBackupDockerRegistryBucketArn
          - pDestinationBackupInputFilesBucketArn
          - pDestinationBackupLicensesBucketArn
          - pDestinationBackupMLflowBucketArn
          - pDestinationBackupPackagesBucketArn
          - pDestinationBackupPublicBucketArn
          - pDestinationBackupResultsBucketArn
      - Label:
          default: Restore Configuration
        Parameters:
          - pRestoreSourceAccountId
    ParameterLabels:
      pDnsName:
        default: DNS Name
      pDnsRootDomain:
        default: DNS Root Domain
      pHostedZoneId:
        default: Existing Route 53 Hosted Zone ID
      pCertificateArn:
        default: Existing ACM Certificate ARN
      pPrivateCA:
        default: Private Certificate Authority
      pVpcId:
        default: VPC ID
      pAvailabilityZone1:
        default: Availability Zone 1
      pAvailabilityZone2:
        default: Availability Zone 2
      pAvailabilityZone3:
        default: Availability Zone 3
      pAvailabilityZone4:
        default: Availability Zone 4
      pAvailabilityZone5:
        default: Availability Zone 5
      pAvailabilityZone6:
        default: Availability Zone 6
      pPrivateSubnet1Id:
        default: Private Subnet 1 ID
      pPrivateSubnet2Id:
        default: Private Subnet 2 ID
      pPrivateSubnet3Id:
        default: Private Subnet 3 ID
      pPrivateSubnet4Id:
        default: Private Subnet 4 ID
      pPrivateSubnet5Id:
        default: Private Subnet 5 ID
      pPrivateSubnet6Id:
        default: Private Subnet 6 ID
      pPublicSubnet1Id:
        default: Public Subnet 1 ID
      pPublicSubnet2Id:
        default: Public Subnet 2 ID
      pPublicSubnet3Id:
        default: Public Subnet 3 ID
      pPublicSubnet4Id:
        default: Public Subnet 4 ID
      pPublicSubnet5Id:
        default: Public Subnet 5 ID
      pPublicSubnet6Id:
        default: Public Subnet 6 ID
      pNewVpcAvailabilityZone1:
        default: Availability Zone 1
      pNewVpcAvailabilityZone2:
        default: Availability Zone 2
      pNewVpcAvailabilityZone3:
        default: Availability Zone 3
      pNewVpcAvailabilityZone4:
        default: Availability Zone 4
      pNewVpcAvailabilityZone5:
        default: Availability Zone 5
      pNewVpcAvailabilityZone6:
        default: Availability Zone 6
      pVpcCIDR:
        default: VPC CIDR
      pVpcFlowLogs:
        default: VPC Flow Logs
      pPrivateSubnet1CIDR:
        default: Private Subnet 1 CIDR
      pPrivateSubnet2CIDR:
        default: Private Subnet 2 CIDR
      pPrivateSubnet3CIDR:
        default: Private Subnet 3 CIDR
      pPrivateSubnet4CIDR:
        default: Private Subnet 4 CIDR
      pPrivateSubnet5CIDR:
        default: Private Subnet 5 CIDR
      pPrivateSubnet6CIDR:
        default: Private Subnet 6 CIDR
      pPublicSubnet1CIDR:
        default: Public Subnet 1 CIDR
      pPublicSubnet2CIDR:
        default: Public Subnet 2 CIDR
      pPublicSubnet3CIDR:
        default: Public Subnet 3 CIDR
      pPublicSubnet4CIDR:
        default: Public Subnet 4 CIDR
      pPublicSubnet5CIDR:
        default: Public Subnet 5 CIDR
      pPublicSubnet6CIDR:
        default: Public Subnet 6 CIDR
      pSystemCapacityTier:
        default: System Capacity Tier
      pDeploymentType:
        default: Deployment Type
      pCustomUpstreamDockerRegistryEndpoint:
        default: Custom Upstream Docker Registry URL
      pCustomUpstreamDockerRegistrySecret:
        default: Custom Upstream Docker Registry Authentication Secret ARN
      pCustomAmi:
        default: AMI ID
      pWorkerInstanceType:
        default: Worker EC2 Instance Type
      pWorkersAsgMaxSize:
        default: Workers Auto Scale Group Max Size
      pMinWorkersAvailable:
        default: Min Available Workers
      pGPUWorkerInstanceType:
        default: GPU Worker EC2 Instance Type
      pGpuWorkersAsgMaxSize:
        default: GPU Workers Auto Scale Group Max Size
      pMinGpuWorkersAvailable:
        default: Min Available GPU Workers
      pAutoScalingIdleTimeout:
        default: Auto Scaling Idle Timeout
      pRdsAllocatedStorage:
        default: Analytics RDS Instance Allocated Storage
      pRdsMaxAllocatedStorage:
        default: Analytics RDS Instance Maximum Allocated Storage
      pAssumableRoles:
        default: Assumable IAM roles
      pBatchMaxvCpus:
        default: Max vCPUs
      BatchInstanceTypes:
        default: Instance Types
      BatcGpuInstanceTypes:
        default: GPU Instance Types
      pBatchVolumeSize:
        default: Volume Size
      pBatchVolumeIops:
        default: Volume IOPS
      pBatchVolumeThroughput:
        default: Volume Throughput
      pBackupSchedule:
        default: Backup Schedule
      pBackupRetentionPeriod:
        default: Backup Retention Period
      pDestinationBackupVaultArn:
        default: Destination Backup Vault ARN
      pDestinationBackupVaultCrossRegion:
        default: Destination Backup Vault In Different Region
      pDestinationBackupRetentionPeriod:
        default: Destination Backup Retention Period
      pDestinationBackupS3KmsKey:
        default: Destination Backup S3 KMS Key
      pDestinationBackupS3StorageClass:
        default: Destination Backup S3 Storage Class
      pDestinationBackupCapsulesBucketArn:
        default: Destination Backup S3 Capsules Bucket ARN
      pDestinationBackupDatasetsBucketArn:
        default: Destination Backup S3 Datasets Bucket ARN
      pDestinationBackupDockerRegistryBucketArn:
        default: Destination Backup S3 Docker Registry Bucket ARN
      pDestinationBackupInputFilesBucketArn:
        default: Destination Backup S3 Input Files Bucket ARN
      pDestinationBackupLicensesBucketArn:
        default: Destination Backup S3 Licenses Bucket ARN
      pDestinationBackupMLflowBucketArn:
        default: Destination Backup S3 MLflow Bucket ARN
      pDestinationBackupPackagesBucketArn:
        default: Destination Backup S3 Packages Bucket ARN
      pDestinationBackupPublicBucketArn:
        default: Destination Backup S3 Public Bucket ARN
      pDestinationBackupResultsBucketArn:
        default: Destination Backup S3 Results Bucket ARN
      pRestoreSourceAccountId:
        default: Restore source account ID
Mappings:
  RegionalConfigJsonMapping:
    ap-southeast-2:
      BedrockModelId: au.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "783225319266"
    ca-central-1:
      BedrockModelId: us.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "985666609251"
    eu-central-1:
      BedrockModelId: eu.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "054676820928"
    eu-north-1:
      BedrockModelId: eu.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "897822967062"
    eu-west-2:
      BedrockModelId: eu.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "652711504416"
    us-east-1:
      BedrockModelId: us.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "127311923021"
    us-east-2:
      BedrockModelId: us.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "033677994240"
    us-west-1:
      BedrockModelId: us.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "027434742980"
    us-west-2:
      BedrockModelId: us.anthropic.claude-sonnet-4-5-20250929-v1:0
      ElbAccountId: "797873946194"
  SystemCapacityConfiguration:
    services:
      small: m7i.large
      medium: m7i.2xlarge
      large: m7i.4xlarge
      xlarge: m7i.8xlarge
    rds:
      small: db.t4g.small
      medium: db.t4g.small
      large: db.t4g.medium
      xlarge: db.t4g.medium
  AMIs:
    ap-southeast-2:
      id: ami-050b0b99e6d195410
    ca-central-1:
      id: ami-0e79a9740ec113bcd
    eu-central-1:
      id: ami-05297cd41ca32575a
    eu-north-1:
      id: ami-06a4482841af82080
    eu-west-2:
      id: ami-02e050b916c3e4fa5
    us-east-1:
      id: ami-0d3c34ee573b3d151
    us-east-2:
      id: ami-03a77330bfecf1cee
    us-west-1:
      id: ami-0b8688144afe170e0
    us-west-2:
      id: ami-0e787bf3b69acb032
Parameters:
  pVpcId:
    Type: String
    Default: ""
    Description: Existing VPC ID. If not specified, a VPC will be created.
  pAvailabilityZone1:
    Type: String
    Default: ""
    Description: Availability Zone 1 for the existing VPC
  pPrivateSubnet1Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 1 located in Availability Zone 1 in Existing VPC
  pPublicSubnet1Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 1 located in Availability Zone 1 in Existing VPC
  pAvailabilityZone2:
    Type: String
    Default: ""
    Description: Availability Zone 2 for the existing VPC
  pPrivateSubnet2Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 2 located in Availability Zone 2 in Existing VPC
  pPublicSubnet2Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 2 located in Availability Zone 2 in Existing VPC
  pAvailabilityZone3:
    Type: String
    Default: ""
    Description: Availability Zone 3 for the existing VPC
  pPrivateSubnet3Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 3 located in Availability Zone 3 in Existing VPC
  pPublicSubnet3Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 3 located in Availability Zone 3 in Existing VPC
  pAvailabilityZone4:
    Type: String
    Default: ""
    Description: Availability Zone 4 for the existing VPC
  pPrivateSubnet4Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 4 located in Availability Zone 4 in Existing VPC
  pPublicSubnet4Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 4 located in Availability Zone 4 in Existing VPC
  pAvailabilityZone5:
    Type: String
    Default: ""
    Description: Availability Zone 5 for the existing VPC
  pPrivateSubnet5Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 5 located in Availability Zone 5 in Existing VPC
  pPublicSubnet5Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 5 located in Availability Zone 5 in Existing VPC
  pAvailabilityZone6:
    Type: String
    Default: ""
    Description: Availability Zone 6 for the existing VPC
  pPrivateSubnet6Id:
    Type: String
    Default: ""
    Description: Subnet ID for private subnet 6 located in Availability Zone 6 in Existing VPC
  pPublicSubnet6Id:
    Type: String
    Default: ""
    Description: Subnet ID for public subnet 6 located in Availability Zone 6 in Existing VPC
  pVpcCIDR:
    Type: String
    Default: 10.0.0.0/16
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for the VPC
  pVpcFlowLogs:
    Type: String
    Default: Disable
    AllowedValues:
      - Disable
      - Enable
    Description: VPC Flow Logs for network traffic monitoring, stored in S3
  pNewVpcAvailabilityZone1:
    Type: String
    Default: ""
    Description: Availability Zone 1 for the new VPC
  pPrivateSubnet1CIDR:
    Type: String
    Default: 10.0.0.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 1 located in Availability Zone 1
  pPublicSubnet1CIDR:
    Type: String
    Default: 10.0.96.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 1 located in Availability Zone 1
  pNewVpcAvailabilityZone2:
    Type: String
    Default: ""
    Description: Availability Zone 2 for the new VPC
  pPrivateSubnet2CIDR:
    Type: String
    Default: 10.0.16.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 2 located in Availability Zone 2
  pPublicSubnet2CIDR:
    Type: String
    Default: 10.0.112.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 2 located in Availability Zone 2
  pNewVpcAvailabilityZone3:
    Type: String
    Default: ""
    Description: Availability Zone 3 for the new VPC
  pPrivateSubnet3CIDR:
    Type: String
    Default: 10.0.32.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 3 located in Availability Zone 3
  pPublicSubnet3CIDR:
    Type: String
    Default: 10.0.128.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 3 located in Availability Zone 3
  pNewVpcAvailabilityZone4:
    Type: String
    Default: ""
    Description: Availability Zone 4 for the new VPC
  pPrivateSubnet4CIDR:
    Type: String
    Default: 10.0.48.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 4 located in Availability Zone 4
  pPublicSubnet4CIDR:
    Type: String
    Default: 10.0.144.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 4 located in Availability Zone 4
  pNewVpcAvailabilityZone5:
    Type: String
    Default: ""
    Description: Availability Zone 5 for the new VPC
  pPrivateSubnet5CIDR:
    Type: String
    Default: 10.0.64.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 5 located in Availability Zone 5
  pPublicSubnet5CIDR:
    Type: String
    Default: 10.0.160.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 5 located in Availability Zone 5
  pNewVpcAvailabilityZone6:
    Type: String
    Default: ""
    Description: Availability Zone 6 for the new VPC
  pPrivateSubnet6CIDR:
    Type: String
    Default: 10.0.80.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for private subnet 6 located in Availability Zone 6
  pPublicSubnet6CIDR:
    Type: String
    Default: 10.0.176.0/20
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Description: CIDR block for public subnet 6 located in Availability Zone 6
  pDnsName:
    Type: String
    Default: codeocean
    Description: Code Ocean application subdomain
  pDnsRootDomain:
    Type: String
    Description: Root domain name (e.g. acmecorp.com)
  pHostedZoneId:
    Type: String
    Default: ""
    Description: (Optional) Existing Route 53 hosted zone ID
  pCertificateArn:
    Type: String
    Default: ""
    Description: (Optional) Existing ACM certificate ARN
  pPrivateCA:
    Type: String
    Default: "false"
    AllowedValues:
      - "false"
      - "true"
    Description: Is the certificate signed by a private certificate authority?
  pCustomUpstreamDockerRegistryEndpoint:
    Type: String
    Default: ""
    AllowedPattern: ^(|(?:(?!https?:\/\/).+[^\/]))$
    Description: (Optional) Custom upstream Docker registry endpoint
  pCustomUpstreamDockerRegistrySecret:
    Type: String
    Default: ""
    Description: (Optional) Secret Manager secret ARN for private registry authentication
  pCustomAmi:
    Type: String
    Default: ""
    AllowedPattern: ^$|^ami-[a-z0-9]+$
    Description: (Optional) Custom AMI ID to use for services, workers, and batch instances
  pSystemCapacityTier:
    Type: String
    Default: small
    AllowedValues:
      - small
      - medium
      - large
      - xlarge
    Description: System capacity tier defines the resource sizes for services machine and RDS
  pDeploymentType:
    Type: String
    Default: internet-facing
    AllowedValues:
      - internet-facing
      - internal
    Description: Make the deployment internet addressable (default) or require a VPN to connect
  pWorkerInstanceType:
    Type: String
    Default: r5d.4xlarge
    AllowedPattern: ^r(5|6i)d\..*$
    ConstraintDescription: Instance type must be from the 'r5d' or 'r6id' families
    Description: EC2 instance type for general purpose workers. Instance type must be from the 'r5d' or 'r6id' families
  pGPUWorkerInstanceType:
    Type: String
    Default: g4dn.4xlarge
    AllowedPattern: ^(g(4dn|5|(r)?6)|p(4d|5))\.([1-9]?[2468])?xlarge$
    ConstraintDescription: Instance type must belong to the P (4d|5) or G (4dn|5|6|r6) families
    Description: EC2 instance type for GPU Workers. Instance type must belong to the P (4d|5) or G (4dn|5|6|r6) families.
  pWorkersAsgMaxSize:
    Type: Number
    Default: "10"
    Description: Maximum number of running worker instances
  pGpuWorkersAsgMaxSize:
    Type: Number
    Default: "10"
    Description: Maximum number of running GPU worker instances
  pMinWorkersAvailable:
    Type: Number
    Default: 1
    Description: Minimum number of worker instances the system keeps in its auto scaling warm pool that are ready to receive computations
  pMinGpuWorkersAvailable:
    Type: Number
    Default: 1
    Description: Minimum number of GPU worker instances the system keeps in its auto scaling warm pool that are ready to receive computations
  pAutoScalingIdleTimeout:
    Type: Number
    Default: "60"
    Description: Number of minutes before system scales-in idle workers
  pRdsAllocatedStorage:
    Type: Number
    Default: 20
    Description: RDS instance allocated storage
    MinValue: 20
  pRdsMaxAllocatedStorage:
    Type: Number
    Default: 100
    Description: RDS instance maximum allocated storage
    MinValue: 100
  pAssumableRoles:
    Type: CommaDelimitedList
    Default: ""
  pBatchMaxvCpus:
    Type: Number
    Default: "256"
    Description: Maximum number of vCPUs that can be used by all batch instances
  BatchInstanceTypes:
    Type: CommaDelimitedList
    Default: c7i,m7i,r7i,optimal
    Description: Comma-delimited list of batch instance types for pipelines
  BatcGpuInstanceTypes:
    Type: CommaDelimitedList
    Default: g4dn
    Description: Comma-delimited list of batch instance types for GPU pipelines
  pBatchVolumeSize:
    Type: Number
    Default: 300
    Description: Volume size, in gigabytes, of the Docker's EBS volumes for batch instances
    MaxValue: 16384
    MinValue: 300
  pBatchVolumeIops:
    Type: Number
    Default: 5000
    Description: Volume IOPS, number of I/O operations per second, of the Docker's EBS volumes for batch instances
    MaxValue: 16000
    MinValue: 5000
  pBatchVolumeThroughput:
    Type: Number
    Default: 500
    Description: Volume Throughput, in MiB/s, of the Docker's EBS volumes for batch instances
    MaxValue: 1000
    MinValue: 500
  pBackupSchedule:
    Type: String
    Default: cron(0 4 ? * * *)
    Description: Backup schedule CRON expression
  pBackupRetentionPeriod:
    Type: Number
    Default: 14
    Description: Backup retention period in days
    MinValue: 1
  pDestinationBackupVaultArn:
    Type: String
    Default: ""
    Description: Copy backup snapshots to a destination backup vault
  pDestinationBackupVaultCrossRegion:
    Type: String
    Default: "no"
    AllowedValues:
      - "yes"
      - "no"
    Description: Is the destination backup vault in a different region?
  pDestinationBackupRetentionPeriod:
    Type: Number
    Default: 14
    Description: Backup retention period in days for snapshots copied to the destination backup vault
    MinValue: 1
  pDestinationBackupS3KmsKey:
    Type: String
    Default: ""
    Description: AWS KMS key ARN to use for encrypting S3 object backup replicas
  pDestinationBackupS3StorageClass:
    Type: String
    Default: GLACIER_IR
    AllowedValues:
      - GLACIER_IR
      - STANDARD
    Description: AWS S3 storage class for backup object replicas
  pDestinationBackupCapsulesBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 capsules bucket ARN
  pDestinationBackupDatasetsBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 datasets bucket ARN
  pDestinationBackupDockerRegistryBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 docker registry bucket ARN
  pDestinationBackupInputFilesBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 input files bucket ARN
  pDestinationBackupLicensesBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 licenses bucket ARN
  pDestinationBackupMLflowBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 MLflow bucket ARN
  pDestinationBackupPackagesBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 packages bucket ARN
  pDestinationBackupPublicBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 public bucket ARN
  pDestinationBackupResultsBucketArn:
    Type: String
    Default: ""
    Description: Destination backup S3 results bucket ARN
  pRestoreSourceAccountId:
    Type: String
    Default: ""
    Description: (Optional) AWS Account ID to restore backups from
Conditions:
  cNewVpc:
    Fn::Equals:
      - Ref: pVpcId
      - ""
  cNewAvailabilityZone3:
    Fn::And:
      - Condition: cNewVpc
      - Fn::Not:
          - Fn::Equals:
              - Ref: pNewVpcAvailabilityZone3
              - ""
  cNewAvailabilityZone4:
    Fn::And:
      - Condition: cNewVpc
      - Fn::Not:
          - Fn::Equals:
              - Ref: pNewVpcAvailabilityZone4
              - ""
  cNewAvailabilityZone5:
    Fn::And:
      - Condition: cNewVpc
      - Fn::Not:
          - Fn::Equals:
              - Ref: pNewVpcAvailabilityZone5
              - ""
  cNewAvailabilityZone6:
    Fn::And:
      - Condition: cNewVpc
      - Fn::Not:
          - Fn::Equals:
              - Ref: pNewVpcAvailabilityZone6
              - ""
  cEnableVpcFlowLogs:
    Fn::And:
      - Condition: cNewVpc
      - Fn::Equals:
          - Ref: pVpcFlowLogs
          - Enable
  cCustomRegistry:
    Fn::Not:
      - Fn::Equals:
          - Ref: pCustomUpstreamDockerRegistryEndpoint
          - ""
  cPrivateCustomRegistry:
    Fn::And:
      - Fn::Not:
          - Fn::Equals:
              - Ref: pCustomUpstreamDockerRegistrySecret
              - ""
      - Fn::Not:
          - Fn::Equals:
              - Ref: pCustomUpstreamDockerRegistryEndpoint
              - ""
  cInternalDeployment:
    Fn::Equals:
      - Ref: pDeploymentType
      - internal
  cIsCustomAmi:
    Fn::Not:
      - Fn::Equals:
          - Ref: pCustomAmi
          - ""
  cNewHostedZone:
    Fn::Equals:
      - Ref: pHostedZoneId
      - ""
  cDnsValidation:
    Fn::And:
      - Fn::Not:
          - Condition: cInternalDeployment
      - Fn::Not:
          - Condition: cNewHostedZone
  cNewCert:
    Fn::Equals:
      - Ref: pCertificateArn
      - ""
  cAssumeRoles:
    Fn::Not:
      - Fn::Equals:
          - Fn::Join:
              - ""
              - Ref: pAssumableRoles
          - ""
  cCopyBackup:
    Fn::Not:
      - Fn::Equals:
          - Ref: pDestinationBackupVaultArn
          - ""
  cCopyBackupCrossRegion:
    Fn::Equals:
      - Ref: pDestinationBackupVaultCrossRegion
      - "yes"
  cS3Backup:
    Fn::Not:
      - Fn::Equals:
          - Ref: pDestinationBackupS3KmsKey
          - ""
  cRestore:
    Fn::Not:
      - Fn::Equals:
          - Ref: pRestoreSourceAccountId
          - ""
Rules:
  rAvailabilityZoneRule:
    RuleCondition:
      Fn::Equals:
        - Ref: pVpcId
        - ""
    Assertions:
      - Assert:
          Fn::And:
            - Fn::Not:
                - Fn::Equals:
                    - Ref: pNewVpcAvailabilityZone1
                    - ""
            - Fn::Not:
                - Fn::Equals:
                    - Ref: pNewVpcAvailabilityZone2
                    - ""
        AssertDescription: Test if availability zone 1 and 2 are set when a new VPC is being created
Resources:
  LogGroupInstances:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /instances
      RetentionInDays: 30
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  LogGroupLambda:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /lambda
      RetentionInDays: 30
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  LogGroupPipelines:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /pipelines
      RetentionInDays: 30
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  LogGroupServices:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /services
      RetentionInDays: 30
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  LogGroupWorkers:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /workers
      RetentionInDays: 30
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3AccessLogsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Infrequent Access
            Status: Enabled
            Transitions:
              - StorageClass: STANDARD_IA
                TransitionInDays: 30
          - Id: Move objects to Deep Archive
            Status: Enabled
            Transitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays: 365
          - ExpirationInDays: 2555
            Id: Auto delete objects
            Status: Enabled
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3AccessLogsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3AccessLogsBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3AccessLogsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3AccessLogsBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action: s3:PutObject
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::FindInMap:
                        - RegionalConfigJsonMapping
                        - Ref: AWS::Region
                        - ElbAccountId
                    - :root
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:s3:::"
                  - Ref: S3AccessLogsBucket
                  - /load-balacing/AWSLogs/
                  - Ref: AWS::AccountId
                  - /*
          - Action: s3:PutObject
            Effect: Allow
            Principal:
              Service: logging.s3.amazonaws.com
            Resource:
              - Fn::GetAtt:
                  - S3AccessLogsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3AccessLogsBucket
                        - Arn
                    - /*
          - Action: s3:PutObject
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control
                aws:SourceAccount:
                  Ref: AWS::AccountId
              ArnLike:
                aws:SourceArn:
                  Fn::Join:
                    - ""
                    - - "arn:aws:logs:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :*
            Effect: Allow
            Principal:
              Service: delivery.logs.amazonaws.com
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - S3AccessLogsBucket
                      - Arn
                  - /vpc-flow-logs/*
            Sid: AWSLogDeliveryWrite
          - Action: s3:GetBucketAcl
            Condition:
              StringEquals:
                aws:SourceAccount:
                  Ref: AWS::AccountId
              ArnLike:
                aws:SourceArn:
                  Fn::Join:
                    - ""
                    - - "arn:aws:logs:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :*
            Effect: Allow
            Principal:
              Service: delivery.logs.amazonaws.com
            Resource:
              Fn::GetAtt:
                - S3AccessLogsBucket
                - Arn
            Sid: AWSLogDeliveryAclCheck
        Version: "2012-10-17"
  S3BatchBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - ExpirationInDays: 30
            Id: Auto delete objects
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Batch/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3BatchBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3BatchBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3BatchBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3BatchBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
        Version: "2012-10-17"
  S3CapsulesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Capsules/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupCapsulesBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3CapsulesBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3CapsulesBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3CapsulesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3CapsulesBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3CapsulesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3CapsulesBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3DatasetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Datasets/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupDatasetsBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3DatasetsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3DatasetsBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3DatasetsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3DatasetsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3DatasetsInputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/DatasetsInput/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3DatasetsInputBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3DatasetsInputBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3DatasetsInputBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsInputBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
        Version: "2012-10-17"
  S3InputFilesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/InputFiles/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupInputFilesBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3InputFilesBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3InputFilesBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3InputFilesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3InputFilesBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3InputFilesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3InputFilesBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3LicensesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays:
                Ref: pBackupRetentionPeriod
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Licenses/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupLicensesBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3LicensesBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3LicensesBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3LicensesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3LicensesBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3LicensesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3LicensesBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3MLflowBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/MLflow/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupMLflowBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3MLflowBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3MLflowBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3MLflowBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3MLflowBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3MLflowBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3MLflowBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3PackagesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Packages/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupPackagesBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3PackagesBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3PackagesBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3PackagesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PackagesBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3PackagesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PackagesBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3PublicBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays:
                Ref: pBackupRetentionPeriod
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Public/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupPublicBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3PublicBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3PublicBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3PublicBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PublicBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3PublicBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PublicBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3DockerRegistryBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 0
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/DockerRegistry/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupDockerRegistryBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3DockerRegistryBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3DockerRegistryBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3DockerRegistryBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DockerRegistryBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3DockerRegistryBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DockerRegistryBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3ResultsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - Id: Move objects to Intelligent-Tiering class
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 14
          - Id: Backup noncurrent object versions
            NoncurrentVersionTransitions:
              - StorageClass: DEEP_ARCHIVE
                TransitionInDays:
                  Ref: pBackupRetentionPeriod
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays: 180
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Results/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      ReplicationConfiguration:
        Fn::If:
          - cS3Backup
          - Role:
              Fn::GetAtt:
                - S3BackupRole
                - Arn
            Rules:
              - DeleteMarkerReplication:
                  Status: Enabled
                Destination:
                  Bucket:
                    Ref: pDestinationBackupResultsBucketArn
                  EncryptionConfiguration:
                    ReplicaKmsKeyID:
                      Ref: pDestinationBackupS3KmsKey
                  StorageClass:
                    Ref: pDestinationBackupS3StorageClass
                Filter:
                  Prefix: ""
                Id: Backup
                Priority: 0
                SourceSelectionCriteria:
                  SseKmsEncryptedObjects:
                    Status: Enabled
                Status: Enabled
          - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3ResultsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3ResultsBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3ResultsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ResultsBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
          - Action:
              - s3:GetBucketVersioning
              - s3:PutBucketVersioning
              - s3:ReplicateDelete
              - s3:ReplicateObject
              - s3:ReplicateTags
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Fn::If:
                        - cRestore
                        - Ref: pRestoreSourceAccountId
                        - Ref: AWS::AccountId
                    - :root
            Resource:
              - Fn::GetAtt:
                  - S3ResultsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ResultsBucket
                        - Arn
                    - /*
        Version: "2012-10-17"
  S3ScratchBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - ExpiredObjectDeleteMarker: true
            Id: Delete noncurrent object versions
            NoncurrentVersionExpiration:
              NoncurrentDays:
                Ref: pBackupRetentionPeriod
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Scratch/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
      VersioningConfiguration:
        Status: Enabled
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3ScratchBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3ScratchBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3ScratchBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ScratchBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
        Version: "2012-10-17"
  S3TempBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Id: Delete incomplete multipart uploads
            Status: Enabled
          - ExpirationInDays: 1
            Id: Auto delete objects
            Status: Enabled
      LoggingConfiguration:
        DestinationBucketName:
          Ref: S3AccessLogsBucket
        LogFilePrefix: s3/Temp/
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  S3TempBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: S3TempBucket
      PolicyDocument:
        Statement:
          - Action: s3:*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Effect: Deny
            Principal:
              AWS: "*"
            Resource:
              - Fn::GetAtt:
                  - S3TempBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3TempBucket
                        - Arn
                    - /*
            Sid: AllowSSLRequestsOnly
        Version: "2012-10-17"
  S3BackupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: s3.amazonaws.com
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: batchoperations.s3.amazonaws.com
        Version: "2012-10-17"
      Description: Provides AWS S3 permissions to backup buckets.
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cS3Backup
  S3BackupReplicationPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetReplicationConfiguration
              - s3:ListBucket
              - s3:PutInventoryConfiguration
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - S3CapsulesBucket
                  - Arn
              - Fn::GetAtt:
                  - S3DatasetsBucket
                  - Arn
              - Fn::GetAtt:
                  - S3InputFilesBucket
                  - Arn
              - Fn::GetAtt:
                  - S3LicensesBucket
                  - Arn
              - Fn::GetAtt:
                  - S3MLflowBucket
                  - Arn
              - Fn::GetAtt:
                  - S3PackagesBucket
                  - Arn
              - Fn::GetAtt:
                  - S3PublicBucket
                  - Arn
              - Fn::GetAtt:
                  - S3DockerRegistryBucket
                  - Arn
              - Fn::GetAtt:
                  - S3ResultsBucket
                  - Arn
          - Action:
              - s3:GetObjectVersionAcl
              - s3:GetObjectVersionForReplication
              - s3:GetObjectVersionTagging
              - s3:InitiateReplication
            Effect: Allow
            Resource:
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3CapsulesBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3InputFilesBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3LicensesBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3MLflowBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PackagesBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3PublicBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DockerRegistryBucket
                        - Arn
                    - /*
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ResultsBucket
                        - Arn
                    - /*
          - Action:
              - s3:ReplicateObject
              - s3:ReplicateDelete
              - s3:ReplicateTags
            Effect: Allow
            Resource:
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupCapsulesBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupDatasetsBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupInputFilesBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupLicensesBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupMLflowBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupPackagesBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupPublicBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupDockerRegistryBucketArn
                    - /*
              - Fn::Join:
                  - ""
                  - - Ref: pDestinationBackupResultsBucketArn
                    - /*
          - Action: s3:PutObject
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - S3TempBucket
                      - Arn
                  - /*
        Version: "2012-10-17"
      PolicyName: S3BackupReplicationPolicy
      Roles:
        - Ref: S3BackupRole
    Condition: cS3Backup
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock:
        Ref: pVpcCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Vpc
    Condition: cNewVpc
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /InternetGateway
    Condition: cNewVpc
  VpcGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: InternetGateway
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  VpcGatewayAttachmentWaitHandle:
    Type: AWS::CloudFormation::WaitConditionHandle
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewVpc
  WaitHandle:
    Type: AWS::CloudFormation::WaitConditionHandle
  VpcGatewayAttachmentWaitCondition:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Count: 0
      Handle:
        Fn::If:
          - cNewVpc
          - Ref: VpcGatewayAttachmentWaitHandle
          - Ref: WaitHandle
      Timeout: "1"
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone1
      CidrBlock:
        Ref: pPublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet1
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PublicSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet1RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet1RouteTable
      SubnetId:
        Ref: PublicSubnet1
    Condition: cNewVpc
  PublicSubnet1DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet1RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewVpc
  PublicSubnet1EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewVpc
  PublicSubnet1NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet1EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet1
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet1NATGateway
    Condition: cNewVpc
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone1
      CidrBlock:
        Ref: pPrivateSubnet1CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet1
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PrivateSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet1RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet1RouteTable
      SubnetId:
        Ref: PrivateSubnet1
    Condition: cNewVpc
  PrivateSubnet1DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet1NATGateway
      RouteTableId:
        Ref: PrivateSubnet1RouteTable
    Condition: cNewVpc
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone2
      CidrBlock:
        Ref: pPublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet2
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PublicSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet2RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet2RouteTable
      SubnetId:
        Ref: PublicSubnet2
    Condition: cNewVpc
  PublicSubnet2DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet2RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewVpc
  PublicSubnet2EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewVpc
  PublicSubnet2NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet2EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet2
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet2NATGateway
    Condition: cNewVpc
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone2
      CidrBlock:
        Ref: pPrivateSubnet2CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet2
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PrivateSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet2RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet2RouteTable
      SubnetId:
        Ref: PrivateSubnet2
    Condition: cNewVpc
  PrivateSubnet2DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet2NATGateway
      RouteTableId:
        Ref: PrivateSubnet2RouteTable
    Condition: cNewVpc
  PublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone3
      CidrBlock:
        Ref: pPublicSubnet3CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet3
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone3
  PublicSubnet3RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet3RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone3
  PublicSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet3RouteTable
      SubnetId:
        Ref: PublicSubnet3
    Condition: cNewAvailabilityZone3
  PublicSubnet3DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet3RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewAvailabilityZone3
  PublicSubnet3EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewAvailabilityZone3
  PublicSubnet3NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet3EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet3
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet3NATGateway
    Condition: cNewAvailabilityZone3
  PrivateSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone3
      CidrBlock:
        Ref: pPrivateSubnet3CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet3
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone3
  PrivateSubnet3RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet3RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone3
  PrivateSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet3RouteTable
      SubnetId:
        Ref: PrivateSubnet3
    Condition: cNewAvailabilityZone3
  PrivateSubnet3DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet3NATGateway
      RouteTableId:
        Ref: PrivateSubnet3RouteTable
    Condition: cNewAvailabilityZone3
  PublicSubnet4:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone4
      CidrBlock:
        Ref: pPublicSubnet4CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet4
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone4
  PublicSubnet4RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet4RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone4
  PublicSubnet4RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet4RouteTable
      SubnetId:
        Ref: PublicSubnet4
    Condition: cNewAvailabilityZone4
  PublicSubnet4DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet4RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewAvailabilityZone4
  PublicSubnet4EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewAvailabilityZone4
  PublicSubnet4NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet4EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet4
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet4NATGateway
    Condition: cNewAvailabilityZone4
  PrivateSubnet4:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone4
      CidrBlock:
        Ref: pPrivateSubnet4CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet4
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone4
  PrivateSubnet4RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet4RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone4
  PrivateSubnet4RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet4RouteTable
      SubnetId:
        Ref: PrivateSubnet4
    Condition: cNewAvailabilityZone4
  PrivateSubnet4DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet4NATGateway
      RouteTableId:
        Ref: PrivateSubnet4RouteTable
    Condition: cNewAvailabilityZone4
  PublicSubnet5:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone5
      CidrBlock:
        Ref: pPublicSubnet5CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet5
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone5
  PublicSubnet5RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet5RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone5
  PublicSubnet5RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet5RouteTable
      SubnetId:
        Ref: PublicSubnet5
    Condition: cNewAvailabilityZone5
  PublicSubnet5DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet5RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewAvailabilityZone5
  PublicSubnet5EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewAvailabilityZone5
  PublicSubnet5NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet5EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet5
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet5NATGateway
    Condition: cNewAvailabilityZone5
  PrivateSubnet5:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone5
      CidrBlock:
        Ref: pPrivateSubnet5CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet5
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone5
  PrivateSubnet5RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet5RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone5
  PrivateSubnet5RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet5RouteTable
      SubnetId:
        Ref: PrivateSubnet5
    Condition: cNewAvailabilityZone5
  PrivateSubnet5DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet5NATGateway
      RouteTableId:
        Ref: PrivateSubnet5RouteTable
    Condition: cNewAvailabilityZone5
  PublicSubnet6:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone6
      CidrBlock:
        Ref: pPublicSubnet6CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet6
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone6
  PublicSubnet6RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet6RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone6
  PublicSubnet6RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PublicSubnet6RouteTable
      SubnetId:
        Ref: PublicSubnet6
    Condition: cNewAvailabilityZone6
  PublicSubnet6DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
      RouteTableId:
        Ref: PublicSubnet6RouteTable
    DependsOn:
      - VpcGatewayAttachment
    Condition: cNewAvailabilityZone6
  PublicSubnet6EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cNewAvailabilityZone6
  PublicSubnet6NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - PublicSubnet6EIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet6
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PublicSubnet6NATGateway
    Condition: cNewAvailabilityZone6
  PrivateSubnet6:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Ref: pNewVpcAvailabilityZone6
      CidrBlock:
        Ref: pPrivateSubnet6CIDR
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet6
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone6
  PrivateSubnet6RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /PrivateSubnet6RouteTable
      VpcId:
        Ref: Vpc
    Condition: cNewAvailabilityZone6
  PrivateSubnet6RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: PrivateSubnet6RouteTable
      SubnetId:
        Ref: PrivateSubnet6
    Condition: cNewAvailabilityZone6
  PrivateSubnet6DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: PublicSubnet6NATGateway
      RouteTableId:
        Ref: PrivateSubnet6RouteTable
    Condition: cNewAvailabilityZone6
  S3VpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1RouteTable
            - Ref: AWS::NoValue
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet2RouteTable
            - Ref: AWS::NoValue
        - Fn::If:
            - cNewAvailabilityZone3
            - Ref: PrivateSubnet3RouteTable
            - Ref: AWS::NoValue
        - Fn::If:
            - cNewAvailabilityZone4
            - Ref: PrivateSubnet4RouteTable
            - Ref: AWS::NoValue
        - Fn::If:
            - cNewAvailabilityZone5
            - Ref: PrivateSubnet5RouteTable
            - Ref: AWS::NoValue
        - Fn::If:
            - cNewAvailabilityZone6
            - Ref: PrivateSubnet6RouteTable
            - Ref: AWS::NoValue
      ServiceName:
        Fn::Join:
          - ""
          - - com.amazonaws.
            - Ref: AWS::Region
            - .s3
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcEndpointType: Gateway
      VpcId:
        Ref: Vpc
    Condition: cNewVpc
  VpcFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      LogDestination:
        Fn::Join:
          - ""
          - - Fn::GetAtt:
                - S3AccessLogsBucket
                - Arn
            - /vpc-flow-logs/
      LogDestinationType: s3
      ResourceId:
        Ref: Vpc
      ResourceType: VPC
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /VpcFlowLog
      TrafficType: ALL
    DependsOn:
      - S3AccessLogsBucketPolicy
    Condition: cEnableVpcFlowLogs
  OpenSearchSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/opensearch/OpenSearchSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  OpenSearchSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: OpenSearchSecurityGroup
      IpProtocol: "-1"
  OpenSearchSecurityGroupIngressJobs:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: es from jobs
      FromPort: 443
      GroupId:
        Ref: OpenSearchSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: JobsSecurityGroup
      ToPort: 443
  OpenSearchSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: es from services
      FromPort: 443
      GroupId:
        Ref: OpenSearchSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 443
  OpenSearchDomainV2:
    Type: AWS::OpenSearchService::Domain
    Properties:
      ClusterConfig:
        DedicatedMasterEnabled: false
        InstanceCount: 2
        InstanceType: m7g.large.search
        ZoneAwarenessConfig:
          AvailabilityZoneCount: 2
        ZoneAwarenessEnabled: true
      DomainEndpointOptions:
        EnforceHTTPS: true
        TLSSecurityPolicy: Policy-Min-TLS-1-2-2019-07
      EBSOptions:
        EBSEnabled: true
        Iops: 3000
        Throughput: 125
        VolumeSize: 20
        VolumeType: gp3
      EncryptionAtRestOptions:
        Enabled: true
      EngineVersion: OpenSearch_2.19
      NodeToNodeEncryptionOptions:
        Enabled: true
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VPCOptions:
        SecurityGroupIds:
          - Ref: OpenSearchSecurityGroup
        SubnetIds:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  DatasetsEfs:
    Type: AWS::EFS::FileSystem
    Properties:
      Encrypted: true
      FileSystemPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: Enforce access via mount targets
            Effect: Deny
            Principal:
              AWS: "*"
            Action: "*"
            Condition:
              Bool:
                elasticfilesystem:AccessedViaMountTarget: "false"
          - Sid: Enforce in-transit encryption
            Effect: Deny
            Principal:
              AWS: "*"
            Action: "*"
            Condition:
              Bool:
                aws:SecureTransport: "false"
      FileSystemTags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /efs/datasets
      LifecyclePolicies:
        - TransitionToIA: AFTER_7_DAYS
        - TransitionToPrimaryStorageClass: AFTER_1_ACCESS
      ThroughputMode: elastic
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  DatasetsEfsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/Datasets/DatasetsEfsSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  DatasetsEfsSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: DatasetsEfsSecurityGroup
      IpProtocol: "-1"
  DatasetsEfsSecurityGroupIngressJobs:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from jobs
      FromPort: 2049
      GroupId:
        Ref: DatasetsEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: JobsSecurityGroup
      ToPort: 2049
  DatasetsEfsSecurityGroupIngressBatch:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from batch
      FromPort: 2049
      GroupId:
        Ref: DatasetsEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: BatchSecurityGroup
      ToPort: 2049
  DatasetsEfsSecurityGroupIngressWorkers:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from workers
      FromPort: 2049
      GroupId:
        Ref: DatasetsEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 2049
  DatasetsEfsSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from services
      FromPort: 2049
      GroupId:
        Ref: DatasetsEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 2049
  DatasetsEfsMountTarget1:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewVpc
          - Ref: PrivateSubnet1
          - Ref: pPrivateSubnet1Id
  DatasetsEfsMountTarget2:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewVpc
          - Ref: PrivateSubnet2
          - Ref: pPrivateSubnet2Id
  DatasetsEfsMountTarget3:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone3
          - Ref: PrivateSubnet3
          - Ref: pPrivateSubnet3Id
    Condition: cNewAvailabilityZone3
  DatasetsEfsMountTarget4:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone4
          - Ref: PrivateSubnet4
          - Ref: pPrivateSubnet4Id
    Condition: cNewAvailabilityZone4
  DatasetsEfsMountTarget5:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone5
          - Ref: PrivateSubnet5
          - Ref: pPrivateSubnet5Id
    Condition: cNewAvailabilityZone5
  DatasetsEfsMountTarget6:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: DatasetsEfs
      SecurityGroups:
        - Ref: DatasetsEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone6
          - Ref: PrivateSubnet6
          - Ref: pPrivateSubnet6Id
    Condition: cNewAvailabilityZone6
  ScratchEfs:
    Type: AWS::EFS::FileSystem
    Properties:
      Encrypted: true
      FileSystemPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: Enforce access via mount targets
            Effect: Deny
            Principal:
              AWS: "*"
            Action: "*"
            Condition:
              Bool:
                elasticfilesystem:AccessedViaMountTarget: "false"
          - Sid: Enforce in-transit encryption
            Effect: Deny
            Principal:
              AWS: "*"
            Action: "*"
            Condition:
              Bool:
                aws:SecureTransport: "false"
      FileSystemTags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /efs/scratch
      LifecyclePolicies:
        - TransitionToIA: AFTER_7_DAYS
        - TransitionToPrimaryStorageClass: AFTER_1_ACCESS
        - TransitionToArchive: AFTER_30_DAYS
      ThroughputMode: elastic
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  ScratchEfsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/Scratch/ScratchEfsSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  ScratchEfsSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: ScratchEfsSecurityGroup
      IpProtocol: "-1"
  ScratchEfsSecurityGroupIngressJobs:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from jobs
      FromPort: 2049
      GroupId:
        Ref: ScratchEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: JobsSecurityGroup
      ToPort: 2049
  ScratchEfsSecurityGroupIngressWorkers:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from workers
      FromPort: 2049
      GroupId:
        Ref: ScratchEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 2049
  ScratchEfsSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: efs from services
      FromPort: 2049
      GroupId:
        Ref: ScratchEfsSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 2049
  ScratchEfsMountTarget1:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewVpc
          - Ref: PrivateSubnet1
          - Ref: pPrivateSubnet1Id
  ScratchEfsMountTarget2:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewVpc
          - Ref: PrivateSubnet2
          - Ref: pPrivateSubnet2Id
  ScratchEfsMountTarget3:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone3
          - Ref: PrivateSubnet3
          - Ref: pPrivateSubnet3Id
    Condition: cNewAvailabilityZone3
  ScratchEfsMountTarget4:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone4
          - Ref: PrivateSubnet4
          - Ref: pPrivateSubnet4Id
    Condition: cNewAvailabilityZone4
  ScratchEfsMountTarget5:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone5
          - Ref: PrivateSubnet5
          - Ref: pPrivateSubnet5Id
    Condition: cNewAvailabilityZone5
  ScratchEfsMountTarget6:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId:
        Ref: ScratchEfs
      SecurityGroups:
        - Ref: ScratchEfsSecurityGroup
      SubnetId:
        Fn::If:
          - cNewAvailabilityZone6
          - Ref: PrivateSubnet6
          - Ref: pPrivateSubnet6Id
    Condition: cNewAvailabilityZone6
  RedisSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/redis/RedisSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  RedisSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: RedisSecurityGroup
      IpProtocol: "-1"
  RedisSecurityGroupIngressJobs:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: redis from jobs
      FromPort: 6379
      GroupId:
        Ref: RedisSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: JobsSecurityGroup
      ToPort: 6379
  RedisSecurityGroupIngressWorkers:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: redis from workers
      FromPort: 6379
      GroupId:
        Ref: RedisSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 6379
  RedisSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: redis from services
      FromPort: 6379
      GroupId:
        Ref: RedisSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 6379
  RedisSubnetGroup:
    Type: AWS::ElastiCache::SubnetGroup
    Properties:
      CacheSubnetGroupName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -redis
      Description: Subnet group for Code Ocean Redis
      SubnetIds:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1
            - Ref: pPrivateSubnet1Id
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet2
            - Ref: pPrivateSubnet2Id
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  RedisAuthToken:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Redis auth token
      GenerateSecretString:
        ExcludePunctuation: true
        PasswordLength: 128
      Name:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /redis/auth-token
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
  RedisReplicationGroup:
    Type: AWS::ElastiCache::ReplicationGroup
    Properties:
      AtRestEncryptionEnabled: true
      AuthToken:
        Fn::Join:
          - ""
          - - "{{resolve:secretsmanager:/"
            - Ref: AWS::StackName
            - /redis/auth-token:SecretString}}
      AutomaticFailoverEnabled: true
      CacheNodeType: cache.t4g.micro
      CacheParameterGroupName: default.valkey8
      CacheSubnetGroupName:
        Ref: RedisSubnetGroup
      Engine: valkey
      EngineVersion: "8.2"
      MultiAZEnabled: true
      NumCacheClusters: 2
      PreferredCacheClusterAZs:
        - Fn::If:
            - cNewVpc
            - Ref: pNewVpcAvailabilityZone1
            - Ref: pAvailabilityZone1
        - Fn::If:
            - cNewVpc
            - Ref: pNewVpcAvailabilityZone2
            - Ref: pAvailabilityZone2
      ReplicationGroupDescription: Code Ocean Redis
      SecurityGroupIds:
        - Ref: RedisSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TransitEncryptionEnabled: true
  ExternalHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      HostedZoneConfig:
        Comment: External name resolution
      HostedZoneTags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Name:
        Fn::Join:
          - ""
          - - Ref: pDnsName
            - "."
            - Ref: pDnsRootDomain
      VPCs:
        Fn::If:
          - cInternalDeployment
          - - VPCId:
                Fn::If:
                  - cNewVpc
                  - Ref: Vpc
                  - Ref: pVpcId
              VPCRegion:
                Ref: AWS::Region
          - Ref: AWS::NoValue
    Condition: cNewHostedZone
  InternalHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      HostedZoneConfig:
        Comment: Internal service name resolution in VPC
      HostedZoneTags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Name: codeocean.internal
      VPCs:
        - VPCId:
            Fn::If:
              - cNewVpc
              - Ref: Vpc
              - Ref: pVpcId
          VPCRegion:
            Ref: AWS::Region
  DeleteRecordSetCustomResourcePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for custom resource
      Path: /
      PolicyDocument:
        Statement:
          - Action:
              - route53:ChangeResourceRecordSets
              - route53:ListResourceRecordSets
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - arn:aws:route53:::hostedzone/
                  - Ref: InternalHostedZone
        Version: "2012-10-17"
  DeleteRecordSetRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      Description: Allows AWS Lambda to call AWS services to delete internal code ocean dns records on cloudformation stack deletion.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - Ref: DeleteRecordSetCustomResourcePolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  DeleteRecordSetFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import logging

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          logger.info("Loading function")


          def handler(event, context):
              record_name = event["ResourceProperties"]["RecordName"]
              hosted_zone_id = event["ResourceProperties"]["HostedZoneId"]
              physical_id = f"delete-record-set-lambda-{hosted_zone_id}"

              logger.info(f"Received event: {event}")

              if event["RequestType"] != 'Delete':
                  logger.info(f"Skipping {event['RequestType']}")
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                  return

              try:
                  route53 = boto3.client("route53")

                  ip_address = get_ip_from_record(route53, hosted_zone_id, record_name)
                  if not ip_address:
                      logger.info(f"Record {record_name} not found in zone {hosted_zone_id}")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                      return

                  if ip_address == 1:
                      logger.info(f"Hosted zone {hosted_zone_id} not found")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                      return

                  changes = [
                      {
                          "Action": "DELETE",
                          "ResourceRecordSet": {
                              "Name": record_name,
                              "TTL": 60,
                              "Type": "A",
                              "ResourceRecords": [{"Value": ip_address}],
                          },
                      }
                  ]

                  params = {"ChangeBatch": {"Changes": changes}, "HostedZoneId": hosted_zone_id}

                  try:
                      response = route53.change_resource_record_sets(**params)
                      logger.info(f"Response from Route53: {response}")
                  except route53.exceptions.InvalidChangeBatch as e:
                      error_message = str(e)
                      if "was not found" in error_message:
                          logger.info(f"Record set not found, treating as success: {error_message}")
                      else:
                          raise

                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)

              except Exception as e:
                  logger.exception(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {}, physical_id)


          def get_ip_from_record(route53, hosted_zone_id, record_name):
              try:
                  response = route53.list_resource_record_sets(
                      HostedZoneId=hosted_zone_id, StartRecordName=record_name, StartRecordType="A"
                  )

                  for record_set in response["ResourceRecordSets"]:
                      if record_set["Name"][:-1] == record_name:
                          return record_set["ResourceRecords"][0]["Value"]
              except route53.exceptions.NoSuchHostedZone as e:
                  logger.exception(e)
                  return 1
      Description: Delete internal Code Ocean DNS records on CloudFormation stack deletion.
      Handler: index.handler
      LoggingConfig:
        LogGroup:
          Fn::Join:
            - ""
            - - /
              - Ref: AWS::StackName
              - /lambda
      Role:
        Fn::GetAtt:
          - DeleteRecordSetRole
          - Arn
      Runtime: python3.13
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Timeout: 60
    DependsOn:
      - DeleteRecordSetRole
  DeleteRecordSetCustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - DeleteRecordSetFunction
          - Arn
      HostedZoneId:
        Ref: InternalHostedZone
      RecordName: codeocean.internal
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName:
        Fn::Join:
          - ""
          - - Ref: pDnsName
            - "."
            - Ref: pDnsRootDomain
      DomainValidationOptions:
        Fn::If:
          - cDnsValidation
          - - DomainName:
                Fn::Join:
                  - ""
                  - - Ref: pDnsName
                    - "."
                    - Ref: pDnsRootDomain
              HostedZoneId:
                Ref: pHostedZoneId
          - - DomainName:
                Fn::Join:
                  - ""
                  - - Ref: pDnsName
                    - "."
                    - Ref: pDnsRootDomain
              ValidationDomain:
                Ref: pDnsRootDomain
      SubjectAlternativeNames:
        - Fn::Join:
            - ""
            - - "*."
              - Ref: pDnsName
              - "."
              - Ref: pDnsRootDomain
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Cert
      ValidationMethod:
        Fn::If:
          - cDnsValidation
          - DNS
          - Ref: AWS::NoValue
    Condition: cNewCert
  AnalyticsDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/analytics/AnalyticsDBSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  AnalyticsDBSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: AnalyticsDBSecurityGroup
      IpProtocol: "-1"
  AnalyticsDBSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: analyticsdb from services
      FromPort: 5432
      GroupId:
        Ref: AnalyticsDBSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 5432
  AnalyticsDBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Description: A parmeter group that includes pg_cron
      Family: postgres17
      Parameters:
        shared_preload_libraries: pg_stat_statements,pg_cron
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  AnalyticsDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Code Ocean analytics DB subnet group
      SubnetIds:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1
            - Ref: pPrivateSubnet1Id
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet2
            - Ref: pPrivateSubnet2Id
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  AnalyticsDBMasterPassword:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Analytics DB master password
      GenerateSecretString:
        ExcludePunctuation: true
        PasswordLength: 16
      Name:
        Fn::Join:
          - ""
          - - /
            - Ref: AWS::StackName
            - /analytics/master-password-0
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
  AnalyticsDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage:
        Ref: pRdsAllocatedStorage
      AllowMajorVersionUpgrade: true
      AutoMinorVersionUpgrade: true
      BackupRetentionPeriod: 0
      CACertificateIdentifier: rds-ca-rsa4096-g1
      DBInstanceClass:
        Fn::FindInMap:
          - SystemCapacityConfiguration
          - rds
          - Ref: pSystemCapacityTier
      DBParameterGroupName:
        Ref: AnalyticsDBParameterGroup
      DBSubnetGroupName:
        Ref: AnalyticsDBSubnetGroup
      Engine: postgres
      EngineVersion: "17"
      MasterUserPassword:
        Fn::Join:
          - ""
          - - "{{resolve:secretsmanager:/"
            - Ref: AWS::StackName
            - /analytics/master-password-0:SecretString}}
      MasterUsername: root
      MaxAllocatedStorage:
        Ref: pRdsMaxAllocatedStorage
      MultiAZ: true
      StorageEncrypted: true
      StorageType: gp3
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VPCSecurityGroups:
        - Ref: AnalyticsDBSecurityGroup
    DependsOn:
      - AnalyticsDBMasterPassword
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  ExternalSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/ingress/ExternalSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  ExternalSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: ExternalSecurityGroup
      IpProtocol: "-1"
  ExternalSecurityGroupIngressHttpIpv4:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      CidrIp: 0.0.0.0/0
      FromPort: 80
      GroupId:
        Ref: ExternalSecurityGroup
      IpProtocol: tcp
      ToPort: 80
  ExternalSecurityGroupIngressHttpIpv6:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      CidrIpv6: ::/0
      FromPort: 80
      GroupId:
        Ref: ExternalSecurityGroup
      IpProtocol: tcp
      ToPort: 80
  ExternalSecurityGroupIngressHttpsIpv4:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      CidrIp: 0.0.0.0/0
      FromPort: 443
      GroupId:
        Ref: ExternalSecurityGroup
      IpProtocol: tcp
      ToPort: 443
  ExternalSecurityGroupIngressHttpsIpv6:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      CidrIpv6: ::/0
      FromPort: 443
      GroupId:
        Ref: ExternalSecurityGroup
      IpProtocol: tcp
      ToPort: 443
  ExternalLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        - Key: access_logs.s3.enabled
          Value: "true"
        - Key: access_logs.s3.bucket
          Value:
            Ref: S3AccessLogsBucket
        - Key: access_logs.s3.prefix
          Value: load-balacing
        - Key: routing.http.drop_invalid_header_fields.enabled
          Value: "true"
        - Key: routing.http.xff_header_processing.mode
          Value: remove
        - Key: routing.http2.enabled
          Value: "false"
        - Key: idle_timeout.timeout_seconds
          Value: "4000"
      Scheme:
        Ref: pDeploymentType
      SecurityGroups:
        - Ref: ExternalSecurityGroup
      Subnets:
        Fn::If:
          - cInternalDeployment
          - - Fn::If:
                - cNewVpc
                - Ref: PrivateSubnet1
                - Ref: pPrivateSubnet1Id
            - Fn::If:
                - cNewVpc
                - Ref: PrivateSubnet2
                - Ref: pPrivateSubnet2Id
          - - Fn::If:
                - cNewVpc
                - Ref: PublicSubnet1
                - Ref: pPublicSubnet1Id
            - Fn::If:
                - cNewVpc
                - Ref: PublicSubnet2
                - Ref: pPublicSubnet2Id
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Type: application
    DependsOn:
      - S3AccessLogsBucketPolicy
      - VpcGatewayAttachmentWaitCondition
  TargetGroupAnalytics:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /api/health
      HealthCheckPort: "8080"
      Port: 8888
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupAssets:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "8120"
      Port: 8120
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupCWProxy:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "8114"
      Port: 8114
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupFileProxy:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "8504"
      Port: 8504
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupGitProxy:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "3050"
      Port: 3050
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupApiService:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "8400"
      Port: 8400
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupGW:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /api/health
      HealthCheckPort: "8080"
      Port: 8080
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupRegistry:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /
      HealthCheckPort: "5000"
      Port: 5000
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupScim:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "9901"
      Port: 9901
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  TargetGroupWeb:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckPath: /health
      HealthCheckPort: "8001"
      Port: 8001
      Protocol: HTTP
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "60"
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  ListenerExternalHTTP:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - RedirectConfig:
            Port: "443"
            Protocol: HTTPS
            StatusCode: HTTP_301
          Type: redirect
      LoadBalancerArn:
        Ref: ExternalLoadBalancer
      Port: 80
      Protocol: HTTP
  ListenerExternalHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Certificates:
        - CertificateArn:
            Fn::If:
              - cNewCert
              - Ref: Certificate
              - Ref: pCertificateArn
      DefaultActions:
        - TargetGroupArn:
            Ref: TargetGroupWeb
          Type: forward
      LoadBalancerArn:
        Ref: ExternalLoadBalancer
      Port: 443
      Protocol: HTTPS
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-Res-2021-06
  ListenerRuleAnalytics:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupAnalytics
          Type: forward
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - Fn::Join:
                  - ""
                  - - analytics.
                    - Ref: pDnsName
                    - "."
                    - Ref: pDnsRootDomain
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 80
  ListenerRuleFileProxy:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /files/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 100
  ListenerRuleFileProxyAuthCallback:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /files_auth_callback
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 200
  ListenerRuleFileProxyDatasets:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /datasets/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 300
  ListenerRuleFileProxyDatasetsAuthCallback:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /datasets_auth_callback
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 400
  ListenerRuleFileProxyInput:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /input/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 500
  ListenerRuleFileProxyInputAuthCallback:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupFileProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /input_auth_callback
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 600
  ListenerRuleCWProxy:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupCWProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /cw/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 700
  ListenerRuleMLflow:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupCWProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /mlflow-server/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 701
  ListenerRuleMLflowApp:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupCWProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /mlflow-app/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 702
  ListenerRuleAPIService:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupApiService
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /api/v1/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 780
  ListenerRuleGW:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupGW
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /api/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 800
  ListenerRuleDockerRegistry:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupRegistry
          Type: forward
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - Fn::Join:
                  - ""
                  - - registry.
                    - Ref: pDnsName
                    - "."
                    - Ref: pDnsRootDomain
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 900
  ListenerRuleAssets:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupAssets
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /a/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 1100
  ListenerRuleGitProxy:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupGitProxy
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /capsule-*.git/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 1200
  ListenerRuleScim:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - TargetGroupArn:
            Ref: TargetGroupScim
          Type: forward
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - /scim/*
      ListenerArn:
        Ref: ListenerExternalHTTPS
      Priority: 1300
  ExternalDnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        DNSName:
          Fn::Join:
            - ""
            - - dualstack.
              - Fn::GetAtt:
                  - ExternalLoadBalancer
                  - DNSName
        EvaluateTargetHealth: false
        HostedZoneId:
          Fn::GetAtt:
            - ExternalLoadBalancer
            - CanonicalHostedZoneID
      HostedZoneId:
        Fn::If:
          - cNewHostedZone
          - Ref: ExternalHostedZone
          - Ref: pHostedZoneId
      Name:
        Fn::Join:
          - ""
          - - Ref: pDnsName
            - "."
            - Ref: pDnsRootDomain
      Type: A
  DockerRegistryDnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        DNSName:
          Fn::Join:
            - ""
            - - dualstack.
              - Fn::GetAtt:
                  - ExternalLoadBalancer
                  - DNSName
        EvaluateTargetHealth: false
        HostedZoneId:
          Fn::GetAtt:
            - ExternalLoadBalancer
            - CanonicalHostedZoneID
      HostedZoneId:
        Fn::If:
          - cNewHostedZone
          - Ref: ExternalHostedZone
          - Ref: pHostedZoneId
      Name:
        Fn::Join:
          - ""
          - - registry.
            - Ref: pDnsName
            - "."
            - Ref: pDnsRootDomain
      Type: A
  AnalyticsDnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        DNSName:
          Fn::Join:
            - ""
            - - dualstack.
              - Fn::GetAtt:
                  - ExternalLoadBalancer
                  - DNSName
        EvaluateTargetHealth: false
        HostedZoneId:
          Fn::GetAtt:
            - ExternalLoadBalancer
            - CanonicalHostedZoneID
      HostedZoneId:
        Fn::If:
          - cNewHostedZone
          - Ref: ExternalHostedZone
          - Ref: pHostedZoneId
      Name:
        Fn::Join:
          - ""
          - - analytics.
            - Ref: pDnsName
            - "."
            - Ref: pDnsRootDomain
      Type: A
  CustomRegistrySecretAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for Code Ocean instances to get the custom registry secret
      Path: /
      PolicyDocument:
        Statement:
          - Action: secretsmanager:GetSecretValue
            Effect: Allow
            Resource:
              Ref: pCustomUpstreamDockerRegistrySecret
        Version: "2012-10-17"
      Roles:
        - Ref: JobsInstanceRole
        - Ref: BatchInstanceRole
        - Ref: WorkerInstanceRole
        - Ref: ServicesInstanceRole
    Condition: cPrivateCustomRegistry
  JobsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/jobs/JobsSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  JobsSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: JobsSecurityGroup
      IpProtocol: "-1"
  JobsInstancePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for jobs instances
      Path: /
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
              - s3:PutObject
              - s3:DeleteObject
              - s3:ListBucket
              - s3:GetBucketLocation
              - s3:ListBucketMultipartUploads
              - s3:ListMultipartUploadParts
              - s3:AbortMultipartUpload
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - S3DatasetsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3DatasetsInputBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsInputBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3ResultsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ResultsBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3TempBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3TempBucket
                        - Arn
                    - /*
          - Action:
              - ec2:CreateTags
              - ec2:DescribeInstances
              - ec2:DescribeInstanceTypes
            Effect: Allow
            Resource: "*"
          - Action: cloudformation:DescribeStacks
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:cloudformation:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :stack/
                  - Ref: AWS::StackName
                  - /*
          - Action: secretsmanager:GetSecretValue
            Effect: Allow
            Resource:
              Ref: RedisAuthToken
          - Action: ssm:GetParametersByPath
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:ssm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :parameter/
                  - Ref: AWS::StackName
                  - /*
          - Action:
              - es:ESHttpDelete
              - es:ESHttpGet
              - es:ESHttpHead
              - es:ESHttpPost
              - es:ESHttpPut
              - es:ESHttpPatch
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - OpenSearchDomainV2
                      - Arn
                  - /*
          - Action:
              - elasticfilesystem:ClientMount
              - elasticfilesystem:ClientRootAccess
              - elasticfilesystem:ClientWrite
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - DatasetsEfs
                  - Arn
              - Fn::GetAtt:
                  - ScratchEfs
                  - Arn
          - Action: acm:GetCertificate
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:acm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :certificate/*
          - Action:
              - ecr:BatchGetImage
              - ecr:GetAuthorizationToken
              - ecr:GetDownloadUrlForLayer
              - ecr-public:GetAuthorizationToken
              - sts:GetServiceBearerToken
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
  JobsInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      Description: Allows EC2 jobs instances to call AWS services.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/AmazonSSMManagedInstanceCore
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/CloudWatchAgentServerPolicy
        - Ref: JobsInstancePolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  JobsAssumeRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Resource:
              Ref: pAssumableRoles
        Version: "2012-10-17"
      PolicyName: JobsAssumeRolePolicy
      Roles:
        - Ref: JobsInstanceRole
    Condition: cAssumeRoles
  BatchJobsInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: JobsInstanceRole
  BatchJobsLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 30
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: 100
              VolumeType: gp3
        EbsOptimized: true
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  MIME-Version: 1.0
                  Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="

                  --==MYBOUNDARY==
                  Content-Type: text/x-shellscript; charset="us-ascii"

                  #! /bin/bash
                  set -e

                  #
                  # Init script for private cloud Batch job scheduling instances.
                  #

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  EC2_INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

                  # Get secrets
                  REDIS_AUTH_TOKEN=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /redis/auth-token | jq -r .SecretString)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "JOBS",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "Provider": "AWS",
                      "App": {
                          "Domain": "
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "AWS": {
                          "AccountID": "
                - Ref: AWS::AccountId
                - |-
                  ",
                          "Region": "
                - Ref: AWS::Region
                - |-
                  ",
                          "AssumableRoles": "
                - Fn::Join:
                    - ","
                    - Ref: pAssumableRoles
                - |-
                  ",
                          "Network": {
                              "PrivateSubnets": ["
                - Fn::If:
                    - cNewVpc
                    - Ref: PrivateSubnet1
                    - Ref: pPrivateSubnet1Id
                - '", "'
                - Fn::If:
                    - cNewVpc
                    - Ref: PrivateSubnet2
                    - Ref: pPrivateSubnet2Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone3
                    - Ref: PrivateSubnet3
                    - Ref: pPrivateSubnet3Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone4
                    - Ref: PrivateSubnet4
                    - Ref: pPrivateSubnet4Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone5
                    - Ref: PrivateSubnet5
                    - Ref: pPrivateSubnet5Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone6
                    - Ref: PrivateSubnet6
                    - Ref: pPrivateSubnet6Id
                - |-
                  "]
                          }
                      },
                      "Buckets": {
                          "Capsules": "
                - Ref: S3CapsulesBucket
                - |-
                  ",
                          "Datasets": "
                - Ref: S3DatasetsBucket
                - |-
                  ",
                          "DatasetsInput": "
                - Ref: S3DatasetsInputBucket
                - |-
                  ",
                          "DockerRegistry": "
                - Ref: S3DockerRegistryBucket
                - |-
                  ",
                          "Inputfiles": "
                - Ref: S3InputFilesBucket
                - |-
                  ",
                          "Licenses": "
                - Ref: S3LicensesBucket
                - |-
                  ",
                          "Public": "
                - Ref: S3PublicBucket
                - |-
                  ",
                          "Results": "
                - Ref: S3ResultsBucket
                - |-
                  ",
                          "Scratch": "
                - Ref: S3ScratchBucket
                - |-
                  ",
                          "Temp": "
                - Ref: S3TempBucket
                - |-
                  "
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      },
                      "Services": {
                          "Host": "codeocean.internal",
                          "ComposeNetwork": false
                      },
                      "Elasticsearch": {
                          "Address": "
                - Fn::GetAtt:
                    - OpenSearchDomainV2
                    - DomainEndpoint
                - |-
                  "
                      },
                      "Redis": {
                          "Address": "
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Address
                - |-
                  ",
                          "Port": 
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Port
                - |-
                  ,
                          "Password": "${REDIS_AUTH_TOKEN}"
                      },
                      "Workers": {
                          "General": {
                              "InstanceType": "
                - Ref: pWorkerInstanceType
                - |-
                  "
                          },
                          "GPU": {
                              "InstanceType": "
                - Ref: pGPUWorkerInstanceType
                - |-
                  "
                          }
                      }
                  }
                  EOF


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/jobs/cloud-init"
                  # -- End cloud-init extension scripts --


                  # -x flag is set here because we want to avoid exposing secrets in the cloud-init logs
                  set -x

                  # Propagate stack tags to instance
                  /opt/codeocean/tools/propagate-stack-tags.sh 
                - Ref: AWS::StackName
                - |-2
                   || true

                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/ecs/ecs-agent.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances",
                                          "log_stream_name": "batch/{instance_id}/{ip_address}/ecs-agent"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "jobs/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  systemctl restart amazon-cloudwatch-agent

                  # Mount Datasets EFS
                  mkdir /datasets
                  echo "
                - Ref: DatasetsEfs
                - |-
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  until mount /datasets; do sleep 1; done

                  # Mount Capsule Scratch EFS
                  mkdir /capsule-scratch
                  echo "
                - Ref: ScratchEfs
                - ":/ /capsule-scratch efs _netdev,tls,iam 0 0\" >> /etc/fstab

                  until mount /capsule-scratch; do sleep 1; done


                  chmod 700 /{datasets,capsule-scratch}


                  /usr/lib/codeocean/jobs/systemd.sh


                  _co_run_cloud_init_scripts after

                  --==MYBOUNDARY==--

                  \        "
    DependsOn:
      - OpenSearchDomainV2
  BatchJobsComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        AllocationStrategy: BEST_FIT_PROGRESSIVE
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceRole:
          Ref: BatchJobsInstanceProfile
        InstanceTypes:
          - c7i.large
          - c7i.xlarge
          - c7i.2xlarge
          - c6i.large
          - c6i.xlarge
          - c6i.2xlarge
          - c5.large
          - c5.xlarge
          - c5.2xlarge
        LaunchTemplate:
          LaunchTemplateId:
            Ref: BatchJobsLaunchTemplate
          Version:
            Fn::GetAtt:
              - BatchJobsLaunchTemplate
              - LatestVersionNumber
        MaxvCpus: 256
        MinvCpus: 0
        SecurityGroupIds:
          - Ref: JobsSecurityGroup
        Subnets:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
        Tags:
          Name: codeocean-jobs-batch-instance
        Type: EC2
      Tags:
        codeocean:deployment:
          Ref: AWS::StackName
      Type: MANAGED
  BatchJobsJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment:
            Ref: BatchJobsComputeEnvironment
          Order: 1
      Priority: 1
      Tags:
        codeocean:component: async-jobs
        codeocean:deployment:
          Ref: AWS::StackName
  BatchInstancePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for batch instance
      Path: /
      PolicyDocument:
        Statement:
          - Action:
              - ec2:AttachVolume
              - ec2:CreateTags
              - ec2:CreateVolume
              - ec2:DescribeInstances
              - ec2:DescribeTags
              - ec2:DescribeVolumeAttribute
              - ec2:DescribeVolumes
              - ec2:DescribeVolumeStatus
              - ec2:DetachVolume
              - ec2:ModifyInstanceAttribute
              - ec2:TerminateInstances
            Effect: Allow
            Resource: "*"
          - Action: ec2:DeleteVolume
            Condition:
              StringEquals:
                ec2:ResourceTag/codeocean:deployment:
                  Ref: AWS::StackName
            Effect: Allow
            Resource: "*"
          - Action:
              - autoscaling:DescribeAutoScalingInstances
              - autoscaling:DetachInstances
            Effect: Allow
            Resource: "*"
          - Action:
              - ecs:DescribeContainerInstances
              - ecs:ListClusters
              - ecs:ListContainerInstances
              - ecs:ListTasks
            Effect: Allow
            Resource: "*"
          - Action:
              - elasticfilesystem:ClientMount
              - elasticfilesystem:ClientRootAccess
              - elasticfilesystem:ClientWrite
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - DatasetsEfs
                - Arn
          - Action: cloudformation:DescribeStacks
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:cloudformation:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :stack/
                  - Ref: AWS::StackName
                  - /*
          - Action: acm:GetCertificate
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:acm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :certificate/*
          - Action:
              - ecr:BatchGetImage
              - ecr:GetAuthorizationToken
              - ecr:GetDownloadUrlForLayer
              - ecr-public:GetAuthorizationToken
              - sts:GetServiceBearerToken
            Effect: Allow
            Resource: "*"
          - Action: ssm:GetParametersByPath
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:ssm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :parameter/
                  - Ref: AWS::StackName
                  - /settings/*
        Version: "2012-10-17"
  BatchInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      Description: Allows EC2 instances in a Code Ocean AWS Batch ECS cluster to access ECS and other required AWS services.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/AmazonSSMManagedInstanceCore
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/CloudWatchAgentServerPolicy
        - Ref: BatchInstancePolicy
      Tags:
        - Key: codeocean:component
          Value: pipelines
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  BatchInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: BatchInstanceRole
  BatchSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/pipelines/BatchSecurityGroup
      Tags:
        - Key: codeocean:component
          Value: pipelines
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  BatchSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: BatchSecurityGroup
      IpProtocol: "-1"
  BatchLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 30
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              Iops:
                Ref: pBatchVolumeIops
              Throughput:
                Ref: pBatchVolumeThroughput
              VolumeSize: 300
              VolumeType: gp3
          - DeviceName: /dev/xvdba
            Ebs:
              Encrypted: true
              Iops:
                Ref: pBatchVolumeIops
              Throughput:
                Ref: pBatchVolumeThroughput
              VolumeSize:
                Ref: pBatchVolumeSize
              VolumeType: gp3
        EbsOptimized: true
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  MIME-Version: 1.0
                  Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="

                  --==MYBOUNDARY==
                  Content-Type: text/x-shellscript; charset="us-ascii"

                  #!/bin/bash
                  set -e

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  EC2_INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "PIPELINES",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "AWS": {
                          "Region": "
                - Ref: AWS::Region
                - |-
                  "
                      },
                      "Batch": {
                          "Volume": {
                              "Size": 
                - Ref: pBatchVolumeSize
                - |-
                  ,
                              "Iops": 
                - Ref: pBatchVolumeIops
                - |-
                  ,
                              "Throughput": 
                - Ref: pBatchVolumeThroughput
                - |-2
                  
                          }
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      }
                  }
                  EOF


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/pipeline/cloud-init"
                  # -- End cloud-init extension scripts --


                  # Install cron jobs
                  chown -R root:root /usr/lib/codeocean/pipelines/cron.d
                  mv /usr/lib/codeocean/pipelines/cron.d/* /etc/cron.d/

                  set -x

                  # Propagate stack tags to instance
                  /opt/codeocean/tools/propagate-stack-tags.sh 
                - Ref: AWS::StackName
                - |-2
                   || true

                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /tmp/cloudwatch-logs-config.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/ecs/ecs-agent.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances",
                                          "log_stream_name": "batch/{instance_id}/{ip_address}/ecs-agent"
                                      },
                                      {
                                          "file_path": "/var/log/ebs-autoscale*.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances",
                                          "log_stream_name": "batch/{instance_id}/{ip_address}/ebs-autoscale"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "batch/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -s -c file:/tmp/cloudwatch-logs-config.json

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Mount Datasets EFS
                  mkdir /datasets
                  echo "
                - Ref: DatasetsEfs
                - |
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  until mount /datasets; do sleep 1; done
                  chmod 700 /datasets

                  /usr/lib/codeocean/pipelines/systemd.sh

                  _co_run_cloud_init_scripts after
                  --==MYBOUNDARY==--
  BatchOnDemandComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        AllocationStrategy: BEST_FIT_PROGRESSIVE
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceRole:
          Ref: BatchInstanceProfile
        InstanceTypes:
          Ref: BatchInstanceTypes
        LaunchTemplate:
          LaunchTemplateId:
            Ref: BatchLaunchTemplate
          Version:
            Fn::GetAtt:
              - BatchLaunchTemplate
              - LatestVersionNumber
        MaxvCpus:
          Ref: pBatchMaxvCpus
        MinvCpus: 0
        SecurityGroupIds:
          - Ref: BatchSecurityGroup
        Subnets:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
        Tags:
          Name: codeocean-ondemand-batch-instance
        Type: EC2
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
      Type: MANAGED
  BatchOnDemandJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment:
            Ref: BatchOnDemandComputeEnvironment
          Order: 1
      JobStateTimeLimitActions:
        - Action: CANCEL
          MaxTimeSeconds: 600
          Reason: MISCONFIGURATION:JOB_RESOURCE_REQUIREMENT
          State: RUNNABLE
      Priority: 1
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
  BatchGpuOnDemandComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        AllocationStrategy: BEST_FIT_PROGRESSIVE
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceRole:
          Ref: BatchInstanceProfile
        InstanceTypes:
          Ref: BatcGpuInstanceTypes
        LaunchTemplate:
          LaunchTemplateId:
            Ref: BatchLaunchTemplate
          Version:
            Fn::GetAtt:
              - BatchLaunchTemplate
              - LatestVersionNumber
        MaxvCpus:
          Ref: pBatchMaxvCpus
        MinvCpus: 0
        SecurityGroupIds:
          - Ref: BatchSecurityGroup
        Subnets:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
        Tags:
          Name: codeocean-gpuondemand-batch-instance
        Type: EC2
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
      Type: MANAGED
  BatchGpuOnDemandJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment:
            Ref: BatchGpuOnDemandComputeEnvironment
          Order: 1
      JobStateTimeLimitActions:
        - Action: CANCEL
          MaxTimeSeconds: 600
          Reason: MISCONFIGURATION:JOB_RESOURCE_REQUIREMENT
          State: RUNNABLE
      Priority: 1
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
  BatchSpotComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        AllocationStrategy: SPOT_CAPACITY_OPTIMIZED
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceRole:
          Ref: BatchInstanceProfile
        InstanceTypes:
          Ref: BatchInstanceTypes
        LaunchTemplate:
          LaunchTemplateId:
            Ref: BatchLaunchTemplate
          Version:
            Fn::GetAtt:
              - BatchLaunchTemplate
              - LatestVersionNumber
        MaxvCpus:
          Ref: pBatchMaxvCpus
        MinvCpus: 0
        SecurityGroupIds:
          - Ref: BatchSecurityGroup
        Subnets:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
        Tags:
          Name: codeocean-spot-batch-instance
        Type: SPOT
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
      Type: MANAGED
  BatchSpotJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment:
            Ref: BatchSpotComputeEnvironment
          Order: 1
      JobStateTimeLimitActions:
        - Action: CANCEL
          MaxTimeSeconds: 600
          Reason: MISCONFIGURATION:JOB_RESOURCE_REQUIREMENT
          State: RUNNABLE
      Priority: 1
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
  BatchGpuSpotComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        AllocationStrategy: SPOT_CAPACITY_OPTIMIZED
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceRole:
          Ref: BatchInstanceProfile
        InstanceTypes:
          Ref: BatcGpuInstanceTypes
        LaunchTemplate:
          LaunchTemplateId:
            Ref: BatchLaunchTemplate
          Version:
            Fn::GetAtt:
              - BatchLaunchTemplate
              - LatestVersionNumber
        MaxvCpus:
          Ref: pBatchMaxvCpus
        MinvCpus: 0
        SecurityGroupIds:
          - Ref: BatchSecurityGroup
        Subnets:
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet1
              - Ref: pPrivateSubnet1Id
          - Fn::If:
              - cNewVpc
              - Ref: PrivateSubnet2
              - Ref: pPrivateSubnet2Id
        Tags:
          Name: codeocean-gpuspot-batch-instance
        Type: SPOT
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
      Type: MANAGED
  BatchGpuSpotJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment:
            Ref: BatchGpuSpotComputeEnvironment
          Order: 1
      JobStateTimeLimitActions:
        - Action: CANCEL
          MaxTimeSeconds: 600
          Reason: MISCONFIGURATION:JOB_RESOURCE_REQUIREMENT
          State: RUNNABLE
      Priority: 1
      Tags:
        codeocean:component: pipelines
        codeocean:deployment:
          Ref: AWS::StackName
  BatchJobPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for Batch job
      Path: /
      PolicyDocument:
        Statement:
          - Action: batch:DescribeComputeEnvironments
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - BatchOnDemandComputeEnvironment
                  - ComputeEnvironmentArn
              - Fn::GetAtt:
                  - BatchGpuOnDemandComputeEnvironment
                  - ComputeEnvironmentArn
              - Fn::GetAtt:
                  - BatchSpotComputeEnvironment
                  - ComputeEnvironmentArn
              - Fn::GetAtt:
                  - BatchGpuSpotComputeEnvironment
                  - ComputeEnvironmentArn
          - Action: batch:DescribeJobQueues
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - BatchOnDemandJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchGpuOnDemandJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchSpotJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchGpuSpotJobQueue
                  - JobQueueArn
          - Action:
              - batch:DescribeJobDefinitions
              - batch:DescribeJobs
            Effect: Allow
            Resource: "*"
          - Action: batch:RegisterJobDefinition
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:batch:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :job-definition/nf-*
          - Action:
              - batch:SubmitJob
              - batch:TagResource
            Effect: Allow
            Resource:
              - Fn::Join:
                  - ""
                  - - "arn:aws:batch:"
                    - Ref: AWS::Region
                    - ":"
                    - Ref: AWS::AccountId
                    - :job-definition/nf-*
              - Fn::GetAtt:
                  - BatchOnDemandJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchGpuOnDemandJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchSpotJobQueue
                  - JobQueueArn
              - Fn::GetAtt:
                  - BatchGpuSpotJobQueue
                  - JobQueueArn
          - Action:
              - batch:CancelJob
              - batch:TerminateJob
            Condition:
              StringEquals:
                aws:ResourceTag/codeocean:component: pipelines
                aws:ResourceTag/codeocean:deployment:
                  Ref: AWS::StackName
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:batch:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :job/*
          - Action:
              - s3:GetObject
              - s3:PutObject
              - s3:DeleteObject
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - S3BatchBucket
                      - Arn
                  - /*/*
          - Action: s3:ListBucket
            Condition:
              StringLike:
                s3:prefix: "*/*"
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - S3BatchBucket
                - Arn
          - Action: s3:PutObject
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - S3ResultsBucket
                      - Arn
                  - /*/*/*/*
          - Action: s3:ListBucket
            Condition:
              StringLike:
                s3:prefix: "*/*/*/*"
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - S3ResultsBucket
                - Arn
          - Action: iam:PassRole
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:iam::"
                  - Ref: AWS::AccountId
                  - :role/
                  - Ref: AWS::StackName
                  - -BatchJobRole-*
          - Action: logs:GetLogEvents
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - Fn::GetAtt:
                      - LogGroupPipelines
                      - Arn
                  - :*
        Version: "2012-10-17"
  BatchJobRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Ref: AWS::AccountId
                    - :root
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
        Version: "2012-10-17"
      Description: Allows Code Ocean AWS Batch jobs to access to AWS services.
      ManagedPolicyArns:
        - Ref: BatchJobPolicy
      Tags:
        - Key: codeocean:component
          Value: pipelines
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  WorkersSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/workers/WorkersSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  WorkersSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: WorkersSecurityGroup
      IpProtocol: "-1"
  WorkersSecurityGroupIngressServices:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: worker proxies from services
      FromPort: 8200
      GroupId:
        Ref: WorkersSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 8200
  WorkersSecurityGroupIngressComputations:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: worker runners/computations from services
      FromPort: 10000
      GroupId:
        Ref: WorkersSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 65535
  DefaultAssumableRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Ref: AWS::AccountId
                    - :root
        Version: "2012-10-17"
      Description: Default IAM role for computations
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - bedrock:InvokeAgent
                  - bedrock:InvokeInlineAgent
                  - bedrock:InvokeModel
                  - bedrock:InvokeModelWithResponseStream
                Effect: Allow
                Resource: "*"
            Version: "2012-10-17"
          PolicyName: BedrockAccess
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  WorkerInstancePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for worker instance
      Path: /
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
              - s3:ListBucket
              - s3:PutObject
              - s3:DeleteObject
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - S3DatasetsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3DatasetsBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3InputFilesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3InputFilesBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3LicensesBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3LicensesBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3MLflowBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3MLflowBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3ResultsBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ResultsBucket
                        - Arn
                    - /*
              - Fn::GetAtt:
                  - S3ScratchBucket
                  - Arn
              - Fn::Join:
                  - ""
                  - - Fn::GetAtt:
                        - S3ScratchBucket
                        - Arn
                    - /*
          - Action:
              - ec2:DescribeAvailabilityZones
              - ec2:DescribeInstances
              - ec2:DescribeInstanceStatus
              - ec2:DescribeInstanceTypes
              - ec2:DescribeSpotPriceHistory
            Effect: Allow
            Resource: "*"
          - Action:
              - autoscaling:CompleteLifecycleAction
              - autoscaling:DescribeAutoScalingGroups
              - autoscaling:DescribeAutoScalingInstances
              - autoscaling:DescribeLifecycleHooks
              - autoscaling:RecordLifecycleActionHeartbeat
              - autoscaling:SetInstanceProtection
              - autoscaling:TerminateInstanceInAutoScalingGroup
            Effect: Allow
            Resource: "*"
          - Action:
              - elasticfilesystem:ClientMount
              - elasticfilesystem:ClientRootAccess
              - elasticfilesystem:ClientWrite
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - DatasetsEfs
                  - Arn
              - Fn::GetAtt:
                  - ScratchEfs
                  - Arn
          - Action: secretsmanager:GetSecretValue
            Effect: Allow
            Resource:
              Ref: RedisAuthToken
          - Action: ssm:GetParametersByPath
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:ssm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :parameter/
                  - Ref: AWS::StackName
                  - /*
          - Action: acm:GetCertificate
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:acm:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :certificate/*
          - Action: cloudformation:DescribeStacks
            Effect: Allow
            Resource:
              Fn::Join:
                - ""
                - - "arn:aws:cloudformation:"
                  - Ref: AWS::Region
                  - ":"
                  - Ref: AWS::AccountId
                  - :stack/
                  - Ref: AWS::StackName
                  - /*
          - Action: cloudwatch:PutMetricData
            Effect: Allow
            Resource: "*"
          - Action: sts:AssumeRole
            Effect: Allow
            Resource:
              - Fn::GetAtt:
                  - DefaultAssumableRole
                  - Arn
              - Fn::GetAtt:
                  - BatchJobRole
                  - Arn
          - Action:
              - ec2:AttachVolume
              - ec2:DescribeVolumeStatus
              - ec2:DescribeVolumes
              - ec2:DescribeTags
              - ec2:ModifyInstanceAttribute
              - ec2:DescribeVolumeAttribute
              - ec2:CreateVolume
              - ec2:CreateTags
              - ec2:DetachVolume
            Effect: Allow
            Resource: "*"
          - Action: ec2:DeleteVolume
            Condition:
              StringEquals:
                ec2:ResourceTag/codeocean:deployment:
                  Ref: AWS::StackName
            Effect: Allow
            Resource: "*"
          - Action:
              - batch:DescribeComputeEnvironments
              - batch:DescribeJobQueues
              - batch:DescribeJobs
            Effect: Allow
            Resource: "*"
          - Action: ecs:DescribeContainerInstances
            Effect: Allow
            Resource: "*"
          - Action:
              - ecr:BatchGetImage
              - ecr:GetAuthorizationToken
              - ecr:GetDownloadUrlForLayer
              - ecr-public:GetAuthorizationToken
              - sts:GetServiceBearerToken
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
  WorkerInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      Description: Allows EC2 worker instances to call AWS services.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/AmazonSSMManagedInstanceCore
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/CloudWatchAgentServerPolicy
        - Ref: WorkerInstancePolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  WorkerAssumeRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Resource:
              Ref: pAssumableRoles
        Version: "2012-10-17"
      PolicyName: WorkerAssumeRolePolicy
      Roles:
        - Ref: WorkerInstanceRole
    Condition: cAssumeRoles
  WorkerInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: WorkerInstanceRole
  WorkersLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 30
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: 300
              VolumeType: gp3
        EbsOptimized: true
        IamInstanceProfile:
          Name:
            Ref: WorkerInstanceProfile
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceType:
          Ref: pWorkerInstanceType
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        SecurityGroupIds:
          - Ref: WorkersSecurityGroup
        TagSpecifications:
          - ResourceType: volume
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  #! /bin/bash
                  set -e

                  #
                  # Init script for private cloud EC2 worker instances.
                  #

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  EC2_INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
                  MAC=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/mac)
                  VPC_CIDR_BLOCK=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-ipv4-cidr-block)

                  MACHINE_TYPE=0

                  # Get secrets
                  REDIS_AUTH_TOKEN=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /redis/auth-token | jq -r .SecretString)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "WORKER",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "Provider": "AWS",
                      "App": {
                          "Domain": "
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "AWS": {
                          "AccountID": "
                - Ref: AWS::AccountId
                - |-
                  ",
                          "Region": "
                - Ref: AWS::Region
                - |-
                  ",
                          "DefaultAssumableRole": "
                - Fn::GetAtt:
                    - DefaultAssumableRole
                    - Arn
                - |-
                  "
                      },
                      "Batch": {
                          "JobRole": "
                - Fn::GetAtt:
                    - BatchJobRole
                    - Arn
                - |-
                  ",
                          "JobQueues": {
                              "General": {
                                  "OnDemand": "
                - Ref: BatchOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchSpotJobQueue
                - |-
                  "
                              },
                              "GPU": {
                                  "OnDemand": "
                - Ref: BatchGpuOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchGpuSpotJobQueue
                - |-
                  "
                              }
                          },
                          "LogsGroup": "/
                - Ref: AWS::StackName
                - |-
                  /pipelines"
                      },
                      "Buckets": {
                          "Batch": "
                - Ref: S3BatchBucket
                - |-
                  ",
                          "Datasets": "
                - Ref: S3DatasetsBucket
                - |-
                  ",
                          "Inputfiles": "
                - Ref: S3InputFilesBucket
                - |-
                  ",
                          "Licenses": "
                - Ref: S3LicensesBucket
                - |-
                  ",
                          "MLflow": "
                - Ref: S3MLflowBucket
                - |-
                  ",
                          "Results": "
                - Ref: S3ResultsBucket
                - |-
                  ",
                          "Scratch": "
                - Ref: S3ScratchBucket
                - |-
                  "
                      },
                      "CloudWatch": {
                          "LogGroup": "/
                - Ref: AWS::StackName
                - |-
                  /workers"
                      },
                      "Services": {
                          "Host": "codeocean.internal"
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      },
                      "Redis": {
                          "Address": "
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Address
                - |-
                  ",
                          "Port": 
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Port
                - |-
                  ,
                          "Password": "${REDIS_AUTH_TOKEN}"
                      },
                      "Worker": {
                          "DedicatedMachine": false,
                          "IdleTimeout": 
                - Ref: pAutoScalingIdleTimeout
                - |-
                  ,
                          "InstanceID": "${EC2_INSTANCE_ID}",
                          "MachineType": ${MACHINE_TYPE}
                      },
                      "Workers": {
                          "General": {
                              "InstanceType": "
                - Ref: pWorkerInstanceType
                - |-
                  "
                          },
                          "GPU": {
                              "InstanceType": "
                - Ref: pGPUWorkerInstanceType
                - |-
                  "
                          }
                      }
                  }
                  EOF


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/worker/cloud-init"
                  # -- End cloud-init extension scripts --


                  # -x flag is set here because we want to avoid exposing secrets in the cloud-init logs
                  set -x



                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Configuring iptables rules
                  cat <<EOF > /etc/sysconfig/iptables
                  *filter
                  :DOCKER-USER - [0:0]
                  # Prevent user containers from accessing Amazon EC2 instance metadata
                  -A DOCKER-USER -d 169.254.169.254/32 -j DROP
                  # Prevent user containers from accessing the internal wapi service
                  -A DOCKER-USER -d $VPC_CIDR_BLOCK -p tcp --dport 8201 -j DROP
                  COMMIT

                  *nat
                  # Redirect calls to 169.254.170.2 to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679
                  -A OUTPUT -p TCP -d 169.254.170.2 --dport 80 -j REDIRECT --to-ports 51679

                  # Redirect calls to 169.254.170.3 to 127.0.0.1:51680 (worker nextflow endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.3 --dport 80 -j DNAT --to-destination 127.0.0.1:51680
                  -A OUTPUT -p TCP -d 169.254.170.3 --dport 80 -j REDIRECT --to-ports 51680

                  # Redirect calls to 169.254.169.254 from the computation subnet to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.169.254 --dport 80 -s 192.168.250.0/24 -j DNAT --to-destination 127.0.0.1:51679
                  COMMIT
                  EOF

                  systemctl enable --now iptables

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Configuring systemd unit file for filesystem mounts
                  cat << EOF > /etc/systemd/system/codeocean-mounts.service
                  [Unit]
                  Description=Code Ocean Worker Filesystem Mounts
                  After=local-fs-pre.target
                  Before=codeocean.service
                  DefaultDependencies=no

                  [Service]
                  Type=oneshot
                  ExecStart=/bin/bash -c '! /etc/codeocean/warm-pool &&     /opt/amazon-ebs-autoscale/install.sh         -d /dev/DEVICE         -s 300         -f lvm.xfs         -m /worker         --max-attached-volumes 24         --max-total-created-size 36000         --imdsv2 > /var/log/ebs-autoscale-install.log &&     chmod 700 /worker'

                  [Install]
                  WantedBy=codeocean.service
                  EOF

                  # Set ebs autoscale for /worker mounted volume
                  mkdir /worker
                  sed -i 's/DEVICE/nvme2n1/g' /etc/systemd/system/codeocean-mounts.service
                  systemctl enable codeocean-mounts

                  cat << EOF > /etc/codeocean/warm-pool
                  AUTO_SCALING_INSTANCES=\$(aws autoscaling describe-auto-scaling-instances --instance-ids $EC2_INSTANCE_ID --region 
                - Ref: AWS::Region
                - |-
                  )
                  echo \$AUTO_SCALING_INSTANCES | grep -Fq "Warmed"
                  EOF
                  chmod +x /etc/codeocean/warm-pool

                  # Add EFS mounts
                  mkdir /{datasets,capsule-scratch}
                  echo "
                - Ref: DatasetsEfs
                - |-
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  echo "
                - Ref: ScratchEfs
                - |-
                  :/ /capsule-scratch efs _netdev,tls,iam 0 0" >> /etc/fstab
                  systemctl enable --now mount-efs-sequentially.service
                  chmod 700 /{datasets,capsule-scratch}

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /tmp/cloudwatch-logs-config.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/ebs-autoscale*.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |
                  /instances",
                                          "log_stream_name": "workers/{instance_id}/{ip_address}/ebs-autoscale"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "workers/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -s -c file:/tmp/cloudwatch-logs-config.json

                  /usr/lib/codeocean/worker/systemd.sh

                  _co_run_cloud_init_scripts after
  GPUWorkersLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 30
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: 300
              VolumeType: gp3
          - DeviceName: /dev/sde
            Ebs:
              Encrypted: true
              VolumeSize: 300
              VolumeType: gp3
        EbsOptimized: true
        IamInstanceProfile:
          Name:
            Ref: WorkerInstanceProfile
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceType:
          Ref: pGPUWorkerInstanceType
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        SecurityGroupIds:
          - Ref: WorkersSecurityGroup
        TagSpecifications:
          - ResourceType: volume
            Tags:
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  #! /bin/bash
                  set -e

                  #
                  # Init script for private cloud EC2 worker instances.
                  #

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  EC2_INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
                  MAC=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/mac)
                  VPC_CIDR_BLOCK=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-ipv4-cidr-block)

                  MACHINE_TYPE=1

                  # Get secrets
                  REDIS_AUTH_TOKEN=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /redis/auth-token | jq -r .SecretString)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "WORKER",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "Provider": "AWS",
                      "App": {
                          "Domain": "
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "AWS": {
                          "AccountID": "
                - Ref: AWS::AccountId
                - |-
                  ",
                          "Region": "
                - Ref: AWS::Region
                - |-
                  ",
                          "DefaultAssumableRole": "
                - Fn::GetAtt:
                    - DefaultAssumableRole
                    - Arn
                - |-
                  "
                      },
                      "Batch": {
                          "JobRole": "
                - Fn::GetAtt:
                    - BatchJobRole
                    - Arn
                - |-
                  ",
                          "JobQueues": {
                              "General": {
                                  "OnDemand": "
                - Ref: BatchOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchSpotJobQueue
                - |-
                  "
                              },
                              "GPU": {
                                  "OnDemand": "
                - Ref: BatchGpuOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchGpuSpotJobQueue
                - |-
                  "
                              }
                          },
                          "LogsGroup": "/
                - Ref: AWS::StackName
                - |-
                  /pipelines"
                      },
                      "Buckets": {
                          "Batch": "
                - Ref: S3BatchBucket
                - |-
                  ",
                          "Datasets": "
                - Ref: S3DatasetsBucket
                - |-
                  ",
                          "Inputfiles": "
                - Ref: S3InputFilesBucket
                - |-
                  ",
                          "Licenses": "
                - Ref: S3LicensesBucket
                - |-
                  ",
                          "MLflow": "
                - Ref: S3MLflowBucket
                - |-
                  ",
                          "Results": "
                - Ref: S3ResultsBucket
                - |-
                  ",
                          "Scratch": "
                - Ref: S3ScratchBucket
                - |-
                  "
                      },
                      "CloudWatch": {
                          "LogGroup": "/
                - Ref: AWS::StackName
                - |-
                  /workers"
                      },
                      "Services": {
                          "Host": "codeocean.internal"
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      },
                      "Redis": {
                          "Address": "
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Address
                - |-
                  ",
                          "Port": 
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Port
                - |-
                  ,
                          "Password": "${REDIS_AUTH_TOKEN}"
                      },
                      "Worker": {
                          "DedicatedMachine": false,
                          "IdleTimeout": 
                - Ref: pAutoScalingIdleTimeout
                - |-
                  ,
                          "InstanceID": "${EC2_INSTANCE_ID}",
                          "MachineType": ${MACHINE_TYPE}
                      },
                      "Workers": {
                          "General": {
                              "InstanceType": "
                - Ref: pWorkerInstanceType
                - |-
                  "
                          },
                          "GPU": {
                              "InstanceType": "
                - Ref: pGPUWorkerInstanceType
                - |-
                  "
                          }
                      }
                  }
                  EOF


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/worker/cloud-init"
                  # -- End cloud-init extension scripts --


                  # -x flag is set here because we want to avoid exposing secrets in the cloud-init logs
                  set -x



                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Configuring iptables rules
                  cat <<EOF > /etc/sysconfig/iptables
                  *filter
                  :DOCKER-USER - [0:0]
                  # Prevent user containers from accessing Amazon EC2 instance metadata
                  -A DOCKER-USER -d 169.254.169.254/32 -j DROP
                  # Prevent user containers from accessing the internal wapi service
                  -A DOCKER-USER -d $VPC_CIDR_BLOCK -p tcp --dport 8201 -j DROP
                  COMMIT

                  *nat
                  # Redirect calls to 169.254.170.2 to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679
                  -A OUTPUT -p TCP -d 169.254.170.2 --dport 80 -j REDIRECT --to-ports 51679

                  # Redirect calls to 169.254.170.3 to 127.0.0.1:51680 (worker nextflow endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.3 --dport 80 -j DNAT --to-destination 127.0.0.1:51680
                  -A OUTPUT -p TCP -d 169.254.170.3 --dport 80 -j REDIRECT --to-ports 51680

                  # Redirect calls to 169.254.169.254 from the computation subnet to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.169.254 --dport 80 -s 192.168.250.0/24 -j DNAT --to-destination 127.0.0.1:51679
                  COMMIT
                  EOF

                  systemctl enable --now iptables

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Configuring systemd unit file for filesystem mounts
                  cat << EOF > /etc/systemd/system/codeocean-mounts.service
                  [Unit]
                  Description=Code Ocean Worker Filesystem Mounts
                  After=local-fs-pre.target
                  Before=codeocean.service
                  DefaultDependencies=no

                  [Service]
                  Type=oneshot
                  ExecStart=/bin/bash -c '! /etc/codeocean/warm-pool &&     /opt/amazon-ebs-autoscale/install.sh         -d /dev/DEVICE         -s 300         -f lvm.xfs         -m /worker         --max-attached-volumes 24         --max-total-created-size 36000         --imdsv2 > /var/log/ebs-autoscale-install.log &&     chmod 700 /worker'

                  [Install]
                  WantedBy=codeocean.service
                  EOF

                  # Set ebs autoscale for /worker mounted volume
                  mkdir /worker
                  sed -i 's/DEVICE/sde/g' /etc/systemd/system/codeocean-mounts.service
                  systemctl enable codeocean-mounts

                  cat << EOF > /etc/codeocean/warm-pool
                  AUTO_SCALING_INSTANCES=\$(aws autoscaling describe-auto-scaling-instances --instance-ids $EC2_INSTANCE_ID --region 
                - Ref: AWS::Region
                - |-
                  )
                  echo \$AUTO_SCALING_INSTANCES | grep -Fq "Warmed"
                  EOF
                  chmod +x /etc/codeocean/warm-pool

                  # Add EFS mounts
                  mkdir /{datasets,capsule-scratch}
                  echo "
                - Ref: DatasetsEfs
                - |-
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  echo "
                - Ref: ScratchEfs
                - |-
                  :/ /capsule-scratch efs _netdev,tls,iam 0 0" >> /etc/fstab
                  systemctl enable --now mount-efs-sequentially.service
                  chmod 700 /{datasets,capsule-scratch}

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /tmp/cloudwatch-logs-config.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/ebs-autoscale*.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |
                  /instances",
                                          "log_stream_name": "workers/{instance_id}/{ip_address}/ebs-autoscale"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "workers/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -s -c file:/tmp/cloudwatch-logs-config.json

                  /usr/lib/codeocean/worker/systemd.sh

                  _co_run_cloud_init_scripts after
  WorkersAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -WorkersAutoScalingGroup-
            - Fn::GetAtt:
                - WorkersLaunchTemplate
                - LatestVersionNumber
      Cooldown: "60"
      LaunchTemplate:
        LaunchTemplateId:
          Ref: WorkersLaunchTemplate
        Version:
          Fn::GetAtt:
            - WorkersLaunchTemplate
            - LatestVersionNumber
      LifecycleHookSpecificationList:
        - HeartbeatTimeout: 300
          LifecycleHookName: worker-init-hook
          LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING
      MaxSize:
        Ref: pWorkersAsgMaxSize
      MetricsCollection:
        - Granularity: 1Minute
      MinSize: "0"
      NewInstancesProtectedFromScaleIn: true
      Tags:
        - Key: codeocean:component
          PropagateAtLaunch: true
          Value: workers
        - Key: codeocean:deployment
          PropagateAtLaunch: true
          Value:
            Ref: AWS::StackName
        - Key: Name
          PropagateAtLaunch: true
          Value: codeocean-worker
      VPCZoneIdentifier:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1
            - Ref: pPrivateSubnet1Id
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet2
            - Ref: pPrivateSubnet2Id
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: true
  WorkersAutoScalingWarmPool:
    Type: AWS::AutoScaling::WarmPool
    Properties:
      AutoScalingGroupName:
        Ref: WorkersAutoScalingGroup
      MaxGroupPreparedCapacity: 0
      MinSize: 0
  IdleWorkersScaleInPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AdjustmentType: ChangeInCapacity
      AutoScalingGroupName:
        Ref: WorkersAutoScalingGroup
      PolicyType: StepScaling
      StepAdjustments:
        - MetricIntervalLowerBound: 0
          ScalingAdjustment: -1
  GPUWorkersAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -GPUWorkersAutoScalingGroup-
            - Fn::GetAtt:
                - GPUWorkersLaunchTemplate
                - LatestVersionNumber
      Cooldown: "60"
      LaunchTemplate:
        LaunchTemplateId:
          Ref: GPUWorkersLaunchTemplate
        Version:
          Fn::GetAtt:
            - GPUWorkersLaunchTemplate
            - LatestVersionNumber
      LifecycleHookSpecificationList:
        - HeartbeatTimeout: 300
          LifecycleHookName: worker-init-hook
          LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING
      MaxSize:
        Ref: pGpuWorkersAsgMaxSize
      MetricsCollection:
        - Granularity: 1Minute
      MinSize: "0"
      NewInstancesProtectedFromScaleIn: true
      Tags:
        - Key: codeocean:component
          PropagateAtLaunch: true
          Value: workers
        - Key: codeocean:deployment
          PropagateAtLaunch: true
          Value:
            Ref: AWS::StackName
        - Key: Name
          PropagateAtLaunch: true
          Value: codeocean-worker-gpu
      VPCZoneIdentifier:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1
            - Ref: pPrivateSubnet1Id
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet2
            - Ref: pPrivateSubnet2Id
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: true
  GPUWorkersAutoScalingWarmPool:
    Type: AWS::AutoScaling::WarmPool
    Properties:
      AutoScalingGroupName:
        Ref: GPUWorkersAutoScalingGroup
      MaxGroupPreparedCapacity: 0
      MinSize: 0
  GPUIdleWorkersScaleInPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AdjustmentType: ChangeInCapacity
      AutoScalingGroupName:
        Ref: GPUWorkersAutoScalingGroup
      PolicyType: StepScaling
      StepAdjustments:
        - MetricIntervalLowerBound: 0
          ScalingAdjustment: -1
  IdleWorkersHighAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: IdleWorkersScaleInPolicy
      AlarmDescription: Number of idle workers is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 6
      EvaluationPeriods: 6
      Metrics:
        - Expression: (m1/10)
          Id: "e1"
          Label: IdleWorkers
          ReturnData: true
        - Id: m1
          MetricStat:
            Metric:
              Dimensions:
                - Name: AutoScalingGroupName
                  Value:
                    Ref: WorkersAutoScalingGroup
              MetricName: Idle
              Namespace: CodeOcean
            Period: 10
            Stat: Sum
          ReturnData: false
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 0
  GPUIdleWorkersHighAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: GPUIdleWorkersScaleInPolicy
      AlarmDescription: Number of available workers is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 6
      EvaluationPeriods: 6
      Metrics:
        - Expression: (m1/10)
          Id: "e1"
          Label: IdleWorkers
          ReturnData: true
        - Id: m1
          MetricStat:
            Metric:
              Dimensions:
                - Name: AutoScalingGroupName
                  Value:
                    Ref: GPUWorkersAutoScalingGroup
              MetricName: Idle
              Namespace: CodeOcean
            Period: 10
            Stat: Sum
          ReturnData: false
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 0
  DedicatedMachinesLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 8
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: 300
              VolumeType: gp3
          - DeviceName: /dev/sdg
            Ebs:
              Encrypted: true
              VolumeSize: 300
              VolumeType: gp3
        EbsOptimized: true
        IamInstanceProfile:
          Name:
            Ref: WorkerInstanceProfile
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceType:
          Ref: pWorkerInstanceType
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: codeocean-dedicated-worker
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: Name
                Value: codeocean-dedicated-worker
              - Key: codeocean:deployment
                Value:
                  Ref: AWS::StackName
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  #! /bin/bash
                  set -e

                  #
                  # Init script for private cloud EC2 worker instances.
                  #

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  EC2_INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
                  MAC=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/mac)
                  VPC_CIDR_BLOCK=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-ipv4-cidr-block)

                  MACHINE_TYPE=$(aws ec2 describe-tags --region 
                - Ref: AWS::Region
                - |-2
                   --filters "Name=resource-id,Values=$EC2_INSTANCE_ID" | jq -r '.Tags[] | select(.Key=="codeocean.com/machine_type") | .Value')

                  # Get secrets
                  REDIS_AUTH_TOKEN=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /redis/auth-token | jq -r .SecretString)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "WORKER",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "Provider": "AWS",
                      "App": {
                          "Domain": "
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "AWS": {
                          "AccountID": "
                - Ref: AWS::AccountId
                - |-
                  ",
                          "Region": "
                - Ref: AWS::Region
                - |-
                  ",
                          "DefaultAssumableRole": "
                - Fn::GetAtt:
                    - DefaultAssumableRole
                    - Arn
                - |-
                  "
                      },
                      "Batch": {
                          "JobRole": "
                - Fn::GetAtt:
                    - BatchJobRole
                    - Arn
                - |-
                  ",
                          "JobQueues": {
                              "General": {
                                  "OnDemand": "
                - Ref: BatchOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchSpotJobQueue
                - |-
                  "
                              },
                              "GPU": {
                                  "OnDemand": "
                - Ref: BatchGpuOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchGpuSpotJobQueue
                - |-
                  "
                              }
                          },
                          "LogsGroup": "/
                - Ref: AWS::StackName
                - |-
                  /pipelines"
                      },
                      "Buckets": {
                          "Batch": "
                - Ref: S3BatchBucket
                - |-
                  ",
                          "Datasets": "
                - Ref: S3DatasetsBucket
                - |-
                  ",
                          "Inputfiles": "
                - Ref: S3InputFilesBucket
                - |-
                  ",
                          "Licenses": "
                - Ref: S3LicensesBucket
                - |-
                  ",
                          "MLflow": "
                - Ref: S3MLflowBucket
                - |-
                  ",
                          "Results": "
                - Ref: S3ResultsBucket
                - |-
                  ",
                          "Scratch": "
                - Ref: S3ScratchBucket
                - |-
                  "
                      },
                      "CloudWatch": {
                          "LogGroup": "/
                - Ref: AWS::StackName
                - |-
                  /workers"
                      },
                      "Services": {
                          "Host": "codeocean.internal"
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      },
                      "Redis": {
                          "Address": "
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Address
                - |-
                  ",
                          "Port": 
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Port
                - |-
                  ,
                          "Password": "${REDIS_AUTH_TOKEN}"
                      },
                      "Worker": {
                          "DedicatedMachine": true,
                          "IdleTimeout": 
                - Ref: pAutoScalingIdleTimeout
                - |-
                  ,
                          "InstanceID": "${EC2_INSTANCE_ID}",
                          "MachineType": ${MACHINE_TYPE}
                      },
                      "Workers": {
                          "General": {
                              "InstanceType": "
                - Ref: pWorkerInstanceType
                - |-
                  "
                          },
                          "GPU": {
                              "InstanceType": "
                - Ref: pGPUWorkerInstanceType
                - |-
                  "
                          }
                      }
                  }
                  EOF


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/worker/cloud-init"
                  # -- End cloud-init extension scripts --


                  # -x flag is set here because we want to avoid exposing secrets in the cloud-init logs
                  set -x

                  # Propagate stack tags to instance
                  /opt/codeocean/tools/propagate-stack-tags.sh 
                - Ref: AWS::StackName
                - |-2
                   || true

                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Configuring iptables rules
                  cat <<EOF > /etc/sysconfig/iptables
                  *filter
                  :DOCKER-USER - [0:0]
                  # Prevent user containers from accessing Amazon EC2 instance metadata
                  -A DOCKER-USER -d 169.254.169.254/32 -j DROP
                  # Prevent user containers from accessing the internal wapi service
                  -A DOCKER-USER -d $VPC_CIDR_BLOCK -p tcp --dport 8201 -j DROP
                  COMMIT

                  *nat
                  # Redirect calls to 169.254.170.2 to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679
                  -A OUTPUT -p TCP -d 169.254.170.2 --dport 80 -j REDIRECT --to-ports 51679

                  # Redirect calls to 169.254.170.3 to 127.0.0.1:51680 (worker nextflow endpoint)
                  -A PREROUTING -p TCP -d 169.254.170.3 --dport 80 -j DNAT --to-destination 127.0.0.1:51680
                  -A OUTPUT -p TCP -d 169.254.170.3 --dport 80 -j REDIRECT --to-ports 51680

                  # Redirect calls to 169.254.169.254 from the computation subnet to 127.0.0.1:51679 (worker credentials endpoint)
                  -A PREROUTING -p TCP -d 169.254.169.254 --dport 80 -s 192.168.250.0/24 -j DNAT --to-destination 127.0.0.1:51679
                  COMMIT
                  EOF

                  systemctl enable --now iptables

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Configuring systemd unit file for filesystem mounts
                  cat << EOF > /etc/systemd/system/codeocean-mounts.service
                  [Unit]
                  Description=Code Ocean Worker Filesystem Mounts
                  After=local-fs-pre.target
                  Before=codeocean.service
                  DefaultDependencies=no

                  [Service]
                  Type=oneshot
                  ExecStart=/bin/bash -c '! /etc/codeocean/warm-pool &&     /opt/amazon-ebs-autoscale/install.sh         -d /dev/DEVICE         -s 300         -f lvm.xfs         -m /worker         --max-attached-volumes 24         --max-total-created-size 36000         --imdsv2 > /var/log/ebs-autoscale-install.log &&     chmod 700 /worker'

                  [Install]
                  WantedBy=codeocean.service
                  EOF

                  # Set ebs autoscale for /worker mounted volume
                  mkdir /worker
                  sed -i 's/DEVICE/sdg/g' /etc/systemd/system/codeocean-mounts.service
                  systemctl enable codeocean-mounts

                  cat << EOF > /etc/codeocean/warm-pool
                  AUTO_SCALING_INSTANCES=\$(aws autoscaling describe-auto-scaling-instances --instance-ids $EC2_INSTANCE_ID --region 
                - Ref: AWS::Region
                - |-
                  )
                  echo \$AUTO_SCALING_INSTANCES | grep -Fq "Warmed"
                  EOF
                  chmod +x /etc/codeocean/warm-pool

                  # Add EFS mounts
                  mkdir /{datasets,capsule-scratch}
                  echo "
                - Ref: DatasetsEfs
                - |-
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  echo "
                - Ref: ScratchEfs
                - |-
                  :/ /capsule-scratch efs _netdev,tls,iam 0 0" >> /etc/fstab
                  systemctl enable --now mount-efs-sequentially.service
                  chmod 700 /{datasets,capsule-scratch}

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /tmp/cloudwatch-logs-config.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/ebs-autoscale*.log",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |
                  /instances",
                                          "log_stream_name": "workers/{instance_id}/{ip_address}/ebs-autoscale"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "workers/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -s -c file:/tmp/cloudwatch-logs-config.json

                  /usr/lib/codeocean/worker/systemd.sh

                  _co_run_cloud_init_scripts after
  WorkersAvailableSlotsScaleOutPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AdjustmentType: ChangeInCapacity
      AutoScalingGroupName:
        Ref: WorkersAutoScalingGroup
      EstimatedInstanceWarmup: 120
      PolicyType: StepScaling
      StepAdjustments:
        - MetricIntervalUpperBound: 0
          ScalingAdjustment: 1
  WorkersAvailableSlotsLowAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: WorkersAvailableSlotsScaleOutPolicy
      AlarmDescription: No workers available to execute computation requests
      ComparisonOperator: GreaterThanOrEqualToThreshold
      DatapointsToAlarm: 1
      Dimensions:
        - Name: MachineType
          Value: "0"
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
      EvaluationPeriods: 1
      MetricName: OverloadStatus
      Namespace: CodeOcean
      Period: 10
      Statistic: Minimum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 1
  GPUWorkersAvailableSlotsScaleOutPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AdjustmentType: ChangeInCapacity
      AutoScalingGroupName:
        Ref: GPUWorkersAutoScalingGroup
      EstimatedInstanceWarmup: 120
      PolicyType: StepScaling
      StepAdjustments:
        - MetricIntervalUpperBound: 0
          ScalingAdjustment: 1
  GPUWorkersAvailableSlotsLowAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: GPUWorkersAvailableSlotsScaleOutPolicy
      AlarmDescription: No workers available to execute computation requests
      ComparisonOperator: GreaterThanOrEqualToThreshold
      DatapointsToAlarm: 1
      Dimensions:
        - Name: MachineType
          Value: "1"
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
      EvaluationPeriods: 1
      MetricName: OverloadStatus
      Namespace: CodeOcean
      Period: 10
      Statistic: Minimum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 1
  ServicesSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: codeocean/services/ServicesSecurityGroup
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      VpcId:
        Fn::If:
          - cNewVpc
          - Ref: Vpc
          - Ref: pVpcId
  ServicesSecurityGroupEgressDefault:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      CidrIp: 0.0.0.0/0
      Description: Allow all outbound traffic by default
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: "-1"
  ServicesSecurityGroupIngressSelf:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: self
      FromPort: 0
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ServicesSecurityGroup
      ToPort: 65535
  ServicesSecurityGroupIngressGitProxy:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: git-proxy
      FromPort: 3050
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 3050
  ServicesSecurityGroupIngressRegistry:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: registry
      FromPort: 5000
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 5000
  ServicesSecurityGroupIngressWeb:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: web
      FromPort: 8001
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8001
  ServicesSecurityGroupIngressGW:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: gw
      FromPort: 8080
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8080
  ServicesSecurityGroupIngressCWProxy:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: cw-proxy
      FromPort: 8114
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8114
  ServicesSecurityGroupIngressAssets:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: assets
      FromPort: 8120
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8120
  ServicesSecurityGroupIngressAPIService:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: api-service
      FromPort: 8400
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8400
  ServicesSecurityGroupIngressFileProxy:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: file-proxy
      FromPort: 8504
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8504
  ServicesSecurityGroupIngressAnalytics:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: analytics
      FromPort: 8888
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 8888
  ServicesSecurityGroupIngressScim:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: scim
      FromPort: 9901
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: ExternalSecurityGroup
      ToPort: 9901
  ServicesSecurityGroupIngressJobs:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: jobs
      FromPort: 0
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: JobsSecurityGroup
      ToPort: 65535
  ServicesSecurityGroupIngressRCache:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: rcache from workers
      FromPort: 8700
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 8700
  ServicesSecurityGroupIngressWapi:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: wapi from workers
      FromPort: 8201
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 8201
  ServicesSecurityGroupIngressWDT:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: wdt from workers
      FromPort: 20000
      GroupId:
        Ref: ServicesSecurityGroup
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: WorkersSecurityGroup
      ToPort: 30000
  ServicesInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      Description: Allows EC2 services instances to call AWS services.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/AmazonSSMManagedInstanceCore
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/CloudWatchAgentServerPolicy
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                  - s3:ListBucket
                  - s3:GetBucketLocation
                  - s3:ListBucketMultipartUploads
                  - s3:ListBucketVersions
                  - s3:ListMultipartUploadParts
                  - s3:AbortMultipartUpload
                Effect: Allow
                Resource:
                  - Fn::GetAtt:
                      - S3BatchBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3BatchBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3CapsulesBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3CapsulesBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3DatasetsBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3DatasetsBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3DatasetsInputBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3DatasetsInputBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3InputFilesBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3InputFilesBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3LicensesBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3LicensesBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3MLflowBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3MLflowBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3PackagesBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3PackagesBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3PublicBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3PublicBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3DockerRegistryBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3DockerRegistryBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3ResultsBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3ResultsBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3ScratchBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3ScratchBucket
                            - Arn
                        - /*
                  - Fn::GetAtt:
                      - S3TempBucket
                      - Arn
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - S3TempBucket
                            - Arn
                        - /*
              - Action: s3:GetObject
                Effect: Allow
                Resource: arn:aws:s3:::codeocean-analytics/*
            Version: "2012-10-17"
          PolicyName: S3Policy
        - PolicyDocument:
            Statement:
              - Action:
                  - ec2:AttachVolume
                  - ec2:DescribeInstances
                  - ec2:DescribeInstanceTypes
                  - ec2:DescribeVolumes
                  - ec2:DescribeSubnets
                Effect: Allow
                Resource: "*"
              - Action: ec2:DeleteVolume
                Condition:
                  StringEquals:
                    ec2:ResourceTag/codeocean:deployment:
                      Ref: AWS::StackName
                Effect: Allow
                Resource: "*"
              - Action:
                  - autoscaling:DescribeAutoScalingInstances
                  - autoscaling:DescribeAutoScalingGroups
                Effect: Allow
                Resource: "*"
              - Action:
                  - autoscaling:PutWarmPool
                  - autoscaling:SetInstanceProtection
                  - autoscaling:UpdateAutoScalingGroup
                Effect: Allow
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:aws:autoscaling:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :autoScalingGroup:*:autoScalingGroupName/
                        - Ref: WorkersAutoScalingGroup
                  - Fn::Join:
                      - ""
                      - - "arn:aws:autoscaling:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :autoScalingGroup:*:autoScalingGroupName/
                        - Ref: GPUWorkersAutoScalingGroup
            Version: "2012-10-17"
          PolicyName: EC2Policy
        - PolicyDocument:
            Statement:
              - Action:
                  - elasticfilesystem:ClientMount
                  - elasticfilesystem:ClientRootAccess
                  - elasticfilesystem:ClientWrite
                Effect: Allow
                Resource:
                  - Fn::GetAtt:
                      - DatasetsEfs
                      - Arn
                  - Fn::GetAtt:
                      - ScratchEfs
                      - Arn
              - Action: secretsmanager:GetSecretValue
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:secretsmanager:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :secret:/
                      - Ref: AWS::StackName
                      - /*
              - Action:
                  - ssm:GetParametersByPath
                  - ssm:PutParameter
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:ssm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :parameter/
                      - Ref: AWS::StackName
                      - /*
              - Action: acm:GetCertificate
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:acm:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :certificate/*
              - Action:
                  - es:ESHttpDelete
                  - es:ESHttpGet
                  - es:ESHttpHead
                  - es:ESHttpPost
                  - es:ESHttpPut
                  - es:ESHttpPatch
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - Fn::GetAtt:
                          - OpenSearchDomainV2
                          - Arn
                      - /*
              - Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetRepositoryPolicy
                  - ecr:DescribeRepositories
                  - ecr:ListImages
                  - ecr:DescribeImages
                  - ecr:BatchGetImage
                  - ecr:GetLifecyclePolicy
                  - ecr:GetLifecyclePolicyPreview
                  - ecr:ListTagsForResource
                  - ecr:DescribeImageScanFindings
                  - ecr-public:GetAuthorizationToken
                  - sts:GetServiceBearerToken
                Effect: Allow
                Resource: "*"
              - Action:
                  - logs:FilterLogEvents
                  - logs:GetLogEvents
                Effect: Allow
                Resource:
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - LogGroupInstances
                            - Arn
                        - :*
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":logs:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :log-group:/
                        - Ref: AWS::StackName
                        - /lambda:*:*
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - LogGroupPipelines
                            - Arn
                        - :*
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - LogGroupServices
                            - Arn
                        - :*
                  - Fn::Join:
                      - ""
                      - - Fn::GetAtt:
                            - LogGroupWorkers
                            - Arn
                        - :*
              - Action:
                  - route53:ChangeResourceRecordSets
                  - route53:ListResourceRecordSets
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - arn:aws:route53:::hostedzone/
                      - Ref: InternalHostedZone
            Version: "2012-10-17"
          PolicyName: GeneralPolicy
        - PolicyDocument:
            Statement:
              - Action: sts:AssumeRole
                Effect: Allow
                Resource:
                  Fn::GetAtt:
                    - BatchJobRole
                    - Arn
              - Action:
                  - batch:DescribeComputeEnvironments
                  - batch:DescribeJobDefinitions
                  - batch:DescribeJobQueues
                  - batch:DescribeJobs
                  - batch:ListJobs
                Effect: Allow
                Resource: "*"
              - Action:
                  - batch:DeregisterJobDefinition
                  - batch:RegisterJobDefinition
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:batch:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :job-definition/
                      - Ref: AWS::StackName
                      - -*
              - Action:
                  - batch:SubmitJob
                  - batch:TagResource
                Effect: Allow
                Resource:
                  - Fn::GetAtt:
                      - BatchJobsJobQueue
                      - JobQueueArn
                  - Fn::Join:
                      - ""
                      - - "arn:aws:batch:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :job-definition/
                        - Ref: AWS::StackName
                        - -*
              - Action: batch:CancelJob
                Condition:
                  StringEquals:
                    aws:ResourceTag/codeocean:component:
                      - pipelines
                      - async-jobs
                    aws:ResourceTag/codeocean:deployment:
                      Ref: AWS::StackName
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:batch:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :job/*
            Version: "2012-10-17"
          PolicyName: BatchPolicy
        - PolicyDocument:
            Statement:
              - Action: pricing:GetProducts
                Effect: Allow
                Resource: "*"
              - Action:
                  - ec2:DescribeInstances
                  - ec2:DescribeInstanceTypes
                  - ec2:DescribeInstanceTypeOfferings
                  - ec2:DescribeSpotInstanceRequests
                  - ec2:CancelSpotInstanceRequests
                Effect: Allow
                Resource: "*"
              - Action: iam:PassRole
                Effect: Allow
                Resource:
                  Fn::GetAtt:
                    - WorkerInstanceRole
                    - Arn
              - Action: ec2:RunInstances
                Effect: Allow
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ::image/
                        - Fn::If:
                            - cIsCustomAmi
                            - Ref: pCustomAmi
                            - Fn::FindInMap:
                                - AMIs
                                - Ref: AWS::Region
                                - id
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :instance/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :launch-template/
                        - Ref: DedicatedMachinesLaunchTemplate
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - :*:network-interface/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :security-group/
                        - Ref: WorkersSecurityGroup
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - :*:subnet/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :volume/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :spot-instances-request/*
              - Action: ec2:CreateTags
                Condition:
                  StringEquals:
                    ec2:CreateAction: RunInstances
                  ForAllValues:StringEquals:
                    aws:TagKeys:
                      - Name
                      - codeocean:deployment
                      - codeocean.com/user
                      - codeocean.com/capsule
                      - codeocean.com/machine_type
                Effect: Allow
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :instance/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :volume/*
                  - Fn::Join:
                      - ""
                      - - "arn:aws:ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :spot-instances-request/*
              - Action: ec2:TerminateInstances
                Condition:
                  StringEquals:
                    ec2:ResourceTag/codeocean:deployment:
                      Ref: AWS::StackName
                    ec2:ResourceTag/Name: codeocean-dedicated-worker
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:aws:ec2:"
                      - Ref: AWS::Region
                      - ":"
                      - Ref: AWS::AccountId
                      - :instance/*
            Version: "2012-10-17"
          PolicyName: DedicatedMachinesPolicy
        - PolicyDocument:
            Statement:
              - Action:
                  - bedrock:InvokeModel
                  - bedrock:InvokeModelWithResponseStream
                Effect: Allow
                Resource: "*"
              - Action:
                  - aws-marketplace:Subscribe
                  - aws-marketplace:Unsubscribe
                Condition:
                  ForAllValues:StringEquals:
                    aws-marketplace:ProductId:
                      - prod-mxcfnwvpd6kb4
                Effect: Allow
                Resource: "*"
              - Action: aws-marketplace:ViewSubscriptions
                Effect: Allow
                Resource: "*"
            Version: "2012-10-17"
          PolicyName: BedrockPolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  ServicesAssumeRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Resource:
              Ref: pAssumableRoles
        Version: "2012-10-17"
      PolicyName: ServicesAssumeRolePolicy
      Roles:
        - Ref: ServicesInstanceRole
    Condition: cAssumeRoles
  ServicesInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: ServicesInstanceRole
  DataVolume:
    Type: AWS::EC2::Volume
    Properties:
      AvailabilityZone:
        Fn::If:
          - cNewVpc
          - Ref: pNewVpcAvailabilityZone1
          - Ref: pAvailabilityZone1
      Encrypted: true
      Size: 500
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
        - Key: Name
          Value:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - -data
      Throughput: 250
      VolumeType: gp3
  ServicesLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              Encrypted: true
              VolumeSize: 30
              VolumeType: gp3
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: 100
              VolumeType: gp3
        EbsOptimized: true
        IamInstanceProfile:
          Name:
            Ref: ServicesInstanceProfile
        ImageId:
          Fn::If:
            - cIsCustomAmi
            - Ref: pCustomAmi
            - Fn::FindInMap:
                - AMIs
                - Ref: AWS::Region
                - id
        InstanceType:
          Fn::FindInMap:
            - SystemCapacityConfiguration
            - services
            - Ref: pSystemCapacityTier
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 2
          HttpTokens: required
        SecurityGroupIds:
          - Ref: ServicesSecurityGroup
        UserData:
          Fn::Base64:
            Fn::Join:
              - ""
              - - |-
                  #! /bin/bash
                  set -e

                  #
                  # Init script for private cloud EC2 services instances.
                  #

                  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                  MAC=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/mac)
                  VPC_CIDR_BLOCK=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-ipv4-cidr-block)

                  # Get secrets
                  ANALYTICS_DB_MASTER_PASSWORD=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /analytics/master-password-0 | jq -r .SecretString)
                  REDIS_AUTH_TOKEN=$(aws secretsmanager get-secret-value --region 
                - Ref: AWS::Region
                - " --secret-id /"
                - Ref: AWS::StackName
                - |-
                  /redis/auth-token | jq -r .SecretString)

                  # Set the stack info
                  cat << EOF > /etc/codeocean/stack.json
                  {
                      "MachineType": "SERVICES",
                      "Stack": "
                - Ref: AWS::StackName
                - |-
                  ",
                      "Provider": "AWS",
                      "AnalyticsDB": {
                          "Host": "
                - Fn::GetAtt:
                    - AnalyticsDBInstance
                    - Endpoint.Address
                - |-
                  ",
                          "Port": "
                - Fn::GetAtt:
                    - AnalyticsDBInstance
                    - Endpoint.Port
                - |-
                  ",
                          "Username": "root",
                          "Password": "${ANALYTICS_DB_MASTER_PASSWORD}"
                      },
                      "App": {
                          "Domain": "
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "AWS": {
                          "AccountID": "
                - Ref: AWS::AccountId
                - |-
                  ",
                          "Bedrock": {
                              "ModelID": "
                - Fn::FindInMap:
                    - RegionalConfigJsonMapping
                    - Ref: AWS::Region
                    - BedrockModelId
                - |-
                  "
                          },
                          "Region": "
                - Ref: AWS::Region
                - |-
                  ",
                          "AssumableRoles": "
                - Fn::Join:
                    - ","
                    - Ref: pAssumableRoles
                - |-
                  ",
                          "Jobs": {
                              "JobQueue": "
                - Ref: BatchJobsJobQueue
                - |-
                  "
                          },
                          "Network": {
                              "PrivateSubnets": ["
                - Fn::If:
                    - cNewVpc
                    - Ref: PrivateSubnet1
                    - Ref: pPrivateSubnet1Id
                - '", "'
                - Fn::If:
                    - cNewVpc
                    - Ref: PrivateSubnet2
                    - Ref: pPrivateSubnet2Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone3
                    - Ref: PrivateSubnet3
                    - Ref: pPrivateSubnet3Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone4
                    - Ref: PrivateSubnet4
                    - Ref: pPrivateSubnet4Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone5
                    - Ref: PrivateSubnet5
                    - Ref: pPrivateSubnet5Id
                - '", "'
                - Fn::If:
                    - cNewAvailabilityZone6
                    - Ref: PrivateSubnet6
                    - Ref: pPrivateSubnet6Id
                - |-
                  "]
                          },
                          "SecurityGroups": {
                              "Workers": "
                - Ref: WorkersSecurityGroup
                - |-
                  "
                          },
                          "BackupVault": "
                - Fn::GetAtt:
                    - BackupVault
                    - BackupVaultArn
                - |-
                  ",
                          "S3BackupRole": "
                - Fn::If:
                    - cS3Backup
                    - Fn::GetAtt:
                        - S3BackupRole
                        - Arn
                    - ""
                - |-
                  "
                      },
                      "Batch": {
                          "JobRole": "
                - Fn::GetAtt:
                    - BatchJobRole
                    - Arn
                - |-
                  ",
                          "JobQueues": {
                              "General": {
                                  "OnDemand": "
                - Ref: BatchOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchSpotJobQueue
                - |-
                  "
                              },
                              "GPU": {
                                  "OnDemand": "
                - Ref: BatchGpuOnDemandJobQueue
                - |-
                  ",
                                  "Spot": "
                - Ref: BatchGpuSpotJobQueue
                - |-
                  "
                              }
                          },
                          "LogsGroup": "/
                - Ref: AWS::StackName
                - |-
                  /pipelines"
                      },
                      "Buckets": {
                          "Batch": "
                - Ref: S3BatchBucket
                - |-
                  ",
                          "Capsules": "
                - Ref: S3CapsulesBucket
                - |-
                  ",
                          "Datasets": "
                - Ref: S3DatasetsBucket
                - |-
                  ",
                          "DatasetsInput": "
                - Ref: S3DatasetsInputBucket
                - |-
                  ",
                          "DockerRegistry": "
                - Ref: S3DockerRegistryBucket
                - |-
                  ",
                          "Inputfiles": "
                - Ref: S3InputFilesBucket
                - |-
                  ",
                          "Licenses": "
                - Ref: S3LicensesBucket
                - |-
                  ",
                          "MLflow": "
                - Ref: S3MLflowBucket
                - |-
                  ",
                          "Packages": "
                - Ref: S3PackagesBucket
                - |-
                  ",
                          "Public": "
                - Ref: S3PublicBucket
                - |-
                  ",
                          "Results": "
                - Ref: S3ResultsBucket
                - |-
                  ",
                          "Scratch": "
                - Ref: S3ScratchBucket
                - |-
                  ",
                          "Temp": "
                - Ref: S3TempBucket
                - |-
                  "
                      },
                      "CloudWatch": {
                          "LogGroup": "/
                - Ref: AWS::StackName
                - |-
                  /services"
                      },
                      "DedicatedMachines": {
                          "LaunchTemplateID": "
                - Ref: DedicatedMachinesLaunchTemplate
                - |-
                  ",
                          "LaunchTemplateVersion": "
                - Fn::GetAtt:
                    - DedicatedMachinesLaunchTemplate
                    - LatestVersionNumber
                - |-
                  "
                      },
                      "DockerRegistry": {
                          "Host": "registry.
                - Ref: pDnsName
                - "."
                - Ref: pDnsRootDomain
                - |-
                  "
                      },
                      "UpstreamDockerRegistry": {
                          "Endpoint": "
                - Fn::If:
                    - cCustomRegistry
                    - Ref: pCustomUpstreamDockerRegistryEndpoint
                    - public.ecr.aws/codeocean
                - |-
                  ",
                          "SecretArn": "
                - Ref: pCustomUpstreamDockerRegistrySecret
                - |-
                  "
                      },
                      "Services": {
                          "Host": "codeocean.internal",
                          "ComposeNetwork": true
                      },
                      "Elasticsearch": {
                          "Address": "
                - Fn::GetAtt:
                    - OpenSearchDomainV2
                    - DomainEndpoint
                - |-
                  "
                      },
                      "Redis": {
                          "Address": "
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Address
                - |-
                  ",
                          "Port": 
                - Fn::GetAtt:
                    - RedisReplicationGroup
                    - PrimaryEndPoint.Port
                - |-
                  ,
                          "Password": "${REDIS_AUTH_TOKEN}"
                      },
                      "Workers": {
                          "General": {
                              "InstanceType": "
                - Ref: pWorkerInstanceType
                - |-
                  "
                          },
                          "GPU": {
                              "InstanceType": "
                - Ref: pGPUWorkerInstanceType
                - |-
                  "
                          }
                      }
                  }
                  EOF

                  function get_metadata() {
                      token=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
                      curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/$1
                  }

                  EC2_INSTANCE_ID=$(get_metadata instance-id)
                  EC2_INSTANCE_IP=$(get_metadata local-ipv4)


                  # -- Cloud-init extension scripts --
                  CO_CLOUDINIT_DIR=$(mktemp -d)
                  _co_fetch_cloud_init_scripts() {
                      local ssm_path="$1"
                      local scripts_json
                      scripts_json=$(aws ssm get-parameters-by-path \
                          --region 
                - Ref: AWS::Region
                - |-2
                   --path "$ssm_path" --recursive \
                          --query 'Parameters[*].{Name:Name,Value:Value}' \
                          --output json 2>/dev/null) || scripts_json="[]"

                      if [ "$scripts_json" = "[]" ] || [ -z "$scripts_json" ]; then return 0; fi

                      echo "$scripts_json" | jq -c '.[]' | while read -r param; do
                          local name=$(echo "$param" | jq -r '.Name' | awk -F/ '{print $NF}')
                          # Reject unsafe filenames (path traversal, dotfiles)
                          if ! echo "$name" | grep -qE '^([0-9]{1,3}-)?[A-Za-z0-9][A-Za-z0-9._-]*$'; then
                              echo "cloud-init-ext: WARNING - skipping invalid script name: $name"
                              continue
                          fi
                          local value=$(echo "$param" | jq -r '.Value')
                          printf '%s\n' "$value" > "$CO_CLOUDINIT_DIR/$name"
                          chmod +x "$CO_CLOUDINIT_DIR/$name"
                      done
                  }
                  _co_run_cloud_init_scripts() {
                      local phase="$1"
                      if [ ! -d "$CO_CLOUDINIT_DIR" ] || [ -z "$(ls -A $CO_CLOUDINIT_DIR 2>/dev/null)" ]; then return 0; fi
                      # Disable xtrace to avoid leaking customer script content into cloud-init logs
                      local _co_xtrace_was_set=false
                      case "$-" in *x*) _co_xtrace_was_set=true; set +x;; esac
                      for script in $(ls "$CO_CLOUDINIT_DIR" | sort); do
                          local num=$(echo "$script" | grep -oE '^[0-9]+' || echo "999")
                          if [ "$phase" = "after" ] && [ "$num" -ge 100 ] && [ "$num" -lt 200 ]; then
                              echo "cloud-init-ext: WARNING - $script uses reserved range 100-199, running in after phase"
                          fi
                          if { [ "$phase" = "before" ] && [ "$num" -lt 100 ]; } || \
                             { [ "$phase" = "after" ] && [ "$num" -ge 100 ]; }; then
                              echo "cloud-init-ext: Running $phase hook $script"
                              set +e; bash "$CO_CLOUDINIT_DIR/$script"; local rc=$?; set -e
                              [ $rc -ne 0 ] && echo "cloud-init-ext: WARNING - $script exited with code $rc"
                          fi
                      done
                      if $_co_xtrace_was_set; then set -x; fi
                  }
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/cloud-init"
                  _co_fetch_cloud_init_scripts "/
                - Ref: AWS::StackName
                - |-
                  /settings/services/cloud-init"
                  # -- End cloud-init extension scripts --


                  # -x flag is set here because we want to avoid exposing secrets in the cloud-init logs
                  set -x

                  # Print instance metadata to the logs
                  /opt/codeocean/tools/ec2-metadata.sh $EC2_INSTANCE_ID

                  _co_run_cloud_init_scripts before

                  # Apply security patches
                  /usr/lib/codeocean/shared/security-updates.sh

                  # Configuring iptables rules
                  cat <<EOF > /etc/sysconfig/iptables
                  *filter
                  :DOCKER-USER - [0:0]
                  # Prevent package build containers from accessing Amazon EC2 instance metadata
                  -A DOCKER-USER -s 192.168.250.0/24 -d 169.254.169.254/32 -j DROP
                  # Prevent package build containers from accessing VPC hosts
                  -A DOCKER-USER -s 192.168.250.0/24 -d $VPC_CIDR_BLOCK -p tcp -j DROP
                  # Allow package build containers to access the rcache service
                  -A DOCKER-USER -p tcp --sport 8700 -j ACCEPT
                  -A DOCKER-USER -p tcp --dport 8700 -j ACCEPT
                  COMMIT
                  EOF

                  systemctl enable --now iptables

                  # Install cron jobs
                  chown -R root:root /usr/lib/codeocean/services/cron.d
                  mv /usr/lib/codeocean/services/cron.d/* /etc/cron.d/

                  # Add private CA trust
                  PRIVATE_CA=
                - Ref: pPrivateCA
                - |-
                  
                  if ${PRIVATE_CA}; then
                    aws acm get-certificate --region 
                - Ref: AWS::Region
                - " --certificate-arn "
                - Ref: pCertificateArn
                - |-2
                   | jq -r .CertificateChain > /etc/pki/ca-trust/source/anchors/private-ca.crt
                    update-ca-trust extract
                  fi

                  # Mount the data EBS volume

                  # First, attach the volume to this instance
                  until aws ec2 attach-volume --region 
                - Ref: AWS::Region
                - " --volume-id "
                - Ref: DataVolume
                - |-2
                   --instance-id ${EC2_INSTANCE_ID} --device /dev/sde; do sleep 5; done

                  # Then, wait for volume attachment
                  until lsblk /dev/nvme2n1 &>/dev/null; do sleep 1; done

                  mkdir /data
                  mkfs -t xfs /dev/nvme2n1 || true
                  echo "UUID=$(blkid -s UUID -o value /dev/nvme2n1)  /data  xfs defaults 0 2" >> /etc/fstab
                  mount /data
                  chmod 700 /data

                  # Add EFS mounts
                  mkdir /{datasets,capsule-scratch}
                  echo "
                - Ref: DatasetsEfs
                - |-
                  :/ /datasets efs _netdev,tls,iam 0 0" >> /etc/fstab
                  echo "
                - Ref: ScratchEfs
                - |-
                  :/ /capsule-scratch efs _netdev,tls,iam 0 0" >> /etc/fstab
                  systemctl enable --now mount-efs-sequentially.service
                  chmod 700 /{datasets,capsule-scratch}

                  # Generate secrets
                  co-init generate-secrets

                  # Set internal services dns record
                  cat << EOF > /etc/codeocean/services_dns_config.json
                  {
                      "Comment": "Update record to reflect new IP address for a system ",
                      "Changes": [
                          {
                              "Action": "UPSERT",
                              "ResourceRecordSet": {
                                  "Name": "codeocean.internal",
                                  "TTL": 60,
                                  "Type": "A",
                                  "ResourceRecords": [
                                      {
                                          "Value": "${EC2_INSTANCE_IP}"
                                      }
                                  ]
                              }
                          }
                      ]
                  }
                  EOF
                  until aws route53 change-resource-record-sets --hosted-zone-id 
                - Ref: InternalHostedZone
                - |-2
                   --change-batch file:///etc/codeocean/services_dns_config.json; do
                      sleep 5
                  done

                  # Docker setup
                  /usr/lib/codeocean/shared/docker-setup.sh

                  # Set cloudwatch logs agent conf
                  cat << EOF > /tmp/cloudwatch-logs-config.json
                  {
                      "logs": {
                          "logs_collected": {
                              "files": {
                                  "collect_list": [
                                      {
                                          "file_path": "/var/log/messages",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances"
                                      },
                                      {
                                          "file_path": "/var/log/top",
                                          "log_group_name": "/
                - Ref: AWS::StackName
                - |-
                  /instances",
                                          "log_stream_name": "services/{instance_id}/{ip_address}/top"
                                      }
                                  ]
                              }
                          },
                          "log_stream_name": "services/{instance_id}/{ip_address}"
                      }
                  }
                  EOF

                  /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -s -c file:/tmp/cloudwatch-logs-config.json

                  /usr/lib/codeocean/services/systemd.sh

                  aws autoscaling put-warm-pool     --auto-scaling-group-name 
                - Ref: WorkersAutoScalingGroup
                - "     --min-size "
                - Ref: pMinWorkersAvailable
                - "     --max-group-prepared-capacity "
                - Ref: pMinWorkersAvailable
                - |-2
                       --pool-state Stopped

                  aws autoscaling put-warm-pool     --auto-scaling-group-name 
                - Ref: GPUWorkersAutoScalingGroup
                - "     --min-size "
                - Ref: pMinGpuWorkersAvailable
                - "     --max-group-prepared-capacity "
                - Ref: pMinGpuWorkersAvailable
                - |2
                       --pool-state Stopped

                  _co_run_cloud_init_scripts after
    DependsOn:
      - OpenSearchDomainV2
  ServicesAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      DesiredCapacity: "1"
      LaunchTemplate:
        LaunchTemplateId:
          Ref: ServicesLaunchTemplate
        Version:
          Fn::GetAtt:
            - ServicesLaunchTemplate
            - LatestVersionNumber
      MaxSize: "1"
      MinSize: "1"
      Tags:
        - Key: codeocean:deployment
          PropagateAtLaunch: true
          Value:
            Ref: AWS::StackName
        - Key: Name
          PropagateAtLaunch: true
          Value: codeocean-services
      TargetGroupARNs:
        - Ref: TargetGroupAnalytics
        - Ref: TargetGroupApiService
        - Ref: TargetGroupAssets
        - Ref: TargetGroupCWProxy
        - Ref: TargetGroupFileProxy
        - Ref: TargetGroupGitProxy
        - Ref: TargetGroupGW
        - Ref: TargetGroupRegistry
        - Ref: TargetGroupScim
        - Ref: TargetGroupWeb
      VPCZoneIdentifier:
        - Fn::If:
            - cNewVpc
            - Ref: PrivateSubnet1
            - Ref: pPrivateSubnet1Id
    CreationPolicy:
      ResourceSignal:
        Count: 0
    UpdatePolicy:
      AutoScalingRollingUpdate:
        WaitOnResourceSignals: false
  AlarmsTopic:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  ServicesRootVolumeUsage90Alarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine root volume usage is more than 90
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 30
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
        - Name: path
          Value: /
      EvaluationPeriods: 30
      MetricName: disk_used_percent
      Namespace: CWAgent
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 90
  ServicesDataVolumeUsage70Alarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine data volume usage is more than 70
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 60
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
        - Name: path
          Value: /data
      EvaluationPeriods: 60
      MetricName: disk_used_percent
      Namespace: CWAgent
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 70
  ServicesDataVolumeUsage95Alarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine data volume usage is more than 95
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 30
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
        - Name: path
          Value: /data
      EvaluationPeriods: 30
      MetricName: disk_used_percent
      Namespace: CWAgent
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 95
  ServicesDockerVolumeUsage90Alarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine docker volume usage is more than 90
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 30
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
        - Name: path
          Value: /docker
      EvaluationPeriods: 30
      MetricName: disk_used_percent
      Namespace: CWAgent
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 90
  ServicesCpuUsageHighAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine CPU usage is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 6
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
      EvaluationPeriods: 6
      MetricName: CPUUtilization
      Namespace: AWS/EC2
      OKActions:
        - Ref: AlarmsTopic
      Period: 300
      Statistic: Average
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 70
  ServicesMemoryUsageHighAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine memory usage is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 6
      Dimensions:
        - Name: AutoScalingGroupName
          Value:
            Ref: ServicesAutoScalingGroup
      EvaluationPeriods: 6
      MetricName: mem_used_percent
      Namespace: CWAgent
      OKActions:
        - Ref: AlarmsTopic
      Period: 300
      Statistic: Average
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 80
  ServicesUnhealthyHostAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Services machine is not responding to load balancer health checks
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 1
      Dimensions:
        - Name: LoadBalancer
          Value:
            Fn::GetAtt:
              - ExternalLoadBalancer
              - LoadBalancerFullName
        - Name: TargetGroup
          Value:
            Fn::GetAtt:
              - TargetGroupWeb
              - TargetGroupFullName
      EvaluationPeriods: 1
      MetricName: UnHealthyHostCount
      Namespace: AWS/ApplicationELB
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 0
  Services500MetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: "[date, time, service!=cw-proxy, level, source=GIN, status=500, ...]"
      LogGroupName:
        Ref: LogGroupServices
      MetricTransformations:
        - MetricName:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Services_HTTP_500_LogCount
          MetricNamespace: CodeOcean
          MetricValue: "1"
  Workers500MetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: "[date, time, service, level, source=GIN, status=500, ...]"
      LogGroupName:
        Ref: LogGroupWorkers
      MetricTransformations:
        - MetricName:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Workers_HTTP_500_LogCount
          MetricNamespace: CodeOcean
          MetricValue: "1"
  ServicesCriticalMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: "[date, time, service, level=CRITICAL, ...]"
      LogGroupName:
        Ref: LogGroupServices
      MetricTransformations:
        - MetricName:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Services_Critical_LogCount
          MetricNamespace: CodeOcean
          MetricValue: "1"
  WorkersCriticalMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: "[date, time, service, level=CRITICAL, ...]"
      LogGroupName:
        Ref: LogGroupWorkers
      MetricTransformations:
        - MetricName:
            Fn::Join:
              - ""
              - - Ref: AWS::StackName
                - /Workers_Critical_LogCount
          MetricNamespace: CodeOcean
          MetricValue: "1"
  CriticalErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Critical level errors returned by CodeOcean services
      ComparisonOperator: GreaterThanThreshold
      EvaluationPeriods: 1
      Metrics:
        - Expression: (m1+m2)
          Id: "e1"
          Label: Critical Error Count
          ReturnData: true
        - Id: m1
          MetricStat:
            Metric:
              MetricName:
                Fn::Join:
                  - ""
                  - - Ref: AWS::StackName
                    - /Services_Critical_LogCount
              Namespace: CodeOcean
            Period: 60
            Stat: Sum
          ReturnData: false
        - Id: m2
          MetricStat:
            Metric:
              MetricName:
                Fn::Join:
                  - ""
                  - - Ref: AWS::StackName
                    - /Workers_Critical_LogCount
              Namespace: CodeOcean
            Period: 60
            Stat: Sum
          ReturnData: false
      OKActions:
        - Ref: AlarmsTopic
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 0
  AnalyticsDBStorageAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Free storage space in Analytics RDS DB is critically low
      ComparisonOperator: LessThanThreshold
      DatapointsToAlarm: 10
      Dimensions:
        - Name: DBInstanceIdentifier
          Value:
            Ref: AnalyticsDBInstance
      EvaluationPeriods: 10
      MetricName: FreeStorageSpace
      Namespace: AWS/RDS
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 2147483648
  AnalyticsDBCpuAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Analytics RDS DB CPU usage is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 5
      Dimensions:
        - Name: DBInstanceIdentifier
          Value:
            Ref: AnalyticsDBInstance
      EvaluationPeriods: 5
      MetricName: CPUUtilization
      Namespace: AWS/RDS
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Average
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 90
  AnalyticsDbMemoryAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Analytics RDS DB Memory usage is high
      ComparisonOperator: LessThanThreshold
      DatapointsToAlarm: 15
      Dimensions:
        - Name: DBInstanceIdentifier
          Value:
            Ref: AnalyticsDBInstance
      EvaluationPeriods: 15
      MetricName: FreeableMemory
      Namespace: AWS/RDS
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Average
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 500000000
  RedisCpuAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Redis max CPU utilization across nodes is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 5
      EvaluationPeriods: 5
      Metrics:
        - Id: m1
          MetricStat:
            Metric:
              Dimensions:
                - Name: CacheClusterId
                  Value:
                    Fn::Join:
                      - ""
                      - - Ref: RedisReplicationGroup
                        - "-001"
                - Name: CacheNodeId
                  Value: "0001"
              MetricName: CPUUtilization
              Namespace: AWS/ElastiCache
            Period: 60
            Stat: Average
          ReturnData: false
        - Id: m2
          MetricStat:
            Metric:
              Dimensions:
                - Name: CacheClusterId
                  Value:
                    Fn::Join:
                      - ""
                      - - Ref: RedisReplicationGroup
                        - "-002"
                - Name: CacheNodeId
                  Value: "0001"
              MetricName: CPUUtilization
              Namespace: AWS/ElastiCache
            Period: 60
            Stat: Average
          ReturnData: false
        - Expression: MAX([m1,m2])
          Id: m3
          Label: Max Redis CPU Utilization
          ReturnData: true
      OKActions:
        - Ref: AlarmsTopic
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 45
      TreatMissingData: notBreaching
  RedisMemoryAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: Redis max memory usage across nodes is high
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 5
      EvaluationPeriods: 5
      Metrics:
        - Id: m1
          MetricStat:
            Metric:
              Dimensions:
                - Name: CacheClusterId
                  Value:
                    Fn::Join:
                      - ""
                      - - Ref: RedisReplicationGroup
                        - "-001"
                - Name: CacheNodeId
                  Value: "0001"
              MetricName: DatabaseMemoryUsagePercentage
              Namespace: AWS/ElastiCache
            Period: 60
            Stat: Average
          ReturnData: false
        - Id: m2
          MetricStat:
            Metric:
              Dimensions:
                - Name: CacheClusterId
                  Value:
                    Fn::Join:
                      - ""
                      - - Ref: RedisReplicationGroup
                        - "-002"
                - Name: CacheNodeId
                  Value: "0001"
              MetricName: DatabaseMemoryUsagePercentage
              Namespace: AWS/ElastiCache
            Period: 60
            Stat: Average
          ReturnData: false
        - Expression: MAX([m1,m2])
          Id: m3
          Label: Max Redis Memory Usage
          ReturnData: true
      OKActions:
        - Ref: AlarmsTopic
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 90
      TreatMissingData: notBreaching
  OpenSearchCpuAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: OpenSearch CPU usage is high
      ComparisonOperator: GreaterThanOrEqualToThreshold
      DatapointsToAlarm: 3
      Dimensions:
        - Name: DomainName
          Value:
            Ref: OpenSearchDomainV2
        - Name: ClientId
          Value:
            Ref: AWS::AccountId
      EvaluationPeriods: 3
      MetricName: CPUUtilization
      Namespace: AWS/ES
      OKActions:
        - Ref: AlarmsTopic
      Period: 300
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 80
      TreatMissingData: notBreaching
  OpenSearchMemoryAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmActions:
        - Ref: AlarmsTopic
      AlarmDescription: OpenSearch memory usage is high
      ComparisonOperator: GreaterThanOrEqualToThreshold
      DatapointsToAlarm: 3
      Dimensions:
        - Name: DomainName
          Value:
            Ref: OpenSearchDomainV2
        - Name: ClientId
          Value:
            Ref: AWS::AccountId
      EvaluationPeriods: 3
      MetricName: JVMMemoryPressure
      Namespace: AWS/ES
      OKActions:
        - Ref: AlarmsTopic
      Period: 60
      Statistic: Maximum
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Threshold: 95
      TreatMissingData: notBreaching
  BackupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: backup.amazonaws.com
        Version: "2012-10-17"
      Description: Provides AWS Backup permission to create backups and perform restores on your behalf across AWS services.
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
  BackupKMSKey:
    Type: AWS::KMS::Key
    Properties:
      Description: KMS key for AWS Backup Vault
      EnableKeyRotation: true
      KeyPolicy:
        Statement:
          - Action: kms:*
            Effect: Allow
            Principal:
              AWS:
                Fn::Join:
                  - ""
                  - - "arn:"
                    - Ref: AWS::Partition
                    - ":iam::"
                    - Ref: AWS::AccountId
                    - :root
            Resource: "*"
            Sid: Enable IAM user permissions
          - Fn::If:
              - cCopyBackup
              - Action:
                  - kms:CreateGrant
                  - kms:Decrypt
                  - kms:DescribeKey
                  - kms:GenerateDataKey*
                Effect: Allow
                Principal:
                  AWS:
                    Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":iam::"
                        - Fn::Select:
                            - 4
                            - Fn::Split:
                                - ":"
                                - Ref: pDestinationBackupVaultArn
                        - :root
                Resource: "*"
                Sid: Allow access to the key from backup account
              - Ref: AWS::NoValue
          - Fn::If:
              - cRestore
              - Action:
                  - kms:CreateGrant
                  - kms:Decrypt
                  - kms:DescribeKey
                  - kms:GenerateDataKey*
                Effect: Allow
                Principal:
                  AWS:
                    Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":iam::"
                        - Ref: pRestoreSourceAccountId
                        - :root
                Resource: "*"
                Sid: Allow access to the key from restore source account
              - Ref: AWS::NoValue
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
        - Key: codeocean:protected
          Value: "true"
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  BackupKMSKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName:
        Fn::Join:
          - ""
          - - alias/
            - Ref: AWS::StackName
            - /backup-kms-key
      TargetKeyId:
        Fn::GetAtt:
          - BackupKMSKey
          - KeyId
  BackupVault:
    Type: AWS::Backup::BackupVault
    Properties:
      AccessPolicy:
        Fn::If:
          - cRestore
          - Statement:
              - Action: backup:CopyIntoBackupVault
                Effect: Allow
                Principal:
                  AWS:
                    Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":iam::"
                        - Ref: pRestoreSourceAccountId
                        - :root
                Resource: "*"
            Version: "2012-10-17"
          - Ref: AWS::NoValue
      BackupVaultName:
        Fn::Join:
          - ""
          - - Ref: AWS::StackName
            - -v1
      BackupVaultTags:
        codeocean:deployment:
          Ref: AWS::StackName
        codeocean:protected: "true"
      EncryptionKeyArn:
        Fn::GetAtt:
          - BackupKMSKey
          - Arn
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  BackupPlan:
    Type: AWS::Backup::BackupPlan
    Properties:
      BackupPlan:
        BackupPlanName:
          Ref: AWS::StackName
        BackupPlanRule:
          - CompletionWindowMinutes: 240
            Lifecycle:
              DeleteAfterDays:
                Ref: pBackupRetentionPeriod
            RuleName: main
            ScheduleExpression:
              Ref: pBackupSchedule
            StartWindowMinutes: 120
            TargetBackupVault:
              Ref: BackupVault
      BackupPlanTags:
        codeocean:deployment:
          Ref: AWS::StackName
  BackupSelection:
    Type: AWS::Backup::BackupSelection
    Properties:
      BackupPlanId:
        Fn::GetAtt:
          - BackupPlan
          - BackupPlanId
      BackupSelection:
        IamRoleArn:
          Fn::GetAtt:
            - BackupRole
            - Arn
        Resources:
          - Fn::Join:
              - ""
              - - "arn:"
                - Ref: AWS::Partition
                - ":ec2:"
                - Ref: AWS::Region
                - ":"
                - Ref: AWS::AccountId
                - :volume/
                - Ref: DataVolume
          - Fn::GetAtt:
              - AnalyticsDBInstance
              - DBInstanceArn
        SelectionName: main
  BackupCopyPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Permissions for backup copy
      Path: /
      PolicyDocument:
        Statement:
          - Action: iam:PassRole
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - BackupRole
                - Arn
          - Action: backup:StartCopyJob
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
  BackupCopyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      Description: Allows BackupCopyFunction lambda to call AWS services
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - Ref: BackupCopyPolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cCopyBackup
  BackupCopyFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import logging
          import os

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          logger.info("Loading function")

          backup_client = boto3.client("backup")

          destination_backup_vault_arn = os.environ["DESTINATION_BACKUP_VAULT"]
          destination_backup_retention_period = int(os.environ["DESTINATION_BACKUP_RETENTION_PERIOD"])


          def handler(event, context):
              logger.info(f"Received event: {event}")

              detail_type = event.get("detail-type")
              if detail_type == "Backup Job State Change":
                  handle_backup_job(event)
              elif detail_type == "Copy Job State Change":
                  handle_copy_job(event)


          def handle_backup_job(event):
              """
              Handles a completed backup job to temporarily copy the snapshot from the backup vault
              to itself to re-encrypt the snapshot with the backup vault KMS key.
              """
              event_detail = event.get("detail")
              if not event_detail:
                  return

              job_State = event_detail.get("state")
              if job_State != "COMPLETED":
                  return

              backup_vault_name = event_detail.get("backupVaultName")
              backup_vault_arn = event_detail.get("backupVaultArn")
              iam_role_arn = event_detail.get("iamRoleArn")
              recovery_point_arn = event.get("resources")[0]

              try:
                  logger.info(f"Copying {recovery_point_arn} from {backup_vault_arn} to itself")
                  response = backup_client.start_copy_job(
                      RecoveryPointArn=recovery_point_arn,
                      SourceBackupVaultName=backup_vault_name,
                      DestinationBackupVaultArn=backup_vault_arn,
                      IamRoleArn=iam_role_arn,
                      Lifecycle={
                          "DeleteAfterDays": 1,
                      },
                  )
                  logger.info(f"start_copy_job done: {response}")
              except Exception as e:
                  logger.exception(e)


          def handle_copy_job(event):
              """
              Handles the completed temporary copy job to copy the snapshot to the destination backup vault.
              """
              event_detail = event.get("detail")
              if not event_detail:
                  return

              job_State = event_detail.get("state")
              if job_State != "COMPLETED":
                  return

              backup_vault_name = event_detail.get("destinationBackupVaultArn").split(":")[-1]
              iam_role_arn = event_detail.get("iamRoleArn")
              recovery_point_arn = event_detail.get("destinationRecoveryPointArn")

              try:
                  logger.info(f"Copying {recovery_point_arn} to {destination_backup_vault_arn} from {backup_vault_name}")  # noqa
                  response = backup_client.start_copy_job(
                      RecoveryPointArn=recovery_point_arn,
                      SourceBackupVaultName=backup_vault_name,
                      DestinationBackupVaultArn=destination_backup_vault_arn,
                      IamRoleArn=iam_role_arn,
                      Lifecycle={
                          "DeleteAfterDays": destination_backup_retention_period,
                      },
                  )
                  logger.info(f"start_copy_job done: {response}")
              except Exception as e:
                  logger.exception(e)
      Description: Lambda function to automate copy of backup snapshots to a destination backup vault
      Environment:
        Variables:
          DESTINATION_BACKUP_VAULT:
            Ref: pDestinationBackupVaultArn
          DESTINATION_BACKUP_RETENTION_PERIOD:
            Ref: pDestinationBackupRetentionPeriod
      Handler: index.handler
      LoggingConfig:
        LogGroup:
          Fn::Join:
            - ""
            - - /
              - Ref: AWS::StackName
              - /lambda
      Role:
        Fn::GetAtt:
          - BackupCopyRole
          - Arn
      Runtime: python3.13
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Timeout: 300
    DependsOn:
      - BackupCopyRole
    Condition: cCopyBackup
  BackupCopyFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: BackupCopyFunction
      Principal: events.amazonaws.com
    Condition: cCopyBackup
  BackupJobEventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: Rule to direct AWS Backup backup job events to handler Lambda
      EventPattern:
        source:
          - aws.backup
        detail-type:
          - Backup Job State Change
        detail:
          state:
            - COMPLETED
          backupVaultArn:
            - Fn::GetAtt:
                - BackupVault
                - BackupVaultArn
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Targets:
        - Arn:
            Fn::GetAtt:
              - BackupCopyFunction
              - Arn
          Id: BackupCopyFunction
    Condition: cCopyBackup
  CopyJobEventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: Rule to direct AWS Backup copy job events to handler Lambda
      EventPattern:
        source:
          - aws.backup
        detail-type:
          - Copy Job State Change
        detail:
          state:
            - COMPLETED
          destinationBackupVaultArn:
            - Fn::GetAtt:
                - BackupVault
                - BackupVaultArn
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Targets:
        - Arn:
            Fn::GetAtt:
              - BackupCopyFunction
              - Arn
          Id: BackupCopyFunction
    Condition: cCopyBackup
  BackupEventBridgeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
        Version: "2012-10-17"
      Description: Allows EventBridge to call AWS services
      Policies:
        - PolicyDocument:
            Statement:
              - Action: events:PutEvents
                Effect: Allow
                Resource:
                  Fn::Join:
                    - ""
                    - - "arn:"
                      - Ref: AWS::Partition
                      - ":events:"
                      - Fn::Select:
                          - 3
                          - Fn::Split:
                              - ":"
                              - Ref: pDestinationBackupVaultArn
                      - ":"
                      - Fn::Select:
                          - 4
                          - Fn::Split:
                              - ":"
                              - Ref: pDestinationBackupVaultArn
                      - :event-bus/default
            Version: "2012-10-17"
          PolicyName: SendEventsPolicy
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
    Condition: cCopyBackupCrossRegion
  CopyJobCompletedEventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: Rule to direct AWS Backup copy job events to the destination default event bus
      EventPattern:
        source:
          - aws.backup
        detail-type:
          - Copy Job State Change
        detail:
          state:
            - COMPLETED
          destinationBackupVaultArn:
            - Ref: pDestinationBackupVaultArn
      Tags:
        - Key: codeocean:deployment
          Value:
            Ref: AWS::StackName
      Targets:
        - Arn:
            Fn::Join:
              - ""
              - - "arn:"
                - Ref: AWS::Partition
                - ":events:"
                - Fn::Select:
                    - 3
                    - Fn::Split:
                        - ":"
                        - Ref: pDestinationBackupVaultArn
                - ":"
                - Fn::Select:
                    - 4
                    - Fn::Split:
                        - ":"
                        - Ref: pDestinationBackupVaultArn
                - :event-bus/default
          Id: DestinationEventBus
          RoleArn:
            Fn::GetAtt:
              - BackupEventBridgeRole
              - Arn
    Condition: cCopyBackupCrossRegion

