CD Cloud Logix

Easily Deploy EventBus with Terraform AWS

Olivier Butterbach
Olivier Butterbach

CEO of CD Cloud Logix

Table of Contents

A simple guide on how to deploy a Cloudwatch EventBus with Terraform. If you check the official Terraform documentation, you will notice that it’s quite light and you’ll probably need some guidance at this point.

Terraform
Passing Cloudwatch Events from one AWS Account to another

This solution would required the need to create an IAM role for allowing CloudWatch Event to PutsEvents into your remote AWS Account.

1. Terraform Providers

This solution is using Terraform version 0.12.16, you can set different aliases as follow. Terraform would then use these providers when we start creating our modules:

provider "aws" {
  region  = "eu-west-1"
  version = "~> 2.6"

  profile = "default"
}

provider "aws" {
  region  = "eu-west-1"
  version = "~> 2.6"

  alias   = "main"
  profile = "default"
}

provider "aws" {
  region  = "eu-west-1"
  version = "~> 2.6"

  alias   = "member"
  profile = "member"
}

Once completed, don’t forget to add your backend configuration:

terraform {
  required_version = "~> 0.12.0"

  backend "s3" {
    acl     = "bucket-owner-full-control"
    bucket  = "tf-state-bucket-guardduty"
    encrypt = true
    key     = "aws.tfstate"
    region  = "eu-west-1"
  }
}

Keep in mind that bucket naming are unique globally, you will need to use your own unique one.

2. Modules

This project is composed of 2 different modules:

  • ec2-events (Member AWS accounts)
  • eventbus-sqs (Main AWS account)

Let’s start with the module what would be install in your main account.

3. Main AWS account module

This module is divided in several parts, let’s see the EventBus configuration:

resource "aws_cloudwatch_event_permission" "accounts" {
  for_each = var.principals

  principal    = each.value
  statement_id = each.key
}

This resource would need the following variable, EventBus would then trust each of your member AWS accounts.

variable "principals" {
  description = "(Required) list of AWS Accounts"
  type        = map(string)
  default     = {
    "member-name"  = "0123456789011"
    "member-name1" = "0123456789012"
  }
}

With this, Terraform would iterate each member account and create a resource for each of them. Keep in mind that EventBus is region restricted, which mean you would need to repeat this variable configuration for each of your regions.

Next part of our module is CloudWatch Event link to SQS for collecting EC2 events from the main AWS account:

#######################
# CloudWatch Event Rule
#######################
resource "aws_cloudwatch_event_rule" "event_rule" {
  count       = var.create ? 1 : 0
  name        = "${var.name}-events"
  description = var.description

  event_pattern = <<PATTERN
{
  "source": [
    "aws.autoscaling"
  ],
  "detail-type": [
    "EC2 Instance Launch Successful",
    "EC2 Instance Terminate Successful",
    "EC2 Instance Launch Unsuccessful",
    "EC2 Instance Terminate Unsuccessful"
  ]
}
PATTERN
}

#######################
# SQS Policy
#######################
resource "aws_sqs_queue_policy" "events_queue_policy" {
  count = var.create ? 1 : 0

  queue_url = aws_sqs_queue.events_queue[0].id
  policy    = <<POLICY
{
  "Version": "2012-10-17",
  "Id": "sqspolicy",
  "Statement": [
    {
      "Sid": "First",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "sqs:SendMessage",
      "Resource": "${aws_sqs_queue.events_queue[0].arn}",
      "Condition": {
        "ArnEquals": {
          "aws:SourceArn": "${aws_cloudwatch_event_rule.event_rule[0].arn}"
        }
      }
    }
  ]
}
POLICY
}

#################################
# CloudWatch Event targeting SQS
#################################
resource "aws_cloudwatch_event_target" "event_target" {
  count = var.create ? 1 : 0

  rule      = aws_cloudwatch_event_rule.event_rule[0].id
  target_id = "${var.name}-sqs"
  arn       = aws_sqs_queue.events_queue[0].arn
}


#################################
# SQS creation
#################################
resource "aws_sqs_queue" "events_queue" {
  count = var.create ? 1 : 0

  name = "${var.name}-ec2-finding-events"
}

This part would then generate CloudWatch Event rule, SQS queue and link each other with a target rule.

Final part, create an IAM role for allowing your AWS member account to put events in your main account:

#################################################################
# Cross Account IAM role
#################################################################

data "aws_iam_policy_document" "assume_policy" {
  count = var.create ? 1 : 0

  dynamic "statement" {
    for_each = var.principals

    content {
      actions = ["sts:AssumeRole"]

      principals {
        type        = "AWS"
        identifiers = ["arn:aws:iam::${statement.value}:root"]
      }
    }
  }

  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
  }
}

data "aws_iam_policy_document" "cloudwatch_access_policy" {
  count = var.create ? 1 : 0

  statement {
    effect = "Allow"

    actions = [
      "events:PutEvents"
    ]

    resources = [
      "*"
    ]
  }
}

