Skip to content

Commit 75dbec2

Browse files
authored
TEAMENG-1220: Update terraform documentation to deploy API Gateway and Lambda in a private subnet (#8)
# Changes - Updates terraform boilerplate to deploy the API Gateway and Lambda in a private subnet
1 parent 8c9fd7d commit 75dbec2

File tree

7 files changed

+210
-75
lines changed

7 files changed

+210
-75
lines changed

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@
22

33
Deploy a Lambda function within your infrastructure to enable users to decrypt sensitive data from their browser.
44

5-
Once deployed you will be able to use the API URL provided by the AWS API Gateway and add the URL as a decryptor URI for
5+
Once deployed, you will be able to use the API URL provided by the AWS API Gateway and add the URL as a decryptor URI for
66
a Formal Encryption Key.
77

88
**Note: we highly encourage making sure the API Gateway is only accessible via a VPN to prevent users outside of your organization from making requests to the /decrypt endpoint.**
99

1010
There are three deployment methods: Terraform, Serverless (via Cloudformation), and Docker.
1111

12-
## Deploying via Terraform
12+
## Deploying via Terraform (Recommended)
1313

1414
To deploy via Terraform, we recommend incorporating the configuration template provided in the `terraform` directory into your Terraform setup.
15-
To deploy the configuration as-is, run `make deploy-terraform` with your AWS credentials and with the Terraform CLI installed.
15+
To deploy the configuration as-is, run `make deploy-terraform` with your AWS credentials and with the Terraform CLI installed. This deployment deploys the API Gateway and Lambda in a *private* subnet within your VPC.
1616

17-
## Deploying via Serverless.
18-
19-
To deploy via Serverless, run `make deploy-sls` with your Serverless credentials. Note: you will need a Serverless licesnse and the Serverless CLI installed.
17+
## Deploying via Serverless
2018

19+
To deploy via Serverless, run `make deploy-sls` with your Serverless credentials. Note: you will need a Serverless licesnse, AWS Account, and the Serverless CLI installed. This deployment deploys the API Gateway and Lambda *publicly.*
2120

2221
## Deploying via Docker
2322

serverless/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Serverless Deployment Guide
22

3+
## Prerequisites
4+
5+
- A Serverless license
6+
- The Serverless CLI (`npm i -E serverless@4.21.1 -g`)
7+
- AWS Credentials with the ability to deploy API Gateways, Lambdas, EC2 instances and the associated networking.
8+
9+
10+
## A note about private deployments
11+
12+
This configuration deploys an AWS Lambda and API Gateway on the public internet. Instead, we recommend modifying this configuration to deploy this in a private subnet and require access to the endpoint via a VPN.
13+
14+
## Deployment steps
15+
316
To deploy using the Serverless framework, run the following commands:
417
```
518
npm i -E serverless@4.21.1 -g

terraform/README.md

Lines changed: 44 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Terraform Deployment Guide
22

3-
This directory contains Terraform configurations to deploy the decrypt Lambda function with API Gateway, equivalent to the serverless configuration.
3+
This directory contains Terraform configurations to deploy the decrypt Lambda function with an API Gateway in a private subnet within your VPC.
4+
5+
This deployment will put the API Gateway and Lambda in a private subnet. We recommend accessing the resulting API Gateway decryptor URI via a VPN.
46

57
## Prerequisites
68

7-
- [Terraform](https://www.terraform.io/downloads.html) >= 1.0
8-
- AWS CLI configured with appropriate credentials
9-
- The `bootstrap` binary compiled and ready for deployment
9+
- [Terraform](https://www.terraform.io/downloads.html)
10+
- An AWS VPC with a private subnet.
11+
- AWS Credentials with the ability to deploy API Gateways, Lambdas, EC2 instances and the associated networking.
1012

1113
## Files
1214

@@ -15,94 +17,63 @@ This directory contains Terraform configurations to deploy the decrypt Lambda fu
1517
- `outputs.tf` - Output definitions
1618
- `terraform.tfvars.example` - Example variables file
1719

18-
## Deployment Steps
19-
20-
### 1. Prepare the Lambda Package
21-
22-
Before deploying, you need to package the `bootstrap` binary:
23-
24-
```bash
25-
# Compile your Go code if not already done
26-
# GOOS=linux GOARCH=arm64 go build -o bootstrap main.go crypto.go
27-
28-
# Create a zip file for Lambda
29-
zip bootstrap.zip bootstrap
30-
```
31-
32-
### 2. Initialize Terraform
33-
34-
```bash
35-
terraform init
36-
```
20+
## Deployment steps
3721

38-
### 3. Configure Variables (Optional)
39-
40-
Copy the example variables file and customize if needed:
22+
1. First, copy the example variables and update them.
4123

4224
```bash
4325
cp terraform.tfvars.example terraform.tfvars
4426
# Edit terraform.tfvars with your preferred values
4527
```
4628

47-
### 4. Plan the Deployment
48-
49-
Review what resources will be created:
50-
51-
```bash
52-
terraform plan
53-
```
54-
55-
### 5. Apply the Configuration
56-
57-
Deploy the infrastructure:
58-
59-
```bash
60-
terraform apply
61-
```
62-
63-
Type `yes` when prompted to confirm.
64-
65-
### 6. Get the API Endpoint
66-
67-
After deployment, the API Gateway URL will be displayed:
29+
2. Deploy the Lambda and API Gateway. Review the plan and apply.
6830

6931
```bash
70-
terraform output api_gateway_url
32+
make deploy-terraform # Requires AWS Credentials
7133
```
72-
7334
## Resources Created
7435

75-
- **Lambda Function**: `decrypt-lambda` (or custom name)
76-
- **IAM Role**: `decrypt-lambda-role` with KMS decrypt permissions
77-
- **API Gateway**: REST API with POST /decrypt endpoint
78-
- **CORS Configuration**: Configured for https://app.joinformal.com (or custom origin)
36+
- **Lambda Function**: `decrypt-lambda` (or custom name) deployed in private subnets
37+
- **IAM Role**: `decrypt-lambda-role` with KMS decrypt and VPC access permissions
38+
- **API Gateway**: Private REST API with POST /decrypt endpoint
39+
- **VPC Endpoint**: Interface endpoint for API Gateway execute-api service
40+
- **Security Groups**:
41+
- Lambda security group with egress to all
42+
- API Gateway VPC endpoint security group with ingress on port 443 from VPC CIDR
43+
- **CORS Configuration**: Configured for https://app.joinformal.coms
7944
- **CloudWatch Log Group**: For Lambda function logs
8045

81-
## Updating the Lambda Function
46+
## VPC Configuration
8247

83-
After making changes to your code:
48+
This deployment creates a **private API Gateway** accessible only from within the VPC. The Lambda function runs in private subnets and connects to API Gateway through a VPC endpoint.
8449

85-
1. Rebuild the bootstrap binary
86-
2. Recreate the zip file: `zip bootstrap.zip bootstrap`
87-
3. Run `terraform apply` to update the Lambda function
50+
### VPC Requirements
8851

89-
## Cleanup
52+
- **VPC ID**: An existing VPC where resources will be deployed
53+
- **Private Subnets**: At least 2 private subnets (recommended for high availability)
54+
- Subnets should have routes to a NAT Gateway if the Lambda needs internet access
55+
- Subnets should be in different Availability Zones for resilience
56+
- **VPC Endpoints**: The terraform configuration automatically creates the required API Gateway execute-api endpoint
9057

91-
To destroy all resources:
58+
### Network Architecture
9259

93-
```bash
94-
terraform destroy
9560
```
61+
Client (within VPC) → VPC Endpoint (execute-api) → Private API Gateway → Lambda (in private subnet)
62+
```
63+
64+
The API Gateway is not accessible from the public internet. We recommend requiring access through a VPN so that users can access the API Gateway from their browsers.
9665

9766
## Variables
9867

99-
| Variable | Description | Default |
100-
|----------|-------------|---------|
101-
| `aws_region` | AWS region to deploy resources | `us-east-1` |
102-
| `function_name` | Name of the Lambda function | `decrypt-lambda` |
103-
| `stage_name` | API Gateway stage name | `prod` |
104-
| `kms_key_arn` | ARN for KMS key we're using to decrypt | `` |
105-
| `log_retention_days` | CloudWatch log retention in days | `14` |
68+
| Variable | Description | Default | Required |
69+
|----------|-------------|---------|----------|
70+
| `aws_region` | AWS region to deploy resources | `us-east-1` | No |
71+
| `function_name` | Name of the Lambda function | `decrypt-lambda` | No |
72+
| `stage_name` | API Gateway stage name | `prod` | No |
73+
| `kms_key_arn` | ARN for KMS key we're using to decrypt | - | Yes |
74+
| `vpc_id` | VPC ID where Lambda and API Gateway will be deployed | - | Yes |
75+
| `private_subnet_ids` | List of private subnet IDs (recommend 2+) | - | Yes |
76+
| `log_retention_days` | CloudWatch log retention in days | `14` | No |
10677

10778
## Outputs
10879

@@ -114,3 +85,7 @@ terraform destroy
11485
| `lambda_function_arn` | Lambda function ARN |
11586
| `lambda_role_arn` | Lambda IAM role ARN |
11687
| `cloudwatch_log_group_name` | CloudWatch Log Group name |
88+
| `vpc_endpoint_id` | VPC Endpoint ID for API Gateway |
89+
| `vpc_endpoint_dns_entries` | DNS entries for the VPC endpoint |
90+
| `vpc_endpoint_private_ips` | Private IP addresses of the VPC endpoint|
91+
| `access_instructions` | Instructions for accessing the private API |

terraform/main.tf

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
5353
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
5454
}
5555

