Amazon EventBridge Schedule to Amazon Elastic Container Service

Sign Up to Build

About this Architecture

Here is some information about this architecture.

How to Build This Solution

Here are the steps you can follow to build this solution on your own.

This hands-on lab allows you to dive into event-driven computing using Amazon EventBridge and Elastic Container Service (ECS). You will set up scheduled events that trigger specific ECS tasks, demonstrating the power of automated, time-based task execution. Understanding this pattern enhances your ability to build flexible and responsive cloud applications.

Get Your AWS Credentials

If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.

Wait for the credentials to load. Then run this in the terminal.

Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.

$ aws configure --profile smx-lab
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: us-west-2
Default output format [None]: 

Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.

Create the Terraform Config

Next, we'll create the required_providers config. This config is used to specify the required provider plugins for our Terraform project. In this case, we are specifying the aws provider from HashiCorp with a minimum version of 4.64.0.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.64.0"
    }
  }
}

Create the aws Provider

Next, we'll create the aws config which is used to specify the AWS provider and the region where the resources will be provisioned.

provider "aws" {
  region = "${local.region}"
}

Create the locals Config

Next, we'll create the locals config. This config is used to define variables that are specific to this Terraform configuration. In this case, we are defining the following variables:

  • region: This variable should be replaced with the desired AWS region where the infrastructure will be deployed.

  • container_image: This variable specifies the container image that will be used for the ECS sample application.

  • vpc_cidr: This variable should be replaced with the desired CIDR block for the VPC.

  • availability_zones: This variable should be replaced with a comma-separated list of the desired availability zones.

  • public_subnets: This variable should be replaced with a comma-separated list of the desired public subnets.

  • private_subnets: This variable should be replaced with a comma-separated list of the desired private subnets.

locals {
      region        = "us-west-2"
      container_image = "amazon/amazon-ecs-sample"
      vpc_cidr = "10.10.0.0/16"
      availability_zones = ["us-west-2a", "us-west-2b"]
      public_subnets     = ["10.10.100.0/24", "10.10.101.0/24"]
      private_subnets    = ["10.10.0.0/24", "10.10.1.0/24"]
}

Create the aws_scheduler_schedule Resource

Next, we'll create the aws_scheduler_schedule config. This config is used to define a scheduled task that invokes an ECS task in the AWS environment.

resource "aws_scheduler_schedule" "serverlessland-eb-ecs-invoke-schedule" {
  name                  = "serverlessland-eb-ecs-invoke-schedule"
  flexible_time_window  = [
    {
      mode = "OFF"
    }
  ]
  schedule_expression   = "rate(5 minute)"
  target                = [
    {
      arn               = "${aws_ecs_cluster.serverlessland-ecs-test-cluster.arn}"
      role_arn          = "${aws_iam_role.serverlessland-eventbridge-invoke-ecs-role.arn}"
      ecs_parameters    = [
        {
          task_count             = 1
          task_definition_arn    = "${aws_ecs_task_definition.serverlessland-ecs-task-definition.arn}"
          launch_type            = "FARGATE"
          network_configuration = [
            {
              subnets             = "${aws_subnet.prod-subnet-public-1.*.id}"
              assign_public_ip    = true
              security_groups     = ["${aws_security_group.prod-sg.id}"]
            }
          ]
        }
      ]
    }
  ]
}

This config creates a scheduled task that runs every 5 minutes. It targets an ECS cluster and uses an IAM role to invoke an ECS task. The task is launched using the Fargate launch type and is configured with network settings to use a specific subnet, assign a public IP, and use a security group.

Create the aws_vpc Resource

Next, we'll create the aws_vpc config. This config is used to define an Amazon Virtual Private Cloud (VPC) in AWS.

resource "aws_vpc" "prod-vpc" {
  cidr_block                = "${local.vpc_cidr}"
  enable_dns_support        = true
  enable_dns_hostnames      = true
  instance_tenancy          = "default"

  tags = {
    Name = "prod-vpc"
  }
}

Create the aws_subnet Resource

Next, we'll create the aws_subnet config. This config is used to define a public subnet in the prod-vpc VPC. The subnet will have a CIDR block and will be associated with an availability zone. It will also have the option to map a public IP address to instances launched in this subnet. The subnet depends on the aws-igw internet gateway and has some tags associated with it.

resource "aws_subnet" "prod-subnet-public-1" {
  vpc_id                  = "${aws_vpc.prod-vpc.id}"
  count                   = "${length(local.public_subnets)}"
  cidr_block              = "${element(local.public_subnets, count.index)}"
  availability_zone       = "${element(local.availability_zones, count.index)}"
  map_public_ip_on_launch = "true"
  depends_on              = ["${aws_internet_gateway.aws-igw}"]

  tags = {
    Name        = "prod-subnet-public-1"
    Environment = "prod"
  }
}

Create the aws_subnet Resource

Next, we'll create the aws_subnet config. This config is used to define a private subnet in the prod-vpc VPC. It will create a subnet with the specified CIDR block and availability zone. The subnet will also depend on the aws-igw internet gateway and will have the Name and Environment tags set to 'prod-subnet-private-1' and 'prod' respectively.

