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

New Serverless pattern - multi-account centralized API Gateway with Transit Gateway #2588

Open
wants to merge 1 commit 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
175 changes: 175 additions & 0 deletions multi-account-central-api-private-targets-tgw-cdk-typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Multi-account centralized API Gateway with private targets via Transit Gateway

This CDK template implements an Amazon API Gateway REST API, as a front door for two services residing in separate AWS accounts. The pattern shows how this can be implemented with the use of AWS Transit Gateway. The pattern allows for centralized governance and security and provides flexibility to development team to develop their applications in their own accounts.


## Architecture Overview

![alt text](src/images/architecture_overview.png)

- The `Central API Account` hosts an API Gateway Regional API which acts as the central API for services in the two workload accounts. A VPC link is created in this account to enable the backend integration to the services in the workload accounts. The VPC link connectivity is established using an NLB, which has 2 target groups consisting of the Elastic Network Interfaces (ENIs) for the NLBs in each workload account.

- In `Workload Account A`, a service is hosted with a Fargate service and an ALB as the backend integration for the central API. An NLB is created to allow connectivity to the ALB of the Fargate service. The NLB is required as it has static IPs which can be used in the target group for the Central API VPC Link's NLB.

- In `Workload Account B`, the other service is hosted with a Fargate service and an NLB as the backend integration for the central API. The Elastic Network Interfaces (ENIs) of the NLB are used as targets for the Central API VPC Link's NLB.

- In the `TGW Account` the AWS Transit Gateway is hosted. The Transit Gateway contains the configurations required to allow connectivity between the VPCs in the Central API, Workload A and Workload B accounts.


