Terraform Lambda ใน VPC (Peered with MongoDB Atlas) Connection NAT Gateway

Aug 18 2020

ฉันได้สร้างการกำหนดค่า Terraform ที่ปรับใช้คลัสเตอร์ MongoDB atlas cloud และตั้งค่าเพียร์ VPC ด้วยบัญชี AWS ของฉัน การกำหนดค่า Terraform จะเก็บข้อมูลรับรองใน AWS Secrets Manager ขออภัย Lambda ของฉันไม่สามารถเข้าถึงปลายทาง Secrets Manager API หรือคลัสเตอร์ MongoDB ที่โฮสต์บน Atlas ฉันได้อ่านแล้วว่าฉันจำเป็นต้องตั้งค่าเกตเวย์ NAT บน VPC ของฉันเพื่อเข้าถึงอินเทอร์เน็ตสาธารณะ ฉันไม่ใช่มืออาชีพด้านเครือข่ายและฉันได้ลองเพิ่มการกำหนดค่าต่างๆมากมาย แต่ก็ไม่มีประโยชน์ กรุณาช่วย:

  1. ฉันต้องตั้งค่าเกตเวย์ NAT สำหรับ VPC ของฉันเพื่อเข้าถึง Secrets Manager หรือไม่ หรือฉันสามารถโฮสต์ Secret ใน VPC ได้หรือไม่? แนวทางปฏิบัติที่ดีที่สุดคืออะไร?
  2. ฉันจำเป็นต้องตั้งค่าเกตเวย์ NAT สำหรับ Lambda ของฉันเพื่อเข้าถึงคลัสเตอร์ MongoDB ที่โฮสต์ Atlas ของฉันหรือไม่แม้ว่าพวกเขาจะอยู่ใน VPC เดียวกันและฉันได้อนุญาตพิเศษให้กับกลุ่มความปลอดภัยที่ Lambda ของฉันอยู่
  3. ฉันจะตั้งค่า NAT Gateway เพื่อให้ Lambda เชื่อมต่อกับ Atlas Cluster ใน Terraform ได้อย่างไร

ตามหลักการแล้วฉันต้องการปิดการเชื่อมต่อกับอินเทอร์เน็ตภายนอกให้มากที่สุด แต่ถ้านั่นไม่ใช่ตัวเลือกฉันก็สามารถใช้งานได้ตามปกติ

นี่คือการกำหนดค่า Terraform ของฉัน

variable "admin_profile" {
  type = string
  default = "superadmin"
}


variable "region" {
  type    = string
  default = "us-west-2"
}

provider "aws" {
  profile = var.admin_profile
  region  = "us-west-2"
  alias   = "admin"
}


// create mongo db organization + cluster on atlas

provider "mongodbatlas" {
  public_key  = var.atlas_public_key
  private_key = var.atlas_private_key
}

//superadmin creds
variable aws_account_id {
  type = string
}

variable atlas_private_key {
  type = string
}

variable atlas_public_key {
  type = string
}

variable atlas_region {
  type    = string
  default = "US_WEST_2"
}

variable atlas_org_id {
  type    = string
  default = "" #EXCLUDE THIS
}


// generated creds for db
variable atlas_db_user {
  default = "mongo_user"
}

resource "random_password" "password" {
  length  = 16
  special = false
  #override_special = "_%-"
}

locals {
  atlas_db_password = random_password.password.result
}

variable atlas_db_vpc_cidr {
  default = "192.168.224.0/21"
}

// resources
resource "mongodbatlas_project" "cluster-partner-project" {
  name   = "live"
  org_id = var.atlas_org_id
}

resource "mongodbatlas_cluster" "cluster-partner" {
  project_id                   = mongodbatlas_project.cluster-partner-project.id
  name                         = "cluster-partner"
  num_shards                   = 1
  replication_factor           = 3
  provider_backup_enabled      = true
  cluster_type                 = "REPLICASET"
  auto_scaling_disk_gb_enabled = true
  mongo_db_major_version       = "4.2"

  //Provider Settings "block"
  provider_name               = "AWS"
  disk_size_gb                = 40
  provider_disk_iops          = 120
  provider_volume_type        = "STANDARD"
  provider_encrypt_ebs_volume = true
  provider_instance_size_name = "M10"
  provider_region_name        = var.atlas_region
}