resource "aws_iam_policy" "cloudwatch_access_policy" {
  count = var.create ? 1 : 0

  name        = "${var.name}-cloudwatch-access"
  policy      = data.aws_iam_policy_document.cloudwatch_access_policy[0].json
  path        = var.iam_path
  description = var.description
}

resource "aws_iam_role_policy_attachment" "across_attach" {
  count = var.create ? 1 : 0

  role       = aws_iam_role.assume_role[0].name
  policy_arn = aws_iam_policy.cloudwatch_access_policy[0].arn
}


# IAM Role
resource "aws_iam_role" "assume_role" {
  count = var.create ? 1 : 0

  name               = "${var.name}-assume-role"
  assume_role_policy = data.aws_iam_policy_document.assume_policy[0].json
  path               = var.iam_path
  description        = var.description
}

This part conclude the main module, let’s have a look on the member one.

4. Member AWS Account module

This module is a lot more simpler that the previous one, start by declaring CloudWatch Event resource:

# -----------------------------------------------------------
# set up AWS Cloudwatch Event rule for EC2 events
# -----------------------------------------------------------

resource "aws_cloudwatch_event_rule" "ec2_events" {
  count = var.create ? 1 : 0

  name        =  "${var.name}-events"
  description = var.description

  event_pattern = <<PATTERN
{
  "source": [
    "aws.autoscaling"
  ],
  "detail-type": [
    "EC2 Instance Launch Successful",
    "EC2 Instance Terminate Successful",
    "EC2 Instance Launch Unsuccessful",
    "EC2 Instance Terminate Unsuccessful"
  ]
}
PATTERN
}

Then, target the default EventBus of your main account and create an IAM role for CloudWatch:

# -------------------------------------------------------------
# set up AWS Cloudwatch Event to target the default eventbus
# -------------------------------------------------------------

resource "aws_cloudwatch_event_target" "event_target" {
  count = var.create ? 1 : 0

  rule      = aws_cloudwatch_event_rule.ec2_events[0].id
  target_id = "${var.name}-eventbus"
  arn       = var.target_eventbus_arn
  role_arn  = aws_iam_role.default[0].arn
}

resource "aws_iam_role" "default" {
  count = var.create ? 1 : 0

  name                  = "${var.name}-cloudwatch-role"
  force_detach_policies = true

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

Finally, create a policy to assume the previously created IAM role (in the main account) and link it to your IAM role as follow:

# -------------------------------------------------------------
# Create a data IAM Policy to allow PutsEvents
# -------------------------------------------------------------

data "aws_iam_policy_document" "eventbus_policy" {
  count = var.create ? 1 : 0

  statement {
    sid    = "AssumeRole"
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    resources = [var.iam_role.arn]
  }

  statement {
    sid    = "CloudwatchAccess"
    effect = "Allow"

    actions = [
      "events:PutEvents"
    ]

    resources = [var.target_eventbus_arn]
  }
}


# -------------------------------------------------------------
# Link IAM Policy to IAM role
# -------------------------------------------------------------

resource "aws_iam_role_policy" "cloudwatch_policy" {
  count = var.create ? 1 : 0

  name   = "${var.name}-role"
  role   = aws_iam_role.default[0].id
  policy = data.aws_iam_policy_document.eventbus_policy[0].json
}

Please take note of the following declaration of variables in this module (partially display):

variable "target_eventbus_arn" {
  description = "(Required) AWS Resource Event Bus arn."
}

variable "iam_role" {
  description = "Main IAM role to assume"
  type = object({
    id  = string
    arn = string
  })
  default = {
    id  = ""
    arn = ""
  }
}

variable "destination_account" {
  description = "(Optional) Main AWS Account ID."
  default     = "012345678900"
}

Let’s move on to the final part for deploying all this

5. Final part and deployment

We almost reach the end of this guideline, we just need to call out our modules as follow:

# Main Account
module "main_account_events" {
  source = "./modules/eventbus-sqs"

  create     = true
  name       = "main-account-ec2-events"
  principals = var.principals

  providers = {
    aws = aws.main
  }
}

# Member Account
module "ec2_events_module" {
  source = "./modules/ec2-events"

  create              = true
  name                = "member-ec2-events"
  target_eventbus_arn = "arn:aws:events::${var.destination_account}:event-bus/default"
  iam_role            = module.main_account_events.assume_role

  providers = {
    aws = aws.member
  }
}

Don’t forget to declare your variables, then just deploy this as follow:

$ terraform init
[...]
$ terraform apply

By triggering some EC2 activities, you should start to see flowing some event into your newly created SQS queue:

SQS Monitoring

You can find all of this configuration at the following Github Repository, any questions, please let me know 😃.

Table of Contents

RELATED

serverless servers

It’s hard to believe that Lambda function have already been...

Read More
automated builds

From a piece of code from a Developer laptop to...

Read More