lambda (terraform)

Outgoing connections via NATGW

Only if it's needed to connect to non-public resources available only if connecting from the static IP addresses of the NATGW.
For lambda to make outgoing connections via NATGW, it is needed to connect the lambda to the VPC.


For example (for 'cou' organization'), for attaching the lambda to the VPC, definition inside the lambda:

vpc_config {

    subnet_ids = var.private_subnets_id

    security_group_ids = [

      aws_security_group.auth_cou.id,

    ]

  }


It's important to take into account that also is needed to add a policy to allow the lambda function rise network interfaces.

resource "aws_iam_role_policy_attachment" "auth_cou_policy_vpc" {

  role       = aws_iam_role.auth_cou.id

  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"

}


In case of using serverless framework, this is the link to the documentation:

https://www.serverless.com/framework/docs/providers/aws/guide/functions#:~:text=handler%3A%20handler.users-,VPC%20IAM%20permissions,-The%20Lambda%20function


Lambda@Edge sample (intercepts 403 and 404 responses from origin S3 and redirects user to /index.html)

Solution for the reload page [F5] issue when frontend is deployed to an S3 bucket (served by cloudfront).

The Lambda@Edge is configured in block lambda_function_association inside the aws_cloudfront_distribution.

Ref: own title val, final project def


Frontend (css, html, javascript) in S3 bucket 

The goal is to have, once deployed, an index.html file placed in the S3 bucket root so that the Lambda@Edge can return it to the user's browser.

Note: This sample uses Angular framework, but the idea is the same for any another frontend development


src/assets/index.html

The file to be copied to the S3 bucket root

<!DOCTYPE HTML>

<html lang="en-US">

    <head>

        <meta charset="UTF-8">

        <meta name="description" content="package.json postbuild copies it to the dist root">

        <script type="text/javascript">

            window.location.href ="/myapp_front/en/index.html"

        </script>

    </head>

    <body>

    </body>

</html>


package.json

Notice the "postbuild" (copy:index) and the "devDependencies" (cpx-fixed)

{

  "name": "myapp_front",

  "version": "5.0.0",

  "scripts": {

    "ng": "ng",

    "start": "ng serve",

    "build": "ng build --prod",

    "test": "ng test",

    "lint": "ng lint",

    "e2e": "ng e2e",

    "start:ca": "ng serve --configuration=ca",

    "build:ca": "ng build --configuration=production-ca",

    "start:es": "ng serve --configuration=es",

    "build:es": "ng build --configuration=production-es",

    "extract": "ng xi18n --output-path=locale",

    "package": "npm install && npm run build && npm run build:ca && npm run build:es",

    "postbuild": "npm run copy:index && node -p \"require('./package.json').version\" > dist/defensatf_front/version",

    "copy:index": "cpx-fixed 'src/assets/index.html' 'dist/'"

  },

  "private": true,

  "dependencies": {

    ...

  },

  "devDependencies": {

    ...

    "cpx-fixed": "^1.6.0",

    ...

  }

}


Terraform CloudFront

modules/lambda/variables.tf

variable "iam_role_lambdaedge_exec_arn" {

  description = "ARN of the IAM execution role for Lambda@Edge"

  type        = string

  nullable    = false

}

modules/lambda/outputs.tf

output "lambdaedge_s3_origin_response_qualified_arn" {

  value = aws_lambda_function.lambdaedge_s3_origin_response.qualified_arn

}

modules/lambda/lambdaedge.tf

# Get the default tags from the provider

data "aws_default_tags" "app" {}


locals {

  env = data.aws_default_tags.app.tags.env

  ci  = data.aws_default_tags.app.tags.ci

}


provider "aws" {

  alias = "n_virginia"

  default_tags {

    tags = {

      env        = local.env

      ci         = local.ci

      Department = data.aws_default_tags.app.tags.Department

      Program    = data.aws_default_tags.app.tags.Program

    }

  }

  region = "us-east-1"

}


#DOC. When CloudFront uses a Lambda@Edge, you need to wait a few hours (!) after you delete the distribution to delete the function.

#     Keep trying to terraform destroy until you succeed.

resource "aws_lambda_function" "lambdaedge_s3_origin_response" {

  #Makes sure this function goes to us-east-1 no matter where the rest of the resources are deployed

  provider = aws.n_virginia


  #Required args

  function_name = "${local.env}-${local.ci}-lamdaedge-s3-origin-response"

  role          = var.iam_role_lambdaedge_exec_arn



  #Optional args

  description      = "Redirect to /index.html if S3 response is 403 or 404 (F5 reload)"

  filename         = "${path.module}/lambda_file/edge_s3_origin_response-23.3.13.zip"

  handler          = "index.handler"

  memory_size      = 128

  publish          = true

  runtime          = "nodejs14.x"

  source_code_hash = filebase64sha256("${path.module}/lambda_file/edge_s3_origin_response-23.3.13.zip")

  timeout          = 1

  tracing_config {

    mode = "Active"

  }

}

modules/lambda/lambda_file/edge_s3_origin_response-23.3.13.zip

Note: Archive contains only one file, with name "index.js" and the following content

"use strict";


/**

 * Region us-east-1 (N. Virginia): You must be in this Region to create Lambda@Edge functions.

 *

 * This function updates the HTTP status code in the response to 302, to redirect to the index.html. Note the following:

 * 1. The function is triggered in a CloudFront origin response

 * 2. The response status from the origin server is an error status code (403 or 404)

 */

exports.handler = async (event, context) => {

    const request = event.Records[0].cf.request;

    const response = event.Records[0].cf.response;


    if (response.status == 403 || response.status == 404) {

        const redirect_path = "/index.html";

            

        response.status = 302;

        response.statusDescription = "Found";

        

        /* Drop the body, as it is not required for redirects */

        delete response.body;

        response.headers["cou-lambdaedge"] = [{ key: "cou-lambdaedge", value: context.functionName + " v" + context.functionVersion }];

        response.headers["location"] = [{ key: "Location", value: redirect_path }];

    }    


    return response;

};


modules/iam/iam.tf

#Lambda@Edge execution role with permission to upload logs to CloudWatch and trigger from CloudFront

resource "aws_iam_role" "lambdaedge_exec" {

  #Required args

  assume_role_policy = <<EOF

{

    "Version": "2012-10-17",

    "Statement": [

        {

            "Effect": "Allow",

            "Principal": {

                "Service": [

                    "edgelambda.amazonaws.com",

                    "lambda.amazonaws.com"

                ]

            },

            "Action": "sts:AssumeRole"

        }

    ]

}

EOF

  #Optional args

  description = "${var.env}-${var.ci} Lambda@Edge execution role with permission to upload logs to CloudWatch and trigger from CloudFront"

  name        = "${var.env}-${var.ci}-lambdaedge-role"

  path        = "/${var.env}/${var.ci}/"

}

modules/iam/outputs.tf

output "role_lambdaedge_exec_arn" {

  value = aws_iam_role.lambdaedge_exec.arn

}


modules/cloudfront/variables.tf

variable "lambda_lambdaedge_s3_origin_response_qualified_arn" {

  description = "Qualified ARN of Lambda@Edge of CloudFront function association for the event type origin-response"

  type        = string

  nullable    = false

}

modules/cloudfront/outputs.tf

modules/cloudfront/cloudfront.tf

resource "aws_cloudfront_distribution" "myapp" {

...

  # Cache behavior with precedence 0

  ordered_cache_behavior {

    path_pattern     = "/myapp_front/*"

    allowed_methods  = ["GET", "HEAD", "OPTIONS"]

    cached_methods   = ["GET", "HEAD", "OPTIONS"]

    target_origin_id = "${var.bucket_myappp}-${var.env}"


    forwarded_values {

      query_string = false

      headers      = ["Origin", "Authorization"]


      cookies {

        forward = "none"

      }

    }

    lambda_function_association {

      event_type   = "origin-response" #{viewer-request, origin-request, viewer-response, origin-response}

      lambda_arn   = var.lambda_lambdaedge_s3_origin_response_qualified_arn

      include_body = false #The IncludeBody option can only be used with viewer-request or origin-request events

    }


    min_ttl                = 0

    default_ttl            = var.default-ttl

    max_ttl                = var.max-ttl

    compress               = true

    viewer_protocol_policy = "redirect-to-https"

  }

...

}


pre/main.tf

module "cloudfront" {

  source = "../modules/cloudfront"

  ...

  lambda_lambdaedge_s3_origin_response_qualified_arn = module.lambda.lambdaedge_s3_origin_response_qualified_arn

  ...

}


module "lambda" {

  source       = "../modules/lambda"

  ...

  iam_role_lambdaedge_exec_arn     = module.iam.role_lambdaedge_exec_arn

  ...

}