In this example we use Terraform to configure our AWS account to send alarm emails to our inbox. The logic is simple. AWS EventBridge Scheduler runs every minute to call Lambda to log something random. Logs will contain INFO, WARN and ERROR levels. This is just for simulating application logs. Now, the main part is just starting. We configure CloudWatch to watch our specific log group every 30 seconds to see if there is a new WARN or ERROR logs entry. If there is, it tells SNS to send alarm emails. This is the whole logic.


Structure


├── cmd
│   └── greeter
│   └── main.go
├── .gitignore
├── go.mod
├── go.sum
├── main.go
└── terraform
└── development
├── consumer.tf
├── main.tf
└── producer.tf

main.go


package main

import (
"context"
"os"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"golang.org/x/exp/slog"
)

func main() {
lambda.Start(handler)
}

func handler(ctx context.Context, event events.CloudWatchEvent) {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

logger.Info("scheduler is calling")

if sec := time.Now().UTC().Nanosecond(); sec%2 == 0 {
logger.Warn("an even event is detected",
slog.Any("event", event),
)

return
}

logger.Error("an uneven event is detected",
slog.Any("event", event),
)
}

.gitignore


.terraform/
terraform.tfstate*

tmp/
bin/

main.tf


terraform {
required_version = "~> 1.4.4"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.41.0"
}

archive = {
source = "hashicorp/archive"
version = "~> 2.3.0"
}

null = {
source = "hashicorp/null"
version = "~> 3.2.1"
}
}
}

provider "aws" {
profile = "development"
region = "eu-west-1"
}

producer.tf


# -- VARS ----------------------------------------------------------------------

locals {
lambda_function_name = "greeter"

go_source_path = "${path.module}/../../cmd/${local.lambda_function_name}/..."
go_binary_path = "${path.module}/../../bin/${local.lambda_function_name}"
go_zip_path = "${path.module}/../../tmp/${local.lambda_function_name}.zip"
}

# -- LAMBDA --------------------------------------------------------------------

resource "null_resource" "lambda_go_binary" {
provisioner "local-exec" {
command = "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOFLAGS=-trimpath go build -mod=readonly -ldflags='-s -w' -o ${local.go_binary_path} ${local.go_source_path}"
}
}

data "archive_file" "lambda_go_zip" {
type = "zip"
source_file = local.go_binary_path
output_path = local.go_zip_path

depends_on = [
null_resource.lambda_go_binary,
]
}

resource "aws_lambda_function" "greeter" {
function_name = local.lambda_function_name
handler = local.lambda_function_name
filename = local.go_zip_path
package_type = "Zip"
runtime = "go1.x"
timeout = 30
memory_size = 128

role = aws_iam_role.lambda_executor.arn
source_code_hash = data.archive_file.lambda_go_zip.output_base64sha256

depends_on = [
aws_cloudwatch_log_group.lambda_log_group,
]
}

resource "aws_iam_role" "lambda_executor" {
name = "greeter-lambda-executor"

managed_policy_arns = [aws_iam_policy.lambda_log.arn]

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}

resource "aws_iam_policy" "lambda_log" {
name = "greeter-lambda-log"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
Effect = "Allow"
Resource = [
"arn:aws:logs:*:*:*",
]
},
]
})
}

resource "aws_cloudwatch_log_group" "lambda_log_group" {
name = "/aws/lambda/${local.lambda_function_name}"
retention_in_days = 5
}

# -- CLOUDWATCH ----------------------------------------------------------------

resource "aws_scheduler_schedule" "schedule" {
name = "greeter-lambda-schedule"

flexible_time_window {
mode = "OFF"
}

schedule_expression = "rate(1 minutes)"

target {
arn = aws_lambda_function.greeter.arn
role_arn = aws_iam_role.schedule_executor.arn
}
}

resource "aws_iam_role" "schedule_executor" {
name = "greeter-lambda-schedule-executor"

managed_policy_arns = [aws_iam_policy.schedule_invoker.arn]

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "scheduler.amazonaws.com"
}
},
]
})
}

resource "aws_iam_policy" "schedule_invoker" {
name = "greeter-lambda-schedule-invoker"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "lambda:InvokeFunction"
Effect = "Allow"
Resource = aws_lambda_function.greeter.arn
},
]
})
}

consumer.tf


# -- CLOUDWATCH ----------------------------------------------------------------

resource "aws_cloudwatch_log_metric_filter" "lambda_log_filter" {
name = "greeter-lambda-log-filter"

pattern = "{ $.level = WARN || $.level = ERROR }"
log_group_name = aws_cloudwatch_log_group.lambda_log_group.name

metric_transformation {
name = "greeter-lambda-warning-and-error-logs"
namespace = "greeter-lambda-issues"
value = "1"
}
}

resource "aws_cloudwatch_metric_alarm" "lambda_log_alarm" {
alarm_name = "greeter-lambda-log-alarm"
alarm_description = "There is an issue with the greeter lambda function"

comparison_operator = "GreaterThanThreshold"
threshold = 0
evaluation_periods = 1
datapoints_to_alarm = 1
period = 30
statistic = "Sum"

alarm_actions = [aws_sns_topic.lambda_log_alarm.arn]
metric_name = aws_cloudwatch_log_metric_filter.lambda_log_filter.metric_transformation[0].name
namespace = aws_cloudwatch_log_metric_filter.lambda_log_filter.metric_transformation[0].namespace
}

# -- SNS -----------------------------------------------------------------------

resource "aws_sns_topic" "lambda_log_alarm" {
name = "greeter-lambda-log-alarm"
}

resource "aws_sns_topic_subscription" "lambda_log_alarm_receiver" {
topic_arn = aws_sns_topic.lambda_log_alarm.arn
protocol = "email"
endpoint = "you@example.com"
}

Provision


me:~/aws/terraform/development$ terraform apply \
-replace="null_resource.lambda_go_binary" \
-replace="archive_file.lambda_go_zip" \
-replace="aws_lambda_function.greeter"

Alarm email


You are receiving this email because your Amazon CloudWatch Alarm "greeter-lambda-log-alarm" in the EU (Ireland) region has entered the ALARM state, because "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (14/07/23 20:51:00)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition)." at "Friday 14 July, 2023 20:51:52 UTC".

View this alarm in the AWS Management Console:
https://eu-west-1.console.aws.amazon.com/cloudwatch/......

Alarm Details:
- Name: greeter-lambda-log-alarm
- Description: There is an issue with the greeter lambda function
- State Change: INSUFFICIENT_DATA -> ALARM
- Reason for State Change: Threshold Crossed: 1 out of the last 1 datapoints [1.0 (14/07/23 20:51:00)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition).
- Timestamp: Friday 14 July, 2023 20:51:52 UTC
- AWS Account: 124789012345
- Alarm Arn: arn:aws:cloudwatch:eu-west-1:124789012345:alarm:greeter-lambda-log-alarm

Threshold:
- The alarm is in the ALARM state when the metric is GreaterThanThreshold 0.0 for at least 1 of the last 1 period(s) of 30 seconds.

Monitored Metric:
- MetricNamespace: greeter-lambda-issues
- MetricName: greeter-lambda-warning-and-error-logs
- Dimensions:
- Period: 30 seconds
- Statistic: Sum
- Unit: not specified
- TreatMissingData: missing

State Change Actions:
- OK:
- ALARM: [arn:aws:sns:eu-west-1:124789012345:greeter-lambda-log-alarm]
- INSUFFICIENT_DATA: