Terraform Lambda в VPC (партнерство с MongoDB Atlas) Подключение NAT-шлюз
Я создал конфигурацию Terraform, которая развертывает облачный кластер атласа MongoDB, и настраивает одноранговый узел VPC с моей учетной записью AWS. Конфигурация terraform хранит учетные данные в AWS Secrets Manager. К сожалению, моя Lambda не может получить доступ к конечной точке API Secrets Manager или кластеру MongoDB, размещенному в Atlas. Я прочитал, что мне нужно настроить шлюз NAT на моем VPC для доступа в общедоступный Интернет. Я не профессионал в области сетевых технологий, и безрезультатно пытался добавить кучу различных конфигураций. Пожалуйста помоги:
- Нужно ли мне настраивать шлюз NAT для моего VPC для доступа к Secrets Manager? Или можно как-то разместить Секрет в VPC? Какая здесь лучшая практика?
- Нужно ли мне настраивать шлюз NAT для моей Lambda для доступа к моему кластеру MongoDB, размещенному в Atlas, даже если они находятся на том же VPC и я внес в белый список группу безопасности, в которой находится моя Lambda?
- Как настроить шлюз NAT, чтобы моя Lambda могла подключаться к моему кластеру Atlas в 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]
}
}
Вот мои тфварки
admin_profile = "default"
atlas_private_key =
atlas_public_key =
atlas_org_id =
aws_account_id =
Вот моя политика лямбда (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-accept-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 или моего лямбда-кода, которые позволят получить доступ к диспетчеру секретов и моему кластеру Mongo. В идеале сохранить весь трафик в VPC, но если необходим публичный доступ в Интернет, пусть будет так.
Изменить Ошибка, которую я получаю, - это тайм-аут. Обратите внимание: даже если я жестко закодирую учетные данные (и пропущу шаг диспетчера секретов), у меня все равно истечет время ожидания при попытке подключиться к экземпляру Mongo, размещенному в Atlas.
Ответы
Нужно ли мне настраивать шлюз NAT для моего VPC для доступа к Secrets Manager? Или можно как-нибудь просто разместить Секрет в 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
}