Search code examples
amazon-web-servicesaws-lambdaterraformterraform-provider-aws

Terraform Lambda in VPC allow network access for HTTP Requests


I am using Terraform for the first time and got a little lost in the configuration and I am facing this issue with my Lambda Function that should be able to do a simple HTTP request to google.com or swapi.com ( or whatever other external API ). In my case in my linkedin callback I need to use the linkedin api. Here is some part of my configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.21.0"
    }

    random = {
      source  = "hashicorp/random"
      version = ">= 3.3.0"
    }

    archive = {
      source  = "hashicorp/archive"
      version = ">= 2.2.0"
    }
  }

  required_version = ">= 1.0"
}


provider "aws" {
  region     = var.region
  access_key = var.access_key
  secret_key = var.secret_key
}

resource "aws_vpc" "main_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "main-vpc"
  }

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_internet_gateway" "main_igw" {
  vpc_id = aws_vpc.main_vpc.id

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_route_table" "route-table-main" {
  vpc_id = aws_vpc.main_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main_igw.id
  }

  tags = {
    Name = "main-route-table"
  }

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_eip" "nat_eip" {
  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_nat_gateway" "nat_gateway" {
  allocation_id = aws_eip.nat_eip.id
  subnet_id     = aws_subnet.main_subnet_nat_gateway_a.id

  tags = {
    Name = "nat-gateway"
  }
}

resource "aws_route_table" "route-table-nat" {
  vpc_id = aws_vpc.main_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.nat_gateway.id
  }

  tags = {
    Name = "nat-route-table"
  }
}