resource "aws_subnet" "prod-subnet-private-1" {
  vpc_id            = "${aws_vpc.prod-vpc.id}"
  count             = "${length(local.private_subnets)}"
  cidr_block        = "${element(local.private_subnets, count.index)}"
  availability_zone = "${element(local.availability_zones, count.index)}"
  depends_on        = ["${aws_internet_gateway.aws-igw}"]

  tags = {
    Name       = "prod-subnet-private-1"
    Environment = "prod"
  }
}

Create the aws_security_group Resource

Next, we'll create the aws_security_group config. This config is used to define a security group in AWS.

resource "aws_security_group" "prod-sg" {
  name        = "prod-sg"
  vpc_id      = "${aws_vpc.prod-vpc.id}"

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    cidr_blocks     = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

Create the aws_internet_gateway Resource

Next, we'll create the aws_internet_gateway config. This config is used to create an internet gateway in AWS.

resource "aws_internet_gateway" "aws-igw" {
  vpc_id = "${aws_vpc.prod-vpc.id}"

  tags = {
    Name        = "prod-igw"
    Environment = "prod"
  }
}

Create the aws_route_table Resource

Next, we'll create the aws_route_table config. This config is used to create a public route table in the prod-vpc VPC. It includes the vpc_id parameter, which specifies the ID of the VPC where the route table will be created. Additionally, it includes the tags parameter, which allows you to assign a name and environment tag to the route table.

resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.prod-vpc.id}"
  tags = {
    Name        = "prod-routing-table-public"
    Environment = "prod"
  }
}

Create the aws_route Resource

Next, we'll create the aws_route config. This config is used to define a route in the AWS route table.

resource "aws_route" "public" {
  route_table_id         = "${aws_route_table.public.id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = "${aws_internet_gateway.aws-igw.id}"
}

Create the aws_route_table_association Resource

Next, we'll create the aws_route_table_association config. This config is used to associate a route table with a subnet in AWS.

resource "aws_route_table_association" "public" {
  count           = "${length(local.public_subnets)}"
  subnet_id       = "${element(aws_subnet.prod-subnet-public-1.*.id, count.index)}"
  route_table_id  = "${aws_route_table.public.id}"
}

Create the aws_ecs_cluster Resource

Next, we'll create the aws_ecs_cluster config. This config is used to define an Amazon ECS cluster. In this example, we are creating a cluster named serverlessland-ecs-test-cluster with the following settings:

  • name: The name of the cluster.

  • depends_on: A list of resources that this cluster depends on. In this case, it depends on an AWS internet gateway named ${aws_internet_gateway.aws-igw}.

  • setting: A list of additional settings for the cluster. In this case, we are enabling container insights by setting containerInsights to enabled.

resource "aws_ecs_cluster" "serverlessland-ecs-test-cluster" {
  name        = "serverlessland-ecs-test-cluster"
  depends_on  = ["${aws_internet_gateway.aws-igw}"]
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

Create the aws_ecs_task_definition Resource

Next, we'll create the aws_ecs_task_definition config. This config is used to define the task definition for an Amazon Elastic Container Service (ECS) task.

The task definition specifies various parameters such as the task family, compatibility requirements, network mode, CPU and memory allocations, task role and execution role ARNs, container definitions, and port mappings.

resource "aws_ecs_task_definition" "serverlessland-ecs-task-definition" {
  family                   = "serverlessland-ecs-task-definition"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 1024
  memory                   = 2048
  task_role_arn            = "${aws_iam_role.serverlessland-ecs-task-role.arn}"
  execution_role_arn       = "${aws_iam_role.serverlessland-ecs-task-execution-role.arn}"
  container_definitions    = '[\n  {\n    "name": "webcontainer",\n    "image": "${local.container_image}",\n    "cpu": 1024,\n    "memory": 2048,\n    "essential": true,\n    "logConfiguration": {\n          "logDriver": "awslogs",\n          "options": {\n            "awslogs-group": "${aws_cloudwatch_log_group.serverlessland-cw-log-grp-dump-ecs.id}",\n            "awslogs-region": "${local.region}",\n            "awslogs-stream-prefix": "ecs"\n          }\n    },\n    "portMappings": [\n        {\n          "containerPort": 8080,\n          "hostPort": 8080\n        }\n      ]\n  }\n]'
}

Create the aws_cloudwatch_log_group Resource

Next, we'll create the aws_cloudwatch_log_group config. This config is used to create a CloudWatch log group named serverlessland-cw-log-grp-dump-ecs with the log group name /ecs/webcontainer.

resource "aws_cloudwatch_log_group" "serverlessland-cw-log-grp-dump-ecs" {
  name = "/ecs/webcontainer"
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an AWS IAM role called 'serverlessland-ecs-task-execution-role'. The role will have a managed policy attached to it, specifically 'AmazonECSTaskExecutionRolePolicy'. Additionally, the role will have an assume role policy that allows the 'ecs-tasks.amazonaws.com' service to assume this role.

resource "aws_iam_role" "serverlessland-ecs-task-execution-role" {
  name               = "serverlessland-ecs-task-execution-role"
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]
  assume_role_policy  = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': 'sts:AssumeRole',
        'Effect': 'Allow',
        'Sid': '',
        'Principal': {
          'Service': 'ecs-tasks.amazonaws.com'
        }
      }
    ]
  })}"
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an AWS IAM role named 'serverlessland-ecs-task-role'. The role will have a managed policy attached to it, specifically 'AmazonECSTaskExecutionRolePolicy'. Additionally, the role will have an assume role policy that allows the 'ecs-tasks.amazonaws.com' service to assume this role.