56+
resource "aws_iam_role_policy_attachment" "lambda_vpc_execution" {
57+
role = aws_iam_role.decrypt_lambda_role.name
58+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
59+
}
60+
61+
resource "aws_security_group" "lambda_sg" {
62+
name = "${var.function_name}-lambda-sg"
63+
description = "Security group for decrypt Lambda function"
64+
vpc_id = var.vpc_id
65+
66+
egress {
67+
from_port = 0
68+
to_port = 0
69+
protocol = "-1"
70+
cidr_blocks = ["0.0.0.0/0"]
71+
}
72+
73+
tags = {
74+
Name = "${var.function_name}-lambda-sg"
75+
}
76+
}
77+
5678
resource "aws_lambda_function" "decrypt" {
5779
filename = "../bootstrap.zip"
5880
function_name = var.function_name
@@ -65,16 +87,92 @@ resource "aws_lambda_function" "decrypt" {
6587
environment {
6688
variables = {}
6789
}
90+
91+
vpc_config {
92+
subnet_ids = [var.private_subnet_id]
93+
security_group_ids = [aws_security_group.lambda_sg.id]
94+
}
6895
}
6996

7097
resource "aws_cloudwatch_log_group" "decrypt_lambda_logs" {
7198
name = "/aws/lambda/${var.function_name}"
7299
retention_in_days = var.log_retention_days
73100
}
74101

102+
data "aws_vpc" "selected" {
103+
id = var.vpc_id
104+
}
105+
106+
resource "aws_security_group" "apigw_endpoint_sg" {
107+
name = "${var.function_name}-apigw-endpoint-sg"
108+
description = "Security group for API Gateway VPC endpoint"
109+
vpc_id = var.vpc_id
110+
111+
ingress {
112+
description = "HTTPS from VPC CIDR"
113+
from_port = 443
114+
to_port = 443
115+
protocol = "tcp"
116+
cidr_blocks = [data.aws_vpc.selected.cidr_block]
117+
}
118+
119+
egress {
120+
from_port = 0
121+
to_port = 0
122+
protocol = "-1"
123+
cidr_blocks = ["0.0.0.0/0"]
124+
}
125+
126+
tags = {
127+
Name = "${var.function_name}-apigw-endpoint-sg"
128+
}
129+
}
130+
131+
resource "aws_vpc_endpoint" "apigw_endpoint" {
132+
vpc_id = var.vpc_id
133+
service_name = "com.amazonaws.${var.aws_region}.execute-api"
134+
vpc_endpoint_type = "Interface"
135+
private_dns_enabled = true
136+
137+
subnet_ids = [var.private_subnet_id]
138+
security_group_ids = [aws_security_group.apigw_endpoint_sg.id]
139+
140+
tags = {
141+
Name = "${var.function_name}-apigw-endpoint"
142+
}
143+
}
144+
145+
# Data source to get details of VPC endpoint ENIs
146+
data "aws_network_interface" "vpc_endpoint_enis" {
147+
count = length(tolist(aws_vpc_endpoint.apigw_endpoint.network_interface_ids))
148+
id = tolist(aws_vpc_endpoint.apigw_endpoint.network_interface_ids)[count.index]
149+
}
150+
75151
resource "aws_api_gateway_rest_api" "decrypt_api" {
76152
name = "${var.function_name}-api"
77153
description = "API Gateway for decrypt Lambda function"
154+
155+
endpoint_configuration {
156+
types = ["PRIVATE"]
157+
vpc_endpoint_ids = [aws_vpc_endpoint.apigw_endpoint.id]
158+
}
159+
160+
policy = jsonencode({
161+
Version = "2012-10-17"
162+
Statement = [
163+
{
164+
Effect = "Allow"
165+
Principal = "*"
166+
Action = "execute-api:Invoke"
167+
Resource = "*"
168+
Condition = {
169+
StringEquals = {
170+
"aws:SourceVpce" = aws_vpc_endpoint.apigw_endpoint.id
171+
}
172+
}
173+
}
174+
]
175+
})
78176
}
79177

80178
resource "aws_api_gateway_resource" "decrypt_resource" {

terraform/outputs.tf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,39 @@ output "cloudwatch_log_group_name" {
2727
description = "Name of the CloudWatch Log Group"
2828
value = aws_cloudwatch_log_group.decrypt_lambda_logs.name
2929
}
30+
31+
output "vpc_endpoint_id" {
32+
description = "ID of the API Gateway VPC Endpoint"
33+
value = aws_vpc_endpoint.apigw_endpoint.id
34+
}
35+
36+
output "vpc_endpoint_dns_entries" {
37+
description = "DNS entries for the VPC endpoint"
38+
value = aws_vpc_endpoint.apigw_endpoint.dns_entry
39+
}
40+
41+
output "vpc_endpoint_private_ips" {
42+
description = "Private IP addresses of the VPC endpoint ENIs"
43+
value = [for eni in data.aws_network_interface.vpc_endpoint_enis : eni.private_ip]
44+
}
45+
46+
output "access_instructions" {
47+
description = "Instructions for accessing the private API"
48+
value = <<-EOT
49+
This is a PRIVATE API Gateway, accessible only from within the VPC.
50+
51+
API Gateway URL: ${aws_api_gateway_stage.decrypt_stage.invoke_url}/decrypt
52+
API Gateway ID: ${aws_api_gateway_rest_api.decrypt_api.id}
53+
VPC Endpoint ID: ${aws_vpc_endpoint.apigw_endpoint.id}
54+
VPC Endpoint Private IPs: ${join(", ", [for eni in data.aws_network_interface.vpc_endpoint_enis : eni.private_ip])}
55+
56+
=== From within VPC (EC2/ECS) ===
57+
curl -X POST ${aws_api_gateway_stage.decrypt_stage.invoke_url}/decrypt \
58+
-H "Content-Type: application/json" \
59+
-d '{"encrypted_data": "your-encrypted-data"}'
60+
61+
DNS Resolution:
62+
- Private DNS is enabled on the VPC endpoint
63+
- The API Gateway hostname will resolve to private IPs within your VPC
64+
EOT
65+
}

terraform/terraform.tfvars.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@ stage_name = "prod"
77
cors_origin = "https://app.joinformal.com"
88
kms_key_arn = ""
99

10+
# VPC Configuration for private subnet deployment
11+
vpc_id = "vpc-xxxxxxxxx"
12+
private_subnet_id = "subnet-xxxxxxxxx" # This private subnet needs to be part of the VPC.
13+
1014
# Optional: Customize CloudWatch log retention
1115
# log_retention_days = 14

terraform/variables.tf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ variable "kms_key_arn" {
2626
description = "ARN of the KMS key to use for decrypting data"
2727
type = string
2828
}
29+
30+
variable "vpc_id" {
31+
description = "VPC ID where Lambda and API Gateway will be deployed"
32+
type = string
33+
}
34+
35+
variable "private_subnet_id" {
36+
description = "Private subnet ID for Lambda and API Gateway VPC endpoint"
37+
type = string
38+
}

0 commit comments

Comments
 (0)