## Requirements
* [Four AWS accounts](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node and NPM](https://nodejs.org/en/download)
* [AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)



## Deployment Instructions

1. Clone the repository
```
git clone https://github.com/aws-samples/serverless-patterns
```

1. Navigate to the directory:
```
cd multi-account-central-api-private-targets-tgw-cdk-typescript
```

1. Install dependencies:
```
npm install
```

1. Open `src/config.json` and modify the cidrs if you require:
```
...
"cidrs": {
"serviceA": "10.5.0.0/16",
"serviceB": "10.6.0.0/16",
"centralAPI": "10.7.0.0/16"
}
...
```

1. Set Environment Variables:
```
export REGION=ca-central-1
export CENTRAL_API_ACCOUNT_NUMBER=xxxxxxxxxxxx
export SERVICE_A_ACCOUNT_NUMBER=xxxxxxxxxxxx
export SERVICE_B_ACCOUNT_NUMBER=xxxxxxxxxxxx
export TGW_ACCOUNT_NUMBER=xxxxxxxxxxxx
```

1. Configure AWS CDK for each AWS account:
```
cdk bootstrap --profile <your-profile-name> $CENTRAL_API_ACCOUNT_NUMBER/$REGION
cdk bootstrap --profile <your-profile-name> $SERVICE_A_ACCOUNT_NUMBER/$REGION
cdk bootstrap --profile <your-profile-name> $SERVICE_B_ACCOUNT_NUMBER/$REGION
cdk bootstrap --profile <your-profile-name> $TGW_ACCOUNT_NUMBER/$REGION
```

1. Deploy using AWS CDK:
```
cdk deploy TgwStack --profile <your-profile-name>
cdk deploy ServiceAStack --profile <your-profile-name>
cdk deploy ServiceBStack --profile <your-profile-name>
cdk deploy CentralApiStack --profile <your-profile-name>
```
## How it works

The CDK application consists of the following 4 stacks.

### CentralApiStack

![alt text](src/images/central-api-stack.png)

In this stack, the central Regional REST API is deployed with the VPC, VPC Link, NLB, NLB targets, and the attachment to the Transit Gateway.

### ServiceAStack

![alt text](src/images/service-a-stack.png)

In this stack, the following are deployed: Fargate, ALB, NLB, and Transit Gateway attachment. In addition to these core components, an Advanced Parameter and an AWS Resource Access Manager Share are created to store the NLB ENIs and share them to the `Central API` Account. The Central API Account needs these ENIs to setup the target group for the VPC Link's NLB.

### ServiceBStack

![alt text](src/images/service-b-stack.png)

In this stack, the following are deployed: Fargate, NLB, and Transit Gateway attachment. In addition to these core components, an Advanced Parameter and an AWS Resource Access Manager Share are created to store the NLB ENIs and share them to the `Central API` Account. The Central API Account needs these ENIs to setup the target group for the VPC Link's NLB.

### TgwStack

![alt text](src/images/tgw-stack.png)

In this stack, the Transit Gateway is deployed and shared to the other accounts using AWS Resource Access Manager. In addition to these core components, an Advanced Parameter is created to store the Transit Gateway ID and share to the other accounts. The other accounts need the Transit Gateway ID to create their attachments.

## Testing

Use the following commands to get the Endpoint URL for Service A and Service B.

```
export SERVICE_A_URL=$(aws cloudformation describe-stacks --stack-name CentralApiStack --query "Stacks[0].Outputs[?ExportName=='ServiceAEndpoint'].OutputValue" --output text --profile <your-profile-name>)

export SERVICE_B_URL=$(aws cloudformation describe-stacks --stack-name CentralApiStack --query "Stacks[0].Outputs[?ExportName=='ServiceBEndpoint'].OutputValue" --output text --profile <your-profile-name>)

```


The APIs for both Service A and Service B are the same. Here is a list of available endpoints:

```
GET /swagger (This is for the API definition)
GET /Demo
POST /Demo
GET /Demo/{id}
PUT /Demo/{id}
DELETE /Demo/{id}
```

Here are example commands that can be used:


1. Get a list of items
```
curl $SERVICE_A_URL/Demo
curl $SERVICE_B_URL/Demo
```

1. Add an item to the data store
```
curl -X POST $SERVICE_A_URL/Demo -d '{"id":"1234", "name":"Service A test"}' -H "Content-Type: application/json"
curl -X POST $SERVICE_B_URL/Demo -d '{"id":"1234", "name":"Service B test"}' -H "Content-Type: application/json"
```

1. Get the item added in step 2
```
curl $SERVICE_A_URL/Demo/1234
curl $SERVICE_B_URL/Demo/1234
```

1. Update the item added in step 2
```
curl -X PUT $SERVICE_A_URL/Demo/1234 -d '{"id":"1234", "name":"This is a test for Service A"}' -H "Content-Type: application/json"
curl -X PUT $SERVICE_B_URL/Demo/1234 -d '{"id":"1234", "name":"This is a test for Service B"}' -H "Content-Type: application/json"
```

1. Delete the item added in step 2
```
curl -X DELETE $SERVICE_A_URL/Demo/1234
curl -X DELETE $SERVICE_B_URL/Demo/1234
```

## Cleanup

1. Destroy the stacks

```
cdk destroy CentralApiStack --profile <your-profile-name>
cdk destroy ServiceBStack --profile <your-profile-name>
cdk destroy ServiceAStack --profile <your-profile-name>
cdk destroy TgwStack --profile <your-profile-name>
```

1. Wait for the resources to delete and confirm their removal
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"title": "Multi-account centralized API Gateway with private targets via Transit Gateway",
"description": "Create a multi-account setup with a centralized REST API Gateway with private integrations via Transit Gateway",
"language": "Typescript",
"level": "300",
"framework": "CDK",
"introBox": {
"headline": "How it works",
"text": [
"This sample project demonstrates how to enable secure, centralized API communications across multiple AWS accounts using Amazon API Gateway and AWS Transit Gateway. It facilitates east/west communication between services while keeping traffic within the AWS network.",
"The architecture utilizes key AWS services such as Amazon API Gateway, VPC links, Network Load Balancers (NLBs) and Transit Gateway. These services work together to securely route requests between multiple AWS accounts and their respective applications.",
"This pattern deploys four separate AWS accounts: one account hosting the central API Gateway, 2 workload accounts hosting private ECS Fargate applications, and a network account hosting the Transit Gateway."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/multi-account-central-api-private-targets-tgw-cdk-typescript",
"templateURL": "serverless-patterns/multi-account-central-api-private-targets-tgw-cdk-typescript",
"projectFolder": "multi-account-central-api-private-targets-tgw-cdk-typescript",
"templateFile": "src/bin/central-api-tgw.ts"
}
},
"resources": {
"bullets": [
{
"text": "Amazon API Gateway",
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html"
},
{
"text": "VPC Links",
"link": "https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html"
},
{
"text": "Network Load Balancer (NLB)",
"link": "https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html"
},
{
"text": "Application Load Balancer (ALB)",
"link": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html"
},
{
"text": "Amazon ECS Fargate",
"link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html"
},
{
"text": "AWS Tranist Gateway",
"link": "https://docs.aws.amazon.com/vpc/latest/tgw/what-is-transit-gateway.html"
}
]
},
"deploy": {
"text": [
"cdk deploy"
]
},
"testing": {
"text": [
"See the GitHub repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk delete</code>."
]
},
"authors": [
{
"name": "Jevon Liburd",
"image": "https://media.licdn.com/dms/image/v2/C5603AQGQeKZyhyvcpw/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1605745311041?e=1742428800&v=beta&t=zYr5hwUCsfljHyeDSMrWTmIrrfMu7LJpV0qD_nsjVYc",
"bio": "Jevon is a Technical Account Manager at Amazon Web Services.",
"linkedin": "jevon-liburd-a436b315",
"twitter": "Jevon_EL"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node
import { App, Tags } from 'aws-cdk-lib';
import { CentralApiStack } from '../lib/central-api-stack';
import { ServiceAStack } from '../lib/service-a-stack';
import { ServiceBStack } from '../lib/service-b-stack';
import { TgwStack } from '../lib/tgw-stack';
import config from '../config.json';

const app = new App();
const centralApiAccount =
config.stacks.centralAPI.account || process.env.CENTRAL_API_ACCOUNT_NUMBER;
const serviceAAccount =
config.stacks.serviceA.account || process.env.SERVICE_A_ACCOUNT_NUMBER;
const serviceBAccount =
config.stacks.serviceB.account || process.env.SERVICE_B_ACCOUNT_NUMBER;
const tgwAccount = config.stacks.tgw.account || process.env.TGW_ACCOUNT_NUMBER;
const region = config.region || process.env.REGION;

// Stop execution if the account numbers are not defined
if (!centralApiAccount || !serviceAAccount || !serviceBAccount || !tgwAccount || !region) {
throw new Error(
'Account numbers and/or region for stacks are not defined. Define them in the config file or as environment variables as mentioned in the README'
);
}

const centralAPIStack = new CentralApiStack(app, 'CentralApiStack', {
svcAAccount: serviceAAccount,
svcAParamId: config.params.svcANLB.paramId,
svcAParamName: config.params.svcANLB.param,
svcBAccount: serviceBAccount,
svcBParamId: config.params.svcBNLB.paramId,
svcBParamName: config.params.svcBNLB.param,
tgwParamId: config.params.tgw.paramId,
tgwParamName: config.params.tgw.param,
tgwAccount: tgwAccount,
svcACidr: config.cidrs.serviceA,
svcBCidr: config.cidrs.serviceB,
centralApiCidr: config.cidrs.centralAPI,
stage: config.stacks.centralAPI.stage,
apiDescription: config.stacks.centralAPI.apiDescription,
tags: convertTagArrayToJSONObj(config.tags),
env: { account: centralApiAccount, region: region },
});
const serviceAStack = new ServiceAStack(app, 'ServiceAStack', {
accountsForNLBShare: [centralApiAccount],
svcACidr: config.cidrs.serviceA,
svcBCidr: config.cidrs.serviceB,
centralApiCidr: config.cidrs.centralAPI,
tgwParamId: config.params.tgw.paramId,
tgwParamName: config.params.tgw.param,
tgwAccount: tgwAccount,
svcANLBParamId: config.params.svcANLB.paramId,
svcANLBParamName: config.params.svcANLB.param,
env: { account: serviceAAccount, region: region },
});
const serviceBStack = new ServiceBStack(app, 'ServiceBStack', {
accountsForNLBShare: [centralApiAccount],
svcACidr: config.cidrs.serviceA,
svcBCidr: config.cidrs.serviceB,
centralApiCidr: config.cidrs.centralAPI,
tgwParamId: config.params.tgw.paramId,
tgwParamName: config.params.tgw.param,
tgwAccount: tgwAccount,
svcBNLBParamId: config.params.svcBNLB.paramId,
svcBNLBParamName: config.params.svcBNLB.param,
env: { account: serviceBAccount, region: region },
});
const tgwStack = new TgwStack(app, 'TgwStack', {
accountsToShare: [
serviceAAccount,
serviceBAccount,
centralApiAccount,
],
tgwParamId: config.params.tgw.paramId,
tgwParamName: config.params.tgw.param,
env: { account: tgwAccount, region: region },
});

// Setting tags for all the taggable resoruces in the stacks
const tags = config.tags;
tags.forEach((tag) => {
Tags.of(centralAPIStack).add(tag.key, tag.value);
Tags.of(tgwStack).add(tag.key, tag.value);
Tags.of(serviceAStack).add(tag.key, tag.value);
Tags.of(serviceBStack).add(tag.key, tag.value);
});

// Function to convert tags area to tags JSON Object
function convertTagArrayToJSONObj(tags: {key: string, value: string}[]) {
const tagsJSON: {[key: string]: string} = {};
tags.forEach((tag) => {
tagsJSON[tag.key] = tag.value;
});
return tagsJSON;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"availability-zones:account=239641027929:region=ca-central-1": [
"ca-central-1a",
"ca-central-1b",
"ca-central-1d"
],
"availability-zones:account=211125558039:region=ca-central-1": [
"ca-central-1a",
"ca-central-1b",
"ca-central-1d"
],
"availability-zones:account=785168607513:region=ca-central-1": [
"ca-central-1a",
"ca-central-1b",
"ca-central-1d"
]
}
Loading