visit
In part 1, I went through the basics of setting up the back-end for the infrastructure state tracking and deploying it to AWS. This time, I’ll focus on creating the remaining part, which is API. So without further ado, let's get started.
For demonstration purposes, I’ll create /users
API endpoint that will allow performing CRUD operations on it. I’ll start from the iac/api
directory where all Lambdas, persistence layer and API gateway will be defined.
Same drill as in part 1. Variables are located in variables.tf
, computed variables in locals.tf
and main assembly point in main.tf
file. Let’s review main.tf
as everything else is very similar to module from part 1.
terraform {
backend "s3" {
region = "eu-central-1"
bucket = "project-123-remote-state"
key = "project-123-remote-state.tfstate"
dynamodb_table = "project-123-tf-statelock"
}
}
1) Using configuration file:
Create a configuration file, usually, each environment will have its own file, and populate it with variables:# iac/api/backend_config
region = "eu-central-1"
bucket = "project-123-remote-state"
key = "project-123-remote-state.tfstate"
dynamodb_table = "project-123-tf-statelock"
terraform init -backend-config=backend_config
2) Using command line arguments:
terraform init -backend-config="region=$REGION,bucket=$BUCKET,key=$KEY,dynamodb_table=$DDB_TABLE"
Regardless of the option, terraform.backend
block can be left empty and the whole module initiated with dynamic values.
data "template_file" "lambda_policy" {
template = file("templates/lambda_policy.json")
}
data "template_file" "lambda_role" {
template = file("templates/lambda_role.json")
}
resource "aws_iam_policy" "policy" {
name = "${local.env}-${var.prefix}-policy"
description = "policy to allow lambda use specified resources"
policy = data.template_file.lambda_policy.rendered
}
resource "aws_iam_role" "role" {
name = "${local.env}-${var.prefix}-role"
assume_role_policy = data.template_file.lambda_role.rendered
}
resource "aws_iam_role_policy_attachment" "policy_attachment" {
role = aws_iam_role.role.name
policy_arn = aws_iam_policy.policy.arn
}
As you can see from the above snippet, I like to keep my policy declarations in separate .json
files so that main.tf
stays DRY.
module "dynamodb_table" {
source = "terraform-aws-modules/dynamodb-table/aws"
name = local.ddb_users_table
hash_key = "username"
attributes = [
{
name = "username"
type = "S"
}
]
tags = {
Env = local.env
}
}
There’s nothing really special about it except that I’ll be using username
as a hash key to uniquely identify my documents. If you need more info on Dynamodb, I find very useful.
Next, I define lambdas needed to perform CRUD operations on /users
resource. I’ll briefly walk you through one lambda definition as remaining Lambdas look exactly the same and only function names and descriptions are changing:
module "create_user_lambda" {
source = "../modules/aws/lambda"
function_name = "create_user"
lambda_path = var.lambda_path
description = "create user lambda, part of /users resource CRUD to handle user creation"
role_arn = aws_iam_role.role.arn
environment = {
ENV = local.env
REGION = var.region
DDB_TABLE_NAME = local.ddb_users_table
}
tags = {
Env = local.env
}
}
The first thing worth mentioning is that I use a custom Lambda module as a source. You can check it out . It helps keep my Lambda definitions DRY by abstracting common functionality and setting configuration values that I would otherwise have to type in over and over again. Each time I’ll run terraform plan
and terraform apply
, this module will use function_name
and lambda_path
values to allocate, build and deploy my Lambdas if there’s a change detected in the source.
data "template_file" "apigw_policy" {
template = file("${path.module}/templates/apigw_policy.json")
}
data "template_file" "api_spec" {
template = file("templates/api.yaml")
vars = {
role_arn = aws_iam_role.role.arn
region = var.region
create_user_lambda_arn = module.create_user_lambda.function_arn
update_user_lambda_arn = module.update_user_lambda.function_arn
get_user_lambda_arn = module.get_user_lambda.function_arn
delete_user_lambda_arn = module.delete_user_lambda.function_arn
}
}
resource "aws_api_gateway_rest_api" "rest_api" {
name = "serverless-api"
description = "serverless-api"
body = data.template_file.api_spec.rendered
policy = data.template_file.apigw_policy.rendered
}
resource "aws_api_gateway_deployment" "client-example-api" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
stage_name = var.api_version
depends_on = [aws_api_gateway_rest_api.rest_api]
variables = {
api_version = md5(file("${path.module}/templates/api.yaml"))
}
lifecycle {
create_before_destroy = true
}
}
There are a few things happening here, but the most interesting one is open API spec. You can check it . As with policies, I’m placing it in a separate templates/api.yaml
file just to keep things more organised. Note that this time, when I’m using API spec template file, I’m passing in a few variables. This is mainly to tell each resource which lambda to trigger when it gets called.
An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.
So what terraform will do, it will take my spec and create API Gateway in AWS. This is done by assigning rendered spec template to aws_api_gateway_rest_api.rest_api.body
. It also makes it very easy to design REST resources as most code editors have swagger extensions that allow previewing changes in real-time and also ensure that the spec is valid.
export AWS_ACCESS_KEY_ID=******
export AWS_SECRET_ACCESS_KEY=******
cd /iac/api/
terraform plan
terraform apply --auto-approve
/iac/api/outputs.tf
file contains properties that will be printed out after terraform apply
finishes running. If all goes well, I should see an output similar to this:
invoke_url = "//dorb127v21.execute-api.eu-central-1.amazonaws.com/v1"
Now with that url in place, I can use curl
to test if everything’s working as expected:
# create document
curl -X POST //dorb127v21.execute-api.eu-central-1.amazonaws.com/v1/users \
-H 'Content-Type: application/json' \
-d '{"username": "foo"}'
# get document
curl -X GET //dorb127v21.execute-api.eu-central-1.amazonaws.com/v1/users/foo
# update document
curl -X PUT //dorb127v21.execute-api.eu-central-1.amazonaws.com/v1/users/foo \
-H 'Content-Type: application/json' \
-d '{"fname": "bar", "lname": "baz", "age": 100}'
# delete document
curl -X DELETE //dorb127v21.execute-api.eu-central-1.amazonaws.com/v1/users/foo