feat: initial POC

Initial POC of an api gateway with cognito authorizer deployed by terraform
main
flavien 2023-06-21 14:39:57 +02:00
parent 4b5da1ccc9
commit ff6bd7bed9
No known key found for this signature in database
24 changed files with 460 additions and 1 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
terraform_api_gateway

1
.task/checksum/build Normal file
View File

@ -0,0 +1 @@
a4f344a1cb3c3e0b077542f62f288858

View File

@ -0,0 +1 @@
a4f344a1cb3c3e0b077542f62f288858

View File

@ -0,0 +1 @@
d53f07fbdb97d1fbe6dd58682cad241a

View File

@ -0,0 +1 @@
f7efe372f54c13c2418d38d14dd5b970

View File

@ -1,3 +1,32 @@
# terraform_api_gateway
API Gateway with terraform for learning!
API Gateway with terraform for learning!
## deployment tasks
⚠️ require [`go-task`](https://taskfile.dev/installation/) and [`jq`](https://jqlang.github.io/jq/)
`task terraform_plan ENV=...` => plan the terraform deployment + create a `tfplan.json` that resume the modifications.
`task terraform_apply ENV=...` => deploy the stack.
`task terraform_destroy ENV=...` => destroy the stack.
configured environments:
- dev
## test commmands
### login to cognito from cli to get IdToken
Run:
`aws cognito-idp initiate-auth --region <...> --auth-flow USER_PASSWORD_AUTH --client-id <...> --auth-parameters USERNAME=<...>,PASSWORD=<...>`
Then copy the `"IdToken"` from response.
### test an api endpoint with cognito authorizer
`curl --header "Authorization: <IdToken>" https://<...>.execute-api.eu-west-1.amazonaws.com/<stage>/<route>`
## TODO
- [ ] create a post route
- [ ] body validation
- [ ] reponse transformation
- [ ] routes module
- [ ] swagger documentation
- [ ] use aws lambda module
## critique du code
Il n'y as pas vraiment de plus value d'avoir une conf de backend par env avec les workspaces.

74
Taskfile.yml Normal file
View File

@ -0,0 +1,74 @@
version: '3'
env:
ENV: "{{.ENV}}"
SERVICES_DIRECTORY: "{{.ROOT_DIR}}/services"
TERRAFORM_DIRECTORY: "{{.ROOT_DIR}}/terraform/code"
CONFIG_DIRECTORY: "../configs"
TFVARS: "{{.CONFIG_DIRECTORY}}/{{.ENV}}/terraform.tfvars"
BACKEND: "{{.CONFIG_DIRECTORY}}/{{.ENV}}/backend.conf"
DEFAULT_VARS: "{{.CONFIG_DIRECTORY}}/_default_values/terraform.tfvars"
TF_IN_AUTOMATION: true
PLAN: plan.tfplan
JSON_PLAN_FILE: tfplan.json
tasks:
check_env:
preconditions:
- sh: "[ $ENV != '' ]"
msg: "Variable ENV is not set"
cmds:
- cmd: 'echo "Using environment: $ENV"'
silent: true
check_jq:
preconditions:
- sh: "command -v jq >/dev/null 2>&1 || { echo >&2 \"jq is not installed. Aborting.\"; exit 1; }"
msg: "jq is not installed"
terraform_init:
deps: [check_env]
dir: "{{.TERRAFORM_DIRECTORY}}"
cmds:
- terraform init -backend-config=${BACKEND} -input=false
- terraform workspace new $ENV || terraform workspace select $ENV
build_services:
dir: "{{.SERVICES_DIRECTORY}}"
cmds:
- zip hello_world.zip hello_world.py
sources:
- hello_world.py
generates:
- hello_world.zip
terraform_plan:
deps: [terraform_init, build_services]
dir: "{{.TERRAFORM_DIRECTORY}}"
sources:
- '*.tf'
- '{{.CONFIG_DIRECTORY}}/_default_values/terraform.tfvars'
- '{{.CONFIG_DIRECTORY}}/{{.ENV}}/terraform.tfvars'
generates:
- '{{.PLAN}}'
- '{{.JSON_PLAN_FILE}}'
cmds:
- terraform fmt -check -diff -recursive
- terraform validate
- terraform plan --var-file=${DEFAULT_VARS} --var-file=${TFVARS} -out=${PLAN} -input=false
- terraform show --json ${PLAN} | jq -r '([.resource_changes[]?.change.actions?]|flatten)|{"create":(map(select(.=="create"))|length),"update":(map(select(.=="update"))|length),"delete":(map(select(.=="delete"))|length)}' > ${JSON_PLAN_FILE}
terraform_apply:
deps: [terraform_plan]
dir: "{{.TERRAFORM_DIRECTORY}}"
sources:
- '{{.PLAN}}'
cmds:
- terraform apply --var-file=${DEFAULT_VARS} --var-file=${TFVARS} -input=false -auto-approve
terraform_destroy:
deps: [terraform_init]
dir: "{{.TERRAFORM_DIRECTORY}}"
cmds:
- terraform destroy --var-file=${DEFAULT_VARS} --var-file=${TFVARS} -input=false -auto-approve
- rm ${PLAN}

7
services/hello_world.py Normal file
View File

@ -0,0 +1,7 @@
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps('Hello World!')
}