resource "aws_security_group" "main_sg" {
  name        = "main-sg"
  description = "Terraform SG"
  vpc_id      = aws_vpc.main_vpc.id

  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24" ]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_subnet" "main_subnet_a" {
  vpc_id            = aws_vpc.main_vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-2a"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_subnet" "main_subnet_b" {
  vpc_id            = aws_vpc.main_vpc.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-2b"

  lifecycle {
    prevent_destroy = true
  }
}


resource "aws_subnet" "main_subnet_c" {
  vpc_id            = aws_vpc.main_vpc.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = "us-east-2c"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_subnet" "main_subnet_nat_gateway_a" {
  vpc_id            = aws_vpc.main_vpc.id
  cidr_block        = "10.0.4.0/24"
  availability_zone = "us-east-2a"
}

resource "aws_subnet" "main_subnet_nat_gateway_b" {
  vpc_id = aws_vpc.main_vpc.id
  cidr_block = "10.0.5.0/24"
  availability_zone = "us-east-2b"
}

resource "aws_subnet" "main_subnet_nat_gateway_c" {
  vpc_id = aws_vpc.main_vpc.id
  cidr_block = "10.0.6.0/24"
  availability_zone = "us-east-2c"
}


resource "aws_db_subnet_group" "db_subnet_group" {
  name        = "db_subnet_group"
  description = "My DB subnet group"

  subnet_ids = [
    aws_subnet.main_subnet_a.id,
    aws_subnet.main_subnet_b.id,
    aws_subnet.main_subnet_c.id
  ]

  lifecycle {
    prevent_destroy = true
  }
}


resource "aws_route_table_association" "subnet-association-a" {
  subnet_id      = aws_subnet.main_subnet_a.id
  route_table_id = aws_route_table.route-table-main.id

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_route_table_association" "subnet-association-b" {
  subnet_id      = aws_subnet.main_subnet_b.id
  route_table_id = aws_route_table.route-table-main.id

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_route_table_association" "subnet-association-c" {
  subnet_id      = aws_subnet.main_subnet_c.id
  route_table_id = aws_route_table.route-table-main.id

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_route_table_association" "subnet-association-nat-gateway-a" {
  subnet_id = aws_subnet.main_subnet_nat_gateway_a.id
  route_table_id = aws_route_table.route-table-nat.id
}


resource "aws_route_table_association" "subnet-association-nat-gateway-b" {
  subnet_id = aws_subnet.main_subnet_nat_gateway_b.id
  route_table_id = aws_route_table.route-table-nat.id
}

resource "aws_route_table_association" "subnet-association-nat-gateway-c" {
  subnet_id = aws_subnet.main_subnet_nat_gateway_c.id
  route_table_id = aws_route_table.route-table-nat.id
}

resource "aws_iam_role" "lambda_exec" {
  name               = "lambda-exec"
  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}


resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_iam_role_policy_attachment" "lambda_sns_policy" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSNSFullAccess"
}

data "archive_file" "lambda_auth" {
  type = "zip"

  source_dir  = "../${path.module}/lambdas/auth"
  output_path = "../${path.module}/lambdas/auth.zip"
}

resource "aws_s3_object" "lambda_auth" {
  bucket = aws_s3_bucket.lambda_bucket.id

  key    = "auth.zip"
  source = data.archive_file.lambda_auth.output_path

  etag = filemd5(data.archive_file.lambda_auth.output_path)
}

resource "aws_lambda_function" "auth_test" {
  function_name = "auth-test"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_auth.key

  runtime = "nodejs18.x"
  handler = "index.test"

  source_code_hash = data.archive_file.lambda_auth.output_base64sha256

  role = aws_iam_role.lambda_exec.arn

  environment {
    variables = {
      RDS_DATABASE_NAME = var.database_name
      RDS_DATABASE_HOST = aws_db_instance.cyrannus_database.address
      RDS_DATABASE_USER = aws_db_instance.cyrannus_database.username
      RDS_DATABASE_PASS = aws_db_instance.cyrannus_database.password
    }
  }

  vpc_config {
    security_group_ids = [aws_security_group.main_sg.id]
    subnet_ids         = [aws_subnet.main_subnet_nat_gateway_a.id, aws_subnet.main_subnet_nat_gateway_b.id, aws_subnet.main_subnet_nat_gateway_c.id]
  }

  timeout = 60
}

resource "aws_lambda_function" "auth_linkedin_callback" {
  function_name = "auth-linkedin-callback"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_auth.key

  runtime = "nodejs18.x"
  handler = "index.linkedinCallback"

  source_code_hash = data.archive_file.lambda_auth.output_base64sha256

  role = aws_iam_role.lambda_exec.arn

  environment {
    variables = {
      RDS_DATABASE_NAME = var.database_name
      RDS_DATABASE_HOST = aws_db_instance.cyrannus_database.address
      RDS_DATABASE_USER = aws_db_instance.cyrannus_database.username
      RDS_DATABASE_PASS = aws_db_instance.cyrannus_database.password
      LINKEDIN_CLIENT_ID = var.linkedin_client_id
      LINKEDIN_CLIENT_SECRET = var.linkedin_client_secret
      LINKEDIN_REDIRECT_URI = "${aws_apigatewayv2_stage.dev.invoke_url}/auth/linkedin/callback"
    }
  }

  vpc_config {
    security_group_ids = [aws_security_group.main_sg.id]
    subnet_ids         = [aws_subnet.main_subnet_nat_gateway_a.id, aws_subnet.main_subnet_nat_gateway_b.id, aws_subnet.main_subnet_nat_gateway_c.id]
  }

  timeout = 60
}


resource "aws_cloudwatch_log_group" "auth_test" {
  name = "/aws/lambda/${aws_lambda_function.auth_test.function_name}"

  retention_in_days = 14
}

resource "aws_cloudwatch_log_group" "auth_linkedin_callback" {
  name = "/aws/lambda/${aws_lambda_function.auth_linkedin_callback.function_name}"

  retention_in_days = 14
}

I've tried to use NAT Gateway as presented in some tutorial but I am a little lost, AWS Console interface seems to do a lot of things by default and I am not sure what I am missing.

I've managed for example to create an EC2 that is in the VPC to access a RDS that is in the same VPC and gave it network access with IGW and I am able to to curl commands an added a simple phpmyadmin.


Solution

  • You have placed a NAT Gateway in a subnet that has a route to the NAT Gateway. That's a circular reference. This subnet does not actually have a route to the Internet, so the NAT Gateway will not actually work.

    • Your NAT Gateway should be in a subnet that has a route to the Internet Gateway.
    • Your Lambda function should be in a subnet that has a route to the NAT Gateway.