AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFormation stack for https://runs-on.com

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Main configuration [required]"
        Parameters:
          - GithubOrganization
          - GithubEnterpriseUrl
          - LicenseKey
          - EmailAddress
          - NetworkingStack
          - Environment
      - Label:
          default: "Networking: use external stack [optional]"
        Parameters:
          - ExternalVpcId
          - ExternalVpcPublicSubnetIds
          - ExternalVpcPrivateSubnetIds
          - ExternalVpcSecurityGroupId
      - Label:
          default: "Networking: use embedded stack [optional]"
        Parameters:
          - VpcCidrBlock
          - VpcCidrSubnetBits
          - VpcEndpoints
          - NatGatewayAvailability
          - NatGatewayElasticIPCount
          - VpcFlowLogFormat
          - VpcFlowLogS3BucketArn
          - VpcFlowLogRetentionInDays
      - Label:
          default: "Network and Security settings [optional]"
        Parameters:
          - Private
          - Ipv6Enabled
          - SSHAllowed
          - SSHCidrRange
          - EC2InstanceCustomPolicy
          - AppCustomPolicy
          - DefaultPermissionBoundaryArn
          - DefaultAdmins
          - EncryptEbs
          - EnableEfs
          - EnableEphemeralRegistry
      - Label:
          default: "Alert and monitoring settings [optional]"
        Parameters:
          - EnableDashboard
          - OtelExporterEndpoint
          - OtelExporterHeaders
          - ServerPassword
          - AppAlarmDailyMinutes
          - CostReportsEnabled
          - AlertTopicSubscriptionHttpsEndpoint
          - AlertTopicSlackWebhookUrl
          - ECInstanceDetailedMonitoring
          - Ec2LogRetentionInDays
          - SqsQueueOldestMessageThresholdSeconds
      - Label:
          default: "Integrations [optional]"
        Parameters:
          - IntegrationStepSecurityApiKey
      - Label:
          default: "Advanced app configuration [optional]"
        Parameters:
          - CostAllocationTag
          - SpotCircuitBreaker
          - RunnerCustomTags
          - RunnerDefaultDiskSize
          - RunnerDefaultVolumeThroughput
          - RunnerLargeDiskSize
          - RunnerLargeVolumeThroughput
          - RunnerMaxRuntime
          - RunnerConfigAutoExtendsFrom
          - AppEc2QueueSize
          - AppGithubApiStrategy
          - AppCPU
          - AppMemory
          - AppRegistry
          - AppDebug
          - LoggerLevel
          - S3CacheExpirationInDays

Parameters:
  GithubOrganization:
    Type: String
    Description: "If your GitHub organization lives at github.com/my-org, then enter: my-org. IMPORTANT: this field is case-sensitive."
    MinLength: 1

  GithubEnterpriseUrl:
    Type: String
    Default: ""
    Description: "Leave blank, unless you use a self-hosted GitHub Enterprise Server (GHES) instance. In that case, if your GHES instance lives at https://github.mycompany.com, then the value of this parameter should be: https://github.mycompany.com"

  LicenseKey:
    Type: String
    Description: |
      License key for RunsOn, either as a string or SSM parameter ARN (with the format `arn:aws:ssm:REGION:ACCOUNT:parameter/STACK_NAME/license-key`). Get one at https://runs-on.com/pricing.
    MinLength: 1

  EmailAddress:
    Type: String
    Description: Email address for cost and alert reports. You will receive a confirmation email from AWS at this address during the installation (check your spam folder if needed) so make sure to use a valid address. You must confirm it if you want to receive cost and alert reports.
    MinLength: 1

  NetworkingStack:
    Type: String
    Default: "embedded"
    AllowedValues:
      - "embedded"
      - "external"
    Description: "Let RunsOn manage your networking stack (`embedded`), or use a stack under your control (`external`). If you select `external`, you will need to provide the VPC ID, the subnet IDs, and the security group ID, and make sure your whole networking setup is compatible with RunsOn (see https://runs-on.com/networking/embedded-vs-external/ for more details). To get started quickly, we recommend using the 'embedded' option."

  ExternalVpcId:
    Type: String
    Description: "ID of the external VPC to reuse."
    Default: ""

  ExternalVpcPublicSubnetIds:
    Type: CommaDelimitedList
    Description: "List of PUBLIC subnet IDs of the external VPC to reuse, comma-separated. The more subnets you have, the more RunsOn will be able to find cheap spot instances, as well as balancing the load across many AZs. You must enter `-` if you don't have any public subnets."
    Default: ""

  ExternalVpcPrivateSubnetIds:
    Type: CommaDelimitedList
    Description: "List of PRIVATE subnet IDs of the external VPC to reuse, comma-separated. The more subnets you have, the more RunsOn will be able to find cheap spot instances, as well as balancing the load across many AZs. You must enter `-` if you don't have any private subnets."
    Default: ""

  ExternalVpcSecurityGroupId:
    Type: String
    Description: "Security group ID of the external VPC to assign to the runners. Must allow outbound access to the GitHub IP ranges, EFS (if enabled), and any other service your jobs will call."
    Default: ""

  Environment:
    Type: String
    Default: "production"
    MinLength: 1
    Description: "Make RunsOn only listen to jobs with an `env` job label matching this value. Only change this if you know what you are doing. See https://runs-on.com/configuration/environments/ for more details."

  AlertTopicSubscriptionHttpsEndpoint:
    Type: String
    Description: "HTTPS endpoint for cost and alert reports."
    Default: ""

  AlertTopicSlackWebhookUrl:
    Type: String
    Description: Slack incoming webhook URL for alert notifications.
    Default: ""
    NoEcho: true

  VpcCidrBlock:
    Type: String
    Description: CIDR block for the VPC. Updating this value after creation will require deleting the stack and recreating it.
    Default: 10.1.0.0/16

  VpcCidrSubnetBits:
    Type: Number
    Description: "[DEPRECATED, use external networking if you need to fine-tune this] Number of bits to allocate for the subnet. For example, if you specify VpcCidrBlock to a /16 (the default), and this value to 12 (the default), you will have 4 bits, i.e. max 16 subnets in the VPC. Updating this value after creation will require deleting the stack and recreating it."
    Default: 12
    MinValue: 9
    MaxValue: 16

  Ipv6Enabled:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable or disable IPv6 for runners. Disabling ipv6 might help with Docker Hub rate limiting issues. If enabled and reusing a VPC, ensure the VPC is IPv6 enabled.

  SSHAllowed:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Allow inbound SSH connections from the specified CIDR range to the runners.

  SSHCidrRange:
    Type: String
    Default: 0.0.0.0/0
    Description: CIDR range for inbound SSH access. By default, only repository collaborators with admin permission will be able to SSH into the runner instances.
    MinLength: 1

  Private:
    Type: String
    Default: "false"
    AllowedValues:
      - "only"
      - "always"
      - "true"
      - "false"
    Description: "Enable ('true', 'always', 'only') or disable ('false') private networking. If 'true', your jobs can opt-in to launch in private subnets with the `private=true` label, and they will get a static egress IP. If 'always', all jobs will run in private subnets, and they will all get a static egress IP, unless you manually opt-out in your job definition with the `private=false` label. If 'only', then jobs can only launch in private subnets. Note that enabling it will create 1 managed NAT gateway, with the corresponding costs. More details at https://runs-on.com/networking/static-ips/."

  VpcEndpoints:
    Type: String
    Default: "none"
    AllowedValues:
      - "EC2"
      - "ECR"
      - "EC2+ECR"
      - "none"
    Description: "Enable or disable VPC endpoints among: EC2 (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html), ECR (API + Docker - https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html). Each endpoint incurs ~$21.6/month ($7.2/month per AZ x3)."

  NatGatewayElasticIPCount:
    Type: Number
    Default: 1
    MinValue: 1
    MaxValue: 8
    Description: "Number of Elastic IPv4s to attach to _each_ NAT Gateway. Only applies if Private mode is enabled. Defaults to 1. Maximum of 8. Increasing this number is useful if you launch a large number of runners and are rate-limited by some external services due to the limited number of NAT Gateway IP addresses. Incurs ~$3.6/month additional cost for each Elastic IP. Note that by default you are limited to 2 EIPs per NAT gateway, but can request a quota increase from AWS."

  NatGatewayAvailability:
    Type: String
    Default: SingleAZ
    AllowedValues:
    - SingleAZ
    - MultiAZ
    Description: "Create either a single NAT Gateway (SingleAZ) for all 3 availability zones, or a NAT Gateway per Availability Zone (MultiAZ). Only applies if Private mode is enabled. Each gateway incurs ~$32.4/month, plus bandwidth costs."

  VpcFlowLogFormat:
    Type: String
    Default: ""
    Description: "[DEPRECATED, use external networking if you need to fine-tune this] VPC Flow Log format. If blank, no VPC Flow Logs will be created. If set to `default`, the default format will be used. More details at https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html#flow-logs-fields"

  VpcFlowLogS3BucketArn:
    Type: String
    Default: ""
    Description: "[DEPRECATED, use external networking if you need to fine-tune this] ARN of an S3 bucket to send VPC Flow Logs to in addition to the default CloudWatch logs group. If blank, the logs will only be sent to CloudWatch Logs."

  VpcFlowLogRetentionInDays:
    Type: Number
    Default: 7
    MinValue: 1
    Description: "[DEPRECATED, use external networking if you need to fine-tune this] Retention period in days for VPC Flow Logs."

  DefaultPermissionBoundaryArn:
    Type: String
    Default: ""
    Description: "ARN of an IAM policy that will be assigned as permission boundary to roles created by the stack. If blank, the roles will not have a permission boundary."

  DefaultAdmins:
    Type: String
    Default: ""
    Description: "[DEPRECATED, prefer to use SSM for admin access]. Comma-separated list of GitHub usernames that will always be granted SSH access to all the runner instances (if SSH access is enabled), in addition to any repository admins. If blank, only repository collaborators with admin permission will be able to SSH into the runner instances."

  AppEc2QueueSize:
    Type: Number
    Default: 2
    MinValue: 1
    Description: "Number of queued jobs that can be processed concurrently by RunsOn. Each job takes about 2-3s to process. Increasing this value should be done with caution, as it will increase the number of EC2 instances that will be launched at once, eating into your AWS and GitHub rate limits. New AWS accounts come with a rate-limit of 2 RunInstances call/s, although you can request a higher limit from AWS (https://docs.aws.amazon.com/AWSEC2/latest/APIReference/throttling.html)."

  AppAlarmDailyMinutes:
    Type: Number
    Default: "4000"
    Description: "Trigger an alarm if the cumulative number of minutes consumed during a day is over that number."

  AppGithubApiStrategy:
    Type: String
    Default: "normal"
    AllowedValues:
      - "normal"
      - "conservative"
    Description: "Use normal or conservative GitHub API strategy. The conservative strategy will attempt to save GitHub API tokens by not automatically de-registering runners from the GitHub API after they are no longer needed. It's more elegant to clean them up (recommended), but in any case GitHub will eventually clean them up on their side after 24h. Especially useful if you launch tens of thousands of jobs every day."

  AppCPU:
    Type: Number
    AllowedValues:
      - 256
      - 512
      - 1024
      - 2048
      - 4096
    Default: 256
    Description: CPU units for RunsOn service (256 or higher). You might only need to change this if you have 10k+ jobs per day.

  AppMemory:
    Type: Number
    AllowedValues:
      - 512
      - 1024
      - 2048
      - 3072
      - 4096
      - 6144
      - 8192
    Default: 512
    Description: Memory in MB for RunsOn service (512 or higher). You might only need to change this if you have 10k+ jobs per day.

  AppRegistry:
    Type: String
    Default: "public.ecr.aws/c5h5o9k1/runs-on/runs-on"
    Description: "Docker image (public) registry for the RunsOn service."

  AppDebug:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable or disable debug mode for the RunsOn stack. Enabling this will switch off the auto-shutdown of the runner instances when they are unable to bootstrap.

  LoggerLevel:
    Type: String
    Default: "info"
    AllowedValues:
      - "trace"
      - "debug"
      - "info"
      - "warn"
    Description: "Log level for the RunsOn server. Options: trace (most verbose), debug, info (default), warn (least verbose)."

  SpotCircuitBreaker:
    Type: String
    Default: "2/15/30"
    AllowedPattern: "^(false|[0-9]+/[0-9]+/[0-9]+)$"
    Description: "Enable (`COUNT/WINDOW_MINUTES/RECOVERY_MINUTES`) or disable (`false`) the spot instance circuit breaker. If enabled, the circuit breaker will prevent the stack from launching new spot instances during RECOVERY_MINUTES if there was at least COUNT spot interruptions during the last WINDOW_MINUTES. For instance 2/15/30 will prevent the stack from launching new spot instances during 30 minutes if there was at least 2 spot interruptions during the last 15 minutes."

  EncryptEbs:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: "Encrypt the EBS volumes of the runner instances. Encryption will slow the boot time of runner instances by ~10s."

  EnableEfs:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: "Enable EFS shared filesystem for runners (only available with 'embedded' networking). If enabled, an EFS volume will be created and made available to runners. Runners will need appropriate permissions and tools (e.g., nfs-utils) to mount it."

  EnableEphemeralRegistry:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: "If enabled, create an ECR ephemeral registry that can be used as a type=registry cache backend for Docker BuildKit. The registry will be created with a 10-day lifecycle policy, and runners will automatically be granted push/pull access."

  RunnerDefaultDiskSize:
    Type: Number
    Default: 40
    MinValue: 10
    Description: Default disk size in GB.

  RunnerDefaultVolumeThroughput:
    Type: Number
    Default: 400
    MinValue: 125
    MaxValue: 1000
    Description: Default volume throughput in MiB/s (the higher the throughput, the faster the boot times, but costs more).

  RunnerLargeDiskSize:
    Type: Number
    Default: 80
    MinValue: 10
    Description: "DEPRECATED: Disk size in GB for legacy `disk=large` label. Use volume= label instead (e.g. volume=80gb)."

  RunnerLargeVolumeThroughput:
    Type: Number
    Default: 750
    MinValue: 125
    MaxValue: 1000
    Description: "DEPRECATED: Volume throughput in MiB/s for legacy `disk=large` runners. The disk= label is deprecated in favor of the volume= label (e.g. volume=80gb:gp3:750mbs:4000iops)."

  RunnerCustomTags:
    Type: CommaDelimitedList
    Default: ""
    Description: "Optional custom tags for the runner instances (e.g. 'key1=value1,key2=value2'). Tag keys cannot start with `runs-on`, and can only use letters (a-z, A-Z), numbers (0-9), and the following characters: + - = . , _ : @ (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions)"

  RunnerMaxRuntime:
    Type: Number
    Default: 720
    MinValue: 30
    Description: Maximum runtime in minutes for runners. After this time, the runner will be forcefully terminated.

  RunnerConfigAutoExtendsFrom:
    Type: String
    Default: ".github-private"
    MinLength: 1
    Description: "When set to a repository name (e.g., '.github-private'), forces all repository configurations to extend from that repository. The config will be read from `<repo-name>/.github/runs-on.yml`. This allows you to provide a base configuration that applies to all repositories, even if they don't have a local runs-on.yml file. Set to '.' to disable auto-extend and use each repository's local extends (if any). Make sure the GitHub App has access to the specified repository."

  CostReportsEnabled:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: Enable or disable cost reports sent by email.

  EC2InstanceCustomPolicy:
    Type: String
    Default: ""
    Description: "Optional managed IAM Policy ARN to assign to the EC2 runner instances. Can be used to give runners access to AWS resources that you manage."

  AppCustomPolicy:
    Type: String
    Default: ""
    Description: "Optional managed IAM Policy ARN to assign to the App runner service role. Can be used to e.g. allow access to KMS decryption keys for AMIs."

  ECInstanceDetailedMonitoring:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: "[DEPRECATED. See https://runs-on.com/monitoring/job-metrics/#performance-metrics for better performance metrics]."

  Ec2LogRetentionInDays:
    Type: Number
    Default: 7
    MinValue: 1
    Description: "Retention period in days for EC2 instance logs."

  SqsQueueOldestMessageThresholdSeconds:
    Type: Number
    Default: 0
    Description: Trigger an alarm per sqs queue if the oldest message in that queue is older than this time in seconds. Leave as 0 to disable SQS alarms.

  EnableDashboard:
    Type: String
    Default: "false"
    AllowedValues: ["true", "false"]
    Description: Create a CloudWatch dashboard for monitoring RunsOn operations (number of jobs processed, rate limit status, last error messages, etc.). The dashboard will be named STACK_NAME-Dashboard.

  S3CacheExpirationInDays:
    Type: Number
    Default: 10
    Description: "Number of days to keep cache entries in the S3 cache bucket."

  CostAllocationTag:
    Type: String
    Default: "stack"
    Description: "Cost allocation tag key to use for all the deployed resources. The value for this tag will be set to the stack name. Updating this after creation will require deleting the stack and recreating it."

  IntegrationStepSecurityApiKey:
    Type: String
    Default: ""
    Description: "API key for Step Security (see https://stepsecurity.io/ for more details)."

  OtelExporterEndpoint:
    Type: String
    Default: ""
    Description: "OTLP endpoint URL (e.g., ingest.eu.signoz.cloud:443, or https://otlp-gateway-prod-eu-west-0.grafana.net/otlp). Only HTTP(s) protocol is supported. Leave empty for no metrics export."

  OtelExporterHeaders:
    Type: String
    Default: ""
    NoEcho: true
    Description: "Headers for OTLP endpoint in W3C Baggage format: 'key1=value1,key2=value2' (e.g., 'signoz-ingestion-key=ABCD1234'). Leave empty if not required."

  ServerPassword:
    Type: String
    Default: ""
    NoEcho: true
    Description: "Password for the /metrics Prometheus endpoint. If blank, the endpoint will be disabled. Access with username 'admin' and this password via HTTP Basic Auth."