resource "aws_iam_role" "serverlessland-ecs-task-role" {
  name               = "serverlessland-ecs-task-role"
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]
  assume_role_policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': 'sts:AssumeRole',
        'Effect': 'Allow',
        'Sid': '',
        'Principal': {
          'Service': 'ecs-tasks.amazonaws.com'
        }
      }
    ]
  })}"
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to define an IAM role in AWS. In this case, we are creating a role named serverlessland-eventbridge-invoke-ecs-role.

The role has a managed policy attached to it, specified by the managed_policy_arns attribute. The value of managed_policy_arns is an array containing the ARN of the policy aws_iam_policy.serverlessland-eventbridge-invoke-ecs-policy.

The assume_role_policy attribute defines the trust policy for the role. It uses the jsonencode function to encode a JSON object representing the policy. The policy allows the scheduler.amazonaws.com service to assume the role.

resource "aws_iam_role" "serverlessland-eventbridge-invoke-ecs-role" {
  name               = "serverlessland-eventbridge-invoke-ecs-role"
  managed_policy_arns = ["${aws_iam_policy.serverlessland-eventbridge-invoke-ecs-policy.arn}"]
  assume_role_policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': 'sts:AssumeRole',
        'Effect': 'Allow',
        'Sid': '',
        'Principal': {
          'Service': 'scheduler.amazonaws.com'
        }
      }
    ]
  })}"
}

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to define an AWS IAM policy named 'serverlessland-eventbridge-invoke-ecs-policy'. The policy allows the 'ecs:RunTask' action on the specified ECS task definition ARN and the 'iam:PassRole' action with a condition that the role is passed to the 'ecs-tasks.amazonaws.com' service.

resource "aws_iam_policy" "serverlessland-eventbridge-invoke-ecs-policy" {
  name   = "serverlessland-eventbridge-invoke-ecs-policy"
  policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Effect': 'Allow',
        'Action': ['ecs:RunTask'],
        'Resource': [
          '${aws_ecs_task_definition.serverlessland-ecs-task-definition.arn}:*',
          '${aws_ecs_task_definition.serverlessland-ecs-task-definition.arn}'
        ],
        'Condition': {
          'ArnLike': {
            'ecs:cluster': '${aws_ecs_cluster.serverlessland-ecs-test-cluster.arn}'
          }
        }
      },
      {
        'Effect': 'Allow',
        'Action': 'iam:PassRole',
        'Resource': ['*'],
        'Condition': {
          'StringLike': {
            'iam:PassedToService': 'ecs-tasks.amazonaws.com'
          }
        }
      }
    ]
  })}"
}

Create the ScheduleTargetTask Output

Next, we'll create the ScheduleTargetTask config. This config is used to define the output value for the ScheduleTargetTask variable. The value of this variable is set to the ARN (Amazon Resource Name) of the ECS (Elastic Container Service) task that is being invoked from the EventBridge Scheduler.

output {
  ScheduleTargetTask = {
    value       = "${aws_ecs_cluster.serverlessland-ecs-test-cluster.arn}"
    description = "The ARN of the ecs task being invoked from EventBridge Scheduler"
  }
}

Create the ScheduleName Output

Next, we'll create the ScheduleName config. This config is used to store the name of the EventBridge schedule.

output "ScheduleName" {
  value = aws_scheduler_schedule.serverlessland-eb-ecs-invoke-schedule.name
  description = "Name of the EventBridge Schedule"
}

Deploy the Solution

Let's deploy this thing! If you haven't done so, start the Skillmix lab session and get the account credentials. Configure your Terraform environment to use those credentials.

Then, open a terminal or command prompt, navigate to the folder with your Terraform file, and execute these commands:

# initiatlize the project 
$ terraform init 

# show the plan 
$ terraform plan 

# apply the changes 
$ terraform apply

Wait for the changes to be applied before proceeding.

Testing

  1. After deployment, a new schedule will be created in the Amazon EventBridge. Check the Amazon EventBridge console and go to the schedule section

  2. The schedule will trigger a new Amazon ECS task in every 5 minutes. The tasks are visible in the Amazon Elastic Container Service clusters section

  3. Open any running task from Amazon Elastic Container Service console and click on the public IP listed there. A sample php page will open in a new browser window or tab