├── .cfnlintrc ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── doc ├── architectures.drawio ├── privatelink_nlb_alb.png └── privatelink_nlb_apigw.png ├── privatelink_nlb_alb ├── README.md ├── cloudformation │ └── privatelink_nlb_alb.yml └── terraform │ ├── .terraform.lock.hcl │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── provider.tf │ ├── variables.tf │ └── versions.tf ├── privatelink_nlb_apigw ├── README.md ├── cloudformation │ └── privatelink_nlb_apigw.yml └── terraform │ ├── .terraform.lock.hcl │ ├── data.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── provider.tf │ ├── variables.tf │ └── versions.tf ├── requirements-test.txt └── test_template.yml /.cfnlintrc: -------------------------------------------------------------------------------- 1 | templates: 2 | - test_template.yml 3 | - privatelink_nlb_alb/cloudformation/privatelink_nlb_alb.yml 4 | - privatelink_nlb_apigw/cloudformation/privatelink_nlb_apigw.yml 5 | include_checks: 6 | - I 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/sam,terraform 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=sam,terraform 3 | 4 | ### SAM ### 5 | # Ignore build directories for the AWS Serverless Application Model (SAM) 6 | # Info: https://aws.amazon.com/serverless/sam/ 7 | # Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html 8 | 9 | **/.aws-sam 10 | samconfig.toml 11 | 12 | ### Terraform ### 13 | # Local .terraform directories 14 | **/.terraform/* 15 | 16 | # .tfstate files 17 | *.tfstate 18 | *.tfstate.* 19 | 20 | # Crash log files 21 | crash.log 22 | crash.*.log 23 | 24 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 25 | # password, private keys, and other secrets. These should not be part of version 26 | # control as they are data points which are potentially sensitive and subject 27 | # to change depending on the environment. 28 | *.tfvars 29 | *.tfvars.json 30 | 31 | # Ignore override files as they are usually used to override resources locally and so 32 | # are not checked in 33 | override.tf 34 | override.tf.json 35 | *_override.tf 36 | *_override.tf.json 37 | 38 | # Include override files you do wish to add to version control using negated pattern 39 | # !example_override.tf 40 | 41 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 42 | # example: *tfplan* 43 | 44 | # Ignore CLI configuration files 45 | .terraformrc 46 | terraform.rc 47 | 48 | # End of https://www.toptal.com/developers/gitignore/api/sam,terraform 49 | 50 | *.bkp 51 | *.dtmp 52 | .venv -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup build test deploy clean 2 | 3 | setup: 4 | python3 -m venv .venv 5 | .venv/bin/python3 -m pip install -U pip 6 | .venv/bin/python3 -m pip install -r requirements-test.txt 7 | 8 | build: 9 | sam build --use-container --parallel --cached 10 | 11 | deploy: 12 | sam deploy 13 | 14 | test: 15 | .venv/bin/cfn-lint 16 | .venv/bin/checkov -d privatelink_nlb_alb/terraform 17 | .venv/bin/checkov -d privatelink_nlb_apigw/terraform 18 | 19 | clean: 20 | sam delete 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS PrivateLink Reference Architectures 2 | 3 | ### Table of contents 4 | 5 | - [AWS PrivateLink Reference Architectures](#aws-privatelink-reference-architectures) 6 | - [Table of contents](#table-of-contents) 7 | - [Introduction](#introduction) 8 | - [What are the benefits of using a VPC endpoint with AWS PrivateLink?](#what-are-the-benefits-of-using-a-vpc-endpoint-with-aws-privatelink) 9 | - [Architectures](#architectures) 10 | - [Network Load Balancer -\> Application Load Balancer](#network-load-balancer---application-load-balancer) 11 | - [Network Load Balancer -\> Private API Gateway](#network-load-balancer---private-api-gateway) 12 | - [Prerequisites](#prerequisites) 13 | - [Tools and services](#tools-and-services) 14 | - [Usage](#usage) 15 | - [Parameters](#parameters) 16 | - [Installation](#installation) 17 | - [Clean up](#clean-up) 18 | - [Reference](#reference) 19 | - [Contributing](#contributing) 20 | - [License](#license) 21 | 22 | ## Introduction 23 | 24 | This project provides reference implementations for [AWS PrivateLink](https://aws.amazon.com/privatelink/) with an [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/) (ALB) or an [Amazon API Gateway](https://aws.amazon.com/api-gateway/). Implementations are provided in both [AWS CloudFormation](https://aws.amazon.com/cloudformation/) and [Hashicorp Terraform](https://www.terraform.io/) templates. 25 | 26 | #### What are the benefits of using a VPC endpoint with AWS PrivateLink? 27 | 28 | VPC endpoints provide secure access to a specific service, with several benefits to the end user: 29 | 30 | - VPC endpoints provide access to a specific service without the need of using any other gateways – no need to use an Internet gateway, a NAT gateway, a VPN connection, or a VPC peering connection, reducing the risks of exposing your resources to the Internet or to other outside networks. 31 | - Your traffic remains within Amazon's private network, reducing the risks of exposing your traffic to the Internet. 32 | - When accessing Amazon services over VPC endpoints, you can restrict the access through a VPC endpoint to specific users, actions, and/or resources. 33 | - You can limit access to resources provided by an Amazon service to traffic originating from a specific VPC or through a specific VPC endpoint. 34 | 35 | ## Architectures 36 | 37 | ### Network Load Balancer -> Application Load Balancer 38 | 39 | ![PrivateLink + NLB -> ALB](doc/privatelink_nlb_alb.png) 40 | 41 | In this architecture, the provider has an existing Internet-facing ALB servicing existing consumers. The templates in this reposititory deploy a [Network Load Balancer](https://aws.amazon.com/elasticloadbalancing/network-load-balancer/) into private subnets within the same VPC and availabiilty zones (AZs) as the ALB. An NLB [target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) is created with the ALB as a single target. An AWS PrivateLink [endpoint service](https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html) is created targeting the NLB. 42 | 43 | ### Network Load Balancer -> Private API Gateway 44 | 45 | ![PrivateLink + NLB -> API Gateway](doc/privatelink_nlb_apigw.png) 46 | 47 | In this architecture, the provider may have an existing edge or regional API Gateway or an existing [private API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-apis.html) servicing existing consumers. A private API is required to provide connectivity through AWS PrivateLink. The templates in this repository deploy a [Network Load Balancer](https://aws.amazon.com/elasticloadbalancing/network-load-balancer/) into private subnets within the same VPC and availabiity zones (AZs) as the [interface VPC endpoint for API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-apis.html#apigateway-private-api-create-interface-vpc-endpoint). An NLB [target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) is created with VPC endpoint elastic network interfaces (ENIs) as the TLS targets. [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/) is used to create a TLS certificate that is associated to both the NLB and API Gateway. 48 | 49 | ## Prerequisites 50 | 51 | - [Python 3](https://www.python.org/downloads/), installed 52 | - [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) version 2, installed. Please follow these instructions with how to [setup your AWS credentials](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started-set-up-credentials.html). 53 | - [AWS Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started.html), installed 54 | - [Docker Desktop](https://www.docker.com/products/docker-desktop), installed 55 | - [GitHub](https://github.com) account 56 | 57 | ## Tools and services 58 | 59 | - [Amazon API Gateway](https://aws.amazon.com/api-gateway/) - Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. 60 | - [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/) - Provision and manage SSL/TLS certificates with AWS services and connected resources. 61 | - [AWS CloudFormation](https://aws.amazon.com/cloudformation/) - AWS CloudFormation lets you model, provision, and manage AWS and third-party resources by treating infrastructure as code. 62 | - [AWS PrivateLink](https://aws.amazon.com/privatelink/) - Establish connectivity between VPCs and AWS services without exposing data to the internet 63 | - [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/) - Application Load Balancer operates at the request level (layer 7), routing traffic to targets (EC2 instances, containers, IP addresses, and Lambda functions) based on the content of the request. 64 | - [Network Load Balancer](https://aws.amazon.com/elasticloadbalancing/network-load-balancer/) - Network Load Balancer operates at the connection level (Layer 4), routing connections to targets (Amazon EC2 instances, microservices, and containers) within Amazon VPC, based on IP protocol data. 65 | 66 | ## Usage 67 | 68 | #### Parameters 69 | 70 | | Parameter | Type | Default | Description | 71 | | ---------------- | :--------------------------: | :-----: | ------------------ | 72 | | VpcId | AWS::EC2::VPC::Id | - | VPC ID | 73 | | PublicSubnetIds | List\ | - | Public Subnet IDs | 74 | | PrivateSubnetIds | List\ | - | Private Subnet IDs | 75 | | HostedZoneId | AWS::Route53::HostedZone::Id | - | Hosted Zone ID | 76 | | DomainName | String | - | Domain Name | 77 | 78 | #### Installation 79 | 80 | If you'd like to deploy a test application that you can then deploy the reference architectures on top of, you can follow the instructions below: 81 | 82 | ``` 83 | git clone https://github.com/aws-samples/aws-privatelink-reference-architectures 84 | cd aws-privatelink-reference-architectures 85 | sam build --use-container --parallel --cached 86 | sam deploy --guided 87 | ``` 88 | 89 | ## Clean up 90 | 91 | Deleting the CloudFormation Stack will remove the Lambda functions, Application Load Balancer, CloudWatch Log Groups, and Amazon API Gateway. 92 | 93 | ``` 94 | sam delete 95 | ``` 96 | 97 | ## Reference 98 | 99 | ## Contributing 100 | 101 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 102 | 103 | ## License 104 | 105 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 106 | -------------------------------------------------------------------------------- /doc/architectures.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /doc/privatelink_nlb_alb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/private-saas-with-aws-privatelink/4115bc0d2382d18ebb26811da5e7300902cf0773/doc/privatelink_nlb_alb.png -------------------------------------------------------------------------------- /doc/privatelink_nlb_apigw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/private-saas-with-aws-privatelink/4115bc0d2382d18ebb26811da5e7300902cf0773/doc/privatelink_nlb_apigw.png -------------------------------------------------------------------------------- /privatelink_nlb_alb/README.md: -------------------------------------------------------------------------------- 1 | # AWS PrivateLink Reference Architectures 2 | 3 | ## Network Load Balancer -> Application Load Balancer 4 | 5 | ![PrivateLink + NLB -> ALB](../doc/privatelink_nlb_alb.png) 6 | 7 | In this architecture, the provider has an existing Internet-facing ALB servicing existing consumers. The templates in this folder deploy a [Network Load Balancer](https://aws.amazon.com/elasticloadbalancing/network-load-balancer/) into private subnets within the same VPC and availabiilty zones (AZs) as the ALB. An NLB [target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) is created with the ALB as a single target. An AWS PrivateLink [endpoint service](https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html) is created targeting the NLB. -------------------------------------------------------------------------------- /privatelink_nlb_alb/cloudformation/privatelink_nlb_alb.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | Description: Provider NLB->ALB Template 7 | 8 | Parameters: 9 | VpcId: 10 | Type: "AWS::EC2::VPC::Id" 11 | Description: VPC hosting the ALB 12 | SubnetIds: 13 | Type: "List" 14 | Description: Private Subnet Ids for NLB. These should be the same AZs the ALB is using. 15 | ApplicationLoadBalancerArn: 16 | Type: String 17 | Description: ALB ARN 18 | HostedZoneId: 19 | Type: "AWS::Route53::HostedZone::Id" 20 | Description: Hosted Zone ID 21 | DomainName: 22 | Type: String 23 | Description: Domain Name 24 | 25 | Resources: 26 | LoadBalancerSecurityGroup: 27 | Type: "AWS::EC2::SecurityGroup" 28 | Metadata: 29 | cfn_nag: 30 | rules_to_suppress: 31 | - id: W2 32 | reason: "Ignoring world access, permissible on ELB" 33 | - id: W5 34 | reason: "Ignoring egress all access" 35 | - id: W9 36 | reason: "Ignoring ingress not /32" 37 | - id: W40 38 | reason: "Ignoring all protocols" 39 | Properties: 40 | GroupDescription: Allow TLS inbound traffic 41 | SecurityGroupEgress: 42 | - CidrIp: "0.0.0.0/0" 43 | Description: Allow all traffic to anywhere 44 | IpProtocol: "-1" 45 | SecurityGroupIngress: 46 | - CidrIp: "0.0.0.0/0" 47 | Description: Allow 443/tcp from anywhere 48 | FromPort: 443 49 | IpProtocol: tcp 50 | ToPort: 443 51 | VpcId: !Ref VpcId 52 | 53 | LoadBalancer: 54 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 55 | DeletionPolicy: Retain 56 | UpdateReplacePolicy: Retain 57 | Metadata: 58 | cfn_nag: 59 | rules_to_suppress: 60 | - id: W52 61 | reason: "Ignoring access logging" 62 | Properties: 63 | IpAddressType: ipv4 64 | LoadBalancerAttributes: 65 | - Key: deletion_protection.enabled 66 | Value: "true" 67 | SecurityGroups: 68 | - !Ref LoadBalancerSecurityGroup 69 | Scheme: internal 70 | Subnets: !Ref SubnetIds 71 | Type: network 72 | 73 | TargetGroup: 74 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 75 | Properties: 76 | HealthCheckProtocol: HTTPS 77 | IpAddressType: ipv4 78 | Port: 443 79 | Protocol: TCP 80 | Targets: 81 | - Id: !Ref ApplicationLoadBalancerArn 82 | Port: 443 83 | TargetType: alb 84 | VpcId: !Ref VpcId 85 | 86 | Listener: 87 | Type: "AWS::ElasticLoadBalancingV2::Listener" 88 | Properties: 89 | DefaultActions: 90 | - Type: forward 91 | TargetGroupArn: !Ref TargetGroup 92 | LoadBalancerArn: !Ref LoadBalancer 93 | Port: 443 94 | Protocol: TCP 95 | 96 | NotificationTopic: 97 | Type: "AWS::SNS::Topic" 98 | Properties: 99 | DisplayName: VPC Endpoint Notifications 100 | KmsMasterKeyId: "alias/aws/sns" 101 | 102 | NotificationTopicPolicy: 103 | Type: "AWS::SNS::TopicPolicy" 104 | Properties: 105 | PolicyDocument: 106 | Id: VPCE 107 | Version: "2012-10-17" 108 | Statement: 109 | - Effect: Allow 110 | Principal: 111 | Service: !Sub "vpce.${AWS::URLSuffix}" 112 | Action: "sns:Publish" 113 | Resource: !Ref NotificationTopic 114 | Condition: 115 | ArnLike: 116 | "aws:SourceArn": !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc-endpoint-service/${VPCEndpointService}" 117 | StringEquals: 118 | "aws:SourceAccount": !Ref "AWS::AccountId" 119 | Topics: 120 | - !Ref NotificationTopic 121 | 122 | VPCEndpointService: 123 | Type: "AWS::EC2::VPCEndpointService" 124 | Properties: 125 | AcceptanceRequired: false 126 | ContributorInsightsEnabled: false 127 | NetworkLoadBalancerArns: 128 | - !Ref LoadBalancer 129 | 130 | VPCEndpointConnectionNotification: 131 | Type: "AWS::EC2::VPCEndpointConnectionNotification" 132 | Properties: 133 | ConnectionEvents: 134 | - Accept 135 | - Connect 136 | - Delete 137 | - Reject 138 | ConnectionNotificationArn: !Ref NotificationTopic 139 | ServiceId: !Ref VPCEndpointService 140 | 141 | PrivateDnsFunctionLogGroup: 142 | Type: "AWS::Logs::LogGroup" 143 | UpdateReplacePolicy: Delete 144 | DeletionPolicy: Delete 145 | Metadata: 146 | cfn_nag: 147 | rules_to_suppress: 148 | - id: W84 149 | reason: "Ignoring KMS key" 150 | Properties: 151 | LogGroupName: !Sub "/aws/lambda/${PrivateDnsFunction}" 152 | RetentionInDays: 3 153 | Tags: 154 | - Key: "aws-cloudformation:stack-name" 155 | Value: !Ref "AWS::StackName" 156 | - Key: "aws-cloudformation:stack-id" 157 | Value: !Ref "AWS::StackId" 158 | - Key: "aws-cloudformation:logical-id" 159 | Value: PrivateDnsFunctionLogGroup 160 | 161 | PrivateDnsFunctionRole: 162 | Type: "AWS::IAM::Role" 163 | Metadata: 164 | cfn_nag: 165 | rules_to_suppress: 166 | - id: W11 167 | reason: "Ignoring wildcard resource" 168 | Properties: 169 | AssumeRolePolicyDocument: 170 | Version: "2012-10-17" 171 | Statement: 172 | Effect: Allow 173 | Principal: 174 | Service: !Sub "lambda.${AWS::URLSuffix}" 175 | Action: "sts:AssumeRole" 176 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 177 | Policies: 178 | - PolicyName: RegisterTargetPolicy 179 | PolicyDocument: 180 | Version: "2012-10-17" 181 | Statement: 182 | - Effect: Allow 183 | Action: "ec2:ModifyVpcEndpointServiceConfiguration" 184 | Resource: !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc-endpoint-service/${VPCEndpointService}" 185 | - Effect: Allow 186 | Action: "ec2:DescribeVpcEndpointServiceConfigurations" 187 | Resource: "*" 188 | Tags: 189 | - Key: "aws-cloudformation:stack-name" 190 | Value: !Ref "AWS::StackName" 191 | - Key: "aws-cloudformation:stack-id" 192 | Value: !Ref "AWS::StackId" 193 | - Key: "aws-cloudformation:logical-id" 194 | Value: PrivateDnsFunctionRole 195 | 196 | CloudWatchLogsPolicy: 197 | Type: "AWS::IAM::Policy" 198 | Properties: 199 | PolicyName: CloudWatchLogs 200 | PolicyDocument: 201 | Version: "2012-10-17" 202 | Statement: 203 | - Effect: Allow 204 | Action: 205 | - "logs:CreateLogStream" 206 | - "logs:PutLogEvents" 207 | Resource: !GetAtt PrivateDnsFunctionLogGroup.Arn 208 | Roles: 209 | - !Ref PrivateDnsFunctionRole 210 | 211 | PrivateDnsFunction: 212 | Type: "AWS::Lambda::Function" 213 | Metadata: 214 | cfn_nag: 215 | rules_to_suppress: 216 | - id: W58 217 | reason: "Ignoring CloudWatch" 218 | - id: W89 219 | reason: "Ignoring VPC" 220 | - id: W92 221 | reason: "Ignoring Reserved Concurrency" 222 | Properties: 223 | Architectures: 224 | - arm64 225 | Description: VPC Endpoint Private DNS Function 226 | Handler: index.lambda_handler 227 | Code: 228 | ZipFile: |- 229 | import logging 230 | 231 | import boto3 232 | import cfnresponse 233 | 234 | logger = logging.getLogger() 235 | logger.setLevel(logging.INFO) 236 | 237 | client = boto3.client("ec2") 238 | 239 | def lambda_handler(event, context): 240 | response_data = {} 241 | status = cfnresponse.SUCCESS 242 | 243 | service_id = event.get("ResourceProperties", {}).get("ServiceId") 244 | domain_name = event.get("ResourceProperties", {}).get("DomainName") 245 | 246 | try: 247 | if event["RequestType"] in ("Create", "Update"): 248 | client.modify_vpc_endpoint_service_configuration(ServiceId=service_id, PrivateDnsName=domain_name) 249 | paginator = client.get_paginator("describe_vpc_endpoint_service_configurations") 250 | page_iterator = paginator.paginate(ServiceIds=[service_id]) 251 | for page in page_iterator: 252 | for service in page.get("ServiceConfigurations", []): 253 | response_data = service.get("PrivateDnsNameConfiguration", {}) 254 | elif event["RequestType"] == "Delete": 255 | client.modify_vpc_endpoint_service_configuration(ServiceId=service_id, RemovePrivateDnsName=True) 256 | except Exception as error: 257 | logger.exception(error) 258 | response_data["Message"] = str(error) 259 | status = cfnresponse.FAILED 260 | else: 261 | response_data["Message"] = "success" 262 | status = cfnresponse.SUCCESS 263 | finally: 264 | cfnresponse.send(event, context, status, response_data) 265 | MemorySize: 128 # megabytes 266 | Role: !GetAtt PrivateDnsFunctionRole.Arn 267 | Runtime: python3.11 268 | Timeout: 10 # seconds 269 | 270 | PrivateDns: 271 | DependsOn: CloudWatchLogsPolicy 272 | Type: "Custom::PrivateDns" 273 | Properties: 274 | ServiceToken: !GetAtt PrivateDnsFunction.Arn 275 | ServiceId: !Ref VPCEndpointService 276 | DomainName: !Ref DomainName 277 | 278 | RecordSet: 279 | Type: "AWS::Route53::RecordSet" 280 | Properties: 281 | HostedZoneId: !Ref HostedZoneId 282 | Name: !Sub "${PrivateDns.Name}.${DomainName}" 283 | ResourceRecords: 284 | - !Sub '"${PrivateDns.Value}"' 285 | TTL: "1800" 286 | Type: !GetAtt PrivateDns.Type 287 | 288 | Outputs: 289 | VpcEndpointServiceId: 290 | Description: VPC Endpoint Service ID 291 | Value: !Ref VPCEndpointService 292 | VpcEndpointServiceName: 293 | Description: VPC Endpoint Service Name 294 | Value: !Sub "com.amazonaws.vpce.${AWS::Region}.${VPCEndpointService}" 295 | PrivateDnsName: 296 | Description: Private DNS Name 297 | Value: !Ref DomainName -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.1.0" 6 | constraints = "5.1.0" 7 | hashes = [ 8 | "h1:Cc/DLDIjTkNGWFWkOGVeVdJjxC/fe82NTxTsUHZ5HL0=", 9 | "zh:0c48f157b804c1f392adb5c14b81e756c652755e358096300ea8dd1283021129", 10 | "zh:1a50495a6c0e5665e51df57dac6e781ec71439b11ebf05f971b6f3a3eb4eb7b2", 11 | "zh:2959ff472c05e56d59e012118dd8d55022f005534c0ae961ce81136de9f66a4d", 12 | "zh:2dfda9133581b99ed6e709e89a453fd2974ce88c703d3e073ec31bf99d7508ce", 13 | "zh:2f3d92cc7a6624da42cee2202f8fb23e6d38f156ab7851884d637282cb0dc709", 14 | "zh:3bc2a34d09cbaf439a1815846904f070c782cd8dfd60b5e0116827cda25f7549", 15 | "zh:4ef43f1a247aa8de8690ac3bbc2b00ebaf6b2872fc8d0f5130e4a8130c874b87", 16 | "zh:5477cb272dcaeb0030091bcf23a9f0f33b5410e44e317e9d3d49446f545dbaa4", 17 | "zh:734c8fb4c0b79c82dd757566761dda5b91ee1ef9a2b848a748ade11e0e1cc69f", 18 | "zh:80346c051b677f4f018da7fe06318b87c5bd0f1ec67ce78ab33baed3bb8b031a", 19 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 20 | "zh:a865b2f88dfee13df14116c5cf53d033d2c15855f4b59b9c65337309a928df2c", 21 | "zh:c0345f266eedaece5612c1000722b302f895d1bc5af1d5a4265f0e7000ca48bb", 22 | "zh:d59703c8e6a9d8b4fbd3b4583b945dfff9cb2844c762c0b3990e1cef18282279", 23 | "zh:d8d04a6a6cd2dfcb23b57e551db7b15e647f6166310fb7d883d8ec67bdc9bdc8", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_iam_policy_document" "sns_topic_policy" { 4 | statement { 5 | principals { 6 | type = "Service" 7 | identifiers = ["vpce.amazonaws.com"] 8 | } 9 | actions = ["sns:Publish"] 10 | resources = [aws_sns_topic.this.arn] 11 | effect = "Allow" 12 | 13 | condition { 14 | test = "ArnLike" 15 | variable = "aws:SourceArn" 16 | values = [aws_vpc_endpoint_service.this.arn] 17 | } 18 | 19 | condition { 20 | test = "StringEquals" 21 | variable = "aws:SourceAccount" 22 | values = [local.aws_account_id] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | tags = { 3 | Terraform = "true" 4 | } 5 | 6 | aws_account_id = data.aws_caller_identity.current.account_id 7 | } -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "allow_tls" { 2 | name = "allow_tls" 3 | description = "Allow TLS inbound traffic" 4 | vpc_id = var.vpc_id 5 | 6 | ingress { 7 | description = "Allow 443/tcp from anywhere" 8 | from_port = 443 9 | to_port = 443 10 | protocol = "tcp" 11 | cidr_blocks = ["0.0.0.0/0"] 12 | ipv6_cidr_blocks = ["::/0"] 13 | } 14 | 15 | egress { 16 | description = "Allow all traffic to anywhere" 17 | from_port = 0 18 | to_port = 0 19 | protocol = "-1" 20 | cidr_blocks = ["0.0.0.0/0"] 21 | ipv6_cidr_blocks = ["::/0"] 22 | } 23 | } 24 | 25 | resource "aws_lb" "this" { 26 | #checkov:skip=CKV_AWS_152:Cross-zone load balancing is disabled 27 | #checkov:skip=CKV_AWS_91:Disabled access logging 28 | 29 | load_balancer_type = "network" 30 | internal = true 31 | ip_address_type = "ipv4" 32 | 33 | enable_deletion_protection = true 34 | 35 | security_groups = [aws_security_group.allow_tls.id] 36 | subnets = var.subnet_ids 37 | } 38 | 39 | resource "aws_lb_target_group" "this" { 40 | target_type = "alb" 41 | port = 443 42 | protocol = "TCP" 43 | vpc_id = var.vpc_id 44 | 45 | health_check { 46 | protocol = "HTTPS" 47 | } 48 | } 49 | 50 | resource "aws_lb_target_group_attachment" "this" { 51 | target_group_arn = aws_lb_target_group.this.arn 52 | target_id = var.alb_arn 53 | } 54 | 55 | resource "aws_lb_listener" "this" { 56 | load_balancer_arn = aws_lb.this.arn 57 | port = 443 58 | protocol = "TCP" 59 | 60 | default_action { 61 | type = "forward" 62 | target_group_arn = aws_lb_target_group.this.arn 63 | } 64 | } 65 | 66 | resource "aws_sns_topic" "this" { 67 | display_name = "VPC Endpoint Notifications" 68 | kms_master_key_id = "alias/aws/sns" 69 | } 70 | 71 | resource "aws_sns_topic_policy" "this" { 72 | arn = aws_sns_topic.this.arn 73 | policy = data.aws_iam_policy_document.sns_topic_policy.json 74 | } 75 | 76 | resource "aws_vpc_endpoint_service" "this" { 77 | #checkov:skip=CKV_AWS_123:Disabled manual acceptance 78 | 79 | acceptance_required = false 80 | network_load_balancer_arns = [aws_lb.this.arn] 81 | private_dns_name = var.domain_name 82 | supported_ip_address_types = ["ipv4"] 83 | } 84 | 85 | resource "aws_vpc_endpoint_connection_notification" "this" { 86 | vpc_endpoint_service_id = aws_vpc_endpoint_service.this.id 87 | connection_notification_arn = aws_sns_topic.this.arn 88 | connection_events = ["Accept", "Reject", "Connect", "Delete"] 89 | } 90 | 91 | resource "aws_route53_record" "this" { 92 | zone_id = var.zone_id 93 | name = "${aws_vpc_endpoint_service.this.private_dns_name_configuration[0].name}.${var.domain_name}" 94 | records = [aws_vpc_endpoint_service.this.private_dns_name_configuration[0].value] 95 | type = aws_vpc_endpoint_service.this.private_dns_name_configuration[0].type 96 | ttl = 1800 97 | } -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_endpoint_service_id" { 2 | value = aws_vpc_endpoint_service.this.id 3 | } 4 | 5 | output "vpc_endpoint_service_name" { 6 | value = aws_vpc_endpoint_service.this.service_name 7 | } 8 | 9 | output "private_dns_name" { 10 | value = var.domain_name 11 | } -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | default_tags { 4 | tags = local.tags 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | description = "VPC hosting the ALB" 4 | } 5 | 6 | variable "subnet_ids" { 7 | type = list(string) 8 | description = "Private Subnet Ids for NLB. These should be the same AZs the ALB is using." 9 | } 10 | 11 | variable "alb_arn" { 12 | type = string 13 | description = "Application Load Balancer ARN" 14 | } 15 | 16 | variable "zone_id" { 17 | type = string 18 | description = "Hosted Zone ID" 19 | } 20 | 21 | variable "domain_name" { 22 | type = string 23 | description = "Domain Name" 24 | } -------------------------------------------------------------------------------- /privatelink_nlb_alb/terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.6" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "5.1.0" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/README.md: -------------------------------------------------------------------------------- 1 | # AWS PrivateLink Reference Architectures 2 | 3 | ## Network Load Balancer -> Private API Gateway 4 | 5 | ![PrivateLink + NLB -> API Gateway](../doc/privatelink_nlb_apigw.png) 6 | 7 | In this architecture, the provider may have an existing edge or regional API Gateway or an existing [private API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-apis.html) servicing existing consumers. A private API is required to provide connectivity through AWS PrivateLink. The templates in this folder deploy a [Network Load Balancer](https://aws.amazon.com/elasticloadbalancing/network-load-balancer/) into private subnets within the same VPC and availabiity zones (AZs) as the [interface VPC endpoint for API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-apis.html#apigateway-private-api-create-interface-vpc-endpoint). An NLB [target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) is created with VPC endpoint elastic network interfaces (ENIs) as the TLS targets. [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/) is used to create a TLS certificate that is associated to both the NLB and API Gateway. -------------------------------------------------------------------------------- /privatelink_nlb_apigw/cloudformation/privatelink_nlb_apigw.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | Description: Provider NLB->APIGW Template 7 | 8 | Parameters: 9 | VpcId: 10 | Type: "AWS::EC2::VPC::Id" 11 | Description: VPC hosting the ALB 12 | SubnetIds: 13 | Type: "List" 14 | Description: Private Subnet Ids for NLB. These should be the same AZs the private API Gateway is using. 15 | VpcEndpointId: 16 | Type: String 17 | Description: VPC Endpoint ID for the private API Gateway 18 | HostedZoneId: 19 | Type: "AWS::Route53::HostedZone::Id" 20 | Description: Hosted Zone ID 21 | DomainName: 22 | Type: String 23 | Description: Domain Name 24 | 25 | Resources: 26 | LoadBalancerSecurityGroup: 27 | Type: "AWS::EC2::SecurityGroup" 28 | Metadata: 29 | cfn_nag: 30 | rules_to_suppress: 31 | - id: W2 32 | reason: "Ignoring world access, permissible on ELB" 33 | - id: W5 34 | reason: "Ignoring egress all access" 35 | - id: W9 36 | reason: "Ignoring ingress not /32" 37 | - id: W40 38 | reason: "Ignoring all protocols" 39 | Properties: 40 | GroupDescription: Allow TLS inbound traffic 41 | SecurityGroupEgress: 42 | - CidrIp: "0.0.0.0/0" 43 | Description: Allow all traffic to anywhere 44 | IpProtocol: "-1" 45 | SecurityGroupIngress: 46 | - CidrIp: "0.0.0.0/0" 47 | Description: Allow 443/tcp from anywhere 48 | FromPort: 443 49 | IpProtocol: tcp 50 | ToPort: 443 51 | VpcId: !Ref VpcId 52 | 53 | LoadBalancer: 54 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 55 | DeletionPolicy: Retain 56 | UpdateReplacePolicy: Retain 57 | Metadata: 58 | cfn_nag: 59 | rules_to_suppress: 60 | - id: W52 61 | reason: "Ignoring access logging" 62 | Properties: 63 | IpAddressType: ipv4 64 | LoadBalancerAttributes: 65 | - Key: deletion_protection.enabled 66 | Value: "true" 67 | SecurityGroups: 68 | - !Ref LoadBalancerSecurityGroup 69 | Scheme: internal 70 | Subnets: !Ref SubnetIds 71 | Type: network 72 | 73 | TargetGroup: 74 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 75 | Properties: 76 | IpAddressType: ipv4 77 | Port: 443 78 | Protocol: TLS 79 | TargetGroupAttributes: 80 | - Key: deregistration_delay.timeout_seconds 81 | Value: "0" 82 | - Key: deregistration_delay.connection_termination.enabled 83 | Value: "true" 84 | - Key: preserve_client_ip.enabled 85 | Value: "true" 86 | TargetType: ip 87 | VpcId: !Ref VpcId 88 | 89 | TargetFunctionLogGroup: 90 | Type: "AWS::Logs::LogGroup" 91 | UpdateReplacePolicy: Delete 92 | DeletionPolicy: Delete 93 | Metadata: 94 | cfn_nag: 95 | rules_to_suppress: 96 | - id: W84 97 | reason: "Ignoring KMS key" 98 | Properties: 99 | LogGroupName: !Sub "/aws/lambda/${TargetFunction}" 100 | RetentionInDays: 3 101 | Tags: 102 | - Key: "aws-cloudformation:stack-name" 103 | Value: !Ref "AWS::StackName" 104 | - Key: "aws-cloudformation:stack-id" 105 | Value: !Ref "AWS::StackId" 106 | - Key: "aws-cloudformation:logical-id" 107 | Value: TargetFunctionLogGroup 108 | 109 | TargetFunctionRole: 110 | Type: "AWS::IAM::Role" 111 | Metadata: 112 | cfn_nag: 113 | rules_to_suppress: 114 | - id: W11 115 | reason: "Ignoring wildcard resource" 116 | Properties: 117 | AssumeRolePolicyDocument: 118 | Version: "2012-10-17" 119 | Statement: 120 | Effect: Allow 121 | Principal: 122 | Service: !Sub "lambda.${AWS::URLSuffix}" 123 | Action: "sts:AssumeRole" 124 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 125 | Policies: 126 | - PolicyName: RegisterTargetPolicy 127 | PolicyDocument: 128 | Version: "2012-10-17" 129 | Statement: 130 | - Effect: Allow 131 | Action: 132 | - "elasticloadbalancing:RegisterTargets" 133 | - "elasticloadbalancing:DeregisterTargets" 134 | Resource: !Ref TargetGroup 135 | - Effect: Allow 136 | Action: "ec2:DescribeVpcEndpoints" 137 | Resource: "*" 138 | Tags: 139 | - Key: "aws-cloudformation:stack-name" 140 | Value: !Ref "AWS::StackName" 141 | - Key: "aws-cloudformation:stack-id" 142 | Value: !Ref "AWS::StackId" 143 | - Key: "aws-cloudformation:logical-id" 144 | Value: TargetFunctionRole 145 | 146 | TargetCloudWatchLogsPolicy: 147 | Type: "AWS::IAM::Policy" 148 | Properties: 149 | PolicyName: CloudWatchLogs 150 | PolicyDocument: 151 | Version: "2012-10-17" 152 | Statement: 153 | - Effect: Allow 154 | Action: 155 | - "logs:CreateLogStream" 156 | - "logs:PutLogEvents" 157 | Resource: !GetAtt TargetFunctionLogGroup.Arn 158 | Roles: 159 | - !Ref TargetFunctionRole 160 | 161 | TargetFunction: 162 | Type: "AWS::Lambda::Function" 163 | Metadata: 164 | cfn_nag: 165 | rules_to_suppress: 166 | - id: W58 167 | reason: "Ignoring CloudWatch" 168 | - id: W89 169 | reason: "Ignoring VPC" 170 | - id: W92 171 | reason: "Ignoring Reserved Concurrency" 172 | Properties: 173 | Architectures: 174 | - arm64 175 | Description: APIGW Target Registration Function 176 | Handler: index.lambda_handler 177 | Code: 178 | ZipFile: |- 179 | import logging 180 | import socket 181 | 182 | import boto3 183 | import cfnresponse 184 | 185 | logger = logging.getLogger() 186 | logger.setLevel(logging.INFO) 187 | 188 | ec2 = boto3.client("ec2") 189 | client = boto3.client("elbv2") 190 | 191 | def get_dns_entry(vpc_endpoint_id: str) -> str: 192 | response = ec2.describe_vpc_endpoints(VpcEndpointIds=[vpc_endpoint_id]) 193 | for vpc_endpoint in response.get("VpcEndpoints", []): 194 | for dns_entry in vpc_endpoint.get("DnsEntries", []): 195 | return dns_entry["DnsName"] 196 | 197 | def lambda_handler(event, context): 198 | response_data = {} 199 | status = cfnresponse.SUCCESS 200 | 201 | target_group_arn = event.get("ResourceProperties", {}).get("TargetGroupArn") 202 | vpc_endpoint_id = event.get("ResourceProperties", {}).get("VpcEndpointId") 203 | 204 | try: 205 | dns_name = get_dns_entry(vpc_endpoint_id) 206 | ip_addresses = socket.gethostbyname_ex(dns_name)[2] 207 | targets = [{"Id": ip_addr} for ip_addr in ip_addresses] 208 | 209 | if event["RequestType"] in ("Create", "Update"): 210 | client.register_targets(TargetGroupArn=target_group_arn, Targets=targets) 211 | elif event["RequestType"] == "Delete": 212 | client.deregister_targets(TargetGroupArn=target_group_arn, Targets=targets) 213 | except Exception as error: 214 | logger.exception(error) 215 | response_data["Message"] = str(error) 216 | status = cfnresponse.FAILED 217 | else: 218 | response_data["Message"] = "success" 219 | status = cfnresponse.SUCCESS 220 | finally: 221 | cfnresponse.send(event, context, status, response_data) 222 | MemorySize: 128 # megabytes 223 | Role: !GetAtt TargetFunctionRole.Arn 224 | Runtime: python3.11 225 | Timeout: 5 # seconds 226 | 227 | RegisterTarget: 228 | DependsOn: TargetCloudWatchLogsPolicy 229 | Type: "Custom::RegisterTarget" 230 | Properties: 231 | ServiceToken: !GetAtt TargetFunction.Arn 232 | TargetGroupArn: !Ref TargetGroup 233 | VpcEndpointId: !Ref VpcEndpointId 234 | 235 | Certificate: 236 | Type: "AWS::CertificateManager::Certificate" 237 | Properties: 238 | DomainName: !Ref DomainName 239 | DomainValidationOptions: 240 | - DomainName: !Ref DomainName 241 | HostedZoneId: !Ref HostedZoneId 242 | ValidationMethod: DNS 243 | 244 | Listener: 245 | Type: "AWS::ElasticLoadBalancingV2::Listener" 246 | Properties: 247 | AlpnPolicy: 248 | - HTTP1Only 249 | Certificates: 250 | - CertificateArn: !Ref Certificate 251 | DefaultActions: 252 | - Type: forward 253 | TargetGroupArn: !Ref TargetGroup 254 | LoadBalancerArn: !Ref LoadBalancer 255 | Port: 443 256 | Protocol: TLS 257 | SslPolicy: ELBSecurityPolicy-TLS13-1-3-2021-06 258 | 259 | NotificationTopic: 260 | Type: "AWS::SNS::Topic" 261 | Properties: 262 | DisplayName: VPC Endpoint Notifications 263 | KmsMasterKeyId: "alias/aws/sns" 264 | 265 | NotificationTopicPolicy: 266 | Type: "AWS::SNS::TopicPolicy" 267 | Properties: 268 | PolicyDocument: 269 | Id: VPCE 270 | Version: "2012-10-17" 271 | Statement: 272 | - Effect: Allow 273 | Principal: 274 | Service: !Sub "vpce.${AWS::URLSuffix}" 275 | Action: "sns:Publish" 276 | Resource: !Ref NotificationTopic 277 | Condition: 278 | ArnLike: 279 | "aws:SourceArn": !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc-endpoint-service/${VPCEndpointService}" 280 | StringEquals: 281 | "aws:SourceAccount": !Ref "AWS::AccountId" 282 | Topics: 283 | - !Ref NotificationTopic 284 | 285 | VPCEndpointService: 286 | Type: "AWS::EC2::VPCEndpointService" 287 | Properties: 288 | AcceptanceRequired: false 289 | ContributorInsightsEnabled: false 290 | NetworkLoadBalancerArns: 291 | - !Ref LoadBalancer 292 | 293 | VPCEndpointConnectionNotification: 294 | Type: "AWS::EC2::VPCEndpointConnectionNotification" 295 | Properties: 296 | ConnectionEvents: 297 | - Accept 298 | - Connect 299 | - Delete 300 | - Reject 301 | ConnectionNotificationArn: !Ref NotificationTopic 302 | ServiceId: !Ref VPCEndpointService 303 | 304 | PrivateDnsFunctionLogGroup: 305 | Type: "AWS::Logs::LogGroup" 306 | UpdateReplacePolicy: Delete 307 | DeletionPolicy: Delete 308 | Metadata: 309 | cfn_nag: 310 | rules_to_suppress: 311 | - id: W84 312 | reason: "Ignoring KMS key" 313 | Properties: 314 | LogGroupName: !Sub "/aws/lambda/${PrivateDnsFunction}" 315 | RetentionInDays: 3 316 | Tags: 317 | - Key: "aws-cloudformation:stack-name" 318 | Value: !Ref "AWS::StackName" 319 | - Key: "aws-cloudformation:stack-id" 320 | Value: !Ref "AWS::StackId" 321 | - Key: "aws-cloudformation:logical-id" 322 | Value: PrivateDnsFunctionLogGroup 323 | 324 | PrivateDnsFunctionRole: 325 | Type: "AWS::IAM::Role" 326 | Metadata: 327 | cfn_nag: 328 | rules_to_suppress: 329 | - id: W11 330 | reason: "Ignoring wildcard resource" 331 | Properties: 332 | AssumeRolePolicyDocument: 333 | Version: "2012-10-17" 334 | Statement: 335 | Effect: Allow 336 | Principal: 337 | Service: !Sub "lambda.${AWS::URLSuffix}" 338 | Action: "sts:AssumeRole" 339 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 340 | Policies: 341 | - PolicyName: RegisterTargetPolicy 342 | PolicyDocument: 343 | Version: "2012-10-17" 344 | Statement: 345 | - Effect: Allow 346 | Action: "ec2:ModifyVpcEndpointServiceConfiguration" 347 | Resource: !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc-endpoint-service/${VPCEndpointService}" 348 | - Effect: Allow 349 | Action: "ec2:DescribeVpcEndpointServiceConfigurations" 350 | Resource: "*" 351 | Tags: 352 | - Key: "aws-cloudformation:stack-name" 353 | Value: !Ref "AWS::StackName" 354 | - Key: "aws-cloudformation:stack-id" 355 | Value: !Ref "AWS::StackId" 356 | - Key: "aws-cloudformation:logical-id" 357 | Value: PrivateDnsFunctionRole 358 | 359 | CloudWatchLogsPolicy: 360 | Type: "AWS::IAM::Policy" 361 | Properties: 362 | PolicyName: CloudWatchLogs 363 | PolicyDocument: 364 | Version: "2012-10-17" 365 | Statement: 366 | - Effect: Allow 367 | Action: 368 | - "logs:CreateLogStream" 369 | - "logs:PutLogEvents" 370 | Resource: !GetAtt PrivateDnsFunctionLogGroup.Arn 371 | Roles: 372 | - !Ref PrivateDnsFunctionRole 373 | 374 | PrivateDnsFunction: 375 | Type: "AWS::Lambda::Function" 376 | Metadata: 377 | cfn_nag: 378 | rules_to_suppress: 379 | - id: W58 380 | reason: "Ignoring CloudWatch" 381 | - id: W89 382 | reason: "Ignoring VPC" 383 | - id: W92 384 | reason: "Ignoring Reserved Concurrency" 385 | Properties: 386 | Architectures: 387 | - arm64 388 | Description: VPC Endpoint Private DNS Function 389 | Handler: index.lambda_handler 390 | Code: 391 | ZipFile: |- 392 | import logging 393 | 394 | import boto3 395 | import cfnresponse 396 | 397 | logger = logging.getLogger() 398 | logger.setLevel(logging.INFO) 399 | 400 | client = boto3.client("ec2") 401 | 402 | def lambda_handler(event, context): 403 | response_data = {} 404 | status = cfnresponse.SUCCESS 405 | 406 | service_id = event.get("ResourceProperties", {}).get("ServiceId") 407 | domain_name = event.get("ResourceProperties", {}).get("DomainName") 408 | 409 | try: 410 | if event["RequestType"] in ("Create", "Update"): 411 | client.modify_vpc_endpoint_service_configuration(ServiceId=service_id, PrivateDnsName=domain_name) 412 | paginator = client.get_paginator("describe_vpc_endpoint_service_configurations") 413 | page_iterator = paginator.paginate(ServiceIds=[service_id]) 414 | for page in page_iterator: 415 | for service in page.get("ServiceConfigurations", []): 416 | response_data = service.get("PrivateDnsNameConfiguration", {}) 417 | elif event["RequestType"] == "Delete": 418 | client.modify_vpc_endpoint_service_configuration(ServiceId=service_id, RemovePrivateDnsName=True) 419 | except Exception as error: 420 | logger.exception(error) 421 | response_data["Message"] = str(error) 422 | status = cfnresponse.FAILED 423 | else: 424 | response_data["Message"] = "success" 425 | status = cfnresponse.SUCCESS 426 | finally: 427 | cfnresponse.send(event, context, status, response_data) 428 | MemorySize: 128 # megabytes 429 | Role: !GetAtt PrivateDnsFunctionRole.Arn 430 | Runtime: python3.11 431 | Timeout: 10 # seconds 432 | 433 | PrivateDns: 434 | DependsOn: CloudWatchLogsPolicy 435 | Type: "Custom::PrivateDns" 436 | Properties: 437 | ServiceToken: !GetAtt PrivateDnsFunction.Arn 438 | ServiceId: !Ref VPCEndpointService 439 | DomainName: !Ref DomainName 440 | 441 | RecordSet: 442 | Type: "AWS::Route53::RecordSet" 443 | Properties: 444 | HostedZoneId: !Ref HostedZoneId 445 | Name: !Sub "${PrivateDns.Name}.${DomainName}" 446 | ResourceRecords: 447 | - !Sub '"${PrivateDns.Value}"' 448 | TTL: "1800" 449 | Type: !GetAtt PrivateDns.Type 450 | 451 | Outputs: 452 | VpcEndpointServiceId: 453 | Description: VPC Endpoint Service ID 454 | Value: !Ref VPCEndpointService 455 | VpcEndpointServiceName: 456 | Description: VPC Endpoint Service Name 457 | Value: !Sub "com.amazonaws.vpce.${AWS::Region}.${VPCEndpointService}" 458 | PrivateDnsName: 459 | Description: Private DNS Name 460 | Value: !Ref DomainName -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.1.0" 6 | constraints = ">= 4.40.0, 5.1.0" 7 | hashes = [ 8 | "h1:Cc/DLDIjTkNGWFWkOGVeVdJjxC/fe82NTxTsUHZ5HL0=", 9 | "zh:0c48f157b804c1f392adb5c14b81e756c652755e358096300ea8dd1283021129", 10 | "zh:1a50495a6c0e5665e51df57dac6e781ec71439b11ebf05f971b6f3a3eb4eb7b2", 11 | "zh:2959ff472c05e56d59e012118dd8d55022f005534c0ae961ce81136de9f66a4d", 12 | "zh:2dfda9133581b99ed6e709e89a453fd2974ce88c703d3e073ec31bf99d7508ce", 13 | "zh:2f3d92cc7a6624da42cee2202f8fb23e6d38f156ab7851884d637282cb0dc709", 14 | "zh:3bc2a34d09cbaf439a1815846904f070c782cd8dfd60b5e0116827cda25f7549", 15 | "zh:4ef43f1a247aa8de8690ac3bbc2b00ebaf6b2872fc8d0f5130e4a8130c874b87", 16 | "zh:5477cb272dcaeb0030091bcf23a9f0f33b5410e44e317e9d3d49446f545dbaa4", 17 | "zh:734c8fb4c0b79c82dd757566761dda5b91ee1ef9a2b848a748ade11e0e1cc69f", 18 | "zh:80346c051b677f4f018da7fe06318b87c5bd0f1ec67ce78ab33baed3bb8b031a", 19 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 20 | "zh:a865b2f88dfee13df14116c5cf53d033d2c15855f4b59b9c65337309a928df2c", 21 | "zh:c0345f266eedaece5612c1000722b302f895d1bc5af1d5a4265f0e7000ca48bb", 22 | "zh:d59703c8e6a9d8b4fbd3b4583b945dfff9cb2844c762c0b3990e1cef18282279", 23 | "zh:d8d04a6a6cd2dfcb23b57e551db7b15e647f6166310fb7d883d8ec67bdc9bdc8", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/dns" { 28 | version = "3.3.2" 29 | hashes = [ 30 | "h1:+EBJn9AcEnwp8PTNr9oGI2BcnDMIqKjBFz9WrcOuA3I=", 31 | "zh:05d2d50e301318362a4a82e6b7a9734ace07bc01abaaa649c566baf98814755f", 32 | "zh:1e9fd1c3bfdda777e83e42831dd45b7b9e794250a0f351e5fd39762e8a0fe15b", 33 | "zh:40e715fc7a2ede21f919567249b613844692c2f8a64f93ee64e5b68bae7ac2a2", 34 | "zh:454d7aa83000a6e2ba7a7bfde4bcf5d7ed36298b22d760995ca5738ab02ee468", 35 | "zh:46124ded51b4153ad90f12b0305fdbe0c23261b9669aa58a94a31c9cca2f4b19", 36 | "zh:55a4f13d20f73534515a6b05701abdbfc54f4e375ba25b2dffa12afdad20e49d", 37 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 38 | "zh:7903b1ceb8211e2b8c79290e2e70906a4b88f4fba71c900eb3a425ce12f1716a", 39 | "zh:b79fc4f444ef7a2fd7111a80428c070ad824f43a681699e99ab7f83074dfedbd", 40 | "zh:ca9f45e0c4cb94e7d62536c226024afef3018b1de84f1ea4608b51bcd497a2a0", 41 | "zh:ddc8bd894559d7d176e0ceb0bb1ae266519b01b315362ebfee8327bb7e7e5fa8", 42 | "zh:e77334c0794ef8f9354b10e606040f6b0b67b373f5ff1db65bddcdd4569b428b", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_iam_policy_document" "sns_topic_policy" { 4 | statement { 5 | principals { 6 | type = "Service" 7 | identifiers = ["vpce.amazonaws.com"] 8 | } 9 | actions = ["sns:Publish"] 10 | resources = [aws_sns_topic.this.arn] 11 | effect = "Allow" 12 | 13 | condition { 14 | test = "ArnLike" 15 | variable = "aws:SourceArn" 16 | values = [aws_vpc_endpoint_service.this.arn] 17 | } 18 | 19 | condition { 20 | test = "StringEquals" 21 | variable = "aws:SourceAccount" 22 | values = [local.aws_account_id] 23 | } 24 | } 25 | } 26 | 27 | data "aws_vpc_endpoint" "apigw" { 28 | id = var.vpc_endpoint_id 29 | } 30 | 31 | data "dns_a_record_set" "vpce" { 32 | host = data.aws_vpc_endpoint.apigw.dns_entry[0].dns_name 33 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | tags = { 3 | Terraform = "true" 4 | } 5 | 6 | aws_account_id = data.aws_caller_identity.current.account_id 7 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "allow_tls" { 2 | name = "allow_tls" 3 | description = "Allow TLS inbound traffic" 4 | vpc_id = var.vpc_id 5 | 6 | ingress { 7 | description = "Allow 443/tcp from anywhere" 8 | from_port = 443 9 | to_port = 443 10 | protocol = "tcp" 11 | cidr_blocks = ["0.0.0.0/0"] 12 | ipv6_cidr_blocks = ["::/0"] 13 | } 14 | 15 | egress { 16 | description = "Allow all traffic to anywhere" 17 | from_port = 0 18 | to_port = 0 19 | protocol = "-1" 20 | cidr_blocks = ["0.0.0.0/0"] 21 | ipv6_cidr_blocks = ["::/0"] 22 | } 23 | } 24 | 25 | resource "aws_lb" "this" { 26 | #checkov:skip=CKV_AWS_152:Cross-zone load balancing is disabled 27 | #checkov:skip=CKV_AWS_91:Disabled access logging 28 | 29 | load_balancer_type = "network" 30 | internal = true 31 | ip_address_type = "ipv4" 32 | 33 | enable_deletion_protection = true 34 | 35 | security_groups = [aws_security_group.allow_tls.id] 36 | subnets = var.subnet_ids 37 | } 38 | 39 | resource "aws_lb_target_group" "this" { 40 | target_type = "ip" 41 | port = 443 42 | protocol = "TLS" 43 | vpc_id = var.vpc_id 44 | 45 | preserve_client_ip = true 46 | deregistration_delay = 0 47 | connection_termination = true 48 | } 49 | 50 | resource "aws_lb_target_group_attachment" "this" { 51 | for_each = toset(data.dns_a_record_set.vpce.addrs) 52 | 53 | target_group_arn = aws_lb_target_group.this.arn 54 | target_id = each.value 55 | } 56 | 57 | module "acm" { 58 | source = "terraform-aws-modules/acm/aws" 59 | version = "4.3.2" 60 | 61 | domain_name = var.domain_name 62 | zone_id = var.zone_id 63 | 64 | wait_for_validation = true 65 | 66 | tags = { 67 | Name = var.domain_name 68 | } 69 | } 70 | 71 | resource "aws_lb_listener" "this" { 72 | load_balancer_arn = aws_lb.this.arn 73 | certificate_arn = module.acm.acm_certificate_arn 74 | port = 443 75 | protocol = "TLS" 76 | ssl_policy = "ELBSecurityPolicy-TLS13-1-3-2021-06" 77 | alpn_policy = "HTTP1Only" 78 | 79 | default_action { 80 | type = "forward" 81 | target_group_arn = aws_lb_target_group.this.arn 82 | } 83 | } 84 | 85 | resource "aws_sns_topic" "this" { 86 | display_name = "VPC Endpoint Notifications" 87 | kms_master_key_id = "alias/aws/sns" 88 | } 89 | 90 | resource "aws_sns_topic_policy" "this" { 91 | arn = aws_sns_topic.this.arn 92 | policy = data.aws_iam_policy_document.sns_topic_policy.json 93 | } 94 | 95 | resource "aws_vpc_endpoint_service" "this" { 96 | #checkov:skip=CKV_AWS_123:Disabled manual acceptance 97 | 98 | acceptance_required = false 99 | network_load_balancer_arns = [aws_lb.this.arn] 100 | private_dns_name = var.domain_name 101 | supported_ip_address_types = ["ipv4"] 102 | } 103 | 104 | resource "aws_vpc_endpoint_connection_notification" "this" { 105 | vpc_endpoint_service_id = aws_vpc_endpoint_service.this.id 106 | connection_notification_arn = aws_sns_topic.this.arn 107 | connection_events = ["Accept", "Reject", "Connect", "Delete"] 108 | } 109 | 110 | resource "aws_route53_record" "this" { 111 | zone_id = var.zone_id 112 | name = "${aws_vpc_endpoint_service.this.private_dns_name_configuration[0].name}.${var.domain_name}" 113 | records = [aws_vpc_endpoint_service.this.private_dns_name_configuration[0].value] 114 | type = aws_vpc_endpoint_service.this.private_dns_name_configuration[0].type 115 | ttl = 1800 116 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_endpoint_service_id" { 2 | value = aws_vpc_endpoint_service.this.id 3 | } 4 | 5 | output "vpc_endpoint_service_name" { 6 | value = aws_vpc_endpoint_service.this.service_name 7 | } 8 | 9 | output "private_dns_name" { 10 | value = var.domain_name 11 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | default_tags { 4 | tags = local.tags 5 | } 6 | } 7 | 8 | provider "dns" { 9 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | description = "VPC hosting the ALB" 4 | } 5 | 6 | variable "subnet_ids" { 7 | type = list(string) 8 | description = "Private Subnet Ids for NLB. These should be the same AZs the private API Gateway is using." 9 | } 10 | 11 | variable "vpc_endpoint_id" { 12 | type = string 13 | description = "VPC Endpoint ID for the private API Gateway" 14 | } 15 | 16 | variable "zone_id" { 17 | type = string 18 | description = "Hosted Zone ID" 19 | } 20 | 21 | variable "domain_name" { 22 | type = string 23 | description = "Domain Name" 24 | } -------------------------------------------------------------------------------- /privatelink_nlb_apigw/terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.6" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "5.1.0" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | checkov 2 | cfn-lint 3 | urllib3[secure]<2 -------------------------------------------------------------------------------- /test_template.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | Transform: "AWS::Serverless-2016-10-31" 7 | Description: Provider Test Template 8 | 9 | Parameters: 10 | VpcId: 11 | Type: "AWS::EC2::VPC::Id" 12 | Description: VPC ID 13 | PublicSubnetIds: 14 | Type: "List" 15 | Description: Public Subnet Ids for the ALB 16 | PrivateSubnetIds: 17 | Type: "List" 18 | Description: Private Subnet Ids for the private API Gateway 19 | HostedZoneId: 20 | Type: "AWS::Route53::HostedZone::Id" 21 | Description: Hosted Zone ID 22 | DomainName: 23 | Type: String 24 | Description: Base Domain Name 25 | 26 | Globals: 27 | Function: 28 | Architectures: 29 | - arm64 30 | Handler: index.lambda_handler 31 | MemorySize: 128 # megabytes 32 | Timeout: 5 # seconds 33 | 34 | Resources: 35 | LoadBalancerSecurityGroup: 36 | Type: "AWS::EC2::SecurityGroup" 37 | Metadata: 38 | cfn_nag: 39 | rules_to_suppress: 40 | - id: W2 41 | reason: "Ignoring world access, permissible on ELB" 42 | - id: W5 43 | reason: "Ignoring egress all access" 44 | - id: W9 45 | reason: "Ignoring ingress not /32" 46 | - id: W40 47 | reason: "Ignoring all protocols" 48 | Properties: 49 | GroupDescription: Load Balancer SG 50 | SecurityGroupEgress: 51 | - CidrIp: "0.0.0.0/0" 52 | Description: Allow any to Internet 53 | IpProtocol: "-1" 54 | SecurityGroupIngress: 55 | - CidrIp: "0.0.0.0/0" 56 | Description: Allow 80/tcp from Internet 57 | FromPort: 80 58 | IpProtocol: tcp 59 | ToPort: 80 60 | - CidrIp: "0.0.0.0/0" 61 | Description: Allow 443/tcp from Internet 62 | FromPort: 443 63 | IpProtocol: tcp 64 | ToPort: 443 65 | VpcId: !Ref VpcId 66 | 67 | LoadBalancer: 68 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 69 | Metadata: 70 | cfn_nag: 71 | rules_to_suppress: 72 | - id: W52 73 | reason: "Ignoring access logging" 74 | Properties: 75 | IpAddressType: ipv4 76 | LoadBalancerAttributes: 77 | - Key: routing.http.drop_invalid_header_fields.enabled 78 | Value: "true" 79 | - Key: routing.http.x_amzn_tls_version_and_cipher_suite.enabled 80 | Value: "true" 81 | - Key: routing.http.xff_client_port.enabled 82 | Value: "true" 83 | - Key: routing.http2.enabled 84 | Value: "true" 85 | SecurityGroups: 86 | - !Ref LoadBalancerSecurityGroup 87 | Scheme: internet-facing 88 | Subnets: !Ref PublicSubnetIds 89 | Type: application 90 | 91 | TargetGroup: 92 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 93 | Properties: 94 | TargetType: lambda 95 | 96 | FunctionLogGroup: 97 | Type: "AWS::Logs::LogGroup" 98 | UpdateReplacePolicy: Delete 99 | DeletionPolicy: Delete 100 | Metadata: 101 | cfn_nag: 102 | rules_to_suppress: 103 | - id: W84 104 | reason: "Ignoring KMS key" 105 | Properties: 106 | LogGroupName: !Sub "/aws/lambda/${Function}" 107 | RetentionInDays: 3 108 | Tags: 109 | - Key: "aws-cloudformation:stack-name" 110 | Value: !Ref "AWS::StackName" 111 | - Key: "aws-cloudformation:stack-id" 112 | Value: !Ref "AWS::StackId" 113 | - Key: "aws-cloudformation:logical-id" 114 | Value: FunctionLogGroup 115 | 116 | FunctionRole: 117 | Type: "AWS::IAM::Role" 118 | Properties: 119 | AssumeRolePolicyDocument: 120 | Version: "2012-10-17" 121 | Statement: 122 | Effect: Allow 123 | Principal: 124 | Service: !Sub "lambda.${AWS::URLSuffix}" 125 | Action: "sts:AssumeRole" 126 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 127 | Tags: 128 | - Key: "aws-cloudformation:stack-name" 129 | Value: !Ref "AWS::StackName" 130 | - Key: "aws-cloudformation:stack-id" 131 | Value: !Ref "AWS::StackId" 132 | - Key: "aws-cloudformation:logical-id" 133 | Value: FunctionRole 134 | 135 | CloudWatchLogsPolicy: 136 | Type: "AWS::IAM::Policy" 137 | Properties: 138 | PolicyName: CloudWatchLogs 139 | PolicyDocument: 140 | Version: "2012-10-17" 141 | Statement: 142 | - Effect: Allow 143 | Action: 144 | - "logs:CreateLogStream" 145 | - "logs:PutLogEvents" 146 | Resource: !GetAtt FunctionLogGroup.Arn 147 | Roles: 148 | - !Ref FunctionRole 149 | 150 | Function: 151 | Type: "AWS::Serverless::Function" 152 | Metadata: 153 | cfn_nag: 154 | rules_to_suppress: 155 | - id: W58 156 | reason: "Ignoring CloudWatch" 157 | - id: W89 158 | reason: "Ignoring VPC" 159 | - id: W92 160 | reason: "Ignoring Reserved Concurrency" 161 | Properties: 162 | Description: !Sub "${AWS::StackName} Demo Function" 163 | InlineCode: |- 164 | exports.lambda_handler = async(event, context) => { 165 | console.log(event); 166 | 167 | const data = { 168 | event 169 | }; 170 | 171 | const response = { 172 | statusCode: 200, 173 | isBase64Encoded: false, 174 | headers: { 175 | 'Content-Type': 'application/json' 176 | }, 177 | body: JSON.stringify(data, null, 2) 178 | }; 179 | return response; 180 | } 181 | Role: !GetAtt FunctionRole.Arn 182 | Runtime: nodejs18.x 183 | 184 | FunctionPermission: 185 | Type: "AWS::Lambda::Permission" 186 | Properties: 187 | FunctionName: !GetAtt Function.Arn 188 | Action: "lambda:InvokeFunction" 189 | Principal: !Sub "elasticloadbalancing.${AWS::URLSuffix}" 190 | SourceAccount: !Ref "AWS::AccountId" 191 | SourceArn: !Sub "arn:${AWS::Partition}:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:${TargetGroup.TargetGroupFullName}" 192 | 193 | TargetFunctionLogGroup: 194 | Type: "AWS::Logs::LogGroup" 195 | UpdateReplacePolicy: Delete 196 | DeletionPolicy: Delete 197 | Metadata: 198 | cfn_nag: 199 | rules_to_suppress: 200 | - id: W84 201 | reason: "Ignoring KMS key" 202 | Properties: 203 | LogGroupName: !Sub "/aws/lambda/${TargetFunction}" 204 | RetentionInDays: 3 205 | Tags: 206 | - Key: "aws-cloudformation:stack-name" 207 | Value: !Ref "AWS::StackName" 208 | - Key: "aws-cloudformation:stack-id" 209 | Value: !Ref "AWS::StackId" 210 | - Key: "aws-cloudformation:logical-id" 211 | Value: TargetFunctionLogGroup 212 | 213 | TargetFunctionRole: 214 | Type: "AWS::IAM::Role" 215 | Properties: 216 | AssumeRolePolicyDocument: 217 | Version: "2012-10-17" 218 | Statement: 219 | Effect: Allow 220 | Principal: 221 | Service: !Sub "lambda.${AWS::URLSuffix}" 222 | Action: "sts:AssumeRole" 223 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 224 | Policies: 225 | - PolicyName: RegisterTargetPolicy 226 | PolicyDocument: 227 | Version: "2012-10-17" 228 | Statement: 229 | - Effect: Allow 230 | Action: 231 | - "elasticloadbalancing:RegisterTargets" 232 | - "elasticloadbalancing:DeregisterTargets" 233 | Resource: !Ref TargetGroup 234 | Tags: 235 | - Key: "aws-cloudformation:stack-name" 236 | Value: !Ref "AWS::StackName" 237 | - Key: "aws-cloudformation:stack-id" 238 | Value: !Ref "AWS::StackId" 239 | - Key: "aws-cloudformation:logical-id" 240 | Value: TargetFunctionRole 241 | 242 | TargetCloudWatchLogsPolicy: 243 | Type: "AWS::IAM::Policy" 244 | Properties: 245 | PolicyName: CloudWatchLogs 246 | PolicyDocument: 247 | Version: "2012-10-17" 248 | Statement: 249 | - Effect: Allow 250 | Action: 251 | - "logs:CreateLogStream" 252 | - "logs:PutLogEvents" 253 | Resource: !GetAtt TargetFunctionLogGroup.Arn 254 | Roles: 255 | - !Ref TargetFunctionRole 256 | 257 | TargetFunction: 258 | Type: "AWS::Serverless::Function" 259 | Metadata: 260 | cfn_nag: 261 | rules_to_suppress: 262 | - id: W58 263 | reason: "Ignoring CloudWatch" 264 | - id: W89 265 | reason: "Ignoring VPC" 266 | - id: W92 267 | reason: "Ignoring Reserved Concurrency" 268 | Properties: 269 | Description: ALB Target Registration Function 270 | InlineCode: |- 271 | import logging 272 | 273 | import boto3 274 | import cfnresponse 275 | 276 | logger = logging.getLogger() 277 | logger.setLevel(logging.INFO) 278 | 279 | client = boto3.client("elbv2") 280 | 281 | def lambda_handler(event, context): 282 | response_data = {} 283 | status = cfnresponse.SUCCESS 284 | 285 | target_group_arn = event.get("ResourceProperties", {}).get("TargetGroupArn") 286 | function_arn = event.get("ResourceProperties", {}).get("FunctionArn") 287 | targets = [{"Id": function_arn}] 288 | 289 | try: 290 | if event["RequestType"] in ("Create", "Update"): 291 | client.register_targets(TargetGroupArn=target_group_arn, Targets=targets) 292 | elif event["RequestType"] == "Delete": 293 | client.deregister_targets(TargetGroupArn=target_group_arn, Targets=targets) 294 | except Exception as error: 295 | logger.exception(error) 296 | response_data["Message"] = str(error) 297 | status = cfnresponse.FAILED 298 | else: 299 | response_data["Message"] = "success" 300 | status = cfnresponse.SUCCESS 301 | finally: 302 | cfnresponse.send(event, context, status, response_data) 303 | Role: !GetAtt TargetFunctionRole.Arn 304 | Runtime: python3.11 305 | 306 | RegisterTarget: 307 | DependsOn: 308 | - FunctionPermission 309 | - TargetCloudWatchLogsPolicy 310 | Type: "Custom::RegisterTarget" 311 | Properties: 312 | ServiceToken: !GetAtt TargetFunction.Arn 313 | TargetGroupArn: !Ref TargetGroup 314 | FunctionArn: !GetAtt Function.Arn 315 | 316 | Certificate: 317 | Type: "AWS::CertificateManager::Certificate" 318 | Properties: 319 | DomainName: !Sub "alb.${DomainName}" 320 | DomainValidationOptions: 321 | - DomainName: !Sub "alb.${DomainName}" 322 | HostedZoneId: !Ref HostedZoneId 323 | ValidationMethod: DNS 324 | 325 | HttpListener: 326 | Type: "AWS::ElasticLoadBalancingV2::Listener" 327 | Metadata: 328 | cfn_nag: 329 | rules_to_suppress: 330 | - id: W56 331 | reason: "Ignoring HTTP" 332 | Properties: 333 | DefaultActions: 334 | - Type: redirect 335 | RedirectConfig: 336 | Host: "#{host}" 337 | Path: "/#{path}" 338 | Protocol: HTTPS 339 | Port: 443 340 | Query: "#{query}" 341 | StatusCode: "HTTP_301" 342 | LoadBalancerArn: !Ref LoadBalancer 343 | Port: 80 344 | Protocol: HTTP 345 | 346 | HttpsListener: 347 | Type: "AWS::ElasticLoadBalancingV2::Listener" 348 | Properties: 349 | Certificates: 350 | - CertificateArn: !Ref Certificate 351 | DefaultActions: 352 | - Type: forward 353 | TargetGroupArn: !Ref TargetGroup 354 | LoadBalancerArn: !Ref LoadBalancer 355 | Port: 443 356 | Protocol: HTTPS 357 | SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 358 | 359 | RecordSet: 360 | Type: "AWS::Route53::RecordSet" 361 | Properties: 362 | AliasTarget: 363 | DNSName: !GetAtt LoadBalancer.DNSName 364 | EvaluateTargetHealth: true 365 | HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID 366 | HostedZoneId: !Ref HostedZoneId 367 | Name: !Sub "alb.${DomainName}" 368 | Type: A 369 | 370 | ######################## API Gateway ###################### 371 | 372 | VpcEndpointSecurityGroup: 373 | Type: "AWS::EC2::SecurityGroup" 374 | Metadata: 375 | cfn_nag: 376 | rules_to_suppress: 377 | - id: W2 378 | reason: "Ignoring world access, permissible on ELB" 379 | - id: W5 380 | reason: "Ignoring egress all access" 381 | - id: W9 382 | reason: "Ignoring ingress not /32" 383 | - id: W40 384 | reason: "Ignoring all protocols" 385 | Properties: 386 | GroupDescription: VPC Endpoint SG 387 | SecurityGroupEgress: 388 | - CidrIp: "0.0.0.0/0" 389 | Description: Allow any to Internet 390 | IpProtocol: "-1" 391 | SecurityGroupIngress: 392 | - CidrIp: "0.0.0.0/0" 393 | Description: Allow 443/tcp from Internet 394 | FromPort: 443 395 | IpProtocol: tcp 396 | ToPort: 443 397 | VpcId: !Ref VpcId 398 | 399 | ApiGatewayRole: 400 | Type: "AWS::IAM::Role" 401 | Properties: 402 | AssumeRolePolicyDocument: 403 | Version: "2012-10-17" 404 | Statement: 405 | Effect: Allow 406 | Principal: 407 | Service: !Sub "apigateway.${AWS::URLSuffix}" 408 | Action: "sts:AssumeRole" 409 | Description: !Sub "DO NOT DELETE - Used by API Gateway. Created by CloudFormation ${AWS::StackId}" 410 | Policies: 411 | - PolicyName: Lambda 412 | PolicyDocument: 413 | Version: "2012-10-17" 414 | Statement: 415 | - Effect: Allow 416 | Action: "lambda:InvokeFunction" 417 | Resource: !GetAtt Function.Arn 418 | Tags: 419 | - Key: "aws-cloudformation:stack-name" 420 | Value: !Ref "AWS::StackName" 421 | - Key: "aws-cloudformation:stack-id" 422 | Value: !Ref "AWS::StackId" 423 | - Key: "aws-cloudformation:logical-id" 424 | Value: ApiGatewayRole 425 | 426 | VpcEndpoint: 427 | Type: "AWS::EC2::VPCEndpoint" 428 | Properties: 429 | PrivateDnsEnabled: true 430 | ServiceName: !Sub "com.amazonaws.${AWS::Region}.execute-api" 431 | SecurityGroupIds: 432 | - !Ref VpcEndpointSecurityGroup 433 | SubnetIds: !Ref PrivateSubnetIds 434 | VpcEndpointType: Interface 435 | VpcId: !Ref VpcId 436 | 437 | PrivateApi: 438 | Type: "AWS::ApiGateway::RestApi" 439 | Properties: 440 | Description: Sample Private API Gateway 441 | DisableExecuteApiEndpoint: true 442 | EndpointConfiguration: 443 | Types: 444 | - PRIVATE 445 | VpcEndpointIds: 446 | - !Ref VpcEndpoint 447 | MinimumCompressionSize: 860 # bytes 448 | Name: !Ref "AWS::StackName" 449 | Policy: 450 | Version: "2012-10-17" 451 | Statement: 452 | - Effect: Allow 453 | Principal: "*" 454 | Action: "execute-api:Invoke" 455 | Resource: "execute-api:/*" 456 | - Effect: Deny 457 | Principal: "*" 458 | Action: "execute-api:Invoke" 459 | Resource: "execute-api:/*" 460 | Condition: 461 | StringNotEquals: 462 | "aws:SourceVpce": !Ref VpcEndpoint 463 | 464 | ProxyResource: 465 | Type: "AWS::ApiGateway::Resource" 466 | Properties: 467 | ParentId: !GetAtt PrivateApi.RootResourceId 468 | RestApiId: !Ref PrivateApi 469 | PathPart: "{proxy+}" 470 | 471 | RootMethod: 472 | Type: "AWS::ApiGateway::Method" 473 | Metadata: 474 | cfn_nag: 475 | rules_to_suppress: 476 | - id: W59 477 | reason: "Ignoring no authorization type" 478 | Properties: 479 | AuthorizationType: NONE 480 | HttpMethod: ANY 481 | Integration: 482 | Credentials: !GetAtt ApiGatewayRole.Arn 483 | ConnectionType: INTERNET 484 | IntegrationHttpMethod: POST 485 | PassthroughBehavior: WHEN_NO_MATCH 486 | TimeoutInMillis: 2000 487 | Type: AWS_PROXY 488 | Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" 489 | ResourceId: !GetAtt PrivateApi.RootResourceId 490 | RestApiId: !Ref PrivateApi 491 | 492 | ProxyMethod: 493 | Type: "AWS::ApiGateway::Method" 494 | Metadata: 495 | cfn_nag: 496 | rules_to_suppress: 497 | - id: W59 498 | reason: "Ignoring no authorization type" 499 | Properties: 500 | AuthorizationType: NONE 501 | HttpMethod: ANY 502 | Integration: 503 | CacheKeyParameters: 504 | - "method.request.path.proxy" 505 | Credentials: !GetAtt ApiGatewayRole.Arn 506 | ConnectionType: INTERNET 507 | IntegrationHttpMethod: POST 508 | PassthroughBehavior: WHEN_NO_MATCH 509 | TimeoutInMillis: 2000 510 | RequestParameters: 511 | integration.request.path.proxy: "method.request.path.proxy" 512 | Type: AWS_PROXY 513 | Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" 514 | RequestParameters: 515 | method.request.path.proxy: true 516 | ResourceId: !Ref ProxyResource 517 | RestApiId: !Ref PrivateApi 518 | 519 | Deployment: 520 | Type: "AWS::ApiGateway::Deployment" 521 | DependsOn: 522 | - RootMethod 523 | - ProxyMethod 524 | Metadata: 525 | cfn_nag: 526 | rules_to_suppress: 527 | - id: W68 528 | reason: "Ignoring usage plan" 529 | Properties: 530 | RestApiId: !Ref PrivateApi 531 | 532 | Stage: 533 | Type: "AWS::ApiGateway::Stage" 534 | UpdateReplacePolicy: Delete 535 | DeletionPolicy: Delete 536 | Metadata: 537 | cfn_nag: 538 | rules_to_suppress: 539 | - id: W69 540 | reason: "Ignoring access logs" 541 | - id: W64 542 | reason: "Ignoring usage plan" 543 | Properties: 544 | DeploymentId: !Ref Deployment 545 | Description: !Sub "${AWS::StackName} v0" 546 | RestApiId: !Ref PrivateApi 547 | StageName: v0 548 | 549 | ApiGatewayFunctionPermission: 550 | Type: "AWS::Lambda::Permission" 551 | Properties: 552 | FunctionName: !GetAtt Function.Arn 553 | Action: "lambda:InvokeFunction" 554 | Principal: !Sub "apigateway.${AWS::URLSuffix}" 555 | SourceAccount: !Ref "AWS::AccountId" 556 | SourceArn: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PrivateApi}/*/*/*" 557 | 558 | ApiCertificate: 559 | Type: "AWS::CertificateManager::Certificate" 560 | Properties: 561 | DomainName: !Sub "apigw.${DomainName}" 562 | DomainValidationOptions: 563 | - DomainName: !Sub "apigw.${DomainName}" 564 | HostedZoneId: !Ref HostedZoneId 565 | ValidationMethod: DNS 566 | 567 | ApiDomainName: 568 | Type: "AWS::ApiGateway::DomainName" 569 | Properties: 570 | DomainName: !Sub "apigw.${DomainName}" 571 | EndpointConfiguration: 572 | Types: 573 | - REGIONAL 574 | RegionalCertificateArn: !Ref ApiCertificate 575 | SecurityPolicy: TLS_1_2 576 | 577 | BasePathMapping: 578 | Type: "AWS::ApiGateway::BasePathMapping" 579 | DependsOn: ApiDomainName 580 | Properties: 581 | DomainName: !Sub "apigw.${DomainName}" 582 | RestApiId: !Ref PrivateApi 583 | Stage: !Ref Stage 584 | 585 | Outputs: 586 | LoadBalancerArn: 587 | Description: Load Balancer ARN 588 | Value: !Ref LoadBalancer 589 | LoadBalancerDNSName: 590 | Description: Load Balancer Hostname 591 | Value: !GetAtt LoadBalancer.DNSName 592 | LoadBalancerURL: 593 | Description: Public Load Balancer URL 594 | Value: !Sub "https://alb.${DomainName}/" 595 | ApiGatewayDNSName: 596 | Description: Private API Gateway Hostname 597 | Value: !Sub "${PrivateApi}-${VpcEndpoint}.execute-api.${AWS::Region}.${AWS::URLSuffix}" 598 | ApiGatewayURL: 599 | Description: Private API Gateway URL 600 | Value: !Sub "https://${PrivateApi}-${VpcEndpoint}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}" 601 | VpcEndpointId: 602 | Description: Private API Gateway VPC Endpoint ID 603 | Value: !Ref VpcEndpoint --------------------------------------------------------------------------------