BIN
services/hello_world.zip Normal file

Binary file not shown.

View File

@ -0,0 +1,25 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "4.44.0"
constraints = "4.44.0"
hashes = [
"h1:IicMBt+WvFATiN4j/oaJYB4Kvk6LCxxpnokv2PXo1ag=",
"zh:08da139140530900ebb07baedd9044b5002f0296f5f160d96783e72080158326",
"zh:2d677b9e4f195481098cec843d0138f3a198f1f93be42c1d1654b71438e2f5ab",
"zh:3cdf4e06b9b8f30f6652a4519d586febc4ab92168a39df2610f06e04a8e6dda7",
"zh:677933957d1de40c8b5ae252c5cd369617af4cb7a26e8f750ad6f175fef4f767",
"zh:6a266ae5488d6daa53bbe6c2cb8368833381eacd9de7f05f059f5100535a0cb2",
"zh:6cdccaab0a444314b10246c8d58b0ffb84d32ddac70e36a12b45eb518e0ae065",
"zh:6ed49c7680298761416d408bc91cd137ae7cff38181fc143b1dfab1a32b44516",
"zh:8403a0fbf439009b3b0c77969c560cf426aeb3d99a78fdd27afbf4694ca0f3e7",
"zh:8ddfd85c6789bca7c66346da1f3488488c20bc4d773cd26669059c437cfbabd6",
"zh:941455d0fa54b0451387cfd611d63766f4b18cb24038f25ee0794097755e654d",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:bc01398c714d621ad9f4e81863446cd8e1135a63a5ff7c36d3fb01ab27e96439",
"zh:ce3b39a43b9c5b34a62047fe2a395b72a62cc0b5dc7e8a5be0f778159ac486d2",
"zh:d5891b82511af25570287578318aaee4fa86e05599cc8c81d44ea9e094f4a728",
"zh:df5329c186545d273f9abaeeb39251d6cfcc446bccc44e27d1d7d02ebf145e2f",
]
}

View File

@ -0,0 +1,12 @@
terraform {
required_version = ">= 1.2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 4.44.0"
}
}
backend "s3" {
key = "test_projects/terraform_api_gateway.tfstate"
}
}

1
terraform/code/_datas.tf Normal file
View File

@ -0,0 +1 @@
data "aws_caller_identity" "current" {}

11
terraform/code/_locals.tf Normal file
View File

@ -0,0 +1,11 @@
locals {
default_tags = {
ManagedBy = "Terraform"
Application = var.application
Environment = var.environment
Region = var.region
}
current_account_id = data.aws_caller_identity.current.account_id
resource_prefix = "${var.application}-${var.environment}-${var.region}"
}

View File

@ -0,0 +1,7 @@
output "invoke_url" {
value = aws_api_gateway_deployment.this.invoke_url
}
output "client_id" {
value = aws_cognito_user_pool_client.client.id
}

View File

@ -0,0 +1,11 @@
provider "aws" {
region = var.region
assume_role {
role_arn = var.role_arn
}
default_tags {
tags = local.default_tags
}
}

View File

