├── locals.tf ├── .gitignore ├── terraform.tf ├── provider.tf ├── data.tf ├── output.tf ├── README.md ├── variables.tf ├── .github └── workflows │ ├── deploy-blog.yaml │ └── deploy-cms.yaml └── main.tf /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | app_name = lower(replace(var.app_name, "/[^A-Za-z0-9]/", "-")) 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .terraform/ 4 | .terraform.lock.hcl 5 | terraform.tfvars 6 | terraform.config 7 | *.zip 8 | -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 6.0.0" 6 | } 7 | } 8 | 9 | backend "s3" {} 10 | } 11 | -------------------------------------------------------------------------------- /provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-west-2" 3 | 4 | default_tags { 5 | tags = { 6 | Service = var.tag_service 7 | Environment = var.tag_environment 8 | Owner = var.tag_owner 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | # caller identity data source 2 | data "aws_caller_identity" "current" {} 3 | 4 | # region data source 5 | data "aws_region" "current" {} 6 | 7 | # partition data source 8 | data "aws_partition" "current" {} 9 | 10 | data "aws_s3_bucket" "aws_bucket" { 11 | bucket = var.aws_bucket 12 | } 13 | -------------------------------------------------------------------------------- /output.tf: -------------------------------------------------------------------------------- 1 | output "web_lambda_function_qualified_arn" { 2 | description = "Current Lambda function version" 3 | value = aws_lambda_function.web_lambda_function.version 4 | } 5 | 6 | output "artisan_lambda_function_qualified_arn" { 7 | description = "Current Lambda function version" 8 | value = aws_lambda_function.artisan_lambda_function.version 9 | } 10 | 11 | output "jobs_worker_lambda_function_qualified_arn" { 12 | description = "Current Lambda function version" 13 | value = aws_lambda_function.jobs_worker_lambda_function.version 14 | } 15 | 16 | output "http_api_id" { 17 | description = "Id of the HTTP API" 18 | value = aws_apigatewayv2_api.http_api.id 19 | } 20 | 21 | output "http_api_url" { 22 | description = "URL of the HTTP API" 23 | value = aws_apigatewayv2_api.http_api.api_endpoint 24 | } 25 | 26 | output "jobs_queue_arn" { 27 | description = "ARN of the \"jobs\" SQS queue." 28 | value = aws_sqs_queue.jobs_queue.arn 29 | } 30 | 31 | output "jobs_queue_url" { 32 | description = "URL of the \"jobs\" SQS queue." 33 | value = aws_sqs_queue.jobs_queue.id 34 | } 35 | 36 | output "jobs_dlq_url" { 37 | description = "URL of the \"jobs\" SQS dead letter queue." 38 | value = aws_sqs_queue.jobs_dlq.id 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Serverless 2 | 3 | Deploy your Laravel App to AWS Lambda with [Terraform](https://www.terraform.io/) or [OpenTofu](https://opentofu.org/). 4 | 5 | ## Use Terraform and Bref Runtime to Run PHP Application in AWS Lambda 6 | 7 | AWS Lambda does not natively support PHP. But we can use [Bref](https://bref.sh/) to run PHP application in AWS Lambda. 8 | 9 | In Bref official document, Bref use [Serverless](https://www.serverless.com/) to provision the AWS resource, but in detailed, Serverless actually uses AWS CloudFormation to deploy resources behind the scenes. 10 | 11 | This project use [cf2tf](https://github.com/DontShaveTheYak/cf2tf) to convert the CloudFormation template to Terraform HCL. After the conversion, I made some changes to make it work. 12 | 13 | > I prefer Terraform to Serverless. 14 | 15 | ## Packaged Your Laravel Application Before Deployment 16 | 17 | Before upload your Laravel application, you need to install dependencies and remove unnecessary files (like `.git` or `node_modules`). 18 | 19 | ```bash 20 | git clone YOUR_LARAVEL_REPO_URL laravel-app 21 | 22 | cd laravel-app 23 | 24 | # install composer dependencies 25 | composer install --prefer-dist --optimize-autoloader --no-dev 26 | php artisan optimize 27 | # don't cache your config! bref will use environment variables in aws lambda 28 | php artisan config:clear 29 | 30 | # remove unnecessary files 31 | rm -rf .git 32 | rm -rf node_modules 33 | rm -rf tests 34 | rm -rf storage 35 | 36 | # zip the laravel application 37 | zip -r "laravel-app.zip" . 38 | ``` 39 | 40 | Then you can upload `laravel-app.zip` to AWS Lambda. 41 | 42 | ## Lambda Can't Store Static Assets 43 | 44 | If you have static assets, like javascript files or css files. 45 | You should upload these files to AWS S3 after you bundled them. 46 | 47 | ```bash 48 | cd laravel-app 49 | 50 | npm install 51 | npm run build 52 | 53 | # upload assets to aws s3 54 | aws s3 sync public s3://YOUR_ASSET_AWS_BUCKET_NAME 55 | ``` 56 | 57 | Then you should set `ASSET_URL` to AWS S3 bucket public url. 58 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # provider settings 3 | # 4 | variable "tag_service" { 5 | description = "Service name" 6 | type = string 7 | } 8 | 9 | variable "tag_environment" { 10 | description = "Environment name" 11 | type = string 12 | } 13 | 14 | variable "tag_owner" { 15 | description = "Owner name" 16 | type = string 17 | } 18 | 19 | # 20 | # lambda settings 21 | # 22 | variable "filename" { 23 | type = string 24 | default = "./laravel-app.zip" 25 | } 26 | 27 | variable "lambda_runtime" { 28 | type = string 29 | # https://bref.sh/docs/runtimes#aws-lambda-layers 30 | default = "provided.al2" 31 | } 32 | 33 | variable "php_lambda_layer_arn" { 34 | type = string 35 | # check all php layer runtime in this page 36 | # https://runtimes.bref.sh/?region=us-west-2 37 | default = "arn:aws:lambda:us-west-2:534081306603:layer:arm-php-84:35" 38 | } 39 | 40 | variable "console_lambda_layer_arn" { 41 | type = string 42 | default = "arn:aws:lambda:us-west-2:534081306603:layer:console:116" 43 | } 44 | 45 | variable "enable_vpc" { 46 | type = bool 47 | default = false 48 | } 49 | 50 | variable "subnet_ids" { 51 | type = list(string) 52 | } 53 | 54 | variable "security_group_ids" { 55 | type = list(string) 56 | } 57 | 58 | variable "enable_filesystem" { 59 | type = bool 60 | default = false 61 | } 62 | 63 | variable "access_point_arn" { 64 | type = string 65 | } 66 | 67 | 68 | # 69 | # api gateway settings 70 | # 71 | variable "certificate_arn" { 72 | type = string 73 | } 74 | 75 | variable "custom_domain_name" { 76 | type = string 77 | } 78 | 79 | # 80 | # laravel settings 81 | # 82 | variable "app_name" { 83 | type = string 84 | } 85 | 86 | # 87 | # Lambda environment variables 88 | # 89 | variable "environment_variables_json_file" { 90 | type = string 91 | description = "Path to the JSON file containing environment variables for the Lambda function." 92 | } 93 | 94 | # 95 | # S3 settings 96 | # 97 | variable "aws_bucket" { 98 | type = string 99 | description = "The name of the S3 bucket to store the Laravel application files." 100 | } 101 | -------------------------------------------------------------------------------- /.github/workflows/deploy-blog.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy my blog to AWS Lambda 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | name: 7 | description: Who to greet 8 | default: Allen 9 | 10 | permissions: 11 | id-token: write 12 | contents: read 13 | 14 | jobs: 15 | deploy-laravel-to-lambda: 16 | name: Deploy my blog to AWS Lambda 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | # https://github.com/actions/checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Configure aws credentials 24 | # https://github.com/aws-actions/configure-aws-credentials 25 | uses: aws-actions/configure-aws-credentials@v4 26 | with: 27 | role-to-assume: arn:aws:iam::154471991214:role/github_action 28 | aws-region: us-west-2 29 | 30 | - name: Setup php 31 | # https://github.com/shivammathur/setup-php 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: "8.4" 35 | 36 | - name: Setup node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: "24.8.0" 40 | 41 | - name: Setup terraform 42 | # https://github.com/hashicorp/setup-terraform 43 | uses: hashicorp/setup-terraform@v3 44 | 45 | - name: Deploy to AWS Lambda 46 | run: | 47 | git clone "${{ vars.BLOG_APP_GITHUB_URL }}" laravel-app 48 | 49 | cd laravel-app 50 | 51 | # install composer dependencies 52 | composer install --prefer-dist --optimize-autoloader --no-dev 53 | php artisan config:clear 54 | php artisan feed:install 55 | 56 | # generate front-end assets 57 | npm ci 58 | npm run build 59 | 60 | # enable bref pgsql extension in lambda if you need it 61 | # https://bref.sh/docs/environment/php#extensions-installed-but-disabled-by-default 62 | # mkdir -p php/conf.d 63 | # echo "extension=pdo_pgsql" > php/conf.d/pgsql.ini 64 | 65 | # remove unnecessary files 66 | rm -rf node_modules 67 | rm -rf public/storage 68 | rm -rf resources/assets 69 | rm -rf resources/css 70 | rm -rf resources/images 71 | rm -rf resources/js 72 | rm -rf resources/ts 73 | rm -rf storage 74 | rm -rf tests 75 | rm -rf .git 76 | rm -rf .github 77 | 78 | # zip the laravel app 79 | zip -r "../laravel-app.zip" . 80 | 81 | cd .. 82 | 83 | # create environment variables json file 84 | echo '${{ secrets.BLOG_ENVIRONMENT_VARIABLES_JSON }}' > environment-variables.json 85 | 86 | # create terraform config file 87 | cat < terraform.config 88 | bucket="us-west-2-terraform-state-storage" 89 | key="us-west-2-blog-serverless.tfstate" 90 | region="us-west-2" 91 | dynamodb_table="us-west-2-terraform-state-locking" 92 | EOF 93 | 94 | # create terraform.tfvars file 95 | cat < terraform.tfvars 96 | app_name="${{ vars.BLOG_APP_NAME }}" 97 | 98 | # VPC settings 99 | enable_vpc = true 100 | subnet_ids = ${{ vars.BLOG_SUBNET_IDS }} 101 | security_group_ids = ${{ vars.BLOG_SECURITY_GROUP_IDS }} 102 | 103 | # File storage settings 104 | enable_filesystem = true 105 | access_point_arn = "${{ vars.BLOG_ACCESS_POINT_ARN }}" 106 | 107 | # api gateway settings 108 | certificate_arn = "${{ vars.BLOG_CERTIFICATE_ARN }}" 109 | custom_domain_name = "${{ vars.BLOG_CUSTOM_DOMAIN_NAME }}" 110 | 111 | # provider settings 112 | tag_service = "${{ vars.BLOG_TAG_SERVICE }}" 113 | tag_environment = "${{ vars.BLOG_TAG_ENVIRONMENT }}" 114 | tag_owner = "${{ vars.BLOG_TAG_OWNER }}" 115 | 116 | # S3 bucket settings 117 | aws_bucket = "${{ vars.BLOG_AWS_BUCKET }}" 118 | 119 | # Lambda settings 120 | environment_variables_json_file = "./environment-variables.json" 121 | 122 | filename = "./laravel-app.zip" 123 | EOF 124 | 125 | # deploy 126 | terraform init -backend-config="./terraform.config" 127 | terraform apply -auto-approve 128 | 129 | # sync front-end assets to s3 bucket 130 | aws s3 sync laravel-app/public "s3://${{ vars.BLOG_ASSET_AWS_BUCKET }}" 131 | -------------------------------------------------------------------------------- /.github/workflows/deploy-cms.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy my CMS to AWS Lambda 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | name: 7 | description: Who to greet 8 | default: Allen 9 | 10 | permissions: 11 | id-token: write 12 | contents: read 13 | 14 | jobs: 15 | deploy-laravel-to-lambda: 16 | name: Deploy my CMS to AWS Lambda 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | # https://github.com/actions/checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Configure aws credentials 24 | # https://github.com/aws-actions/configure-aws-credentials 25 | uses: aws-actions/configure-aws-credentials@v4 26 | with: 27 | role-to-assume: arn:aws:iam::154471991214:role/github_action 28 | aws-region: us-west-2 29 | 30 | - name: Setup php 31 | # https://github.com/shivammathur/setup-php 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: "8.4" 35 | 36 | - name: Setup node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: "24.8.0" 40 | 41 | - name: Setup terraform 42 | # https://github.com/hashicorp/setup-terraform 43 | uses: hashicorp/setup-terraform@v3 44 | 45 | - name: Deploy to AWS Lambda 46 | run: | 47 | git clone "${{ vars.CMS_APP_GITHUB_URL }}" laravel-app 48 | 49 | cd laravel-app 50 | 51 | # install composer dependencies 52 | composer install --prefer-dist --optimize-autoloader --no-dev 53 | php artisan optimize 54 | php artisan config:clear 55 | php artisan migrate --force 56 | 57 | # generate front-end assets 58 | npm ci 59 | php artisan wayfinder:generate 60 | npm run build 61 | 62 | # enable bref pgsql extension in lambda if you need it 63 | # https://bref.sh/docs/environment/php#extensions-installed-but-disabled-by-default 64 | # mkdir -p php/conf.d 65 | # echo "extension=pdo_pgsql" > php/conf.d/pgsql.ini 66 | 67 | # remove unnecessary files 68 | rm -rf node_modules 69 | rm -rf public/storage 70 | rm -rf resources/assets 71 | rm -rf resources/css 72 | rm -rf resources/images 73 | rm -rf resources/js 74 | rm -rf resources/ts 75 | rm -rf storage 76 | rm -rf tests 77 | rm -rf .git 78 | rm -rf .github 79 | rm database/database.sqlite 80 | 81 | # zip the laravel app 82 | zip -r "../laravel-app.zip" . 83 | 84 | cd .. 85 | 86 | # create environment variables json file 87 | echo '${{ secrets.CMS_ENVIRONMENT_VARIABLES_JSON }}' > environment-variables.json 88 | 89 | # create terraform config file 90 | cat < terraform.config 91 | bucket="us-west-2-terraform-state-storage" 92 | key="us-west-2-blog-cms-serverless.tfstate" 93 | region="us-west-2" 94 | dynamodb_table="us-west-2-terraform-state-locking" 95 | EOF 96 | 97 | # create terraform.tfvars file 98 | cat < terraform.tfvars 99 | app_name="${{ vars.CMS_APP_NAME }}" 100 | 101 | # VPC settings 102 | enable_vpc = true 103 | subnet_ids = ${{ vars.CMS_SUBNET_IDS }} 104 | security_group_ids = ${{ vars.CMS_SECURITY_GROUP_IDS }} 105 | 106 | # File storage settings 107 | enable_filesystem = true 108 | access_point_arn = "${{ vars.CMS_ACCESS_POINT_ARN }}" 109 | 110 | # api gateway settings 111 | certificate_arn = "${{ vars.CMS_CERTIFICATE_ARN }}" 112 | custom_domain_name = "${{ vars.CMS_CUSTOM_DOMAIN_NAME }}" 113 | 114 | # provider settings 115 | tag_service = "${{ vars.CMS_TAG_SERVICE }}" 116 | tag_environment = "${{ vars.CMS_TAG_ENVIRONMENT }}" 117 | tag_owner = "${{ vars.CMS_TAG_OWNER }}" 118 | 119 | # S3 bucket settings 120 | aws_bucket = "${{ vars.CMS_AWS_BUCKET }}" 121 | 122 | # Lambda settings 123 | environment_variables_json_file = "./environment-variables.json" 124 | 125 | filename="./laravel-app.zip" 126 | EOF 127 | 128 | # deploy 129 | terraform init -backend-config="./terraform.config" 130 | terraform apply -auto-approve 131 | 132 | # sync front-end assets to s3 bucket 133 | aws s3 sync laravel-app/public "s3://${{ vars.CMS_ASSET_AWS_BUCKET }}" 134 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # CloudWatch 3 | # 4 | resource "aws_cloudwatch_log_group" "web_log_group" { 5 | name = "/aws/lambda/${local.app_name}-web" 6 | retention_in_days = 1 7 | } 8 | 9 | resource "aws_cloudwatch_log_group" "artisan_log_group" { 10 | name = "/aws/lambda/${local.app_name}-artisan" 11 | retention_in_days = 1 12 | } 13 | 14 | resource "aws_cloudwatch_log_group" "jobs_worker_log_group" { 15 | name = "/aws/lambda/${local.app_name}-jobs-worker" 16 | retention_in_days = 1 17 | } 18 | 19 | resource "aws_cloudwatch_event_rule" "artisan_events_rule_schedule" { 20 | name = "${local.app_name}-artisan-schedule-runner" 21 | schedule_expression = "rate(1 day)" 22 | state = "ENABLED" 23 | } 24 | 25 | resource "aws_cloudwatch_event_target" "artisan_schedule" { 26 | target_id = "artisan-schedule" 27 | rule = aws_cloudwatch_event_rule.artisan_events_rule_schedule.name 28 | arn = aws_lambda_function.artisan_lambda_function.arn 29 | input = "\"schedule:run\"" 30 | } 31 | 32 | # 33 | # IAM 34 | # 35 | resource "aws_iam_role" "lambda_execution" { 36 | name = "${local.app_name}-lambda-role" 37 | 38 | assume_role_policy = jsonencode({ 39 | Version = "2012-10-17" 40 | Statement = [ 41 | { 42 | Effect = "Allow" 43 | Principal = { 44 | Service = [ 45 | "lambda.amazonaws.com" 46 | ] 47 | } 48 | Action = [ 49 | "sts:AssumeRole" 50 | ] 51 | } 52 | ] 53 | }) 54 | } 55 | 56 | resource "aws_iam_policy" "lambda_execution" { 57 | name = "${local.app_name}-lambda-policy" 58 | 59 | policy = jsonencode({ 60 | Version = "2012-10-17" 61 | Statement = [ 62 | { 63 | Action = [ 64 | "logs:CreateLogStream", 65 | "logs:CreateLogGroup", 66 | "logs:TagResource" 67 | ] 68 | Resource = [ 69 | join(":", [ 70 | "arn", 71 | data.aws_partition.current.id, 72 | "logs", 73 | data.aws_region.current.region, 74 | data.aws_caller_identity.current.account_id, 75 | "log-group", 76 | "/aws/lambda/${local.app_name}-*", 77 | "*" 78 | ]), 79 | ] 80 | Effect = "Allow" 81 | }, 82 | { 83 | Action = [ 84 | "logs:PutLogEvents" 85 | ] 86 | Resource = [ 87 | join(":", [ 88 | "arn", 89 | data.aws_partition.current.id, 90 | "logs", 91 | data.aws_region.current.region, 92 | data.aws_caller_identity.current.account_id, 93 | "log-group", 94 | "/aws/lambda/${local.app_name}-*", 95 | "*", 96 | "*" 97 | ]), 98 | ] 99 | Effect = "Allow" 100 | }, 101 | { 102 | Action = [ 103 | "s3:GetObject", 104 | "s3:PutObject", 105 | "s3:DeleteObject" 106 | ] 107 | Resource = "${data.aws_s3_bucket.aws_bucket.arn}/*" 108 | Effect = "Allow" 109 | }, 110 | { 111 | Action = [ 112 | "dynamodb:DescribeTable", 113 | "dynamodb:Query", 114 | "dynamodb:Scan", 115 | "dynamodb:GetItem", 116 | "dynamodb:PutItem", 117 | "dynamodb:UpdateItem", 118 | "dynamodb:DeleteItem" 119 | ] 120 | Resource = aws_dynamodb_table.cache_table.arn 121 | Effect = "Allow" 122 | }, 123 | { 124 | Action = [ 125 | "sqs:SendMessage", 126 | "sqs:ChangeMessageVisibility" 127 | ] 128 | Resource = aws_sqs_queue.jobs_queue.arn 129 | Effect = "Allow" 130 | }, 131 | { 132 | Action = [ 133 | "sqs:ReceiveMessage", 134 | "sqs:DeleteMessage", 135 | "sqs:GetQueueAttributes" 136 | ] 137 | Resource = aws_sqs_queue.jobs_queue.arn 138 | Effect = "Allow" 139 | }, 140 | { 141 | Action = [ 142 | "ec2:CreateNetworkInterface", 143 | "ec2:DescribeNetworkInterfaces", 144 | "ec2:DescribeSubnets", 145 | "ec2:DeleteNetworkInterface", 146 | "ec2:AssignPrivateIpAddresses", 147 | "ec2:UnassignPrivateIpAddresses", 148 | "ec2:DescribeSecurityGroups", 149 | "ec2:DescribeSubnets", 150 | "ec2:DescribeVpcs" 151 | ] 152 | Resource = "*" 153 | Effect = "Allow" 154 | } 155 | ] 156 | }) 157 | } 158 | 159 | resource "aws_iam_role_policy_attachment" "lambda_execution" { 160 | role = aws_iam_role.lambda_execution.name 161 | policy_arn = aws_iam_policy.lambda_execution.arn 162 | } 163 | 164 | # 165 | # Lambda 166 | # 167 | resource "aws_lambda_function" "web_lambda_function" { 168 | filename = var.filename 169 | source_code_hash = filesha256(var.filename) 170 | handler = "Bref\\LaravelBridge\\Http\\OctaneHandler" 171 | runtime = var.lambda_runtime 172 | function_name = "${local.app_name}-web" 173 | memory_size = 1024 174 | timeout = 28 175 | architectures = ["arm64"] 176 | role = aws_iam_role.lambda_execution.arn 177 | layers = [var.php_lambda_layer_arn] 178 | 179 | environment { 180 | variables = merge({ 181 | BREF_LOOP_MAX = "250" 182 | OCTANE_PERSIST_DATABASE_SESSIONS = "1" 183 | DYNAMODB_CACHE_TABLE = aws_dynamodb_table.cache_table.name 184 | SQS_QUEUE = aws_sqs_queue.jobs_queue.url 185 | }, jsondecode(file(var.environment_variables_json_file))) 186 | } 187 | 188 | dynamic "vpc_config" { 189 | for_each = var.enable_vpc ? ["apply"] : [] 190 | content { 191 | subnet_ids = var.subnet_ids 192 | security_group_ids = var.security_group_ids 193 | ipv6_allowed_for_dual_stack = true 194 | } 195 | } 196 | 197 | dynamic "file_system_config" { 198 | for_each = var.enable_filesystem ? ["apply"] : [] 199 | 200 | content { 201 | arn = var.access_point_arn 202 | local_mount_path = "/mnt/efs" 203 | } 204 | } 205 | } 206 | 207 | resource "aws_lambda_function" "artisan_lambda_function" { 208 | filename = var.filename 209 | source_code_hash = filesha256(var.filename) 210 | handler = "artisan" 211 | runtime = var.lambda_runtime 212 | function_name = "${local.app_name}-artisan" 213 | memory_size = 1024 214 | timeout = 720 215 | architectures = ["arm64"] 216 | role = aws_iam_role.lambda_execution.arn 217 | layers = [ 218 | var.php_lambda_layer_arn, 219 | var.console_lambda_layer_arn 220 | ] 221 | 222 | environment { 223 | variables = merge({ 224 | DYNAMODB_CACHE_TABLE = aws_dynamodb_table.cache_table.name 225 | SQS_QUEUE = aws_sqs_queue.jobs_queue.url 226 | }, jsondecode(file(var.environment_variables_json_file))) 227 | } 228 | 229 | dynamic "vpc_config" { 230 | for_each = var.enable_vpc ? ["apply"] : [] 231 | 232 | content { 233 | subnet_ids = var.subnet_ids 234 | security_group_ids = var.security_group_ids 235 | ipv6_allowed_for_dual_stack = true 236 | } 237 | } 238 | 239 | dynamic "file_system_config" { 240 | for_each = var.enable_filesystem ? ["apply"] : [] 241 | 242 | content { 243 | arn = var.access_point_arn 244 | local_mount_path = "/mnt/efs" 245 | } 246 | } 247 | } 248 | 249 | resource "aws_lambda_function" "jobs_worker_lambda_function" { 250 | filename = var.filename 251 | source_code_hash = filesha256(var.filename) 252 | handler = "Bref\\LaravelBridge\\Queue\\QueueHandler" 253 | runtime = var.lambda_runtime 254 | function_name = "${local.app_name}-jobs-worker" 255 | memory_size = 1024 256 | timeout = 60 257 | architectures = ["arm64"] 258 | role = aws_iam_role.lambda_execution.arn 259 | layers = [var.php_lambda_layer_arn] 260 | 261 | environment { 262 | variables = merge({ 263 | DYNAMODB_CACHE_TABLE = aws_dynamodb_table.cache_table.name 264 | SQS_QUEUE = aws_sqs_queue.jobs_queue.url 265 | }, jsondecode(file(var.environment_variables_json_file))) 266 | } 267 | 268 | dynamic "vpc_config" { 269 | for_each = var.enable_vpc ? ["apply"] : [] 270 | 271 | content { 272 | subnet_ids = var.subnet_ids 273 | security_group_ids = var.security_group_ids 274 | ipv6_allowed_for_dual_stack = true 275 | } 276 | } 277 | 278 | dynamic "file_system_config" { 279 | for_each = var.enable_filesystem ? ["apply"] : [] 280 | 281 | content { 282 | arn = var.access_point_arn 283 | local_mount_path = "/mnt/efs" 284 | } 285 | } 286 | } 287 | 288 | resource "aws_lambda_permission" "artisan_lambda_permission_events_rule_schedule" { 289 | function_name = aws_lambda_function.artisan_lambda_function.function_name 290 | action = "lambda:InvokeFunction" 291 | principal = "events.amazonaws.com" 292 | source_arn = aws_cloudwatch_event_rule.artisan_events_rule_schedule.arn 293 | } 294 | 295 | resource "aws_lambda_event_source_mapping" "jobs_worker_event_source_mapping_sqs_jobs_queue" { 296 | batch_size = 1 297 | maximum_batching_window_in_seconds = 0 298 | event_source_arn = aws_sqs_queue.jobs_queue.arn 299 | function_name = aws_lambda_function.jobs_worker_lambda_function.function_name 300 | enabled = true 301 | bisect_batch_on_function_error = false 302 | function_response_types = [ 303 | "ReportBatchItemFailures" 304 | ] 305 | } 306 | 307 | # 308 | # SQS 309 | # 310 | resource "random_string" "random" { 311 | length = 6 312 | special = false 313 | } 314 | 315 | resource "aws_sqs_queue" "jobs_queue" { 316 | name = "${local.app_name}-jobs-${random_string.random.result}" 317 | redrive_policy = jsonencode({ 318 | deadLetterTargetArn = aws_sqs_queue.jobs_dlq.arn 319 | maxReceiveCount = 3 320 | }) 321 | visibility_timeout_seconds = 360 322 | } 323 | 324 | resource "aws_sqs_queue" "jobs_dlq" { 325 | message_retention_seconds = 1209600 326 | name = "${local.app_name}-jobs-dlq-${random_string.random.result}" 327 | } 328 | 329 | # 330 | # DynamoDB 331 | # 332 | resource "aws_dynamodb_table" "cache_table" { 333 | name = "${local.app_name}-cache-table-${random_string.random.result}" 334 | billing_mode = "PAY_PER_REQUEST" 335 | hash_key = "id" 336 | 337 | attribute { 338 | name = "id" 339 | type = "S" 340 | } 341 | 342 | ttl { 343 | attribute_name = "ttl" 344 | enabled = true 345 | } 346 | } 347 | 348 | # 349 | # API Gateway 350 | # 351 | resource "aws_apigatewayv2_api" "http_api" { 352 | name = local.app_name 353 | protocol_type = "HTTP" 354 | disable_execute_api_endpoint = true 355 | ip_address_type = "dualstack" 356 | } 357 | 358 | resource "aws_apigatewayv2_stage" "http_api_stage" { 359 | api_id = aws_apigatewayv2_api.http_api.id 360 | name = "$default" 361 | auto_deploy = true 362 | 363 | default_route_settings { 364 | detailed_metrics_enabled = false 365 | throttling_burst_limit = 500 366 | throttling_rate_limit = 1000 367 | } 368 | } 369 | 370 | resource "aws_apigatewayv2_domain_name" "custom_domain" { 371 | domain_name = var.custom_domain_name 372 | 373 | domain_name_configuration { 374 | certificate_arn = var.certificate_arn 375 | endpoint_type = "REGIONAL" 376 | security_policy = "TLS_1_2" 377 | } 378 | } 379 | 380 | resource "aws_apigatewayv2_api_mapping" "custom_domain_mapping" { 381 | api_id = aws_apigatewayv2_api.http_api.id 382 | domain_name = aws_apigatewayv2_domain_name.custom_domain.id 383 | stage = aws_apigatewayv2_stage.http_api_stage.id 384 | } 385 | 386 | resource "aws_lambda_permission" "web_lambda_permission_http_api" { 387 | function_name = aws_lambda_function.web_lambda_function.function_name 388 | action = "lambda:InvokeFunction" 389 | principal = "apigateway.amazonaws.com" 390 | source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*" 391 | } 392 | 393 | resource "aws_apigatewayv2_integration" "http_api_integration_web" { 394 | api_id = aws_apigatewayv2_api.http_api.id 395 | integration_type = "AWS_PROXY" 396 | integration_uri = aws_lambda_function.web_lambda_function.invoke_arn 397 | payload_format_version = "2.0" 398 | timeout_milliseconds = 30000 399 | } 400 | 401 | resource "aws_apigatewayv2_route" "http_api_route_default" { 402 | api_id = aws_apigatewayv2_api.http_api.id 403 | route_key = "$default" 404 | target = join("/", ["integrations", aws_apigatewayv2_integration.http_api_integration_web.id]) 405 | } 406 | --------------------------------------------------------------------------------