Skip to content

Commit

Permalink
feat: initial module version (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
MickVanDuijn authored Aug 19, 2022
1 parent ff18b20 commit e2aab8c
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @skyleague/oss
9 changes: 9 additions & 0 deletions .github/workflows/tfsec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: tfsec
on: push

jobs:
tfsec:
uses: skyleague/node-standards/.github/workflows/reusable-tfsec.yml@main
with:
terraform-version: "1.2.7"
working-directory: "./"
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SkyLeague `aws-lambda` - easy AWS Lambda deployments with Terraform

[![tfsec](https://github.com/skyleague/aws-lambda/actions/workflows/tfsec.yml/badge.svg?branch=main)](https://github.com/skyleague/aws-lambda/actions/workflows/tfsec.yml)

This module simplifies the deployment of AWS Lambda Functions using Terraform, as well as simplifying the adoption of the [Principle of Least Privilege](https://aws.amazon.com/blogs/security/techniques-for-writing-least-privilege-iam-policies/). When using this module, there is no need to attach AWS Managed Policies for basic functionality (CloudWatch logging, XRay tracing, VPC access). The Principle of Least Privilege is achieved by letting this module create a separate role for each Lambda Function. This role is granted the bare minimum set of permissions to match the configuration provided to this module. For example, `xray` permissions are automatically granted if (and only if) `xray_tracing_enabled = true`. Similar (dynamic) permissions are provided for other inputs (see [`iam.tf`](./iam.tf) for all dynamic permissions). Additional `existing_policy_arns` and `inline_policies` can be provided to grant the Lambda Function more permissions required by the application code (think of an S3 bucked or DynamoDB table used by your application code).

## Usage

```terraform
module "this" {
source = "[email protected]:skyleague/aws-lambda.git?ref=v1.0.0
function_name = "hello-world"
local_artifact = {
type = "dir"
path = "${path.module}/.build/hello-world"
s3_bucket = "my-artifact-bucket"
s3_prefix = null
}
}
```

## Options

For a complete reference of all variables, have a look at the descriptions in [`variables.tf`](./variables.tf).

## Outputs

The module outputs the `lambda`, `log_group` and `role` as objects, providing the flexibility to extend the Lambda Function with additional functionality, and without limiting the set of exposed outputs.

## Future additions

This is the initial release of the module, with a very minimal set of standardized functionality. Most other functionality can already be achieved by utilizing the outputs, even the ones mentioned for standardization below. We plan on standardizing more integrations, so feel free to leave suggestions! Candidates include:

- Event triggers with automatic Least Privilige permissions added to the Lambda role
- ... **Your suggestions!**
69 changes: 69 additions & 0 deletions artifact.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# When s3_artifact is provided, fetch the metadata of the object
data "aws_s3_object" "artifact" {
count = var.s3_artifact == null ? 0 : 1

bucket = var.s3_artifact.bucket
key = var.s3_artifact.key
version_id = var.s3_artifact.version_id

lifecycle {
precondition {
condition = var.local_artifact == null
error_message = "Variable s3_artifact should not be provided when local_artifact is provided"
}
}
}

locals {
s3_artifact_prefix = try(
var.local_artifact.s3_prefix != null ? "${var.local_artifact.s3_prefix}/${var.function_name}" : var.function_name,
var.function_name
)
}

# When local_artifact is provided and the type is "dir", zip the contents of the directory
data "archive_file" "artifact_dir" {
count = var.local_artifact == null ? 0 : var.local_artifact.type == "dir" ? 1 : 0

type = "zip"
source_dir = var.local_artifact.path
output_path = "${path.module}/.artifacts/handler.zip"
}
# Upload the zipped artifact to S3
resource "aws_s3_object" "artifact_dir" {
count = var.local_artifact == null ? 0 : var.local_artifact.type == "dir" ? 1 : 0

bucket = var.local_artifact.s3_bucket
key = "${local.s3_artifact_prefix}/handler.zip"
source = data.archive_file.artifact_dir[0].output_path
source_hash = data.archive_file.artifact_dir[0].output_base64sha256

lifecycle {
precondition {
condition = var.s3_artifact == null
error_message = "Variable local_artifact should not be provided when s3_artifact is provided"
}
}
}

# Upload the pre-existing artifact zip to S3
resource "aws_s3_object" "artifact_zip" {
count = var.local_artifact == null ? 0 : var.local_artifact.type == "zip" ? 1 : 0

bucket = var.local_artifact.s3_bucket
key = "${local.s3_artifact_prefix}/${basename(var.local_artifact.path)}"
source = var.local_artifact.path
source_hash = filebase64sha256(var.local_artifact.path)

lifecycle {
precondition {
condition = var.s3_artifact == null
error_message = "Variable local_artifact should not be provided when s3_artifact is provided"
}
}
}

locals {
# Choose the correct artifact according to the input definition
artifact = var.local_artifact == null ? data.aws_s3_object.artifact[0] : var.local_artifact.type == "zip" ? aws_s3_object.artifact_zip[0] : aws_s3_object.artifact_dir[0]
}
101 changes: 101 additions & 0 deletions iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}

resource "aws_iam_role" "this" {
path = "/lambda/"
name_prefix = var.function_name

assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "this" {
statement {
effect = "Allow"
actions = [
# We explicitly exclude the CreateLogGroup action
# The log group is created by Terraform
"logs:CreateLogStream",
"logs:PutLogEvents"
]

#tfsec:ignore:aws-iam-no-policy-wildcards
resources = ["${aws_cloudwatch_log_group.this.arn}:*", ]
}

dynamic "statement" {
for_each = var.xray_tracing_enabled ? [true] : []
content {
effect = "Allow"
actions = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
]
resources = ["*"]
}
}

dynamic "statement" {
for_each = var.vpc_config != null ? [true] : []
content {
effect = "Allow"
actions = [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ec2:AssignPrivateIpAddresses",
"ec2:UnassignPrivateIpAddresses",
]
resources = ["*"]
}
}

dynamic "statement" {
for_each = var.dead_letter_arn != null ? [var.dead_letter_arn] : []
content {
effect = "Allow"
actions = can(regex("^arn:aws:sqs", statement.value)) ? ["sqs:SendMessage"] : can(regex("^arn:aws:sns", statement.value)) ? ["sns:Publish"] : []
resources = [statement.value]
}
}

dynamic "statement" {
for_each = var.file_system_config != null ? [var.file_system_config] : []
content {
effect = "Allow"
actions = statement.value.read_only == false ? [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
] : ["elasticfilesystem:ClientMount"]
resources = [statement.value.arn]
}
}
}

resource "aws_iam_role_policy" "this" {
role = aws_iam_role.this.id
name_prefix = "base"
policy = data.aws_iam_policy_document.this.json
}

resource "aws_iam_role_policy" "inline_policies" {
for_each = var.inline_policies

role = aws_iam_role.this.id
name_prefix = each.key
policy = each.value.json
}

resource "aws_iam_role_policy_attachment" "existing_policies" {
for_each = var.existing_policy_arns

role = aws_iam_role.this.id
policy_arn = each.value
}
74 changes: 74 additions & 0 deletions lambda.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
locals {
environment = merge(
can(regex("^nodejs", var.runtime)) ? {
AWS_NODEJS_CONNECTION_REUSE_ENABLED = "1"
} : {},
var.environment,
)
}

resource "aws_lambda_function" "this" {
function_name = var.function_name
description = var.description

handler = var.handler
runtime = var.runtime
memory_size = var.memory_size
timeout = var.timeout

architectures = var.graviton ? ["arm64"] : ["x86_64"]

role = aws_iam_role.this.arn

dynamic "environment" {
for_each = length(keys(local.environment)) > 0 ? [local.environment] : []
content {
variables = environment.value
}
}

tracing_config {
mode = var.xray_tracing_enabled ? "Active" : "Passive"
}

ephemeral_storage {
size = var.ephemeral_storage
}

s3_bucket = local.artifact.bucket
s3_key = local.artifact.key
s3_object_version = local.artifact.version_id
source_code_hash = coalesce(try(local.artifact.source_code_hash, null), base64encode(local.artifact.etag))

dynamic "dead_letter_config" {
for_each = var.dead_letter_arn != null ? [var.dead_letter_arn] : []
content {
target_arn = dead_letter_config.value
}
}

dynamic "file_system_config" {
for_each = var.file_system_config != null ? [var.file_system_config] : []
content {
arn = file_system_config.value.arn
local_mount_path = file_system_config.value.local_mount_path
}
}

dynamic "vpc_config" {
for_each = var.vpc_config != null ? [var.vpc_config] : []
content {
security_group_ids = vpc_config.value.security_group_ids
subnet_ids = vpc_config.value.subnet_ids
}
}

lifecycle {
precondition {
condition = var.s3_artifact != null || var.local_artifact != null
error_message = "Either local_artifact or s3_artifact is required."
}
}
}


5 changes: 5 additions & 0 deletions logs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "aws_cloudwatch_log_group" "this" {
name = "/aws/lambda/${var.function_name}"
retention_in_days = var.log_retention_in_days
kms_key_id = var.log_kms_key_id
}
11 changes: 11 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
output "lambda" {
value = aws_lambda_function.this
}

output "log_group" {
value = aws_cloudwatch_log_group.this
}

output "role" {
value = aws_iam_role.this
}
Loading

0 comments on commit e2aab8c

Please sign in to comment.