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

Add CloudFormation template to deploy signed API #122

Merged
merged 6 commits into from
Nov 22, 2023
Merged
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
21 changes: 12 additions & 9 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ the data in memory and provides endpoints to push and retrieve beacon data.
1. `cp config/signed-api.example.json config/signed-api.json` - To create a config file from the example one. Optionally
change the defaults.
2. `cp .env.example .env` - To copy the example environment variables. Optionally change the defaults.
3. `pnpm run dev` - To start the API server. The port number can be configured in the configuration file.
3. `pnpm run dev` - To start the API server. The port number can be configured by `DEV_SERVER_PORT` environment
variable.

### Testing

Expand Down Expand Up @@ -134,10 +135,6 @@ The delay in seconds for the endpoint. The endpoint will only serve data that is
The maximum number of signed data entries that can be inserted in one batch. This is a safety measure to prevent
spamming theAPI with large payloads. The batch is rejected if it contains more entries than this value.

#### `port`

The port on which the API is served.

#### `cache.maxAgeSeconds`

The maximum age of the cache header in seconds.
Expand Down Expand Up @@ -177,15 +174,21 @@ The API provides the following endpoints:

## Deployment

TODO: Write example how to deploy on AWS.
To deploy signed API on AWS you can use a CloudFormation template in the `deployment` folder. You need to specify the
docker image of the signed API and the URL of the signed API configuration which will be download when the service is
started.

The template will create all necessary AWS resources and assign a domain name to access the the API. You can get the URL
from the output parameters of the CloudFormation stack or by checking the DNS record of the load balancer.

To deploy on premise you can use the Docker instructions below.

## Docker

The API is also dockerized. To run the dockerized APi, you need to:

1. Publish the port of the API to the host machine using the `--publish` flag.
1. Publish the port of the API to the host machine. The port number of signed API in the container is set to `80`. So
the command should look like `--publish <HOST_PORT>:80`.
2. Mount config folder to `/app/config`. The folder should contain the `signed-api.json` file.
3. Pass the `-it --init` flags to the docker run command. This is needed to ensure the docker is stopped gracefully. See
[this](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals) for details.
Expand All @@ -195,8 +198,8 @@ The API is also dockerized. To run the dockerized APi, you need to:
For example:

```sh
# Assuming the current folder contains the "config" folder and ".env" file and the API port is 8090.
docker run --publish 8090:8090 -it --init --volume $(pwd)/config:/app/config --env-file .env --rm api3/signed-api:latest
# Assuming the current folder contains the "config" folder and ".env" file and the intended host port is 8090.
docker run --publish 8090:80 -it --init --volume $(pwd)/config:/app/config --env-file .env --rm api3/signed-api:latest
```