resource "mongodbatlas_database_user" "cluster-partner-user" {
  username           = var.atlas_db_user
  password           = local.atlas_db_password
  auth_database_name = "admin"
  project_id         = mongodbatlas_project.cluster-partner-project.id
  roles {
    role_name     = "readAnyDatabase"
    database_name = "admin"
  }

  roles {
    role_name     = "readWrite"
    database_name = "app_db"
  }
}

resource "mongodbatlas_network_container" "cluster-partner-network" {
  atlas_cidr_block = var.atlas_db_vpc_cidr
  project_id       = mongodbatlas_project.cluster-partner-project.id
  provider_name    = "AWS"
  region_name      = var.atlas_region
}

resource "mongodbatlas_network_peering" "cluster-partner-network-peering" {
  accepter_region_name   = var.region
  project_id             = mongodbatlas_project.cluster-partner-project.id
  container_id           = mongodbatlas_network_container.cluster-partner-network.container_id
  provider_name          = "AWS"
  route_table_cidr_block = aws_vpc.primary.cidr_block
  vpc_id                 = aws_vpc.primary.id
  aws_account_id         = var.aws_account_id
}

resource "mongodbatlas_project_ip_whitelist" "default-db-access" {
  project_id         = mongodbatlas_project.cluster-partner-project.id
  aws_security_group = aws_security_group.primary_default.id
  comment            = "Access for App to MongoDB"
  depends_on         = [mongodbatlas_network_peering.cluster-partner-network-peering]
}

// create a vpc in AWS
resource "aws_vpc" "primary" {
  provider             = aws.admin
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
}
// Internet Gateway
resource "aws_internet_gateway" "primary" {
  provider = aws.admin
  vpc_id   = aws_vpc.primary.id
}
// route table
resource "aws_route" "primary-internet_access" {
  provider               = aws.admin
  route_table_id         = aws_vpc.primary.main_route_table_id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.primary.id
}

resource "aws_route" "peeraccess" {
  provider                  = aws.admin
  route_table_id            = aws_vpc.primary.main_route_table_id
  destination_cidr_block    = var.atlas_db_vpc_cidr
  vpc_peering_connection_id = mongodbatlas_network_peering.cluster-partner-network-peering.connection_id
  depends_on                = [aws_vpc_peering_connection_accepter.peer]
}

//subnets

//public
resource "aws_subnet" "primary-az1" {
  provider                = aws.admin
  tags = {
    Name = "public primary subnet"
  }
  vpc_id                  = aws_vpc.primary.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "${var.region}a" } //private resource "aws_subnet" "primary-az2" { provider = aws.admin tags = { Name = "private subnet 0" } vpc_id = aws_vpc.primary.id cidr_block = "10.0.2.0/24" map_public_ip_on_launch = false availability_zone = "${var.region}b"
}

// security groups for mongo vpc connect

