Intro
You will create a simple Node.js HTTP API with 2 endpoints:
- POST
/post
to create a new post - GET
/posts
to get all the existing posts
- POST
Your application code will run in a fully managed, scalable and self-healing container workload.
The data will be stored in a Postgres database.
To simplify database access and migrations, this tutorial uses Prisma (next-gen ORM).
This tutorial uses YAML to write the configuration, but you can also use javascript or typescript.
Pricing
Two resources will incur costs (least expensive options are used):
- Container workload (~$0.012/hour, ~$9/month)
- Relational (SQL) database ($0.017/hour, ~$12.5/month, free-tier eligible)
There are also other resources that might incur costs (with pay-per-use pricing). If your load won't get high, the costs will be close to 0. You can also control your cloud spend using budget control.
1. Configure your stack
Start with an empty project directory. Call it whatever you like.
To deploy your application, you need to configure your "stack". The stack consists of an application code and the
infrastructure required to run it. You can deploy your stack to multiple environments (stages) - for example
production
, staging
or testing
.
1.1 Add a configuration file
To configure the stack, Stacktape requires you to write a simple configuration.
Start by creating a file called stacktape.yml
in the root directory of your project. In this tutorial, you will use
YAML to write your configuration, but Stacktape supports more options (JSON, Javascript and Typescript).
1.2 Configure service name
You can choose an arbitrary name, for example posts-api-pg
.
Copy
serviceName: posts-api-pg
1.3 Create infrastructure resources
This project requires 3 stacktape resources:
- HTTP API Gateway will receive requests and route them to your container.
- Relational (SQL) database with Postgres engine will store the posts data.
- Container workload with a single container "api-container" that will run your code.
1.3.1 Add HTTP API Gateway
First, configure an HTTP API Gateway and call it mainApiGateway. You can also allow CORS.
Copy
serviceName: posts-api-pgresources:mainApiGateway:type: http-api-gatewayproperties:cors:enabled: true
1.3.2 Add relational database
Now, add a relational-database (SQL database) resource. You need to configure a few things:
Database credentials. For now, input them directly. For production workloads, you should use secrets to store them securely.
Engine type. Use
postgres
engine. It uses a single-node database server - simplest and cheapest option.Instance size. Use the
db.t2.micro
instance. It has 1 vCPU, 1GB of memory and is free-tier eligible (~$12.5/month without a free tier). To see the full list of available options, refer to AWS instance type list.
By default, the version used for your database is the latest AWS-supported stable version (currently 13.4
). Minor
version upgrades are done automatically.
You can also configure many other aspects of your database, such as storage, logging, read replicas, or failover instances.
Copy
serviceName: posts-api-pgresources:mainApiGateway:type: http-api-gatewayproperties:cors:enabled: truemainDatabase:type: relational-databaseproperties:credentials:masterUserName: admin_usermasterUserPassword: my_secret_passwordengine:type: postgresproperties:primaryInstance:instanceSize: db.t2.micro
1.3.3 Add container workload
Finally, add a container workload that will run your code. You need to configure several things:
- Container. This container workload will use only a single container:
api-container
. You need to configure 3 things:- Packaging - determines how the Docker container image will be built. The easiest and most optimized way to build
the image from a Typescript application is using
stacktape-image-buildpack
. You only need to configureentryfilePath
. Stacktape will automatically transpile and build your application code with all of its dependencies, build the Docker image, and push it to a pre-created image repository on AWS. You can also use other types of packaging. - Database connection string - you can pass it to the container as an environment variable. You can easily
reference it using a $ResourceParam() directive. This directive accepts
a resource name (
mainDatabase
in this case) and the name of the relational database referenceable parameter (connectionString
in this case). If you want to learn more, refer to referencing parameters guide and directives guide. - Events that will be able to reach your container. By configuring path to
/{proxy+}
, method to'*'
and containerPort to3000
, the event integration will route all requests (no matter the method or path) coming to the HTTP Api Gateway to port3000
of your container.
- Packaging - determines how the Docker container image will be built. The easiest and most optimized way to build
the image from a Typescript application is using
- Resources. The cheapest available configuration is
0.25
of virtual CPU and512
MB of RAM.
You can also configure scaling. New (parallel) container workload instance can be added when the utilization of your CPU or RAM gets larger than 80%. The HTTP API Gateway will evenly distribute the traffic to all container workloads.
Copy
serviceName: posts-api-pgresources:mainApiGateway:type: http-api-gatewayproperties:cors:enabled: truemainDatabase:type: relational-databaseproperties:credentials:masterUserName: admin_usermasterUserPassword: my_secret_passwordengine:type: postgresproperties:primaryInstance:instanceSize: db.t2.microapiServer:type: container-workloadproperties:resources:cpu: 0.25memory: 512containers:- name: api-containerpackaging:type: stacktape-image-buildpackproperties:entryfilePath: ./src/index.tsenvironment:- name: DB_CONNECTION_STRINGvalue: $ResourceParam('mainDatabase', 'connectionString')- name: PORTvalue: 3000events:- type: http-api-gatewayproperties:containerPort: 3000httpApiGatewayName: mainApiGatewaymethod: "*"path: /{proxy+}
1.4 Add Prisma
To simplify database access and migrations, you can use Prisma. If you're not familiar with it, don't worry - it's very simple.
First, initialize your project directory as a javascript project.
Copy
npm init
Next, install 2 packages: @prisma/client
and prisma
.
Copy
npm install @prisma/client prisma
or
Copy
yarn add @prisma/client prisma
Now, let's create a Prisma schema at
prisma/schema.prisma
in your project directory.
The schema declares a single model called Post
.
It also configures prisma to use database specified by its url (taken from the DB_CONNECTION_STRING
environment
variable in our case). This database url will be used for both migrations and queries.
Copy
generator client {provider = "prisma-client-js"binaryTargets = ["native", "linux-musl"]}datasource db {provider = "postgres"url = env("DB_CONNECTION_STRING")}model Post {id Int @id @default(autoincrement())createdAt DateTime @default(now())updatedAt DateTime @updatedAttitle String @uniquecontent StringauthorEmail String}
Prisma generates a database client that you can then import from your code. To generate it, use the
npx prisma generate
command. To do it automatically every time before the stack is deployed, you can save the command
as a script and then use it inside a before:deploy
hook.
Copy
serviceName: posts-api-pgscripts:generatePrismaClient:executeCommand: npx prisma generatehooks:- triggers: ["before:deploy"]scriptName: generatePrismaClientresources:mainApiGateway:type: http-api-gatewayproperties:cors:enabled: truemainDatabase:type: relational-databaseproperties:credentials:masterUserName: admin_usermasterUserPassword: my_secret_passwordengine:type: postgresproperties:primaryInstance:instanceSize: db.t2.microapiServer:type: container-workloadproperties:resources:cpu: 0.25memory: 512containers:- name: api-containerpackaging:type: stacktape-image-buildpackproperties:entryfilePath: ./src/index.tsenvironment:- name: DB_CONNECTION_STRINGvalue: $ResourceParam('mainDatabase', 'connectionString')- name: PORTvalue: 3000events:- type: http-api-gatewayproperties:containerPort: 3000httpApiGatewayName: mainApiGatewaymethod: "*"path: /{proxy+}
To sync your Prisma schema with the database, you can use npx prisma db push
command. You can't do this before the
database is created, so you should use the after:deploy
hook.
You also need to pass the DB_CONNECTION_STRING
environment variable to the script. You can do it using the
$ResourceParam() directive that will automatically download the connection
string value and pass it to the script.
Copy
serviceName: posts-api-pgscripts:generatePrismaClient:executeCommand: npx prisma generatemigrateDb:executeCommand: npx prisma db push --skip-generateenvironment:- name: DB_CONNECTION_STRINGvalue: $ResourceParam('mainDatabase', 'connectionString')hooks:- triggers: ["before:deploy"]scriptName: generatePrismaClient- triggers: ["after:deploy"]scriptName: migrateDbresources:mainApiGateway:type: http-api-gatewayproperties:cors:enabled: truemainDatabase:type: relational-databaseproperties:credentials:masterUserName: admin_usermasterUserPassword: my_secret_passwordengine:type: postgresproperties:primaryInstance:instanceSize: db.t2.microapiServer:type: container-workloadproperties:resources:cpu: 0.25memory: 512containers:- name: api-containerpackaging:type: stacktape-image-buildpackproperties:entryfilePath: ./src/index.tsenvironment:- name: DB_CONNECTION_STRINGvalue: $ResourceParam('mainDatabase', 'connectionString')- name: PORTvalue: 3000events:- type: http-api-gatewayproperties:containerPort: 3000httpApiGatewayName: mainApiGatewaymethod: "*"path: /{proxy+}
2. Write application code
2.1 Install dependencies
First we need to install some dependencies.
Copy
npm install express body-parser @types/express
or
Copy
yarn add express body-parser @types/express
2.2 Api-container application code
Create a new typescript file at ./src/index.ts
. For simplicity, let's put everything into a single file.
Copy
import express from "express";import { json } from "body-parser";import { PrismaClient } from "@prisma/client";// Instantiate the Prisma client.const prisma = new PrismaClient();const app = express();app.use(json());app.get("/posts", async (req, res) => {try {const posts = await prisma.post.findMany();res.send({ message: "success", data: posts });} catch (error) {// If anything goes wrong, log the error.// You can later access the log data in the AWS console.console.error(error);res.status(400).send({ message: "error", error: error.message });}});app.post("/post", async (req, res) => {try {// save post data to the database using the Prisma clientconst postData = await prisma.post.create({data: {title: req.body.title,content: req.body.content,authorEmail: req.body.authorEmail}});res.send({ message: "success", data: postData });} catch (error) {console.error(error);res.status(400).send({ message: "error", error: error.message });}});app.listen(process.env.PORT, () => {console.info(`Api container started. Listening on port ${process.env.PORT}`);});
You don't need to configure anything else. Stacktape will automatically transpile Typescript and bundle your source code with all of the required dependencies.
3. Deployment
To deploy your application (stack), all you need is a single command:
Copy
stacktape deploy --stage <<stage>> --region <<region>>
- stage is an arbitrary name of your environment (for example staging, production or dev-john)
- region is the AWS region, where your stack will be deployed to. All the available regions are listed below.
Region name & Location | code |
---|---|
Europe (Ireland) | eu-west-1 |
Europe (London) | eu-west-2 |
Europe (Frankfurt) | eu-central-1 |
Europe (Milan) | eu-south-1 |
Europe (Paris) | eu-west-3 |
Europe (Stockholm) | eu-north-1 |
US East (Ohio) | us-east-2 |
US East (N. Virginia) | us-east-1 |
US West (N. California) | us-west-1 |
US West (Oregon) | us-west-2 |
Canada (Central) | ca-central-1 |
Africa (Cape Town) | af-south-1 |
Asia Pacific (Hong Kong) | ap-east-1 |
Asia Pacific (Mumbai) | ap-south-1 |
Asia Pacific (Osaka-Local) | ap-northeast-3 |
Asia Pacific (Seoul) | ap-northeast-2 |
Asia Pacific (Singapore) | ap-southeast-1 |
Asia Pacific (Sydney) | ap-southeast-2 |
Asia Pacific (Tokyo) | ap-northeast-1 |
China (Beijing) | cn-north-1 |
China (Ningxia) | cn-northwest-1 |
Middle East (Bahrain) | me-south-1 |
South America (São Paulo) | sa-east-1 |
The deployment progress is printed to the terminal.
The deployment will take ~20 minutes. Subsequent deploys will be way faster.
3.1 Deploying only source code
Deployment using the deploy command uses AWS CloudFormation under the hood.
This gives a lot of guarantees and convenience, but can be slow for certain use-cases.
If you only want to deploy your source code (not any configuration changes), you can use cw:deploy-fast. The deployment will be significantly faster (~60-120 seconds).
Copy
stacktape cw:deploy-fast --stage <<stage>> --region <<region>> --resourceName nextJsApp
4. After deployment
4.1 Explore your application
If the deployment is successful, some information about the deployed stack will be printed to the console. For instance:
- stack URL is a link to AWS CloudFormation console of your stack. As you can see, more than 40 resources are required to run just a simple API.
- apiServer -> logs-api-container is a link to AWS console where you can see logs produced by your container.
- mainDatabase -> logs is a link to AWS console where you can see the logs produced by your database.
- mainApiGateway -> logs is a link to AWS console where you can see the API Gateway access logs.
- mainApiGateway -> url is the URL of the deployed HTTP API Gateway. Save this, you will need it in the next step.
You will also see links to metrics
of your resources (such as CPU/memory usage of your database, etc.).
Furthermore, detailed information about the stack and deployed resources is saved to
.stacktape-stack-info/<<stack-name>>
.
4.2 Test the application
Make a
POST
request to<<your_http_api_gateway_url>>/post
with the JSON data in its body to save the post. You can use a the following cURL commands (replace<<your_http_api_gateway_url>>
with yourmainApiGateway -> url
):Copy
curl -X POST <<your_http_api_gateway_url>>/post -H 'content-type: application/json' -d '{ "title": "MyPost", "content": "Hello!", "authorEmail": "info@stacktape.com"}'If the above command did not work, try escaping the JSON content, or use your preferred HTTP client.
Copy
curl -X POST <<your_http_api_gateway_url>>/post -H 'content-type: application/json' -d '{ \"title\":\"MyPost\",\"content\":\"Hello!\",\"authorEmail\":\"info@stacktape.com\"}'
Make a
GET
request to<<your_http_api_gateway_url>>/posts
to fetch all posts.Copy
curl <<your_http_api_gateway_url>>/posts
5. Next steps
5. a) Iteratively develop your application code
Stacktape allows you to easily develop your application in a "development" mode.
To develop a container workload, you can use the cw:run-local command. For example, to
develop and debug container api-container
within apiServer
container workload, you can use
Copy
stacktape cw:run-local --region eu-west-1 --stage <<your-previously-deployed-stage>> --resourceName apiServer --container api-container
This will run the container locally, map all of the container ports specified in the events
section to the host
machine, and pretty-print logs produced by the container.
Stacktape emulates the workload as closely to the deployed version as possible. It does 2 things:
- Injects parameters referenced in the environment variables by
$ResourceParam
and$Secret
directives to the workload. - Injects credentials of the assumed role to the workload. This means that your locally running container will have the exact same IAM permissions as the deployed version.
The container is rebuilt and restarted, when you either:
- type
rs + enter
to the terminal - use the
--watch
option and one of your source code files changes
5. b) Evolve your stack
Stacktape allows you to do much more things with your stack. For instance:
- Add a CDN to cache
GET
requests coming to your HTTP API Gateway at an edge location. This can decrease load on your API and improve response times. - Create a CI/CD pipeline to deploy your application.
- Add a custom domain name to your HTTP API Gateway.
- Add a dynamic behavior to your configuration file using a custom directive. You can write it using Javascript, Typescript or Python.
- Add a storage bucket to store files.
- Add a Redis cluster to cache requests to the database. It can improve the response times and decrease the load on your database.
- Use a different relational database engine. For instance, you can use one
of
aurora
engines to increase performance, reliability and fault-tolerance. - Store your sensitive data (such as database credentials) as secrets.
- Add a userpool to store, authenticate and authorize users.
- Add a policy. For instance, you can allow only 2 stages for your application:
staging
andproduction
. - Use a completely different database type. For instance DynamoDb or MongoDb.
- Add a progress notification to get notified in Slack or MS Teams when the deployment finishes or fails.
5. c) Delete your stack
If you no longer want to use your stack, you can delete it. Stacktape will automatically delete every resource and deployment artifact associated with your stack.
Copy
stacktape delete --stage <<stage>> --region <<region>>