As of now, the docker image is not published anywhere. You need to build it locally. To build the image run:
Expand Down
1 change: 0 additions & 1 deletion packages/api/config/signed-api.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
],
"allowedAirnodes": ["0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"],
"maxBatchSize": 10,
"port": 8090,
"cache": {
"maxAgeSeconds": 300
}
Expand Down
272 changes: 272 additions & 0 deletions packages/api/deployment/cloudformation-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "CloudFormation template for deploying Signed API with public access",
"Outputs": {
"LoadBalancerDNS": {
"Description": "The DNS name of the load balancer",
"Value": { "Fn::GetAtt": ["ELB", "DNSName"] },
"Export": {
"Name": { "Fn::Sub": "${AWS::StackName}-LoadBalancerDNS" }
}
}
},
"Resources": {
"SignedApiLogsGroup": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/ecs/signedApi",
"RetentionInDays": 7
}
},
"SignedApiTaskDefinition": {
"Type": "AWS::ECS::TaskDefinition",
"Properties": {
"Family": "signed-api-task",
"Cpu": "256",
"Memory": "512",
"NetworkMode": "awsvpc",
"RequiresCompatibilities": ["FARGATE"],
"ExecutionRoleArn": { "Ref": "ECSTaskRole" },
"ContainerDefinitions": [
{
"Name": "signed-api-container",
"Image": "<DOCKER_IMAGE>",
"Environment": [
{
"Name": "CONFIG_SOURCE",
"Value": "local"
},
{
"Name": "LOG_LEVEL",
"Value": "debug"
}
],
"EntryPoint": [
"/bin/sh",
"-c",
"wget -O - <SIGNED_API_CONFIGURATION_URL> >> ./config/signed-api.json && node dist/index.js"
],
"PortMappings": [
{
"ContainerPort": 80,
"HostPort": 80
}
],
"LogConfiguration": {
"LogDriver": "awslogs",
"Options": {
"awslogs-group": { "Ref": "SignedApiLogsGroup" },
"awslogs-region": { "Ref": "AWS::Region" },
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
},
"SignedApiService": {
"Type": "AWS::ECS::Service",
"DependsOn": "SignedApiListener",
"Properties": {
"Cluster": { "Ref": "ECSCluster" },
"LaunchType": "FARGATE",
"TaskDefinition": { "Ref": "SignedApiTaskDefinition" },
"DesiredCount": 1,
"NetworkConfiguration": {
"AwsvpcConfiguration": {
"Subnets": [{ "Ref": "PublicSubnet1" }, { "Ref": "PublicSubnet2" }],
"SecurityGroups": [{ "Ref": "ECSSecurityGroup" }],
"AssignPublicIp": "ENABLED"
}
},
"LoadBalancers": [
{
"ContainerName": "signed-api-container",
"ContainerPort": 80,
"TargetGroupArn": { "Ref": "SignedApiTargetGroup" }
}
]
}
},
"ECSCluster": {
"Type": "AWS::ECS::Cluster",
"Properties": {
"ClusterName": "signed-api-cluster"
}
},
"ELB": {
"Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
"Properties": {
"Name": "signed-api-elb",
"Subnets": [{ "Ref": "PublicSubnet1" }, { "Ref": "PublicSubnet2" }],
"SecurityGroups": [{ "Ref": "ELBSecurityGroup" }],
"Scheme": "internet-facing"
}
},
"SignedApiListener": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"Properties": {
"DefaultActions": [
{
"Type": "forward",
"TargetGroupArn": { "Ref": "SignedApiTargetGroup" }
}
],
"LoadBalancerArn": { "Ref": "ELB" },
"Port": 80,
"Protocol": "HTTP"
}
},
"SignedApiTargetGroup": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
"Properties": {
"Port": 80,
"Protocol": "HTTP",
"VpcId": { "Ref": "VPC" },
"TargetType": "ip"
}
},
"VPC": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": "10.0.0.0/16",
"EnableDnsSupport": "true",
"EnableDnsHostnames": "true"
}
},
"PublicSubnet1": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": { "Ref": "VPC" },
"CidrBlock": "10.0.1.0/24",
"AvailabilityZone": { "Fn::Select": [0, { "Fn::GetAZs": "" }] },
"MapPublicIpOnLaunch": "true"
}
},
"PublicSubnet2": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": { "Ref": "VPC" },
"CidrBlock": "10.0.2.0/24",
"AvailabilityZone": { "Fn::Select": [1, { "Fn::GetAZs": "" }] },
"MapPublicIpOnLaunch": "true"
}
},
"InternetGateway": {
"Type": "AWS::EC2::InternetGateway"
},
"GatewayAttachment": {
"Type": "AWS::EC2::VPCGatewayAttachment",
"Properties": {
"VpcId": { "Ref": "VPC" },
"InternetGatewayId": { "Ref": "InternetGateway" }
}
},
"RouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": { "Ref": "VPC" }
}
},
"Route": {
"Type": "AWS::EC2::Route",
"DependsOn": "GatewayAttachment",
"Properties": {
"RouteTableId": { "Ref": "RouteTable" },
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": { "Ref": "InternetGateway" }
}
},
"Subnet1RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": { "Ref": "PublicSubnet1" },
"RouteTableId": { "Ref": "RouteTable" }
}
},
"Subnet2RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"SubnetId": { "Ref": "PublicSubnet2" },
"RouteTableId": { "Ref": "RouteTable" }
}
},
"ECSSecurityGroup": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Security Group for ECS Tasks",
"VpcId": { "Ref": "VPC" },
"SecurityGroupIngress": [
{
"IpProtocol": "tcp",
"FromPort": 80,
"ToPort": 80,
"SourceSecurityGroupId": { "Ref": "ELBSecurityGroup" }
}
]
}
},
"ELBSecurityGroup": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Security Group for ELB",
"VpcId": { "Ref": "VPC" },
"SecurityGroupIngress": [
{
"IpProtocol": "tcp",
"FromPort": 80,
"ToPort": 80,
"CidrIp": "0.0.0.0/0"
}
]
}
},
"ECSTaskRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["ecs-tasks.amazonaws.com"]
},
"Action": ["sts:AssumeRole"]
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName": "ecs-service",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:Describe*",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:DeregisterTargets",
"elasticloadbalancing:Describe*",
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
"elasticloadbalancing:RegisterTargets",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
}
]
}
}
}
}
6 changes: 3 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rm -rf coverage dist",
"dev": "nodemon --ext ts,js,json,env --exec \"pnpm ts-node src/index.ts\"",
"dev": "nodemon --ext ts,js,json,env --exec \"pnpm ts-node src/dev-server.ts\"",
"docker:build": "docker build --target api --tag api3/signed-api:latest ../../",
"docker:start": "docker run --publish 8090:8090 -it --init --volume $(pwd)/config:/app/config --env-file .env --rm api3/signed-api:latest",
"docker:run": "docker run --publish 8090:80 -it --init --volume $(pwd)/config:/app/config --env-file .env --rm api3/signed-api:latest",
"eslint:check": "eslint . --ext .js,.ts --max-warnings 0",
"eslint:fix": "eslint . --ext .js,.ts --fix",
"prettier:check": "prettier --check \"./**/*.{js,ts,md,json}\"",
"prettier:fix": "prettier --write \"./**/*.{js,ts,md,json}\"",
"start-prod": "node dist/index.js",
"start-prod": "node dist/dev-server.js",
Siegrift marked this conversation as resolved.
Show resolved Hide resolved
"test": "jest",
"tsc": "tsc --project ."
},
Expand Down
30 changes: 30 additions & 0 deletions packages/api/src/dev-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import z from 'zod';

import { fetchAndCacheConfig } from './config';
import { logger } from './logger';
import { DEFAULT_PORT, startServer } from './server';

const portSchema = z.number().int().positive();

const startDevServer = async () => {
const config = await fetchAndCacheConfig();
logger.info('Using configuration', config);

const parsedPort = portSchema.safeParse(process.env.DEV_SERVER_PORT);
let port: number;
if (parsedPort.success) {
port = parsedPort.data;
logger.debug('Using DEV_SERVER_PORT environment variable as port number.', {
port,
});
} else {
port = DEFAULT_PORT;
logger.debug('DEV_SERVER_PORT environment variable not set or invalid. Using default port.', {
port,
});
}

startServer(config, port);
};

void startDevServer();
Loading