{"id":13485068,"url":"https://github.com/cdktf/docker-on-aws-ecs-with-terraform-cdk-using-typescript","last_synced_at":"2025-03-27T17:30:47.726Z","repository":{"id":39800736,"uuid":"377857955","full_name":"cdktf/docker-on-aws-ecs-with-terraform-cdk-using-typescript","owner":"cdktf","description":"End to End example for deploying a docker container and a static frontend to AWS ECS and AWS Cloudfront","archived":true,"fork":false,"pushed_at":"2023-11-24T12:06:47.000Z","size":1357,"stargazers_count":66,"open_issues_count":0,"forks_count":23,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-01-07T13:01:31.397Z","etag":null,"topics":["aws","cdk","cdk-examples","cdktf","ecs","example","terraform","terraform-cdk"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cdktf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-06-17T14:24:48.000Z","updated_at":"2024-08-22T18:07:06.000Z","dependencies_parsed_at":"2023-11-09T15:46:04.370Z","dependency_job_id":"afdbc79a-3b63-4782-b8bb-7846d6b061db","html_url":"https://github.com/cdktf/docker-on-aws-ecs-with-terraform-cdk-using-typescript","commit_stats":null,"previous_names":["hashicorp/docker-on-aws-ecs-with-terraform-cdk-using-typescript"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cdktf","download_url":"https://codeload.github.com/cdktf/docker-on-aws-ecs-with-terraform-cdk-using-typescript/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245892459,"owners_count":20689506,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","cdk","cdk-examples","cdktf","ecs","example","terraform","terraform-cdk"],"created_at":"2024-07-31T17:01:44.841Z","updated_at":"2025-03-27T17:30:46.937Z","avatar_url":"https://github.com/cdktf.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Run Docker Container on AWS ECS with Terraform CDK using Typescript\n\n_This repository was created for demo purposes and will not be kept up-to-date with future releases of CDK for Terraform (CDKTF); as such, it has been archived and is no longer supported in any way by HashiCorp. You are welcome to try out the archived version of the code in this example project, but there are no guarantees that it will continue to work with newer versions of CDKTF. We do not recommend directly using this sample code in production projects without extensive testing, and HashiCorp disclaims any and all liability resulting from use of this code._\n\n-----\n\nDid you ever wanted to get a backend service in a Docker container with a static (e.g. React) frontend running on AWS?\nIn this example we are going to walk you through how to set everything up in AWS and how to configure the backend to run against a Postgres Database, all using the CDK for Terraform.\n\nYou can find the application under [`./application`](./application) and the infrastructure under [infrastructure](./infrastructure) in this repository.\n\nFirst of all we start with `cdktf init --template typescript` to get our project setup started. This gives us a `main.ts` file as entrypoint for our infrastructure definition. To start we first need to configure a Virtual Private Cloud (VPC) to host all of our resources in, most services need to have an association with a VPC.\n\n```ts\nimport { TerraformAwsModulesVpcAws as VPC } from \"./.gen/modules/terraform-aws-modules/vpc/aws\";\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    super(scope, name);\n    const region = \"us-east-1\";\n\n    // We need to instanciate all providers we are going to use\n    new AwsProvider(this, \"aws\", {\n      region: REGION,\n    });\n    new DockerProvider(this, \"docker\");\n    new NullProvider(this, \"provider\", {});\n\n    const vpc = new VPC(this, \"vpc\", {\n      // We use the name of the stack\n      name,\n      // We tag every resource with the same set of tags to easily identify the resources\n      tags,\n      cidr: \"10.0.0.0/16\",\n      // We want to run on three availability zones\n      azs: [\"a\", \"b\", \"c\"].map((i) =\u003e `${REGION}${i}`),\n      // We need three CIDR blocks as we have three availability zones\n      privateSubnets: [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"],\n      publicSubnets: [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"],\n      databaseSubnets: [\"10.0.201.0/24\", \"10.0.202.0/24\", \"10.0.203.0/24\"],\n      createDatabaseSubnetGroup: true,\n      enableNatGateway: true,\n      // Using a single NAT Gateway will save us some money, coming with the cost of less redundancy\n      singleNatGateway: true,\n    });\n  }\n}\n\nconst app = new App();\nnew MyStack(app, \"example-docker-aws\");\napp.synth();\n```\n\nNow that we have the VPC set up we need to create a ECS Cluster to host our dockerized application in.\nFor this we create a nice, reusable abstraction that we can share with others:\n\n```ts\nfunction Cluster(scope: Construct, name: string) {\n  const cluster = new EcsCluster(scope, name, {\n    name,\n    capacityProviders: [\"FARGATE\"],\n    tags,\n  });\n\n  return {\n    cluster,\n    // we will discuss this later on\n    runDockerImage(\n      name: string,\n      tag: string,\n      image: Resource,\n      env: Record\u003cstring, string | undefined\u003e\n    ) {\n      // ...\n    },\n  };\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const cluster = Cluster(this, \"cluster\");\n  }\n}\n```\n\nOur service has some dependencies, it needs a load balancer and a postgres database.\nThe service also needs a security group to run in and that security group needs to allow access to the load balancer and to the database.\nWe start by creating a Load Balancer:\n\n```ts\nclass LoadBalancer extends Construct {\n  lb: Lb;\n  lbl: LbListener;\n  vpc: VPC;\n  cluster: EcsCluster;\n\n  constructor(scope: Construct, name: string, vpc: VPC, cluster: EcsCluster) {\n    super(scope, name);\n    this.vpc = vpc;\n    this.cluster = cluster;\n\n    const lbSecurityGroup = new SecurityGroup(\n      scope,\n      `${name}-lb-security-group`,\n      {\n        vpcId: vpc.vpcIdOutput,\n        tags,\n        ingress: [\n          // allow HTTP traffic from everywhere\n          {\n            protocol: \"TCP\",\n            fromPort: 80,\n            toPort: 80,\n            cidrBlocks: [\"0.0.0.0/0\"],\n            ipv6CidrBlocks: [\"::/0\"],\n          },\n        ],\n        egress: [\n          // allow all traffic to every destination\n          {\n            fromPort: 0,\n            toPort: 0,\n            protocol: \"-1\",\n            cidrBlocks: [\"0.0.0.0/0\"],\n            ipv6CidrBlocks: [\"::/0\"],\n          },\n        ],\n      }\n    );\n    this.lb = new Lb(scope, `${name}-lb`, {\n      name,\n      tags,\n      // we want this to be our public load balancer so that cloudfront can access it\n      internal: false,\n      loadBalancerType: \"application\",\n      securityGroups: [lbSecurityGroup.id],\n      subnets: Fn.tolist(vpc.publicSubnetsOutput),\n    });\n\n    this.lbl = new LbListener(scope, `${name}-lb-listener`, {\n      loadBalancerArn: this.lb.arn,\n      port: 80,\n      protocol: \"HTTP\",\n      tags,\n      defaultAction: [\n        // We define a fixed 404 message, just in case\n        {\n          type: \"fixed-response\",\n          fixedResponse: [\n            {\n              contentType: \"text/plain\",\n              statusCode: \"404\",\n              messageBody: \"Could not find the resource you are looking for\",\n            },\n          ],\n        },\n      ],\n    });\n  }\n\n  exposeService(\n    name: string,\n    task: EcsTaskDefinition,\n    serviceSecurityGroup: SecurityGroup\n  ) {\n    // we will discuss this later on\n  }\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const loadBalancer = new LoadBalancer(\n      this,\n      \"loadbalancer\",\n      vpc,\n      cluster.cluster\n    );\n  }\n}\n```\n\nThe `LoadBalancer` resource creates a `SecurityGroup` that allows the Load Balancer to receive traffic on port 80 and send traffic to any destination.\nWe then create a `Lb` resource, which builds an Application Load Balancer (ALB) for us.\nTo receive traffic we create a Load Balancer Listener for port 80. We don't expose port 443 currently, as SSL is handled by CloudFront later on.\nIf we wanted to expose the `Lb` directly on the internet we would change the port to 443 and the protocol to HTTPS while creating a valid certificate.\n\nTo see something in case our backend service is not responding we create a defaultAction that returns a static text.\nWe use a `SecurityGroup` to allow our load balancer to access the service:\n\n```ts\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const serviceSecurityGroup = new SecurityGroup(\n      this,\n      `${name}-service-security-group`,\n      {\n        vpcId: vpc.vpcIdOutput,\n        tags,\n        ingress: [\n          // only allow incoming traffic from our load balancer\n          {\n            protocol: \"TCP\",\n            fromPort: 80,\n            toPort: 80,\n            securityGroups: loadBalancer.lb.securityGroups,\n          },\n        ],\n        egress: [\n          // allow all outgoing traffic\n          {\n            fromPort: 0,\n            toPort: 0,\n            protocol: \"-1\",\n            cidrBlocks: [\"0.0.0.0/0\"],\n            ipv6CidrBlocks: [\"::/0\"],\n          },\n        ],\n      }\n    );\n  }\n}\n```\n\nNow we can use this security group to allow the postgres instance to receive traffic from our service:\n\n```ts\nclass PostgresDB extends Construct {\n  public instance: TerraformAwsModulesRdsAws;\n\n  constructor(\n    scope: Construct,\n    name: string,\n    vpc: VPC,\n    serviceSecurityGroup: SecurityGroup\n  ) {\n    super(scope, name);\n\n    // Create a password stored in the TF State on the fly\n    const password = new Password(scope, `${name}-db-password`, {\n      length: 16,\n      special: false,\n    });\n\n    const dbPort = 5432;\n\n    const dbSecurityGroup = new SecurityGroup(scope, \"db-security-group\", {\n      vpcId: vpc.vpcIdOutput,\n      ingress: [\n        // allow traffic to the DBs port from the service\n        {\n          fromPort: dbPort,\n          toPort: dbPort,\n          protocol: \"TCP\",\n          securityGroups: [serviceSecurityGroup.id],\n        },\n      ],\n      tags,\n    });\n\n    // Using this module: https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest\n    const db = new TerraformAwsModulesRdsAws(scope, \"db\", {\n      identifier: `${name}-db`,\n\n      engine: \"postgres\",\n      engineVersion: \"11.10\",\n      family: \"postgres11\",\n      instanceClass: \"db.t3.micro\",\n      allocatedStorage: \"5\",\n\n      createDbOptionGroup: false,\n      createDbParameterGroup: false,\n      applyImmediately: true,\n\n      name,\n      port: String(dbPort),\n      username: `${name}user`,\n      password: password.result,\n\n      maintenanceWindow: \"Mon:00:00-Mon:03:00\",\n      backupWindow: \"03:00-06:00\",\n\n      // This is necessary due to a shortcoming in our token system to be adressed in\n      // https://github.com/hashicorp/terraform-cdk/issues/651\n      subnetIds: vpc.databaseSubnetsOutput as unknown as any,\n      vpcSecurityGroupIds: [dbSecurityGroup.id],\n      tags,\n    });\n\n    this.instance = db;\n  }\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const db = new PostgresDB(\n      this,\n      \"dockerintegration\",\n      vpc,\n      serviceSecurityGroup\n    );\n  }\n}\n```\n\nWe create a security group restricted to our service to secure that only our service can talk to the database.\nBy using the [AWS RDS Terraform module](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) we can\nleverage all the knowledge that went into creating this module and get our Postgres instance.\n\nTo deploy the ECS Task we first need to have a docker image pushed. It's up to you if you want to push and deploy it with the CDK or if you want to separate your deployment pipeline from your infrastructure. To me, having everything in one go feels easier and more integrated with the cost that the state gets a bit bigger. So let's see how it can be done within the CDK:\n\n```ts\nfunction PushedECRImage(scope: Construct, name: string, projectPath: string) {\n  const repo = new EcrRepository(this, `${name}-ecr`, {\n    name,\n    tags,\n  });\n\n  const auth = new DataAwsEcrAuthorizationToken(this, `${name}-auth`, {\n    dependsOn: [repo],\n    registryId: repo.registryId,\n  });\n\n  const asset = new TerraformAsset(this, `${name}-project`, {\n    path: projectPath,\n  });\n\n  const version = require(`${projectPath}/package.json`).version;\n  const tag = `${repo.repositoryUrl}:${version}-${asset.assetHash}`;\n  // Workaround due to https://github.com/kreuzwerker/terraform-provider-docker/issues/189\n  const image = new Resource(this, `image`, {\n    provisioners: [\n      {\n        type: \"local-exec\",\n        workingDir: asset.path,\n        command: `docker login -u ${auth.userName} -p ${auth.password} ${auth.proxyEndpoint} \u0026\u0026 \n  docker build -t ${this.tag} . \u0026\u0026 \n  docker push ${this.tag}`,\n      },\n    ],\n  });\n\n  return { image, tag };\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const { image: backendImage, tag: backendTag } = PushedECRImage(\n      this,\n      \"backend\",\n      path.resolve(__dirname, \"../application/backend\")\n    );\n  }\n}\n```\n\nFirst we ensure we have an ECR Repository to push our docker image into and we get authentication credentials for it.\nBy using `TerraformAsset` we transfer the backend into the context of Terraform to run our `docker login`, `docker build`, `docker push` chain to get our image pushed.\nI require the package.json of the project so that our image tag is prefixed with the version of the application.\n\nNow that Database, ECS Cluster, and Image are in place we can run our docker image in ECR:\n\n```ts\nclass Cluster extends Construct {\n  public cluster: EcsCluster;\n  // ...\n  public runDockerImage(\n    name: string,\n    tag: string,\n    image: Resource,\n    env: Record\u003cstring, string | undefined\u003e\n  ) {\n    // Role that allows us to get the Docker image\n    const executionRole = new IamRole(this, `${name}-execution-role`, {\n      name: `${name}-execution-role`,\n      tags,\n      inlinePolicy: [\n        {\n          name: \"allow-ecr-pull\",\n          policy: JSON.stringify({\n            Version: \"2012-10-17\",\n            Statement: [\n              {\n                Effect: \"Allow\",\n                Action: [\n                  \"ecr:GetAuthorizationToken\",\n                  \"ecr:BatchCheckLayerAvailability\",\n                  \"ecr:GetDownloadUrlForLayer\",\n                  \"ecr:BatchGetImage\",\n                  \"logs:CreateLogStream\",\n                  \"logs:PutLogEvents\",\n                ],\n                Resource: \"*\",\n              },\n            ],\n          }),\n        },\n      ],\n      // this role shall only be used by an ECS task\n      assumeRolePolicy: JSON.stringify({\n        Version: \"2012-10-17\",\n        Statement: [\n          {\n            Action: \"sts:AssumeRole\",\n            Effect: \"Allow\",\n            Sid: \"\",\n            Principal: {\n              Service: \"ecs-tasks.amazonaws.com\",\n            },\n          },\n        ],\n      }),\n    });\n\n    // Role that allows us to push logs\n    const taskRole = new IamRole(this, `${name}-task-role`, {\n      name: `${name}-task-role`,\n      tags,\n      inlinePolicy: [\n        {\n          name: \"allow-logs\",\n          policy: JSON.stringify({\n            Version: \"2012-10-17\",\n            Statement: [\n              {\n                Effect: \"Allow\",\n                Action: [\"logs:CreateLogStream\", \"logs:PutLogEvents\"],\n                Resource: \"*\",\n              },\n            ],\n          }),\n        },\n      ],\n      assumeRolePolicy: JSON.stringify({\n        Version: \"2012-10-17\",\n        Statement: [\n          {\n            Action: \"sts:AssumeRole\",\n            Effect: \"Allow\",\n            Sid: \"\",\n            Principal: {\n              Service: \"ecs-tasks.amazonaws.com\",\n            },\n          },\n        ],\n      }),\n    });\n\n    // Creates a log group for the task\n    const logGroup = new CloudwatchLogGroup(this, `${name}-loggroup`, {\n      name: `${this.cluster.name}/${name}`,\n      retentionInDays: 30,\n      tags,\n    });\n\n    // Creates a task that runs the docker container\n    const task = new EcsTaskDefinition(this, `${name}-task`, {\n      // We want to wait until the image is actually pushed\n      dependsOn: [image],\n      tags,\n      // These values are fixed for the example, we can make them part of our function invocation if we want to change them\n      cpu: \"256\",\n      memory: \"512\",\n      requiresCompatibilities: [\"FARGATE\", \"EC2\"],\n      networkMode: \"awsvpc\",\n      executionRoleArn: executionRole.arn,\n      taskRoleArn: taskRole.arn,\n      containerDefinitions: JSON.stringify([\n        {\n          name,\n          image: tag,\n          cpu: 256,\n          memory: 512,\n          environment: Object.entries(env).map(([name, value]) =\u003e ({\n            name,\n            value,\n          })),\n          portMappings: [\n            {\n              containerPort: 80,\n              hostPort: 80,\n            },\n          ],\n          logConfiguration: {\n            logDriver: \"awslogs\",\n            options: {\n              // Defines the log\n              \"awslogs-group\": logGroup.name,\n              \"awslogs-region\": REGION,\n              \"awslogs-stream-prefix\": name,\n            },\n          },\n        },\n      ]),\n      family: \"service\",\n    });\n\n    return task;\n  }\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const task = cluster.runDockerImage(\"backend\", backendTag, backendImage, {\n      PORT: \"80\",\n      POSTGRES_USER: db.instance.username,\n      POSTGRES_PASSWORD: db.instance.password,\n      POSTGRES_DB: db.instance.name,\n      POSTGRES_HOST: db.instance.dbInstanceAddressOutput,\n      POSTGRES_PORT: db.instance.dbInstancePortOutput,\n    });\n  }\n}\n```\n\nOur call on the `MainStack` hides a lot of the underlying complexity; the interface is fairly simple: we pass in a name, Docker image name with tag, the image resource, and an object representing the environment variables.\nInside the function we create an execution role (used when the docker container is spawned) that allows our task to pull images from ECR and a task role (used by the running docker container) that allows the task to send logs.\n\nWe also create a Log Group in Cloudwatch for this service that automatically deletes logs older than 30 days.\nThe ECS Task uses all these resources and assumes some other settings (CPU and Memory are fixed and the port is expected to be 80). As you can see we transform the user-friendly object that defines our environment variables into a list of object with name and value properties that the API requires.\n\nNow that we have a running task we need to expose it on our load balancer:\n\n```ts\nclass LoadBalancer extends Construct {\n  lb: Lb;\n  lbl: LbListener;\n  vpc: VPC;\n  cluster: EcsCluster;\n  // ...\n\n  exposeService(\n    name: string,\n    task: EcsTaskDefinition,\n    serviceSecurityGroup: SecurityGroup,\n    path: string\n  ) {\n    // Define Load Balancer target group with a health check on /ready\n    const targetGroup = new LbTargetGroup(this, `${name}-target-group`, {\n      dependsOn: [this.lbl],\n      tags,\n      name: `${name}-target-group`,\n      port: 80,\n      protocol: \"HTTP\",\n      targetType: \"ip\",\n      vpcId: this.vpc.vpcIdOutput,\n      healthCheck: [\n        {\n          enabled: true,\n          path: \"/ready\",\n        },\n      ],\n    });\n\n    // Makes the listener forward requests from subpath to the target group\n    new LbListenerRule(this, `${name}-rule`, {\n      listenerArn: this.lbl.arn,\n      priority: 100,\n      tags,\n      action: [\n        {\n          type: \"forward\",\n          targetGroupArn: targetGroup.arn,\n        },\n      ],\n\n      condition: [\n        {\n          pathPattern: [{ values: [`${path}*`] }],\n        },\n      ],\n    });\n\n    // Ensure the task is running and wired to the target group, within the right security group\n    const service = new EcsService(this, `${name}-service`, {\n      dependsOn: [this.lbl],\n      tags,\n      name,\n      launchType: \"FARGATE\",\n      cluster: this.cluster.id,\n      desiredCount: 1,\n      taskDefinition: task.arn,\n      networkConfiguration: [\n        {\n          subnets: Fn.tolist(this.vpc.publicSubnetsOutput),\n          assignPublicIp: true,\n          securityGroups: [serviceSecurityGroup.id],\n        },\n      ],\n      loadBalancer: [\n        {\n          containerPort: 80,\n          containerName: name,\n          targetGroupArn: targetGroup.arn,\n        },\n      ],\n    });\n  }\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    loadBalancer.exposeService(\n      \"backend\",\n      task,\n      serviceSecurityGroup,\n      \"/backend\"\n    );\n  }\n}\n```\n\nIn the Stack we define that we want to expose a service named backend with the task and serivce security group we just created and it shall be accessible at `/backend` on the load balancer.\nThis is implemented by a `TargetGroup` (defining port and health check), a `LBListenerRule` (forwarding all requests under the path to the `TargetGroup`), and a service that ensure the task is running and wired to the target group, within the right security group.\n\nAt this point we have our backend up and running, it's exposed on the load balancer and reachable from the outside.\nTo finish up we need to push frontend into the cloud, we will do this by serving a S3 Bucket through a CloudfrontDistribution that acts as a Content Delivery Network (CDN).\n\n```ts\nfunction PublicS3Bucket(\n  scope: Construct,\n  name: string,\n  absoluteContentPath: string\n) {\n  // Get built frontend into the terraform context\n  const { path: contentPath, assetHash: contentHash } = new TerraformAsset(\n    scope,\n    `${name}-frontend`,\n    {\n      path: absoluteContentPath,\n    }\n  );\n\n  // create bucket with website delivery enabled\n  const bucket = new S3Bucket(scope, `${name}-bucket`, {\n    bucketPrefix: `${name}-frontend`,\n\n    website: [\n      {\n        indexDocument: \"index.html\",\n        errorDocument: \"index.html\", // we could put a static error page here\n      },\n    ],\n    tags: {\n      ...tags,\n      \"hc-internet-facing\": \"true\", // this is only needed for HashiCorp internal security auditing\n    },\n  });\n\n  // Get all build files synchronously\n  const files = glob(\"**/*.{json,js,html,png,ico,txt,map,css}\", {\n    cwd: absoluteContentPath,\n  });\n\n  files.forEach((f) =\u003e {\n    // Construct the local path to the file\n    const filePath = path.join(contentPath, f);\n\n    // Creates all the files in the bucket\n    new S3BucketObject(scope, `${bucket.id}/${f}/${contentHash}`, {\n      bucket: bucket.id,\n      tags,\n      key: f,\n      source: filePath,\n      // mime is an open source node.js tool to get mime types per extension\n      contentType: mime(path.extname(f)) || \"text/html\",\n      etag: `filemd5(\"${filePath}\")`,\n    });\n  });\n\n  // allow read access to all elements within the S3Bucket\n  new S3BucketPolicy(scope, `${name}-s3-policy`, {\n    bucket: bucket.id,\n    policy: JSON.stringify({\n      Version: \"2012-10-17\",\n      Id: `${name}-public-website`,\n      Statement: [\n        {\n          Sid: \"PublicRead\",\n          Effect: \"Allow\",\n          Principal: \"*\",\n          Action: [\"s3:GetObject\"],\n          Resource: [`${bucket.arn}/*`, `${bucket.arn}`],\n        },\n      ],\n    }),\n  });\n\n  return bucket;\n}\n\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const bucket = PublicS3Bucket(\n      this,\n      name,\n      path.resolve(__dirname, \"../application/frontend/build\")\n    );\n  }\n}\n```\n\nSimilar to the docker push this approach is opinionated, you can find [a different one here](https://github.com/hashicorp/cdktf-integration-serverless-example). In this example we aim to have everything under the control of Terraform,\nso we create the `S3Bucket` and upload all files in the build directory through `S3BucketObject`s. We create a `S3BucketPolicy` that allows everyone to access the content of the `S3Bucket`, effectively making our site public.\n\nThe React.js application expects the backend to run under the same URL as the website with the prefix `/backend`. To enable this we need to create a CDN that handles caching and forwarding:\n\n```ts\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    const cdn = new CloudfrontDistribution(this, \"cf\", {\n      comment: `Docker example frontend`,\n      tags,\n      enabled: true,\n      defaultCacheBehavior: [\n        {\n          // Allow every method as we want to also serve the backend through this\n          allowedMethods: [\n            \"DELETE\",\n            \"GET\",\n            \"HEAD\",\n            \"OPTIONS\",\n            \"PATCH\",\n            \"POST\",\n            \"PUT\",\n          ],\n          cachedMethods: [\"GET\", \"HEAD\"],\n          targetOriginId: S3_ORIGIN_ID,\n          viewerProtocolPolicy: \"redirect-to-https\", // ensure we serve https\n          forwardedValues: [\n            { queryString: false, cookies: [{ forward: \"none\" }] },\n          ],\n        },\n      ],\n      // origins describe different entities that can serve traffic\n      origin: [\n        {\n          originId: S3_ORIGIN_ID, // origin ids can be freely chosen\n          domainName: bucket.websiteEndpoint, // we serve the website hosted by S3 here\n          customOriginConfig: [\n            {\n              originProtocolPolicy: \"http-only\", // the CDN terminates the SSL connection, we can use http internally\n              httpPort: 80,\n              httpsPort: 443,\n              originSslProtocols: [\"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"],\n            },\n          ],\n        },\n        {\n          originId: BACKEND_ORIGIN_ID,\n          domainName: loadBalancer.lb.dnsName, // our backend is served by the load balancer\n          customOriginConfig: [\n            {\n              originProtocolPolicy: \"http-only\",\n              httpPort: 80,\n              httpsPort: 443,\n              originSslProtocols: [\"TLSv1.2\", \"TLSv1.1\", \"TLSv1\"],\n            },\n          ],\n        },\n      ],\n      // We define everything that should not be served by the default here\n      orderedCacheBehavior: [\n        {\n          allowedMethods: [\n            \"HEAD\",\n            \"DELETE\",\n            \"POST\",\n            \"GET\",\n            \"OPTIONS\",\n            \"PUT\",\n            \"PATCH\",\n          ],\n          cachedMethods: [\"HEAD\", \"GET\"],\n          pathPattern: \"/backend/*\", // our backend should be served under /backend\n          targetOriginId: BACKEND_ORIGIN_ID,\n          // low TTLs so that the cache is busted relatively quickly\n          minTtl: 0,\n          defaultTtl: 10,\n          maxTtl: 50,\n          viewerProtocolPolicy: \"redirect-to-https\",\n          // currently our backend needs none of this, but it could potentially use any of these now\n          forwardedValues: [\n            {\n              queryString: true,\n              headers: [\"*\"],\n              cookies: [\n                {\n                  forward: \"all\",\n                },\n              ],\n            },\n          ],\n        },\n      ],\n      defaultRootObject: \"index.html\",\n      restrictions: [{ geoRestriction: [{ restrictionType: \"none\" }] }],\n      viewerCertificate: [{ cloudfrontDefaultCertificate: true }], // we use the default SSL Certificate\n    });\n  }\n}\n```\n\nWe configure the `CloudfrontDistribution` so that it serves our traffic via https, by default sends requests to the S3 bucket, and routes every request under `/backend` towards our backend service.\n\nWith this in place we output our DNS name to visit our fully working site.\n\n```ts\nclass MyStack extends TerraformStack {\n  constructor(scope: Construct, name: string) {\n    // ...\n    // Prints the domain name that serves our application\n    new TerraformOutput(this, \"domainName\", {\n      value: cdn.domainName,\n    });\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcdktf%2Fdocker-on-aws-ecs-with-terraform-cdk-using-typescript/lists"}