Mappings:
  App:
    Tags:
      BootstrapTag: v0.1.12
      AppTag: v2.11.0
      ImageTag: v2.11.0@sha256:875bcd8a36be7be78509a4c8371cdb4bff01af06c49f4a2d2a2647e3bf44bac5

Conditions:
  EmbeddedVpc: !Equals [!Ref NetworkingStack, "embedded"]
  ExternalVpc: !Equals [!Ref NetworkingStack, "external"]
  ExternalVpcPublicSubnetIdsProvided: !And [!Not [!Equals [!Join [",", !Ref ExternalVpcPublicSubnetIds], "-"]], !Not [!Equals [!Join [",", !Ref ExternalVpcPublicSubnetIds], ""]]]
  ExternalVpcPrivateSubnetIdsProvided: !And [!Not [!Equals [!Join [",", !Ref ExternalVpcPrivateSubnetIds], "-"]], !Not [!Equals [!Join [",", !Ref ExternalVpcPrivateSubnetIds], ""]]]
  EmailProvided: !Not [!Equals [!Ref EmailAddress, ""]]
  CostReportsEnabled: !Equals [!Ref CostReportsEnabled, "true"]
  AlertTopicSubscriptionHttpsProvided:
    !Not [!Equals [!Ref AlertTopicSubscriptionHttpsEndpoint, ""]]
  AlertTopicSlackWebhookProvided: !Not [!Equals [!Ref AlertTopicSlackWebhookUrl, ""]]
  PrivateEnabled: !Not [!Equals [!Ref Private, "false"]]
  HasPrivateSubnet: !And [!Condition EmbeddedVpc, !Condition PrivateEnabled]
  PrivateSubnetIsMultiAZ: !And [!Condition HasPrivateSubnet, !Equals [!Ref NatGatewayAvailability, "MultiAZ"]]
  HasEncryptEbs: !Equals [!Ref EncryptEbs, "true"]
  IsPrivateOnly: !Equals [!Ref Private, "only"]
  CustomPolicyProvided: !Not [!Equals [!Ref EC2InstanceCustomPolicy, ""]]
  AppCustomPolicyProvided: !Not [!Equals [!Ref AppCustomPolicy, ""]]
  ECInstanceDetailedMonitoringEnabled: !Equals ["true", !Ref ECInstanceDetailedMonitoring]
  CreateElasticIP1: !And [!Condition HasPrivateSubnet, !Not [!Equals [!Ref NatGatewayElasticIPCount, 0]]]
  CreateElasticIP2: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP1, !Not [!Equals [!Ref NatGatewayElasticIPCount, 1]]]
  CreateElasticIP3: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP2, !Not [!Equals [!Ref NatGatewayElasticIPCount, 2]]]
  CreateElasticIP4: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP3, !Not [!Equals [!Ref NatGatewayElasticIPCount, 3]]]
  CreateElasticIP5: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP4, !Not [!Equals [!Ref NatGatewayElasticIPCount, 4]]]
  CreateElasticIP6: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP5, !Not [!Equals [!Ref NatGatewayElasticIPCount, 5]]]
  CreateElasticIP7: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP6, !Not [!Equals [!Ref NatGatewayElasticIPCount, 6]]]
  CreateElasticIP8: !And [!Condition HasPrivateSubnet, !Condition CreateElasticIP7, !Not [!Equals [!Ref NatGatewayElasticIPCount, 7]]]
  CreateElasticIP1B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP1]
  CreateElasticIP2B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP2]
  CreateElasticIP3B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP3]
  CreateElasticIP4B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP4]
  CreateElasticIP5B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP5]
  CreateElasticIP6B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP6]
  CreateElasticIP7B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP7]
  CreateElasticIP8B: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP8]
  CreateElasticIP1C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP1]
  CreateElasticIP2C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP2]
  CreateElasticIP3C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP3]
  CreateElasticIP4C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP4]
  CreateElasticIP5C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP5]
  CreateElasticIP6C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP6]
  CreateElasticIP7C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP7]
  CreateElasticIP8C: !And [!Condition PrivateSubnetIsMultiAZ, !Condition CreateElasticIP8]
  CreateInterfaceEndpointEc2: !And [!Condition EmbeddedVpc, !Or [!Equals [!Ref VpcEndpoints, "EC2"], !Equals [!Ref VpcEndpoints, "EC2+ECR"]]]
  CreateInterfaceEndpointEcrApi: !And [!Condition EmbeddedVpc, !Or [!Equals [!Ref VpcEndpoints, "ECR"], !Equals [!Ref VpcEndpoints, "EC2+ECR"]]]
  CreateInterfaceEndpointEcrDocker: !And [!Condition EmbeddedVpc, !Or [!Equals [!Ref VpcEndpoints, "ECR"], !Equals [!Ref VpcEndpoints, "EC2+ECR"]]]
  CreateSqsQueueOldestMessageAlarms: !Not [!Equals [!Ref SqsQueueOldestMessageThresholdSeconds, 0]]
  HasDefaultPermissionBoundaryArn: !Not [!Equals [!Ref DefaultPermissionBoundaryArn, ""]]
  HasVpcFlowLogEnabled: !And [!Condition EmbeddedVpc, !Not [!Equals [!Ref VpcFlowLogFormat, ""]]]
  HasVpcFlowLogFormat: !Not [!Equals [!Ref VpcFlowLogFormat, "default"]]
  HasVpcFlowLogS3BucketArn: !And [!Condition HasVpcFlowLogEnabled, !Not [!Equals [!Ref VpcFlowLogS3BucketArn, ""]]]
  HasIpv6Enabled: !Equals [!Ref Ipv6Enabled, "true"]
  HasSSHAllowed: !Equals [!Ref SSHAllowed, "true"]
  CreateEfs: !And [!Condition EmbeddedVpc, !Equals [!Ref EnableEfs, "true"]] # EFS only supported for Embedded VPC for now
  CreateEphemeralRegistry: !Equals [!Ref EnableEphemeralRegistry, "true"]
  CreateDashboard: !Equals [!Ref EnableDashboard, "true"]

