├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── backend-server-deployments.md ├── continuous-deployments.md ├── diagrams │ ├── Deployment Options.drawio │ ├── Multi Zone - Independent Zones.png │ ├── Multi Zone - Shared Distribution and Bucket.png │ ├── Multi Zone - Shared Distribution.png │ └── Single Zone.png └── domain-config.md ├── makefile └── modules ├── legacy ├── README.md ├── data.tf ├── main.tf ├── modules │ ├── tf-aws-lambda │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── variables.tf │ │ └── versions.tf │ └── tf-aws-scheduled-lambda │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── variables.tf │ │ └── versions.tf ├── outputs.tf ├── scripts │ └── invalidate-cloudfront.sh ├── variables.tf └── versions.tf ├── tf-aws-lambda ├── data.tf ├── main.tf ├── outputs.tf ├── scripts │ └── update-alias.sh ├── variables.tf └── versions.tf ├── tf-aws-open-next-aliases ├── data.tf ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── tf-aws-open-next-multi-zone ├── README.md ├── data.tf ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── tf-aws-open-next-public-resources ├── code │ ├── auth │ │ └── index.js │ ├── cloudfrontFunctionOpenNextV3.js │ └── xForwardedHost.js ├── data.tf ├── main.tf ├── outputs.tf ├── scripts │ ├── invalidate-cloudfront.sh │ ├── promote-distribution.sh │ └── remove-continuous-deployment-policy-id.sh ├── variables.tf └── versions.tf ├── tf-aws-open-next-s3-assets ├── main.tf ├── outputs.tf ├── scripts │ ├── delete-folder.sh │ └── sync-file.sh ├── variables.tf └── versions.tf └── tf-aws-open-next-zone ├── README.md ├── data.tf ├── main.tf ├── outputs.tf ├── scripts ├── save-item-to-dynamo.sh └── update-parameter.sh ├── variables.tf └── versions.tf /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owner for all pull requests 2 | * @RJPearson94 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the module 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Technical Details 10 | 11 | - OS: [e.g. MacOS] 12 | - Terraform Version: [e.g. 0.12.26] 13 | - Provider Version [e.g. v0.1.0] 14 | - Module Version [e.g. v0.1.0] 15 | - Module Affected [e.g. tf-aws-open-next-zone] 16 | - Deployment Model [e.g. Single zone] 17 | 18 | ## Describe the bug 19 | 20 | A clear and concise description of what the bug is. Feel free to include Terraform configuration, logs, etc. 21 | 22 | ## Steps to Reproduce 23 | 24 | Steps to reproduce the behaviour: 25 | 26 | 1. Step 1 27 | 2. Step 2 28 | 3. See error 29 | 30 | ## Expected behaviour 31 | 32 | A clear and concise description of what you expected to happen. 33 | 34 | ## Logs 35 | 36 | Add logs of the error to help explain your problem. 37 | 38 | ## Additional context 39 | 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Is your feature request related to a problem 10 | 11 | A clear and concise description of what the problem is. 12 | 13 | ## Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. Optionally you can include example terraform configuration too 16 | 17 | ```hcl 18 | copy and paste an example terraform configuration here 19 | ``` 20 | 21 | ## Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "terraform" 4 | directory: "/modules/*" 5 | schedule: 6 | interval: "weekly" 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc 2 | examples 3 | config.json 4 | .DS_Store 5 | *.drawio.bkp 6 | .prototools 7 | 8 | # Open Next 9 | .open-next 10 | 11 | # Terraform 12 | .Terraform 13 | *tfstate* 14 | *tfvars 15 | *tfbackend 16 | examples/*/backend.tf 17 | .terraform.lock.hcl 18 | *.tftest.hcl 19 | 20 | # Terragrunt 21 | .terragrunt-cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Rob Pearson (RJPearson94) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Next Terraform 2 | 3 | This module deploys a next.js website using [Open Next](https://github.com/serverless-stack/open-next) to AWS utilising lambda, S3 and CloudFront. 4 | 5 | This repo contains modules that let you build single-zone or multi-zone websites using Open Next v1 (legacy module - this is deprecated), v2 and v3. 6 | 7 | Version 3 of the module supports many of the features released as part of Open Next v2 and V3, including: 8 | 9 | - Support for function splitting 10 | - Support for external middleware (Only Lambda@edge is supported) 11 | - Support for `open-next.output.json` configuration file 12 | - Support for server actions 13 | - Support for ISR revalidation 14 | - Support for Image Optimisation 15 | 16 | **NOTE:** Open Next v3 introduced additional deployment targets. This module only supports hosting resources in lambda and lambda@edge; running resources on Cloudflare, ECS, etc., is not supported. 17 | 18 | Where possible, the modules try to give you as many configuration options as possible; however, the module won't be able to cater to all use cases. For the majority of components, you can curate your bespoke resources in Terraform, i.e. WAF, CloudFront distribution, etc., and pass the ARN of the resource to the module to use. 19 | 20 | Some additional features of the module include: 21 | 22 | - CloudFront continuous deployments 23 | - AWS Developer Guide - https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/continuous-deployment.html 24 | - AWS Blog - https://aws.amazon.com/blogs/networking-and-content-delivery/use-cloudfront-continuous-deployment-to-safely-validate-cdn-changes/ 25 | - Lambda function URLs with IAM Auth 26 | - using lambda@edge auth function - AWS Blog - https://aws.amazon.com/blogs/compute/protecting-an-aws-lambda-function-url-with-amazon-cloudfront-and-lambdaedge/ 27 | - using Origin Access Control - AWS Blog - https://aws.amazon.com/blogs/networking-and-content-delivery/secure-your-lambda-function-urls-using-amazon-cloudfront-origin-access-control/ 28 | - WAF 29 | - Includes AWS recommended rules (AWSManagedRulesAmazonIpReputationList, AWSManagedRulesCommonRuleSet & AWSManagedRulesKnownBadInputsRuleSet) 30 | - Configure multiple IP rate-based rules 31 | - Configure SQL injection rules 32 | - Configure account takeover protection rules 33 | - Configure account creation fraud prevention rules 34 | - Configure basic auth 35 | - This is inspired by Vercel's password-protected deployment feature - https://vercel.com/guides/how-do-i-add-password-protection-to-my-vercel-deployment 36 | - Credit to Shinji Nakamatu for this idea - https://dev.to/snaka/implementing-secure-access-control-using-aws-waf-with-ip-address-and-basic-authentication-45hn 37 | - Custom rules (with custom response bodies), i.e. add a maintenance page to take the website offline when maintenance is taking place 38 | - Credit to Paul L for this idea - https://repost.aws/questions/QUeXIw1g0hSxiF0BpugsT7aw/how-to-implement-the-maintenance-page-using-route-53-to-switch-between-cloudfront-distributions 39 | - Configure default action (with custom response bodies) 40 | - Custom Error Pages 41 | 42 | And more 43 | 44 | If you plan to use a single-zone deployment model only, please use the [tf-aws-open-next-zone](./modules/tf-aws-open-next-zone) module. If you manage multiple zones in the same terraform state, please use the [tf-aws-open-next-multi-zone](./modules/tf-aws-open-next-multi-zone) module. Installation and module documentation can be found in the corresponding folders. 45 | 46 | *Note:* These modules use multiple bash scripts to delete data, upload data and mutate resources outside of Terraform. This prevents Terraform from removing resources when they have changed and allows the staging distribution functionality to work correctly. You must have bash and the AWS CLI available. The CLI must be configured to use the same credentials or environment variables as the default AWS provider for this functionality to work correctly. 47 | 48 | The module is available in the [Terraform registry](https://registry.terraform.io/modules/RJPearson94/open-next/aws/latest) 49 | 50 | ## Upgrading 51 | 52 | ### From 1.x to 2.x 53 | 54 | #### Use v1/ legacy module 55 | 56 | The v1, also known as the legacy module, has been kept to ensure that you can continue to use the module and receive some updates (will be determined on a case-by-case basis); you may want to use this instead of adopting the newer features. To use the legacy module, please add the following code. The existing variables and inputs have been retained. 57 | 58 | ```tf 59 | module "legacy" { 60 | source = "RJPearson94/open-next/aws//modules/legacy" 61 | version = ">= 2.0.0, < 3.0.0" 62 | 63 | ... 64 | } 65 | ``` 66 | 67 | #### Use new single-zone or multi-zone module 68 | 69 | When upgrading to v2 of the module, it is recommended that you redeploy your application into a new Terraform state and then shift traffic over due to the number of changes compared to the legacy module. If this is not possible, you can attempt to import the existing resources into your terraform state. See the [terraform docs](https://developer.hashicorp.com/terraform/language/state/import) for more information 70 | 71 | ### From 2.x to 3.x 72 | 73 | The module has been made backwards compatible with 2.x where possible. 74 | 75 | For open next v3, the module will read the `open-next.output.json` file in the .open-next directory to determine the edge and server functions that need to be configured, as well as any default configuration, e.g., streaming. 76 | 77 | **NOTE:** Deprecated fields have been removed. If you use one of the following values, you will need to modify your Terraform/ Terragrunt configuration 78 | 79 | - The `EDGE_LAMBDA` backend deployment type is no longer supported for server function. In open next v3, this has been moved into edge functions instead [BREAKING CHANGE] 80 | - `aws_lambda_permission.server_function_url_permission` and `aws_lambda_permission.image_optimisation_function_url_permission` have been merged into a list of `aws_lambda_permission.function_url_permission` resources in the `tf-aws-open-next-multi-zone` module. You can either let the resources re-create or update the references using either [move config](https://developer.hashicorp.com/terraform/tutorials/configuration-language/move-config), [import block](https://developer.hashicorp.com/terraform/language/import) or [import command](https://developer.hashicorp.com/terraform/cli/commands/import) [BREAKING CHANGE] 81 | - CloudFront cache policy ARN - you must now set the CloudFront Cache Policy ID 82 | - Auth function CloudFront log group - this configuration had no effect, so it has been removed 83 | - Lambda Logging configuration (for regional lambda's only) [NEW FEATURE] 84 | - Additional configuration of CloudWatch log groups, including having a log group for all regional functions in a zone [NEW FEATURE] 85 | 86 | If you are still using open next v2.x, you can set the `open_next_version` variable to `v2.x.x` (the default value). If you upgrade to Open Next v3.x, please set the `open_next_version` variable to `v3.x.x`. 87 | 88 | If you want to migrate from Open Next v2 to v3, see the [migration guide](https://open-next.js.org/migration#from-opennext-v2), which the Open Next team created. 89 | 90 | ## Deployment options 91 | 92 | Below are diagrams of the possible architecture combinations that can configured using v2 of this module. For each of the architectures, the following components are optional: 93 | 94 | - Route 53 (A and AAAA records) 95 | - WAF 96 | - Staging Distribution 97 | - Warmer function 98 | - Image Optimisation function 99 | - Tag to Path Mapping DB 100 | 101 | ### Single Zone 102 | 103 | ![Single Zone Complete](https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/v3.6.0/docs/diagrams/Single%20Zone.png) 104 | 105 | #### Terraform 106 | 107 | ```tf 108 | data "aws_caller_identity" "current" {} 109 | 110 | module "single_zone" { 111 | source = "RJPearson94/open-next/aws//modules/tf-aws-open-next-zone" 112 | version = ">= 2.0.0, < 3.0.0" 113 | 114 | prefix = "open-next-${data.aws_caller_identity.current.account_id}" 115 | folder_path = "./.open-next" 116 | } 117 | ``` 118 | 119 | #### Terragrunt 120 | 121 | ```hcl 122 | terraform { 123 | source = "tfr://registry.terraform.io/RJPearson94/open-next/aws//modules/tf-aws-open-next-zone?version=v3.6.0" 124 | include_in_copy = ["./.open-next"] 125 | } 126 | 127 | inputs = { 128 | prefix = "open-next-${get_aws_account_id()}" 129 | folder_path = "./.open-next" 130 | } 131 | ``` 132 | 133 | ### Multi-Zone - Independent Zones 134 | 135 | ![Multi Zone - Independent Zones](https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/v3.6.0/docs/diagrams/Multi%20Zone%20-%20Independent%20Zones.png) 136 | 137 | #### Terraform 138 | 139 | ```tf 140 | data "aws_caller_identity" "current" {} 141 | 142 | module "independent_zones" { 143 | source = "RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone" 144 | version = ">= 2.0.0, < 3.0.0" 145 | 146 | prefix = "open-next-ind-${data.aws_caller_identity.current.account_id}" 147 | deployment = "INDEPENDENT_ZONES" 148 | 149 | zones = [{ 150 | root = true 151 | name = "home" 152 | folder_path = "./home/.open-next" 153 | },{ 154 | root = false 155 | name = "docs" 156 | folder_path = "./docs/.open-next" 157 | }] 158 | } 159 | ``` 160 | 161 | #### Terragrunt 162 | 163 | ```hcl 164 | terraform { 165 | source = "tfr://registry.terraform.io/RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone?version=v3.6.0" 166 | include_in_copy = ["./docs/.open-next", "./home/.open-next"] 167 | } 168 | 169 | inputs = { 170 | prefix = "open-next-ind-${get_aws_account_id()}" 171 | deployment = "INDEPENDENT_ZONES" 172 | 173 | zones = [{ 174 | root = true 175 | name = "home" 176 | folder_path = "./home/.open-next" 177 | },{ 178 | root = false 179 | name = "docs" 180 | folder_path = "./docs/.open-next" 181 | }] 182 | } 183 | ``` 184 | 185 | _Note:_ If you use tools like Terragrunt or CDKTF, you can use the Single Zone module to deploy each zone into its own terraform state 186 | 187 | ### Multi-Zone - Shared Distribution 188 | 189 | ![Multi Zone - Shared Distribution](https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/v3.6.0/docs/diagrams/Multi%20Zone%20-%20Shared%20Distribution.png) 190 | 191 | #### Terraform 192 | 193 | ```tf 194 | data "aws_caller_identity" "current" {} 195 | 196 | module "shared_distribution" { 197 | source = "RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone" 198 | version = ">= 2.0.0, < 3.0.0" 199 | 200 | prefix = "open-next-sd-${data.aws_caller_identity.current.account_id}" 201 | deployment = "SHARED_DISTRIBUTION" 202 | 203 | zones = [{ 204 | root = true 205 | name = "home" 206 | folder_path = "./home/.open-next" 207 | },{ 208 | root = false 209 | name = "docs" 210 | folder_path = "./docs/.open-next" 211 | }] 212 | } 213 | ``` 214 | 215 | #### Terragrunt 216 | 217 | ```hcl 218 | terraform { 219 | source = "tfr://registry.terraform.io/RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone?version=v3.6.0" 220 | include_in_copy = ["./docs/.open-next", "./home/.open-next"] 221 | } 222 | 223 | inputs = { 224 | prefix = "open-next-sd-${get_aws_account_id()}" 225 | deployment = "SHARED_DISTRIBUTION" 226 | 227 | zones = [{ 228 | root = true 229 | name = "home" 230 | folder_path = "./home/.open-next" 231 | },{ 232 | root = false 233 | name = "docs" 234 | folder_path = "./docs/.open-next" 235 | }] 236 | } 237 | ``` 238 | 239 | ### Multi-Zone - Shared Distribution and Bucket 240 | 241 | ![Multi Zone - Shared Distribution and Bucket](https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/v3.6.0/docs/diagrams/Multi%20Zone%20-%20Shared%20Distribution%20and%20Bucket.png) 242 | 243 | #### Terraform 244 | 245 | ```tf 246 | data "aws_caller_identity" "current" {} 247 | 248 | module "shared_distribution_and_bucket" { 249 | source = "RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone" 250 | version = ">= 2.0.0, < 3.0.0" 251 | 252 | prefix = "open-next-sb-${data.aws_caller_identity.current.account_id}" 253 | deployment = "SHARED_DISTRIBUTION_AND_BUCKET" 254 | 255 | zones = [{ 256 | root = true 257 | name = "home" 258 | folder_path = "./home/.open-next" 259 | },{ 260 | root = false 261 | name = "docs" 262 | folder_path = "./docs/.open-next" 263 | }] 264 | } 265 | ``` 266 | 267 | #### Terragrunt 268 | 269 | ```hcl 270 | terraform { 271 | source = "tfr://registry.terraform.io/RJPearson94/open-next/aws//modules/tf-aws-open-next-multi-zone?version=2.0.2" 272 | include_in_copy = ["./docs/.open-next", "./home/.open-next"] 273 | } 274 | 275 | inputs = { 276 | prefix = "open-next-sb-${get_aws_account_id()}" 277 | deployment = "SHARED_DISTRIBUTION_AND_BUCKET" 278 | 279 | zones = [{ 280 | root = true 281 | name = "home" 282 | folder_path = "./home/.open-next" 283 | },{ 284 | root = false 285 | name = "docs" 286 | folder_path = "./docs/.open-next" 287 | }] 288 | } 289 | ``` 290 | 291 | ## Custom Domains 292 | 293 | For information on managing custom domains, see the [domain config documentation](https://github.com/RJPearson94/terraform-aws-open-next/blob/v3.6.0/docs/domain-config.md) 294 | 295 | ## Examples 296 | 297 | The examples have been moved to a separate repository to reduce the amount of code that Terraform downloads. You can find them at [RJPearson94/terraform-aws-open-next-examples repo](https://github.com/RJPearson94/terraform-aws-open-next-examples) 298 | -------------------------------------------------------------------------------- /docs/backend-server-deployments.md: -------------------------------------------------------------------------------- 1 | # Backend Server Deployment Options 2 | 3 | Several options exist to deploy the backend. 4 | 5 | By default, all zones will use the same deployment options; however, you can override the deployment options for each zone. See the module documentation below for more information. 6 | 7 | The backend options are as follows: 8 | 9 | ## Lambda function URLs (with no auth) 10 | 11 | Lambda function URL (with no auth) is supported for both the Server and Image Optimisation functions. This is the default deployment model. To configure this, please add the following configuration. 12 | 13 | ```tf 14 | ... 15 | server_function = { 16 | backend_deployment_type = "REGIONAL_LAMBDA" 17 | ... 18 | } 19 | 20 | image_optimisation_function = { 21 | backend_deployment_type = "REGIONAL_LAMBDA" 22 | ... 23 | } 24 | ``` 25 | 26 | _Note:_ This will deploy the corresponding function without any auth 27 | 28 | ## Lambda function URL with IAM Auth (using CloudFront Origin Access Control) 29 | 30 | CloudFront has added support for Origin Access Control for lambda function URLs. This is supported for both the Server and Image Optimisation functions. To configure this, please add the following configuration. 31 | 32 | ```tf 33 | ... 34 | server_function = { 35 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_OAC" 36 | ... 37 | } 38 | 39 | image_optimisation_function = { 40 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_OAC" 41 | ... 42 | } 43 | ``` 44 | 45 | **NOTE:** If you make a PUT or POST request to your backend, then the OAC might not be suitable as you must provide a signed payload as CloudFront doesn't currently support this; see [docs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html) for more details. If you see the following error: `The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.` you will either need to find a way to sign the body using an AWS Secret Access Key or use either the Lambda@edge auth function (backend_deployment_type = 'REGIONAL_LAMBDA_WITH_AUTH_LAMBDA') or do not have auth on the lambda URLs (backend_deployment_type = 'REGIONAL_LAMBDA') 46 | 47 | ## Lambda function URL with IAM Auth (using CloudFront Origin Access Control and allow any principal) 48 | 49 | This deployment option has been included to allow users to safely migrate to using CloudFront OACs as the auth method for lambda URLs. Because resources are moving to prevent cyclic dependencies, there is a risk that requests will fail while the lambda permissions are updated and the CloudFront changes are propagating. 50 | 51 | To mitigate this, a new backend deployment type, `REGIONAL_LAMBDA_WITH_OAC_AND_ANY_PRINCIPAL,` was introduced to allow you to have both any principal permitted (used for the auth function and public lambda URLs) and the OAC associated with the CloudFront distributions. 52 | 53 | To configure this, please add the following configuration. 54 | 55 | ```tf 56 | ... 57 | server_function = { 58 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_OAC_AND_ANY_PRINCIPAL" 59 | ... 60 | } 61 | 62 | image_optimisation_function = { 63 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_OAC_AND_ANY_PRINCIPAL" 64 | ... 65 | } 66 | ``` 67 | 68 | **NOTE:** This is meant to aid with migrating server and image optimisation functions that were previously deployed with `REGIONAL_LAMBDA_WITH_AUTH_LAMBDA` or `REGIONAL_LAMBDA` to using `REGIONAL_LAMBDA_WITH_OAC`. 69 | 70 | Assuming you already have resources deployed, you can update your server and image optimisation functions configuration to set the `backend_deployment_type` to `REGIONAL_LAMBDA_WITH_OAC_AND_ANY_PRINCIPAL`. 71 | 72 | After applying this change, you can update your server and image optimisation functions configuration to set the `backend_deployment_type` to `REGIONAL_LAMBDA_WITH_OAC`. You must apply these changes to remove the permission to allow any principal to invoke the lambda. 73 | 74 | ## Lambda function URLs with IAM Auth (using lambda@edge auth function) 75 | 76 | Some companies only allow lambda URLs to be configured with authorisation. Hence, AWS released a [blog post](https://aws.amazon.com/blogs/compute/protecting-an-aws-lambda-function-url-with-amazon-cloudfront-and-lambdaedge/) which demonstrated how you could use an auth function (running as lambda@edge) to generate the SigV4 required to call the sever function with the correct Authorisation header. To configure this, please add the following configuration. 77 | 78 | ```tf 79 | ... 80 | server_function = { 81 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_AUTH_LAMBDA" 82 | ... 83 | } 84 | 85 | image_optimisation_function = { 86 | backend_deployment_type = "REGIONAL_LAMBDA_WITH_AUTH_LAMBDA" 87 | ... 88 | } 89 | ``` 90 | 91 | Using this deployment model does add additional resources and cost; however, this is a workaround until CloudFront natively supports generating signatures to call a lambda protected by IAM auth. 92 | 93 | ## Lambda@edge (server function only) 94 | 95 | Please add the following configuration to run the server function as a lambda@edge function. 96 | 97 | ```tf 98 | provider "aws" { 99 | alias = "server_function" 100 | region = "us-east-1" # For lambda@edge to be used, the region must be set to us-east-1 101 | } 102 | 103 | ... 104 | server_function = { 105 | backend_deployment_type = "EDGE_LAMBDA" 106 | ... 107 | } 108 | ``` 109 | 110 | As lambda@edge does not support environment variables, the module will inject them at the top of the server code before it is uploaded to AWS. Credit to SST for the inspiration behind this. [Link](https://github.com/sst/sst/blob/3b792053d90c49d9ca693308646a3389babe9ceb/packages/sst/src/constructs/EdgeFunction.ts#L193) 111 | -------------------------------------------------------------------------------- /docs/continuous-deployments.md: -------------------------------------------------------------------------------- 1 | # Continuous deployment 2 | 3 | For this functionality to work correctly, lifecycle rules have been added to the production distribution to ignore changes to origins, ordered_cache_behaviors, default_cache_behavior and custom_error_responses. For Terraform to be able to update the distribution, you will need to update the staging distribution and then promote the changes. 4 | 5 | ## Initial deployment 6 | 7 | AWS doesn't allow you to attach the continuous deployment policy to the production distribution on the first deployment. Therefore, you will need to set the deployment to `NONE`. To configure this, please add the following configuration. 8 | 9 | ```tf 10 | continuous_deployment = { 11 | use = true 12 | deployment = "NONE" 13 | } 14 | ``` 15 | 16 | ## Use staging distribution 17 | 18 | When you want to create/ use the staging distribution, there are two options for shifting traffic to the staging distribution header or by weight (up to 15% at the time of writing). To configure this, please add the following configuration. 19 | 20 | ```tf 21 | continuous_deployment = { 22 | use = true 23 | deployment = "ACTIVE" 24 | traffic_config = { 25 | header = { 26 | name = "aws-cf-cd-staging" # Update the header name with a value of your choice. Currently, AWS enforce the header starts with `aws-cf-cd` 27 | value = "true" # Update the header value with a value of your choice. 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | or 34 | 35 | ```tf 36 | continuous_deployment = { 37 | use = true 38 | deployment = "ACTIVE" 39 | traffic_config = { 40 | weight = { 41 | percentage = "0.10" 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | For weighted deployments, you can also configure session stickiness. See the documentation below for more information. 48 | 49 | *Note:* You can update the staging distribution multiple times before promoting the changes. 50 | 51 | ## Promotion 52 | 53 | Please add the following configuration to promote the staging distribution. 54 | 55 | ```tf 56 | continuous_deployment = { 57 | use = true 58 | deployment = "PROMOTE" 59 | traffic_config = { 60 | weight = { 61 | percentage = "0.10" 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## Detach staging distribution 68 | 69 | You must remove the continuous deployment policy from the production distribution to remove the staging distribution. To do this, you must set the deployment to `DETACH`. Please add the following configuration. 70 | 71 | ```tf 72 | continuous_deployment = { 73 | use = true 74 | deployment = "DETACH" 75 | traffic_config = { 76 | weight = { 77 | percentage = "0.10" 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | *Note:* you can detach the staging distribution without promoting the changes to the production distribution. This will remove the continuous deployment policy, shifting 100% of traffic back to the production distribution. 84 | 85 | ## Remove staging distribution 86 | 87 | Please add the following configuration to remove the staging distribution. 88 | 89 | ```tf 90 | continuous_deployment = { 91 | use = true 92 | deployment = "NONE" 93 | } 94 | ``` 95 | 96 | *Note:* Please detach the staging distribution before removing it, otherwise Terraform may fail if the production distribution is retained. 97 | 98 | -------------------------------------------------------------------------------- /docs/diagrams/Multi Zone - Independent Zones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/c7f414cadd4c7fe5b0513490e4684760b37af6bb/docs/diagrams/Multi Zone - Independent Zones.png -------------------------------------------------------------------------------- /docs/diagrams/Multi Zone - Shared Distribution and Bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/c7f414cadd4c7fe5b0513490e4684760b37af6bb/docs/diagrams/Multi Zone - Shared Distribution and Bucket.png -------------------------------------------------------------------------------- /docs/diagrams/Multi Zone - Shared Distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/c7f414cadd4c7fe5b0513490e4684760b37af6bb/docs/diagrams/Multi Zone - Shared Distribution.png -------------------------------------------------------------------------------- /docs/diagrams/Single Zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RJPearson94/terraform-aws-open-next/c7f414cadd4c7fe5b0513490e4684760b37af6bb/docs/diagrams/Single Zone.png -------------------------------------------------------------------------------- /docs/domain-config.md: -------------------------------------------------------------------------------- 1 | # Certificates and Domain 2 | 3 | *NOTE:* If you do not need a custom domain, please ignore this documentation. The autogenerated CloudFront distribution URL can be used instead. 4 | 5 | Due to the variety of DNS providers and approaches to configuring DNS records, the module has been designed to handle many scenarios. 6 | 7 | By default, the module assumes you want to create and manage custom domains (including www subdomains, custom subdomains, etc.) using Route53. The sections below outline examples of changing the default configuration for managing custom domains. 8 | 9 | At the time of writing, the module does not support creating and validating ACM certificates. Therefore, you must supply the ARN of an ACM certificate (hosted in us-east-1) to attach to the CloudFront distribution. The example below shows the minimum configuration to provide the ACM certificate ARN. 10 | 11 | ```tf 12 | domain_config = { 13 | hosted_zones = [{ 14 | name = "test.co.uk" 15 | }] 16 | viewer_certificate = { 17 | acm_certificate_arn = "<" 18 | } 19 | } 20 | ``` 21 | 22 | If `ipv6_enabled` is enabled (default of true), then A and AAAA records will be created in Route53. If false, only A record(s) will be created. 23 | 24 | ## Disabling the creation of Route 53 records 25 | 26 | *NOTE:* By default, the module will create route53 records 27 | 28 | Under specific scenarios, you may want a custom domain to be associated with your single or multi-zone application; however, you do not want the Terraform module to manage the creation of Route53 records. Therefore, you can specify the following configuration to turn off the creation of the records. 29 | 30 | ```tf 31 | domain_config = { 32 | create_route53_entries = false 33 | hosted_zones = [{ 34 | name = "test.co.uk" 35 | }] 36 | viewer_certificate = { 37 | acm_certificate_arn = "<" 38 | } 39 | } 40 | ``` 41 | 42 | ## Including the www subdomain 43 | 44 | *NOTE:* By default, this is disabled 45 | 46 | You may want a custom subdomain and include the `www` subdomain. Therefore, you can specify the following configuration to create Route53 records and CloudFront distribution aliases for `www.website.test.co.uk` and `website.test.co.uk` 47 | 48 | ```tf 49 | domain_config = { 50 | include_www = true 51 | sub_domain = "website" 52 | hosted_zones = [{ 53 | name = "test.co.uk" 54 | }] 55 | viewer_certificate = { 56 | acm_certificate_arn = "<" 57 | } 58 | } 59 | ``` 60 | 61 | The subdomain argument can be omitted if you do not need a subdomain. Therefore, you can specify the following configuration to create Route53 records and CloudFront distribution aliases for `www.test.co.uk` and `test.co.uk` 62 | 63 | ```tf 64 | domain_config = { 65 | include_www = true 66 | hosted_zones = [{ 67 | name = "test.co.uk" 68 | }] 69 | viewer_certificate = { 70 | acm_certificate_arn = "<" 71 | } 72 | } 73 | ``` 74 | 75 | ## Specifying a subdomain 76 | 77 | You may want to create a subdomain for your website. Therefore, you can specify the following configuration to create Route53 records and CloudFront distribution aliases for `website.test.co.uk` 78 | 79 | ```tf 80 | domain_config = { 81 | sub_domain = "website" 82 | hosted_zones = [{ 83 | name = "test.co.uk" 84 | }] 85 | viewer_certificate = { 86 | acm_certificate_arn = "<" 87 | } 88 | } 89 | ``` 90 | 91 | ## Using hosted zone name, i.e. no subdomain 92 | 93 | For a given single or multi-zone application, you may want to avoid creating a subdomain for your website. Therefore, you can specify the following configuration to create Route53 records and CloudFront distribution aliases for `test.co.uk` 94 | 95 | ```tf 96 | domain_config = { 97 | hosted_zones = [{ 98 | name = "test.co.uk" 99 | }] 100 | viewer_certificate = { 101 | acm_certificate_arn = "<" 102 | } 103 | } 104 | ``` 105 | 106 | ## Overwriting Route53 records 107 | 108 | *NOTE:* By default, this is disabled 109 | 110 | If a record already exists in Route53 that you want to use for your single or multi-zone application, you may see the following error. 111 | 112 | ```sh 113 | creating Route 53 Record: InvalidChangeBatch: [Tried to create resource record set [name='test.com.', type='A'] but it already exists] 114 | ``` 115 | 116 | You can import the resource into the Terraform state file or use the `route53_record_allow_overwrite` argument of the `domain_config` variable to force Route53 to overwrite the existing records. An example terraform configuration can be seen below. 117 | 118 | ```tf 119 | domain_config = { 120 | route53_record_allow_overwrite = true 121 | hosted_zones = [{ 122 | name = "test.co.uk" 123 | }] 124 | viewer_certificate = { 125 | acm_certificate_arn = "<" 126 | } 127 | } 128 | ``` 129 | 130 | *NOTE:* Although this is possible, the Terraform docs do not recommend this for most environments 131 | 132 | ## Configuring public and private hosted zones 133 | 134 | For each zone you can specify if the zone is a public hosted zone (default/ false) or a private hosted zone (true). The example terraform configuration below shows how to configure DNS records for both the public and private hosted zone for `test.co.uk` 135 | 136 | ```tf 137 | domain_config = { 138 | route53_record_allow_overwrite = true 139 | hosted_zones = [{ 140 | name = "test.co.uk" 141 | },{ 142 | name = "test.co.uk" 143 | private_zone = false 144 | }] 145 | viewer_certificate = { 146 | acm_certificate_arn = "<" 147 | } 148 | } 149 | ``` 150 | 151 | *NOTE:* The module does not currently support having different subdomains for each hosted zone. -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | @echo "==> Format Terraform Code" 3 | terraform fmt -recursive 4 | 5 | .PHONY: fmt -------------------------------------------------------------------------------- /modules/legacy/README.md: -------------------------------------------------------------------------------- 1 | # Open Next Terraform - Legacy 2 | 3 | **WARNING:** This module is deprecated and will be removed in v4 of this module 4 | 5 | This module deploys a next.js website using [Open Next](https://github.com/serverless-stack/open-next) to AWS utilising lambda, S3 and CloudFront. 6 | 7 | This module will build the corresponding resources to host the single-zone or multi-zone website; several options exist to deploy the backend. The options are: 8 | 9 | - Lambda function URLs (with no auth) 10 | - HTTP API Gateway (with proxy integrations to lambda functions) 11 | - Lambda@edge (server function only) 12 | 13 | **NOTE:** If lambda@edge is used, then the warmer function is not deployed 14 | 15 | The script to invalidate the CloudFront distribution uses bash, [AWS CLI](https://aws.amazon.com/cli/) and [jq](https://github.com/jqlang/jq). The invalidation script and Terraform apply will fail if the script fails to run. 16 | 17 | As part of Terraform 2.2 Open Next introduced a DynamoDB for Tag mapping as part of ISR. 18 | 19 | If you are using version 2.1 or 2.0, please add the following to your Terraform/ Terragrunt configuration 20 | 21 | ```tf 22 | ... 23 | 24 | create = true 25 | tag_mapping_db = { 26 | create = false 27 | } 28 | 29 | ... 30 | ``` 31 | 32 | If you are using 1.x, please add the following to your Terraform/ Terragrunt configuration 33 | 34 | ```tf 35 | ... 36 | 37 | isr = { 38 | create = false 39 | } 40 | 41 | ... 42 | ``` 43 | 44 | The module is available in the [Terraform registry](https://registry.terraform.io/modules/RJPearson94/open-next/aws/latest). Please use 1.x of the module. 45 | 46 | *Note:* it is recommended that you upgrade to either the [tf-aws-open-next-zone](./modules/tf-aws-open-next-zone) module or [tf-aws-open-next-multi-zone](./modules/tf-aws-open-next-multi-zone) module. 47 | 48 | ## Module documentation 49 | 50 | Below is the documentation for the Terraform module, outlining the providers, modules and resources required to deploy the website. The documentation includes the inputs that can be supplied (including any defaults) and what is outputted from the module. 51 | 52 | **NOTE:** The module will zip all the necessary open-next artefacts as part of a Terraform deployment. To facilitate this, the .open-next folders need to be stored locally. 53 | 54 | You must configure the AWS providers four times because some organisations use different accounts or roles for IAM, DNS, etc. The module has been designed to cater for these requirements. The server function is a separate provider to allow your backend resources to be deployed to a region, i.e. eu-west-1, and deploy the server function to another region, i.e. us-east-1, for lambda@edge. 55 | 56 | Below is an example setup. 57 | 58 | ```tf 59 | provider "aws" { 60 | 61 | } 62 | 63 | provider "aws" { 64 | alias = "server_function" 65 | } 66 | 67 | provider "aws" { 68 | alias = "iam" 69 | } 70 | 71 | provider "aws" { 72 | alias = "dns" 73 | } 74 | ``` 75 | 76 | ### Requirements 77 | 78 | | Name | Version | 79 | |------|---------| 80 | | [terraform](#requirement\_terraform) | >= 1.4.0 | 81 | | [archive](#requirement\_archive) | >= 2.3.0 | 82 | | [aws](#requirement\_aws) | >= 4.67.0 | 83 | 84 | ### Providers 85 | 86 | | Name | Version | 87 | |------|---------| 88 | | [archive](#provider\_archive) | >= 2.3.0 | 89 | | [aws](#provider\_aws) | >= 4.67.0 | 90 | | [aws.dns](#provider\_aws.dns) | >= 4.67.0 | 91 | | [terraform](#provider\_terraform) | n/a | 92 | 93 | ### Modules 94 | 95 | | Name | Source | Version | 96 | |------|--------|---------| 97 | | [image\_optimisation\_function](#module\_image\_optimisation\_function) | ./modules/tf-aws-lambda | n/a | 98 | | [revalidation\_function](#module\_revalidation\_function) | ./modules/tf-aws-lambda | n/a | 99 | | [server\_function](#module\_server\_function) | ./modules/tf-aws-lambda | n/a | 100 | | [warmer\_function](#module\_warmer\_function) | ./modules/tf-aws-scheduled-lambda | n/a | 101 | 102 | ### Resources 103 | 104 | | Name | Type | 105 | |------|------| 106 | | [aws_apigatewayv2_api.api](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api) | resource | 107 | | [aws_apigatewayv2_deployment.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_deployment) | resource | 108 | | [aws_apigatewayv2_stage.stable](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stage) | resource | 109 | | [aws_cloudfront_cache_policy.cache_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_cache_policy) | resource | 110 | | [aws_cloudfront_distribution.website_distribution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource | 111 | | [aws_cloudfront_function.x_forwarded_host](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function) | resource | 112 | | [aws_cloudfront_origin_access_control.website_origin_access_control](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource | 113 | | [aws_dynamodb_table.dynamodb_table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | 114 | | [aws_dynamodb_table_item.isr_table_item](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table_item) | resource | 115 | | [aws_lambda_event_source_mapping.revalidation_queue_source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | 116 | | [aws_lambda_permission.image_optimisation_function_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | 117 | | [aws_lambda_permission.server_function_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | 118 | | [aws_route53_record.route53_a_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 119 | | [aws_route53_record.route53_aaaa_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 120 | | [aws_s3_bucket.website_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 121 | | [aws_s3_bucket_policy.website_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | 122 | | [aws_s3_object.cache_asset](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | 123 | | [aws_s3_object.website_asset](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | 124 | | [aws_sqs_queue.revalidation_queue](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | 125 | | terraform_data.invalidate_distribution | resource | 126 | | [archive_file.image_optimization_function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 127 | | [archive_file.revalidation_function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 128 | | [archive_file.server_function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 129 | | [archive_file.warmer_function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 130 | | [aws_cloudfront_cache_policy.caching_optimized](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source | 131 | | [aws_cloudfront_origin_request_policy.all_viewer_except_host_header](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_origin_request_policy) | data source | 132 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 133 | | [aws_route53_zone.hosted_zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | 134 | 135 | ### Inputs 136 | 137 | | Name | Description | Type | Default | Required | 138 | |------|-------------|------|---------|:--------:| 139 | | [cache\_control\_immutable\_assets\_regex](#input\_cache\_control\_immutable\_assets\_regex) | Regex to set public,max-age=31536000,immutable on immutable resources | `string` | `"^.*(\\.js|\\.css|\\.woff2)$"` | no | 140 | | [cloudfront](#input\_cloudfront) | Configuration for the CloudFront distribution |
object({
enabled = optional(bool, true)
invalidate_on_change = optional(bool, true)
minimum_protocol_version = optional(string, "TLSv1.2_2021")
ssl_support_method = optional(string, "sni-only")
http_version = optional(string, "http2and3")
ipv6_enabled = optional(bool, true)
price_class = optional(string, "PriceClass_100")
geo_restrictions = optional(object({
type = optional(string, "none"),
locations = optional(list(string), [])
}), {})
})
| `{}` | no | 141 | | [cloudwatch\_log](#input\_cloudwatch\_log) | Override the Cloudwatch logs configuration |
object({
retention_in_days = number
})
|
{
"retention_in_days": 7
}
| no | 142 | | [content\_types](#input\_content\_types) | The MIME type mapping and default for artefacts generated by Open Next |
object({
mapping = optional(map(string), {
"svg" = "image/svg+xml",
"js" = "application/javascript",
"css" = "text/css",
})
default = optional(string, "binary/octet-stream")
})
| `{}` | no | 143 | | [domain](#input\_domain) | Configuration to for attaching a custom domain to the CloudFront distribution |
object({
create = optional(bool, false)
hosted_zone_name = optional(string),
name = optional(string),
alternate_names = optional(list(string), [])
acm_certificate_arn = optional(string),
evaluate_target_health = optional(bool, false)
})
| `{}` | no | 144 | | [force\_destroy](#input\_force\_destroy) | Whether to allow force destruction of resources i.e. website bucket where all objects should be deleted from the bucket when the bucket is destroyed | `bool` | `false` | no | 145 | | [iam](#input\_iam) | Override the default IAM configuration |
object({
path = optional(string, "/")
permissions_boundary = optional(string)
})
| `{}` | no | 146 | | [image\_optimisation\_function](#input\_image\_optimisation\_function) | Configuration for the image optimisation function |
object({
runtime = optional(string, "nodejs18.x")
deployment = optional(string, "REGIONAL_LAMBDA")
timeout = optional(number, 25)
memory_size = optional(number, 1536)
additional_environment_variables = optional(map(string), {})
additional_iam_policies = optional(list(object({
name = string,
arn = optional(string)
policy = optional(string)
})), [])
vpc = optional(object({
security_group_ids = list(string),
subnet_ids = list(string)
}))
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}), {})
})
| `{}` | no | 147 | | [isr](#input\_isr) | Configuration for ISR, including creation and function config. To use ISR you need to use at least 2.x of Open Next, for 1.x please set create to false |
object({
create = bool
revalidation_function = optional(object({
runtime = optional(string, "nodejs18.x")
deployment = optional(string, "REGIONAL_LAMBDA")
timeout = optional(number, 30)
memory_size = optional(number, 128)
additional_environment_variables = optional(map(string), {})
additional_iam_policies = optional(list(object({
name = string,
arn = optional(string)
policy = optional(string)
})), [])
vpc = optional(object({
security_group_ids = list(string),
subnet_ids = list(string)
}))
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}), {})
}), {})
tag_mapping_db = optional(object({
create = bool
billing_mode = optional(string, "PAY_PER_REQUEST")
read_capacity = optional(number)
write_capacity = optional(number)
}), {
create = true
})
})
|
{
"create": true
}
| no | 148 | | [open\_next](#input\_open\_next) | The next.js website config for single and multi-zone deployments |
object({
exclusion_regex = optional(string)
root_folder_path = string
additional_zones = optional(list(object({
name = string
http_path = string
folder_path = string
})), [])
})
| n/a | yes | 149 | | [preferred\_architecture](#input\_preferred\_architecture) | Preferred instruction set architecture for the lambda function. If lambda@edge is used for the server function, the architecture will be set to x86\_64 for that function | `string` | `"arm64"` | no | 150 | | [prefix](#input\_prefix) | A prefix which will be attached to the resource name to ensure resources are random | `string` | `null` | no | 151 | | [server\_function](#input\_server\_function) | Configuration for the server function |
object({
runtime = optional(string, "nodejs18.x")
deployment = optional(string, "REGIONAL_LAMBDA")
timeout = optional(number, 10)
memory_size = optional(number, 1024)
additional_environment_variables = optional(map(string), {})
additional_iam_policies = optional(list(object({
name = string,
arn = optional(string)
policy = optional(string)
})), [])
vpc = optional(object({
security_group_ids = list(string),
subnet_ids = list(string)
}))
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}), {})
})
| `{}` | no | 152 | | [suffix](#input\_suffix) | A suffix which will be attached to the resource name to ensure resources are random | `string` | `null` | no | 153 | | [vpc](#input\_vpc) | The default VPC configuration for the lambda resources. This can be overridden for each function |
object({
security_group_ids = list(string),
subnet_ids = list(string)
})
| `null` | no | 154 | | [waf](#input\_waf) | Configuration for the CloudFront distribution WAF |
object({
web_acl_id = optional(string)
})
| `{}` | no | 155 | | [warmer\_function](#input\_warmer\_function) | Configuration for the warmer function |
object({
create = bool
runtime = optional(string, "nodejs18.x")
concurrency = optional(number, 20)
timeout = optional(number, 15 * 60) // 15 minutes
memory_size = optional(number, 1024)
schedule = optional(string, "rate(5 minutes)")
additional_environment_variables = optional(map(string), {})
additional_iam_policies = optional(list(object({
name = string,
arn = optional(string)
policy = optional(string)
})), [])
vpc = optional(object({
security_group_ids = list(string),
subnet_ids = list(string)
}))
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}), {})
})
|
{
"create": false
}
| no | 156 | 157 | ### Outputs 158 | 159 | | Name | Description | 160 | |------|-------------| 161 | | [cloudfront\_url](#output\_cloudfront\_url) | The URL for the cloudfront distribution | 162 | | [domain\_names](#output\_domain\_names) | The custom domain names attached to the cloudfront distribution | 163 | -------------------------------------------------------------------------------- /modules/legacy/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | # CloudFront 4 | 5 | data "aws_cloudfront_origin_request_policy" "all_viewer_except_host_header" { 6 | name = "Managed-AllViewerExceptHostHeader" 7 | } 8 | 9 | data "aws_cloudfront_cache_policy" "caching_optimized" { 10 | name = "Managed-CachingOptimized" 11 | } 12 | 13 | # Route 53 14 | 15 | data "aws_route53_zone" "hosted_zone" { 16 | count = var.domain.create ? 1 : 0 17 | name = var.domain.hosted_zone_name 18 | 19 | provider = aws.dns 20 | } 21 | 22 | # Zip Archives 23 | 24 | data "archive_file" "server_function" { 25 | for_each = local.zones_set 26 | type = "zip" 27 | output_path = "${local.zones_map[each.value].folder_path}/server-function.zip" 28 | source_dir = "${local.zones_map[each.value].folder_path}/server-function" 29 | } 30 | 31 | data "archive_file" "image_optimization_function" { 32 | type = "zip" 33 | output_path = "${local.zones_map[local.root].folder_path}/image-optimization-function.zip" 34 | source_dir = "${local.zones_map[local.root].folder_path}/image-optimization-function" 35 | } 36 | 37 | data "archive_file" "warmer_function" { 38 | for_each = local.should_create_warmer_function ? local.zones_set : [] 39 | type = "zip" 40 | output_path = "${local.zones_map[each.value].folder_path}/warmer-function.zip" 41 | source_dir = "${local.zones_map[each.value].folder_path}/warmer-function" 42 | } 43 | 44 | data "archive_file" "revalidation_function" { 45 | for_each = local.should_create_revalidation_resources ? local.zones_set : [] 46 | type = "zip" 47 | output_path = "${local.zones_map[each.value].folder_path}/revalidation-function.zip" 48 | source_dir = "${local.zones_map[each.value].folder_path}/revalidation-function" 49 | } 50 | -------------------------------------------------------------------------------- /modules/legacy/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | root = "root" 3 | zones_map = merge({ 4 | for zone in var.open_next.additional_zones : zone.name => { 5 | name = zone.name, 6 | http_path = trimsuffix(zone.http_path, "/"), 7 | folder_path = zone.folder_path 8 | } 9 | }, { 10 | root = { 11 | name = local.root, 12 | http_path = "", 13 | folder_path = var.open_next.root_folder_path 14 | } 15 | }) 16 | zones = keys(local.zones_map) 17 | zones_set = toset(local.zones) 18 | additional_zones_set = toset([for zone in local.zones : zone if zone != local.root]) 19 | 20 | assets_folders = { 21 | for name, zone in local.zones_map : name => "${zone.folder_path}/assets" 22 | } 23 | assets = flatten([ 24 | for name, zone in local.zones_map : 25 | [for file in toset([for file in fileset(local.assets_folders[name], "**") : file if var.open_next.exclusion_regex != null ? length(regexall(var.open_next.exclusion_regex, file)) == 0 : true]) : { 26 | prefix = name == local.root ? "" : "${local.zones_map[name].http_path}/" 27 | zone = zone 28 | file = file 29 | hash = filesha1("${local.assets_folders[name]}/${file}") 30 | }]]) 31 | 32 | cache_folders = { 33 | for name, zone in local.zones_map : name => "${zone.folder_path}/cache" 34 | } 35 | cache_assets = flatten([ 36 | for name, zone in local.zones_map : 37 | [for file in toset([for file in fileset(local.cache_folders[name], "**") : file if var.open_next.exclusion_regex != null ? length(regexall(var.open_next.exclusion_regex, file)) == 0 : true]) : { 38 | prefix = name == local.root ? "" : "${local.zones_map[name].http_path}/" 39 | zone = zone 40 | file = file 41 | hash = filesha1("${local.cache_folders[name]}/${file}") 42 | }]]) 43 | 44 | prefix = var.prefix == null ? "" : "${var.prefix}-" 45 | suffix = var.suffix == null ? "" : "-${var.suffix}" 46 | 47 | s3_origin_id = "${local.prefix}s3-origin${local.suffix}" 48 | image_function_origin = "${local.prefix}image-function-origin${local.suffix}" 49 | server_function_origins = { 50 | for name, zone in local.zones_map : name => "${local.prefix}${name == local.root ? "" : "${name}-"}server-function-origin${local.suffix}" 51 | } 52 | origin_groups = { 53 | for name, zone in local.zones_map : name => "${local.prefix}${name == local.root ? "" : "${name}-"}origin-group${local.suffix}" 54 | } 55 | 56 | aliases = var.domain.create == true ? coalescelist(var.domain.alternate_names, [var.domain.name]) : tolist([]) 57 | 58 | api_gateway_name = "${local.prefix}website-backend-api-gateway${local.suffix}" 59 | api_gateway_version = "1.0.0" 60 | 61 | backend_type_api_gateway = "API_GATEWAY" 62 | backend_type_regional_lambda = "REGIONAL_LAMBDA" 63 | backend_type_edge_lambda = "EDGE_LAMBDA" 64 | 65 | image_optimisation_use_api_gateway = var.image_optimisation_function.deployment == local.backend_type_api_gateway 66 | server_backend_use_api_gateway = var.server_function.deployment == local.backend_type_api_gateway 67 | server_backend_use_edge_lambda = var.server_function.deployment == local.backend_type_edge_lambda 68 | should_create_api_gateway_resources = local.image_optimisation_use_api_gateway || local.server_backend_use_api_gateway 69 | 70 | should_create_warmer_function = var.warmer_function.create == true && local.server_backend_use_edge_lambda == false 71 | should_create_revalidation_resources = var.isr.create == true 72 | should_create_revalidation_tag_mapping_resources = var.isr.create == true && var.isr.tag_mapping_db.create == true 73 | 74 | tag_mapping_items = local.should_create_revalidation_tag_mapping_resources ? flatten([ 75 | for name, zone in local.zones_map : 76 | fileexists("${zone.folder_path}/dynamodb-provider/dynamodb-cache.json") ? [for tag_mapping in jsondecode(file("${zone.folder_path}/dynamodb-provider/dynamodb-cache.json")) : { 77 | zone = zone 78 | item = tag_mapping 79 | }] : []]) : [] 80 | } 81 | 82 | # S3 83 | 84 | resource "aws_s3_bucket" "website_bucket" { 85 | bucket = "${local.prefix}website-bucket${local.suffix}" 86 | force_destroy = var.force_destroy 87 | } 88 | 89 | resource "aws_s3_bucket_policy" "website_bucket_policy" { 90 | bucket = aws_s3_bucket.website_bucket.id 91 | policy = jsonencode({ 92 | "Version" : "2012-10-17", 93 | "Statement" : [ 94 | { 95 | "Principal" : { 96 | "Service" : "cloudfront.amazonaws.com" 97 | }, 98 | "Action" : [ 99 | "s3:GetObject" 100 | ], 101 | "Resource" : "${aws_s3_bucket.website_bucket.arn}/*", 102 | "Effect" : "Allow", 103 | "Condition" : { 104 | "StringEquals" : { 105 | "AWS:SourceArn" : aws_cloudfront_distribution.website_distribution.arn 106 | } 107 | } 108 | } 109 | ] 110 | }) 111 | } 112 | 113 | resource "aws_s3_object" "website_asset" { 114 | for_each = { 115 | for asset in local.assets : "${asset.zone.name}-${asset.file}" => asset 116 | } 117 | 118 | bucket = aws_s3_bucket.website_bucket.bucket 119 | key = "${each.value.prefix}${each.value.file}" 120 | source = "${local.assets_folders[each.value.zone.name]}/${each.value.file}" 121 | etag = filemd5("${local.assets_folders[each.value.zone.name]}/${each.value.file}") 122 | 123 | cache_control = length(regexall(var.cache_control_immutable_assets_regex, each.value.file)) > 0 ? "public,max-age=31536000,immutable" : "public,max-age=0,s-maxage=31536000,must-revalidate" 124 | content_type = lookup(var.content_types.mapping, reverse(split(".", each.value.file))[0], var.content_types.default) 125 | } 126 | 127 | resource "aws_s3_object" "cache_asset" { 128 | for_each = { 129 | for asset in local.cache_assets : "${asset.zone.name}-${asset.file}" => asset 130 | } 131 | 132 | bucket = aws_s3_bucket.website_bucket.bucket 133 | key = "${each.value.prefix}/_cache/${each.value.file}" 134 | source = "${local.cache_folders[each.value.zone.name]}/${each.value.file}" 135 | etag = filemd5("${local.cache_folders[each.value.zone.name]}/${each.value.file}") 136 | 137 | content_type = lookup(var.content_types.mapping, reverse(split(".", each.value.file))[0], var.content_types.default) 138 | } 139 | 140 | # Server Function 141 | 142 | module "server_function" { 143 | for_each = local.zones_set 144 | source = "./modules/tf-aws-lambda" 145 | 146 | function_name = "${each.value == local.root ? "" : "${each.value}-"}server-function" 147 | zip_file = data.archive_file.server_function[each.value].output_path 148 | hash = data.archive_file.server_function[each.value].output_base64sha256 149 | 150 | runtime = var.server_function.runtime 151 | handler = "index.handler" 152 | 153 | memory_size = var.server_function.memory_size 154 | timeout = var.server_function.timeout 155 | 156 | additional_iam_policies = var.server_function.additional_iam_policies 157 | iam_policy_statements = concat([ 158 | { 159 | "Action" : [ 160 | "s3:GetObject*", 161 | "s3:GetBucket*", 162 | "s3:List*", 163 | "s3:DeleteObject*", 164 | "s3:PutObject", 165 | "s3:PutObjectLegalHold", 166 | "s3:PutObjectRetention", 167 | "s3:PutObjectTagging", 168 | "s3:PutObjectVersionTagging", 169 | "s3:Abort*" 170 | ], 171 | "Resource" : [ 172 | aws_s3_bucket.website_bucket.arn, 173 | "${aws_s3_bucket.website_bucket.arn}/*" 174 | ], 175 | "Effect" : "Allow" 176 | }, 177 | ], 178 | local.should_create_revalidation_resources ? [{ 179 | "Action" : [ 180 | "sqs:SendMessage" 181 | ], 182 | "Resource" : aws_sqs_queue.revalidation_queue[each.value].arn, 183 | "Effect" : "Allow" 184 | }] : [], 185 | local.should_create_revalidation_tag_mapping_resources ? [{ 186 | "Action" : [ 187 | "dynamodb:BatchGetItem", 188 | "dynamodb:GetRecords", 189 | "dynamodb:GetShardIterator", 190 | "dynamodb:Query", 191 | "dynamodb:GetItem", 192 | "dynamodb:Scan", 193 | "dynamodb:BatchWriteItem", 194 | "dynamodb:PutItem", 195 | "dynamodb:UpdateItem", 196 | "dynamodb:DeleteItem", 197 | "dynamodb:DescribeTable" 198 | ], 199 | "Resource" : [ 200 | aws_dynamodb_table.dynamodb_table[each.value].arn, 201 | "${aws_dynamodb_table.dynamodb_table[each.value].arn}/index/*" 202 | ], 203 | "Effect" : "Allow" 204 | }] : [] 205 | ) 206 | 207 | environment_variables = merge( 208 | local.should_create_revalidation_resources ? { 209 | "CACHE_BUCKET_NAME" : aws_s3_bucket.website_bucket.id, 210 | "CACHE_BUCKET_KEY_PREFIX" : "${each.value == local.root ? "" : "${each.value}/"}_cache", 211 | "CACHE_BUCKET_REGION" : data.aws_region.current.name, 212 | "REVALIDATION_QUEUE_URL" : aws_sqs_queue.revalidation_queue[each.value].url, 213 | "REVALIDATION_QUEUE_REGION" : data.aws_region.current.name, 214 | } : {}, 215 | local.should_create_revalidation_tag_mapping_resources ? { 216 | "CACHE_DYNAMO_TABLE" : aws_dynamodb_table.dynamodb_table[each.value].name, 217 | } : {}, 218 | var.server_function.additional_environment_variables 219 | ) 220 | 221 | architecture = local.server_backend_use_edge_lambda ? "x86_64" : var.preferred_architecture 222 | 223 | iam = var.iam 224 | cloudwatch_log = var.cloudwatch_log 225 | 226 | vpc = var.server_function.vpc != null ? var.server_function.vpc : var.vpc 227 | 228 | prefix = local.prefix 229 | suffix = local.suffix 230 | 231 | run_at_edge = local.server_backend_use_edge_lambda 232 | 233 | function_url = { 234 | create = var.server_function.deployment == local.backend_type_regional_lambda 235 | } 236 | 237 | timeouts = var.server_function.timeouts 238 | 239 | providers = { 240 | aws = aws.server_function 241 | aws.iam = aws.iam 242 | } 243 | } 244 | 245 | # Image Optimisation Function 246 | 247 | module "image_optimisation_function" { 248 | source = "./modules/tf-aws-lambda" 249 | 250 | function_name = "image-optimization-function" 251 | zip_file = data.archive_file.image_optimization_function.output_path 252 | hash = data.archive_file.image_optimization_function.output_base64sha256 253 | 254 | runtime = var.image_optimisation_function.runtime 255 | handler = "index.handler" 256 | 257 | memory_size = var.image_optimisation_function.memory_size 258 | timeout = var.image_optimisation_function.timeout 259 | 260 | environment_variables = merge({ 261 | "BUCKET_NAME" = aws_s3_bucket.website_bucket.id 262 | }, var.image_optimisation_function.additional_environment_variables) 263 | 264 | additional_iam_policies = var.image_optimisation_function.additional_iam_policies 265 | iam_policy_statements = [ 266 | { 267 | "Action" : [ 268 | "s3:GetObject" 269 | ], 270 | "Resource" : "${aws_s3_bucket.website_bucket.arn}/*", 271 | "Effect" : "Allow" 272 | } 273 | ] 274 | 275 | architecture = var.preferred_architecture 276 | 277 | iam = var.iam 278 | cloudwatch_log = var.cloudwatch_log 279 | 280 | vpc = var.image_optimisation_function.vpc != null ? var.image_optimisation_function.vpc : var.vpc 281 | 282 | prefix = local.prefix 283 | suffix = local.suffix 284 | 285 | function_url = { 286 | create = var.image_optimisation_function.deployment == local.backend_type_regional_lambda 287 | } 288 | 289 | timeouts = var.image_optimisation_function.timeouts 290 | 291 | providers = { 292 | aws.iam = aws.iam 293 | } 294 | } 295 | 296 | # Warmer Function 297 | 298 | module "warmer_function" { 299 | for_each = local.should_create_warmer_function ? local.zones_set : [] 300 | source = "./modules/tf-aws-scheduled-lambda" 301 | 302 | function_name = "${each.value == local.root ? "" : "${each.value}-"}warmer-function" 303 | zip_file = data.archive_file.warmer_function[each.value].output_path 304 | hash = data.archive_file.warmer_function[each.value].output_base64sha256 305 | 306 | runtime = var.warmer_function.runtime 307 | handler = "index.handler" 308 | 309 | memory_size = var.warmer_function.memory_size 310 | timeout = var.warmer_function.timeout 311 | 312 | environment_variables = merge({ 313 | "FUNCTION_NAME" : module.server_function[each.value].function_name, 314 | "CONCURRENCY" : var.warmer_function.concurrency, 315 | }, var.warmer_function.additional_environment_variables) 316 | 317 | additional_iam_policies = var.warmer_function.additional_iam_policies 318 | iam_policy_statements = [ 319 | { 320 | "Action" : [ 321 | "lambda:InvokeFunction" 322 | ], 323 | "Resource" : module.server_function[each.value].function_arn, 324 | "Effect" : "Allow" 325 | } 326 | ] 327 | 328 | architecture = var.preferred_architecture 329 | 330 | iam = var.iam 331 | cloudwatch_log = var.cloudwatch_log 332 | 333 | vpc = var.warmer_function.vpc != null ? var.warmer_function.vpc : var.vpc 334 | 335 | prefix = local.prefix 336 | suffix = local.suffix 337 | 338 | schedule = var.warmer_function.schedule 339 | 340 | timeouts = var.warmer_function.timeouts 341 | 342 | providers = { 343 | aws.iam = aws.iam 344 | } 345 | } 346 | 347 | # Revalidation Function 348 | 349 | module "revalidation_function" { 350 | for_each = local.should_create_revalidation_resources ? local.zones_set : [] 351 | source = "./modules/tf-aws-lambda" 352 | 353 | function_name = "${each.value == local.root ? "" : "${each.value}-"}revalidation-function" 354 | zip_file = data.archive_file.revalidation_function[each.value].output_path 355 | hash = data.archive_file.revalidation_function[each.value].output_base64sha256 356 | 357 | runtime = var.isr.revalidation_function.runtime 358 | handler = "index.handler" 359 | 360 | memory_size = var.isr.revalidation_function.memory_size 361 | timeout = var.isr.revalidation_function.timeout 362 | 363 | environment_variables = var.isr.revalidation_function.additional_environment_variables 364 | 365 | additional_iam_policies = var.isr.revalidation_function.additional_iam_policies 366 | iam_policy_statements = [ 367 | { 368 | "Action" : [ 369 | "sqs:ReceiveMessage", 370 | "sqs:ChangeMessageVisibility", 371 | "sqs:GetQueueUrl", 372 | "sqs:DeleteMessage", 373 | "sqs:GetQueueAttributes" 374 | ], 375 | "Resource" : aws_sqs_queue.revalidation_queue[each.value].arn, 376 | "Effect" : "Allow" 377 | } 378 | ] 379 | 380 | architecture = var.preferred_architecture 381 | 382 | iam = var.iam 383 | cloudwatch_log = var.cloudwatch_log 384 | 385 | vpc = var.isr.revalidation_function.vpc != null ? var.isr.revalidation_function.vpc : var.vpc 386 | 387 | prefix = local.prefix 388 | suffix = local.suffix 389 | 390 | timeouts = var.isr.revalidation_function.timeouts 391 | 392 | providers = { 393 | aws.iam = aws.iam 394 | } 395 | } 396 | 397 | # SQS 398 | 399 | resource "aws_sqs_queue" "revalidation_queue" { 400 | for_each = local.should_create_revalidation_resources ? local.zones_set : [] 401 | name = "${local.prefix}${each.value == local.root ? "" : "${each.value}-"}isr-queue${local.suffix}.fifo" 402 | fifo_queue = true 403 | content_based_deduplication = true 404 | } 405 | 406 | resource "aws_lambda_event_source_mapping" "revalidation_queue_source" { 407 | for_each = local.should_create_revalidation_resources ? local.zones_set : [] 408 | event_source_arn = aws_sqs_queue.revalidation_queue[each.value].arn 409 | function_name = module.revalidation_function[each.value].function_arn 410 | } 411 | 412 | # DynamoDB 413 | 414 | resource "aws_dynamodb_table" "dynamodb_table" { 415 | for_each = local.should_create_revalidation_tag_mapping_resources ? local.zones_set : [] 416 | name = "${local.prefix}${each.value == local.root ? "" : "${each.value}-"}isr-tag-mapping${local.suffix}" 417 | 418 | billing_mode = var.isr.tag_mapping_db.billing_mode 419 | read_capacity = var.isr.tag_mapping_db.read_capacity 420 | write_capacity = var.isr.tag_mapping_db.write_capacity 421 | 422 | hash_key = "tag" 423 | range_key = "path" 424 | 425 | attribute { 426 | name = "tag" 427 | type = "S" 428 | } 429 | 430 | attribute { 431 | name = "path" 432 | type = "S" 433 | } 434 | 435 | attribute { 436 | name = "revalidatedAt" 437 | type = "N" 438 | } 439 | 440 | global_secondary_index { 441 | name = "revalidate" 442 | hash_key = "path" 443 | range_key = "revalidatedAt" 444 | projection_type = "ALL" 445 | read_capacity = var.isr.tag_mapping_db.read_capacity 446 | write_capacity = var.isr.tag_mapping_db.write_capacity 447 | } 448 | } 449 | 450 | resource "aws_dynamodb_table_item" "isr_table_item" { 451 | for_each = { 452 | for tag_mapping_item in local.tag_mapping_items : "${tag_mapping_item.zone.name}-${tag_mapping_item.item.tag.S}" => tag_mapping_item 453 | } 454 | 455 | table_name = aws_dynamodb_table.dynamodb_table[each.value.zone.name].name 456 | hash_key = aws_dynamodb_table.dynamodb_table[each.value.zone.name].hash_key 457 | range_key = aws_dynamodb_table.dynamodb_table[each.value.zone.name].range_key 458 | 459 | item = jsonencode(each.value.item) 460 | } 461 | 462 | # API Gateway 463 | 464 | resource "aws_apigatewayv2_api" "api" { 465 | count = local.should_create_api_gateway_resources ? 1 : 0 466 | 467 | name = local.api_gateway_name 468 | version = local.api_gateway_version 469 | 470 | protocol_type = "HTTP" 471 | 472 | body = jsonencode({ 473 | "openapi" : "3.0.3", 474 | "info" : { 475 | "title" : "${local.api_gateway_name}", 476 | "version" : "${local.api_gateway_version}" 477 | }, 478 | "paths" : merge( 479 | local.image_optimisation_use_api_gateway ? { 480 | "/images/{proxy+}" : { 481 | "x-amazon-apigateway-any-method" : { 482 | "parameters" : [ 483 | { 484 | "name" : "proxy", 485 | "in" : "path", 486 | "required" : true, 487 | "type" : "string" 488 | } 489 | ], 490 | "responses" : {}, 491 | "x-amazon-apigateway-integration" : { 492 | "type" : "AWS_PROXY", 493 | "httpMethod" : "POST", 494 | "uri" : "${module.image_optimisation_function.invoke_arn}", 495 | "requestParameters" : { 496 | "overwrite:path" : "$request.path.proxy" 497 | }, 498 | "payloadFormatVersion" : "2.0", 499 | "enableSimpleResponses" : true 500 | } 501 | } 502 | } 503 | } : {}, 504 | local.server_backend_use_api_gateway ? { 505 | for zone in local.additional_zones_set : "/${zone}" => { 506 | "x-amazon-apigateway-any-method" : { 507 | "responses" : {}, 508 | "x-amazon-apigateway-integration" : { 509 | "type" : "AWS_PROXY", 510 | "httpMethod" : "POST", 511 | "uri" : "${module.server_function[zone].invoke_arn}", 512 | "requestParameters" : { 513 | "overwrite:path" : "$request.path" 514 | }, 515 | "payloadFormatVersion" : "2.0", 516 | "enableSimpleResponses" : true 517 | } 518 | } 519 | } 520 | } : {}, 521 | local.server_backend_use_api_gateway ? { 522 | for zone in local.additional_zones_set : "/${zone}/{proxy+}" => { 523 | "x-amazon-apigateway-any-method" : { 524 | "parameters" : [ 525 | { 526 | "name" : "proxy", 527 | "in" : "path", 528 | "required" : true, 529 | "type" : "string" 530 | } 531 | ], 532 | "responses" : {}, 533 | "x-amazon-apigateway-integration" : { 534 | "type" : "AWS_PROXY", 535 | "httpMethod" : "POST", 536 | "uri" : "${module.server_function[zone].invoke_arn}", 537 | "requestParameters" : { 538 | "overwrite:path" : "$request.path" 539 | }, 540 | "payloadFormatVersion" : "2.0", 541 | "enableSimpleResponses" : true 542 | } 543 | } 544 | } 545 | } : {}, 546 | local.server_backend_use_api_gateway ? { 547 | "/$default" : { 548 | "x-amazon-apigateway-any-method" : { 549 | "isDefaultRoute" : true, 550 | "responses" : {}, 551 | "x-amazon-apigateway-integration" : { 552 | "type" : "AWS_PROXY", 553 | "httpMethod" : "POST", 554 | "uri" : "${module.server_function[local.root].invoke_arn}", 555 | "requestParameters" : { 556 | "overwrite:path" : "$request.path" 557 | }, 558 | "payloadFormatVersion" : "2.0", 559 | "enableSimpleResponses" : true 560 | } 561 | } 562 | } 563 | } : {} 564 | ) 565 | }) 566 | 567 | fail_on_warnings = true 568 | } 569 | 570 | resource "aws_apigatewayv2_deployment" "deployment" { 571 | count = local.should_create_api_gateway_resources ? 1 : 0 572 | 573 | api_id = one(aws_apigatewayv2_api.api[*].id) 574 | 575 | triggers = { 576 | redeployment = sha512(one(aws_apigatewayv2_api.api[*].body)) 577 | } 578 | 579 | lifecycle { 580 | create_before_destroy = true 581 | } 582 | } 583 | 584 | resource "aws_apigatewayv2_stage" "stable" { 585 | count = local.should_create_api_gateway_resources ? 1 : 0 586 | 587 | api_id = one(aws_apigatewayv2_api.api[*].id) 588 | name = "stable" 589 | 590 | deployment_id = one(aws_apigatewayv2_deployment.deployment[*].id) 591 | } 592 | 593 | resource "aws_lambda_permission" "server_function_permission" { 594 | for_each = local.server_backend_use_api_gateway ? local.zones_set : [] 595 | statement_id = "AllowAPIGatewayInvoke" 596 | action = "lambda:InvokeFunction" 597 | function_name = module.server_function[each.value].function_name 598 | principal = "apigateway.amazonaws.com" 599 | source_arn = "${one(aws_apigatewayv2_stage.stable[*].execution_arn)}/*" 600 | } 601 | 602 | resource "aws_lambda_permission" "image_optimisation_function_permission" { 603 | count = local.image_optimisation_use_api_gateway ? 1 : 0 604 | statement_id = "AllowAPIGatewayInvoke" 605 | action = "lambda:InvokeFunction" 606 | function_name = module.image_optimisation_function.function_name 607 | principal = "apigateway.amazonaws.com" 608 | source_arn = "${one(aws_apigatewayv2_stage.stable[*].execution_arn)}/*" 609 | } 610 | 611 | # CloudFront 612 | 613 | resource "aws_cloudfront_function" "x_forwarded_host" { 614 | name = "${local.prefix}x-forwarded-host-function${local.suffix}" 615 | runtime = "cloudfront-js-1.0" 616 | publish = true 617 | code = "\nfunction handler(event) {\n var request = event.request;\n request.headers[\"x-forwarded-host\"] = request.headers.host;\n \n return request;\n}" 618 | } 619 | 620 | resource "aws_cloudfront_origin_access_control" "website_origin_access_control" { 621 | name = "${local.prefix}website${local.suffix}" 622 | origin_access_control_origin_type = "s3" 623 | signing_behavior = "always" 624 | signing_protocol = "sigv4" 625 | } 626 | 627 | resource "aws_cloudfront_cache_policy" "cache_policy" { 628 | name = "${local.prefix}cache-policy${local.suffix}" 629 | 630 | default_ttl = 0 631 | max_ttl = 31536000 632 | min_ttl = 0 633 | 634 | parameters_in_cache_key_and_forwarded_to_origin { 635 | cookies_config { 636 | cookie_behavior = "all" 637 | } 638 | headers_config { 639 | header_behavior = "whitelist" 640 | headers { 641 | items = ["accept", "rsc", "next-router-prefetch", "next-router-state-tree"] 642 | } 643 | } 644 | query_strings_config { 645 | query_string_behavior = "all" 646 | } 647 | } 648 | } 649 | 650 | resource "aws_cloudfront_distribution" "website_distribution" { 651 | dynamic "origin_group" { 652 | for_each = local.server_backend_use_edge_lambda ? [] : local.zones_set 653 | 654 | content { 655 | origin_id = local.origin_groups[origin_group.value] 656 | 657 | failover_criteria { 658 | status_codes = [503] 659 | } 660 | 661 | member { 662 | origin_id = local.server_function_origins[origin_group.value] 663 | } 664 | 665 | member { 666 | origin_id = local.s3_origin_id 667 | } 668 | } 669 | } 670 | 671 | origin { 672 | domain_name = aws_s3_bucket.website_bucket.bucket_regional_domain_name 673 | origin_access_control_id = aws_cloudfront_origin_access_control.website_origin_access_control.id 674 | origin_id = local.s3_origin_id 675 | } 676 | 677 | dynamic "origin" { 678 | for_each = local.server_backend_use_edge_lambda ? [] : local.zones_set 679 | 680 | content { 681 | domain_name = local.server_backend_use_api_gateway ? trimprefix(one(aws_apigatewayv2_api.api[*].api_endpoint), "https://") : module.server_function[origin.value].function_url_hostname 682 | origin_id = local.server_function_origins[origin.value] 683 | origin_path = local.server_backend_use_api_gateway ? "/${one(aws_apigatewayv2_stage.stable[*].name)}" : null 684 | 685 | custom_origin_config { 686 | http_port = 80 687 | https_port = 443 688 | origin_protocol_policy = "https-only" 689 | origin_ssl_protocols = ["TLSv1.2"] 690 | } 691 | } 692 | } 693 | 694 | origin { 695 | domain_name = local.image_optimisation_use_api_gateway ? trimprefix(one(aws_apigatewayv2_api.api[*].api_endpoint), "https://") : module.image_optimisation_function.function_url_hostname 696 | origin_id = local.image_function_origin 697 | origin_path = local.image_optimisation_use_api_gateway ? "/${one(aws_apigatewayv2_stage.stable[*].name)}/images" : null 698 | 699 | custom_origin_config { 700 | http_port = 80 701 | https_port = 443 702 | origin_protocol_policy = "https-only" 703 | origin_ssl_protocols = ["TLSv1.2"] 704 | } 705 | } 706 | 707 | enabled = var.cloudfront.enabled 708 | is_ipv6_enabled = var.cloudfront.ipv6_enabled 709 | http_version = var.cloudfront.http_version 710 | web_acl_id = try(var.waf.web_acl_id, null) 711 | 712 | dynamic "ordered_cache_behavior" { 713 | for_each = local.zones_set 714 | 715 | content { 716 | path_pattern = "${ordered_cache_behavior.value == local.root ? "" : "${local.zones_map[ordered_cache_behavior.value].http_path}/"}api/*" 717 | allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"] 718 | cached_methods = ["GET", "HEAD", "OPTIONS"] 719 | target_origin_id = local.server_backend_use_edge_lambda ? local.s3_origin_id : local.server_function_origins[ordered_cache_behavior.value] 720 | 721 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 722 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id 723 | 724 | compress = true 725 | viewer_protocol_policy = "redirect-to-https" 726 | 727 | function_association { 728 | event_type = "viewer-request" 729 | function_arn = aws_cloudfront_function.x_forwarded_host.arn 730 | } 731 | 732 | dynamic "lambda_function_association" { 733 | for_each = local.server_backend_use_edge_lambda ? [true] : [] 734 | 735 | content { 736 | event_type = "origin-request" 737 | lambda_arn = module.server_function[ordered_cache_behavior.value].qualified_arn 738 | include_body = true 739 | } 740 | } 741 | } 742 | } 743 | 744 | dynamic "ordered_cache_behavior" { 745 | for_each = local.zones_set 746 | 747 | content { 748 | path_pattern = "${ordered_cache_behavior.value == local.root ? "" : "${local.zones_map[ordered_cache_behavior.value].http_path}/"}_next/data/*" 749 | allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"] 750 | cached_methods = ["GET", "HEAD", "OPTIONS"] 751 | target_origin_id = local.server_backend_use_edge_lambda ? local.s3_origin_id : local.server_function_origins[ordered_cache_behavior.value] 752 | 753 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 754 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id 755 | 756 | compress = true 757 | viewer_protocol_policy = "redirect-to-https" 758 | 759 | function_association { 760 | event_type = "viewer-request" 761 | function_arn = aws_cloudfront_function.x_forwarded_host.arn 762 | } 763 | 764 | dynamic "lambda_function_association" { 765 | for_each = local.server_backend_use_edge_lambda ? [true] : [] 766 | 767 | content { 768 | event_type = "origin-request" 769 | lambda_arn = module.server_function[ordered_cache_behavior.value].qualified_arn 770 | include_body = true 771 | } 772 | } 773 | } 774 | } 775 | 776 | dynamic "ordered_cache_behavior" { 777 | for_each = local.zones_set 778 | 779 | content { 780 | path_pattern = "${ordered_cache_behavior.value == local.root ? "" : "${local.zones_map[ordered_cache_behavior.value].http_path}/"}_next/image*" 781 | allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"] 782 | cached_methods = ["GET", "HEAD", "OPTIONS"] 783 | target_origin_id = local.image_function_origin 784 | 785 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 786 | 787 | compress = true 788 | viewer_protocol_policy = "redirect-to-https" 789 | } 790 | } 791 | 792 | dynamic "ordered_cache_behavior" { 793 | for_each = local.zones_set 794 | 795 | content { 796 | path_pattern = "${ordered_cache_behavior.value == local.root ? "" : "${local.zones_map[ordered_cache_behavior.value].http_path}/"}_next/*" 797 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 798 | cached_methods = ["GET", "HEAD", "OPTIONS"] 799 | target_origin_id = local.s3_origin_id 800 | 801 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 802 | 803 | compress = true 804 | viewer_protocol_policy = "redirect-to-https" 805 | } 806 | } 807 | 808 | dynamic "ordered_cache_behavior" { 809 | for_each = local.additional_zones_set 810 | 811 | content { 812 | path_pattern = "${local.zones_map[ordered_cache_behavior.value].http_path}/*" 813 | allowed_methods = ["GET", "HEAD"] 814 | cached_methods = ["GET", "HEAD"] 815 | target_origin_id = local.server_backend_use_edge_lambda ? local.s3_origin_id : local.origin_groups[ordered_cache_behavior.value] 816 | 817 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 818 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id 819 | 820 | compress = true 821 | viewer_protocol_policy = "redirect-to-https" 822 | 823 | function_association { 824 | event_type = "viewer-request" 825 | function_arn = aws_cloudfront_function.x_forwarded_host.arn 826 | } 827 | 828 | dynamic "lambda_function_association" { 829 | for_each = local.server_backend_use_edge_lambda ? [true] : [] 830 | 831 | content { 832 | event_type = "origin-request" 833 | lambda_arn = module.server_function[ordered_cache_behavior.value].qualified_arn 834 | include_body = true 835 | } 836 | } 837 | } 838 | } 839 | 840 | dynamic "ordered_cache_behavior" { 841 | for_each = local.additional_zones_set 842 | 843 | content { 844 | path_pattern = local.zones_map[ordered_cache_behavior.value].http_path 845 | allowed_methods = ["GET", "HEAD"] 846 | cached_methods = ["GET", "HEAD"] 847 | target_origin_id = local.server_backend_use_edge_lambda ? local.s3_origin_id : local.origin_groups[ordered_cache_behavior.value] 848 | 849 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 850 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id 851 | 852 | compress = true 853 | viewer_protocol_policy = "redirect-to-https" 854 | 855 | function_association { 856 | event_type = "viewer-request" 857 | function_arn = aws_cloudfront_function.x_forwarded_host.arn 858 | } 859 | 860 | dynamic "lambda_function_association" { 861 | for_each = local.server_backend_use_edge_lambda ? [true] : [] 862 | 863 | content { 864 | event_type = "origin-request" 865 | lambda_arn = module.server_function[ordered_cache_behavior.value].qualified_arn 866 | include_body = true 867 | } 868 | } 869 | } 870 | } 871 | 872 | default_cache_behavior { 873 | allowed_methods = ["GET", "HEAD"] 874 | cached_methods = ["GET", "HEAD"] 875 | target_origin_id = local.server_backend_use_edge_lambda ? local.s3_origin_id : local.origin_groups[local.root] 876 | 877 | cache_policy_id = aws_cloudfront_cache_policy.cache_policy.id 878 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id 879 | 880 | compress = true 881 | viewer_protocol_policy = "redirect-to-https" 882 | 883 | function_association { 884 | event_type = "viewer-request" 885 | function_arn = aws_cloudfront_function.x_forwarded_host.arn 886 | } 887 | 888 | dynamic "lambda_function_association" { 889 | for_each = local.server_backend_use_edge_lambda ? [true] : [] 890 | 891 | content { 892 | event_type = "origin-request" 893 | lambda_arn = module.server_function[local.root].qualified_arn 894 | include_body = true 895 | } 896 | } 897 | } 898 | 899 | price_class = var.cloudfront.price_class 900 | 901 | restrictions { 902 | geo_restriction { 903 | restriction_type = var.cloudfront.geo_restrictions.type 904 | locations = var.cloudfront.geo_restrictions.locations 905 | } 906 | } 907 | 908 | dynamic "viewer_certificate" { 909 | for_each = var.domain.create == false ? [{ 910 | cloudfront_default_certificate = true 911 | }] : [{ 912 | acm_certificate_arn = var.domain.acm_certificate_arn 913 | minimum_protocol_version = var.cloudfront.minimum_protocol_version 914 | ssl_support_method = var.cloudfront.ssl_support_method 915 | }] 916 | content { 917 | cloudfront_default_certificate = lookup(viewer_certificate.value, "cloudfront_default_certificate", null) 918 | acm_certificate_arn = lookup(viewer_certificate.value, "acm_certificate_arn", null) 919 | minimum_protocol_version = lookup(viewer_certificate.value, "minimum_protocol_version", null) 920 | ssl_support_method = lookup(viewer_certificate.value, "ssl_support_method", null) 921 | } 922 | } 923 | 924 | aliases = local.aliases 925 | 926 | depends_on = [aws_s3_object.website_asset, aws_s3_object.cache_asset] 927 | } 928 | 929 | # Invalidate Distribution 930 | 931 | resource "terraform_data" "invalidate_distribution" { 932 | count = var.cloudfront.invalidate_on_change ? 1 : 0 933 | 934 | triggers_replace = [ 935 | flatten([for zone in local.zones : module.server_function[zone].function_version]), 936 | sha1(join("", [for f in aws_s3_object.website_asset : f.etag])) 937 | ] 938 | 939 | provisioner "local-exec" { 940 | command = "/bin/bash ${path.module}/scripts/invalidate-cloudfront.sh" 941 | 942 | environment = { 943 | "CDN_ID" = aws_cloudfront_distribution.website_distribution.id 944 | } 945 | } 946 | } 947 | 948 | # Route 53 949 | 950 | resource "aws_route53_record" "route53_a_record" { 951 | for_each = toset(local.aliases) 952 | 953 | zone_id = one(data.aws_route53_zone.hosted_zone[*].zone_id) 954 | name = each.value 955 | type = "A" 956 | 957 | alias { 958 | name = aws_cloudfront_distribution.website_distribution.domain_name 959 | zone_id = aws_cloudfront_distribution.website_distribution.hosted_zone_id 960 | evaluate_target_health = var.domain.evaluate_target_health 961 | } 962 | 963 | provider = aws.dns 964 | } 965 | 966 | resource "aws_route53_record" "route53_aaaa_record" { 967 | for_each = var.cloudfront.ipv6_enabled == true ? toset(local.aliases) : toset([]) 968 | 969 | zone_id = one(data.aws_route53_zone.hosted_zone[*].zone_id) 970 | name = each.value 971 | type = "AAAA" 972 | 973 | alias { 974 | name = aws_cloudfront_distribution.website_distribution.domain_name 975 | zone_id = aws_cloudfront_distribution.website_distribution.hosted_zone_id 976 | evaluate_target_health = var.domain.evaluate_target_health 977 | } 978 | 979 | provider = aws.dns 980 | } 981 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-lambda/main.tf: -------------------------------------------------------------------------------- 1 | # Lambda 2 | 3 | resource "aws_lambda_function" "lambda_function" { 4 | filename = var.zip_file 5 | source_code_hash = var.hash 6 | 7 | function_name = "${var.prefix}${var.function_name}${var.suffix}" 8 | role = aws_iam_role.lambda_iam.arn 9 | 10 | runtime = var.runtime 11 | handler = var.handler 12 | memory_size = var.memory_size 13 | timeout = var.timeout 14 | 15 | architectures = [var.architecture] 16 | publish = var.publish 17 | 18 | environment { 19 | variables = var.environment_variables 20 | } 21 | 22 | dynamic "vpc_config" { 23 | for_each = var.vpc != null ? [var.vpc] : [] 24 | 25 | content { 26 | security_group_ids = vpc_config.value["security_group_ids"] 27 | subnet_ids = vpc_config.value["subnet_ids"] 28 | } 29 | } 30 | 31 | timeouts { 32 | create = try(var.timeouts.create, null) 33 | update = try(var.timeouts.update, null) 34 | delete = try(var.timeouts.delete, null) 35 | } 36 | } 37 | 38 | resource "aws_lambda_function_url" "function_url" { 39 | count = var.function_url.create ? 1 : 0 40 | 41 | function_name = aws_lambda_function.lambda_function.function_name 42 | authorization_type = var.function_url.authorization_type 43 | invoke_mode = "BUFFERED" 44 | } 45 | 46 | resource "aws_lambda_permission" "function_url_permission" { 47 | count = var.function_url.create ? 1 : 0 48 | 49 | action = "lambda:InvokeFunctionUrl" 50 | function_name = aws_lambda_function.lambda_function.function_name 51 | principal = "*" 52 | function_url_auth_type = var.function_url.authorization_type 53 | } 54 | 55 | # Cloudwatch Logs 56 | 57 | resource "aws_cloudwatch_log_group" "lambda_log_group" { 58 | count = var.run_at_edge ? 0 : 1 59 | name = "/aws/lambda/${var.prefix}${var.function_name}${var.suffix}" 60 | retention_in_days = var.cloudwatch_log.retention_in_days 61 | } 62 | 63 | # IAM 64 | 65 | resource "aws_iam_role" "lambda_iam" { 66 | name = "${var.prefix}${var.function_name}-role${var.suffix}" 67 | path = var.iam.path 68 | permissions_boundary = var.iam.permissions_boundary 69 | 70 | assume_role_policy = jsonencode({ 71 | "Version" : "2012-10-17", 72 | "Statement" : concat([ 73 | { 74 | "Action" : "sts:AssumeRole", 75 | "Principal" : { 76 | "Service" : "lambda.amazonaws.com" 77 | }, 78 | "Effect" : "Allow" 79 | } 80 | ], var.run_at_edge ? [ 81 | { 82 | "Action" : "sts:AssumeRole", 83 | "Principal" : { 84 | "Service" : "edgelambda.amazonaws.com" 85 | }, 86 | "Effect" : "Allow" 87 | } 88 | ] : []) 89 | }) 90 | } 91 | 92 | resource "aws_iam_policy" "lambda_policy" { 93 | name = "${var.prefix}${var.function_name}-lambda-policy${var.suffix}" 94 | path = var.iam.path 95 | 96 | policy = jsonencode({ 97 | "Version" : "2012-10-17", 98 | "Statement" : concat([ 99 | { 100 | "Action" : concat([ 101 | "logs:CreateLogStream", 102 | "logs:PutLogEvents" 103 | ], var.run_at_edge ? ["logs:CreateLogGroup"] : []), 104 | "Resource" : var.run_at_edge ? "*" : "${one(aws_cloudwatch_log_group.lambda_log_group[*].arn)}:*", 105 | "Effect" : "Allow" 106 | } 107 | ], 108 | var.vpc != null ? [ 109 | { 110 | "Action" : [ 111 | "ec2:CreateNetworkInterface", 112 | "ec2:DescribeNetworkInterfaces", 113 | "ec2:DeleteNetworkInterface", 114 | "ec2:AssignPrivateIpAddresses", 115 | "ec2:UnassignPrivateIpAddresses" 116 | ], 117 | "Resource" : "*" 118 | "Effect" : "Allow" 119 | } 120 | ] : [], 121 | var.iam_policy_statements) 122 | }) 123 | } 124 | 125 | resource "aws_iam_role_policy_attachment" "policy_attachment" { 126 | role = aws_iam_role.lambda_iam.name 127 | policy_arn = aws_iam_policy.lambda_policy.arn 128 | } 129 | 130 | resource "aws_iam_policy" "additional_policy" { 131 | for_each = { 132 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.name => additional_iam_policy if additional_iam_policy.policy != null 133 | } 134 | name = "${var.prefix}${var.function_name}-${each.key}${var.suffix}" 135 | path = var.iam.path 136 | 137 | policy = each.value.policy 138 | } 139 | 140 | resource "aws_iam_role_policy_attachment" "additional_policy_attachment" { 141 | for_each = { 142 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.name => additional_iam_policy 143 | } 144 | role = aws_iam_role.lambda_iam.name 145 | policy_arn = each.value.arn != null ? each.value.arn : aws_iam_policy.additional_policy[each.key].arn 146 | } 147 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_name" { 2 | description = "The lambda function name" 3 | value = aws_lambda_function.lambda_function.function_name 4 | } 5 | 6 | output "function_arn" { 7 | description = "The lambda ARN" 8 | value = aws_lambda_function.lambda_function.arn 9 | } 10 | 11 | output "function_version" { 12 | description = "The function version that is deployed" 13 | value = aws_lambda_function.lambda_function.version 14 | } 15 | 16 | output "invoke_arn" { 17 | description = "The API Gateway invoke ARN" 18 | value = aws_lambda_function.lambda_function.invoke_arn 19 | } 20 | 21 | output "qualified_arn" { 22 | description = "The function qualified ARN" 23 | value = aws_lambda_function.lambda_function.qualified_arn 24 | } 25 | 26 | output "function_url_hostname" { 27 | description = "The hostname for the lambda function url" 28 | value = length(aws_lambda_function_url.function_url) > 0 ? trimsuffix(trimprefix(one(aws_lambda_function_url.function_url).function_url, "https://"), "/") : null 29 | } 30 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "function_name" { 2 | description = "The name of the lambda function" 3 | type = string 4 | } 5 | 6 | variable "zip_file" { 7 | description = "The absolute path to the lambda function zip" 8 | type = string 9 | } 10 | 11 | variable "hash" { 12 | description = "A hash to determine whether a new version of the lambda function should be deployed" 13 | type = string 14 | } 15 | 16 | variable "runtime" { 17 | description = "The runtime of the lambda function" 18 | type = string 19 | } 20 | 21 | variable "handler" { 22 | description = "The handler of the lambda function" 23 | type = string 24 | } 25 | 26 | variable "memory_size" { 27 | description = "The memory size of the lambda function" 28 | type = number 29 | } 30 | 31 | variable "timeout" { 32 | description = "The timeout of the lambda function" 33 | type = number 34 | } 35 | 36 | variable "iam" { 37 | description = "Override the default IAM configuration" 38 | type = object({ 39 | path = optional(string, "/") 40 | permissions_boundary = optional(string) 41 | }) 42 | default = {} 43 | } 44 | 45 | variable "cloudwatch_log" { 46 | description = "Override the Cloudwatch logs configuration" 47 | type = object({ 48 | retention_in_days = number 49 | }) 50 | default = { 51 | retention_in_days = 7 52 | } 53 | } 54 | 55 | variable "environment_variables" { 56 | description = "Specify additional environment variables for the lambda function" 57 | type = map(string) 58 | default = {} 59 | } 60 | 61 | variable "iam_policy_statements" { 62 | description = "Additional IAM policy statements to attach to the role in addition to the default cloudwatch logs, xray and kms permissions" 63 | type = any 64 | default = [] 65 | } 66 | 67 | variable "additional_iam_policies" { 68 | description = "Specify additional IAM policies to attach to the lambda execution role" 69 | type = list(object({ 70 | name = string, 71 | arn = optional(string) 72 | policy = optional(string) 73 | })) 74 | default = [] 75 | 76 | validation { 77 | condition = alltrue([ 78 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.arn != null || additional_iam_policy.policy != null 79 | ]) 80 | error_message = "Either the ARN or policy must be specified for each additional IAM policy" 81 | } 82 | } 83 | 84 | variable "vpc" { 85 | description = "The configuration to run the lambda in a VPC" 86 | type = object({ 87 | security_group_ids = list(string), 88 | subnet_ids = list(string) 89 | }) 90 | default = null 91 | } 92 | 93 | variable "publish" { 94 | description = "Whether to publish a new lambda version" 95 | type = bool 96 | default = true 97 | } 98 | 99 | variable "function_url" { 100 | description = "Configure function URL" 101 | type = object({ 102 | create = optional(bool, true) 103 | authorization_type = optional(string, "NONE") 104 | }) 105 | default = {} 106 | } 107 | 108 | variable "architecture" { 109 | description = "Instruction set architecture for the lambda function" 110 | type = string 111 | } 112 | 113 | variable "run_at_edge" { 114 | description = "Whether the function runs at the edge" 115 | type = bool 116 | default = false 117 | } 118 | 119 | variable "prefix" { 120 | description = "A prefix which will be attached to resource name to esnure resources are random" 121 | type = string 122 | } 123 | 124 | variable "suffix" { 125 | description = "A suffix which will be attached to resource name to esnure resources are random" 126 | type = string 127 | } 128 | 129 | variable "timeouts" { 130 | description = "Define maximum timeout for creating, updating, and deleting function" 131 | type = object({ 132 | create = optional(string) 133 | update = optional(string) 134 | delete = optional(string) 135 | }) 136 | default = {} 137 | } -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-lambda/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 4.67.0" 8 | configuration_aliases = [aws.iam] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-scheduled-lambda/main.tf: -------------------------------------------------------------------------------- 1 | # Lambda Function 2 | 3 | module "function" { 4 | source = "../tf-aws-lambda" 5 | 6 | function_name = var.function_name 7 | zip_file = var.zip_file 8 | hash = var.hash 9 | 10 | runtime = var.runtime 11 | handler = var.handler 12 | 13 | memory_size = var.memory_size 14 | timeout = var.timeout 15 | 16 | environment_variables = var.environment_variables 17 | 18 | additional_iam_policies = var.additional_iam_policies 19 | iam_policy_statements = var.iam_policy_statements 20 | 21 | architecture = var.architecture 22 | 23 | iam = var.iam 24 | cloudwatch_log = var.cloudwatch_log 25 | 26 | vpc = var.vpc 27 | 28 | prefix = var.prefix 29 | suffix = var.suffix 30 | 31 | timeouts = var.timeouts 32 | 33 | providers = { 34 | aws.iam = aws.iam 35 | } 36 | } 37 | 38 | # Eventbridge 39 | 40 | resource "aws_cloudwatch_event_rule" "cron" { 41 | name = "${var.prefix}${var.function_name}-cron${var.suffix}" 42 | schedule_expression = var.schedule 43 | } 44 | 45 | resource "aws_cloudwatch_event_target" "trigger_lambda_on_schedule" { 46 | rule = aws_cloudwatch_event_rule.cron.name 47 | target_id = "lambda" 48 | arn = module.function.function_arn 49 | } 50 | 51 | resource "aws_lambda_permission" "allow_eventbridge_to_invoke_lambda" { 52 | statement_id = "AllowExecutionFromEventbridge" 53 | action = "lambda:InvokeFunction" 54 | function_name = module.function.function_name 55 | principal = "events.amazonaws.com" 56 | source_arn = aws_cloudwatch_event_rule.cron.arn 57 | } 58 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-scheduled-lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_name" { 2 | description = "The lambda function name" 3 | value = module.function.function_name 4 | } 5 | 6 | output "function_arn" { 7 | description = "The lambda ARN" 8 | value = module.function.function_arn 9 | } 10 | 11 | output "function_version" { 12 | description = "The function version that is deployed" 13 | value = module.function.function_version 14 | } 15 | 16 | output "function_url_hostname" { 17 | description = "The hostname for the lambda function url" 18 | value = module.function.function_url_hostname 19 | } 20 | -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-scheduled-lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "function_name" { 2 | description = "The name of the lambda function" 3 | type = string 4 | } 5 | 6 | variable "zip_file" { 7 | description = "The absolute path to the lambda function zip" 8 | type = string 9 | } 10 | 11 | variable "hash" { 12 | description = "A hash to determine whether a new version of the lambda function should be deployed" 13 | type = string 14 | } 15 | 16 | variable "runtime" { 17 | description = "The runtime of the lambda function" 18 | type = string 19 | } 20 | 21 | variable "handler" { 22 | description = "The handler of the lambda function" 23 | type = string 24 | } 25 | 26 | variable "memory_size" { 27 | description = "The memory size of the lambda function" 28 | type = number 29 | } 30 | 31 | variable "timeout" { 32 | description = "The timeout of the lambda function" 33 | type = number 34 | } 35 | 36 | variable "iam" { 37 | description = "Override the default IAM configuration" 38 | type = object({ 39 | path = optional(string, "/") 40 | permissions_boundary = optional(string) 41 | }) 42 | default = {} 43 | } 44 | 45 | variable "cloudwatch_log" { 46 | description = "Override the Cloudwatch logs configuration" 47 | type = object({ 48 | retention_in_days = number 49 | }) 50 | default = { 51 | retention_in_days = 7 52 | } 53 | } 54 | 55 | variable "environment_variables" { 56 | description = "Specify additional environment variables for the lambda function" 57 | type = map(string) 58 | default = {} 59 | } 60 | 61 | variable "iam_policy_statements" { 62 | description = "Additional IAM policy statements to attach to the role in addition to the default cloudwatch logs, xray and kms permissions" 63 | type = any 64 | default = [] 65 | } 66 | 67 | variable "additional_iam_policies" { 68 | description = "Specify additional IAM policies to attach to the lambda execution role" 69 | type = list(object({ 70 | name = string, 71 | arn = optional(string) 72 | policy = optional(string) 73 | })) 74 | default = [] 75 | } 76 | 77 | variable "vpc" { 78 | description = "The configuration to run the lambda in a VPC" 79 | type = object({ 80 | security_group_ids = list(string), 81 | subnet_ids = list(string) 82 | }) 83 | default = null 84 | } 85 | 86 | variable "publish" { 87 | description = "Whether to publish a new lambda version" 88 | type = bool 89 | default = true 90 | } 91 | 92 | variable "architecture" { 93 | description = "Instruction set architecture for the lambda function" 94 | type = string 95 | default = "arm64" 96 | } 97 | 98 | variable "schedule" { 99 | description = "The schedule to invoke the function" 100 | type = string 101 | } 102 | 103 | variable "prefix" { 104 | description = "A prefix which will be attached to resource name to esnure resources are random" 105 | type = string 106 | } 107 | 108 | variable "suffix" { 109 | description = "A suffix which will be attached to resource name to esnure resources are random" 110 | type = string 111 | } 112 | 113 | variable "timeouts" { 114 | description = "Define maximum timeout for creating, updating, and deleting function" 115 | type = object({ 116 | create = optional(string) 117 | update = optional(string) 118 | delete = optional(string) 119 | }) 120 | default = {} 121 | } -------------------------------------------------------------------------------- /modules/legacy/modules/tf-aws-scheduled-lambda/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 4.67.0" 8 | configuration_aliases = [aws.iam] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/legacy/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cloudfront_url" { 2 | description = "The URL for the cloudfront distribution" 3 | value = "https://${aws_cloudfront_distribution.website_distribution.domain_name}" 4 | } 5 | 6 | output "domain_names" { 7 | description = "The custom domain names attached to the cloudfront distribution" 8 | value = [for alias in local.aliases : "https://${alias}"] 9 | } 10 | -------------------------------------------------------------------------------- /modules/legacy/scripts/invalidate-cloudfront.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INVALIDATION_ID=$(aws cloudfront create-invalidation --distribution-id $CDN_ID --paths "/*" | jq -r '.Invalidation.Id') 4 | wait_for_invalidation() { 5 | while [ $(aws cloudfront get-invalidation --id $INVALIDATION_ID --distribution-id $CDN_ID | jq -r '.Invalidation.Status') != "Completed" ] 6 | do 7 | aws cloudfront wait invalidation-completed --distribution-id $CDN_ID --id $INVALIDATION_ID 8 | done 9 | echo "Invalidation Cache Complete"; 10 | } 11 | wait_for_invalidation 12 | aws cloudfront wait distribution-deployed --id $CDN_ID -------------------------------------------------------------------------------- /modules/legacy/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "A prefix which will be attached to the resource name to ensure resources are random" 3 | type = string 4 | default = null 5 | } 6 | 7 | variable "suffix" { 8 | description = "A suffix which will be attached to the resource name to ensure resources are random" 9 | type = string 10 | default = null 11 | } 12 | 13 | variable "force_destroy" { 14 | description = "Whether to allow force destruction of resources i.e. website bucket where all objects should be deleted from the bucket when the bucket is destroyed" 15 | type = bool 16 | default = false 17 | } 18 | 19 | variable "iam" { 20 | description = "Override the default IAM configuration" 21 | type = object({ 22 | path = optional(string, "/") 23 | permissions_boundary = optional(string) 24 | }) 25 | default = {} 26 | } 27 | 28 | variable "cloudwatch_log" { 29 | description = "Override the Cloudwatch logs configuration" 30 | type = object({ 31 | retention_in_days = number 32 | }) 33 | default = { 34 | retention_in_days = 7 35 | } 36 | } 37 | 38 | variable "preferred_architecture" { 39 | description = "Preferred instruction set architecture for the lambda function. If lambda@edge is used for the server function, the architecture will be set to x86_64 for that function" 40 | type = string 41 | default = "arm64" 42 | } 43 | 44 | variable "vpc" { 45 | description = "The default VPC configuration for the lambda resources. This can be overridden for each function" 46 | type = object({ 47 | security_group_ids = list(string), 48 | subnet_ids = list(string) 49 | }) 50 | default = null 51 | } 52 | 53 | variable "open_next" { 54 | description = "The next.js website config for single and multi-zone deployments" 55 | type = object({ 56 | exclusion_regex = optional(string) 57 | root_folder_path = string 58 | additional_zones = optional(list(object({ 59 | name = string 60 | http_path = string 61 | folder_path = string 62 | })), []) 63 | }) 64 | } 65 | 66 | variable "cache_control_immutable_assets_regex" { 67 | description = "Regex to set public,max-age=31536000,immutable on immutable resources" 68 | type = string 69 | default = "^.*(\\.js|\\.css|\\.woff2)$" 70 | } 71 | 72 | variable "content_types" { 73 | description = "The MIME type mapping and default for artefacts generated by Open Next" 74 | type = object({ 75 | mapping = optional(map(string), { 76 | "svg" = "image/svg+xml", 77 | "js" = "application/javascript", 78 | "css" = "text/css", 79 | }) 80 | default = optional(string, "binary/octet-stream") 81 | }) 82 | default = {} 83 | } 84 | 85 | variable "domain" { 86 | description = "Configuration to for attaching a custom domain to the CloudFront distribution" 87 | type = object({ 88 | create = optional(bool, false) 89 | hosted_zone_name = optional(string), 90 | name = optional(string), 91 | alternate_names = optional(list(string), []) 92 | acm_certificate_arn = optional(string), 93 | evaluate_target_health = optional(bool, false) 94 | }) 95 | default = {} 96 | } 97 | 98 | variable "cloudfront" { 99 | description = "Configuration for the CloudFront distribution" 100 | type = object({ 101 | enabled = optional(bool, true) 102 | invalidate_on_change = optional(bool, true) 103 | minimum_protocol_version = optional(string, "TLSv1.2_2021") 104 | ssl_support_method = optional(string, "sni-only") 105 | http_version = optional(string, "http2and3") 106 | ipv6_enabled = optional(bool, true) 107 | price_class = optional(string, "PriceClass_100") 108 | geo_restrictions = optional(object({ 109 | type = optional(string, "none"), 110 | locations = optional(list(string), []) 111 | }), {}) 112 | }) 113 | default = {} 114 | } 115 | 116 | variable "warmer_function" { 117 | description = "Configuration for the warmer function" 118 | type = object({ 119 | create = bool 120 | runtime = optional(string, "nodejs18.x") 121 | concurrency = optional(number, 20) 122 | timeout = optional(number, 15 * 60) // 15 minutes 123 | memory_size = optional(number, 1024) 124 | schedule = optional(string, "rate(5 minutes)") 125 | additional_environment_variables = optional(map(string), {}) 126 | additional_iam_policies = optional(list(object({ 127 | name = string, 128 | arn = optional(string) 129 | policy = optional(string) 130 | })), []) 131 | vpc = optional(object({ 132 | security_group_ids = list(string), 133 | subnet_ids = list(string) 134 | })) 135 | timeouts = optional(object({ 136 | create = optional(string) 137 | update = optional(string) 138 | delete = optional(string) 139 | }), {}) 140 | }) 141 | default = { 142 | create = false 143 | } 144 | } 145 | 146 | variable "server_function" { 147 | description = "Configuration for the server function" 148 | type = object({ 149 | runtime = optional(string, "nodejs18.x") 150 | deployment = optional(string, "REGIONAL_LAMBDA") 151 | timeout = optional(number, 10) 152 | memory_size = optional(number, 1024) 153 | additional_environment_variables = optional(map(string), {}) 154 | additional_iam_policies = optional(list(object({ 155 | name = string, 156 | arn = optional(string) 157 | policy = optional(string) 158 | })), []) 159 | vpc = optional(object({ 160 | security_group_ids = list(string), 161 | subnet_ids = list(string) 162 | })) 163 | timeouts = optional(object({ 164 | create = optional(string) 165 | update = optional(string) 166 | delete = optional(string) 167 | }), {}) 168 | }) 169 | default = {} 170 | 171 | validation { 172 | condition = contains(["API_GATEWAY", "REGIONAL_LAMBDA", "EDGE_LAMBDA"], var.server_function.deployment) 173 | error_message = "The server function deployment can be one of API_GATEWAY, REGIONAL_LAMBDA or EDGE_LAMBDA" 174 | } 175 | } 176 | 177 | variable "image_optimisation_function" { 178 | description = "Configuration for the image optimisation function" 179 | type = object({ 180 | runtime = optional(string, "nodejs18.x") 181 | deployment = optional(string, "REGIONAL_LAMBDA") 182 | timeout = optional(number, 25) 183 | memory_size = optional(number, 1536) 184 | additional_environment_variables = optional(map(string), {}) 185 | additional_iam_policies = optional(list(object({ 186 | name = string, 187 | arn = optional(string) 188 | policy = optional(string) 189 | })), []) 190 | vpc = optional(object({ 191 | security_group_ids = list(string), 192 | subnet_ids = list(string) 193 | })) 194 | timeouts = optional(object({ 195 | create = optional(string) 196 | update = optional(string) 197 | delete = optional(string) 198 | }), {}) 199 | }) 200 | default = {} 201 | 202 | validation { 203 | condition = contains(["API_GATEWAY", "REGIONAL_LAMBDA"], var.image_optimisation_function.deployment) 204 | error_message = "The image optimisation function deployment can be either API_GATEWAY or REGIONAL_LAMBDA" 205 | } 206 | } 207 | 208 | variable "isr" { 209 | description = "Configuration for ISR, including creation and function config. To use ISR you need to use at least 2.x of Open Next, for 1.x please set create to false" 210 | type = object({ 211 | create = bool 212 | revalidation_function = optional(object({ 213 | runtime = optional(string, "nodejs18.x") 214 | deployment = optional(string, "REGIONAL_LAMBDA") 215 | timeout = optional(number, 30) 216 | memory_size = optional(number, 128) 217 | additional_environment_variables = optional(map(string), {}) 218 | additional_iam_policies = optional(list(object({ 219 | name = string, 220 | arn = optional(string) 221 | policy = optional(string) 222 | })), []) 223 | vpc = optional(object({ 224 | security_group_ids = list(string), 225 | subnet_ids = list(string) 226 | })) 227 | timeouts = optional(object({ 228 | create = optional(string) 229 | update = optional(string) 230 | delete = optional(string) 231 | }), {}) 232 | }), {}) 233 | tag_mapping_db = optional(object({ 234 | create = bool 235 | billing_mode = optional(string, "PAY_PER_REQUEST") 236 | read_capacity = optional(number) 237 | write_capacity = optional(number) 238 | }), { 239 | create = true 240 | }) 241 | }) 242 | default = { 243 | create = true 244 | } 245 | } 246 | 247 | variable "waf" { 248 | description = "Configuration for the CloudFront distribution WAF" 249 | type = object({ 250 | web_acl_id = optional(string) 251 | }) 252 | default = {} 253 | } 254 | -------------------------------------------------------------------------------- /modules/legacy/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | archive = { 6 | source = "hashicorp/archive" 7 | version = ">= 2.3.0" 8 | } 9 | aws = { 10 | source = "hashicorp/aws" 11 | version = ">= 4.67.0" 12 | configuration_aliases = [aws.server_function, aws.iam, aws.dns] 13 | } 14 | terraform = { 15 | source = "terraform.io/builtin/terraform" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/tf-aws-lambda/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /modules/tf-aws-lambda/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | prefix = var.prefix == null ? "" : "${var.prefix}-" 3 | suffix = var.suffix == null ? "" : "-${var.suffix}" 4 | 5 | should_use_zip = var.function_code.zip != null 6 | trigger_on_schedule = var.schedule != null 7 | 8 | alias_names = var.aliases.create ? var.aliases.names : [] 9 | 10 | has_log_group_per_function = try(var.cloudwatch_log.deployment, "PER_FUNCTION") == "PER_FUNCTION" 11 | 12 | # Ensure IAM role name stays within 64 character limit. If name exceeds limit, truncate and add a hash suffix for uniqueness 13 | full_role_name = "${local.prefix}${var.function_name}-role${local.suffix}" 14 | role_name = length(local.full_role_name) > 64 ? "${substr(local.full_role_name, 0, 58)}-${substr(sha1(local.full_role_name), 0, 5)}" : local.full_role_name 15 | } 16 | 17 | # Lambda 18 | 19 | resource "aws_lambda_function" "lambda_function" { 20 | filename = local.should_use_zip ? try(var.function_code.zip.path) : null 21 | source_code_hash = local.should_use_zip ? try(var.function_code.zip.hash) : null 22 | s3_bucket = local.should_use_zip ? null : try(var.function_code.s3.bucket) 23 | s3_key = local.should_use_zip ? null : try(var.function_code.s3.key) 24 | s3_object_version = local.should_use_zip ? null : try(var.function_code.s3.object_version) 25 | 26 | function_name = "${local.prefix}${var.function_name}${local.suffix}" 27 | role = aws_iam_role.lambda_iam.arn 28 | 29 | runtime = var.runtime 30 | handler = var.handler 31 | memory_size = var.memory_size 32 | timeout = var.timeout 33 | 34 | layers = var.layers 35 | architectures = var.run_at_edge == false ? [var.architecture] : ["x86_64"] 36 | publish = var.publish 37 | 38 | dynamic "environment" { 39 | for_each = length(var.environment_variables) > 0 ? [var.environment_variables] : [] 40 | content { 41 | variables = environment.value 42 | } 43 | } 44 | 45 | dynamic "tracing_config" { 46 | for_each = var.xray_tracing.enable == true ? [var.xray_tracing] : [] 47 | content { 48 | mode = tracing_config.value.mode 49 | } 50 | } 51 | 52 | dynamic "logging_config" { 53 | for_each = var.logging_config != null || contains(["SHARED_PER_ZONE", "USE_EXISTING"], try(var.cloudwatch_log.deployment, null)) ? [var.logging_config] : [] 54 | 55 | content { 56 | log_format = coalesce(try(logging_config.value.log_format, null), "Text") 57 | log_group = contains(["SHARED_PER_ZONE", "USE_EXISTING"], try(var.cloudwatch_log.deployment, null)) ? var.cloudwatch_log.name : null 58 | application_log_level = try(logging_config.value.application_log_level, null) 59 | system_log_level = try(logging_config.value.system_log_level, null) 60 | } 61 | } 62 | 63 | dynamic "vpc_config" { 64 | for_each = var.vpc != null ? [var.vpc] : [] 65 | 66 | content { 67 | security_group_ids = vpc_config.value.security_group_ids 68 | subnet_ids = vpc_config.value.subnet_ids 69 | } 70 | } 71 | 72 | timeouts { 73 | create = try(var.timeouts.create, null) 74 | update = try(var.timeouts.update, null) 75 | delete = try(var.timeouts.delete, null) 76 | } 77 | } 78 | 79 | resource "aws_lambda_alias" "lambda_alias" { 80 | for_each = var.run_at_edge == false ? toset(local.alias_names) : [] 81 | name = each.value 82 | function_name = aws_lambda_function.lambda_function.function_name 83 | function_version = aws_lambda_function.lambda_function.version 84 | 85 | lifecycle { 86 | ignore_changes = [ 87 | function_version 88 | ] 89 | } 90 | } 91 | resource "terraform_data" "update_alias" { 92 | count = var.run_at_edge == false && var.aliases.create ? 1 : 0 93 | triggers_replace = [aws_lambda_function.lambda_function.version, var.aliases.alias_to_update] 94 | 95 | provisioner "local-exec" { 96 | command = "${coalesce(try(var.scripts.update_alias_script.interpreter, var.scripts.interpreter, null), "/bin/bash")} ${try(var.scripts.update_alias_script.path, "${path.module}/scripts/update-alias.sh")}" 97 | 98 | environment = merge({ 99 | "FUNCTION_NAME" = aws_lambda_function.lambda_function.function_name 100 | "FUNCTION_VERSION" = aws_lambda_function.lambda_function.version 101 | "FUNCTION_ALIAS" = var.aliases.alias_to_update 102 | }, try(var.scripts.additional_environment_variables, {}), try(var.scripts.update_alias_script.additional_environment_variables, {})) 103 | } 104 | 105 | depends_on = [aws_lambda_alias.lambda_alias] 106 | } 107 | 108 | resource "aws_lambda_function_url" "function_url" { 109 | for_each = var.run_at_edge == false && var.function_url.create ? toset(local.alias_names) : [] 110 | 111 | function_name = aws_lambda_function.lambda_function.function_name 112 | qualifier = aws_lambda_alias.lambda_alias[each.value].name 113 | authorization_type = var.function_url.authorization_type 114 | invoke_mode = var.function_url.enable_streaming == true ? "RESPONSE_STREAM" : "BUFFERED" 115 | } 116 | 117 | resource "aws_lambda_permission" "function_url_permission" { 118 | count = var.run_at_edge == false && var.function_url.create && var.function_url.allow_any_principal ? 1 : 0 119 | 120 | action = "lambda:InvokeFunctionUrl" 121 | function_name = aws_lambda_function.lambda_function.function_name 122 | principal = "*" 123 | function_url_auth_type = var.function_url.authorization_type 124 | } 125 | 126 | # Cloudwatch Logs 127 | 128 | resource "aws_cloudwatch_log_group" "lambda_log_group" { 129 | count = var.run_at_edge == false && local.has_log_group_per_function ? 1 : 0 130 | 131 | name = "/aws/lambda/${local.prefix}${coalesce(try(var.cloudwatch_log.name, null), var.function_name)}${local.suffix}" 132 | retention_in_days = try(var.cloudwatch_log.retention_in_days, null) 133 | log_group_class = try(var.cloudwatch_log.log_group_class, null) 134 | skip_destroy = try(var.cloudwatch_log.skip_destroy, null) 135 | } 136 | 137 | # IAM 138 | 139 | resource "aws_iam_role" "lambda_iam" { 140 | name = local.role_name 141 | path = try(var.iam.path, null) 142 | permissions_boundary = try(var.iam.permissions_boundary, null) 143 | 144 | assume_role_policy = jsonencode({ 145 | "Version" : "2012-10-17", 146 | "Statement" : concat([ 147 | { 148 | "Action" : "sts:AssumeRole", 149 | "Principal" : { 150 | "Service" : "lambda.amazonaws.com" 151 | }, 152 | "Effect" : "Allow" 153 | } 154 | ], var.run_at_edge ? [ 155 | { 156 | "Action" : "sts:AssumeRole", 157 | "Principal" : { 158 | "Service" : "edgelambda.amazonaws.com" 159 | }, 160 | "Effect" : "Allow" 161 | } 162 | ] : []) 163 | }) 164 | 165 | provider = aws.iam 166 | } 167 | 168 | resource "aws_iam_policy" "lambda_policy" { 169 | name = "${local.prefix}${var.function_name}-lambda-policy${local.suffix}" 170 | path = try(var.iam.path, null) 171 | 172 | policy = jsonencode({ 173 | "Version" : "2012-10-17", 174 | "Statement" : concat([ 175 | { 176 | "Action" : concat([ 177 | "logs:CreateLogStream", 178 | "logs:PutLogEvents" 179 | ], var.run_at_edge ? ["logs:CreateLogGroup"] : []), 180 | "Resource" : var.run_at_edge ? "*" : local.has_log_group_per_function ? "${one(aws_cloudwatch_log_group.lambda_log_group[*].arn)}:*" : "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:${var.cloudwatch_log.name}:*" 181 | "Effect" : "Allow" 182 | } 183 | ], 184 | var.vpc != null ? [ 185 | { 186 | "Action" : [ 187 | "ec2:CreateNetworkInterface", 188 | "ec2:DescribeNetworkInterfaces", 189 | "ec2:DeleteNetworkInterface", 190 | "ec2:AssignPrivateIpAddresses", 191 | "ec2:UnassignPrivateIpAddresses" 192 | ], 193 | "Resource" : "*" 194 | "Effect" : "Allow" 195 | } 196 | ] : [], 197 | var.xray_tracing.enable ? [ 198 | { 199 | "Action" : [ 200 | "xray:PutTraceSegments", 201 | "xray:PutTelemetryRecords" 202 | ], 203 | "Resource" : "*", 204 | "Effect" : "Allow" 205 | } 206 | ] : [], 207 | var.iam_policy_statements) 208 | }) 209 | 210 | provider = aws.iam 211 | } 212 | 213 | resource "aws_iam_role_policy_attachment" "policy_attachment" { 214 | role = aws_iam_role.lambda_iam.name 215 | policy_arn = aws_iam_policy.lambda_policy.arn 216 | 217 | provider = aws.iam 218 | } 219 | 220 | resource "aws_iam_policy" "additional_policy" { 221 | for_each = { 222 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.name => additional_iam_policy if additional_iam_policy.policy != null 223 | } 224 | name = "${local.prefix}${var.function_name}-${each.key}${local.suffix}" 225 | path = try(var.iam.path, null) 226 | 227 | policy = each.value.policy 228 | 229 | provider = aws.iam 230 | } 231 | 232 | resource "aws_iam_role_policy_attachment" "additional_policy_attachment" { 233 | for_each = { 234 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.name => additional_iam_policy 235 | } 236 | role = aws_iam_role.lambda_iam.name 237 | policy_arn = coalesce(each.value.arn, try(aws_iam_policy.additional_policy[each.key].arn, null)) 238 | 239 | provider = aws.iam 240 | } 241 | 242 | # Event Rule (CRON) 243 | 244 | resource "aws_cloudwatch_event_rule" "cron" { 245 | count = local.trigger_on_schedule ? 1 : 0 246 | 247 | name = "${local.prefix}${var.function_name}-cron${local.suffix}" 248 | schedule_expression = var.schedule 249 | } 250 | 251 | resource "aws_cloudwatch_event_target" "trigger_lambda_on_schedule" { 252 | count = local.trigger_on_schedule ? 1 : 0 253 | 254 | rule = aws_cloudwatch_event_rule.cron[0].name 255 | target_id = "lambda" 256 | arn = try(aws_lambda_alias.lambda_alias[0].arn, aws_lambda_function.lambda_function.arn) 257 | } 258 | 259 | resource "aws_lambda_permission" "allow_eventbridge_to_invoke_lambda" { 260 | count = local.trigger_on_schedule ? 1 : 0 261 | 262 | statement_id = "AllowExecutionFromEventbridge" 263 | action = "lambda:InvokeFunction" 264 | function_name = try(aws_lambda_alias.lambda_alias[local.alias_names[0]].function_name, aws_lambda_function.lambda_function.function_name) 265 | principal = "events.amazonaws.com" 266 | source_arn = aws_cloudwatch_event_rule.cron[0].arn 267 | } 268 | -------------------------------------------------------------------------------- /modules/tf-aws-lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | description = "The lambda function name" 3 | value = aws_lambda_function.lambda_function.function_name 4 | } 5 | 6 | output "arn" { 7 | description = "The lambda ARN" 8 | value = aws_lambda_function.lambda_function.arn 9 | } 10 | 11 | output "version" { 12 | description = "The function version that is deployed" 13 | value = aws_lambda_function.lambda_function.version 14 | } 15 | 16 | output "qualified_arn" { 17 | description = "The function qualified ARN" 18 | value = aws_lambda_function.lambda_function.qualified_arn 19 | } 20 | 21 | output "url_hostnames" { 22 | description = "The hostname for the lambda function urls" 23 | value = length(aws_lambda_function_url.function_url) > 0 ? { for url in aws_lambda_function_url.function_url : url.qualifier => trimsuffix(trimprefix(aws_lambda_function_url.function_url[url.qualifier].function_url, "https://"), "/") } : {} 24 | } 25 | -------------------------------------------------------------------------------- /modules/tf-aws-lambda/scripts/update-alias.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $FUNCTION_NAME ]; then 17 | exitcode=1 18 | echo "Error: function name was not supplied" 19 | fi 20 | 21 | if [ -z $FUNCTION_ALIAS ]; then 22 | exitcode=1 23 | echo "Error: function alias was not supplied" 24 | fi 25 | 26 | if [ -z $FUNCTION_VERSION ]; then 27 | exitcode=1 28 | echo "Error: function version was not supplied" 29 | fi 30 | 31 | if [ $exitcode -ne 0 ]; then 32 | exit $exitcode 33 | fi 34 | 35 | ## Script 36 | 37 | aws lambda update-alias --function-name $FUNCTION_NAME --name $FUNCTION_ALIAS --function-version $FUNCTION_VERSION 38 | -------------------------------------------------------------------------------- /modules/tf-aws-lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "A prefix which will be attached to resource name to esnure resources are random" 3 | type = string 4 | } 5 | 6 | variable "suffix" { 7 | description = "A suffix which will be attached to resource name to esnure resources are random" 8 | type = string 9 | } 10 | 11 | variable "function_name" { 12 | description = "The name of the lambda function" 13 | type = string 14 | } 15 | 16 | variable "function_code" { 17 | type = object({ 18 | zip = optional(object({ 19 | path = string 20 | hash = string 21 | })) 22 | s3 = optional(object({ 23 | bucket = string 24 | key = string 25 | object_version = optional(string) 26 | })) 27 | }) 28 | } 29 | 30 | variable "runtime" { 31 | description = "The runtime of the lambda function" 32 | type = string 33 | } 34 | 35 | variable "handler" { 36 | description = "The handler of the lambda function" 37 | type = string 38 | } 39 | 40 | variable "memory_size" { 41 | description = "The memory size of the lambda function" 42 | type = number 43 | } 44 | 45 | variable "timeout" { 46 | description = "The timeout of the lambda function" 47 | type = number 48 | } 49 | 50 | variable "iam" { 51 | description = "Override the default IAM configuration" 52 | type = object({ 53 | path = optional(string, "/") 54 | permissions_boundary = optional(string) 55 | }) 56 | default = {} 57 | } 58 | 59 | variable "logging_config" { 60 | description = "Override default function logging configuration. The log group is determined by the cloudwatch_log variable" 61 | type = object({ 62 | log_format = optional(string) 63 | application_log_level = optional(string) 64 | system_log_level = optional(string) 65 | }) 66 | default = null 67 | } 68 | 69 | variable "cloudwatch_log" { 70 | description = "Override the Cloudwatch logs configuration" 71 | type = object({ 72 | deployment = optional(string, "PER_FUNCTION") 73 | retention_in_days = optional(number, 7) 74 | name = optional(string) 75 | log_group_class = optional(string) 76 | skip_destroy = optional(bool) 77 | }) 78 | default = {} 79 | 80 | validation { 81 | condition = try(var.cloudwatch_log.deployment, null) == null || contains(["SHARED_PER_ZONE", "PER_FUNCTION", "USE_EXISTING"], var.cloudwatch_log.deployment) 82 | error_message = "The cloudwatch log group deployment type can be one of null, SHARED_PER_ZONE, PER_FUNCTION or USE_EXISTING" 83 | } 84 | } 85 | 86 | variable "environment_variables" { 87 | description = "Specify environment variables for the lambda function" 88 | type = map(string) 89 | default = {} 90 | } 91 | 92 | variable "iam_policy_statements" { 93 | description = "Additional IAM policy statements to attach to the role in addition to the default cloudwatch logs, xray and kms permissions" 94 | type = any 95 | default = [] 96 | } 97 | 98 | variable "additional_iam_policies" { 99 | description = "Specify additional IAM policies to attach to the lambda execution role" 100 | type = list(object({ 101 | name = string, 102 | arn = optional(string) 103 | policy = optional(string) 104 | })) 105 | default = [] 106 | 107 | validation { 108 | condition = alltrue([ 109 | for additional_iam_policy in var.additional_iam_policies : additional_iam_policy.arn != null || additional_iam_policy.policy != null 110 | ]) 111 | error_message = "Either the ARN or policy must be specified for each additional IAM policy" 112 | } 113 | } 114 | 115 | variable "vpc" { 116 | description = "The configuration to run the lambda in a VPC" 117 | type = object({ 118 | security_group_ids = list(string), 119 | subnet_ids = list(string) 120 | }) 121 | default = null 122 | } 123 | 124 | variable "publish" { 125 | description = "Whether to publish a new lambda version" 126 | type = bool 127 | default = true 128 | } 129 | 130 | variable "function_url" { 131 | description = "Configure function URL" 132 | type = object({ 133 | create = optional(bool, true) 134 | allow_any_principal = optional(bool, false) 135 | authorization_type = optional(string, "NONE") 136 | enable_streaming = optional(bool, false) 137 | }) 138 | default = {} 139 | } 140 | 141 | variable "architecture" { 142 | description = "Instruction set architecture for the lambda function" 143 | type = string 144 | default = "arm64" 145 | } 146 | 147 | variable "run_at_edge" { 148 | description = "Whether the function runs at the edge" 149 | type = bool 150 | default = false 151 | } 152 | 153 | variable "aliases" { 154 | description = "List of aliases to create" 155 | type = object({ 156 | create = optional(bool, true) 157 | names = optional(list(string), ["stable"]) 158 | alias_to_update = optional(string, "stable") 159 | }) 160 | default = {} 161 | } 162 | 163 | variable "xray_tracing" { 164 | description = "Configuration for AWS tracing on the function" 165 | type = object({ 166 | enable = optional(bool, false), 167 | mode = optional(string, "Active") 168 | }) 169 | default = {} 170 | } 171 | 172 | variable "layers" { 173 | description = "A list of layer arns to associate with the lambda" 174 | type = list(string) 175 | default = null 176 | } 177 | 178 | variable "timeouts" { 179 | description = "Define maximum timeout for creating, updating, and deleting function" 180 | type = object({ 181 | create = optional(string) 182 | update = optional(string) 183 | delete = optional(string) 184 | }) 185 | default = {} 186 | } 187 | 188 | variable "schedule" { 189 | description = "The schedule to invoke the function" 190 | type = string 191 | default = null 192 | } 193 | 194 | variable "scripts" { 195 | description = "Modify default script behaviours" 196 | type = object({ 197 | interpreter = optional(string) 198 | additional_environment_variables = optional(map(string)) 199 | update_alias_script = optional(object({ 200 | interpreter = optional(string) 201 | path = optional(string) 202 | additional_environment_variables = optional(map(string)) 203 | })) 204 | }) 205 | default = {} 206 | } 207 | -------------------------------------------------------------------------------- /modules/tf-aws-lambda/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.26.0" 8 | configuration_aliases = [aws.iam] 9 | } 10 | terraform = { 11 | source = "terraform.io/builtin/terraform" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-aliases/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_ssm_parameter" "origin_aliases" { 2 | name = aws_ssm_parameter.origin_aliases.name 3 | } 4 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-aliases/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | prefix = var.prefix == null ? "" : "${var.prefix}-" 3 | suffix = var.suffix == null ? "" : "-${var.suffix}" 4 | 5 | origin_aliases = jsondecode(data.aws_ssm_parameter.origin_aliases.insecure_value) 6 | alias_inverse_mappings = { 7 | "nextjs" = "opennext", 8 | "opennext" = "nextjs" 9 | } 10 | 11 | use_production_alias_when_non_continuous_deployment_strategy = var.continuous_deployment_strategy == "NONE" ? local.origin_aliases.production : null 12 | use_staging_alias_when_deployment_is_detach_or_promote = contains(["DETACH", "PROMOTE"], var.continuous_deployment_strategy) ? local.origin_aliases.staging : null 13 | use_default_alias_when_staging_is_not_set = local.origin_aliases.staging == null ? local.alias_inverse_mappings[local.origin_aliases.production] : null 14 | flip_staging_alias_when_aliases_match = local.origin_aliases.production == local.origin_aliases.staging ? local.alias_inverse_mappings[local.origin_aliases.staging] : local.origin_aliases.staging 15 | alias_details = { 16 | aliases = keys(local.alias_inverse_mappings) 17 | production = local.origin_aliases.production 18 | staging = var.use_continuous_deployment == false ? local.origin_aliases.production : coalesce( 19 | local.use_production_alias_when_non_continuous_deployment_strategy, 20 | local.use_staging_alias_when_deployment_is_detach_or_promote, 21 | local.use_default_alias_when_staging_is_not_set, 22 | local.flip_staging_alias_when_aliases_match 23 | ) 24 | } 25 | 26 | updated_alias_mapping = var.continuous_deployment_strategy == "PROMOTE" ? { production = local.alias_details.staging, staging = local.alias_details.staging } : { production = local.alias_details.production, staging = local.alias_details.staging } 27 | } 28 | 29 | # Config 30 | 31 | resource "aws_ssm_parameter" "origin_aliases" { 32 | name = "${local.prefix}website-origin-aliases${local.suffix}" 33 | type = "String" 34 | value = jsonencode({ production = "nextjs", staging = null }) 35 | 36 | lifecycle { 37 | ignore_changes = [value] 38 | } 39 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-aliases/outputs.tf: -------------------------------------------------------------------------------- 1 | output "parameter_name" { 2 | description = "The name of the SSM parameter" 3 | value = aws_ssm_parameter.origin_aliases.name 4 | } 5 | 6 | output "alias_details" { 7 | description = "The alias details used to update functions, s3 and CloudFront" 8 | value = local.alias_details 9 | } 10 | 11 | output "updated_alias_mapping" { 12 | description = "The update alias mapping" 13 | value = local.updated_alias_mapping 14 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-aliases/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "A prefix which will be attached to the resource name to ensure resources are random" 3 | type = string 4 | default = null 5 | } 6 | 7 | variable "suffix" { 8 | description = "A suffix which will be attached to the resource name to ensure resources are random" 9 | type = string 10 | default = null 11 | } 12 | 13 | variable "use_continuous_deployment" { 14 | description = "Whether continuous deployment is used" 15 | type = bool 16 | } 17 | 18 | variable "continuous_deployment_strategy" { 19 | description = "The continuous deployment strategy" 20 | type = string 21 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-aliases/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.26.0" 8 | } 9 | terraform = { 10 | source = "terraform.io/builtin/terraform" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-multi-zone/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-multi-zone/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | prefix = var.prefix == null ? "" : "${var.prefix}-" 3 | suffix = var.suffix == null ? "" : "-${var.suffix}" 4 | 5 | root_zone_name = [for zone in var.zones : zone.name if zone.root == true][0] 6 | zones = [for zone in var.zones : merge(zone, { path = var.deployment == "INDEPENDENT_ZONES" || zone.root == true ? null : coalesce(zone.path, zone.name) })] 7 | use_shared_distribution = contains(["SHARED_DISTRIBUTION_AND_BUCKET", "SHARED_DISTRIBUTION"], var.deployment) 8 | 9 | function_url_permission_details = local.use_shared_distribution ? merge(flatten([ 10 | for zone in local.zones : [ 11 | for distribution_name in try(one(module.public_resources[*].distributions_provisioned), []) : [ 12 | for alias in try(module.website_zone[zone.name].alias_details, []) : { 13 | for origin_name in try(module.website_zone[zone.name].zone_config.origin_names, []) : "${distribution_name}-${zone.name}-${alias}-${origin_name}" => { 14 | function_name = module.website_zone[zone.name].zone_config.origins[origin_name].backend_name 15 | auth = module.website_zone[zone.name].zone_config.origins[origin_name].auth 16 | zone_name = zone.name, 17 | distribution_name = distribution_name, 18 | alias = alias 19 | } if module.website_zone[zone.name].zone_config.origins[origin_name].auth == "OAC" 20 | } 21 | ] 22 | ] 23 | ])...) : {} 24 | 25 | merged_static_assets = { 26 | zone_overrides = { for zone in local.zones : zone.name => { 27 | paths = try(module.website_zone[zone.name].behaviours.static_assets.paths, null) 28 | additional_paths = try(module.website_zone[zone.name].behaviours.static_assets.additional_paths, null) 29 | } } 30 | } 31 | merged_server = { 32 | zone_overrides = { for zone in local.zones : zone.name => { 33 | paths = try(module.website_zone[zone.name].behaviours.server.paths, null) 34 | origin_request = try(module.website_zone[zone.name].behaviours.server.origin_request, null) 35 | path_overrides = try(module.website_zone[zone.name].behaviours.server.path_overrides, null) 36 | } } 37 | } 38 | merged_additional_origins = merge([for zone in local.zones : { 39 | for name, origin in try(module.website_zone[zone.name].behaviours.additional_origins, {}) : "${zone.name}-${name}" => origin 40 | }]...) 41 | merged_image_optimisation = { 42 | zone_overrides = { for zone in local.zones : zone.name => { 43 | paths = try(module.website_zone[zone.name].behaviours.image_optimisation.paths, null) 44 | } } 45 | } 46 | } 47 | 48 | module "public_resources" { 49 | count = local.use_shared_distribution ? 1 : 0 50 | source = "../tf-aws-open-next-public-resources" 51 | 52 | zones = [for zone in local.zones : merge(module.website_zone[zone.name].zone_config, { 53 | root = zone.root 54 | name = zone.name 55 | path = zone.path 56 | origins = { for name, origin in module.website_zone[zone.name].zone_config.origins : contains(["server", "static_assets", "image_optimisation"], name) ? name : "${zone.name}-${name}" => origin } 57 | })] 58 | 59 | prefix = var.prefix 60 | suffix = var.suffix 61 | 62 | enabled = var.distribution.enabled 63 | ipv6_enabled = var.distribution.ipv6_enabled 64 | http_version = var.distribution.http_version 65 | price_class = var.distribution.price_class 66 | geo_restrictions = var.distribution.geo_restrictions 67 | x_forwarded_host_function = var.distribution.x_forwarded_host_function 68 | auth_function = var.distribution.auth_function 69 | cache_policy = var.distribution.cache_policy 70 | response_headers = var.distribution.response_headers 71 | 72 | behaviours = merge(var.behaviours, { 73 | static_assets = var.behaviours.static_assets == null ? local.merged_static_assets : merge(var.behaviours.static_assets, local.merged_static_assets) 74 | server = var.behaviours.server == null ? local.merged_server : merge(var.behaviours.server, local.merged_server) 75 | image_optimisation = var.behaviours.image_optimisation == null ? local.merged_image_optimisation : merge(var.behaviours.image_optimisation, local.merged_image_optimisation) 76 | additional_origins = var.behaviours.additional_origins == null ? local.merged_additional_origins : merge(var.behaviours.additional_origins, local.merged_additional_origins) 77 | }) 78 | 79 | waf = var.waf 80 | domain_config = var.domain_config 81 | continuous_deployment = var.continuous_deployment 82 | 83 | custom_error_responses = module.website_zone[local.root_zone_name].custom_error_responses 84 | 85 | open_next_version_alias = can(regex("^v2\\.[0-9x]+\\.[0-9x]+$", var.open_next_version)) == true ? "v2" : "v3" 86 | 87 | scripts = var.scripts 88 | 89 | providers = { 90 | aws = aws.global 91 | aws.dns = aws.dns 92 | aws.iam = aws.iam 93 | } 94 | } 95 | 96 | # S3 97 | 98 | resource "aws_s3_bucket" "shared_bucket" { 99 | count = var.deployment == "SHARED_DISTRIBUTION_AND_BUCKET" ? 1 : 0 100 | bucket = "${local.prefix}website-bucket${local.suffix}" 101 | force_destroy = var.website_bucket.force_destroy 102 | } 103 | 104 | resource "aws_s3_bucket_policy" "shared_bucket_policy" { 105 | count = var.deployment == "SHARED_DISTRIBUTION_AND_BUCKET" ? 1 : 0 106 | bucket = one(aws_s3_bucket.shared_bucket[*].id) 107 | policy = jsonencode({ 108 | "Version" : "2012-10-17", 109 | "Statement" : [ 110 | { 111 | "Principal" : { 112 | "Service" : "cloudfront.amazonaws.com" 113 | }, 114 | "Action" : [ 115 | "s3:GetObject" 116 | ], 117 | "Resource" : "${one(aws_s3_bucket.shared_bucket[*].arn)}/*", 118 | "Effect" : "Allow", 119 | "Condition" : { 120 | "StringEquals" : { 121 | "AWS:SourceArn" : compact([try(one(module.public_resources[*].arn), null), try(one(module.public_resources[*].staging_arn), null)]) 122 | } 123 | } 124 | } 125 | ] 126 | }) 127 | } 128 | 129 | # Zones 130 | 131 | module "website_zone" { 132 | for_each = { for zone in local.zones : zone.name => zone } 133 | source = "../tf-aws-open-next-zone" 134 | 135 | prefix = "${local.prefix}${each.value.name}" 136 | suffix = var.suffix 137 | 138 | folder_path = each.value.folder_path 139 | 140 | open_next_version = try(coalesce(each.value.open_next_version, var.open_next_version), null) 141 | 142 | zone_suffix = each.value.path 143 | s3_folder_prefix = var.deployment == "SHARED_DISTRIBUTION_AND_BUCKET" ? each.value.name : null 144 | s3_exclusion_regex = try(coalesce(each.value.s3_exclusion_regex, var.s3_exclusion_regex), null) 145 | function_architecture = try(coalesce(each.value.function_architecture, var.function_architecture), null) 146 | iam = try(coalesce(each.value.iam, var.iam), null) 147 | cloudwatch_log = try(coalesce(each.value.cloudwatch_log, var.cloudwatch_log), null) 148 | vpc = try(coalesce(each.value.vpc, var.vpc), null) 149 | aliases = try(coalesce(each.value.aliases, var.aliases), null) 150 | cache_control_immutable_assets_regex = try(coalesce(each.value.cache_control_immutable_assets_regex, var.cache_control_immutable_assets_regex), null) 151 | content_types = try(coalesce(each.value.content_types, var.content_types), null) 152 | layers = try(coalesce(each.value.layers, var.layers), null) 153 | origin_timeouts = try(coalesce(each.value.origin_timeouts, var.origin_timeouts), null) 154 | xray_tracing = try(coalesce(each.value.xray_tracing, var.xray_tracing), null) 155 | 156 | warmer_function = try(coalesce(each.value.warmer_function, var.warmer_function), null) 157 | server_function = try(coalesce(each.value.server_function, var.server_function), null) 158 | image_optimisation_function = try(coalesce(each.value.image_optimisation_function, var.image_optimisation_function), null) 159 | revalidation_function = try(coalesce(each.value.revalidation_function, var.revalidation_function), null) 160 | additional_server_functions = try(coalesce(each.value.additional_server_functions, var.additional_server_functions), null) 161 | edge_functions = try(coalesce(each.value.edge_functions, var.edge_functions), null) 162 | 163 | behaviours = try(coalesce(each.value.behaviours, var.behaviours), null) 164 | tag_mapping_db = try(coalesce(each.value.tag_mapping_db, var.tag_mapping_db), null) 165 | 166 | website_bucket = var.deployment != "SHARED_DISTRIBUTION_AND_BUCKET" ? merge(try(coalesce(each.value.website_bucket, var.website_bucket), {}), { deployment = "CREATE", create_bucket_policy = var.deployment == "INDEPENDENT_ZONES" }) : { deployment = "NONE", arn = one(aws_s3_bucket.shared_bucket[*].arn), name = one(aws_s3_bucket.shared_bucket[*].id), region = data.aws_region.current.name, domain_name = one(aws_s3_bucket.shared_bucket[*].bucket_regional_domain_name) } 167 | distribution = var.deployment == "INDEPENDENT_ZONES" ? try(coalesce(each.value.distribution, var.distribution), {}) : { deployment = "NONE", enabled = null, ipv6_enabled = null, http_version = null, price_class = null, geo_restrictions = null, x_forwarded_host_function = null, auth_function = null, lambda_url_oac = null, cache_policy = null, response_headers = null } 168 | waf = try(coalesce(each.value.waf, var.waf), null) 169 | domain_config = try(coalesce(each.value.domain_config, var.domain_config), null) 170 | continuous_deployment = coalesce(each.value.continuous_deployment, var.continuous_deployment) 171 | custom_error_responses = var.deployment == "INDEPENDENT_ZONES" ? try(coalesce(each.value.custom_error_responses, var.custom_error_responses), []) : each.value.root == true ? var.custom_error_responses : [] 172 | 173 | scripts = var.scripts 174 | 175 | providers = { 176 | aws.global = aws.global 177 | aws.dns = aws.dns 178 | aws.iam = aws.iam 179 | aws.server_function = aws.server_function 180 | } 181 | } 182 | 183 | # Moved to the multi-zone to prevent a cyclic dependency 184 | 185 | resource "aws_lambda_permission" "function_url_permission" { 186 | for_each = local.function_url_permission_details 187 | 188 | action = "lambda:InvokeFunctionUrl" 189 | function_name = each.value.function_name 190 | principal = "cloudfront.amazonaws.com" 191 | source_arn = each.value.distribution_name == "production" ? one(module.public_resources[*].arn) : one(module.public_resources[*].staging_arn) 192 | qualifier = each.value.alias 193 | function_url_auth_type = "AWS_IAM" 194 | } 195 | 196 | resource "aws_s3_bucket_policy" "shared_distribution_bucket_policy" { 197 | for_each = var.deployment == "SHARED_DISTRIBUTION" ? { for zone in local.zones : zone.name => zone } : {} 198 | bucket = module.website_zone[each.key].bucket_name 199 | policy = jsonencode({ 200 | "Version" : "2012-10-17", 201 | "Statement" : [ 202 | { 203 | "Principal" : { 204 | "Service" : "cloudfront.amazonaws.com" 205 | }, 206 | "Action" : [ 207 | "s3:GetObject" 208 | ], 209 | "Resource" : "${module.website_zone[each.key].bucket_arn}/*", 210 | "Effect" : "Allow", 211 | "Condition" : { 212 | "StringEquals" : { 213 | "AWS:SourceArn" : compact([try(one(module.public_resources[*].arn), null), try(one(module.public_resources[*].staging_arn), null)]) 214 | } 215 | } 216 | } 217 | ] 218 | }) 219 | } 220 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-multi-zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cloudfront_url" { 2 | description = "The URL for the root cloudfront distribution" 3 | value = local.use_shared_distribution ? one(module.public_resources[*].url) : module.website_zone[local.root_zone_name].cloudfront_url 4 | } 5 | 6 | output "cloudfront_distribution_id" { 7 | description = "The ID for the root cloudfront distribution" 8 | value = local.use_shared_distribution ? one(module.public_resources[*].id) : module.website_zone[local.root_zone_name].cloudfront_distribution_id 9 | } 10 | 11 | output "cloudfront_staging_distribution_id" { 12 | description = "The ID for the root cloudfront staging distribution" 13 | value = local.use_shared_distribution ? one(module.public_resources[*].staging_id) : module.website_zone[local.root_zone_name].cloudfront_staging_distribution_id 14 | } 15 | 16 | output "alternate_domain_names" { 17 | description = "Extra CNAMEs (alternate domain names) associated with the cloudfront distribution" 18 | value = local.use_shared_distribution ? one(module.public_resources[*].aliases) : module.website_zone[local.root_zone_name].alternate_domain_names 19 | } 20 | 21 | output "zones" { 22 | description = "Configuration details of the zones" 23 | value = [for zone in local.zones : { 24 | name = zone.name 25 | root = zone.root 26 | path = zone.path 27 | cloudfront_url = module.website_zone[zone.name].cloudfront_url 28 | cloudfront_distribution_id = module.website_zone[zone.name].cloudfront_distribution_id 29 | cloudfront_staging_distribution_id = module.website_zone[zone.name].cloudfront_staging_distribution_id 30 | alternate_domain_names = module.website_zone[zone.name].alternate_domain_names 31 | bucket_name = module.website_zone[zone.name].bucket_name 32 | } 33 | ] 34 | } 35 | 36 | output "response_headers_policy_id" { 37 | description = "The ID of the response header policy" 38 | value = one(module.public_resources[*].response_headers_policy_id) 39 | } 40 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-multi-zone/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | archive = { 6 | source = "hashicorp/archive" 7 | version = ">= 2.3.0" 8 | } 9 | aws = { 10 | source = "hashicorp/aws" 11 | version = ">= 5.46.0" 12 | configuration_aliases = [aws.server_function, aws.iam, aws.dns, aws.global] 13 | } 14 | local = { 15 | source = "hashicorp/local" 16 | version = ">= 2.4.0" 17 | } 18 | terraform = { 19 | source = "terraform.io/builtin/terraform" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/code/auth/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * adapted from https://github.com/aws-samples/aws-lambda-function-url-secured/blob/main/src/functions/auth/auth.js to use v3 of the aws js sdk 3 | * blog https://aws.amazon.com/blogs/compute/protecting-an-aws-lambda-function-url-with-amazon-cloudfront-and-lambdaedge/ 4 | * 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | */ 8 | const { createHash, createHmac } = require('crypto'); 9 | const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 10 | const { HttpRequest } = require("@aws-sdk/protocol-http"); 11 | const { SignatureV4 } = require("@aws-sdk/signature-v4"); 12 | 13 | /** 14 | * Credit to msaphire on this solution to workaround for the @aws-crypto/sha256-js library not being included https://github.com/aws/aws-sdk-js-v3/issues/3590#issuecomment-1531763656 15 | */ 16 | class Sha256 { 17 | hash; 18 | 19 | constructor(secret) { 20 | this.hash = secret ? createHmac('sha256', secret) : createHash('sha256'); 21 | } 22 | update(array) { 23 | this.hash.update(array); 24 | } 25 | digest() { 26 | const buffer = this.hash.digest(); 27 | return Promise.resolve(new Uint8Array(buffer.buffer)); 28 | } 29 | } 30 | 31 | exports.handler = async (event) => { 32 | const request = event.Records[0].cf.request; 33 | const headers = request.headers; 34 | 35 | delete headers["x-forwarded-for"]; 36 | 37 | if (!request.origin.custom) { 38 | throw new Error(`Unexpected origin type. Expected 'custom'. Got: ${JSON.stringify(request.origin)}`); 39 | } 40 | 41 | const host = request.headers["host"][0].value; 42 | const region = host.split(".")[2]; 43 | 44 | // Parse the query string into an object for proper handling by SignatureV4 45 | const queryParams = {}; 46 | if (request.querystring) { 47 | const searchParams = new URLSearchParams(request.querystring); 48 | for (const [key, value] of searchParams.entries()) { 49 | if (queryParams[key] === undefined) { 50 | // First occurrence of this key 51 | queryParams[key] = value; 52 | } else if (Array.isArray(queryParams[key])) { 53 | // Already an array, just push 54 | queryParams[key].push(value); 55 | } else { 56 | // Second occurrence, convert to array 57 | queryParams[key] = [queryParams[key], value]; 58 | } 59 | } 60 | } 61 | 62 | const req = new HttpRequest({ 63 | method: request.method, 64 | protocol: "https:", 65 | hostname: host, 66 | path: request.uri, 67 | query: queryParams, 68 | headers: Object.values(headers).map(headers => headers[0]).reduce((previousValue, newValue) => ({ ...previousValue, ...{ [newValue.key]: newValue.value } }), {}), 69 | body: request.body ? Buffer.from(request.body.data, request.body.encoding) : undefined, 70 | }); 71 | 72 | const signer = new SignatureV4({ credentials: defaultProvider(), region, service: 'lambda', sha256: Sha256 }); 73 | const signedRequest = await signer.sign(req); 74 | 75 | for (const [key, value] of Object.entries(signedRequest.headers)) { 76 | request.headers[key.toLowerCase()] = [{ key, value }]; 77 | } 78 | 79 | return request; 80 | }; -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/code/cloudfrontFunctionOpenNextV3.js: -------------------------------------------------------------------------------- 1 | // Adapted from the sst CDK construct https://github.com/sst/sst/blob/1e9a23a61ecdaf96651db9be8af45089eca13fd3/packages/sst/src/constructs/NextjsSite.ts 2 | // Copyright (c) 2020 SST 3 | // SPDX-License-Identifier: MIT 4 | 5 | function handler(event) { 6 | var request = event.request; 7 | 8 | request.headers["x-forwarded-host"] = request.headers.host; 9 | 10 | function getHeader(key) { 11 | var header = request.headers[key]; 12 | if (header) { 13 | if (header.multiValue) { 14 | return header.multiValue.map((header) => header.value).join(","); 15 | } 16 | if (header.value) { 17 | return header.value; 18 | } 19 | } 20 | return ""; 21 | } 22 | 23 | var cacheKey = ""; 24 | if (request.uri.includes("/_next/image")) { 25 | cacheKey = getHeader("accept"); 26 | } else { 27 | cacheKey = 28 | getHeader("rsc") + 29 | getHeader("next-router-prefetch") + 30 | getHeader("next-router-state-tree") + 31 | getHeader("next-url") + 32 | getHeader("x-prerender-revalidate"); 33 | } 34 | if (request.cookies["__prerender_bypass"]) { 35 | cacheKey += request.cookies["__prerender_bypass"] 36 | ? request.cookies["__prerender_bypass"].value 37 | : ""; 38 | } 39 | 40 | var crypto = require("crypto"); 41 | var hashedKey = crypto.createHash("md5").update(cacheKey).digest("hex"); 42 | request.headers["x-open-next-cache-key"] = { value: hashedKey }; 43 | 44 | if (request.headers["cloudfront-viewer-city"]) { 45 | request.headers["x-open-next-city"] = 46 | request.headers["cloudfront-viewer-city"]; 47 | } 48 | if (request.headers["cloudfront-viewer-country"]) { 49 | request.headers["x-open-next-country"] = 50 | request.headers["cloudfront-viewer-country"]; 51 | } 52 | if (request.headers["cloudfront-viewer-region"]) { 53 | request.headers["x-open-next-region"] = 54 | request.headers["cloudfront-viewer-region"]; 55 | } 56 | if (request.headers["cloudfront-viewer-latitude"]) { 57 | request.headers["x-open-next-latitude"] = 58 | request.headers["cloudfront-viewer-latitude"]; 59 | } 60 | if (request.headers["cloudfront-viewer-longitude"]) { 61 | request.headers["x-open-next-longitude"] = 62 | request.headers["cloudfront-viewer-longitude"]; 63 | } 64 | 65 | return request; 66 | } 67 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/code/xForwardedHost.js: -------------------------------------------------------------------------------- 1 | function handler(event) { 2 | var request = event.request; 3 | request.headers["x-forwarded-host"] = request.headers.host; 4 | return request; 5 | } 6 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | # Route 53 4 | 5 | data "aws_route53_zone" "hosted_zone" { 6 | for_each = try(var.domain_config.create_route53_entries, false) == true ? { for hosted_zone in var.domain_config.hosted_zones : join("-", compact([hosted_zone.name, hosted_zone.private_zone])) => hosted_zone if hosted_zone.id == null } : {} 7 | name = each.value.name 8 | private_zone = each.value.private_zone 9 | 10 | provider = aws.dns 11 | } 12 | 13 | # CloudFront 14 | 15 | data "aws_cloudfront_origin_request_policy" "all_viewer_except_host_header" { 16 | name = "Managed-AllViewerExceptHostHeader" 17 | } 18 | 19 | data "aws_cloudfront_cache_policy" "caching_optimized" { 20 | name = "Managed-CachingOptimized" 21 | } 22 | 23 | # Zip Archives 24 | 25 | data "archive_file" "auth_function" { 26 | count = local.should_create_auth_lambda && try(var.auth_function.function_code.zip, null) == null && try(var.auth_function.function_code.s3, null) == null ? 1 : 0 27 | type = "zip" 28 | output_path = "${path.module}/code/auth.zip" 29 | source_dir = "${path.module}/code/auth" 30 | } 31 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/outputs.tf: -------------------------------------------------------------------------------- 1 | output "url" { 2 | description = "The URL for the cloudfront distribution" 3 | value = var.continuous_deployment.use ? "https://${one(aws_cloudfront_distribution.production_distribution[*].domain_name)}" : "https://${one(aws_cloudfront_distribution.website_distribution[*].domain_name)}" 4 | } 5 | 6 | output "arn" { 7 | description = "The arn for the cloudfront distribution" 8 | value = var.continuous_deployment.use ? one(aws_cloudfront_distribution.production_distribution[*].arn) : one(aws_cloudfront_distribution.website_distribution[*].arn) 9 | } 10 | 11 | output "id" { 12 | description = "The ID for the cloudfront distribution" 13 | value = var.continuous_deployment.use ? one(aws_cloudfront_distribution.production_distribution[*].id) : one(aws_cloudfront_distribution.website_distribution[*].id) 14 | } 15 | 16 | output "etag" { 17 | description = "The etag for the cloudfront distribution" 18 | value = var.continuous_deployment.use ? coalesce(try(one(terraform_data.promote_distribution[*].output.etag), null), one(aws_cloudfront_distribution.production_distribution[*].etag)) : one(aws_cloudfront_distribution.website_distribution[*].etag) 19 | } 20 | 21 | output "distributions_provisioned" { 22 | description = "The CloudFront distributions that are provisioned" 23 | value = local.includes_staging_distribution ? ["production", "staging"] : ["production"] 24 | } 25 | 26 | output "staging_etag" { 27 | description = "The etag for the cloudfront staging distribution" 28 | value = try(one(aws_cloudfront_distribution.staging_distribution[*].etag), null) 29 | } 30 | 31 | output "staging_arn" { 32 | description = "The arn for the cloudfront staging distribution" 33 | value = try(one(aws_cloudfront_distribution.staging_distribution[*].arn), null) 34 | } 35 | 36 | output "staging_id" { 37 | description = "The ID for the cloudfront staging distribution" 38 | value = try(one(aws_cloudfront_distribution.staging_distribution[*].id), null) 39 | } 40 | 41 | output "aliases" { 42 | description = "Extra CNAMEs (alternate domain names) associated with the distribution" 43 | value = local.aliases 44 | } 45 | 46 | output "distribution" { 47 | description = "The configuration of the cloudfront distribution. These are added to aid with testing" 48 | value = var.continuous_deployment.use ? one(aws_cloudfront_distribution.production_distribution[*]) : one(aws_cloudfront_distribution.website_distribution[*]) 49 | } 50 | 51 | output "staging_distribution" { 52 | description = "The configuration of the cloudfront distribution. These are added to aid with testing" 53 | value = try(one(aws_cloudfront_distribution.staging_distribution[*]), null) 54 | } 55 | 56 | output "waf" { 57 | description = "The configuration of the WAF ACL. These are added to aid with testing" 58 | value = try(one(aws_wafv2_web_acl.distribution_waf[*]), null) 59 | } 60 | 61 | output "cache_policy_id" { 62 | description = "The default cache policy ID to associate with the distribution" 63 | value = local.cache_policy_id 64 | } 65 | 66 | output "response_headers_policy_id" { 67 | value = local.response_headers_policy_id 68 | } 69 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/scripts/invalidate-cloudfront.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $CDN_ID ]; then 17 | exitcode=1 18 | echo "Error: CDN ID was not supplied" 19 | fi 20 | 21 | if [ $exitcode -ne 0 ]; then 22 | exit $exitcode 23 | fi 24 | 25 | ## Script 26 | 27 | INVALIDATION_ID=$(aws cloudfront create-invalidation --distribution-id $CDN_ID --paths "/*" --query 'Invalidation.Id' --output text) 28 | DESIRED_STATE="Completed" 29 | INTERVAL=30 30 | 31 | while true; do 32 | STATUS=$(aws cloudfront get-invalidation --id $INVALIDATION_ID --distribution-id $CDN_ID --query 'Invalidation.Status' --output text) 33 | 34 | if [ "$STATUS" == "$DESIRED_STATE" ]; then 35 | echo "Distribution $CDN_ID reached state $DESIRED_STATE." 36 | break 37 | else 38 | echo "Current status of $CDN_ID is $STATUS. Waiting for $DESIRED_STATE..." 39 | fi 40 | 41 | sleep $INTERVAL 42 | done 43 | 44 | ETAG=$(aws cloudfront get-distribution --id $CDN_ID --query 'ETag' --output text) 45 | echo "{\"etag\": $ETAG}" -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/scripts/promote-distribution.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $CDN_PRODUCTION_ID ]; then 17 | exitcode=1 18 | echo "Error: production CDN ID was not supplied" 19 | fi 20 | 21 | if [ -z $CDN_STAGING_ID ]; then 22 | exitcode=1 23 | echo "Error: staging CDN ID was not supplied" 24 | fi 25 | 26 | if [ -z $CDN_PRODUCTION_ETAG ]; then 27 | exitcode=1 28 | echo "Error: production CDN ETag was not supplied" 29 | fi 30 | 31 | if [ -z $CDN_STAGING_ETAG ]; then 32 | exitcode=1 33 | echo "Error: staging CDN ETag was not supplied" 34 | fi 35 | 36 | if [ $exitcode -ne 0 ]; then 37 | exit $exitcode 38 | fi 39 | 40 | ## Script 41 | 42 | aws cloudfront update-distribution-with-staging-config --id $CDN_PRODUCTION_ID --staging-distribution-id $CDN_STAGING_ID --if-match "$CDN_PRODUCTION_ETAG,$CDN_STAGING_ETAG" > /dev/null 43 | 44 | DESIRED_STATE="Deployed" 45 | INTERVAL=60 46 | 47 | while true; do 48 | STATUS=$(aws cloudfront get-distribution --id $CDN_PRODUCTION_ID --query 'Distribution.Status' --output text) 49 | 50 | if [ "$STATUS" == "$DESIRED_STATE" ]; then 51 | echo "Distribution $CDN_PRODUCTION_ID reached state $DESIRED_STATE." 52 | break 53 | else 54 | echo "Current status of $CDN_PRODUCTION_ID is $STATUS. Waiting for $DESIRED_STATE..." 55 | fi 56 | 57 | sleep $INTERVAL 58 | done 59 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/scripts/remove-continuous-deployment-policy-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $CDN_PRODUCTION_ID ]; then 17 | exitcode=1 18 | echo "Error: production CDN ID was not supplied" 19 | fi 20 | 21 | if [ $exitcode -ne 0 ]; then 22 | exit $exitcode 23 | fi 24 | 25 | ## Script 26 | 27 | ETAG=$(aws cloudfront get-distribution --id $CDN_PRODUCTION_ID --query 'ETag' --output text) 28 | 29 | aws cloudfront get-distribution --id $CDN_PRODUCTION_ID | \ 30 | jq '.Distribution.DistributionConfig.ContinuousDeploymentPolicyId=""' | \ 31 | jq .Distribution.DistributionConfig > config.json 32 | aws cloudfront update-distribution --id $CDN_PRODUCTION_ID --distribution-config "file://config.json" --if-match $ETAG > /dev/null 33 | 34 | rm config.json 35 | 36 | DESIRED_STATE="Deployed" 37 | INTERVAL=60 38 | 39 | while true; do 40 | STATUS=$(aws cloudfront get-distribution --id $CDN_PRODUCTION_ID --query 'Distribution.Status' --output text) 41 | 42 | if [ "$STATUS" == "$DESIRED_STATE" ]; then 43 | echo "Distribution $CDN_PRODUCTION_ID reached state $DESIRED_STATE." 44 | break 45 | else 46 | echo "Current status of $CDN_PRODUCTION_ID is $STATUS. Waiting for $DESIRED_STATE..." 47 | fi 48 | 49 | sleep $INTERVAL 50 | done 51 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-public-resources/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | archive = { 6 | source = "hashicorp/archive" 7 | version = ">= 2.3.0" 8 | } 9 | aws = { 10 | source = "hashicorp/aws" 11 | version = ">= 5.46.0" 12 | configuration_aliases = [aws.iam, aws.dns] 13 | } 14 | terraform = { 15 | source = "terraform.io/builtin/terraform" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | assets_folder = "${var.folder_path}/assets" 3 | asset_key_prefix = join("/", compact([var.s3_path_prefix, "assets", var.zone_suffix])) 4 | origin_asset_path = join("/", compact([var.s3_path_prefix, "assets"])) 5 | assets = [for file in toset([for file in fileset(local.assets_folder, "**") : file if var.s3_exclusion_regex != null ? length(regexall(var.s3_exclusion_regex, file)) == 0 : true]) : { 6 | file = file 7 | key = "${local.asset_key_prefix}/${file}" 8 | source = "${local.assets_folder}/${file}" 9 | path_parts = split("/", file) 10 | cache_control = length(regexall(var.cache_control_immutable_assets_regex, file)) > 0 ? "public,max-age=31536000,immutable" : "public,max-age=0,s-maxage=31536000,must-revalidate" 11 | md5 = filemd5("${local.assets_folder}/${file}") 12 | }] 13 | 14 | cache_folder = "${var.folder_path}/cache" 15 | cache_key_prefix = join("/", compact([var.s3_path_prefix, "cache", var.zone_suffix])) 16 | cache_assets = [for file in toset([for file in fileset(local.cache_folder, "**") : file if var.s3_exclusion_regex != null ? length(regexall(var.s3_exclusion_regex, file)) == 0 : true]) : { 17 | file = file 18 | key = "${local.cache_key_prefix}/${file}" 19 | source = "${local.cache_folder}/${file}" 20 | md5 = filemd5("${local.cache_folder}/${file}") 21 | }] 22 | 23 | additional_files = [for file in var.additional_files : { 24 | file = file.name 25 | key = join("/", [coalesce(file.s3_path_prefix, var.s3_path_prefix), "custom", file.path_prefix, file.name]) 26 | source = file.source 27 | md5 = filemd5(file.source) 28 | }] 29 | } 30 | 31 | resource "terraform_data" "remove_folder" { 32 | count = var.remove_folder ? 1 : 0 33 | 34 | triggers_replace = [var.s3_path_prefix, file("${local.assets_folder}/BUILD_ID")] 35 | 36 | provisioner "local-exec" { 37 | command = "${coalesce(try(var.scripts.delete_folder_script.interpreter, var.scripts.interpreter, null), "/bin/bash")} ${try(var.scripts.delete_folder_script.path, "${path.module}/scripts/delete-folder.sh")}" 38 | 39 | environment = merge({ 40 | "BUCKET_NAME" = var.bucket_name 41 | "FOLDER" = local.asset_key_prefix 42 | }, try(var.scripts.additional_environment_variables, {}), try(var.scripts.delete_folder_script.additional_environment_variables, {})) 43 | } 44 | } 45 | 46 | resource "terraform_data" "file_sync" { 47 | for_each = merge({ 48 | for asset in local.assets : asset.file => asset 49 | }, { 50 | for cache in local.cache_assets : cache.file => cache 51 | }, { 52 | for additional_file in local.additional_files : additional_file.file => additional_file 53 | }) 54 | 55 | triggers_replace = [var.zone_suffix, var.s3_path_prefix, each.value.md5] 56 | 57 | provisioner "local-exec" { 58 | command = "${coalesce(try(var.scripts.file_sync_script.interpreter, var.scripts.interpreter, null), "/bin/bash")} ${try(var.scripts.file_sync_script.path, "${path.module}/scripts/sync-file.sh")}" 59 | 60 | environment = merge({ 61 | "BUCKET_NAME" = var.bucket_name 62 | "KEY" = each.value.key 63 | "SOURCE" = each.value.source 64 | "CACHE_CONTROL" = try(each.value.cache_control, null) 65 | "CONTENT_TYPE" = lookup(var.content_types.mapping, reverse(split(".", each.value.file))[0], var.content_types.default) 66 | }, try(var.scripts.additional_environment_variables, {}), try(var.scripts.file_sync_script.additional_environment_variables, {})) 67 | } 68 | 69 | depends_on = [terraform_data.remove_folder] 70 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cloudfront_asset_mappings" { 2 | description = "Assets to be added as behaviours in CloudFront" 3 | value = sort(distinct([for file in local.assets : "/${file.path_parts[0]}${length(file.path_parts) > 1 ? "/*" : ""}" if !contains(["_next", "BUILD_ID"], file.path_parts[0])])) 4 | } 5 | 6 | output "file_hashes" { 7 | description = "List of md5 hashes for each file uploaded" 8 | value = concat([for asset in local.assets : asset.md5], [for cache in local.cache_assets : cache.md5], [for additional_file in local.additional_files : additional_file.md5]) 9 | } 10 | 11 | output "asset_key_prefix" { 12 | description = "The prefix for assets in the bucket" 13 | value = local.asset_key_prefix 14 | } 15 | 16 | output "origin_asset_path" { 17 | description = "The origin path for assets in the bucket" 18 | value = local.origin_asset_path 19 | } 20 | 21 | output "cache_key_prefix" { 22 | description = "The prefix for assets in the bucket" 23 | value = local.cache_key_prefix 24 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/scripts/delete-folder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $BUCKET_NAME ]; then 17 | exitcode=1 18 | echo "Error: bucket name was not supplied" 19 | fi 20 | 21 | if [ -z $FOLDER ]; then 22 | exitcode=1 23 | echo "Error: folder was not supplied" 24 | fi 25 | 26 | if [ $exitcode -ne 0 ]; then 27 | exit $exitcode 28 | fi 29 | 30 | ## Script 31 | 32 | aws s3 rm s3://$BUCKET_NAME/$FOLDER/* -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/scripts/sync-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $SOURCE ]; then 17 | exitcode=1 18 | echo "Error: source was not supplied" 19 | fi 20 | 21 | if [ -z $BUCKET_NAME ]; then 22 | exitcode=1 23 | echo "Error: bucket name was not supplied" 24 | fi 25 | 26 | if [ -z $KEY ]; then 27 | exitcode=1 28 | echo "Error: key was not supplied" 29 | fi 30 | 31 | if [[ -z $CONTENT_TYPE ]]; then 32 | exitcode=1 33 | echo "Error: content type was not supplied" 34 | fi 35 | 36 | if [ $exitcode -ne 0 ]; then 37 | exit $exitcode 38 | fi 39 | 40 | ## Script 41 | 42 | if [ -z "$CACHE_CONTROL" ] 43 | then 44 | aws s3 cp "$SOURCE" "s3://$BUCKET_NAME/$KEY" --cache-control "$CACHE_CONTROL" --content-type "$CONTENT_TYPE" 45 | else 46 | aws s3 cp "$SOURCE" "s3://$BUCKET_NAME/$KEY" --content-type "$CONTENT_TYPE" 47 | fi 48 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/variables.tf: -------------------------------------------------------------------------------- 1 | variable "folder_path" { 2 | description = "The path to the open next artifacts" 3 | type = string 4 | } 5 | 6 | variable "zone_suffix" { 7 | description = "An optional zone suffix to add to the assets and cache folder to allow files to be loaded correctly" 8 | type = string 9 | default = null 10 | } 11 | 12 | variable "s3_exclusion_regex" { 13 | description = "A regex of files to exclude from the s3 copy" 14 | type = string 15 | default = null 16 | } 17 | 18 | variable "cache_control_immutable_assets_regex" { 19 | description = "Regex to set public,max-age=31536000,immutable on immutable resources" 20 | type = string 21 | default = "^.*(\\.next)$" 22 | } 23 | 24 | variable "force_destroy" { 25 | description = "Whether to force destroy the bucket and associated resources" 26 | type = bool 27 | default = false 28 | } 29 | 30 | variable "remove_folder" { 31 | description = "Whether to delete the folder before uploading assets" 32 | type = bool 33 | } 34 | 35 | variable "s3_path_prefix" { 36 | description = "The path to add all the files under" 37 | type = string 38 | } 39 | 40 | variable "content_types" { 41 | description = "The MIME type mapping and default for artefacts generated by Open Next" 42 | type = object({ 43 | mapping = optional(map(string), { 44 | "svg" = "image/svg+xml", 45 | "js" = "application/javascript", 46 | "css" = "text/css", 47 | "html" = "text/html", 48 | }) 49 | default = optional(string, "binary/octet-stream") 50 | }) 51 | default = {} 52 | } 53 | 54 | variable "bucket_name" { 55 | description = "The name of the bucket to upload files to" 56 | type = string 57 | } 58 | 59 | variable "additional_files" { 60 | description = "Allow additional files to be uploaded to the bucket. All files will be added as per the s3_path_prefix which can also be overridden" 61 | type = list(object({ 62 | name = string 63 | source = string 64 | path_prefix = string 65 | s3_path_prefix = optional(string) 66 | })) 67 | default = [] 68 | } 69 | 70 | variable "scripts" { 71 | description = "Modify default script behaviours" 72 | type = object({ 73 | interpreter = optional(string) 74 | additional_environment_variables = optional(map(string)) 75 | file_sync_script = optional(object({ 76 | interpreter = optional(string) 77 | path = optional(string) 78 | additional_environment_variables = optional(map(string)) 79 | })) 80 | delete_folder_script = optional(object({ 81 | interpreter = optional(string) 82 | path = optional(string) 83 | additional_environment_variables = optional(map(string)) 84 | })) 85 | }) 86 | default = {} 87 | } -------------------------------------------------------------------------------- /modules/tf-aws-open-next-s3-assets/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | terraform = { 6 | source = "terraform.io/builtin/terraform" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-zone/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | # Zip Archives 4 | 5 | data "archive_file" "server_function" { 6 | count = try(var.server_function.function_code.zip, null) == null && try(var.server_function.function_code.s3, null) == null ? 1 : 0 7 | type = "zip" 8 | output_path = "${local.open_next_default_server}.zip" 9 | source_dir = local.open_next_default_server 10 | 11 | lifecycle { 12 | precondition { 13 | condition = local.open_next_versions.v2 == true || (local.open_next_versions.v3 == true && contains(["aws-lambda", "aws-lambda-streaming"], lookup(local.default_server_function, "wrapper", ""))) 14 | error_message = "This module only supports hosting default server function using lambda" 15 | } 16 | } 17 | 18 | depends_on = [local_file.lambda_at_edge_modifications] 19 | } 20 | 21 | // This is resource is only supported for open next v3 22 | data "archive_file" "additional_server_function" { 23 | for_each = { for name, additional_server_function in local.additional_server_functions : name => additional_server_function if try(var.additional_server_functions.function_overrides[name].function_code.zip, null) == null && try(var.additional_server_functions.function_overrides[name].function_code.s3, null) == null } 24 | type = "zip" 25 | output_path = "${local.open_next_path_without_folder}/${each.value.bundle}.zip" 26 | source_dir = "${local.open_next_path_without_folder}/${each.value.bundle}" 27 | 28 | lifecycle { 29 | precondition { 30 | condition = local.open_next_versions.v3 == true && contains(["aws-lambda", "aws-lambda-streaming"], each.value.wrapper) 31 | error_message = "This module only supports hosting server functions using lambda" 32 | } 33 | } 34 | } 35 | 36 | // This is resource is only supported for open next v3 37 | data "archive_file" "edge_function" { 38 | for_each = { for name, edge_function in local.edge_functions : name => edge_function if try(var.edge_functions.function_overrides[name].function_code.zip, null) == null && try(var.edge_functions.function_overrides[name].function_code.s3, null) == null } 39 | type = "zip" 40 | output_path = "${local.open_next_path_without_folder}/${each.value.bundle}.zip" 41 | source_dir = "${local.open_next_path_without_folder}/${each.value.bundle}" 42 | 43 | lifecycle { 44 | precondition { 45 | condition = local.open_next_versions.v3 == true && contains(["aws-lambda", "aws-lambda-streaming"], each.value.wrapper) 46 | error_message = "This module only supports hosting edge functions using lambda@edge" 47 | } 48 | } 49 | 50 | depends_on = [local_file.edge_functions_modifications] 51 | } 52 | 53 | data "archive_file" "revalidation_function" { 54 | count = try(var.revalidation_function.function_code.zip, null) == null && try(var.revalidation_function.function_code.s3, null) == null ? 1 : 0 55 | type = "zip" 56 | output_path = "${local.open_next_revalidation}.zip" 57 | source_dir = local.open_next_revalidation 58 | } 59 | 60 | data "archive_file" "image_optimisation_function" { 61 | count = try(var.image_optimisation_function.function_code.zip, null) == null && try(var.image_optimisation_function.function_code.s3, null) == null ? 1 : 0 62 | type = "zip" 63 | output_path = "${local.open_next_image_optimisation}.zip" 64 | source_dir = local.open_next_image_optimisation 65 | 66 | lifecycle { 67 | precondition { 68 | condition = local.open_next_versions.v2 == true || (local.open_next_versions.v3 == true && contains(["aws-lambda", "aws-lambda-streaming"], lookup(local.image_optimisation_function, "wrapper", ""))) 69 | error_message = "This module only supports hosting image optimisation resource using lambda" 70 | } 71 | } 72 | } 73 | 74 | data "archive_file" "warmer_function" { 75 | count = try(var.warmer_function.function_code.zip, null) == null && try(var.warmer_function.function_code.s3, null) == null ? 1 : 0 76 | type = "zip" 77 | output_path = "${local.open_next_warmer}.zip" 78 | source_dir = local.open_next_warmer 79 | } 80 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "alias_details" { 2 | description = "The alias config" 3 | value = local.aliases 4 | } 5 | 6 | output "bucket_name" { 7 | description = "The name of the s3 bucket" 8 | value = local.should_create_website_bucket ? one(aws_s3_bucket.bucket[*].id) : null 9 | } 10 | 11 | output "bucket_arn" { 12 | description = "The ARN of the s3 bucket" 13 | value = local.should_create_website_bucket ? one(aws_s3_bucket.bucket[*].arn) : null 14 | } 15 | 16 | output "zone_config" { 17 | description = "The zone config" 18 | value = local.zone 19 | } 20 | 21 | output "behaviours" { 22 | description = "The behaviours for the zone" 23 | value = local.zone_behaviours 24 | } 25 | 26 | output "custom_error_responses" { 27 | description = "The custom error responses for the zone" 28 | value = local.custom_error_responses 29 | } 30 | 31 | output "cloudfront_url" { 32 | description = "The URL for the cloudfront distribution" 33 | value = local.create_distribution ? one(module.public_resources[*].url) : null 34 | } 35 | 36 | output "cloudfront_distribution_id" { 37 | description = "The ID for the cloudfront distribution" 38 | value = local.create_distribution ? one(module.public_resources[*].id) : null 39 | } 40 | 41 | output "cloudfront_staging_distribution_id" { 42 | description = "The ID for the cloudfront staging distribution" 43 | value = local.create_distribution ? one(module.public_resources[*].staging_id) : null 44 | } 45 | 46 | output "alternate_domain_names" { 47 | description = "Extra CNAMEs (alternate domain names) associated with the cloudfront distribution" 48 | value = local.create_distribution ? one(module.public_resources[*].aliases) : null 49 | } 50 | 51 | output "response_headers_policy_id" { 52 | description = "The ID of the response header policy" 53 | value = one(module.public_resources[*].response_headers_policy_id) 54 | } 55 | -------------------------------------------------------------------------------- /modules/tf-aws-open-next-zone/scripts/save-item-to-dynamo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $TABLE_NAME ]; then 17 | exitcode=1 18 | echo "Error: table name was not supplied" 19 | fi 20 | 21 | if [ -z $ITEM ]; then 22 | exitcode=1 23 | echo "Error: item was not supplied" 24 | fi 25 | 26 | if [ $exitcode -ne 0 ]; then 27 | exit $exitcode 28 | fi 29 | 30 | ## Script 31 | 32 | aws dynamodb put-item --table-name $TABLE_NAME --item $ITEM -------------------------------------------------------------------------------- /modules/tf-aws-open-next-zone/scripts/update-parameter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Setup 4 | 5 | set -e 6 | 7 | ## Validation 8 | 9 | exitcode=0 10 | 11 | if [ ! -x "$(command -v aws)" ]; then 12 | exitcode=1 13 | echo "Error: AWS CLI not found. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html to install the CLI" 14 | fi 15 | 16 | if [ -z $PARAMETER_NAME ]; then 17 | exitcode=1 18 | echo "Error: parameter name was not supplied" 19 | fi 20 | 21 | if [ -z $VALUE ]; then 22 | exitcode=1 23 | echo "Error: value was not supplied" 24 | fi 25 | 26 | if [ $exitcode -ne 0 ]; then 27 | exit $exitcode 28 | fi 29 | 30 | ## Script 31 | 32 | aws ssm put-parameter --name $PARAMETER_NAME --value $VALUE --overwrite -------------------------------------------------------------------------------- /modules/tf-aws-open-next-zone/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | archive = { 6 | source = "hashicorp/archive" 7 | version = ">= 2.3.0" 8 | } 9 | aws = { 10 | source = "hashicorp/aws" 11 | version = ">= 5.46.0" 12 | configuration_aliases = [aws.server_function, aws.iam, aws.dns, aws.global] 13 | } 14 | local = { 15 | source = "hashicorp/local" 16 | version = ">= 2.4.0" 17 | } 18 | terraform = { 19 | source = "terraform.io/builtin/terraform" 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------