resource "aws_security_group" "primary_default" {
  provider    = aws.admin
  name_prefix = "defaultvpc-"
  description = "Default security group for all instances in VPC ${aws_vpc.primary.id}"
  vpc_id      = aws_vpc.primary.id
  ingress {
    from_port = 0
    to_port   = 0
    protocol  = "-1"
    cidr_blocks = [
      aws_vpc.primary.cidr_block,
      var.atlas_db_vpc_cidr
    ]
    # cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

// vpc peering auto accept

resource "aws_vpc_peering_connection_accepter" "peer" {
  provider                  = aws.admin
  vpc_peering_connection_id = mongodbatlas_network_peering.cluster-partner-network-peering.connection_id
  auto_accept               = true
}

// save mongo account details to secret manager


resource "aws_secretsmanager_secret" "partner_iam_mongo_access" {
  provider = aws.admin
  name     = "mongo-access"
}

locals {
  mongo_credentials = {
    connection_strings = mongodbatlas_cluster.cluster-partner.connection_strings
    password           = local.atlas_db_password
  }
}

resource "aws_secretsmanager_secret_version" "partner_iam_mongo_access" {
  provider      = aws.admin
  secret_id     = aws_secretsmanager_secret.partner_iam_mongo_access.id
  secret_string = jsonencode(local.mongo_credentials)
}


// create lambdas for each of the key steps in the app

// have to add the vpc

resource "aws_iam_role_policy" "lambda_policy" {
  provider = aws.admin
  name     = "lambda_policy"
  role     = aws_iam_role.lambda_role.id
  policy   = file("./lambda-policy.json")
}

data "aws_iam_policy" "aws_lambda_vpc_access_execution_role" {
  provider = aws.admin
  arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_iam_role" "lambda_role" {
  provider           = aws.admin
  name               = "lambda-vpc-role-managed"
  assume_role_policy = file("./lambda-assume-policy.json")
}

data "archive_file" "test-connection" {
  type        = "zip"
  source_file = "./test-connection"
  output_path = "./test-connection_deploy.zip"
}

resource "aws_lambda_function" "test-connection" {
  provider         = aws.admin
  filename         = "./test-connection_deploy.zip"
  function_name    = "test-connection"
  role             = aws_iam_role.lambda_role.arn
  handler          = "test-connection"
  runtime          = "go1.x"
  timeout          = 15
  source_code_hash = data.archive_file.test-connection.output_base64sha256
  vpc_config {
    subnet_ids         = [aws_subnet.primary-az1.id] // public subnet
    security_group_ids = [aws_security_group.primary_default.id]
  }

}

นี่คือ tfvars ของฉัน

admin_profile     = "default"
atlas_private_key = 
atlas_public_key  = 
atlas_org_id      = 
aws_account_id    = 

นี่คือนโยบาย Lambda ของฉัน (lambda-policy.json)

{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Action":[
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "ec2:DescribeNetworkInterfaces",
            "ec2:CreateNetworkInterface",
            "ec2:DeleteNetworkInterface",
            "ec2:DescribeInstances",
            "ec2:AttachNetworkInterface",
            "secretsmanager:DescribeSecret",
            "secretsmanager:GetSecretValue",
            "secretsmanager:ListSecretVersionIds",
            "secretsmanager:ListSecrets"
         ],
         "Resource":"*"
      }
   ]
}

นี่คือนโยบายแลมบ์ดาของฉัน (lambda-สมมติ-policy.json)

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

และนี่คือรหัส (GoLang) สำหรับ Lambda ของฉัน

package main

import (
    "context"
    "fmt"
    "errors"
    "time"
    "encoding/json"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/sparrc/go-ping"
    "github.com/aws/aws-sdk-go/service/secretsmanager"  
    "go.mongodb.org/mongo-driver/mongo"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)


type MongoCreds struct {
    ConnectionStrings []map[string]interface{} `json:"connection_strings"`
    Password          string   `json:"password"`
}

var MainRegion = "us-west-2"

func HandleRequest(ctx context.Context, updatedValues interface{}) (string, error) {
    fmt.Println("we are pinging")
    pinger, err := ping.NewPinger("www.google.com")
    if err != nil {
        panic(err)
    }

    pinger.Count = 3
    pinger.Run() // blocks until finished
    stats := pinger.Statistics() // get send/receive/rtt stats
    fmt.Println(stats)
    fmt.Println("connecting to mongo")
    err = ConnectToMongoClient()
    if err != nil {
        fmt.Println("failure to connect to mongo")
    }
    return "", err
}