Resources:
  VPC:
    Condition: EmbeddedVpc
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidrBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-VPC

  VPCIPv6CidrBlock:
    Condition: EmbeddedVpc
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      VpcId: !Ref VPC
      AmazonProvidedIpv6CidrBlock: true

  # EC2.6 - VPC flow logging should be enabled in all VPCs
  VPCFlowLog:
    Type: AWS::EC2::FlowLog
    Condition: HasVpcFlowLogEnabled
    Properties:
      ResourceType: VPC
      ResourceId: !Ref VPC
      TrafficType: ALL
      DeliverLogsPermissionArn: !GetAtt VPCFlowLogRole.Arn
      LogDestinationType: cloud-watch-logs
      LogGroupName: !Ref VPCFlowLogGroup
      LogFormat: !If [HasVpcFlowLogFormat, !Ref VpcFlowLogFormat, !Ref "AWS::NoValue"]

  VPCFlowLogGroup:
    Type: AWS::Logs::LogGroup
    Condition: HasVpcFlowLogEnabled
    Properties:
      RetentionInDays: !Ref VpcFlowLogRetentionInDays

  VPCFlowLogS3:
    Condition: HasVpcFlowLogS3BucketArn
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceType: VPC
      ResourceId: !Ref VPC
      TrafficType: ALL
      LogDestinationType: s3
      LogDestination: !Ref VpcFlowLogS3BucketArn
      LogFormat: !If [HasVpcFlowLogFormat, !Ref VpcFlowLogFormat, !Ref "AWS::NoValue"]

  VPCFlowLogRole:
    Type: AWS::IAM::Role
    Condition: HasVpcFlowLogEnabled
    Properties:
      PermissionsBoundary: !If [HasDefaultPermissionBoundaryArn, !Ref DefaultPermissionBoundaryArn, !Ref "AWS::NoValue"]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: vpc-flow-logs.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: allow-flow-logs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                Resource: !GetAtt VPCFlowLogGroup.Arn

  PrivateSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet1RouteTable

  PrivateSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet2RouteTable

  PrivateSubnet3RouteTable:
    Type: AWS::EC2::RouteTable
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet3RouteTable

  PrivateSubnet1Route:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet1RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref PrivateSubnetGateway1

  PrivateSubnet1RouteIPv6:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet1RouteTable
      DestinationIpv6CidrBlock: ::/0
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway

  PrivateSubnet2Route:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !If [PrivateSubnetIsMultiAZ, !Ref PrivateSubnetGateway2, !Ref PrivateSubnetGateway1]

  PrivateSubnet2RouteIPv6:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet2RouteTable
      DestinationIpv6CidrBlock: ::/0
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway

  PrivateSubnet3Route:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet3RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !If [PrivateSubnetIsMultiAZ, !Ref PrivateSubnetGateway3, !Ref PrivateSubnetGateway1]

  PrivateSubnet3RouteIPv6:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateSubnet3RouteTable
      DestinationIpv6CidrBlock: ::/0
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway

  EgressOnlyInternetGateway:
    Type: AWS::EC2::EgressOnlyInternetGateway
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateSubnet1RouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateSubnet2RouteTable

  PrivateSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PrivateSubnet3
      RouteTableId: !Ref PrivateSubnet3RouteTable

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Select [8, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: false
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 0
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs ""]
      CidrBlock: !Select [9, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: false
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 1
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet2

  PrivateSubnet3:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [2, !GetAZs ""]
      CidrBlock: !Select [10, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: false
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 2
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnet3

  PrivateSubnetGateway1:
    Condition: HasPrivateSubnet
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !If
        - CreateElasticIP1
        - !GetAtt ElasticIP1.AllocationId
        - !Ref AWS::NoValue
      SecondaryAllocationIds:
        - !If [CreateElasticIP2, !GetAtt ElasticIP2.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP3, !GetAtt ElasticIP3.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP4, !GetAtt ElasticIP4.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP5, !GetAtt ElasticIP5.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP6, !GetAtt ElasticIP6.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP7, !GetAtt ElasticIP7.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP8, !GetAtt ElasticIP8.AllocationId, !Ref "AWS::NoValue"]
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnetGateway1

  PrivateSubnetGateway2:
    Condition: PrivateSubnetIsMultiAZ
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !If
        - CreateElasticIP1B
        - !GetAtt ElasticIP1B.AllocationId
        - !Ref AWS::NoValue
      SecondaryAllocationIds:
        - !If [CreateElasticIP2B, !GetAtt ElasticIP2B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP3B, !GetAtt ElasticIP3B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP4B, !GetAtt ElasticIP4B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP5B, !GetAtt ElasticIP5B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP6B, !GetAtt ElasticIP6B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP7B, !GetAtt ElasticIP7B.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP8B, !GetAtt ElasticIP8B.AllocationId, !Ref "AWS::NoValue"]
      SubnetId: !Ref PublicSubnet2
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnetGateway2

  PrivateSubnetGateway3:
    Condition: PrivateSubnetIsMultiAZ
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !If
        - CreateElasticIP1C
        - !GetAtt ElasticIP1C.AllocationId
        - !Ref AWS::NoValue
      SecondaryAllocationIds:
        - !If [CreateElasticIP2C, !GetAtt ElasticIP2C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP3C, !GetAtt ElasticIP3C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP4C, !GetAtt ElasticIP4C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP5C, !GetAtt ElasticIP5C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP6C, !GetAtt ElasticIP6C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP7C, !GetAtt ElasticIP7C.AllocationId, !Ref "AWS::NoValue"]
        - !If [CreateElasticIP8C, !GetAtt ElasticIP8C.AllocationId, !Ref "AWS::NoValue"]
      SubnetId: !Ref PublicSubnet3
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PrivateSubnetGateway3

  ElasticIP1:
    Condition: CreateElasticIP1
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP1

  ElasticIP2:
    Condition: CreateElasticIP2
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP2

  ElasticIP3:
    Condition: CreateElasticIP3
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP3

  ElasticIP4:
    Condition: CreateElasticIP4
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP4

  ElasticIP5:
    Condition: CreateElasticIP5
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP5

  ElasticIP6:
    Condition: CreateElasticIP6
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP6

  ElasticIP7:
    Condition: CreateElasticIP7
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP7

  ElasticIP8:
    Condition: CreateElasticIP8
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP8

  ElasticIP1B:
    Condition: CreateElasticIP1B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP1B

  ElasticIP2B:
    Condition: CreateElasticIP2B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP2B

  ElasticIP3B:
    Condition: CreateElasticIP3B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP3B

  ElasticIP4B:
    Condition: CreateElasticIP4B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP4B

  ElasticIP5B:
    Condition: CreateElasticIP5B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP5B

  ElasticIP6B:
    Condition: CreateElasticIP6B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP6B

  ElasticIP7B:
    Condition: CreateElasticIP7B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP7B

  ElasticIP8B:
    Condition: CreateElasticIP8B
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP8B

  ElasticIP1C:
    Condition: CreateElasticIP1C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP1C

  ElasticIP2C:
    Condition: CreateElasticIP2C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP2C

  ElasticIP3C:
    Condition: CreateElasticIP3C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP3C

  ElasticIP4C:
    Condition: CreateElasticIP4C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP4C

  ElasticIP5C:
    Condition: CreateElasticIP5C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP5C

  ElasticIP6C:
    Condition: CreateElasticIP6C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP6C

  ElasticIP7C:
    Condition: CreateElasticIP7C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP7C

  ElasticIP8C:
    Condition: CreateElasticIP8C
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-ElasticIP8C

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Select [0, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: !If [IsPrivateOnly, false, true]
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 3
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PublicSubnet1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs ""]
      CidrBlock: !Select [1, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: !If [IsPrivateOnly, false, true]
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 4
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PublicSubnet2

  PublicSubnet3:
    Type: AWS::EC2::Subnet
    Condition: EmbeddedVpc
    DependsOn: VPCIPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [2, !GetAZs ""]
      CidrBlock: !Select [2, !Cidr [!GetAtt [VPC, CidrBlock], 16, !Ref VpcCidrSubnetBits]]
      MapPublicIpOnLaunch: !If [IsPrivateOnly, false, true]
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Select:
          - 5
          - Fn::Cidr:
            - Fn::Select:
              - 0
              - Fn::GetAtt:
                - VPC
                - Ipv6CidrBlocks
            - 256
            - 64
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PublicSubnet3

  S3VpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      ServiceName:
        Fn::Sub: "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PublicRouteTable
        - !Ref PrivateSubnet1RouteTable
        - !Ref PrivateSubnet2RouteTable
        - !Ref PrivateSubnet3RouteTable

  VPCEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: EmbeddedVpc
    Properties:
      GroupDescription: Security group for VPC Endpoints (EC2 and ECR)
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref VpcCidrBlock
          Description: "Allow HTTPS from VPC CIDR for EC2 and ECR endpoints"
      SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: "0.0.0.0/0"
          Description: "Allow all outbound traffic"
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  # EC2.10 - Amazon EC2 should be configured to use VPC endpoints that are created for the Amazon EC2 service
  # Incurs additional cost
  EC2VpcEndpoint:
    Condition: CreateInterfaceEndpointEc2
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
        - !Ref PrivateSubnet3
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup

  ECRApiVpcEndpoint:
    Condition: CreateInterfaceEndpointEcrApi
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ecr.api"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
        - !Ref PrivateSubnet3
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup

  ECRDockerVpcEndpoint:
    Condition: CreateInterfaceEndpointEcrDocker
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ecr.dkr"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
        - !Ref PrivateSubnet3
      SecurityGroupIds:
        - !Ref VPCEndpointSecurityGroup

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Condition: EmbeddedVpc
    Properties:
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Condition: EmbeddedVpc
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-PublicRouteTable

  PublicRoute:
    Type: AWS::EC2::Route
    Condition: EmbeddedVpc
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicRouteIPv6:
    Type: AWS::EC2::Route
    Condition: EmbeddedVpc
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref InternetGateway

  SubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  SubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  SubnetRouteTableAssociation3:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: EmbeddedVpc
    Properties:
      SubnetId: !Ref PublicSubnet3
      RouteTableId: !Ref PublicRouteTable

  # --- EFS Resources ---
  EfsFileSystem:
    Condition: CreateEfs
    Type: AWS::EFS::FileSystem
    UpdateReplacePolicy: Retain
    DeletionPolicy: RetainExceptOnCreate
    Properties:
      Encrypted: true
      FileSystemTags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-EFS

  EfsSecurityGroup:
    Condition: CreateEfs
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub "Allow NFS traffic from RunsOn EC2 instances to EFS ${AWS::StackName}"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 2049
          ToPort: 2049
          SourceSecurityGroupId: !GetAtt SecurityGroup.GroupId
          Description: "Allow NFS from RunsOn EC2 instances"
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: Name
          Value: !Sub ${AWS::StackName}-EFSSecurityGroup

  EfsMountTarget1:
    Condition: CreateEfs
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EfsFileSystem
      SubnetId: !Ref PublicSubnet1 # Mount in public subnet for simplicity, runners in private can still reach
      SecurityGroups:
        - !Ref EfsSecurityGroup

  EfsMountTarget2:
    Condition: CreateEfs
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EfsFileSystem
      SubnetId: !Ref PublicSubnet2
      SecurityGroups:
        - !Ref EfsSecurityGroup

  EfsMountTarget3:
    Condition: CreateEfs
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref EfsFileSystem
      SubnetId: !Ref PublicSubnet3
      SecurityGroups:
        - !Ref EfsSecurityGroup
  # --- End EFS Resources ---

  # --- Ephemeral Registry Resources ---
  EphemeralRegistry:
    Condition: CreateEphemeralRegistry
    Type: AWS::ECR::Repository
    UpdateReplacePolicy: Retain
    DeletionPolicy: RetainExceptOnCreate
    Properties:
      RepositoryName: !Sub "${AWS::StackName}/ephemeral-registry"
      ImageScanningConfiguration:
        ScanOnPush: false
      ImageTagMutability: MUTABLE
      LifecyclePolicy:
        LifecyclePolicyText: |
          {
            "rules": [
              {
                "rulePriority": 1,
                "description": "Expire images older than 10 days",
                "selection": {
                  "tagStatus": "any",
                  "countType": "sinceImagePushed",
                  "countUnit": "days",
                  "countNumber": 10
                },
                "action": {
                  "type": "expire"
                }
              }
            ]
          }
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
  # --- End Ephemeral Registry Resources ---

  EC2FleetLaunchTemplateLinuxDefault:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      TagSpecifications:
        - ResourceType: launch-template
          Tags:
            - Key: !Ref CostAllocationTag
              Value: !Ref AWS::StackName
            - Key: runs-on-stack-name
              Value: !Ref AWS::StackName
            - Key: "runs-on/launch-template-name"
              Value: !Join ["-", [!Ref AWS::StackName, "LinuxDefault"]]
      LaunchTemplateData:
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: network-interface
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        EbsOptimized: "true"
        MetadataOptions:
          HttpTokens: required
          HttpPutResponseHopLimit: 2
        InstanceInitiatedShutdownBehavior: "terminate"
        Monitoring:
          !If
            - ECInstanceDetailedMonitoringEnabled
            - Enabled: true
            - Ref: AWS::NoValue
        NetworkInterfaces:
          - DeleteOnTermination: true
            DeviceIndex: "0"
            AssociatePublicIpAddress: !If [IsPrivateOnly, false, true]
            Groups:
              - !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
            Ipv6AddressCount: !If [HasIpv6Enabled, 1, 0]
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash -ex
              date -u
              BOOTSTRAP_BIN=/usr/local/bin/runs-on-bootstrap-${BootstrapTag}
              export RUNS_ON_RUNNER_MAX_RUNTIME="${RunnerMaxRuntime}"
              export RUNS_ON_LOG_GROUP_NAME="${EC2InstanceLogGroup}"
              export RUNS_ON_DEBUG="${AppDebug}"
              export AWS_REGION="${AWS::Region}"
              ${EfsExport}
              ${EphemeralRegistryExport}
              _the_end() { if [ "$RUNS_ON_DEBUG" != "true" ] ; then echo "THE END" ; sleep 180 ; shutdown -h now ; fi ; } ; trap _the_end EXIT INT TERM
              test -f $BOOTSTRAP_BIN || time curl -L --connect-timeout 3 --max-time 15 --retry 5 -s https://github.com/runs-on/bootstrap/releases/download/${BootstrapTag}/bootstrap-${BootstrapTag}-linux-$(uname -m) -o $BOOTSTRAP_BIN
              chmod a+x $BOOTSTRAP_BIN && $BOOTSTRAP_BIN --debug=${AppDebug} --exec --post-exec shutdown "s3://${S3Bucket}/agents/${AppTag}/agent-linux-$(uname -m)"
            - AppTag: !FindInMap [App, Tags, AppTag]
              BootstrapTag: !FindInMap [App, Tags, BootstrapTag]
              EfsExport: !If [CreateEfs, !Sub 'export RUNS_ON_EFS_ID="${EfsFileSystem}"', '']
              EphemeralRegistryExport: !If [CreateEphemeralRegistry, !Sub 'export RUNS_ON_ECR_CACHE="${EphemeralRegistry.RepositoryUri}"', '']

  EC2FleetLaunchTemplateWindowsDefault:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      TagSpecifications:
        - ResourceType: launch-template
          Tags:
            - Key: !Ref CostAllocationTag
              Value: !Ref AWS::StackName
            - Key: runs-on-stack-name
              Value: !Ref AWS::StackName
            - Key: "runs-on/launch-template-name"
              Value: !Join ["-", [!Ref AWS::StackName, "WindowsDefault"]]
      LaunchTemplateData:
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: network-interface
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        EbsOptimized: "true"
        MetadataOptions:
          HttpTokens: required
          HttpPutResponseHopLimit: 2
        InstanceInitiatedShutdownBehavior: "terminate"
        Monitoring:
          !If
            - ECInstanceDetailedMonitoringEnabled
            - Enabled: true
            - Ref: AWS::NoValue
        NetworkInterfaces:
          - DeleteOnTermination: true
            DeviceIndex: "0"
            AssociatePublicIpAddress: !If [IsPrivateOnly, false, true]
            Groups:
              - !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
            Ipv6AddressCount: !If [HasIpv6Enabled, 1, 0]
        UserData:
          Fn::Base64: !Sub
            - |
              <powershell>
              Get-Date -format s
              $env:RUNS_ON_RUNNER_MAX_RUNTIME = "${RunnerMaxRuntime}"
              $env:RUNS_ON_LOG_GROUP_NAME = "${EC2InstanceLogGroup}"
              $env:RUNS_ON_DEBUG = "${AppDebug}"
              $env:AWS_REGION = "${AWS::Region}"
              ${EfsEnvVar}
              ${EphemeralRegistryExport}
              # Enable and start SSM Agent service
              try {
                Set-Service -Name "AmazonSSMAgent" -StartupType Automatic -ErrorAction SilentlyContinue
                Start-Service -Name "AmazonSSMAgent" -ErrorAction SilentlyContinue
                Write-Output "SSM Agent service enabled and started"
              } catch {
                Write-Output "Warning: Failed to start SSM Agent service: $($_.Exception.Message)"
              }
              $bootstrapBin = "C:\runs-on\bootstrap-${BootstrapTag}.exe"
              try {
                New-Item -ItemType Directory -Force -Path (Split-Path $bootstrapBin)
                if (-not (Test-Path $bootstrapBin)) {
                  # Enable TLS 1.2 for Windows Server 2016 compatibility
                  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
                  # Disable progress bar for much faster downloads (can be 100x faster)
                  $ProgressPreference = 'SilentlyContinue'
                  Invoke-WebRequest -Uri "https://github.com/runs-on/bootstrap/releases/download/${BootstrapTag}/bootstrap-${BootstrapTag}-windows-$env:PROCESSOR_ARCHITECTURE.exe" -OutFile $bootstrapBin -UseBasicParsing
                }
                Add-MpPreference -ExclusionProcess $bootstrapBin
                & $bootstrapBin --debug=${AppDebug} --exec --post-exec shutdown "s3://${S3Bucket}/agents/${AppTag}/agent-windows-$env:PROCESSOR_ARCHITECTURE.exe"
              } finally {
                if ($env:RUNS_ON_DEBUG -ne "true") {
                  Write-Output "user-data: Going to shut down in a few seconds..."
                  Start-Sleep -Seconds 180
                  Stop-Computer -Force
                }
              }
              </powershell>
              <detach>true</detach>
              <persist>true</persist>
            - AppTag: !FindInMap [App, Tags, AppTag]
              BootstrapTag: !FindInMap [App, Tags, BootstrapTag]
              EfsEnvVar: !If [CreateEfs, !Sub '$env:RUNS_ON_EFS_ID = "${EfsFileSystem}"', '']
              EphemeralRegistryExport: !If [CreateEphemeralRegistry, !Sub '$env:RUNS_ON_ECR_CACHE = "${EphemeralRegistry.RepositoryUri}"', '']

  EC2FleetLaunchTemplateLinuxPrivate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      TagSpecifications:
        - ResourceType: launch-template
          Tags:
            - Key: !Ref CostAllocationTag
              Value: !Ref AWS::StackName
            - Key: runs-on-stack-name
              Value: !Ref AWS::StackName
            - Key: "runs-on/launch-template-name"
              Value: !Join ["-", [!Ref AWS::StackName, "LinuxPrivate"]]
      LaunchTemplateData:
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: network-interface
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        EbsOptimized: "true"
        MetadataOptions:
          HttpTokens: required
          HttpPutResponseHopLimit: 2
        InstanceInitiatedShutdownBehavior: "terminate"
        Monitoring:
          !If
            - ECInstanceDetailedMonitoringEnabled
            - Enabled: true
            - Ref: AWS::NoValue
        NetworkInterfaces:
          - DeleteOnTermination: true
            DeviceIndex: "0"
            AssociatePublicIpAddress: false
            Groups:
              - !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
            Ipv6AddressCount: !If [HasIpv6Enabled, 1, 0]
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash -ex
              date -u
              BOOTSTRAP_BIN=/usr/local/bin/runs-on-bootstrap-${BootstrapTag}
              export RUNS_ON_RUNNER_MAX_RUNTIME="${RunnerMaxRuntime}"
              export RUNS_ON_LOG_GROUP_NAME="${EC2InstanceLogGroup}"
              export RUNS_ON_DEBUG="${AppDebug}"
              export AWS_REGION="${AWS::Region}"
              ${EfsExport}
              ${EphemeralRegistryExport}
              _the_end() { if [ "$RUNS_ON_DEBUG" != "true" ] ; then echo "THE END" ; sleep 180 ; shutdown -h now ; fi ; } ; trap _the_end EXIT INT TERM
              test -f $BOOTSTRAP_BIN || time curl -L --connect-timeout 3 --max-time 15 --retry 5 -s https://github.com/runs-on/bootstrap/releases/download/${BootstrapTag}/bootstrap-${BootstrapTag}-linux-$(uname -m) -o $BOOTSTRAP_BIN
              chmod a+x $BOOTSTRAP_BIN && $BOOTSTRAP_BIN --debug=${AppDebug} --exec --post-exec shutdown "s3://${S3Bucket}/agents/${AppTag}/agent-linux-$(uname -m)"
            - AppTag: !FindInMap [App, Tags, AppTag]
              BootstrapTag: !FindInMap [App, Tags, BootstrapTag]
              EfsExport: !If [CreateEfs, !Sub 'export RUNS_ON_EFS_ID="${EfsFileSystem}"', '']
              EphemeralRegistryExport: !If [CreateEphemeralRegistry, !Sub 'export RUNS_ON_ECR_CACHE="${EphemeralRegistry.RepositoryUri}"', '']

  EC2FleetLaunchTemplateWindowsPrivate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      TagSpecifications:
        - ResourceType: launch-template
          Tags:
            - Key: !Ref CostAllocationTag
              Value: !Ref AWS::StackName
            - Key: runs-on-stack-name
              Value: !Ref AWS::StackName
            - Key: "runs-on/launch-template-name"
              Value: !Join ["-", [!Ref AWS::StackName, "WindowsPrivate"]]
      LaunchTemplateData:
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: volume
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
          - ResourceType: network-interface
            Tags:
              - Key: !Ref CostAllocationTag
                Value: !Ref AWS::StackName
              - Key: runs-on-stack-name
                Value: !Ref AWS::StackName
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        EbsOptimized: "true"
        MetadataOptions:
          HttpTokens: required
          HttpPutResponseHopLimit: 2
        InstanceInitiatedShutdownBehavior: "terminate"
        Monitoring:
          !If
            - ECInstanceDetailedMonitoringEnabled
            - Enabled: true
            - Ref: AWS::NoValue
        NetworkInterfaces:
          - DeleteOnTermination: true
            DeviceIndex: "0"
            AssociatePublicIpAddress: false
            Groups:
              - !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
            Ipv6AddressCount: !If [HasIpv6Enabled, 1, 0]
        UserData:
          Fn::Base64: !Sub
            - |
              <powershell>
              Get-Date -format s
              $env:RUNS_ON_RUNNER_MAX_RUNTIME = "${RunnerMaxRuntime}"
              $env:RUNS_ON_LOG_GROUP_NAME = "${EC2InstanceLogGroup}"
              $env:RUNS_ON_DEBUG = "${AppDebug}"
              $env:AWS_REGION = "${AWS::Region}"
              ${EfsEnvVar}
              ${EphemeralRegistryExport}
              # Enable and start SSM Agent service
              try {
                Set-Service -Name "AmazonSSMAgent" -StartupType Automatic -ErrorAction SilentlyContinue
                Start-Service -Name "AmazonSSMAgent" -ErrorAction SilentlyContinue
                Write-Output "SSM Agent service enabled and started"
              } catch {
                Write-Output "Warning: Failed to start SSM Agent service: $($_.Exception.Message)"
              }
              $bootstrapBin = "C:\runs-on\bootstrap-${BootstrapTag}.exe"
              try {
                New-Item -ItemType Directory -Force -Path (Split-Path $bootstrapBin)
                if (-not (Test-Path $bootstrapBin)) {
                  # Enable TLS 1.2 for Windows Server 2016 compatibility
                  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
                  # Disable progress bar for much faster downloads (can be 100x faster)
                  $ProgressPreference = 'SilentlyContinue'
                  Invoke-WebRequest -Uri "https://github.com/runs-on/bootstrap/releases/download/${BootstrapTag}/bootstrap-${BootstrapTag}-windows-$env:PROCESSOR_ARCHITECTURE.exe" -OutFile $bootstrapBin -UseBasicParsing
                }
                Add-MpPreference -ExclusionProcess $bootstrapBin
                & $bootstrapBin --debug=${AppDebug} --exec --post-exec shutdown "s3://${S3Bucket}/agents/${AppTag}/agent-windows-$env:PROCESSOR_ARCHITECTURE.exe"
              } finally {
                if ($env:RUNS_ON_DEBUG -ne "true") {
                  Write-Output "user-data: Going to shut down in a few seconds..."
                  Start-Sleep -Seconds 180
                  Stop-Computer -Force
                }
              }
              </powershell>
              <detach>true</detach>
              <persist>true</persist>
            - AppTag: !FindInMap [App, Tags, AppTag]
              BootstrapTag: !FindInMap [App, Tags, BootstrapTag]
              EfsEnvVar: !If [CreateEfs, !Sub '$env:RUNS_ON_EFS_ID = "${EfsFileSystem}"', '']
              EphemeralRegistryExport: !If [CreateEphemeralRegistry, !Sub '$env:RUNS_ON_ECR_CACHE = "${EphemeralRegistry.RepositoryUri}"', '']

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: EmbeddedVpc
    Properties:
      GroupDescription: Security group for SSH access and EFS egress
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - !If
          - HasSSHAllowed
          - CidrIp:
              Fn::Sub: "${SSHCidrRange}"
            FromPort: 22
            ToPort: 22
            IpProtocol: tcp
            Description: "Allow SSH Ingress"
          - !Ref AWS::NoValue
      SecurityGroupEgress:
        # Default Egress Rule (Allow All IPv4)
        - IpProtocol: "-1"
          CidrIp: "0.0.0.0/0"
          Description: "Allow all outbound IPv4 traffic"
        # Default Egress Rule (Allow All IPv6)
        - IpProtocol: "-1"
          CidrIpv6: "::/0"
          Description: "Allow all outbound IPv6 traffic"
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  S3Bucket:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: RetainExceptOnCreate
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
            BucketKeyEnabled: true
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: ExpireAgentBinaries
            Prefix: agents/v1*
            Status: Enabled
            ExpirationInDays: 30
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: ExpireDbEntries
            Prefix: runs-on/db/
            Status: Enabled
            ExpirationInDays: 30
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: CleanupIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
          - Id: CleanupExpiredObjectDeleteMarkers
            Status: Enabled
            ExpiredObjectDeleteMarker: true
      LoggingConfiguration:
        DestinationBucketName: !Ref S3BucketLogging
        LogFilePrefix: s3-config-access-logs/
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: runs-on-resource
          Value: config-bucket
        - Key: "runs-on/purpose"
          Value: "config"

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyUnencryptedConnections
            Effect: Deny
            Principal: "*"
            Action: "s3:*"
            Resource:
              - !Sub "arn:aws:s3:::${S3Bucket}"
              - !Sub "arn:aws:s3:::${S3Bucket}/*"
            Condition:
              Bool:
                aws:SecureTransport: false

  S3BucketCache:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: RetainExceptOnCreate
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
            BucketKeyEnabled: true
      VersioningConfiguration:
        Status: Suspended
      LifecycleConfiguration:
        Rules:
          - Id: ExpireRunnerConfig
            Prefix: runners/
            Status: Enabled
            ExpirationInDays: 1
            NoncurrentVersionExpiration:
              NoncurrentDays: 1
          - Id: ExpireCache
            Prefix: cache/
            Status: Enabled
            ExpirationInDays: !Ref S3CacheExpirationInDays
            NoncurrentVersionExpiration:
              NoncurrentDays: 1
          - Id: CleanupIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 1
          - Id: CleanupExpiredObjectDeleteMarkers
            Status: Enabled
            ExpiredObjectDeleteMarker: true
      LoggingConfiguration:
        DestinationBucketName: !Ref S3BucketLogging
        LogFilePrefix: s3-cache-access-logs/
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: runs-on-resource
          Value: cache-bucket
        - Key: "runs-on/purpose"
          Value: "cache"

  S3BucketCachePolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketCache
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyUnencryptedConnections
            Effect: Deny
            Principal: "*"
            Action: "s3:*"
            Resource:
              - !Sub "arn:aws:s3:::${S3BucketCache}"
              - !Sub "arn:aws:s3:::${S3BucketCache}/*"
            Condition:
              Bool:
                aws:SecureTransport: false

  S3BucketLogging:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: RetainExceptOnCreate
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
            BucketKeyEnabled: true
      VersioningConfiguration:
        Status: Enabled
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldLogs
            Status: Enabled
            ExpirationInDays: 30
            NoncurrentVersionExpiration:
              NoncurrentDays: 7
          - Id: CleanupIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
          - Id: CleanupExpiredObjectDeleteMarkers
            Status: Enabled
            ExpiredObjectDeleteMarker: true
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: runs-on-resource
          Value: logging-bucket
        - Key: "runs-on/purpose"
          Value: "s3-access-logs"

  S3BucketLoggingPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketLogging
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyUnencryptedConnections
            Effect: Deny
            Principal: "*"
            Action: "s3:*"
            Resource:
              - !Sub "arn:aws:s3:::${S3BucketLogging}"
              - !Sub "arn:aws:s3:::${S3BucketLogging}/*"
            Condition:
              Bool:
                aws:SecureTransport: false
          - Sid: S3ServerAccessLogsPolicy
            Effect: Allow
            Principal:
              Service: logging.s3.amazonaws.com
            Action: "s3:PutObject"
            Resource:
              - !Sub "arn:aws:s3:::${S3BucketLogging}/s3-cache-access-logs/*"
              - !Sub "arn:aws:s3:::${S3BucketLogging}/s3-config-access-logs/*"
            Condition:
              StringEquals:
                aws:SourceAccount: !Sub ${AWS::AccountId}
              ArnLike:
                aws:Sourcearn:
                  - !Sub "arn:aws:s3:::${S3BucketCache}"
                  - !Sub "arn:aws:s3:::${S3Bucket}"

  EC2InstanceResourceGroup:
    Type: AWS::ResourceGroups::Group
    Properties:
      Name: !Sub "${AWS::StackName}-EC2Instances"
      Description: "Resource group for EC2 instances belonging to this stack"
      ResourceQuery:
        Type: TAG_FILTERS_1_0
        Query:
          ResourceTypeFilters:
            - "AWS::EC2::Instance"
          TagFilters:
            - Key: "runs-on-stack-name"
              Values:
                - !Ref AWS::StackName
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  EC2InstanceLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: !Ref Ec2LogRetentionInDays
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
        - Key: runs-on-resource
          Value: ec2-log-group

  EC2InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasDefaultPermissionBoundaryArn, !Ref DefaultPermissionBoundaryArn, !Ref "AWS::NoValue"]
      ManagedPolicyArns:
        !If 
          - CustomPolicyProvided
          - [!Ref EC2InstanceCustomPolicy, "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", "arn:aws:iam::aws:policy/AmazonElasticContainerRegistryPublicFullAccess"]
          - ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", "arn:aws:iam::aws:policy/AmazonElasticContainerRegistryPublicFullAccess"]
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ReadOnly
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeTags
                  - ec2:DescribeAvailabilityZones
                Resource: "*"
        - PolicyName: CreateTags
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateTags
                Resource: "*"
                Condition:
                  StringEquals:
                    "aws:ARN": "${ec2:SourceInstanceARN}"
        - PolicyName: CreateTagsOnVolumesAndSnapshots
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateTags
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
                  - !Sub "arn:aws:ec2:${AWS::Region}:*:snapshot/*"
        - PolicyName: SendLogs
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:PutLogEvents"
                  - "logs:PutRetentionPolicy"
                  - "logs:DescribeLogStreams"
                  - "logs:DescribeLogGroups"
                  - "logs:CreateLogStream"
                  - "logs:CreateLogGroup"
                Resource:
                - !Sub "${EC2InstanceLogGroup.Arn}"
                - !Sub "${EC2InstanceLogGroup.Arn}:*"
        - PolicyName: PutMetrics
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - cloudwatch:PutMetricData
                Resource: "*"
                Condition:
                  StringEquals:
                    cloudwatch:namespace:
                      - "RunsOn/Runners"
                      - "CWAgent"
        - PolicyName: GetMetrics
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                # can't restrict on specific namespaces unfortunately
                Action:
                  - cloudwatch:GetMetricData
                  - cloudwatch:GetMetricStatistics
                Resource: "*"
        - PolicyName: EC2AccessS3BucketPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                  - s3:ListBucket
                  - s3:GetBucketLocation
                  - s3:ListBucketMultipartUploads
                  - s3:ListMultipartUploadParts
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketCache}"
                  - !Sub "arn:aws:s3:::${S3BucketCache}/cache/*"
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource:
                  - !Join
                    - ""
                    - [
                        !Sub "arn:aws:s3:::${S3BucketCache}/runners/",
                        "${aws:userid}/*",
                      ]
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource:
                  - !Sub "arn:aws:s3:::${S3Bucket}/agents/*"
        - PolicyName: VolumeSnapshotDescribe
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeVolumes
                  - ec2:DescribeSnapshots
                Resource: "*"
        - PolicyName: VolumeSnapshotCreate
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateVolume
                  - ec2:CreateSnapshot
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
                  - !Sub "arn:aws:ec2:${AWS::Region}::snapshot/*" # no account id in snapshot ARN
        - PolicyName: VolumeSnapshotLifecycle
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:AttachVolume
                  - ec2:DetachVolume
                  - ec2:DeleteVolume
                  - ec2:DeleteSnapshot
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
                  - !Sub "arn:aws:ec2:${AWS::Region}::snapshot/*"
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                Condition:
                  StringEquals:
                    "aws:ResourceTag/runs-on-stack-name": !Ref AWS::StackName
        - PolicyName: EnableDetailedMonitoring
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:MonitorInstances
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                Condition:
                  StringEquals:
                    "aws:ResourceTag/runs-on-stack-name": !Ref AWS::StackName
        - !If
          - CreateEfs
          - PolicyName: EfsMountAccess
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action:
                    # Required for mounting
                    - elasticfilesystem:ClientMount
                    - elasticfilesystem:ClientWrite
                    - elasticfilesystem:DescribeMountTargets
                    # Required by efs-utils helper to find mount target IP
                    - ec2:DescribeSubnets
                    - ec2:DescribeNetworkInterfaces
                  Resource: "*" # Scoping down requires knowing EFS ARN, Subnet ARNs, etc. Keep broad for simplicity.
          - !Ref AWS::NoValue
        - !If
          - CreateEphemeralRegistry
          - PolicyName: EphemeralRegistryAccess
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action:
                    # Needed for docker login
                    - ecr:GetAuthorizationToken
                  Resource: "*" # GetAuthorizationToken requires "*" resource
                - Effect: Allow
                  Action:
                    # Pull permissions
                    - ecr:BatchCheckLayerAvailability
                    - ecr:GetDownloadUrlForLayer
                    - ecr:BatchGetImage
                    # Push permissions
                    - ecr:InitiateLayerUpload
                    - ecr:UploadLayerPart
                    - ecr:CompleteLayerUpload
                    - ecr:PutImage
                  Resource: !GetAtt EphemeralRegistry.Arn
          - !Ref AWS::NoValue
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EC2InstanceRole

  SchedulerCostReport:
    Condition: CostReportsEnabled
    Type: AWS::Scheduler::Schedule
    Properties:
      ScheduleExpression: "cron(5 0 * * ? *)"
      ScheduleExpressionTimezone: "UTC"
      FlexibleTimeWindow:
        Mode: "OFF"
      Target:
        Arn: !GetAtt RunsOnQueueEvents.Arn
        RoleArn: !GetAtt SchedulerInvokeRole.Arn
        RetryPolicy:
          MaximumRetryAttempts: 0
        Input: '{"detail-type":"RunsOn Cost Report"}'

  SchedulerCostAllocationTag:
    Condition: CostReportsEnabled
    Type: AWS::Scheduler::Schedule
    Properties:
      ScheduleExpression: "cron(10 0 * * ? *)"
      ScheduleExpressionTimezone: "UTC"
      FlexibleTimeWindow:
        Mode: "OFF"
      Target:
        Arn: !GetAtt RunsOnQueueEvents.Arn
        RoleArn: !GetAtt SchedulerInvokeRole.Arn
        RetryPolicy:
          MaximumRetryAttempts: 0
        Input: '{"detail-type":"RunsOn Cost Allocation Tag"}'

  SchedulerInvokeRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasDefaultPermissionBoundaryArn, !Ref DefaultPermissionBoundaryArn, !Ref "AWS::NoValue"]
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: scheduler.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: "AllowSQSSendMessage"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: sqs:SendMessage
                Resource: !GetAtt RunsOnQueueEvents.Arn

  RunsOnServiceAutoScalingConfiguration:
    Type: AWS::AppRunner::AutoScalingConfiguration
    Properties:
      MaxConcurrency: 100
      MaxSize: 25
      MinSize: 1
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnService:
    Type: AWS::AppRunner::Service
    Properties:
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
      InstanceConfiguration:
        Cpu: !Ref AppCPU
        Memory: !Ref AppMemory
        InstanceRoleArn: !GetAtt RunsOnServiceRole.Arn
      NetworkConfiguration:
        EgressConfiguration:
          EgressType: !If [PrivateEnabled, VPC, DEFAULT]
          VpcConnectorArn: !If [PrivateEnabled, !Ref RunsOnServiceVpcConnector, !Ref "AWS::NoValue"]
        IngressConfiguration:
          IsPubliclyAccessible: true
        IpAddressType: IPV4
      HealthCheckConfiguration:
        Path: /ping
        Protocol: HTTP
        HealthyThreshold: 1
        UnhealthyThreshold: 10
        Interval: 3
      AutoScalingConfigurationArn: !Ref RunsOnServiceAutoScalingConfiguration
      SourceConfiguration:
        ImageRepository:
          ImageConfiguration:
            Port: 8080
            RuntimeEnvironmentVariables:
              - Name: RUNS_ON_AWS_ACCOUNT_ID
                Value: !Ref AWS::AccountId
              - Name: RUNS_ON_ENV
                Value: !Ref Environment
              - Name: RUNS_ON_COST_ALLOCATION_TAG
                Value: !Ref CostAllocationTag
              - Name: RUNS_ON_STACK_NAME
                Value: !Ref AWS::StackName
              - Name: RUNS_ON_LOCKS_TABLE
                Value: !Ref RunsOnLocksTable
              - Name: RUNS_ON_WORKFLOW_JOBS_TABLE
                Value: !Ref RunsOnWorkflowJobsTable
              - Name: RUNS_ON_NETWORKING_STACK
                Value: !Ref NetworkingStack
              - Name: RUNS_ON_GITHUB_ORGANIZATION
                Value: !Ref GithubOrganization
              - Name: RUNS_ON_APP_TAG
                Value: !FindInMap [App, Tags, AppTag]
              - Name: RUNS_ON_BOOTSTRAP_TAG
                Value: !FindInMap [App, Tags, BootstrapTag]
              - Name: RUNS_ON_LICENSE_KEY
                Value: !Ref LicenseKey
              - Name: RUNS_ON_RUNNER_CUSTOM_TAGS
                Value: !Join [",", !Ref RunnerCustomTags]
              - Name: RUNS_ON_BUCKET_CONFIG
                Value: !Ref S3Bucket
              - Name: RUNS_ON_BUCKET_CACHE
                Value: !Ref S3BucketCache
              - Name: RUNS_ON_VPC_ID
                Value: !If [ExternalVpc, !Ref ExternalVpcId, !Ref VPC]
              - Name: RUNS_ON_SECURITY_GROUP_ID
                Value: !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
              - Name: RUNS_ON_INSTANCE_PROFILE_ARN
                Value: !GetAtt EC2InstanceProfile.Arn
              - Name: RUNS_ON_INSTANCE_ROLE_NAME
                Value: !Ref EC2InstanceRole
              - Name: RUNS_ON_TOPIC_ARN
                Value: !Ref AlertTopic
              - Name: RUNS_ON_REGION
                Value: !Ref AWS::Region
              - Name: RUNS_ON_SSH_ALLOWED
                Value: !Ref SSHAllowed
              - Name: RUNS_ON_APP_EC2_QUEUE_SIZE
                Value: !Ref AppEc2QueueSize
              - Name: RUNS_ON_EBS_ENCRYPTION_KEY
                Value: !If [HasEncryptEbs, "alias/aws/ebs", ""]
              - Name: RUNS_ON_APP_GITHUB_API_STRATEGY
                Value: !Ref AppGithubApiStrategy
              - Name: RUNS_ON_PUBLIC_SUBNET_IDS
                Value: !If [ExternalVpc,
                  !If [ExternalVpcPublicSubnetIdsProvided, !Join [",", !Ref ExternalVpcPublicSubnetIds], "-"],
                  !Join [",", [!Ref PublicSubnet1, !Ref PublicSubnet2, !Ref PublicSubnet3]]]
              - Name: RUNS_ON_PRIVATE_SUBNET_IDS
                Value: !If [ExternalVpc,
                  !If [ExternalVpcPrivateSubnetIdsProvided, !Join [",", !Ref ExternalVpcPrivateSubnetIds], "-"],
                  !Join [",", [!Ref PrivateSubnet1, !Ref PrivateSubnet2, !Ref PrivateSubnet3]]]
              - Name: RUNS_ON_PRIVATE
                Value: !Ref Private
              - Name: RUNS_ON_DEFAULT_ADMINS
                Value: !Ref DefaultAdmins
              - Name: RUNS_ON_RUNNER_MAX_RUNTIME
                Value: !Ref RunnerMaxRuntime
              - Name: RUNS_ON_RUNNER_CONFIG_AUTO_EXTENDS_FROM
                Value: !Ref RunnerConfigAutoExtendsFrom
              - Name: RUNS_ON_LAUNCH_TEMPLATE_LINUX_DEFAULT
                Value: !Join [":", [!Ref EC2FleetLaunchTemplateLinuxDefault, !GetAtt EC2FleetLaunchTemplateLinuxDefault.LatestVersionNumber]]
              - Name: RUNS_ON_LAUNCH_TEMPLATE_WINDOWS_DEFAULT
                Value: !Join [":", [!Ref EC2FleetLaunchTemplateWindowsDefault, !GetAtt EC2FleetLaunchTemplateWindowsDefault.LatestVersionNumber]]
              - Name: RUNS_ON_LAUNCH_TEMPLATE_LINUX_PRIVATE
                Value: !Join [":", [!Ref EC2FleetLaunchTemplateLinuxPrivate, !GetAtt EC2FleetLaunchTemplateLinuxPrivate.LatestVersionNumber]]
              - Name: RUNS_ON_LAUNCH_TEMPLATE_WINDOWS_PRIVATE
                Value: !Join [":", [!Ref EC2FleetLaunchTemplateWindowsPrivate, !GetAtt EC2FleetLaunchTemplateWindowsPrivate.LatestVersionNumber]]
              - Name: RUNS_ON_RUNNER_DEFAULT_DISK_SIZE
                Value: !Ref RunnerDefaultDiskSize
              - Name: RUNS_ON_RUNNER_DEFAULT_VOLUME_THROUGHPUT
                Value: !Ref RunnerDefaultVolumeThroughput
              - Name: RUNS_ON_RUNNER_LARGE_DISK_SIZE
                Value: !Ref RunnerLargeDiskSize
              - Name: RUNS_ON_RUNNER_LARGE_VOLUME_THROUGHPUT
                Value: !Ref RunnerLargeVolumeThroughput
              - Name: RUNS_ON_QUEUE
                Value: !Ref RunsOnQueue
              - Name: RUNS_ON_QUEUE_POOL
                Value: !Ref RunsOnQueuePool
              - Name: RUNS_ON_QUEUE_HOUSEKEEPING
                Value: !Ref RunsOnQueueHousekeeping
              - Name: RUNS_ON_QUEUE_TERMINATION
                Value: !Ref RunsOnQueueTermination
              - Name: RUNS_ON_QUEUE_EVENTS
                Value: !Ref RunsOnQueueEvents
              - Name: RUNS_ON_QUEUE_JOBS
                Value: !Ref RunsOnQueueJobs
              - Name: RUNS_ON_QUEUE_GITHUB
                Value: !Ref RunsOnQueueGithub
              - Name: RUNS_ON_COST_REPORTS_ENABLED
                Value: !Ref CostReportsEnabled
              - Name: RUNS_ON_SERVER_PASSWORD
                Value: !Ref ServerPassword
              - Name: RUNS_ON_SPOT_CIRCUIT_BREAKER
                Value: !Ref SpotCircuitBreaker
              - Name: RUNS_ON_INTEGRATION_STEP_SECURITY_API_KEY
                Value: !Ref IntegrationStepSecurityApiKey
              - Name: RUNS_ON_GITHUB_ENTERPRISE_URL
                Value: !Ref GithubEnterpriseUrl
              - Name: OTEL_EXPORTER_OTLP_ENDPOINT
                Value: !Ref OtelExporterEndpoint
              - Name: OTEL_EXPORTER_OTLP_HEADERS
                Value: !Ref OtelExporterHeaders
              - Name: RUNS_ON_LOGGER_LEVEL
                Value: !Ref LoggerLevel
          ImageIdentifier: !Sub
            - "${AppRegistry}:${ImageTag}"
            - ImageTag: !FindInMap [App, Tags, ImageTag]
          ImageRepositoryType: ECR_PUBLIC

  RunsOnServiceVpcConnector:
    Type: AWS::AppRunner::VpcConnector
    Condition: PrivateEnabled
    Properties:
      Subnets: !If [ExternalVpc, !Ref ExternalVpcPrivateSubnetIds, [!Ref PrivateSubnet1, !Ref PrivateSubnet2, !Ref PrivateSubnet3]]
      SecurityGroups: !If [ExternalVpc, [!Ref ExternalVpcSecurityGroupId], [!Ref SecurityGroup]]
      Tags: # note: adding a new tag requires a full replacement
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName

  RunsOnServiceRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasDefaultPermissionBoundaryArn, !Ref DefaultPermissionBoundaryArn, !Ref "AWS::NoValue"]
      ManagedPolicyArns:
        !If [AppCustomPolicyProvided, [!Ref AppCustomPolicy], []]
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - tasks.apprunner.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: AppRunnerEC2Permissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeImages
                  - ec2:DescribeInstances
                  - ec2:DescribeSubnets
                  - ec2:DescribeRouteTables
                  - ec2:DescribeVolumes
                  - ec2:DescribeSnapshots
                  - ec2:DescribeLaunchTemplateVersions
                  - ce:GetCostAndUsage
                  - ce:UpdateCostAllocationTagsStatus
                  - cloudwatch:PutMetricData
                  - cloudwatch:GetMetricData
                  - cloudwatch:DescribeAlarms
                  - cloudtrail:LookupEvents
                  - iam:GetRole
                  - sns:ListSubscriptionsByTopic
                Resource: "*"
              - Effect: Allow
                Action:
                  - iam:CreateServiceLinkedRole
                Resource: "arn:aws:iam::*:role/aws-service-role/spot.amazonaws.com/AWSServiceRoleForEC2Spot"
                Condition:
                  StringEquals:
                    "iam:AWSServiceName": "spot.amazonaws.com"
              - Effect: Allow
                Action:
                  - ec2:CreateFleet
                  - ec2:DeleteFleets
                Resource: "*"
              - Effect: Allow
                Action:
                  - ec2:CreateTags
                  - ec2:RunInstances
                Resource:
                  - !Sub "arn:aws:ec2:${AWS::Region}::image/*"
                  - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:*"
              - Effect: Allow
                Action:
                  - iam:PassRole
                  - iam:GetRole
                Resource: !GetAtt EC2InstanceRole.Arn
              - Effect: Allow
                Action:
                  - ssm:PutParameter
                  - ssm:GetParameter
                  - ssm:GetParameters
                  - ssm:DeleteParameter
                  - ssm:DeleteParameters
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
              - Effect: Allow
                Action:
                  - ec2:TerminateInstances
                  - ec2:StopInstances
                  - ec2:StartInstances
                Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                Condition:
                  StringEquals:
                    "aws:ResourceTag/runs-on-stack-name": !Ref AWS::StackName
              - Effect: Allow
                Action:
                  - ec2:DeleteVolume
                  - ec2:DeleteSnapshot
                Resource: "*"
                Condition:
                  StringEquals:
                    "aws:ResourceTag/runs-on-stack-name": !Ref AWS::StackName
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:ListBucket
                Resource:
                  - Fn::Sub: arn:aws:s3:::${S3Bucket}
                  - Fn::Sub: arn:aws:s3:::${S3Bucket}/*
              - Effect: Allow
                Action:
                  - s3:DeleteObject
                Resource:
                  - Fn::Sub: arn:aws:s3:::${S3Bucket}/runs-on/db/*
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - Fn::Sub: arn:aws:s3:::${S3BucketCache}
                  - Fn::Sub: arn:aws:s3:::${S3BucketCache}/runners/*
                  - Fn::Sub: arn:aws:s3:::${S3BucketCache}/agents/*
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref AlertTopic
              - Effect: Allow
                Action:
                  - sqs:SendMessage
                  - sqs:ReceiveMessage
                  - sqs:DeleteMessage
                  - sqs:GetQueueAttributes
                Resource:
                  - !GetAtt RunsOnQueue.Arn
                  - !GetAtt RunsOnQueuePool.Arn
                  - !GetAtt RunsOnQueueHousekeeping.Arn
                  - !GetAtt RunsOnQueueTermination.Arn
                  - !GetAtt RunsOnQueueEvents.Arn
                  - !GetAtt RunsOnQueueJobs.Arn
                  - !GetAtt RunsOnQueueGithub.Arn
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource: !GetAtt RunsOnLocksTable.Arn
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:BatchGetItem
                  - dynamodb:UpdateItem
                  - dynamodb:Query
                  - dynamodb:Scan
                Resource:
                  - !GetAtt RunsOnWorkflowJobsTable.Arn
                  - !Sub "${RunsOnWorkflowJobsTable.Arn}/index/*"
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/apprunner/RunsOnService-*"

  MinutesPerDayAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "RunsOn daily usage exceeds ${AppAlarmDailyMinutes} minutes"
      Namespace: RunsOn
      MetricName: minutesNoDimension
      Dimensions:
        - Name: StackName
          Value: !Ref AWS::StackName
      Statistic: Sum
      Period: 86400 # 24h
      EvaluationPeriods: 1
      Threshold: !Ref AppAlarmDailyMinutes
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueueDeadLetter:
    Type: AWS::SQS::Queue
    Properties:
      FifoQueue: true
      MessageRetentionPeriod: 259200 # 3 days

  RunsOnQueueDeadLetterPolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues:
        - !Ref RunsOnQueueDeadLetter
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: "*"
            Action: "sqs:SendMessage"
            Resource: !GetAtt RunsOnQueueDeadLetter.Arn
            Condition:
              ArnEquals:
                aws:SourceArn: !Ref RunsOnQueue

  RunsOnQueue:
    Type: AWS::SQS::Queue
    Properties:
      FifoQueue: true
      ContentBasedDeduplication: true
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 10
      VisibilityTimeout: 120 # AWS recommends 6 times the max processing time
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt RunsOnQueueDeadLetter.Arn
        maxReceiveCount: 5
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueJobsDeadLetter:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 259200 # 3 days
      FifoQueue: true
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueJobs:
    Type: AWS::SQS::Queue
    Properties:
      FifoQueue: true
      ContentBasedDeduplication: false
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 10
      VisibilityTimeout: 120 # 2 minutes for scheduling
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt RunsOnQueueJobsDeadLetter.Arn
        maxReceiveCount: 3
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueGithubDeadLetter:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 259200 # 3 days
      FifoQueue: true
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueGithub:
    Type: AWS::SQS::Queue
    Properties:
      FifoQueue: true
      ContentBasedDeduplication: false
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 10
      VisibilityTimeout: 120 # GitHub API calls + S3 uploads
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt RunsOnQueueGithubDeadLetter.Arn
        maxReceiveCount: 3
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueuePoolDeadLetter:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 259200 # 3 days
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueuePool:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 10
      VisibilityTimeout: 120
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt RunsOnQueuePoolDeadLetter.Arn
        maxReceiveCount: 5
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueHousekeeping:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 20
      VisibilityTimeout: 120
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueTermination:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 86400 # 24h
      ReceiveMessageWaitTimeSeconds: 20
      VisibilityTimeout: 120
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueEvents:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 7200
      ReceiveMessageWaitTimeSeconds: 20
      VisibilityTimeout: 120
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  # DynamoDB table for distributed locking
  RunsOnLocksTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub "${AWS::StackName}-locks"
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: key
          AttributeType: S
      KeySchema:
        - AttributeName: key
          KeyType: HASH
      TimeToLiveSpecification:
        AttributeName: expiresAt
        Enabled: true
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  # DynamoDB table for workflow job tracking
  RunsOnWorkflowJobsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub "${AWS::StackName}-workflow-jobs"
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: job_id
          AttributeType: N
        - AttributeName: next_check_partition
          AttributeType: S
        - AttributeName: next_check_at_unix
          AttributeType: N
        - AttributeName: created_at_date
          AttributeType: S
        - AttributeName: created_at_unix
          AttributeType: N
      KeySchema:
        - AttributeName: job_id
          KeyType: HASH
      GlobalSecondaryIndexes:
        - IndexName: next-check-index
          KeySchema:
            - AttributeName: next_check_partition
              KeyType: HASH
            - AttributeName: next_check_at_unix
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
        - IndexName: daily-activity-index
          KeySchema:
            - AttributeName: created_at_date
              KeyType: HASH
            - AttributeName: created_at_unix
              KeyType: RANGE
          Projection:
            ProjectionType: INCLUDE
            NonKeyAttributes:
              - installation_id
              - org_name
              - repo_name
              - job_id
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  RunsOnQueueDeadLetterOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueueDeadLetter oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueueDeadLetter.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueueOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueue oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueue.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueuePoolOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueuePool oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueuePool.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueueHousekeepingOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueueHousekeeping oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueueHousekeeping.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueueTerminationOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueueTermination oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueueTermination.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  RunsOnQueueEventsOldestMessageAlarm:
    Condition: CreateSqsQueueOldestMessageAlarms
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: !Sub "SQS queue RunsOnQueueEvents oldest message age excedes ${SqsQueueOldestMessageThresholdSeconds} seconds."
      Namespace: AWS/SQS
      MetricName: ApproximateAgeOfOldestMessage
      Dimensions:
      - Name: QueueName
        Value: !GetAtt RunsOnQueueEvents.QueueName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 1
      Threshold: !Ref SqsQueueOldestMessageThresholdSeconds
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: "notBreaching"
      AlarmActions:
        - !Ref AlertTopic
      OKActions:
        - !Ref AlertTopic

  SpotInstanceEventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Rule to capture EC2 Spot Instance interruption notices"
      EventPattern:
        source:
          - aws.ec2
        detail-type:
          - EC2 Spot Instance Interruption Warning
          - EC2 Instance State-change Notification
      State: ENABLED
      Targets:
        - Arn: !GetAtt RunsOnQueueEvents.Arn
          Id: InstanceEvents

  SpotInstanceEventRulePermission:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues:
        - !Ref RunsOnQueueEvents
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sqs:SendMessage
            Resource: !GetAtt RunsOnQueueEvents.Arn
            Condition:
              ArnEquals:
                aws:SourceArn: !GetAtt SpotInstanceEventRule.Arn

  AlertTopic:
    Type: AWS::SNS::Topic
    Properties:
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
      DisplayName: RunsOn Alerts

  AlertTopicSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      TopicArn: !Ref AlertTopic
      Endpoint: !If [EmailProvided, !Ref EmailAddress, ""]

  AlertTopicSubscriptionHttps:
    Condition: AlertTopicSubscriptionHttpsProvided
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: https
      TopicArn: !Ref AlertTopic
      Endpoint: !Ref AlertTopicSubscriptionHttpsEndpoint

  AlertTopicSlackWebhookRole:
    Condition: AlertTopicSlackWebhookProvided
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

  AlertTopicSlackWebhookFunction:
    Condition: AlertTopicSlackWebhookProvided
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.11
      Handler: index.handler
      Role: !GetAtt AlertTopicSlackWebhookRole.Arn
      Timeout: 10
      MemorySize: 128
      Environment:
        Variables:
          SLACK_WEBHOOK_URL: !Ref AlertTopicSlackWebhookUrl
          STACK_NAME: !Ref AWS::StackName
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName
      Code:
        ZipFile: |
          import json
          import logging
          import os
          import time
          import urllib.error
          import urllib.request

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def get_alert_color(subject, state=None):
              """Determine the color for a Slack alert based on content."""
              if state:
                  state_lower = state.lower()
                  if state_lower == "alarm":
                      return "danger"
                  elif state_lower == "ok":
                      return "good" 
                  elif state_lower == "insufficient_data":
                      return "warning"
              
              if subject:
                  subject_lower = subject.lower()
                  if any(word in subject_lower for word in ["error", "fail", "❌"]):
                      return "danger"
                  elif any(word in subject_lower for word in ["warn", "⚠️", "unable"]):
                      return "warning"
                  elif any(word in subject_lower for word in ["success", "✅", "complete"]):
                      return "good"
              
              return "#439FE0"  # Default blue color

          def create_attachment(title, text, color, stack_name):
              """Create a Slack attachment."""
              return {
                  "color": color,
                  "title": title,
                  "text": text,
                  "footer": stack_name,
                  "ts": int(time.time())
              }

          def handler(event, context):
              webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
              stack_name = os.environ.get("STACK_NAME", "RunsOn")
              
              if not webhook_url:
                  logger.error("Slack webhook URL is not configured")
                  return {"statusCode": 500, "body": "Slack webhook URL is not configured"}

              records = event.get("Records", [])
              for record in records:
                  sns = record.get("Sns", {})
                  subject = sns.get("Subject", "")
                  message = sns.get("Message", "")

                  # Base payload with username and icon
                  payload = {
                      "username": stack_name,
                      "icon_url": "https://runs-on.com/logo.png"
                  }

                  # Try to parse message as JSON (CloudWatch alarms)
                  try:
                      alarm_data = json.loads(message)
                      if "AlarmName" in alarm_data:
                          # CloudWatch alarm formatting
                          state = alarm_data.get("NewStateValue", "UNKNOWN")
                          state_emoji = {"ALARM": ":rotating_light:", "OK": ":white_check_mark:", "INSUFFICIENT_DATA": ":warning:"}.get(state, ":question:")
                          title = f"{state_emoji} CloudWatch Alarm: {state}"
                          text = f"*Alarm:* {alarm_data.get('AlarmName', 'N/A')}\n*Reason:* {alarm_data.get('NewStateReason', 'N/A')}"
                          color = get_alert_color(None, state)
                      else:
                          # JSON but not an alarm, format as code block
                          title = subject if subject else "JSON Message"
                          text = f"```\n{json.dumps(alarm_data, indent=2)}\n```"
                          color = get_alert_color(subject)
                  except (json.JSONDecodeError, TypeError):
                      # Plain text message
                      title = subject if subject else "Alert"
                      text = message
                      color = get_alert_color(subject)

                  payload["attachments"] = [create_attachment(title, text, color, stack_name)]

                  data = json.dumps(payload).encode("utf-8")
                  request = urllib.request.Request(
                      webhook_url,
                      data=data,
                      headers={"Content-Type": "application/json"},
                      method="POST",
                  )

                  try:
                      with urllib.request.urlopen(request, timeout=5) as response:
                          logger.info("Sent alert to Slack with status %s", response.status)
                  except urllib.error.HTTPError as error:
                      logger.error(
                          "Failed to send alert to Slack: HTTP %s %s", error.code, error.reason
                      )
                  except Exception as error:  # noqa: BLE001
                      logger.exception("Unexpected error sending alert to Slack: %s", error)

              return {"statusCode": 200, "body": "Processed SNS records"}

  AlertTopicSlackWebhookPermission:
    Condition: AlertTopicSlackWebhookProvided
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref AlertTopicSlackWebhookFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref AlertTopic

  AlertTopicSlackWebhookSubscription:
    Condition: AlertTopicSlackWebhookProvided
    Type: AWS::SNS::Subscription
    DependsOn:
      - AlertTopicSlackWebhookPermission
    Properties:
      Protocol: lambda
      TopicArn: !Ref AlertTopic
      Endpoint: !GetAtt AlertTopicSlackWebhookFunction.Arn

  DashboardStack:
    Condition: CreateDashboard
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://runs-on.s3.eu-west-1.amazonaws.com/cloudformation/dashboard/template-v2.11.0.yaml"
      Parameters:
        LogGroupName: !Join
          - ""
          - - "/aws/apprunner/"
            - !Select
              - 1
              - !Split
                - "service/"
                - !GetAtt RunsOnService.ServiceArn
            - "/application"
        DashboardName: !Sub "${AWS::StackName}-Dashboard"
        QueueName: !GetAtt RunsOnQueue.QueueName
      Tags:
        - Key: !Ref CostAllocationTag
          Value: !Ref AWS::StackName
        - Key: runs-on-stack-name
          Value: !Ref AWS::StackName

Outputs:
  RunsOnAwsAccountId:
    Description: AWS Account ID
    Value: !Ref AWS::AccountId
  RunsOnEntryPoint:
    Description: Entrypoint for the RunsOn service
    Value: !GetAtt RunsOnService.ServiceUrl
  RunsOnService:
    Description: Link to the AppRunner service in AWS console
    Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/apprunner/home?region=${AWS::Region}#/services/dashboard?service_arn=${RunsOnService}"
  RunsOnServiceLogGroupName:
    Description: Log group for the RunsOn service
    Value: !Join
      - ""
      - - "/aws/apprunner/"
        - !Select
          - 1
          - !Split
            - "service/"
            - !GetAtt RunsOnService.ServiceArn
        - "/application"
  RunsOnAppTag:
    Description: RunsOn application tag
    Value: !FindInMap [App, Tags, AppTag]
  RunsOnBootstrapTag:
    Description: RunsOn bootstrap tag
    Value: !FindInMap [App, Tags, BootstrapTag]
  RunsOnInstanceRoleName:
    Description: Name of the instance role for the RunsOn service
    Value: !Ref EC2InstanceRole
  RunsOnRegion:
    Description: AWS region
    Value: !Ref AWS::Region
  RunsOnVPCId:
    Description: VPC ID
    Value: !If [EmbeddedVpc, !Ref VPC, !Ref ExternalVpcId]
    Export:
      Name: !Sub "${AWS::StackName}-VPCId"
  RunsOnPublicSubnetIds:
    Description: Public subnet IDs
    Value: !If [ExternalVpc,
      !If [ExternalVpcPublicSubnetIdsProvided, !Join [",", !Ref ExternalVpcPublicSubnetIds], "-"],
      !Join [",", [!Ref PublicSubnet1, !Ref PublicSubnet2, !Ref PublicSubnet3]]]
  RunsOnPrivateSubnetIds:
    Description: Private subnet IDs
    Value: !If [ExternalVpc,
      !If [ExternalVpcPrivateSubnetIdsProvided, !Join [",", !Ref ExternalVpcPrivateSubnetIds], "-"],
      !Join [",", [!Ref PrivateSubnet1, !Ref PrivateSubnet2, !Ref PrivateSubnet3]]]
  RunsOnVpcCidrBlock:
    Description: VPC CIDR block
    Value: !If [EmbeddedVpc, !Ref VpcCidrBlock, "-"]
    Export:
      Name: !Sub "${AWS::StackName}-VpcCidrBlock"
  RunsOnPublicRouteTableId:
    Description: Public Route Table ID
    Value: !If [EmbeddedVpc, !Ref PublicRouteTable, "-"]
    Export:
      Name: !Sub "${AWS::StackName}-PublicRouteTableId"
  RunsOnPrivateRouteTable1Id:
    Description: Private Route Table 1 ID
    Value: !If [EmbeddedVpc, !Ref PrivateSubnet1RouteTable, "-"]
    Export:
      Name: !Sub "${AWS::StackName}-PrivateRouteTable1Id"
  RunsOnPrivateRouteTable2Id:
    Description: Private Route Table 2 ID
    Value: !If [EmbeddedVpc, !Ref PrivateSubnet2RouteTable, "-"]
    Export:
      Name: !Sub "${AWS::StackName}-PrivateRouteTable2Id"
  RunsOnPrivateRouteTable3Id:
    Description: Private Route Table 3 ID
    Value: !If [EmbeddedVpc, !Ref PrivateSubnet3RouteTable, "-"]
    Export:
      Name: !Sub "${AWS::StackName}-PrivateRouteTable3Id"
  RunsOnEgressStaticIps:
    Description: Static IPs for egress traffic (if configured)
    Value: !If
      - HasPrivateSubnet
      - !Join
        - ","
        - !Split
          - ","
          - !Join
            - ","
            - - !If [CreateElasticIP1, !Ref ElasticIP1, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP2, !Ref ElasticIP2, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP3, !Ref ElasticIP3, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP4, !Ref ElasticIP4, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP5, !Ref ElasticIP5, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP6, !Ref ElasticIP6, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP7, !Ref ElasticIP7, !Ref "AWS::NoValue"]
              - !If [CreateElasticIP8, !Ref ElasticIP8, !Ref "AWS::NoValue"]
      - ""
  RunsOnPrivate:
    Description: Private subnets enabled
    Value: !Ref Private
  RunsOnSecurityGroupId:
    Description: Security group for runners
    Value: !If [ExternalVpc, !Ref ExternalVpcSecurityGroupId, !Ref SecurityGroup]
  RunsOnBucketConfig:
    Description: S3 bucket for storing configuration
    Value: !Ref S3Bucket
  RunsOnBucketCache:
    Description: S3 bucket for storing cache artifacts
    Value: !Ref S3BucketCache
  RunsOnTopicArn:
    Description: SNS Topic where email alerts and reports are sent
    Value: !Ref AlertTopic
  RunsOnLaunchTemplateLinuxDefault:
    Description: Default Linux launch template for the RunsOn service
    Value: !Join [":", [!Ref EC2FleetLaunchTemplateLinuxDefault, !GetAtt EC2FleetLaunchTemplateLinuxDefault.LatestVersionNumber]]
  RunsOnLaunchTemplateWindowsDefault:
    Description: Default Windows launch template for the RunsOn service
    Value: !Join [":", [!Ref EC2FleetLaunchTemplateWindowsDefault, !GetAtt EC2FleetLaunchTemplateWindowsDefault.LatestVersionNumber]]
  RunsOnLaunchTemplateLinuxPrivate:
    Description: Private Linux launch template for the RunsOn service
    Value: !Join [":", [!Ref EC2FleetLaunchTemplateLinuxPrivate, !GetAtt EC2FleetLaunchTemplateLinuxPrivate.LatestVersionNumber]]
  RunsOnLaunchTemplateWindowsPrivate:
    Description: Private Windows launch template for the RunsOn service
    Value: !Join [":", [!Ref EC2FleetLaunchTemplateWindowsPrivate, !GetAtt EC2FleetLaunchTemplateWindowsPrivate.LatestVersionNumber]]
  RunsOnQueue:
    Description: SQS queue for the RunsOn service
    Value: !Ref RunsOnQueue
  RunsOnQueueJobs:
    Description: SQS FIFO queue for workflow job scheduling
    Value: !Ref RunsOnQueueJobs
  RunsOnQueueGithub:
    Description: SQS FIFO queue for GitHub registration tasks
    Value: !Ref RunsOnQueueGithub
  RunsOnQueuePool:
    Description: SQS queue for the RunsOn service, used for pool management tasks
    Value: !Ref RunsOnQueuePool
  RunsOnQueueHousekeeping:
    Description: SQS queue for the RunsOn service, used for non-critical housekeeping tasks
    Value: !Ref RunsOnQueueHousekeeping
  RunsOnQueueTermination:
    Description: SQS queue for the RunsOn service, used for runner termination tasks
    Value: !Ref RunsOnQueueTermination
  RunsOnQueueEvents:
    Description: SQS queue for the RunsOn service, used for events from other AWS services
    Value: !Ref RunsOnQueueEvents
  RunsOnLocksTable:
    Description: DynamoDB table for distributed locking
    Value: !Ref RunsOnLocksTable
  RunsOnWorkflowJobsTable:
    Description: DynamoDB table for workflow job tracking
    Value: !Ref RunsOnWorkflowJobsTable
  RunsOnServiceArn:
    Description: ARN of the App Runner service
    Value: !Ref RunsOnService
  RunsOnServiceRoleArn:
    Description: ARN of the App Runner service role
    Value: !GetAtt RunsOnServiceRole.Arn
  RunsOnEC2InstanceLogGroupArn:
    Description: Log group ARN for the EC2 instances
    Value: !Ref EC2InstanceLogGroup
  RunsOnEfsFileSystemId:
    Condition: CreateEfs
    Description: EFS File System ID (if enabled)
    Value: !Ref EfsFileSystem
  EphemeralRegistryUri:
    Condition: CreateEphemeralRegistry
    Description: ECR Repository URI for Ephemeral Registry (if enabled)
    Value: !GetAtt EphemeralRegistry.RepositoryUri
