From 51783f9262f3600cab6f22e9ed35add53dc6885b Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Wed, 16 Oct 2024 17:31:12 +0200 Subject: [PATCH 1/8] Added possibility to deploy sample example with terraform --- .env.example | 3 + .gitignore | 27 +- Makefile | 34 ++ README.md | 47 ++- bin/build_lambdas.sh | 28 ++ bin/deploy.sh | 26 +- deployment/policies/lambda.json | 12 + deployment/policies/lambda_ssm.json | 14 + .../policies/list_lambda_s3_buckets.json.tpl | 18 + .../presign_lambda_s3_buckets.json.tpl | 17 + .../resize_lambda_s3_buckets.json.tpl | 17 + .../policies/resize_lambda_sns.json.tpl | 17 + .../policies/website_bucket_policy.json.tpl | 13 + deployment/terraform/main.tf | 356 ++++++++++++++++++ 14 files changed, 588 insertions(+), 41 deletions(-) create mode 100644 .env.example create mode 100644 Makefile create mode 100755 bin/build_lambdas.sh create mode 100644 deployment/policies/lambda.json create mode 100644 deployment/policies/lambda_ssm.json create mode 100644 deployment/policies/list_lambda_s3_buckets.json.tpl create mode 100644 deployment/policies/presign_lambda_s3_buckets.json.tpl create mode 100644 deployment/policies/resize_lambda_s3_buckets.json.tpl create mode 100644 deployment/policies/resize_lambda_sns.json.tpl create mode 100644 deployment/policies/website_bucket_policy.json.tpl create mode 100644 deployment/terraform/main.tf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0249c39 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +LOCALSTACK_AUTH_TOKEN=YOUR_TOKEN +AWS_DEFAULT_REGION=us-east-1 +# LOCAL_RUN=false # Default is true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7419686..5471905 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,29 @@ volume/ # lambda packages lambdas/*/package/ -lambdas/*/lambda.zip \ No newline at end of file +lambdas/*/lambda.zip +terraform/*/*.zip + +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +*.tfvars +*.tfvars.json + +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +.terraform.tfstate.lock.info + +.terraformrc +terraform.rc \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1731e05 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +export AWS_DEFAULT_REGION=us-east-1 +export AWS_ACCESS_KEY_ID ?= test +export AWS_SECRET_ACCESS_KEY ?= test +export LOCAL_RUN ?= true +SHELL := /bin/bash + +include .env + +build: + bin/build_lambdas.sh; + +awslocal-setup: + bin/deploy.sh + +terraform-setup: + $(MAKE) build + cd deployment/terraform; \ + tflocal init; \ + echo "Deploying Terraform configuration 🚀"; \ + tflocal apply --auto-approve -var="local_run=${LOCAL_RUN}"; \ + echo "Paste the function URLs above to the WebApp 🎉"; + +terraform-destroy: + cd deployment/terraform; \ + tflocal destroy --auto-approve; + +start: + LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d + +stop: + @echo + localstack stop + +.PHONY: build awslocal-setup terraform-setup terraform-destroy start stop diff --git a/README.md b/README.md index aa18679..f7a0965 100644 --- a/README.md +++ b/README.md @@ -69,22 +69,37 @@ source .venv/bin/activate pip install -r requirements-dev.txt ``` +## Instructions + +You can set up and deploy the sample application on LocalStack by executing the commands in our Makefile. First, create a `.env` file using the provided `.env.example` file as a template, and include your LocalStack token in it. Then, run `make -s start` to initiate LocalStack on your machine. + +Next, execute `make -s terraform-setup` to provision the infrastructure on LocalStack using Terraform CLI and its scripts. Alternatively, run `make -s awslocal-setup` to set up the infrastructure with the local AWS CLI. + +If you prefer, you can also follow these step-by-step instructions for a manual deployment. + ### LocalStack -Start LocalStack Pro with the appropriate CORS configuration for the S3 Website: +Start LocalStack Pro with Auth Token: ```bash -LOCALSTACK_AUTH_TOKEN=... localstack start +LOCALSTACK_AUTH_TOKEN=... localstack start (-d) ``` -## Instructions +### Terraform + +To create the infrastructure using Terraform, run the following commands: + +```shell +cd deployment/terraform +tflocal init +tflocal apply --auto-approve +``` -You can create the AWS infrastructure on LocalStack by running `bin/deploy.sh`. -Make sure you have Python 3.11 activated before running the script. +We are using the `tflocal` wrapper to configure the local service endpoints, and send the API requests to LocalStack, instead of AWS. -Here are instructions to deploy it manually step-by-step. +### AWS CLI -### Create the buckets +#### Create the buckets The names are completely configurable via SSM: @@ -93,14 +108,14 @@ awslocal s3 mb s3://localstack-thumbnails-app-images awslocal s3 mb s3://localstack-thumbnails-app-resized ``` -### Put the bucket names into the parameter store +#### Put the bucket names into the parameter store ```bash awslocal ssm put-parameter --name /localstack-thumbnail-app/buckets/images --type "String" --value "localstack-thumbnails-app-images" awslocal ssm put-parameter --name /localstack-thumbnail-app/buckets/resized --type "String" --value "localstack-thumbnails-app-resized" ``` -### Create the DLQ Topic for failed lambda invokes +#### Create the DLQ Topic for failed lambda invokes ```bash awslocal sns create-topic --name failed-resize-topic @@ -115,9 +130,9 @@ awslocal sns subscribe \ --notification-endpoint my-email@example.com ``` -### Create the lambdas +#### Create the lambdas -#### S3 pre-signed POST URL generator +##### S3 pre-signed POST URL generator This Lambda is responsible for generating pre-signed POST URLs to upload files to an S3 bucket. @@ -143,7 +158,7 @@ awslocal lambda create-function-url-config \ Copy the `FunctionUrl` from the response, you will need it later to make the app work. -### Image lister lambda +#### Image lister lambda The `list` Lambda is very similar: @@ -166,7 +181,7 @@ awslocal lambda create-function-url-config \ --auth-type NONE ``` -### Resizer Lambda +#### Resizer Lambda ```bash ( @@ -189,7 +204,7 @@ awslocal lambda create-function \ --environment Variables="{STAGE=local}" ``` -### Connect the S3 bucket to the resizer lambda +#### Connect the S3 bucket to the resizer lambda ```bash awslocal s3api put-bucket-notification-configuration \ @@ -197,7 +212,7 @@ awslocal s3api put-bucket-notification-configuration \ --notification-configuration "{\"LambdaFunctionConfigurations\": [{\"LambdaFunctionArn\": \"$(awslocal lambda get-function --function-name resize | jq -r .Configuration.FunctionArn)\", \"Events\": [\"s3:ObjectCreated:*\"]}]}" ``` -### Create the static s3 webapp +#### Create the static s3 webapp ```bash awslocal s3 mb s3://webapp @@ -205,7 +220,7 @@ awslocal s3 sync --delete ./website s3://webapp awslocal s3 website s3://webapp --index-document index.html ``` -### Using the application +#### Using the application Once deployed, visit http://webapp.s3-website.localhost.localstack.cloud:4566 diff --git a/bin/build_lambdas.sh b/bin/build_lambdas.sh new file mode 100755 index 0000000..5dc6c45 --- /dev/null +++ b/bin/build_lambdas.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +(cd lambdas/presign; rm -f lambda.zip; zip lambda.zip handler.py) + +(cd lambdas/list; rm -f lambda.zip; zip lambda.zip handler.py) + +os=$(uname -s) +if [ "$os" == "Darwin" ]; then + ( + cd lambdas/resize + rm -rf libs lambda.zip + docker run --platform linux/x86_64 --rm -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.11" /bin/sh -c "pip3 install -r requirements.txt -t libs; exit" + + cd libs && zip -r ../lambda.zip . && cd .. + zip lambda.zip handler.py + rm -rf libs + ) +else + ( + cd lambdas/resize + rm -rf package lambda.zip + mkdir package + pip3 install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: -t package + zip lambda.zip handler.py + cd package + zip -r ../lambda.zip *; + ) +fi \ No newline at end of file diff --git a/bin/deploy.sh b/bin/deploy.sh index 37e146b..b2555bd 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -2,6 +2,8 @@ export AWS_DEFAULT_REGION=us-east-1 +bin/build_lambdas.sh + awslocal s3 mb s3://localstack-thumbnails-app-images awslocal s3 mb s3://localstack-thumbnails-app-resized @@ -14,7 +16,6 @@ awslocal sns subscribe \ --protocol email \ --notification-endpoint my-email@example.com -(cd lambdas/presign; rm -f lambda.zip; zip lambda.zip handler.py) awslocal lambda create-function \ --function-name presign \ --runtime python3.11 \ @@ -30,7 +31,6 @@ awslocal lambda create-function-url-config \ --function-name presign \ --auth-type NONE -(cd lambdas/list; rm -f lambda.zip; zip lambda.zip handler.py) awslocal lambda create-function \ --function-name list \ --runtime python3.11 \ @@ -46,28 +46,6 @@ awslocal lambda create-function-url-config \ --function-name list \ --auth-type NONE -os=$(uname -s) -if [ "$os" == "Darwin" ]; then - ( - cd lambdas/resize - rm -rf libs lambda.zip - docker run --platform linux/x86_64 --rm -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.11" /bin/sh -c "pip3 install -r requirements.txt -t libs; exit" - - cd libs && zip -r ../lambda.zip . && cd .. - zip lambda.zip handler.py - rm -rf libs - ) -else - ( - cd lambdas/resize - rm -rf package lambda.zip - mkdir package - pip3 install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: -t package - zip lambda.zip handler.py - cd package - zip -r ../lambda.zip *; - ) -fi awslocal lambda create-function \ --function-name resize \ diff --git a/deployment/policies/lambda.json b/deployment/policies/lambda.json new file mode 100644 index 0000000..b531d5d --- /dev/null +++ b/deployment/policies/lambda.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + } \ No newline at end of file diff --git a/deployment/policies/lambda_ssm.json b/deployment/policies/lambda_ssm.json new file mode 100644 index 0000000..d4c8ee0 --- /dev/null +++ b/deployment/policies/lambda_ssm.json @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ssm:GetParameters" + ], + "Resource": [ + "arn:aws:ssm:::parameter/localstack-thumbnail-app/*" + ] + } + ] +} \ No newline at end of file diff --git a/deployment/policies/list_lambda_s3_buckets.json.tpl b/deployment/policies/list_lambda_s3_buckets.json.tpl new file mode 100644 index 0000000..0278bf1 --- /dev/null +++ b/deployment/policies/list_lambda_s3_buckets.json.tpl @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::${images_bucket}", + "arn:aws:s3:::${images_bucket}/*", + "arn:aws:s3:::${images_resized_bucket}", + "arn:aws:s3:::${images_resized_bucket}/*" + ] + } + ] +} \ No newline at end of file diff --git a/deployment/policies/presign_lambda_s3_buckets.json.tpl b/deployment/policies/presign_lambda_s3_buckets.json.tpl new file mode 100644 index 0000000..1788bf1 --- /dev/null +++ b/deployment/policies/presign_lambda_s3_buckets.json.tpl @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::${images_bucket}", + "arn:aws:s3:::${images_bucket}/*" + ] + } + ] +} \ No newline at end of file diff --git a/deployment/policies/resize_lambda_s3_buckets.json.tpl b/deployment/policies/resize_lambda_s3_buckets.json.tpl new file mode 100644 index 0000000..c21fcfe --- /dev/null +++ b/deployment/policies/resize_lambda_s3_buckets.json.tpl @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::${images_resized_bucket}", + "arn:aws:s3:::${images_resized_bucket}/*" + ] + } + ] +} \ No newline at end of file diff --git a/deployment/policies/resize_lambda_sns.json.tpl b/deployment/policies/resize_lambda_sns.json.tpl new file mode 100644 index 0000000..34a7540 --- /dev/null +++ b/deployment/policies/resize_lambda_sns.json.tpl @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sns:Publish" + ], + "Effect": "Allow", + "Resource": "${failure_notifications_topic_arn}" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "${resize_lambda_arn}" + } + ] +} \ No newline at end of file diff --git a/deployment/policies/website_bucket_policy.json.tpl b/deployment/policies/website_bucket_policy.json.tpl new file mode 100644 index 0000000..0e11a91 --- /dev/null +++ b/deployment/policies/website_bucket_policy.json.tpl @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "${cdn_identity_arn}" + }, + "Action": "s3:GetObject", + "Resource": "${website_bucket_arn}/*" + } + ] +} \ No newline at end of file diff --git a/deployment/terraform/main.tf b/deployment/terraform/main.tf new file mode 100644 index 0000000..4797619 --- /dev/null +++ b/deployment/terraform/main.tf @@ -0,0 +1,356 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "<= 5.72.1" + } + } +} + +provider "aws" { + region = "us-east-1" + skip_requesting_account_id = var.local_run +} + +variable "local_run" { + description = "Flag to determine if running locally using LocalStack" + type = bool +} + +resource "random_integer" "images_bucket_id" { + min = 0 + max = 10000 +} + +resource "random_integer" "image_resized_bucket_id" { + min = 0 + max = 10000 +} + +resource "random_integer" "website_bucket_id" { + min = 0 + max = 10000 +} + +locals { + env_variables = var.local_run ? { STAGE = "local" } : {} + root_dir = "${path.module}/../.." + images_bucket = "localstack-thumbnails-app-images${var.local_run ? "" : "-${random_integer.images_bucket_id.result}"}" + image_resized_bucket = "localstack-thumbnails-app-resized${var.local_run ? "" : "-${random_integer.image_resized_bucket_id.result}"}" + website_bucket = "localstack-website${var.local_run ? "" : "-${random_integer.website_bucket_id.result}"}" + failure_notifications_email = "my-email@example.com" +} + +# S3 +resource "aws_s3_bucket" "images_bucket" { + bucket = local.images_bucket +} + +resource "aws_s3_bucket" "image_resized_bucket" { + bucket = local.image_resized_bucket +} + +# SSM + +resource "aws_ssm_parameter" "images_bucket_ssm" { + name = "/localstack-thumbnail-app/buckets/images" + type = "String" + value = aws_s3_bucket.images_bucket.bucket +} + +resource "aws_ssm_parameter" "images_resized_bucket_ssm" { + name = "/localstack-thumbnail-app/buckets/resized" + type = "String" + value = aws_s3_bucket.image_resized_bucket.bucket +} + +## Lambdas + +# IAM SSM Policy + +resource "aws_iam_policy" "lambdas_ssm" { + name = "LambdasAccessSsm" + policy = file("../policies/lambda_ssm.json") +} + +# Presign Lambda + +resource "aws_iam_role" "presign_lambda_role" { + name = "PresignLambdaRole" + assume_role_policy = file("../policies/lambda.json") +} + +resource "aws_iam_policy" "presign_lambda_s3_buckets" { + name = "PresignLambdaS3AccessPolicy" + policy = templatefile("../policies/presign_lambda_s3_buckets.json.tpl", { + images_bucket = aws_s3_bucket.images_bucket.bucket + }) +} + +resource "aws_iam_role_policy_attachment" "presign_lambda_s3_buckets" { + role = aws_iam_role.presign_lambda_role.name + policy_arn = aws_iam_policy.presign_lambda_s3_buckets.arn +} + +resource "aws_iam_role_policy_attachment" "presign_lambda_ssm" { + role = aws_iam_role.presign_lambda_role.name + policy_arn = aws_iam_policy.lambdas_ssm.arn +} + +resource "aws_lambda_function" "presign_lambda" { + function_name = "presign" + filename = "${local.root_dir}/lambdas/presign/lambda.zip" + handler = "handler.handler" + runtime = "python3.11" + timeout = 10 + role = aws_iam_role.presign_lambda_role.arn + source_code_hash = filebase64sha256("${local.root_dir}/lambdas/presign/lambda.zip") + + environment { + variables = local.env_variables + } +} + +resource "aws_lambda_function_url" "presign_lambda_function" { + function_name = aws_lambda_function.presign_lambda.function_name + authorization_type = "NONE" +} + +# List images lambda + +resource "aws_iam_role" "list_lambda_role" { + name = "ListLambdaRole" + assume_role_policy = file("../policies/lambda.json") +} + +resource "aws_iam_policy" "list_lambda_s3_buckets" { + name = "ListLambdaS3AccessPolicy" + policy = templatefile("../policies/list_lambda_s3_buckets.json.tpl", { + images_bucket = aws_s3_bucket.images_bucket.bucket, + images_resized_bucket = aws_s3_bucket.image_resized_bucket.bucket + }) +} + +resource "aws_iam_role_policy_attachment" "list_lambda_s3_buckets" { + role = aws_iam_role.list_lambda_role.name + policy_arn = aws_iam_policy.list_lambda_s3_buckets.arn +} + +resource "aws_iam_role_policy_attachment" "list_lambda_ssm" { + role = aws_iam_role.list_lambda_role.name + policy_arn = aws_iam_policy.lambdas_ssm.arn +} + +resource "aws_lambda_function" "list_lambda" { + function_name = "list" + filename = "${local.root_dir}/lambdas/list/lambda.zip" + handler = "handler.handler" + runtime = "python3.11" + timeout = 10 + role = aws_iam_role.list_lambda_role.arn + source_code_hash = filebase64sha256("${local.root_dir}/lambdas/list/lambda.zip") + + environment { + variables = local.env_variables + } +} + +resource "aws_lambda_function_url" "list_lambda_function" { + function_name = aws_lambda_function.list_lambda.function_name + authorization_type = "NONE" +} + +# Resize lambda + +resource "aws_iam_role" "resize_lambda_role" { + name = "ResizeLambdaRole" + assume_role_policy = file("../policies/lambda.json") +} + +resource "aws_iam_policy" "resize_lambda_s3_buckets" { + name = "ResizeLambdaS3Buckets" + policy = templatefile("../policies/resize_lambda_s3_buckets.json.tpl", { + images_resized_bucket = aws_s3_bucket.image_resized_bucket.bucket + }) +} + +resource "aws_iam_role_policy_attachment" "resize_lambda_s3_buckets" { + role = aws_iam_role.resize_lambda_role.name + policy_arn = aws_iam_policy.resize_lambda_s3_buckets.arn +} + +resource "aws_iam_policy" "resize_lambda_sns" { + name = "ResizeLambdaSNS" + policy = templatefile("../policies/resize_lambda_sns.json.tpl", { + failure_notifications_topic_arn = aws_sns_topic.failure_notifications.arn, + resize_lambda_arn = aws_lambda_function.resize_lambda.arn + }) +} + +resource "aws_iam_role_policy_attachment" "resize_lambda_sns" { + role = aws_iam_role.resize_lambda_role.name + policy_arn = aws_iam_policy.resize_lambda_sns.arn +} + +resource "aws_iam_role_policy_attachment" "resize_lambda_ssm" { + role = aws_iam_role.resize_lambda_role.name + policy_arn = aws_iam_policy.lambdas_ssm.arn +} + +resource "aws_lambda_function" "resize_lambda" { + function_name = "resize" + filename = "${local.root_dir}/lambdas/resize/lambda.zip" + handler = "handler.handler" + runtime = "python3.11" + role = aws_iam_role.resize_lambda_role.arn + source_code_hash = filebase64sha256("${local.root_dir}/lambdas/resize/lambda.zip") + + environment { + variables = local.env_variables + } + + dead_letter_config { + target_arn = aws_sns_topic.failure_notifications.arn + } +} + +# SNS Topic for failure notifications +resource "aws_sns_topic" "failure_notifications" { + name = "image_resize_failures" +} + +resource "aws_sns_topic_subscription" "email_sub" { + topic_arn = aws_sns_topic.failure_notifications.arn + protocol = "email" + endpoint = local.failure_notifications_email +} + +# S3 Bucket Notification for Lambda trigger +resource "aws_s3_bucket_notification" "bucket_notification" { + bucket = aws_s3_bucket.images_bucket.id + + lambda_function { + lambda_function_arn = aws_lambda_function.resize_lambda.arn + events = ["s3:ObjectCreated:*"] + } +} + +resource "aws_lambda_permission" "s3_invoke_resize_lambda" { + statement_id = "AllowS3InvokeLambda" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.resize_lambda.function_name + principal = "s3.amazonaws.com" + source_arn = aws_s3_bucket.images_bucket.arn +} + +# CloudFront + +resource "aws_s3_bucket" "website_bucket" { + bucket = local.website_bucket +} + +resource "aws_s3_bucket_website_configuration" "website_configuration" { + bucket = aws_s3_bucket.website_bucket.bucket + index_document { + suffix = "index.html" + } +} + +resource "aws_s3_object" "website_file_index" { + bucket = aws_s3_bucket.website_bucket.bucket + key = "index.html" + source = "${local.root_dir}/website/index.html" + etag = filemd5("${local.root_dir}/website/index.html") + content_type = "text/html" + acl = "public-read" +} + +resource "aws_s3_object" "website_file_js" { + bucket = aws_s3_bucket.website_bucket.bucket + key = "app.js" + source = "${local.root_dir}/website/app.js" + etag = filemd5("${local.root_dir}/website/app.js") + content_type = "application/javascript" + acl = "public-read" +} + +resource "aws_s3_object" "website_file_icon" { + bucket = aws_s3_bucket.website_bucket.bucket + key = "favicon.ico" + source = "${local.root_dir}/website/favicon.ico" + etag = filemd5("${local.root_dir}/website/favicon.ico") + content_type = "image/x-icon" + acl = "public-read" +} + +resource "aws_cloudfront_origin_access_identity" "cdn_identity" { + comment = "OAI for CloudFront to access S3 bucket" +} + +resource "aws_s3_bucket_policy" "website_bucket_policy" { + bucket = aws_s3_bucket.website_bucket.bucket + policy = templatefile("../policies/website_bucket_policy.json.tpl", { + cdn_identity_arn = aws_cloudfront_origin_access_identity.cdn_identity.iam_arn + website_bucket_arn = aws_s3_bucket.website_bucket.arn + }) +} + +resource "aws_cloudfront_distribution" "cdn" { + origin { + domain_name = aws_s3_bucket.website_bucket.bucket_regional_domain_name + origin_id = aws_s3_bucket.website_bucket.bucket + + s3_origin_config { + origin_access_identity = aws_cloudfront_origin_access_identity.cdn_identity.cloudfront_access_identity_path + } + } + + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" + + viewer_certificate { + cloudfront_default_certificate = true + } + + default_cache_behavior { + target_origin_id = aws_s3_bucket.website_bucket.bucket + + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 86400 + max_ttl = 31536000 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } +} + +resource "aws_s3_bucket_public_access_block" "website_block_public_access" { + bucket = aws_s3_bucket.website_bucket.bucket + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# Outputs + +output "presign_lambda_function_url" { + value = aws_lambda_function_url.presign_lambda_function.function_url +} + +output "list_lambda_function_url" { + value = aws_lambda_function_url.list_lambda_function.function_url +} + +output "cloudfront_url" { + value = "Now open the Web app under: http://${aws_cloudfront_distribution.cdn.domain_name}" +} \ No newline at end of file From 1756406ffdd217775d9596343cbc1ecafc47292d Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Sun, 20 Oct 2024 21:11:14 +0200 Subject: [PATCH 2/8] Removed unnecessary region, renamed policy file --- .env.example | 1 - Makefile | 1 - ...ebsite_bucket_policy.json.tpl => website_s3_bucket.json.tpl} | 0 deployment/terraform/main.tf | 2 +- 4 files changed, 1 insertion(+), 3 deletions(-) rename deployment/policies/{website_bucket_policy.json.tpl => website_s3_bucket.json.tpl} (100%) diff --git a/.env.example b/.env.example index 0249c39..5a1fae9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ LOCALSTACK_AUTH_TOKEN=YOUR_TOKEN -AWS_DEFAULT_REGION=us-east-1 # LOCAL_RUN=false # Default is true \ No newline at end of file diff --git a/Makefile b/Makefile index 1731e05..e3ab564 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ -export AWS_DEFAULT_REGION=us-east-1 export AWS_ACCESS_KEY_ID ?= test export AWS_SECRET_ACCESS_KEY ?= test export LOCAL_RUN ?= true diff --git a/deployment/policies/website_bucket_policy.json.tpl b/deployment/policies/website_s3_bucket.json.tpl similarity index 100% rename from deployment/policies/website_bucket_policy.json.tpl rename to deployment/policies/website_s3_bucket.json.tpl diff --git a/deployment/terraform/main.tf b/deployment/terraform/main.tf index 4797619..096afee 100644 --- a/deployment/terraform/main.tf +++ b/deployment/terraform/main.tf @@ -289,7 +289,7 @@ resource "aws_cloudfront_origin_access_identity" "cdn_identity" { resource "aws_s3_bucket_policy" "website_bucket_policy" { bucket = aws_s3_bucket.website_bucket.bucket - policy = templatefile("../policies/website_bucket_policy.json.tpl", { + policy = templatefile("../policies/website_s3_bucket.json.tpl", { cdn_identity_arn = aws_cloudfront_origin_access_identity.cdn_identity.iam_arn website_bucket_arn = aws_s3_bucket.website_bucket.arn }) From b3f1bc3a62651436c991c888f9208ad12c8ad400 Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Sun, 20 Oct 2024 22:51:14 +0200 Subject: [PATCH 3/8] Silent command with token, remove -s flag from readme --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e3ab564..8efd4b2 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,9 @@ terraform-destroy: tflocal destroy --auto-approve; start: - LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d + @LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d stop: - @echo localstack stop .PHONY: build awslocal-setup terraform-setup terraform-destroy start stop From d9a1d065280f3a41efb8ee3544a18e2d02d0f9ba Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Mon, 21 Oct 2024 11:15:50 +0200 Subject: [PATCH 4/8] Moved policices to terraform folder --- deployment/terraform/main.tf | 18 +++++++++--------- .../{ => terraform}/policies/lambda.json | 0 .../{ => terraform}/policies/lambda_ssm.json | 0 .../policies/list_lambda_s3_buckets.json.tpl | 0 .../presign_lambda_s3_buckets.json.tpl | 0 .../policies/resize_lambda_s3_buckets.json.tpl | 0 .../policies/resize_lambda_sns.json.tpl | 0 .../policies/website_s3_bucket.json.tpl | 0 8 files changed, 9 insertions(+), 9 deletions(-) rename deployment/{ => terraform}/policies/lambda.json (100%) rename deployment/{ => terraform}/policies/lambda_ssm.json (100%) rename deployment/{ => terraform}/policies/list_lambda_s3_buckets.json.tpl (100%) rename deployment/{ => terraform}/policies/presign_lambda_s3_buckets.json.tpl (100%) rename deployment/{ => terraform}/policies/resize_lambda_s3_buckets.json.tpl (100%) rename deployment/{ => terraform}/policies/resize_lambda_sns.json.tpl (100%) rename deployment/{ => terraform}/policies/website_s3_bucket.json.tpl (100%) diff --git a/deployment/terraform/main.tf b/deployment/terraform/main.tf index 096afee..feccf35 100644 --- a/deployment/terraform/main.tf +++ b/deployment/terraform/main.tf @@ -70,19 +70,19 @@ resource "aws_ssm_parameter" "images_resized_bucket_ssm" { resource "aws_iam_policy" "lambdas_ssm" { name = "LambdasAccessSsm" - policy = file("../policies/lambda_ssm.json") + policy = file("policies/lambda_ssm.json") } # Presign Lambda resource "aws_iam_role" "presign_lambda_role" { name = "PresignLambdaRole" - assume_role_policy = file("../policies/lambda.json") + assume_role_policy = file("policies/lambda.json") } resource "aws_iam_policy" "presign_lambda_s3_buckets" { name = "PresignLambdaS3AccessPolicy" - policy = templatefile("../policies/presign_lambda_s3_buckets.json.tpl", { + policy = templatefile("policies/presign_lambda_s3_buckets.json.tpl", { images_bucket = aws_s3_bucket.images_bucket.bucket }) } @@ -120,12 +120,12 @@ resource "aws_lambda_function_url" "presign_lambda_function" { resource "aws_iam_role" "list_lambda_role" { name = "ListLambdaRole" - assume_role_policy = file("../policies/lambda.json") + assume_role_policy = file("policies/lambda.json") } resource "aws_iam_policy" "list_lambda_s3_buckets" { name = "ListLambdaS3AccessPolicy" - policy = templatefile("../policies/list_lambda_s3_buckets.json.tpl", { + policy = templatefile("policies/list_lambda_s3_buckets.json.tpl", { images_bucket = aws_s3_bucket.images_bucket.bucket, images_resized_bucket = aws_s3_bucket.image_resized_bucket.bucket }) @@ -164,12 +164,12 @@ resource "aws_lambda_function_url" "list_lambda_function" { resource "aws_iam_role" "resize_lambda_role" { name = "ResizeLambdaRole" - assume_role_policy = file("../policies/lambda.json") + assume_role_policy = file("policies/lambda.json") } resource "aws_iam_policy" "resize_lambda_s3_buckets" { name = "ResizeLambdaS3Buckets" - policy = templatefile("../policies/resize_lambda_s3_buckets.json.tpl", { + policy = templatefile("policies/resize_lambda_s3_buckets.json.tpl", { images_resized_bucket = aws_s3_bucket.image_resized_bucket.bucket }) } @@ -181,7 +181,7 @@ resource "aws_iam_role_policy_attachment" "resize_lambda_s3_buckets" { resource "aws_iam_policy" "resize_lambda_sns" { name = "ResizeLambdaSNS" - policy = templatefile("../policies/resize_lambda_sns.json.tpl", { + policy = templatefile("policies/resize_lambda_sns.json.tpl", { failure_notifications_topic_arn = aws_sns_topic.failure_notifications.arn, resize_lambda_arn = aws_lambda_function.resize_lambda.arn }) @@ -289,7 +289,7 @@ resource "aws_cloudfront_origin_access_identity" "cdn_identity" { resource "aws_s3_bucket_policy" "website_bucket_policy" { bucket = aws_s3_bucket.website_bucket.bucket - policy = templatefile("../policies/website_s3_bucket.json.tpl", { + policy = templatefile("policies/website_s3_bucket.json.tpl", { cdn_identity_arn = aws_cloudfront_origin_access_identity.cdn_identity.iam_arn website_bucket_arn = aws_s3_bucket.website_bucket.arn }) diff --git a/deployment/policies/lambda.json b/deployment/terraform/policies/lambda.json similarity index 100% rename from deployment/policies/lambda.json rename to deployment/terraform/policies/lambda.json diff --git a/deployment/policies/lambda_ssm.json b/deployment/terraform/policies/lambda_ssm.json similarity index 100% rename from deployment/policies/lambda_ssm.json rename to deployment/terraform/policies/lambda_ssm.json diff --git a/deployment/policies/list_lambda_s3_buckets.json.tpl b/deployment/terraform/policies/list_lambda_s3_buckets.json.tpl similarity index 100% rename from deployment/policies/list_lambda_s3_buckets.json.tpl rename to deployment/terraform/policies/list_lambda_s3_buckets.json.tpl diff --git a/deployment/policies/presign_lambda_s3_buckets.json.tpl b/deployment/terraform/policies/presign_lambda_s3_buckets.json.tpl similarity index 100% rename from deployment/policies/presign_lambda_s3_buckets.json.tpl rename to deployment/terraform/policies/presign_lambda_s3_buckets.json.tpl diff --git a/deployment/policies/resize_lambda_s3_buckets.json.tpl b/deployment/terraform/policies/resize_lambda_s3_buckets.json.tpl similarity index 100% rename from deployment/policies/resize_lambda_s3_buckets.json.tpl rename to deployment/terraform/policies/resize_lambda_s3_buckets.json.tpl diff --git a/deployment/policies/resize_lambda_sns.json.tpl b/deployment/terraform/policies/resize_lambda_sns.json.tpl similarity index 100% rename from deployment/policies/resize_lambda_sns.json.tpl rename to deployment/terraform/policies/resize_lambda_sns.json.tpl diff --git a/deployment/policies/website_s3_bucket.json.tpl b/deployment/terraform/policies/website_s3_bucket.json.tpl similarity index 100% rename from deployment/policies/website_s3_bucket.json.tpl rename to deployment/terraform/policies/website_s3_bucket.json.tpl From 9c40758e7b8a2ee908f41c10935572d5828c3f8d Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Mon, 21 Oct 2024 12:26:54 +0200 Subject: [PATCH 5/8] Moved deploy script to deployment/awslocal folder --- .circleci/config.yml | 8 +++++++- .github/workflows/cloudpod_release.yml | 6 +++++- .github/workflows/preview_create.yml | 2 +- .gitlab-ci.yml | 3 ++- Makefile | 3 ++- README.md | 8 ++++++-- buildspec.yml | 3 ++- {bin => deployment/awslocal}/deploy.sh | 2 -- 8 files changed, 25 insertions(+), 10 deletions(-) rename {bin => deployment/awslocal}/deploy.sh (99%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7c0c79e..f96189a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,10 +26,16 @@ jobs: command: pip3 install -r requirements-dev.txt --upgrade - localstack/wait + - run: + name: Build lambdas + command: + bin/build_lambdas.sh + - run: name: Deploy infrastructure command: - bin/deploy.sh + deployment/awslocal/deploy.sh + - run: name: Export state command: localstack state export ls-state.zip diff --git a/.github/workflows/cloudpod_release.yml b/.github/workflows/cloudpod_release.yml index c40f941..e695759 100644 --- a/.github/workflows/cloudpod_release.yml +++ b/.github/workflows/cloudpod_release.yml @@ -45,9 +45,13 @@ jobs: env: LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + - name: Build lambdas + run: | + bin/build_lambdas.sh + - name: Deploy infrastructure run: | - bin/deploy.sh + deployment/awslocal/deploy.sh - name: Run Tests env: diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 9166fb0..93d1e63 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -46,4 +46,4 @@ jobs: preview-cmd: | # Add your custom deployment commands here. # Below is an example for the Image resizer application. - bin/deploy.sh \ No newline at end of file + bin/build_lambdas.sh && deployment/awslocal/deploy.sh \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bbd9714..72a3082 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -56,7 +56,8 @@ deploy: - .github/* - when: always script: - - ./bin/deploy.sh + - ./bin/build_lambdas.sh + - ./deployment/awslocal/deploy.sh - localstack state export ./ls-state-pod.zip test: diff --git a/Makefile b/Makefile index 8efd4b2..b81f187 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ build: bin/build_lambdas.sh; awslocal-setup: - bin/deploy.sh + $(MAKE) build + deployment/awslocal/deploy.sh terraform-setup: $(MAKE) build diff --git a/README.md b/README.md index f7a0965..1cbe547 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ pip install -r requirements-dev.txt ## Instructions -You can set up and deploy the sample application on LocalStack by executing the commands in our Makefile. First, create a `.env` file using the provided `.env.example` file as a template, and include your LocalStack token in it. Then, run `make -s start` to initiate LocalStack on your machine. +You can set up and deploy the sample application on LocalStack by executing the commands in our Makefile. First, create a `.env` file using the provided `.env.example` file as a template, and include your LocalStack token in it. Then, run `make start` to initiate LocalStack on your machine. -Next, execute `make -s terraform-setup` to provision the infrastructure on LocalStack using Terraform CLI and its scripts. Alternatively, run `make -s awslocal-setup` to set up the infrastructure with the local AWS CLI. +Next, execute `make terraform-setup` to provision the infrastructure on LocalStack using Terraform CLI and its scripts. Alternatively, run `make awslocal-setup` to set up the infrastructure with the local AWS CLI. If you prefer, you can also follow these step-by-step instructions for a manual deployment. @@ -99,6 +99,10 @@ We are using the `tflocal` wrapper to configure the local service endpoints, and ### AWS CLI +You can execute the following commands to set up the infrastructure using `awslocal`, a wrapper for the AWS CLI. + +All the commands are also available in the `deployment/awslocal/deploy.sh` script. + #### Create the buckets The names are completely configurable via SSM: diff --git a/buildspec.yml b/buildspec.yml index 6127a83..8b1471f 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -25,7 +25,8 @@ phases: - localstack start -d pre_build: commands: - - bin/deploy.sh + - bin/build_lambdas.sh + - deployment/awslocal/deploy.sh build: commands: - pytest tests diff --git a/bin/deploy.sh b/deployment/awslocal/deploy.sh similarity index 99% rename from bin/deploy.sh rename to deployment/awslocal/deploy.sh index b2555bd..d4eb22d 100755 --- a/bin/deploy.sh +++ b/deployment/awslocal/deploy.sh @@ -2,8 +2,6 @@ export AWS_DEFAULT_REGION=us-east-1 -bin/build_lambdas.sh - awslocal s3 mb s3://localstack-thumbnails-app-images awslocal s3 mb s3://localstack-thumbnails-app-resized From d82782701e8267df48e65c505df7f64c7784c3e6 Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Mon, 21 Oct 2024 17:40:08 +0200 Subject: [PATCH 6/8] Changed deploy.sh script --- .github/workflows/integration-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 7704f8c..a3630e4 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -58,10 +58,13 @@ jobs: install-awslocal: 'true' env: LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + - name: Build lambdas + run: | + bin/build_lambdas.sh - name: Deploy infrastructure run: | - bin/deploy.sh + deployment/awslocal/deploy.sh - name: Run Tests env: From e7804cf711ab86a0b89113edfd068d2da4408477 Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Mon, 21 Oct 2024 17:42:06 +0200 Subject: [PATCH 7/8] Added usage and build targets to Makefile, updated readme and python requirements file --- Makefile | 22 ++++++++++++++-------- README.md | 10 +++++----- requirements-dev.txt | 1 + 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index b81f187..f0c8068 100644 --- a/Makefile +++ b/Makefile @@ -5,14 +5,20 @@ SHELL := /bin/bash include .env -build: - bin/build_lambdas.sh; +usage: ## Show this help + @grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' -e 's/##//' -awslocal-setup: +install: ## Install dependencies + @pip install -r requirements-dev.txt + +build: ## Build lambdas in the lambdas folder + bin/build_lambdas.sh; + +awslocal-setup: ## Deploy the application locally using `awslocal`, a wrapper for the AWS CLI $(MAKE) build deployment/awslocal/deploy.sh -terraform-setup: +terraform-setup: ## Deploy the application locally using `tflocal`, a wrapper for Terraform CLI $(MAKE) build cd deployment/terraform; \ tflocal init; \ @@ -20,14 +26,14 @@ terraform-setup: tflocal apply --auto-approve -var="local_run=${LOCAL_RUN}"; \ echo "Paste the function URLs above to the WebApp 🎉"; -terraform-destroy: +terraform-destroy: ## Destroy all resources created locally using terraform scripts cd deployment/terraform; \ tflocal destroy --auto-approve; -start: +start: ## Start the LocalStack Pro container in the detached mode @LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d -stop: +stop: ## Stop the LocalStack Pro container localstack stop -.PHONY: build awslocal-setup terraform-setup terraform-destroy start stop +.PHONY: usage install build awslocal-setup terraform-setup terraform-destroy start stop diff --git a/README.md b/README.md index 1cbe547..cf8c338 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,11 @@ pip install -r requirements-dev.txt ## Instructions -You can set up and deploy the sample application on LocalStack by executing the commands in our Makefile. First, create a `.env` file using the provided `.env.example` file as a template, and include your LocalStack token in it. Then, run `make start` to initiate LocalStack on your machine. +You can set up and deploy the sample application on LocalStack by executing the commands in our Makefile. First, create a `.env` file using the provided `.env.example` file as a template, and include your LocalStack token in it. Then, run `make start` to initiate LocalStack on your machine. -Next, execute `make terraform-setup` to provision the infrastructure on LocalStack using Terraform CLI and its scripts. Alternatively, run `make awslocal-setup` to set up the infrastructure with the local AWS CLI. +Next, execute `make install` to install needed dependencies. + +After that, launch `make terraform-setup` to provision the infrastructure on LocalStack using Terraform CLI and its scripts. Alternatively, run `make awslocal-setup` to set up the infrastructure using `awslocal`, a wrapper for the AWS CLI. If you prefer, you can also follow these step-by-step instructions for a manual deployment. @@ -99,9 +101,7 @@ We are using the `tflocal` wrapper to configure the local service endpoints, and ### AWS CLI -You can execute the following commands to set up the infrastructure using `awslocal`, a wrapper for the AWS CLI. - -All the commands are also available in the `deployment/awslocal/deploy.sh` script. +You can execute the following commands to set up the infrastructure using `awslocal`. All the commands are also available in the `deployment/awslocal/deploy.sh` script. #### Create the buckets diff --git a/requirements-dev.txt b/requirements-dev.txt index 2e13e4f..9bf6a99 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,3 +9,4 @@ black pytest awscli awscli-local +terraform-local From dab31f47f16d3c079ae790b224c7852b5677a66c Mon Sep 17 00:00:00 2001 From: "Anastasia D." Date: Tue, 22 Oct 2024 13:26:14 +0200 Subject: [PATCH 8/8] Removed LOCAL_RUN env variable --- .env.example | 1 - Makefile | 3 +-- deployment/terraform/main.tf | 32 +++++++------------------------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/.env.example b/.env.example index 5a1fae9..8c63a9f 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ LOCALSTACK_AUTH_TOKEN=YOUR_TOKEN -# LOCAL_RUN=false # Default is true \ No newline at end of file diff --git a/Makefile b/Makefile index f0c8068..064ce5f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ export AWS_ACCESS_KEY_ID ?= test export AWS_SECRET_ACCESS_KEY ?= test -export LOCAL_RUN ?= true SHELL := /bin/bash include .env @@ -23,7 +22,7 @@ terraform-setup: ## Deploy the application locally using `tflocal`, a wrapper f cd deployment/terraform; \ tflocal init; \ echo "Deploying Terraform configuration 🚀"; \ - tflocal apply --auto-approve -var="local_run=${LOCAL_RUN}"; \ + tflocal apply --auto-approve; \ echo "Paste the function URLs above to the WebApp 🎉"; terraform-destroy: ## Destroy all resources created locally using terraform scripts diff --git a/deployment/terraform/main.tf b/deployment/terraform/main.tf index feccf35..a70c437 100644 --- a/deployment/terraform/main.tf +++ b/deployment/terraform/main.tf @@ -9,35 +9,17 @@ terraform { provider "aws" { region = "us-east-1" - skip_requesting_account_id = var.local_run -} - -variable "local_run" { - description = "Flag to determine if running locally using LocalStack" - type = bool -} - -resource "random_integer" "images_bucket_id" { - min = 0 - max = 10000 -} - -resource "random_integer" "image_resized_bucket_id" { - min = 0 - max = 10000 -} - -resource "random_integer" "website_bucket_id" { - min = 0 - max = 10000 + skip_requesting_account_id = true } locals { - env_variables = var.local_run ? { STAGE = "local" } : {} + # TO-DO: The environment variable STAGE is required for Lambdas to connect to LocalStack endpoints. + # The environment variable can be removed once Lambdas are adapted to support transparent endpoint injection. + env_variables = { STAGE = "local" } root_dir = "${path.module}/../.." - images_bucket = "localstack-thumbnails-app-images${var.local_run ? "" : "-${random_integer.images_bucket_id.result}"}" - image_resized_bucket = "localstack-thumbnails-app-resized${var.local_run ? "" : "-${random_integer.image_resized_bucket_id.result}"}" - website_bucket = "localstack-website${var.local_run ? "" : "-${random_integer.website_bucket_id.result}"}" + images_bucket = "localstack-thumbnails-app-images" + image_resized_bucket = "localstack-thumbnails-app-resized" + website_bucket = "localstack-website" failure_notifications_email = "my-email@example.com" }