Building serverless web crawler with Puppeteer on AWS Fargate
Tags: serverless, aws, puppeteer, awsfargate
Published: 2019-11-01 14:36:41

This post is originally published on [our blog](https://innomizetech.com/blog/building-serverless-web-crawler-with-puppeteer-on-aws-fargate).

<!-- wp:paragraph -->
<p>Recently our team has been working on a project with data crawling from various sites for pricing comparison.  Our team has selected <strong><a href="https://developers.google.com/web/tools/puppeteer">Puppeteer</a></strong> to implement. We have successfully built and delivered this solution to our customer, so I would like to write this article to share and provide some outlines steps to help you set up and have a try. I hope you enjoy it!</p>
<!-- /wp:paragraph -->

<!-- wp:more -->
<!--more-->
<!-- /wp:more -->

<!-- wp:heading -->
<h2>Why Puppeteer?</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Per Google's documentation</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p><strong>Puppeteer</strong> is a Node library which provides a high-level API to control <a href="https://developers.google.com/web/updates/2017/04/headless-chrome">headless</a> Chrome or Chromium over the <a href="https://chromedevtools.github.io/devtools-protocol/">DevTools Protocol</a>. It can also be configured to use full (non-headless) Chrome or Chromium.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>There are a couple of reasons why we've selected <strong>Puppeteer</strong>:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Headless supported so easy to run on we can run on any container as <em>serverless</em> such as AWS Lambda, Azure Function, ECS.</li><li>It is powerful, provides us a lot of commands and API.</li><li>Easy to get started, we can automate input and navigation easily with a few lines of code. For our pricing crawler, inputs, actions, and navigation are required, therefore, using <strong>Puppeteer</strong> can help us.</li><li>Node.js + TypeScript supported, we can utilize our automation development with Selenium, Protractor, Cypress skills.</li><li>And much more, you can find out why people select Puppeteer over other tools by reading some articles below:<ul><li><em><a href="https://www.lucidchart.com/techblog/2018/08/08/why-puppeteer-is-better-than-selenium/">WHY PUPPETEER IS BETTER THAN SELENIUM</a></em></li><li><a href="https://flood.io/blog/selenium-vs-puppeteer-for-test-automation-is-a-new-leader-emerging/">Selenium vs. Puppeteer for Test Automation: Is a New Leader Emerging?</a></li></ul></li></ul>
<!-- /wp:list -->

<!-- wp:heading -->
<h2>Our case study</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The idea is we wanted to crawl pricing info from the online shop sites of trade-in mobile devices so that we can compare prices. End-user can select the best site for them. Once we crawled data from the sites, we need to write output in .csv file and upload to AWS S3 for archiving, we also need to archive output our database for reporting purpose.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Basically, <strong>Puppeteer</strong> can run on any serverless container such as AWS Lambda or Azure Function, but because of time limitations, we cannot running process longer than <a href="https://aws.amazon.com/about-aws/whats-new/2018/10/aws-lambda-supports-functions-that-can-run-up-to-15-minutes/">15 minutes for AWS Lambda</a> or <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-scale#consumption-plan">10 minutes for Azure Function that use Consumption plan</a>. Basically, our crapper process took around 1 hour to finish. That is the reason we've selected <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_GetStarted_Fargate.html">AWS Fargate</a> to run our crapper process. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":266,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://blog.innomizetech.com/wp-content/uploads/2019/11/Puppeteer-on-AWS-Fargate-1-1024x753.png" alt="Puppeteer on AWS Fargate" class="wp-image-266"/><figcaption>Serverless Web Crawler on AWS Fargate Architecture</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p><strong>Why  AWS Fargate over other services</strong>?</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong><a href="https://aws.amazon.com/fargate/">AWS Fargate</a></strong><a href="https://aws.amazon.com/fargate/"> </a>allows you to run containers without having to manage servers or clusters. With AWS <em>Fargate</em>, you no longer have to provision, configure, and scale clusters of virtual machines to run containers. Like AWS Lambda, AWS Fargate, there are no upfront payments and you only pay for the resources that you use. You pay for the amount of vCPU and memory resources consumed by your containerized applications.</p>
<!-- /wp:paragraph -->

<!-- wp:separator -->
<hr class="wp-block-separator"/>
<!-- /wp:separator -->

<!-- wp:heading -->
<h2>Let's get started</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Configure, and launch a Puppeteer crawler is pretty simple, so I will not mention how to do it in this article. In case if you don't know how to get started, then refer to <a href="https://developers.google.com/web/tools/puppeteer/get-started">this instruction</a> for more details. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Example project</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>In order to help you easy to follow this article, I have created a sample project, you can find it on <a href="https://github.com/innomizetech/ecs-puppeteer-crawler-example">Github</a>. This project is different from the above case study, but it is easy to understand and setup. This project contains two simple processes to generate pdf and screenshot for a URL then upload those files to an S3 bucket.</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>First of all, you will need to clone our <a href="https://github.com/innomizetech/ecs-puppeteer-crawler-example">ecs-puppeteer-crawler-example</a> repository from Github:</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>git clone git@github.com:innomizetech/ecs-puppeteer-crawler-example.git</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>Install node modules</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>cd ecs-puppeteer-crawler-example
npm install</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>Below is the code of the pdf generator <em>pdf-generator.ts</em> using <strong>Puppeteer</strong> and </li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>import { archive } from './lib/s3-util';
import * as bluebird from 'bluebird';
import { emptyDirSync } from 'fs-extra';
const argv = require('yargs').argv;

const browserless = require('browserless')({
  headless: !!argv.headless,
  ignoreHTTPSErrors: true,
  defaultViewport: null,
  dumpio: false,
  args: ['--disable-setuid-sandbox', '--no-sandbox']
});

if (!argv.url) {
  console.log('Url is required');
  process.exit(1);
}

const devices = browserless.devices.slice(0, 2);
const hostname = new URL(argv.url).hostname;

console.time('Finished in');

const generatePdf = async () => {
  const generatePdfPerDevice = async device => {
    console.log(
      `${new Date().toUTCString()}: Generating pdf for ${device.name} device`
    );

    const buffer = await browserless.pdf(argv.url, {
      waitUntil: ['load', 'networkidle2'],
      fullPage: true,
      device: device.name
    });

    const fileName = `output/${hostname} on (${device.name}).pdf`;

    return { buffer, fileName };
  };

  const data = await bluebird.map(
    devices,
    device => generatePdfPerDevice(device),
    {
      concurrency: 5
    }
  );

  await bluebird.map(data, item => archive(item));

  console.timeEnd('Finished in');

  process.exit(0);
};

emptyDirSync('output');
generatePdf();
</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>You can then generate .pdf for a URL such as <a href="https://www.innomizetech.com"></a><a href="https://www.innomizetech.com">https://www.innomizetech.com</a> by running below command:</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code># --headless means we want to run without UI
env-cmd npx ts-node src/screenshot-generator.ts --url=https://www.innomizetech.com/ --headless</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><em>And here is the console output</em></p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":247,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://blog.innomizetech.com/wp-content/uploads/2019/11/image-1-1024x245.png" alt="Puppeteer output" class="wp-image-247"/><figcaption>Generate pdf console output</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:separator -->
<hr class="wp-block-separator"/>
<!-- /wp:separator -->

<!-- wp:heading -->
<h2>The steps</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Now you might wonder how to run, build, deploy and run your task on AWS ECS either manually or automation. So I wanted to outline the steps we need to perform to set up and run our example project on AWS Fargate.</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>Build a docker image and upload it into ECR repo.</li><li>Create an ECR repo to store docker images. Alternative you can use public <a href="https://hub.docker.com/">docker hub</a>.</li><li>Setup VPC in case if you want to run your task under your private network. Otherwise, you can use default VPC on AWS. <strong>In this project, we use default VPC on a public subnet because our process doesn't contain any sensitive information, create a new VPC doesn't need, even add more cost if we run under a private subnet.</strong></li><li>Create an S3 bucket for achieving output files.</li><li>Create task definition, ECS cluster and setup scheduled task to run our task (optional).</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>A lot of steps, right? Don't worry, I will show you how to automate those steps. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Infrastructure as code (IaC)</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>There are a couple of ways to automate your infrastructure on AWS cloud such as  <a href="https://aws.amazon.com/cloudformation/">AWS CloudFormation</a>, <a href="https://www.terraform.io/">Terraform</a>, <a href="https://docs.aws.amazon.com/cdk/latest/guide/home.html">AWS CDK</a>. In this project, we use <a href="https://docs.aws.amazon.com/cdk/latest/guide/home.html">AWS CDK</a>, I will have another article to provide more details about AWS CDK and why we use it for our IaC stuff.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Pre-requisites</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Before continuing you need an AWS account. If you do not already have an AWS account, you can utilize their <a href="https://portal.aws.amazon.com/billing/signup">free-tier accounts</a>. Follow <strong>Step 1: Prepare credentials </strong>on <a href="https://blog.innomizetech.com/2019/09/23/serverless-fundamentals-and-how-to-build-test-and-deploy-lambda-function-to-aws-part-2/">my article</a> to configure your credentials to interact with AWS API via CLI. You also need to install <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html">AWS CLI</a> and <a href="https://docs.docker.com/v17.12/install/">Docker</a> on your machine.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4>Step 1 - Build docker and push to ECR</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>To run <strong>Puppeteer</strong> on AWS Fargate, we need a docker image. Below is our <em>Dockerfile</em> to build our <strong>Puppeteer</strong> code:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>FROM node:10
RUN apt-get update

ENV HOME_DIR /usr/src/app

# for https
RUN apt-get install -yyq ca-certificates

# install libraries
RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6

# tools
RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils

# and fonts
RUN apt-get install -yyq fonts-liberation

RUN mkdir -p $HOME_DIR
RUN mkdir -p $HOME_DIR/views

WORKDIR $HOME_DIR

# Add our package.json and install *before* adding our application files to
# optimize build performance
ADD package.json $HOME_DIR
ADD package-lock.json $HOME_DIR

# install the necessary packages
RUN npm install --unsafe-perm --save-exact --production
COPY . $HOME_DIR
RUN npm run clean &amp;&amp; npm run build

ENTRYPOINT ["/usr/local/bin/npm", "run"]
CMD ["clean"]</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>To create an ECR repo, build, push a docker image to AWS ECR, use the <em>build_and_push.sh</em> script. This script also will check and create the repo for you if it doesn't exist yet.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>#!/bin/bash

# This script is using to build and push docker image to ECR repository

AWS_PROFILE=development
REGION=us-east-2
REPO_NAME=ecs-puppeteer-crawler-example
TAG=latest

# Get the account number associated with the current IAM credentials
ACCOUNT_ID=$(aws sts get-caller-identity --profile $AWS_PROFILE --query Account --output text)

if [ $? -ne 0 ]
then
    exit 255
fi


# If the repository doesn't exist in ECR, create it.
echo 'Checking repo existance...'
aws ecr describe-repositories --region $REGION --profile $AWS_PROFILE --repository-names "${REPO_NAME}" > /dev/null 2>&amp;1

if [ $? -ne 0 ]
then
    echo "Repo $REPO_NAME doesn't exist, try to create a new one"
    aws ecr create-repository --repository-name "${REPO_NAME} --region $REGION --profile $AWS_PROFILE" > /dev/null
fi

echo 'Login to ECR Repository...'

$(aws ecr get-login --no-include-email --region $REGION --profile $AWS_PROFILE)

echo 'Building and pushing docker image to ECR repository...'
docker build -t $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$TAG .

if [ "$1" != "true" ]; then
docker push $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$TAG
fi

echo 'Publish docker image completed'</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Review and update the following parameters on the top of the script file according to your environment and AWS credentials</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>AWS_PROFILE=development
REGION=us-east-2
REPO_NAME=ecs-puppeteer-crawler-example
TAG=latest</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>And then execute the script to create the repo, build docker and push to ECR</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>chmod +x scripts/build_and_push.sh
./build_and_push.sh</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Wait until the script finished, then you can find your docker image on the <a href="https://us-east-2.console.aws.amazon.com/ecr/repositories?region=us-east-2">AWS ECR</a></p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":253,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://blog.innomizetech.com/wp-content/uploads/2019/11/image-2-1024x229.png" alt="ECR Image" class="wp-image-253"/><figcaption>ECR Docker Image</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4>Step 2 - 5: Provision AWS service and run your task</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>As I have mentioned above, we can automate step #2 through #5 above by using the <strong>AWS CDK </strong>framework<strong>.</strong>  I have created <strong>PuppeteerCrawlerFargateStack</strong> to construct all required resources</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>import cdk = require('@aws-cdk/core');
import ec2 = require('@aws-cdk/aws-ec2');
import ecr = require('@aws-cdk/aws-ecr');
import events = require('@aws-cdk/aws-events');
import ecs = require('@aws-cdk/aws-ecs');
import iam = require('@aws-cdk/aws-iam');
import { EcsTask } from '@aws-cdk/aws-events-targets';
import { PolicyStatement } from '@aws-cdk/aws-iam';
import { Bucket } from '@aws-cdk/aws-s3/lib/bucket';
import { PuppeteerCrawlerFargateStackProps } from './interfaces';

export class PuppeteerCrawlerFargateStack extends cdk.Stack {
  constructor(
    scope: cdk.Construct,
    id: string,
    props?: PuppeteerCrawlerFargateStackProps
  ) {
    super(scope, id, props);

    const vpc = this.constructVpc(props);
    const cluster = new ecs.Cluster(this, 'PuppeteerCrawlerCluster', { vpc });
    const bucket = this.constructArchiveBucket(props);
    const taskDef = this.constructTaskDef(props, bucket);

    // Create rule to trigger this be run task
    new events.Rule(this, 'PuppeteerCrawlerSchedule', {
      schedule: events.Schedule.expression(
        props.scheduleExpression || 'cron(0/5 * * * ? *)'
      ),
      description: 'Starts the puppeteer crawler job',
      targets: [
        new EcsTask({
          cluster: cluster,
          taskDefinition: taskDef,
          subnetSelection: {
            subnetType: ec2.SubnetType.PUBLIC
          },
          containerOverrides: [
            {
              containerName: 'PuppeteerContainer',
              command: ['pdf']
            }
          ]
        }),
        new EcsTask({
          cluster: cluster,
          taskDefinition: taskDef,
          subnetSelection: {
            subnetType: ec2.SubnetType.PUBLIC
          },
          containerOverrides: [
            {
              containerName: 'PuppeteerContainer',
              command: ['screenshot']
            }
          ]
        })
      ]
    });
  }

  private constructVpc(props: PuppeteerCrawlerFargateStackProps) {
    if (props.useDefaultVpc) {
      return ec2.Vpc.fromLookup(this, 'VPC', {
        isDefault: true
      });
    }

    return new ec2.Vpc(this, 'EcsFargateVPC', {
      cidr: '172.15.0.0/16',
      maxAzs: 1,
      // natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 16,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC
        }
        // In case if you want to run your task under private subnet
        // then uncomment below lines and change subnetSelection
        // options above to PRIVATE
        // {
        //   cidrMask: 20,
        //   name: 'Private',
        //   subnetType: ec2.SubnetType.PRIVATE
        // }
      ]
    });
  }

  private constructArchiveBucket(props: PuppeteerCrawlerFargateStackProps) {
    return new Bucket(this, 'PuppeteerCrawlerArchiveBucket', {
      bucketName: props.bucketName,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });
  }

  private constructTaskDef(
    props: PuppeteerCrawlerFargateStackProps,
    bucket: Bucket
  ) {
    // Role for task
    const taskRole = this.constructTaskRole(bucket);
    const taskDef = new ecs.FargateTaskDefinition(
      this,
      'PuppeteerCrawlerTaskDefinition',
      {
        memoryLimitMiB: 8192,
        cpu: 2048,
        taskRole
      }
    );

    const repo = ecr.Repository.fromRepositoryName(
      this,
      'PuppeteerCrawlerRepo',
      props.repoName
    );

    taskDef.addContainer('PuppeteerContainer', {
      image: ecs.ContainerImage.fromEcrRepository(repo),
      logging: new ecs.AwsLogDriver({
        streamPrefix: 'puppeteer-data-crawler',
        logRetention: 1
      }),
      environment: {
        S3_BUCKET: bucket.bucketName
      }
    });

    return taskDef;
  }

  private constructTaskRole(bucket: Bucket) {
    const taskRole = new iam.Role(this, 'EcsTaskRole', {
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com')
    });

    // Note: we can add more policy here
    taskRole.addToPolicy(
      new PolicyStatement({
        resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
        actions: ['s3:PutObject']
      })
    );

    return taskRole;
  }
}
</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Open the <em>infra/app.ts</em> file and make your desired changes then execute <em>cdk</em> <em>deploy</em> command to deploy and create resources on AWS</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>cd infra
cdk deploy</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>An ECS cluster, an S3 bucket, and a CloudWatch schedule will be created automatically, you can find them on <strong>AWS Console Management</strong></p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":255,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://i0.wp.com/blog.innomizetech.com/wp-content/uploads/2019/11/image-3.png?fit=640%2C244&amp;ssl=1" alt="" class="wp-image-255"/><figcaption>ECS Cluster</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Wait for 5 minutes (default schedule expression from our Github code) then check the ECS Task execution logs on CloudWatch and also S3 bucket</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":256,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://i1.wp.com/blog.innomizetech.com/wp-content/uploads/2019/11/image-4.png?fit=640%2C383&amp;ssl=1" alt="" class="wp-image-256"/><figcaption>ECS Task Logs</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:image {"id":257,"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://blog.innomizetech.com/wp-content/uploads/2019/11/image-5.png" alt="" class="wp-image-257"/><figcaption>Archived output files on S3 bucket</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>So far so good, I believe that within 10 minutes you will have a web crawler up and running. You can our setup and modify your Puppeteer codes based on your requirement. I also mentioned database archiving (i.e. MySQL or DynamoDB). To implement this, I have some options as below:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Implement directly on the puppeteer code, you can use the <strong>bulkInsert</strong> function of the <em>s3-util</em> to insert bulk record to the database.</li><li>Build a Lambda function trigger object create event of the archive S3 bucket.</li></ul>
<!-- /wp:list -->

<!-- wp:heading -->
<h2>Clean up</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>To avoid increasing your AWS bills, you need to clean up and remove resources on AWS:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>First, you need to empty your S3 bucket.</li></ul>
<!-- /wp:list -->

<!-- wp:list -->
<ul><li>Then delete AWS resource by running <strong>destroy</strong> command.</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>cd infra
cdk destroy</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>Finally, delete your ECS repo.</li></ul>
<!-- /wp:list -->

<!-- wp:heading -->
<h2>Summary</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>I know that this article is very long, but I wanted to provide detailed steps to help you understand step by step. Any suggestions and recommendations are welcome.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Should you run into issues while practicing the steps outlined in this article, I encourage you to reach out to me. You can get in touch with me through my Twitter handle <a href="https://twitter.com/hoangleitvn">@hoangleitvn</a></p>
<!-- /wp:paragraph -->

Visit our [blog](https://innomizetech.com/blog) for more interesting articles. If you have any questions or need help you can contact me via [Twitter](https://twitter.com/hoangleitvn).


If you are looking for developers, offshore team, or need consulting about the AWS cloud, Serverless architecture, and so on, then [hire us](https://innomizetech.com/contact-us/), we can help you!

<!-- wp:paragraph -->
<p>Thank you for reading!</p>
<!-- /wp:paragraph -->