Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for private REST API Gateways #87

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added support for PRIVATE-type cirrus and stac-server API Gateways

### Changed

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions ci.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ stac_server_inputs = {
app_name = "stac_server"
version = "v3.8.0"
deploy_cloudfront = false
api_rest_type = "EDGE"
web_acl_id = ""
domain_alias = ""
enable_transactions_extension = false
Expand Down Expand Up @@ -151,6 +152,7 @@ cirrus_inputs = {
payload_bucket = "" # If left blank the deployment will create the payload bucket
log_level = "DEBUG"
deploy_alarms = true
api_rest_type = "EDGE"
custom_alarms = {
warning = {}
critical = {}
Expand Down
2 changes: 2 additions & 0 deletions default.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sns_critical_subscriptions_map = {}
stac_server_inputs = {
app_name = "stac_server"
version = "v3.8.0"
api_rest_type = "EDGE"
deploy_cloudfront = true
web_acl_id = ""
domain_alias = ""
Expand Down Expand Up @@ -155,6 +156,7 @@ cirrus_inputs = {
payload_bucket = "" # If left blank the deployment will create the payload bucket
log_level = "DEBUG"
deploy_alarms = true
api_rest_type = "EDGE"
custom_alarms = {
warning = {}
critical = {}
Expand Down
4 changes: 4 additions & 0 deletions inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ variable "stac_server_inputs" {
cors_methods = string
cors_headers = string
authorized_s3_arns = list(string)
api_rest_type = string
auth_function = object({
cf_function_name = string
cf_function_runtime = string
Expand Down Expand Up @@ -141,6 +142,7 @@ variable "stac_server_inputs" {
cors_methods = ""
cors_headers = ""
authorized_s3_arns = []
api_rest_type = "EDGE"
auth_function = {
cf_function_name = ""
cf_function_runtime = "cloudfront-js-2.0"
Expand Down Expand Up @@ -339,6 +341,7 @@ variable "cirrus_inputs" {
data_bucket = string
payload_bucket = string
log_level = string
api_rest_type = string
deploy_alarms = bool
custom_alarms = object({
warning = map(any)
Expand Down Expand Up @@ -567,6 +570,7 @@ variable "cirrus_inputs" {
data_bucket = "cirrus-data-bucket-name"
payload_bucket = "cirrus-payload-bucket-name"
log_level = "INFO"
api_rest_type = "EDGE"
deploy_alarms = true
custom_alarms = {
warning = {}
Expand Down
86 changes: 83 additions & 3 deletions modules/cirrus/builtin-functions/api.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
locals {
is_private_endpoint = var.cirrus_api_rest_type == "PRIVATE" ? true : false
}


resource "aws_iam_role" "cirrus_api_lambda_role" {
name_prefix = "${var.cirrus_prefix}-api-role-"

Expand Down Expand Up @@ -117,17 +122,92 @@ resource "aws_lambda_function" "cirrus_api" {
}
}

resource "aws_security_group" "cirrus_api_gateway_private_vpce" {
count = local.is_private_endpoint ? 1 : 0

name_prefix = "${var.cirrus_prefix}-apigw-vcpe-sg-"
description = "Allows TCP inbound on 443 from VPC private subnet CIDRs"

vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "cirrus_api_gateway_private_vpce" {
for_each = local.is_private_endpoint ? data.aws_subnet.selected : {}

security_group_id = aws_security_group.cirrus_api_gateway_private_vpce[0].id
description = "Allow TCP on 443 for subnet ${each.value.id}"

cidr_ipv4 = each.value.cidr_block
ip_protocol = "tcp"
from_port = 443
to_port = 443
}

resource "aws_vpc_endpoint" "cirrus_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

service_name = "com.amazonaws.${data.aws_region.current.name}.execute-api"
vpc_id = var.vpc_id
vpc_endpoint_type = "Interface"
ip_address_type = "ipv4"
subnet_ids = [for subnet in data.aws_subnet.selected : subnet.id]
security_group_ids = aws_security_group.cirrus_api_gateway_private_vpce[*].id
auto_accept = true
private_dns_enabled = false

dns_options {
dns_record_ip_type = "ipv4"
}
}

resource "aws_api_gateway_rest_api" "cirrus_api_gateway" {
name = "${var.cirrus_prefix}-api"

endpoint_configuration {
types = [var.api_rest_type]
types = [var.cirrus_api_rest_type]
vpc_endpoint_ids = local.is_private_endpoint ? aws_vpc_endpoint.cirrus_api_gateway_private[*].id : null
}
}

lifecycle {
ignore_changes = [policy]
data "aws_iam_policy_document" "cirrus_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

statement {
sid = "DenyApiInvokeForNonVpceTraffic"
effect = "Deny"
actions = ["execute-api:Invoke"]
resources = ["arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.cirrus_api_gateway.id}/*"]

principals {
type = "AWS"
identifiers = ["*"]
}

condition {
variable = "aws:SourceVpce"
test = "StringNotEquals"
values = [aws_vpc_endpoint.cirrus_api_gateway_private[0].id]
}
}

statement {
sid = "AllowApiInvoke"
effect = "Allow"
actions = ["execute-api:Invoke"]
resources = ["arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.cirrus_api_gateway.id}/*"]

principals {
type = "AWS"
identifiers = ["*"]
}
}
}

resource "aws_api_gateway_rest_api_policy" "cirrus_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

rest_api_id = aws_api_gateway_rest_api.cirrus_api_gateway.id
policy = data.aws_iam_policy_document.cirrus_api_gateway_private[0].json
}

resource "aws_api_gateway_method" "cirrus_api_gateway_root_method" {
Expand Down
6 changes: 6 additions & 0 deletions modules/cirrus/builtin-functions/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ data "aws_caller_identity" "current" {

data "aws_region" "current" {
}

data "aws_subnet" "selected" {
for_each = toset(var.vpc_subnet_ids)

id = each.value
}
7 changes: 6 additions & 1 deletion modules/cirrus/builtin-functions/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ variable "cirrus_workflow_event_sns_topic_arn" {
type = string
}

variable "vpc_id" {
description = "FilmDrop VPC ID"
type = string
}

variable "vpc_subnet_ids" {
description = "List of subnet ids in the FilmDrop vpc"
type = list(string)
Expand All @@ -116,7 +121,7 @@ variable "vpc_security_group_ids" {
type = list(string)
}

variable "api_rest_type" {
variable "cirrus_api_rest_type" {
description = "Cirrus API Gateway type"
type = string
default = "EDGE"
Expand Down
2 changes: 2 additions & 0 deletions modules/cirrus/builtin_functions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ moved {
module "builtin_functions" {
source = "./builtin-functions"

vpc_id = var.vpc_id
vpc_subnet_ids = var.vpc_subnet_ids
vpc_security_group_ids = var.vpc_security_group_ids
cirrus_prefix = local.cirrus_prefix
cirrus_log_level = var.cirrus_log_level
cirrus_data_bucket = module.base.cirrus_data_bucket
cirrus_payload_bucket = module.base.cirrus_payload_bucket
cirrus_api_rest_type = var.cirrus_api_rest_type
cirrus_api_lambda_timeout = var.cirrus_api_lambda_timeout
cirrus_api_lambda_memory = var.cirrus_api_lambda_memory
cirrus_process_lambda_timeout = var.cirrus_process_lambda_timeout
Expand Down
11 changes: 11 additions & 0 deletions modules/cirrus/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ variable "cirrus_log_level" {
default = "INFO"
}

variable "cirrus_api_rest_type" {
description = "Cirrus API Gateway type"
type = string
default = "EDGE"
}

variable "cirrus_api_lambda_timeout" {
description = "Cirrus API lambda timeout (sec)"
type = number
Expand Down Expand Up @@ -122,6 +128,11 @@ variable "cirrus_post_batch_lambda_memory" {
default = 128
}

variable "vpc_id" {
description = "FilmDrop VPC ID"
type = string
}

variable "vpc_subnet_ids" {
description = "List of subnet ids in the FilmDrop vpc"
type = list(string)
Expand Down
86 changes: 83 additions & 3 deletions modules/stac-server/api.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
locals {
is_private_endpoint = var.api_rest_type == "PRIVATE" ? true : false
}


resource "aws_lambda_function" "stac_server_api" {
filename = "${path.module}/lambda/api/api.zip"
function_name = "${local.name_prefix}-stac-server-api"
Expand Down Expand Up @@ -54,17 +59,92 @@ resource "aws_lambda_function" "stac_server_api" {
}
}

resource "aws_security_group" "stac_server_api_gateway_private_vpce" {
count = local.is_private_endpoint ? 1 : 0

name_prefix = "${local.name_prefix}-apigw-vcpe-sg-"
description = "Allows TCP inbound on 443 from VPC private subnet CIDRs"

vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "stac_server_api_gateway_private_vcpe" {
for_each = local.is_private_endpoint ? data.aws_subnet.selected : {}

security_group_id = aws_security_group.stac_server_api_gateway_private_vpce[0].id
description = "Allow TCP on 443 for subnet ${each.value.id}"

cidr_ipv4 = each.value.cidr_block
ip_protocol = "tcp"
from_port = 443
to_port = 443
}

resource "aws_vpc_endpoint" "stac_server_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

service_name = "com.amazonaws.${data.aws_region.current.name}.execute-api"
vpc_id = var.vpc_id
vpc_endpoint_type = "Interface"
ip_address_type = "ipv4"
subnet_ids = [for subnet in data.aws_subnet.selected : subnet.id]
security_group_ids = aws_security_group.stac_server_api_gateway_private_vpce[*].id
auto_accept = true
private_dns_enabled = false

dns_options {
dns_record_ip_type = "ipv4"
}
}

resource "aws_api_gateway_rest_api" "stac_server_api_gateway" {
name = "${local.name_prefix}-stac-server"

endpoint_configuration {
types = [var.api_rest_type]
types = [var.api_rest_type]
vpc_endpoint_ids = local.is_private_endpoint ? aws_vpc_endpoint.stac_server_api_gateway_private[*].id : null
}
}

lifecycle {
ignore_changes = [policy]
data "aws_iam_policy_document" "stac_server_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

statement {
sid = "DenyApiInvokeForNonVpceTraffic"
effect = "Deny"
actions = ["execute-api:Invoke"]
resources = ["arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.stac_server_api_gateway.id}/*"]

principals {
type = "AWS"
identifiers = ["*"]
}

condition {
variable = "aws:SourceVpce"
test = "StringNotEquals"
values = [aws_vpc_endpoint.stac_server_api_gateway_private[0].id]
}
}

statement {
sid = "AllowApiInvoke"
effect = "Allow"
actions = ["execute-api:Invoke"]
resources = ["arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.stac_server_api_gateway.id}/*"]

principals {
type = "AWS"
identifiers = ["*"]
}
}
}

resource "aws_api_gateway_rest_api_policy" "stac_server_api_gateway_private" {
count = local.is_private_endpoint ? 1 : 0

rest_api_id = aws_api_gateway_rest_api.stac_server_api_gateway.id
policy = data.aws_iam_policy_document.stac_server_api_gateway_private[0].json
}

resource "aws_api_gateway_method" "stac_server_api_gateway_root_method" {
Expand Down
6 changes: 6 additions & 0 deletions modules/stac-server/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ data "archive_file" "waiting_for_opensearch_lambda_zip" {
]
}

data "aws_subnet" "selected" {
for_each = toset(var.vpc_subnet_ids)

id = each.key
}

# this forces the user_init_lambda_zip to always be built
resource "random_string" "user_init_lambda_zip_poke" {
length = 16
Expand Down
8 changes: 7 additions & 1 deletion modules/stac-server/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ variable "stac_api_stage_description" {
}

variable "stac_api_rootpath" {
description = "This should be set to an empty string when there is a cloudfront distribution in front of stac-server. This should be set to null to default to use the stac_api_stage var as the root path. The cloudfront distros are created via the `cloudfront/apigw_endpoint` module."
description = <<-DESCRIPTION
If stac-server has a cloudfront distribution, this should be an empty string.
If stac-server does not have a cloudfront distribution, the api_rest_type is
PRIVATE, and you're managing a custom API Gateway domain outside of this module,
this should be an empty string.
If neither is true, the stac_api_stage var should be used.
DESCRIPTION
type = string
default = ""
}
Expand Down
Loading
Loading