Skip to content
This repository has been archived by the owner on Apr 18, 2022. It is now read-only.

Commit

Permalink
Add module to run a Lambda function as a "cron" job (#21)
Browse files Browse the repository at this point in the history
* Adding a terraform module to create a cron-triggered lambda function

* Add environment variables

* Fix?? default map value for env_vars

* Fix default empty list for secrets

* Add test case

* Fix IAM copypasta

* Reversed my references 🤦

* Make the list variables into lists
  • Loading branch information
robertfairhead authored Nov 27, 2018
1 parent 68d3821 commit d062728
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 0 deletions.
23 changes: 23 additions & 0 deletions lambda_cron/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
data "aws_s3_bucket" "releases" {
bucket = "${var.domain_name}-${var.env}-lambda-releases"
}

data "aws_vpc" "vpc" {
tags {
env = "${var.env}"
}
}

data "aws_subnet" "application_subnet" {
count = 3
vpc_id = "${data.aws_vpc.vpc.id}"

tags {
name = "app-sub-${count.index}"
env = "${var.env}"
}
}

data "aws_kms_alias" "main" {
name = "alias/${var.env}-main"
}
185 changes: 185 additions & 0 deletions lambda_cron/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
locals {
handler = "${coalesce(var.handler, var.job_name)}"
}

resource "aws_lambda_function" "job" {
function_name = "${var.job_name}"
description = "Terraform-managed cron job for ${var.job_name} that invokes ${local.handler}"

s3_bucket = "${data.aws_s3_bucket.releases.id}"
s3_key = "${var.job_name}.zip"

handler = "${local.handler}"
runtime = "${var.runtime}"

// Allow to run up to 5 minutes. Max is 15 minutes
timeout = 600
memory_size = "${var.memory_size}"

role = "${aws_iam_role.job.arn}"

environment {
variables = "${var.env_vars}"
}

// Encrypts any environment variables
kms_key_arn = "${data.aws_kms_alias.main.target_key_arn}"

vpc_config {
subnet_ids = ["${data.aws_subnet.application_subnet.*.id}"]
security_group_ids = ["${aws_security_group.job.id}]"]
}

tags {
terraform = "True"
app = "${var.job_name}"
handler = "${local.handler}"
type = "cron"
}
}

#####
# Default security group for lambda function
#####
resource "aws_security_group" "job" {
name_prefix = "${var.job_name}-"
vpc_id = "${data.aws_vpc.vpc.id}"

tags {
env = "${var.env}"
terraform = "True"
app = "${var.job_name}"
name = "cron-${var.job_name}"
}
}

// Block all inbound
resource "aws_security_group_rule" "ingress" {
type = "ingress"
from_port = 0
to_port = 0
protocol = -1
self = true

security_group_id = "${aws_security_group.job.id}"
}

// Allow all outbound by default
resource "aws_security_group_rule" "lb_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]

security_group_id = "${aws_security_group.job.id}"
}

######
# Cron job setup
######
resource "aws_cloudwatch_event_rule" "crontab" {
name = "crontab-${var.env}-${var.job_name}"
description = "Terraform-managed crontab for firing ${var.job_name}"
schedule_expression = "cron(${var.cron_expression})"
}

resource "aws_cloudwatch_event_target" "crontab" {
target_id = "propman_sync_lambda_target"
rule = "${aws_cloudwatch_event_rule.crontab.name}"
arn = "${aws_lambda_function.job.arn}"
}

resource "aws_lambda_permission" "allow_cloudwatch_to_call_crontab" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.job.function_name}"
principal = "events.amazonaws.com"
source_arn = "${aws_cloudwatch_event_rule.crontab.arn}"
}

#####
# IAM role which dictates what other AWS services the Lambda function
# may access.
#####