@ -0,0 +1,56 @@
variable "application" {
description = "Application name"
type = string
}
variable "role_arn" {
description = "Role arn used by the provider"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "region" {
description = "Project name"
type = string
}
variable "services_path" {
description = "Path to the services folder"
type = string
}
variable "xray_tracing_enabled" {
description = "Enables the XRay tracing and will create the necessary IAM permissions "
type = bool
default = false
}
variable "api_throttling_burst_limit" {
description = "API Gateway total concurrent connections allowed for all API's within a REST endpoint"
}
variable "api_throttling_rate_limit" {
description = "API Gateway total requests across all API's within a REST endpoint"
}
variable "api_metrics_enabled" {
description = "Enables detailed API Gateway metrics"
type = bool
default = false
}
variable "api_logging_level" {
description = " (Optional) Specifies the logging level for this method, which effects the log entries pushed to Amazon CloudWatch Logs. The available levels are OFF, ERROR, and INFO."
type = string
default = "OFF"
}
variable "api_data_trace_enabled" {
description = "(Optional) Specifies whether data trace logging is enabled for this method, which effects the log entries pushed to Amazon CloudWatch Logs."
type = bool
default = false
}

127
terraform/code/api_gtw.tf Normal file
View File

@ -0,0 +1,127 @@
resource "aws_api_gateway_rest_api" "this" {
name = "${local.resource_prefix}-api-gateway"
}
resource "aws_api_gateway_deployment" "this" {
rest_api_id = aws_api_gateway_rest_api.this.id
triggers = {
redeployment = timestamp()
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_method.hello_world,
aws_api_gateway_integration.hello_world,
# aws_api_gateway_method.method, aws_api_gateway_method.options_method,
# aws_api_gateway_method_response.method_response, aws_api_gateway_method_response.options_200,
# aws_api_gateway_integration.integration, aws_api_gateway_integration.options_integration,
# aws_api_gateway_integration_response.options_integration_response,
# aws_api_gateway_authorizer.api_authorizer,
# aws_iam_role.invocation_role, aws_iam_role_policy.invocation_policy,
# aws_acm_certificate.ssl_certificate, aws_route53_record.cert_validation,
# aws_acm_certificate_validation.ssl_certificate_validation, aws_api_gateway_domain_name.this,
# aws_route53_record.api, aws_api_gateway_gateway_response.unauthorized_response, module.apigateway_resources,
]
}
resource "aws_api_gateway_stage" "this" {
stage_name = var.environment
rest_api_id = aws_api_gateway_rest_api.this.id
deployment_id = aws_api_gateway_deployment.this.id
xray_tracing_enabled = var.xray_tracing_enabled
tags = {
Name = local.resource_prefix
}
depends_on = [
aws_api_gateway_method.hello_world,
aws_api_gateway_integration.hello_world,
# aws_api_gateway_method.method, aws_api_gateway_method.options_method,
# aws_api_gateway_method_response.method_response, aws_api_gateway_method_response.options_200,
# aws_api_gateway_integration.integration, aws_api_gateway_integration.options_integration,
# aws_api_gateway_integration_response.options_integration_response,
# aws_api_gateway_authorizer.api_authorizer,
# aws_iam_role.invocation_role, aws_iam_role_policy.invocation_policy,
# aws_acm_certificate.ssl_certificate, aws_route53_record.cert_validation,
# aws_acm_certificate_validation.ssl_certificate_validation, aws_api_gateway_domain_name.this,
# aws_route53_record.api, aws_api_gateway_gateway_response.unauthorized_response, module.apigateway_resources
]
}
resource "aws_api_gateway_method_settings" "this" {
rest_api_id = aws_api_gateway_rest_api.this.id
stage_name = aws_api_gateway_stage.this.stage_name
method_path = "*/*"
settings {
throttling_burst_limit = var.api_throttling_burst_limit
throttling_rate_limit = var.api_throttling_rate_limit
metrics_enabled = var.api_metrics_enabled
logging_level = var.api_logging_level
data_trace_enabled = var.api_data_trace_enabled
}
}
resource "aws_iam_role" "cloudwatch" {
name = "api_gateway_cloudwatch_global"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "cloudwatch" {
name = "default"
role = aws_iam_role.cloudwatch.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"logs:GetLogEvents",
"logs:FilterLogEvents"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_api_gateway_account" "this" {
cloudwatch_role_arn = aws_iam_role.cloudwatch.arn
}
resource "aws_api_gateway_authorizer" "this" {
name = "${local.resource_prefix}-authorizer"
type = "COGNITO_USER_POOLS"
rest_api_id = aws_api_gateway_rest_api.this.id
provider_arns = ["${aws_cognito_user_pool.pool.arn}"]
}

View File

@ -0,0 +1,23 @@
resource "aws_api_gateway_resource" "hello_world" {
rest_api_id = aws_api_gateway_rest_api.this.id
parent_id = aws_api_gateway_rest_api.this.root_resource_id
path_part = "hello_world"
}
resource "aws_api_gateway_method" "hello_world" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.this.id
# authorization_scopes = ["${aws_cognito_resource_server.resource_server.scope_identifiers}"]
}
resource "aws_api_gateway_integration" "hello_world" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello_world.invoke_arn
}

24
terraform/code/cognito.tf Normal file
View File

@ -0,0 +1,24 @@
resource "aws_cognito_user_pool" "pool" {
name = "${local.resource_prefix}-user-pool"
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
recovery_mechanism {
name = "verified_phone_number"
priority = 2
}
}
}
resource "aws_cognito_user_pool_client" "client" {
name = "${local.resource_prefix}-client"
user_pool_id = aws_cognito_user_pool.pool.id
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
}

36
terraform/code/lambdas.tf Normal file
View File

@ -0,0 +1,36 @@
resource "aws_iam_role" "role" {
name = "LambdaRole"
assume_role_policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
POLICY
}
resource "aws_lambda_function" "hello_world" {
filename = "${var.services_path}/hello_world.zip"
function_name = "hello_world"
role = aws_iam_role.role.arn
handler = "hello_world.lambda_handler"
runtime = "python3.9"
source_code_hash = filebase64sha256("${var.services_path}/hello_world.zip")
}
resource "aws_lambda_permission" "apigw_lambda" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_world.function_name
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:${var.region}:${local.current_account_id}:${aws_api_gateway_rest_api.this.id}/*/${aws_api_gateway_method.hello_world.http_method}${aws_api_gateway_resource.hello_world.path}"
statement_id = "AllowExecutionFromAPIGateway"
}

0
terraform/code/main.tf Normal file
View File

BIN
terraform/code/plan.tfplan Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
{
"create": 16,
"update": 0,
"delete": 0
}

View File

@ -0,0 +1,6 @@
acl = "bucket-owner-full-control"
bucket = "flavien-revolve-terraform-tfstate"
workspace_key_prefix = "workspace"
region = "eu-west-1"
profile = "revolve"
dynamodb_table = "flavien-revolve-terraform-tfstate"