├── LICENSE ├── README.md ├── aws-git-backed-static-website-architecture.gif ├── aws-git-backed-static-website-architecture.png ├── aws-git-backed-static-website-cloudformation.yml ├── aws-git-backed-static-website-lambda.py └── build-upload-aws-lambda-function /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 {yyyy} {name of copyright owner} 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 | 2 | # Git-backed static website powered entirely by AWS 3 | 4 | ![diagram](https://raw.githubusercontent.com/alestic/aws-git-backed-static-website/master/aws-git-backed-static-website-architecture.gif "Architecture dagram: Git-backed static website powerd by AWS") 5 | 6 | [![Launch CloudFormation stack][launchimg]][launch] 7 | 8 | [launchimg]: https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png 9 | 10 | [launch]: https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?templateURL=https:%2F%2Fs3.amazonaws.com%2Frun.alestic.com%2Fcloudformation%2Faws-git-backed-static-website-cloudformation.yml&stackName=aws-git-backed-static-website 11 | 12 | ## Blog Article 13 | 14 | Please read the following blog article for more information: 15 | 16 | [AWS Git-backed Static Website][blog] 17 | 18 | [blog]: https://alestic.com/2016/10/aws-git-backed-static-website/ 19 | 20 | ## Overview 21 | 22 | This project contains a YAML CloudFormation template that, by default, 23 | creates a CodeCommit Git repository and a static https website, along 24 | with the necessary AWS infrastructure glue so that every change to 25 | content in the Git repository is automatically deployed to the static 26 | web site. 27 | 28 | The website can serve the exact contents of the Git repository, or a 29 | static site generator plugin (e.g., Hugo) can be specified on launch 30 | to automatically generate the site content from the source in the Git 31 | repository. 32 | 33 | The required stack parameters are a domain name and an email address. 34 | 35 | The primary output values are a list of nameservers to set in your 36 | domain's registrar and a Git repository URL for adding and updating 37 | the website content. 38 | 39 | CodeCommit Git repository event notifications are sent to an SNS topic 40 | and your provided email address is initially subscribed. 41 | 42 | Access logs for the website are stored in an S3 bucket. 43 | 44 | Benefits of this architecture include: 45 | 46 | - Trivial to launch - Can use aws-cli or AWS console (click "launch" 47 | above) 48 | 49 | - Maintenance-free - Amazon is responsible for managing all the 50 | services used. 51 | 52 | - Negligible cost at substantial traffic volume - Starts off as low 53 | as $0.51 per month if running alone in a new AWS account. (Route 53 54 | has no free tier.) Your cost may vary over time and if other 55 | resources are running in your AWS account. 56 | 57 | - Scales forever - No action is needed to support more web site 58 | traffic, though the costs for network traffic and DNS lookups will 59 | start to add up to more than a penny per month. 60 | 61 | ## Create CloudFormation stack for static website 62 | 63 | This CloudFormation stack has an AWS Lambda plugin architecture that 64 | supports arbitrary static site generators. Here are some generator 65 | plugins that are currently available 66 | 67 | 1. [Identity transformation plugin][identity] - This copies the entire Git 68 | repository content to the static website with no 69 | modifications. This is currently the default plugin for the static 70 | website CloudFormation template. 71 | 72 | 2. [Subdirectory plugin][subdirectory] - This plugin is useful if your 73 | Git repository has files that should not be included as part of the 74 | static site. It publishes a specified subdirectory (e.g., "htdocs" 75 | or "public-html") as the static website, keeping the rest of your 76 | repository private. 77 | 78 | 3. [Hugo plugin][hugoplugin] - This plugin runs the popular 79 | [Hugo][hugo] static site generator. The Git repository should 80 | include all source templates, content, theme, and config. 81 | 82 | You are welcome to make your own static site generators based on one 83 | of these and pass it into the CloudFormation stack. 84 | 85 | [identity]: https://github.com/alestic/aws-lambda-codepipeline-site-generator-identity 86 | [subdirectory]: https://github.com/alestic/aws-lambda-codepipeline-site-generator-subdirectory 87 | [hugoplugin]: https://github.com/alestic/aws-lambda-codepipeline-site-generator-hugo 88 | [hugo]: https://gohugo.io/ 89 | 90 | ## Create CloudFormation stack for static website 91 | 92 | Here is the basic approach to creating the stack with CloudFormation. 93 | 94 | domain=example.com 95 | email=yourrealemail@anotherdomain.com 96 | 97 | template=aws-git-backed-static-website-cloudformation.yml 98 | stackname=${domain/./-}-$(date +%Y%m%d-%H%M%S) 99 | region=us-east-1 100 | 101 | aws cloudformation create-stack \ 102 | --region "$region" \ 103 | --stack-name "$stackname" \ 104 | --capabilities CAPABILITY_IAM \ 105 | --template-body "file://$template" \ 106 | --tags "Key=Name,Value=$stackname" \ 107 | --parameters \ 108 | "ParameterKey=DomainName,ParameterValue=$domain" \ 109 | "ParameterKey=NotificationEmail,ParameterValue=$email" 110 | echo region=$region stackname=$stackname 111 | 112 | The above defaults to the Identity transformation plugin. You can 113 | specify the Hugo static site generator plugin by adding these 114 | parameters: 115 | 116 | "ParameterKey=GeneratorLambdaFunctionS3Bucket,ParameterValue=run.alestic.com" \ 117 | "ParameterKey=GeneratorLambdaFunctionS3Key,ParameterValue=lambda/aws-lambda-site-generator-hugo.zip" 118 | 119 | Go to the email address you used above and approve the SNS topic 120 | subscription. 121 | 122 | When the stack starts up, the ACM certificate will be in "pending" 123 | status until you verify ownership of the domain. 124 | 125 | If you are using "EMAIL" as the CertificateValidationMethod, then you 126 | need to open the email sent to the address associated with your 127 | domain's registration and approve it. 128 | 129 | If you are using "DNS" as the CertificateValidationMethod, then go to 130 | the AWS Certificate Manager (ACM) console, find the two pending 131 | certificates, and click "Create record in Route 53" for both. 132 | 133 | Once the ACM certificates are verified, the CloudFront distributions 134 | will be created. This can take 20-40+ minutes to complete. 135 | 136 | ### Get the name servers for updating in the registrar 137 | 138 | hosted_zone_id=$(aws cloudformation describe-stacks \ 139 | --region "$region" \ 140 | --stack-name "$stackname" \ 141 | --output text \ 142 | --query 'Stacks[*].Outputs[?OutputKey==`HostedZoneId`].[OutputValue]') 143 | echo hosted_zone_id=$hosted_zone_id 144 | 145 | aws route53 get-hosted-zone \ 146 | --id "$hosted_zone_id" \ 147 | --output text \ 148 | --query 'DelegationSet.NameServers' 149 | 150 | Set nameservers in your domain registrar to the above. 151 | 152 | ### Get the Git clone URL 153 | 154 | git_clone_url_http=$(aws cloudformation describe-stacks \ 155 | --region "$region" \ 156 | --stack-name "$stackname" \ 157 | --output text \ 158 | --query 'Stacks[*].Outputs[?OutputKey==`GitCloneUrlHttp`].[OutputValue]') 159 | echo git_clone_url_http=$git_clone_url_http 160 | 161 | ### Use Git 162 | 163 | repository=$domain 164 | profile=$AWS_PROFILE # The correct aws-cli profile name 165 | 166 | git clone \ 167 | --config 'credential.helper=!aws codecommit --profile '$profile' --region '$region' credential-helper $@' \ 168 | --config 'credential.UseHttpPath=true' \ 169 | $git_clone_url_http 170 | 171 | cd $repository 172 | 173 | ## Using GitHub 174 | 175 | Thanks to the folks at [Elementryx][elementryx], this CloudFormation 176 | template has been extended to support public and private repositories 177 | in GitHub as an alternative to AWS CodeCommit. 178 | 179 | Your GitHub repository must already exist when you create or update 180 | the CloudFormation stack. You must also generate a (secret) personal 181 | access token on GitHub and save it in AWS SSM Parameter Store. 182 | 183 | Here are Amazon's instructions for generating the GitHub personal 184 | access token on GitHub (ignore the steps after you generate the token 185 | on GitHub and copy it): 186 | 187 | > https://docs.aws.amazon.com/quickstart/latest/cicd-taskcat/step2.html 188 | 189 | Once you have the token, save it in SSM Parameter Store: 190 | 191 | github_token="YOUR-SECRET-GITHUB-TOKEN" 192 | github_token_key="/YOUR-SSM-PARAMETER-STORE-KEY-FOR-GITHUB-TOKEN" 193 | 194 | # Should be SecureString, but not yet supported by CloudFormation! 195 | aws ssm put-parameter \ 196 | --type String \ 197 | --name "$github_token_key" \ 198 | --value "$github_token" 199 | 200 | unset github_token 201 | 202 | Now set up these environment variables: 203 | 204 | source_type=GitHub 205 | github_user="YOUR-GITHUB-USER" 206 | github_repository="YOUR-GITHUB-REPO" 207 | github_token_key="SAME-AS-ABOVE" 208 | 209 | and pass in these parameters when creating or updating the 210 | CloudFormation stack above: 211 | 212 | "ParameterKey=SourceType,ParameterValue=$source_type" \ 213 | "ParameterKey=GitHubRepository,ParameterValue=$github_repository" \ 214 | "ParameterKey=GitHubUser,ParameterValue=$github_user" \ 215 | "ParameterKey=GitHubToken,ParameterValue=$github_token_key" 216 | 217 | [elementryx]: https://github.com/elementryx/codepipeline-powered-static-website 218 | 219 | ## Clean up after testing 220 | 221 | ### Delete a stack (mostly) 222 | 223 | aws cloudformation delete-stack \ 224 | --region "$region" \ 225 | --stack-name "$stackname" 226 | 227 | This leaves behind: 228 | 229 | - Route 53 hosted zone 230 | - S3 buckets 231 | - Git repository 232 | 233 | ### Clean up Route 53 hosted zone 234 | 235 | domain=... 236 | 237 | hosted_zone_id=$( 238 | aws route53 list-hosted-zones \ 239 | --output text \ 240 | --query 'HostedZones[?Name==`'$domain'.`].Id' 241 | ) 242 | echo hosted_zone_id=$hosted_zone_id 243 | aws route53 delete-hosted-zone \ 244 | --id "$hosted_zone_id" 245 | 246 | ### Clean up S3 buckets 247 | 248 | # WARNING! DESTROYS CONTENT AND LOGS! 249 | 250 | aws s3 rm --recursive s3://logs.$domain 251 | aws s3 rb s3://logs.$domain 252 | aws s3 rm --recursive s3://$domain 253 | aws s3 rb s3://$domain 254 | aws s3 rb s3://www.$domain 255 | aws s3 rm --recursive s3://codepipeline.$domain 256 | aws s3 rb s3://codepipeline.$domain 257 | 258 | That last command will fail, so go use the web console to delete the 259 | versioned codepipeline bucket. 260 | 261 | ### Delete CodeCommit Git repository 262 | 263 | # WARNING! DESTROYS CONTENT! 264 | 265 | #aws codecommit delete-repository \ 266 | --region "$region" \ 267 | --repository-name "$domain" 268 | -------------------------------------------------------------------------------- /aws-git-backed-static-website-architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alestic/aws-git-backed-static-website/9d106ef20f167095112624c7ee295515cd60bf52/aws-git-backed-static-website-architecture.gif -------------------------------------------------------------------------------- /aws-git-backed-static-website-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alestic/aws-git-backed-static-website/9d106ef20f167095112624c7ee295515cd60bf52/aws-git-backed-static-website-architecture.png -------------------------------------------------------------------------------- /aws-git-backed-static-website-cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Description: | 4 | Static web site stack including: 5 | * CodeCommit Git repository (OR) GitHub Git repository 6 | * S3 bucket for web site content 7 | * Redirect from "www." to base domain 8 | * Access logs written to logs bucket 9 | * ACM Certificate for SSL 10 | * CloudFront distributions for website https access 11 | * Route 53 hosted zone with DNS entries 12 | * CodePipeline (source CodeCommit, invoke Lambda functions) 13 | * S3 bucket for CodePipeline artifacts 14 | * AWS Lambda function(s) to generate static website from Git contents 15 | * AWS Lambda function to copy generated website to website S3 bucket 16 | * SNS topic for CodeCommit Git change notifications 17 | * Email address subscribed to SNS notification topic 18 | * EventBridge rule for triggering pipeline on CodeCommit changes 19 | 20 | Parameters: 21 | # Domain: example.com 22 | DomainName: 23 | Type: String 24 | Description: "The base domain name for the web site (no 'www')" 25 | MinLength: 4 26 | MaxLength: 253 27 | AllowedPattern: "[a-z0-9]+[-.a-z0-9]*(\\.[a-z][a-z]+)+" 28 | ConstraintDescription: "Provide a valid domain name using only lowercase letters, numbers, and dash (-)" 29 | 30 | # Email address to receive CodeCommit Git activity notifications: 31 | # you@anotherdomain.com (CANNOT be in same domain!) 32 | NotificationEmail: 33 | Type: String 34 | Description: "Initial email address to receive Git change notifications" 35 | MinLength: 6 36 | AllowedPattern: ".+@[a-z0-9]+[-.a-z0-9]*(\\.[a-z][a-z]+)+" 37 | ConstraintDescription: "Provide a valid email address" 38 | 39 | IndexDocument: 40 | Type: String 41 | Description: "Filename to use for home page and directory path requests" 42 | Default: "index.html" 43 | ConstraintDescription: "Provide a valid index document file name" 44 | 45 | ErrorDocument: 46 | Type: String 47 | Description: "Filename to use for errors (e.g., not found)" 48 | Default: "error.html" 49 | ConstraintDescription: "Provide a valid error document file name" 50 | 51 | DefaultTTL: 52 | Type: Number 53 | Description: "TTL in seconds" 54 | Default: 30 55 | 56 | MinimumTTL: 57 | Description: "Minimum cache lifetime in seconds for the CloudFront distribution" 58 | Default: 5 59 | Type: Number 60 | 61 | PriceClass: 62 | Description: "Distribution price class. Default is US-only, PriceClass_All is worldwide but more expensive." 63 | Default: PriceClass_100 64 | AllowedValues: 65 | - PriceClass_100 66 | - PriceClass_200 67 | - PriceClass_All 68 | Type: String 69 | 70 | GeneratorLambdaFunctionS3Bucket: 71 | Type: String 72 | Description: "S3 bucket containing ZIP of AWS Lambda function (static site generator)" 73 | Default: "run.alestic.com" 74 | 75 | GeneratorLambdaFunctionS3Key: 76 | Type: String 77 | Description: "S3 key containing ZIP of AWS Lambda function (static site generator)" 78 | Default: "lambda/aws-lambda-site-generator-identity.zip" 79 | 80 | GeneratorLambdaFunctionRuntime: 81 | Type: String 82 | Description: "Runtime language for AWS Lambda function (static site generator)" 83 | Default: "python3.9" 84 | AllowedValues: 85 | - "python3.9" 86 | - "python2.7" 87 | - "nodejs" 88 | - "nodejs4.3" 89 | - "java8" 90 | 91 | GeneratorLambdaFunctionHandler: 92 | Type: String 93 | Description: "Function Handler for AWS Lambda function (static site generator)" 94 | Default: "index.handler" 95 | 96 | GeneratorLambdaFunctionUserParameters: 97 | Type: String 98 | Description: "User parameters for AWS Lambda function (static site generator)" 99 | Default: "unused" 100 | MinLength: 1 101 | MaxLength: 1000 102 | 103 | SyncLambdaFunctionS3Bucket: 104 | Type: String 105 | Description: "S3 bucket containing ZIP of AWS Lambda function (sync to S3)" 106 | Default: "run.alestic.com" 107 | 108 | SyncLambdaFunctionS3Key: 109 | Type: String 110 | Description: "S3 key containing ZIP of AWS Lambda function (sync to S3)" 111 | Default: "lambda/aws-lambda-git-backed-static-website.zip" 112 | 113 | CertificateValidationMethod: 114 | Type: String 115 | Description: "Validation method for ACM certificate (DNS or EMAIL)" 116 | Default: "DNS" 117 | 118 | SourceType: 119 | Description: "CodeCommit | GitHub" 120 | Type: String 121 | Default: "CodeCommit" 122 | AllowedValues: 123 | - CodeCommit 124 | - GitHub 125 | 126 | BranchName: 127 | Description: "Name of Git branch to build from" 128 | Type: String 129 | Default: master 130 | 131 | GitHubRepository: 132 | Description: "Optional GitHub repository name for pre-existing GitHub repository. Leave empty to use CodeCommit." 133 | Type: String 134 | Default: "" 135 | 136 | GitHubUser: 137 | Description: "Optional GitHub user name that owns the GitHub repository. Leave empty to use CodeCommit." 138 | Type: String 139 | Default: "" 140 | 141 | GitHubTokenName: 142 | Description: "Optional name of the AWS Secrets Manager secret containing GitHub access token for pre-existing GitHub repository. DO NOT PASS THE OAUTH TOKEN HERE! Leave empty to use CodeCommit. See: https://github.com/settings/tokens" 143 | Type: String 144 | Default: "" 145 | 146 | GitHubTokenVersion: 147 | Description: "Optional version of the AWS Secrets Manager secret containing GitHub access token for pre-existing GitHub repository. Leave empty for AWSCURRENT or to use CodeCommit." 148 | Type: String 149 | Default: "" 150 | 151 | PreExistingGitRepository: 152 | Description: "Optional Git repository name for pre-existing CodeCommit repository. Leave empty to have CodeCommit Repository created and managed by this stack." 153 | Type: String 154 | Default: "" 155 | 156 | PreExistingHostedZoneDomain: 157 | Description: "Optional domain name for pre-existing Route 53 hosted zone. Leave empty to have hosted zone created and managed by this stack." 158 | Type: String 159 | Default: "" 160 | 161 | PreExistingSiteBucket: 162 | Description: "Optional name of pre-existing website bucket. Leave empty to have website bucket created and managed by this stack." 163 | Type: String 164 | Default: "" 165 | 166 | PreExistingRedirectBucket: 167 | Description: "Optional name of pre-existing redirect bucket. Leave empty to have redirect bucket created and managed by this stack." 168 | Type: String 169 | Default: "" 170 | 171 | PreExistingLogsBucket: 172 | Description: "Optional name of pre-existing access logs bucket. Leave empty to have access logs bucket created and managed by this stack." 173 | Type: String 174 | Default: "" 175 | 176 | PreExistingCodePipelineBucket: 177 | Description: "Optional name of pre-existing CodePipeline artifact bucket. Leave empty to have CodePipeline bucket created and managed by this stack." 178 | Type: String 179 | Default: "" 180 | 181 | Conditions: 182 | UseCodeCommit: !Equals [!Ref SourceType, "CodeCommit"] 183 | NeedsNewGitRepository: !And [!Equals [!Ref PreExistingGitRepository, ""], !Equals [!Ref SourceType, "CodeCommit"]] 184 | NeedsNewHostedZone: !Equals [!Ref PreExistingHostedZoneDomain, ""] 185 | NeedsNewSiteBucket: !Equals [!Ref PreExistingSiteBucket, ""] 186 | NeedsNewRedirectBucket: !Equals [!Ref PreExistingRedirectBucket, ""] 187 | NeedsNewLogsBucket: !Equals [!Ref PreExistingLogsBucket, ""] 188 | NeedsNewCodePipelineBucket: !Equals [!Ref PreExistingCodePipelineBucket, ""] 189 | 190 | Resources: 191 | # Bucket for CloudFront and S3 access logs: logs.example.com 192 | LogsBucket: 193 | Condition: NeedsNewLogsBucket 194 | Type: "AWS::S3::Bucket" 195 | Properties: 196 | BucketName: !Sub "logs.${DomainName}" 197 | AccessControl: LogDeliveryWrite 198 | DeletionPolicy: Retain 199 | 200 | # Bucket for site content: example.com 201 | SiteBucket: 202 | Condition: NeedsNewSiteBucket 203 | Type: "AWS::S3::Bucket" 204 | Properties: 205 | BucketName: !Ref DomainName 206 | AccessControl: PublicRead 207 | WebsiteConfiguration: 208 | IndexDocument: !Ref IndexDocument 209 | ErrorDocument: !Ref ErrorDocument 210 | # logs.example.com/logs/s3/example.com/ 211 | LoggingConfiguration: 212 | DestinationBucketName: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket] 213 | LogFilePrefix: !Sub "logs/s3/${DomainName}/" 214 | DeletionPolicy: Retain 215 | 216 | # Bucket to redirect to example.com: www.example.com 217 | RedirectBucket: 218 | Condition: NeedsNewRedirectBucket 219 | Type: "AWS::S3::Bucket" 220 | Properties: 221 | BucketName: !Sub "www.${DomainName}" 222 | AccessControl: BucketOwnerFullControl 223 | # logs.example.com/logs/s3/www.example.com/ 224 | LoggingConfiguration: 225 | DestinationBucketName: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket] 226 | LogFilePrefix: !Sub "logs/s3/www.${DomainName}/" 227 | WebsiteConfiguration: 228 | RedirectAllRequestsTo: 229 | HostName: !Ref DomainName 230 | Protocol: https 231 | DeletionPolicy: Delete 232 | 233 | # Bucket for CodePipeline artifact storage: codepipeline.example.com 234 | CodePipelineBucket: 235 | Condition: NeedsNewCodePipelineBucket 236 | Type: "AWS::S3::Bucket" 237 | Properties: 238 | BucketName: !Sub "codepipeline.${DomainName}" 239 | VersioningConfiguration: 240 | Status: Enabled 241 | DeletionPolicy: Retain 242 | 243 | # Certificate for HTTPS accesss through CloudFront 244 | Certificate: 245 | Type: "AWS::CertificateManager::Certificate" 246 | Properties: 247 | DomainName: !Ref DomainName 248 | SubjectAlternativeNames: 249 | - !Sub "www.${DomainName}" 250 | ValidationMethod: !Ref CertificateValidationMethod 251 | 252 | # CDN serves S3 content over HTTPS for example.com 253 | CloudFrontDistribution: 254 | Type: "AWS::CloudFront::Distribution" 255 | Properties: 256 | DistributionConfig: 257 | Enabled: true 258 | Aliases: 259 | - !Ref DomainName 260 | DefaultRootObject: index.html 261 | PriceClass: !Ref PriceClass 262 | Origins: 263 | - 264 | DomainName: !Join ["", [!Ref DomainName, ".", !FindInMap [RegionMap, !Ref "AWS::Region", websiteendpoint]]] 265 | Id: S3Origin 266 | CustomOriginConfig: 267 | HTTPPort: 80 268 | HTTPSPort: 443 269 | OriginProtocolPolicy: http-only 270 | DefaultCacheBehavior: 271 | TargetOriginId: S3Origin 272 | AllowedMethods: 273 | - GET 274 | - HEAD 275 | Compress: true 276 | DefaultTTL: !Ref DefaultTTL 277 | MinTTL: !Ref MinimumTTL 278 | ForwardedValues: 279 | QueryString: false 280 | Cookies: 281 | Forward: none 282 | ViewerProtocolPolicy: redirect-to-https 283 | # logs.example.com/logs/cloudfront/example.com/ 284 | Logging: 285 | Bucket: !Join ["", [!If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket], ".s3.amazonaws.com"]] 286 | Prefix: !Sub "logs/cloudfront/${DomainName}/" 287 | IncludeCookies: false 288 | ViewerCertificate: 289 | AcmCertificateArn: !Ref Certificate 290 | SslSupportMethod: sni-only 291 | 292 | # CDN serves S3 content over HTTPS for www.example.com 293 | RedirectCloudFrontDistribution: 294 | Type: "AWS::CloudFront::Distribution" 295 | Properties: 296 | DistributionConfig: 297 | Enabled: true 298 | Aliases: 299 | - !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket] 300 | PriceClass: PriceClass_100 301 | Origins: 302 | - 303 | DomainName: !Join ["", [!If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket], ".", !FindInMap [RegionMap, !Ref "AWS::Region", websiteendpoint]]] 304 | Id: RedirectS3Origin 305 | CustomOriginConfig: 306 | HTTPPort: 80 307 | HTTPSPort: 443 308 | OriginProtocolPolicy: http-only 309 | DefaultCacheBehavior: 310 | TargetOriginId: RedirectS3Origin 311 | AllowedMethods: 312 | - GET 313 | - HEAD 314 | DefaultTTL: !Ref DefaultTTL 315 | MinTTL: !Ref MinimumTTL 316 | ForwardedValues: 317 | QueryString: false 318 | Cookies: 319 | Forward: none 320 | ViewerProtocolPolicy: allow-all 321 | # logs.example.com/logs/cloudfront/www.example.com/ 322 | Logging: 323 | Bucket: !Join ["", [!If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket], ".s3.amazonaws.com"]] 324 | Prefix: !Sub "logs/cloudfront/www.${DomainName}/" 325 | IncludeCookies: false 326 | ViewerCertificate: 327 | AcmCertificateArn: !Ref Certificate 328 | SslSupportMethod: sni-only 329 | 330 | # DNS: example.com, www.example.com 331 | Route53HostedZone: 332 | Condition: NeedsNewHostedZone 333 | Type: "AWS::Route53::HostedZone" 334 | Properties: 335 | HostedZoneConfig: 336 | Comment: !Sub "Created by CloudFormation stack: ${AWS::StackName}" 337 | Name: !Ref DomainName 338 | DeletionPolicy: Retain 339 | 340 | Route53RecordSetGroup: 341 | Type: "AWS::Route53::RecordSetGroup" 342 | Properties: 343 | HostedZoneId: !If [NeedsNewHostedZone, !Ref Route53HostedZone, !Ref "AWS::NoValue"] 344 | HostedZoneName: !If [NeedsNewHostedZone, !Ref "AWS::NoValue", !Sub "${PreExistingHostedZoneDomain}."] 345 | RecordSets: 346 | # example.com 347 | - Name: !Sub "${DomainName}." 348 | Type: A 349 | # Resolve to CloudFront distribution 350 | AliasTarget: 351 | HostedZoneId: Z2FDTNDATAQYW2 # CloudFront 352 | DNSName: !GetAtt CloudFrontDistribution.DomainName 353 | # www.example.com 354 | - Name: !Sub "www.${DomainName}." 355 | Type: A 356 | # Resolve to Redirect CloudFront distribution 357 | AliasTarget: 358 | HostedZoneId: Z2FDTNDATAQYW2 # CloudFront 359 | DNSName: !GetAtt RedirectCloudFrontDistribution.DomainName 360 | 361 | # SNS topic for Git repository activity. Email subscription 362 | NotificationTopic: 363 | Type: "AWS::SNS::Topic" 364 | Properties: 365 | DisplayName: !Sub "Activity in ${DomainName} Git repository" 366 | Subscription: 367 | - Endpoint: !Ref NotificationEmail 368 | Protocol: email 369 | 370 | # Git repository: example.com 371 | GitRepository: 372 | Condition: NeedsNewGitRepository 373 | Type: "AWS::CodeCommit::Repository" 374 | Properties: 375 | RepositoryDescription: !Sub "Git repository for ${DomainName}" 376 | RepositoryName: !Ref DomainName 377 | Triggers: 378 | - Name: !Sub "Activity in ${DomainName} Git repository" 379 | DestinationArn: !Ref NotificationTopic 380 | Events: 381 | - all 382 | DeletionPolicy: Retain 383 | 384 | # IAM info for AWS Lambda functions 385 | LambdaExecutionRole: 386 | Type: AWS::IAM::Role 387 | Properties: 388 | AssumeRolePolicyDocument: 389 | Version: '2012-10-17' 390 | Statement: 391 | - Effect: Allow 392 | Principal: 393 | Service: 394 | - lambda.amazonaws.com 395 | Action: 396 | - sts:AssumeRole 397 | Path: "/" 398 | Policies: 399 | - PolicyName: !Sub "${DomainName}-execution-policy" 400 | PolicyDocument: 401 | Version: '2012-10-17' 402 | Statement: 403 | - Effect: Allow 404 | Action: "logs:*" 405 | Resource: "arn:aws:logs:*:*:*" 406 | - Effect: Allow 407 | Action: 408 | - codepipeline:PutJobSuccessResult 409 | - codepipeline:PutJobFailureResult 410 | Resource: "*" 411 | - Effect: Allow 412 | Action: 413 | - s3:GetBucketLocation 414 | - s3:ListBucket 415 | - s3:ListBucketMultipartUploads 416 | Resource: 417 | - !Join ["", ["arn:aws:s3:::", !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket]]] 418 | - !Join ["", ["arn:aws:s3:::", !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket]]] 419 | - Effect: Allow 420 | Action: 421 | - s3:AbortMultipartUpload 422 | - s3:DeleteObject 423 | - s3:GetObject 424 | - s3:GetObjectAcl 425 | - s3:ListMultipartUploadParts 426 | - s3:PutObject 427 | - s3:PutObjectAcl 428 | Resource: 429 | - !Join ["", ["arn:aws:s3:::", !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket], "/*"]] 430 | - !Join ["", ["arn:aws:s3:::", !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket], "/*"]] 431 | - Effect: Allow 432 | Action: "cloudfront:CreateInvalidation" 433 | Resource: "*" 434 | 435 | GeneratorLambdaFunction: 436 | Type: "AWS::Lambda::Function" 437 | Properties: 438 | Description: !Sub "Static site generator for ${DomainName}" 439 | #TBD: Some static site generators might need more permissions 440 | Role: !GetAtt LambdaExecutionRole.Arn 441 | MemorySize: 1536 442 | Timeout: 300 443 | Runtime: !Ref GeneratorLambdaFunctionRuntime 444 | Handler: !Ref GeneratorLambdaFunctionHandler 445 | Code: 446 | S3Bucket: !Ref GeneratorLambdaFunctionS3Bucket 447 | S3Key: !Ref GeneratorLambdaFunctionS3Key 448 | 449 | SyncLambdaFunction: 450 | Type: "AWS::Lambda::Function" 451 | Properties: 452 | Description: !Sub "Copy Git branch contents to S3 bucket for ${DomainName}" 453 | Role: !GetAtt LambdaExecutionRole.Arn 454 | MemorySize: 1536 455 | Timeout: 300 456 | Runtime: python3.9 457 | Handler: index.handler 458 | Code: 459 | S3Bucket: !Ref SyncLambdaFunctionS3Bucket 460 | S3Key: !Ref SyncLambdaFunctionS3Key 461 | Environment: 462 | Variables: 463 | site_bucket: !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket] 464 | cloudfront_distribution: !Ref CloudFrontDistribution 465 | 466 | # IAM info for CodePipeline 467 | CodePipelineRole: 468 | Type: "AWS::IAM::Role" 469 | Properties: 470 | AssumeRolePolicyDocument: 471 | Version: "2012-10-17" 472 | Statement: 473 | - Effect: Allow 474 | Principal: 475 | Service: 476 | - "lambda.amazonaws.com" 477 | - "codepipeline.amazonaws.com" 478 | - "events.amazonaws.com" 479 | Action: 480 | - "sts:AssumeRole" 481 | Path: "/" 482 | Policies: 483 | - PolicyName: "codepipeline-service" 484 | PolicyDocument: 485 | Version: "2012-10-17" 486 | Statement: 487 | - Effect: "Allow" 488 | Action: "*" 489 | Resource: "*" 490 | 491 | # EventBridge rule for CodeCommit changes 492 | CodeCommitEventRule: 493 | Type: AWS::Events::Rule 494 | Condition: UseCodeCommit 495 | Properties: 496 | Description: "Rule to trigger pipeline on CodeCommit repository changes" 497 | State: ENABLED 498 | EventPattern: 499 | source: 500 | - aws.codecommit 501 | detail-type: 502 | - 'CodeCommit Repository State Change' 503 | resources: 504 | - !If 505 | - NeedsNewGitRepository 506 | - !GetAtt GitRepository.Arn 507 | - !Sub "arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${PreExistingGitRepository}" 508 | detail: 509 | event: 510 | - referenceCreated 511 | - referenceUpdated 512 | referenceType: 513 | - branch 514 | referenceName: 515 | - !Ref BranchName 516 | Targets: 517 | - Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${DomainName}-codepipeline" 518 | RoleArn: !GetAtt CodePipelineRole.Arn 519 | Id: "CodePipelineTarget" 520 | 521 | CodePipeline: 522 | Type: "AWS::CodePipeline::Pipeline" 523 | Properties: 524 | Name: !Sub "${DomainName}-codepipeline" 525 | ArtifactStore: 526 | Type: S3 527 | Location: !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket] 528 | RestartExecutionOnUpdate: false 529 | RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/${CodePipelineRole}" 530 | Stages: 531 | - Name: Source 532 | Actions: 533 | - Name: SourceAction 534 | ActionTypeId: 535 | Category: Source 536 | Owner: !If [UseCodeCommit, "AWS", "ThirdParty"] 537 | Provider: !Ref SourceType 538 | Version: 1 539 | Configuration: 540 | !If 541 | - UseCodeCommit 542 | - 543 | RepositoryName: !If [NeedsNewGitRepository, !Ref DomainName, !Ref PreExistingGitRepository] 544 | BranchName: !Ref BranchName 545 | PollForSourceChanges: false 546 | - 547 | Owner: !Ref GitHubUser 548 | Repo: !Ref GitHubRepository 549 | Branch: !Ref BranchName 550 | OAuthToken: !Join ['', ['{{resolve:secretsmanager:', !Ref GitHubTokenName, ':SecretString:::', !Ref GitHubTokenVersion, '}}' ]] 551 | 552 | #OAuthToken: !Join ['', ['{{resolve:ssm:', !Ref GitHubTokenName, ':', !Ref GitHubTokenVersion, '}}' ]] 553 | #OAuthToken: !Join ['', ['{{resolve:ssm-secure:', !Ref GitHubTokenName, ':', !Ref GitHubTokenVersion, '}}' ]] 554 | #OAuthToken: !Sub “{{resolve:secretsmanager:${GitHubTokenName}:SecretString:::${GitHubTokenVersion}}}” 555 | #OAuthToken: !Sub “{{resolve:ssm:${GitHubTokenName}:${GitHubTokenVersion}}}” 556 | #OAuthToken: !Sub “{{resolve:ssm-secure:${GitHubTokenName}:${GitHubTokenVersion}}}” 557 | 558 | OutputArtifacts: 559 | - Name: SiteSource 560 | RunOrder: 1 561 | - Name: InvokeGenerator 562 | Actions: 563 | - Name: InvokeAction 564 | InputArtifacts: 565 | - Name: SiteSource 566 | ActionTypeId: 567 | Category: Invoke 568 | Owner: AWS 569 | Provider: Lambda 570 | Version: 1 571 | Configuration: 572 | FunctionName: !Ref GeneratorLambdaFunction 573 | UserParameters: !Ref GeneratorLambdaFunctionUserParameters 574 | OutputArtifacts: 575 | - Name: SiteContent 576 | RunOrder: 1 577 | - Name: InvokeSync 578 | Actions: 579 | - Name: InvokeAction 580 | InputArtifacts: 581 | - Name: SiteContent 582 | ActionTypeId: 583 | Category: Invoke 584 | Owner: AWS 585 | Provider: Lambda 586 | Version: 1 587 | Configuration: 588 | FunctionName: !Ref SyncLambdaFunction 589 | RunOrder: 1 590 | 591 | Mappings: 592 | RegionMap: 593 | ap-northeast-1: 594 | S3hostedzoneID: "Z2M4EHUR26P7ZW" 595 | websiteendpoint: "s3-website-ap-northeast-1.amazonaws.com" 596 | ap-northeast-2: 597 | S3hostedzoneID: "Z3W03O7B5YMIYP" 598 | websiteendpoint: "s3-website.ap-northeast-2.amazonaws.com" 599 | ap-south-1: 600 | S3hostedzoneID: "Z11RGJOFQNVJUP" 601 | websiteendpoint: "s3-website.ap-south-1.amazonaws.com" 602 | ap-southeast-1: 603 | S3hostedzoneID: "Z3O0J2DXBE1FTB" 604 | websiteendpoint: "s3-website-ap-southeast-1.amazonaws.com" 605 | ap-southeast-2: 606 | S3hostedzoneID: "Z1WCIGYICN2BYD" 607 | websiteendpoint: "s3-website-ap-southeast-2.amazonaws.com" 608 | eu-central-1: 609 | S3hostedzoneID: "Z21DNDUVLTQW6Q" 610 | websiteendpoint: "s3-website.eu-central-1.amazonaws.com" 611 | eu-west-1: 612 | S3hostedzoneID: "Z1BKCTXD74EZPE" 613 | websiteendpoint: "s3-website-eu-west-1.amazonaws.com" 614 | sa-east-1: 615 | S3hostedzoneID: "Z7KQH4QJS55SO" 616 | websiteendpoint: "s3-website-sa-east-1.amazonaws.com" 617 | us-east-1: 618 | S3hostedzoneID: "Z3AQBSTGFYJSTF" 619 | websiteendpoint: "s3-website-us-east-1.amazonaws.com" 620 | us-east-2: 621 | S3hostedzoneID: "Z2O1EMRO9K5GLX" 622 | websiteendpoint: "s3-website.us-east-2.amazonaws.com" 623 | us-west-1: 624 | S3hostedzoneID: "Z2F56UZL2M1ACD" 625 | websiteendpoint: "s3-website-us-west-1.amazonaws.com" 626 | us-west-2: 627 | S3hostedzoneID: "Z3BJ6K6RIION7M" 628 | websiteendpoint: "s3-website-us-west-2.amazonaws.com" 629 | 630 | Outputs: 631 | DomainName: 632 | Description: Domain name 633 | Value: !Ref DomainName 634 | RedirectDomainName: 635 | Description: Redirect hostname 636 | Value: !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket] 637 | SiteBucket: 638 | Value: !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket] 639 | RedirectBucket: 640 | Value: !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket] 641 | LogsBucket: 642 | Description: S3 Bucket with access logs 643 | Value: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket] 644 | HostedZoneId: 645 | Description: Route 53 Hosted Zone id 646 | Value: !If [NeedsNewHostedZone, !Ref Route53HostedZone, "N/A"] 647 | CloudFrontDomain: 648 | Description: CloudFront distribution domain name 649 | Value: !Ref CloudFrontDistribution 650 | RedirectCloudFrontDomain: 651 | Description: Redirect CloudFront distribution domain name 652 | Value: !Ref RedirectCloudFrontDistribution 653 | CodePipelineArn: 654 | Description: CodePipeline ARN 655 | Value: !Ref CodePipeline 656 | GitRepositoryName: 657 | Description: Git repository name 658 | Value: !If [UseCodeCommit, !If [NeedsNewGitRepository, !Ref DomainName, !Ref PreExistingGitRepository], !Ref GitHubRepository] 659 | GitCloneUrlHttp: 660 | Description: Git https clone endpoint 661 | Value: !If [UseCodeCommit, !If [NeedsNewGitRepository, !GetAtt GitRepository.CloneUrlHttp, "N/A"], "N/A"] 662 | GitCloneUrlSsh: 663 | Description: Git ssh clone endpoint 664 | Value: !If [UseCodeCommit, !If [NeedsNewGitRepository, !GetAtt GitRepository.CloneUrlSsh, "N/A"], "N/A"] 665 | 666 | Metadata: 667 | AWS::CloudFormation::Interface: 668 | ParameterGroups: 669 | - Label: 670 | default: Website and Git repository 671 | Parameters: 672 | - DomainName 673 | - Label: 674 | default: Git Activity 675 | Parameters: 676 | - NotificationEmail 677 | - Label: 678 | default: AWS Lambda Function (static site generator) 679 | Parameters: 680 | - GeneratorLambdaFunctionS3Bucket 681 | - GeneratorLambdaFunctionS3Key 682 | - GeneratorLambdaFunctionRuntime 683 | - GeneratorLambdaFunctionHandler 684 | - GeneratorLambdaFunctionUserParameters 685 | - Label: 686 | default: AWS Lambda Function (Sync to S3) 687 | Parameters: 688 | - SyncLambdaFunctionS3Bucket 689 | - SyncLambdaFunctionS3Key 690 | - Label: 691 | default: CloudFront CDN 692 | Parameters: 693 | - PriceClass 694 | - MinimumTTL 695 | - DefaultTTL 696 | - Label: 697 | default: Git Source 698 | Parameters: 699 | - SourceType 700 | - BranchName 701 | - GitHubRepository 702 | - GitHubUser 703 | - GitHubTokenName 704 | - GitHubTokenVersion 705 | - Label: 706 | default: PreExisting Resources To Use (Leave empty for stack to create and manage) 707 | Parameters: 708 | - PreExistingGitRepository 709 | - PreExistingHostedZoneDomain 710 | - PreExistingSiteBucket 711 | - PreExistingRedirectBucket 712 | - PreExistingLogsBucket 713 | - PreExistingCodePipelineBucket 714 | -------------------------------------------------------------------------------- /aws-git-backed-static-website-lambda.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.9 2 | # 3 | # Lambda function for git-backed-static-website 4 | # 5 | # For more info, see: TBD 6 | # 7 | # This Lambda function is invoked by CodePipeline. 8 | # - Download a ZIP file from the CodePipeline artifact S3 bucket 9 | # - Unzip the contents into a temporary directory 10 | # - Sync the contents to the S3 bucket specified in the request 11 | # 12 | from __future__ import print_function 13 | from boto3.session import Session 14 | import boto3 15 | import botocore 16 | import os 17 | import time 18 | import zipfile 19 | import tempfile 20 | import shutil 21 | import subprocess 22 | import traceback 23 | 24 | code_pipeline = boto3.client('codepipeline') 25 | cloudfront = boto3.client('cloudfront') 26 | 27 | def setup(event): 28 | # Extract attributes passed in by CodePipeline 29 | job_id = event['CodePipeline.job']['id'] 30 | job_data = event['CodePipeline.job']['data'] 31 | artifact = job_data['inputArtifacts'][0] 32 | config = job_data['actionConfiguration']['configuration'] 33 | credentials = job_data['artifactCredentials'] 34 | from_bucket = artifact['location']['s3Location']['bucketName'] 35 | from_key = artifact['location']['s3Location']['objectKey'] 36 | from_revision = artifact['revision'] 37 | #output_artifact = job_data['outputArtifacts'][0] 38 | #to_bucket = output_artifact['location']['s3Location']['bucketName'] 39 | #to_key = output_artifact['location']['s3Location']['objectKey'] 40 | 41 | # Temporary credentials to access CodePipeline artifact in S3 42 | key_id = credentials['accessKeyId'] 43 | key_secret = credentials['secretAccessKey'] 44 | session_token = credentials['sessionToken'] 45 | session = Session(aws_access_key_id=key_id, 46 | aws_secret_access_key=key_secret, 47 | aws_session_token=session_token) 48 | s3 = session.client('s3', 49 | config=botocore.client.Config(signature_version='s3v4')) 50 | 51 | return (job_id, s3, from_bucket, from_key, from_revision) 52 | 53 | def download_source(s3, from_bucket, from_key, from_revision, source_dir): 54 | with tempfile.NamedTemporaryFile() as tmp_file: 55 | #TBD: from_revision is not used here! 56 | s3.download_file(from_bucket, from_key, tmp_file.name) 57 | with zipfile.ZipFile(tmp_file.name, 'r') as zip: 58 | zip.extractall(source_dir) 59 | 60 | def handler(event, context): 61 | try: 62 | (job_id, s3, from_bucket, from_key, from_revision) = setup(event) 63 | 64 | to_bucket = os.environ['site_bucket'] 65 | cloudfront_distribution = os.environ['cloudfront_distribution'] 66 | 67 | # Directories for source content, and transformed static site 68 | source_dir = tempfile.mkdtemp() 69 | 70 | # Download and unzip the source for the static site 71 | download_source(s3, from_bucket, from_key, from_revision, source_dir) 72 | 73 | # Generate static website from source 74 | upload_static_site(source_dir, to_bucket) 75 | 76 | # Invalidate content cached in the CloudFront distribution 77 | invalidate_cloudfront(cloudfront_distribution) 78 | 79 | # Tell CodePipeline we succeeded 80 | code_pipeline.put_job_success_result(jobId=job_id) 81 | 82 | except Exception as e: 83 | print(e) 84 | traceback.print_exc() 85 | # Tell CodePipeline we failed 86 | code_pipeline.put_job_failure_result(jobId=job_id, failureDetails={'message': e, 'type': 'JobFailed'}) 87 | 88 | finally: 89 | shutil.rmtree(source_dir) 90 | 91 | return "complete" 92 | 93 | def upload_static_site(source_dir, to_bucket): 94 | # Sync Git branch contents to S3 bucket 95 | command = ["./aws", "s3", "sync", "--acl", "public-read", "--delete", 96 | source_dir + "/", "s3://" + to_bucket + "/"] 97 | print(command) 98 | print(subprocess.check_output(command, stderr=subprocess.STDOUT)) 99 | 100 | def invalidate_cloudfront(cloudfront_distribution): 101 | response = cloudfront.create_invalidation( 102 | DistributionId=cloudfront_distribution, 103 | InvalidationBatch={ 104 | 'Paths': { 105 | 'Quantity': 1, 106 | 'Items': ['/*'] 107 | }, 108 | 'CallerReference': str(time.time()) 109 | } 110 | ) 111 | -------------------------------------------------------------------------------- /build-upload-aws-lambda-function: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | # 3 | # Build AWS Lambda function ZIP file and upload to S3 4 | # 5 | # Usage: ./build-upload-aws-lambda-function S3BUCKET S3KEY 6 | # 7 | # ./build-upload-aws-lambda-function run.alestic.com lambda/aws-lambda-git-backed-static-website.zip 8 | # 9 | 10 | s3bucket=${1:?Specify target S3 bucket name} 11 | s3key=${2:?Specify target S3 key} 12 | target=s3://$s3bucket/$s3key 13 | 14 | tmpdir=$(mktemp -d /tmp/lambda-XXXXXX) 15 | zipfile=$tmpdir/lambda.zip 16 | virtualenv=$tmpdir/virtual-env 17 | ( 18 | virtualenv $virtualenv 19 | source $virtualenv/bin/activate 20 | pip install awscli boto3 21 | ) 22 | 23 | # "aws" command (fixing shabang line) 24 | rsync -va $virtualenv/bin/aws $tmpdir/aws 25 | perl -pi -e '$_ ="#!/var/lang/bin/python\n" if $. == 1' $tmpdir/aws 26 | (cd $tmpdir; zip -r9 $zipfile aws) 27 | 28 | # aws-cli package requirements 29 | (cd $virtualenv/lib/python3.10/site-packages && zip -r9 $zipfile *) 30 | 31 | # AWS Lambda function (with the right name) 32 | rsync -va aws-git-backed-static-website-lambda.py $tmpdir/index.py 33 | (cd $tmpdir; zip -r9 $zipfile index.py) 34 | 35 | # Upload to S3 36 | aws s3 cp --acl=public-read $zipfile $target 37 | 38 | # Clean up 39 | rm -rf $tmpdir 40 | --------------------------------------------------------------------------------