func ConnectToMongoClient() error {
    sess := session.Must(session.NewSession(&aws.Config{
        Region: aws.String(MainRegion),
    }))

    svc := secretsmanager.New(sess)
    input := &secretsmanager.GetSecretValueInput{
        SecretId: aws.String("mongo-access"),
    }
    fmt.Println("getting credentials")

    secret, err := svc.GetSecretValue(input)
    if err != nil {
        return err
    }
    var mongo_creds MongoCreds
    secretJson := []byte(*secret.SecretString)
    err = json.Unmarshal(secretJson, &mongo_creds)
    fmt.Println("credentials fetched")
    fmt.Println(mongo_creds)
    if err != nil {
        return err
    }
    var mongoURI string 
    for _, connection := range(mongo_creds.ConnectionStrings) {
        if val, ok := connection["standard_srv"]; ok {
            mongoURI = val.(string)
        }
    }
    if mongoURI == "" {
        return errors.New("Unable to parse a connecting string from secret")
    }
    clientOptions := options.Client().ApplyURI(mongoURI).SetAuth(options.Credential{Username: "mongo_user", Password: mongo_creds.Password})
    ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
    client, err := mongo.Connect(ctx, clientOptions)
    fmt.Println("connecting")
    if err != nil {
        fmt.Println(err.Error())
        return err
    }

    ctx, _ = context.WithTimeout(context.Background(), 5*time.Second)
    if err = client.Ping(ctx, readpref.Primary()); err != nil {
        return err
    }
    return err
}

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

หากใครสามารถแนะนำการใช้งานหรือการปรับเปลี่ยนการกำหนดค่า VPC ของฉันหรือโค้ด Lambda ของฉันที่อนุญาตให้เข้าถึง Secrets Manager และ Mongo Cluster ของฉัน ตามหลักการแล้วการรักษาปริมาณการใช้งานทั้งหมดใน VPC แต่หากจำเป็นต้องเข้าถึงอินเทอร์เน็ตสาธารณะก็ไม่ว่ากัน

แก้ไขข้อผิดพลาดที่ฉันได้รับคือการหมดเวลา โปรดทราบว่าแม้ว่าฉันจะฮาร์ดโค้ดข้อมูลประจำตัว (และข้ามขั้นตอน Secret Manager) ฉันก็ยังหมดเวลาเมื่อพยายามเชื่อมต่อกับอินสแตนซ์ Mongo ที่โฮสต์ Atlas

คำตอบ

3 MarkB Aug 18 2020 at 23:07

ฉันต้องตั้งค่าเกตเวย์ NAT สำหรับ VPC ของฉันเพื่อเข้าถึง Secrets Manager หรือไม่ หรือฉันสามารถโฮสต์ Secret ใน VPC ได้หรือไม่?

คุณต้องสร้างเกตเวย์ NAT หรือกำหนดค่าปลายทาง VPCสำหรับ Secrets Manager

แนวทางปฏิบัติที่ดีที่สุดคืออะไร?

สร้างจุดสิ้นสุด VPCสำหรับ Secrets Manager

ฉันจำเป็นต้องตั้งค่าเกตเวย์ NAT สำหรับ Lambda ของฉันเพื่อเข้าถึงคลัสเตอร์ MongoDB ที่โฮสต์ Atlas ของฉันหรือไม่แม้ว่าพวกเขาจะอยู่ใน VPC เดียวกันและฉันได้อนุญาตพิเศษให้กับกลุ่มความปลอดภัยที่ Lambda ของฉันอยู่

ไม่จุดรวมของการเพียร์ VPC คือคุณสามารถเชื่อมต่อโดยตรงภายใน VPC ของคุณโดยไม่ต้องออกไปทางอินเทอร์เน็ต โปรดทราบว่าพวกเขาไม่ได้ "อยู่ใน VPC เดียวกัน" แต่อยู่ใน VPC สองรายการที่แยกจากกันโดยมีการเชื่อมต่อแบบเพียร์

ฉันไม่เห็นปัญหาใด ๆ ใน Terraform ของคุณดูเหมือนว่าฟังก์ชัน Lambda ควรจะเชื่อมต่อกับคลัสเตอร์ Mongo Atlas ได้ อาจช่วยได้หากคุณเพิ่มข้อความแสดงข้อผิดพลาดจริงที่คุณเห็นเมื่อไม่สามารถเชื่อมต่อกับคำถามเดิมของคุณได้

รหัส Terraform จะมีลักษณะดังนี้:

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-west-2.secretsmanager"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.sg1.id,
  ]

  private_dns_enabled = true
}