resource "aws_iam_role" "job" {
name = "cronjob-${var.env}-${var.job_name}"

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

# Helper for allowing access to securely stored secrets
# The helper uses the main shared key, provide your own by attaching to the output
# role if you have a more restricted key used in Secrets Manager
resource "aws_iam_role_policy_attachment" "basic_exec_role" {
role = "${aws_iam_role.job.name}"
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "aws_iam_policy_document" "secrets" {
statement {
actions = [
"secretsmanager:GetSecretValue",
]

resources = "${var.secrets}"
}
}

resource "aws_iam_policy" "secrets" {
name = "${var.env}-${var.job_name}-secrets"
path = "/${var.env}/${var.job_name}/"
policy = "${data.aws_iam_policy_document.secrets.json}"
}

resource "aws_iam_role_policy_attachment" "secrets" {
role = "${aws_iam_role.job.name}"
policy_arn = "${aws_iam_policy.secrets.arn}"
}

# Use of the shared KMS key for secrets decryption
resource "aws_iam_policy" "shared_key_access" {
name = "${var.env}-${var.job_name}-key-access"
path = "/${var.env}/${var.job_name}/"
description = "Allows function to use main KMS key for environment"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "${data.aws_kms_alias.main.target_key_arn}"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "shared_key_access" {
role = "${aws_iam_role.job.name}"
policy_arn = "${aws_iam_policy.shared_key_access.arn}"
}

resource "aws_kms_grant" "primary" {
name = "${var.env}-cron-${var.job_name}"
key_id = "${data.aws_kms_alias.main.target_key_arn}"
grantee_principal = "${aws_iam_role.job.arn}"
operations = ["Decrypt"]
}
4 changes: 4 additions & 0 deletions lambda_cron/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "job_iam_role" {
description = "IAM role name for attaching additional policies to the lambda function with aws_iam_role_policy_attachment"
value = "${aws_iam_role.job.name}"
}
42 changes: 42 additions & 0 deletions lambda_cron/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
variable "env" {
description = "the name of the environment, e.g. \"testing\". it must be unique in the account."
}

variable "domain_name" {
description = "the external domain name for reaching the public resources. must have a certificate in ACM associated with it."
}

variable "job_name" {
description = "name of the job to be run. must be unique in the environment."
}

variable "cron_expression" {
description = "cron schedule expression. For example, \"0 12 * * ? *\" for daily at noon UTC. https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions"
}

variable "runtime" {
description = "OPTIONAL: which lambda runtime to use to invoke the function. Defaults to Go 1.x"
default = "go1.x"
}

variable "handler" {
description = "OPTIONAL: name of the handler for the lambda. Defaults to job_name"
default = ""
}

variable "memory_size" {
description = "OPTIONAL: memory to allocate to the lambda function. Defaults to 1024mb"
default = "1024"
}

variable "env_vars" {
type = "map"
description = "OPTIONAL: a map of environment variables to set for the job"
default = {}
}

variable "secrets" {
type = "list"
description = "OPTIONAL: a list of ARNs for Secret Manager secrets to allow job to access"
default = []
}
16 changes: 16 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,19 @@ module "wildcard" {
root_domain = "${var.domain_name}"
domain = "${var.domain_name}"
}

resource "aws_s3_bucket" "lambda_releases" {
bucket = "${var.domain_name}-${var.env}-lambda-releases"
acl = "private"

versioning {
enabled = true
}

tags {
env = "${var.env}"
domain_name = "${var.domain_name}"
terraform = "True"
app = "lambda-releases"
}
}
19 changes: 19 additions & 0 deletions test/all.tf
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,22 @@ module "postgres" {
app_sg = "${module.demo.app_sg_id}"
password = "{$var.db_password}"
}

module "lambda_cron" {
source = "../lambda_cron"

env = "${local.env}"
domain_name = "${local.domain_name}"
job_name = "crontab"
cron_expression = "* * ? * * *"

env_vars = {
"SOMETHING" = "ANYTHING"
"SOMETHING_ELSE" = "NOTHING"
}

secrets = [
"arn:aws:secretsmanager:us-east-1:000000000000:secret:${local.env}/lambda/crontab/secret1",
"arn:aws:secretsmanager:us-east-1:000000000000:secret:${local.env}/lambda/crontab/secret2",
]
}

0 comments on commit d062728

Please sign in to comment.