├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── LICENSE.txt ├── NOTICE ├── README.md ├── Rakefile ├── VERSION ├── aws-rails-provisioner.gemspec ├── bin └── aws-rails-provisioner ├── buildspecs ├── buildspec-db.yml └── buildspec-ecr.yml ├── doc-src └── templates │ └── default │ └── layout │ └── html │ ├── footer.erb │ └── layout.erb ├── examples ├── multi_service.yml ├── no_db.yml ├── single_service.yml └── tiny.yml ├── lib ├── aws-rails-provisioner.rb └── aws-rails-provisioner │ ├── build.rb │ ├── cdk_builder.rb │ ├── cdk_code_builder.rb │ ├── cdk_deployer.rb │ ├── code_build.rb │ ├── db_cluster.rb │ ├── errors.rb │ ├── fargate.rb │ ├── migration.rb │ ├── parser.rb │ ├── scaling.rb │ ├── service.rb │ ├── services.rb │ ├── subnet_selection.rb │ ├── utils.rb │ ├── version.rb │ ├── view.rb │ ├── views │ ├── fargate_stack.rb │ ├── init_stack.rb │ ├── pipeline_stack.rb │ └── project.rb │ └── vpc.rb ├── spec ├── cdk_code_builder_spec.rb ├── fixtures │ ├── cdk │ │ ├── multi_service │ │ │ ├── cdk-sample-init-stack.ts │ │ │ ├── cdk-sample.ts │ │ │ ├── rails-foo-fargate-stack.ts │ │ │ ├── rails-foo-pipeline-stack.ts │ │ │ ├── rails-no-db-fargate-stack.ts │ │ │ └── rails-no-db-pipeline-stack.ts │ │ ├── no_db │ │ │ ├── cdk-sample-init-stack.ts │ │ │ ├── cdk-sample.ts │ │ │ ├── rails-no-db-fargate-stack.ts │ │ │ └── rails-no-db-pipeline-stack.ts │ │ └── single_service │ │ │ ├── cdk-sample-init-stack.ts │ │ │ ├── cdk-sample.ts │ │ │ ├── rails-foo-fargate-stack.ts │ │ │ └── rails-foo-pipeline-stack.ts │ └── yml │ │ ├── multi_service.yml │ │ ├── no_db.yml │ │ └── single_service.yml └── spec_helper.rb └── templates ├── fargate_stack.mustache ├── init_stack.mustache ├── pipeline_stack.mustache └── project.mustache /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby-version 2 | .yardoc/ 3 | Gemfile.lock 4 | doc 5 | 6 | aws-rails-provisioner.yml 7 | cdk-sample/ 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tasks/release"] 2 | path = tasks/release 3 | url = https://github.com/aws/aws-sdk-ruby-release-tools.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.0 5 | - 2.1 6 | - 2.2 7 | - 2.3 8 | - 2.4 9 | - 2.5 10 | - 2.6 11 | - 2.7 12 | - jruby-9.1 13 | - jruby-9.2 14 | 15 | sudo: false 16 | 17 | script: bundle exec rake release:test 18 | 19 | bundler_args: --without docs release repl 20 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title 'AWS Rails Provisioner' 2 | --template-path doc-src/templates 3 | --plugin sitemap 4 | --hide-api private 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Unreleased Changes 2 | ------------------ 3 | 4 | 0.0.2.rc4 (2020-11-23) 5 | ------------------ 6 | 7 | * Issue - Use `ParameterGroup` as `ClusterParameterGroup` was removed. (#14) 8 | 9 | 0.0.2.rc3 (2020-05-27) 10 | ------------------ 11 | 12 | * Issue - Run `cdk bootstrap` automatically. 13 | 14 | * Issue - Use docker image asset instead of repo from ECR (fixes failed builds). 15 | 16 | * Issue - Upgrade postgresql DB engine family. 17 | 18 | * Issue - Fix `--profile` option being ignored. 19 | 20 | 0.0.1.rc2 (2019-10-14) 21 | ----------------- 22 | 23 | * Issue - Update `LoadBalancerFargate` to `ApplicationLoadBalancerFargate` 24 | 25 | * Issue - Remove CDK default test file 26 | 27 | 0.0.0.rc1 (2019-07-10) 28 | ------------------ 29 | 30 | * Initial preview release of `aws-rails-provisioner` gem. 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-rails-provisioner/issues), or [recently closed](https://github.com/awslabs/aws-rails-provisioner/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-rails-provisioner/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-rails-provisioner/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rake', require: false 6 | 7 | group :docs do 8 | gem 'yard' 9 | gem 'yard-sitemap', '~> 1.0' 10 | end 11 | 12 | group :test do 13 | gem 'rspec' 14 | end 15 | 16 | group :release do 17 | gem 'octokit' 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | aws-rails-provisioner 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This tool is deprecated 2 | 3 | This tool is deprecated. We cannot guarantee that it will work. 4 | 5 | This documentation will remain for historical purposes. 6 | 7 | ## aws-rails-provisioner 8 | 9 | [![Build Status](https://travis-ci.org/awslabs/aws-rails-provisioner.svg?branch=master)](https://travis-ci.org/awslabs/aws-rails-provisioner) 10 | 11 | A tool for defining and deploying containerized Ruby on Rails applications on AWS. 12 | 13 | `aws-rails-provisioner` is a command line tool using your configurations defined in `aws-rails-provisioner.yml` file to generate [AWS CDK](https://github.com/awslabs/aws-cdk) stacks on your behalf, provisioning required AWS resources for running your containerized Ruby on Rails applications on AWS (current supported platform: AWS Fargate) within a few commands. 14 | 15 | 16 | --- 17 | ![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) 18 | > This tool is under developer preview (beta) stage, with active development, releases might lack features and might have future breaking changes. 19 | --- 20 | 21 | 22 | ## Links of Interest 23 | 24 | * [Change Log](./CHANGELOG.md) 25 | * [Issues](https://github.com/awslabs/aws-rails-provisioner/issues) 26 | * [License](http://aws.amazon.com/apache2.0/) 27 | * [Documentation](https://docs.aws.amazon.com/awsrailsprovisioner/api/) 28 | 29 | ## Getting Started 30 | 31 | ### Prerequisites 32 | 33 | Before using the `aws-rails-provisioner` gem, you need to have: 34 | 35 | * a [Ruby on Rails](https://rubyonrails.org/) application with a Dockerfile 36 | * install or update the [AWS CDK Toolkit] from npm (requires [Node.js ≥ 8.11.x](https://nodejs.org/en/download)): 37 | 38 | ```bash 39 | $ npm i -g aws-cdk 40 | ``` 41 | * docker daemon vailable for building images 42 | 43 | ### Install aws-rails-provisioner 44 | 45 | `aws-rails-provisioner` gem is available from RubyGems, currently with [preview versions](https://rubygems.org/gems/aws-rails-provisioner). 46 | 47 | ``` 48 | gem install aws-rails-provisioner --pre 49 | ``` 50 | 51 | ### Define aws-rails-provisioner.yml 52 | 53 | `aws-rails-provisioner.yml` is a configuration file defining how `aws-rails-provisioner` bootstraps required AWS resources for your Ruby on Rails applications. 54 | 55 | ``` 56 | version: '0' 57 | 58 | vpc: 59 | max_az: 2 60 | enable_dns: true 61 | services: 62 | my_rails_foo: 63 | source_path: ./path/to/my_rails_foo # relative path from `aws-rails-provisioner.yml` 64 | fargate: 65 | desired_count: 3 66 | memory: 512 67 | cpu: 256 68 | envs: 69 | PORT: 80 70 | RAILS_LOG_TO_STDOUT: true 71 | public: true 72 | db_cluster: 73 | engine: aurora-postgresql 74 | db_name: app_development 75 | scaling: 76 | max_capacity: 5 77 | on_cpu: 78 | target_util_percent: 80 79 | scale_in_cool_down: 300 80 | on_requests: 81 | requests_per_target: 1000 82 | my_another_rails: 83 | ... 84 | ``` 85 | 86 | See `./examples` for more `aws-rails-provisioner` examples (see `tiny.yml` for a minimal `aws-rails-provisioner.yml` configuration example). The full configuration options are documented [here](https://docs.aws.amazon.com/awsrailsprovisioner/api/). 87 | 88 | ### Build and Deploy 89 | 90 | Once `aws-rails-provisioner.yml` is defined, run the build command. This will boostrap AWS CDK stacks and define all required AWS resources and connections in CDK code. 91 | 92 | ``` 93 | aws-rails-provisioner build 94 | ``` 95 | 96 | By default, it defines a VPC with public and private subnets, an Amazon RDS Database Cluster, an ECS cluster with AWS Fargate services containing application images. When building with `--with-cicd` option a CICD stack will be defined automatically (including a Rails data migration step). 97 | 98 | ``` 99 | aws-rails-provisioner build --with-cicd 100 | ``` 101 | 102 | After the build completes, run the deploy command to run CDK code that deploys all defined AWS resources: 103 | 104 | ``` 105 | aws-rails-provisioner deploy 106 | ``` 107 | 108 | Instead of deploying everything all at once, you can deploy stack by stack, application by application: 109 | 110 | ``` 111 | # only deploys the stack creates VPC and ECS cluster 112 | aws-rails-provisioner deploy --init 113 | 114 | # deploys fargate service and database cluster when defined 115 | aws-rails-provisioner deploy --fargate 116 | 117 | # deploy CICD stack 118 | aws-rails-provisioner deploy --cicd 119 | 120 | # deploy only :rails_foo application 121 | aws-rails-provisioner deploy --fargate --service my_rails_foo 122 | ``` 123 | 124 | After the deployment completes, your applications are now running on AWS Fargate fronted with AWS Application LoadBalancer. You can view your website using the public IP address of the network interface's load balancer. 125 | 126 | > Note: for applications with databases, rails db migration is needed; the CICD stack contains a migration phase by default, running DB migration commands insides private subnets, connected to the DB Cluster. 127 | 128 | ### CICD 129 | 130 | When `--with-cicd` is enabled at build, a CICD stack is created. Once deployment completes, an AWS CodePipeline is available with source, build, migration, and deploy phases for your Rails application. You need to commit your Rails application to the CodeCommit source repository in the pipeline with `buildspec` files to activate the pipeline. Sample `buildspec`s are availble under `./buildspecs` handling application image builds and rails migrations. 131 | 132 | Full `aws-rails-provisioner` command line options see: 133 | 134 | ``` 135 | aws-rails-provisioner -h 136 | ``` 137 | 138 | ## Contributing 139 | 140 | We welcome community contributions and pull requests. See [CONTRIBUTING](./CONTRIBUTING.md) for details. 141 | 142 | ## Getting Help 143 | 144 | Please use these community resources for getting help. We use the GitHub issues for tracking bugs and feature requests. 145 | 146 | * Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/aws-rails-provisioner) and tag it with `aws-rails-provisioner` 147 | * If it turns out that you may have found a bug, or want to submit a feature request, please open an [issue](https://github.com/awslabs/aws-cdk/issues/new) 148 | 149 | ## License 150 | 151 | The `aws-rails-provisioner` is distributed under [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). See [LICENSE](./LICENSE.txt) for more information. 152 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | $REPO_ROOT = File.dirname(__FILE__) 4 | $LOAD_PATH.unshift(File.join($REPO_ROOT, 'lib')) 5 | $VERSION = ENV['VERSION'] || File.read(File.join($REPO_ROOT, 'VERSION')).strip 6 | 7 | task 'test:coverage:clear' do 8 | sh("rm -rf #{File.join($REPO_ROOT, 'coverage')}") 9 | end 10 | 11 | desc 'run unit tests' 12 | RSpec::Core::RakeTask.new do |t| 13 | t.rspec_opts = "-I #{$REPO_ROOT}/lib -I #{$REPO_ROOT}/spec" 14 | t.pattern = "#{$REPO_ROOT}/spec" 15 | end 16 | 17 | task :spec => 'test:coverage:clear' 18 | task :default => :spec 19 | task 'release:test' => :spec 20 | 21 | Dir.glob('**/*.rake').each do |task_file| 22 | load task_file 23 | end 24 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.2.rc4 2 | -------------------------------------------------------------------------------- /aws-rails-provisioner.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path('../VERSION', __FILE__)).strip 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "aws-rails-provisioner" 5 | spec.version = version 6 | spec.authors = ["Amazon Web Services"] 7 | spec.email = ["mamuller@amazon.com", "alexwoo@amazon.com"] 8 | 9 | spec.summary = "Deploy a Ruby on Rails application on AWS." 10 | spec.description = "Define and deploy containerized Ruby on Rails Applications on AWS." 11 | spec.homepage = "http://github.com/awslabs/aws-rails-provisioner" 12 | spec.license = "Apache-2.0" 13 | 14 | spec.require_paths = ["lib"] 15 | spec.files = Dir['lib/**/*.rb', 'templates/*'] 16 | spec.bindir = 'bin' 17 | spec.executables << 'aws-rails-provisioner' 18 | 19 | spec.add_dependency('aws-sdk-rds', '~> 1') 20 | spec.add_dependency('aws-sdk-secretsmanager', '~> 1') 21 | spec.add_dependency('mustache', '~> 1') 22 | end 23 | -------------------------------------------------------------------------------- /bin/aws-rails-provisioner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'optparse' 5 | require 'fileutils' 6 | 7 | # load the `aws-rails-provisioner` gem 8 | $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) 9 | require 'aws-rails-provisioner' 10 | 11 | # setup default options 12 | options = { 13 | file_path: 'aws-rails-provisioner.yml' 14 | } 15 | options[ARGV[0].to_sym] = {} 16 | 17 | OptionParser.new do |opts| 18 | 19 | opts.banner = "Usage: aws-rails-provisioner [command] [options]" 20 | opts.separator("") 21 | opts.separator("Commands:") 22 | opts.separator(" build: generate CDK stacks from aws-rails-provisioner.yml") 23 | opts.separator(" deploy: deploy generated CDK stacks") 24 | opts.separator("") 25 | opts.separator("Options:") 26 | 27 | svc_name_msg = 'Service name. Changes apply to all services '\ 28 | 'configured under :services if not provided' 29 | opts.on('-s', '--service SERVICE_NAME', svc_name_msg) do |name| 30 | options[:service_name] = name.to_sym 31 | end 32 | 33 | opts.on('-p', '--profile AWS_PROFILE_NAME', 'Used the indicated AWS profile') do |profile| 34 | options[:profile] = profile 35 | end 36 | 37 | file_path_msg = 'File path to `aws-rails-provisioner.yml`, default to `aws-rails-provisioner.yml`'\ 38 | ' at current directory' 39 | opts.on('-f', '--file YAML_FILE_PATH', file_path_msg) do |path| 40 | options[:file_path] = path 41 | end 42 | 43 | dir_msg = "Directory where generated code lives, defaults to `cdk-sample`" 44 | opts.on('-d', '--cdk-directory DIR_PATH', dir_msg) do |dir| 45 | options[:cdk_dir] = dir 46 | end 47 | 48 | # Build 49 | cicd_msg = "Enable CICD stack generation, use with `build`" 50 | opts.on('--with-cicd', cicd_msg) do 51 | if options[:build].nil? 52 | raise Aws::RailsProvisioner::Errors::InvalidCommandOption.new('build', '--with-cicd') 53 | end 54 | options[:build][:enable_cicd] = true 55 | end 56 | 57 | # need to consider scenarios for dryrun 58 | #opts.on('--dryrun') do 59 | #options[:dryrun] = true 60 | #end 61 | 62 | # Deploy 63 | init_msg = "Deploy the stack that creates VPC and ECS cluster" 64 | opts.on('--init', init_msg) do 65 | if options[:deploy].nil? 66 | raise Aws::RailsProvisioner::Errors::InvalidCommandOption.new('deploy', '--init') 67 | end 68 | options[:deploy][:init] = true 69 | end 70 | 71 | fargate_msg = "Deploy the stack that creates Fargate service and DBCluster (if specificed)" 72 | opts.on('--fargate', fargate_msg) do 73 | if options[:deploy].nil? 74 | raise Aws::RailsProvisioner::Errors::InvalidCommandOption.new('deploy', '--fargate') 75 | end 76 | options[:deploy][:fargate] = true 77 | end 78 | 79 | cicd_deploy_msg = "Deploy the stack that builds CICD flow" 80 | opts.on('--cicd', cicd_deploy_msg) do 81 | if options[:deploy].nil? 82 | raise Aws::RailsProvisioner::Errors::InvalidCommandOption.new('deploy', '--cicd') 83 | end 84 | options[:deploy][:cicd] = true 85 | end 86 | 87 | opts.on('-h', '--help', 'Show help') do 88 | puts opts 89 | exit 90 | end 91 | 92 | end.parse! 93 | 94 | config = Aws::RailsProvisioner::Utils.parse(options[:file_path]) 95 | cdk_dir = options[:cdk_dir] || config[:cdk_dir] || 'cdk-sample' 96 | config[:cdk_dir] = cdk_dir 97 | 98 | # prepare codebuilder params 99 | if options[:build] 100 | options[:build][:profile] = options[:profile] 101 | options[:build][:file_path] = options[:file_path] 102 | if svc_name = options[:service_name] 103 | config[:services][svc_name].merge!(options[:build]) 104 | else 105 | config[:services].each do |_, svc| 106 | svc.merge!(options[:build]) 107 | end 108 | end 109 | end 110 | code_builder = Aws::RailsProvisioner::CDKCodeBuilder.new(config) 111 | 112 | if options[:build] 113 | Aws::RailsProvisioner::CDKBuilder.new( 114 | cdk_dir: cdk_dir, 115 | source_files: code_builder.source_files, 116 | default_stack: code_builder.default_stack, 117 | default_test: code_builder.default_test, 118 | services: code_builder.services 119 | ).run 120 | elsif options[:deploy] 121 | deploy_params = { 122 | cdk_dir: cdk_dir, 123 | services: code_builder.services, 124 | stack_prefix: code_builder.stack_prefix, 125 | service_name: options[:service_name] 126 | } 127 | if options[:profile] 128 | deploy_params[:profile] = options[:profile] 129 | end 130 | deploy_params.merge!(options[:deploy]) 131 | deployer = Aws::RailsProvisioner::CDKDeployer.new(deploy_params) 132 | deployer.run 133 | end 134 | -------------------------------------------------------------------------------- /buildspecs/buildspec-db.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - gem install bundler 7 | - bundler install 8 | pre_build: 9 | commands: 10 | - DATABASE_URL=$DATABASE_URL RAILS_ENV=$RAILS_ENV bundle exec rake db:migrate 11 | -------------------------------------------------------------------------------- /buildspecs/buildspec-ecr.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) 7 | pre_build: 8 | commands: 9 | - TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION 10 | - echo $TAG 11 | - docker build -t $REPO_NAME:$TAG . 12 | - docker tag $REPO_NAME:$TAG $REPO_PREFIX:$TAG 13 | post_build: 14 | commands: 15 | - docker push $REPO_PREFIX:$TAG 16 | - CONTAINER_NAME='FargateTaskContainer' 17 | - URI=$REPO_PREFIX:$TAG 18 | - echo Writing image definitions file... 19 | - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $URI > imagedefinitions.json 20 | artifacts: 21 | files: imagedefinitions.json 22 | -------------------------------------------------------------------------------- /doc-src/templates/default/layout/html/footer.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /doc-src/templates/default/layout/html/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= erb(:headers) %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 |
17 | 22 | 23 |
24 | 25 | <%= yieldall %> 26 |
27 | 28 | <%= erb(:footer) %> 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/multi_service.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | 3 | vpc: 4 | max_azs: 2 5 | enable_dns: true 6 | services: 7 | rails_foo: 8 | source_path: /path/to/rails_foo 9 | fargate: 10 | desired_count: 1 11 | public: true 12 | envs: 13 | PORT: 80 14 | db_cluster: 15 | engine: aurora-mysql 16 | db_name: app_development 17 | scaling: 18 | max_capacity: 2 19 | on_cpu: 20 | target_util_percent: 80 21 | scale_in_cool_down: 300 22 | rails_no_db: 23 | source_path: /path/to/no_db 24 | fargate: 25 | desired_count: 1 26 | public: true 27 | envs: 28 | PORT: 80 29 | -------------------------------------------------------------------------------- /examples/no_db.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | vpc: 3 | max_azs: 2 4 | services: 5 | rails_no_db: 6 | source_path: /path/to/rails 7 | fargate: 8 | desired_count: 1 9 | task: 10 | environment: 11 | RAILS_LOG_TO_STDOUT: true 12 | -------------------------------------------------------------------------------- /examples/single_service.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | 3 | vpc: 4 | max_azs: 2 5 | enable_dns: true 6 | services: 7 | rails_foo: 8 | source_path: /path/to/rails_foo 9 | fargate: 10 | desired_count: 5 11 | public: true 12 | db_cluster: 13 | engine: aurora-postgresql 14 | db_name: app_development 15 | cicd: 16 | pipeline_name: RailsFoo 17 | -------------------------------------------------------------------------------- /examples/tiny.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | 3 | vpc: 4 | enable_dns: true 5 | services: 6 | my_rails: 7 | source_path: ./path/to/my/rails 8 | fargate: 9 | public: true 10 | db_cluster: # skip if no db 11 | engine: aurora-postgresql 12 | db_name: app_development 13 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner.rb: -------------------------------------------------------------------------------- 1 | # services 2 | require 'aws-sdk-rds' 3 | 4 | # code gen helpers 5 | require_relative 'aws-rails-provisioner/cdk_builder' 6 | require_relative 'aws-rails-provisioner/cdk_deployer' 7 | require_relative 'aws-rails-provisioner/cdk_code_builder' 8 | 9 | # utils 10 | require_relative 'aws-rails-provisioner/services' 11 | require_relative 'aws-rails-provisioner/service' 12 | require_relative 'aws-rails-provisioner/utils' 13 | require_relative 'aws-rails-provisioner/errors' 14 | 15 | # init stack 16 | require_relative 'aws-rails-provisioner/vpc' 17 | require_relative 'aws-rails-provisioner/subnet_selection' 18 | 19 | # CICD stack 20 | require_relative 'aws-rails-provisioner/code_build' 21 | require_relative 'aws-rails-provisioner/build' 22 | require_relative 'aws-rails-provisioner/migration' 23 | 24 | # fargate stack 25 | require_relative 'aws-rails-provisioner/db_cluster' 26 | require_relative 'aws-rails-provisioner/fargate' 27 | require_relative 'aws-rails-provisioner/scaling' 28 | 29 | # views 30 | require_relative 'aws-rails-provisioner/view' 31 | require_relative 'aws-rails-provisioner/views/init_stack' 32 | require_relative 'aws-rails-provisioner/views/fargate_stack' 33 | require_relative 'aws-rails-provisioner/views/pipeline_stack' 34 | require_relative 'aws-rails-provisioner/views/project' 35 | 36 | # version 37 | require_relative 'aws-rails-provisioner/version' 38 | 39 | module Aws 40 | module RailsProvisioner 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/build.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class Build < Aws::RailsProvisioner::CodeBuild 3 | 4 | # An AWS CodeBuild Project that build, tag and 5 | # push image to AWS ECR Repo 6 | # 7 | # configuration for :build 8 | # 9 | # @param [Hash] options 10 | # 11 | # @option options [String] :project_name name for 12 | # the CodeBuild project, default to 'RailsProvisionerImageBuild' 13 | # 14 | # @option options [String] :description description for this 15 | # CodeBuild project, default to 'build, tag and push image to ECR' 16 | # 17 | # @option options [String] :buildspec buildspec.yml file path, default 18 | # to `buildspec-ecr.yml` under root directory, using template 19 | # under `buildspecs/` 20 | # 21 | # @option options [String] :build_image default to codebuild `ubuntu_14_04_docker_18_09_0` 22 | # full list of supported images see: 23 | # https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-codebuild.LinuxBuildImage.html 24 | # 25 | # @option options [Integer] :timeout number of minutes after which 26 | # CodeBuild stops the build if it’s not complete 27 | # 28 | def initialize(options = {}) 29 | unless options[:description] 30 | options[:description] = 'build, tag and push image to ECR' 31 | end 32 | unless options[:buildspec] 33 | options[:buildspec] = 'buildspec-ecr.yml' 34 | end 35 | unless options[:build_image] 36 | options[:build_image] = 'ubuntu_14_04_docker_18_09_0' 37 | end 38 | super(options) 39 | end 40 | 41 | end 42 | end 43 | 44 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/cdk_builder.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class CDKBuilder 3 | 4 | def initialize(options = {}) 5 | @source_files = options[:source_files] 6 | @default_stack = options[:default_stack] 7 | @default_test = options[:default_test] 8 | @cdk_dir = options[:cdk_dir] 9 | @services = options[:services] 10 | end 11 | 12 | def run 13 | _init_cdk 14 | # code to files 15 | files = @source_files 16 | files.each do |path, code| 17 | if File.exists?(path) 18 | puts "replacing #{path}" 19 | else 20 | puts "creating #{path}" 21 | end 22 | FileUtils.mkdir_p(File.dirname(path)) 23 | File.open(path, 'w') do |f| 24 | f.write(code) 25 | end 26 | end 27 | _install_dependencies 28 | _npm_build 29 | end 30 | 31 | private 32 | 33 | def _init_cdk 34 | unless Dir.exist?(@cdk_dir) 35 | FileUtils.mkdir_p(@cdk_dir) 36 | unless @dryrun 37 | Dir.chdir(@cdk_dir) do 38 | `npm i -g aws-cdk` 39 | `cdk init app --language=typescript` 40 | end 41 | 42 | if File.exists?(@default_stack) 43 | FileUtils.rm_f(@default_stack) 44 | end 45 | if File.exists?(@default_test) 46 | FileUtils.rm_f(@default_test) 47 | end 48 | end 49 | end 50 | end 51 | 52 | def _install_dependencies 53 | pkgs = @services.inject(Set.new) do |set, svc| 54 | set.merge(svc.packages) 55 | set 56 | end 57 | Dir.chdir(@cdk_dir) do 58 | pkgs.each do |pkg| 59 | puts "installing pkg: #{pkg} ..." 60 | `npm install #{pkg}` 61 | end 62 | end 63 | end 64 | 65 | def _npm_build 66 | Dir.chdir(@cdk_dir) do 67 | puts "running npm run build ..." 68 | `npm run build --verbose` 69 | end 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/cdk_code_builder.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Aws::RailsProvisioner 4 | class CDKCodeBuilder 5 | 6 | # Generates CDK projects with separate stacks: 7 | # 8 | # * InitStack - VPC and Fargate Cluster 9 | # * FargetStack - Fargate Service, AutoScaling, ALB 10 | # * PipelineStack - CICD CodePipline (when enabled) that: 11 | # * Source: CodeCommit Repository when host rails applications 12 | # * Build : CodeBuild Project that takes care of image build, tag(commit id) and push to ECR 13 | # * Migration: CodeBuild Project that run migration inside DB subnet (optional) 14 | # * Deploy: CodeDeploy that deploys tagged(commit id) image from ECR to Fargate service 15 | # 16 | # @param [Hash] options 17 | # 18 | # @option options [String] :cdk_dir Directory path for 19 | # generated CDK code, defaults to "cdk-sample" 20 | # 21 | # @option options [Hash] :vpc VPC configurations 22 | # @see {Aws::RailsProvisioner::Vpc} 23 | # 24 | # @option options [Hash] :services 25 | # 26 | # @option options [Hash] :db_cluster DB cluster configurations 27 | # @see {Aws::RailsProvisioner::DBCluster} 28 | # 29 | # @option options [required, String] :source_path path to the directory containing 30 | # Rails Application source 31 | # 32 | # @option options [Hash] :fargate Fargate service configurations 33 | # @example: at `aws-rails-provisioner.yml` 34 | # fargate: 35 | # desired_count: 5 36 | # task: 37 | # environment: 38 | # RAILS_LOG_TO_STDOUT: true 39 | # @see {Aws::RailsProvisioner::Fargate} 40 | # 41 | # @option options [Hash] :scaling Scaling definitions for the Fargate service 42 | # @example: at `aws-rails-provisioner.yml` 43 | # scaling: 44 | # max_capacity: 7 45 | # on_cpu: 46 | # target_util_percent: 40 47 | # on_request: 48 | # requests_per_target: 20000 49 | # @see {Aws::RailsProvisioner::Scaling} 50 | # 51 | # @option options [Hash] :loadbalancer Application Loadbalancer that front 52 | # the Fargate service 53 | # @example: at `aws-rails-provisioner.yml` 54 | # loadbalancer: 55 | # internet_facing: true 56 | # @see {Aws::RailsProvisioner::LoadBalancer} 57 | # 58 | # @option options [Hash] :cicd configurations for CICD that covers: 59 | # source, build, database migration with code pipline 60 | # @see {Aws::RailsProvisioner::Pipeline} 61 | # 62 | def initialize(options = {}) 63 | @cdk_dir = options[:cdk_dir] || "cdk-sample" 64 | @stack_prefix = _stack_prefix 65 | 66 | # init, vpc & fargate cluster 67 | @vpc = options[:vpc] || {} 68 | 69 | # fargate services defs 70 | # including related db, alb, scaling, cicd etc. 71 | @services = Aws::RailsProvisioner::ServiceEnumerator.new(options[:services] || {}) 72 | 73 | # npm cdk service packages need to be installed 74 | @packages = Set.new 75 | end 76 | 77 | # @return [Enumerable] 78 | attr_reader :services 79 | 80 | # @return [Set] 81 | attr_reader :packages 82 | 83 | # @return [String] 84 | attr_reader :cdk_dir 85 | 86 | # @api private 87 | # @return [String] 88 | attr_reader :stack_prefix 89 | 90 | def source_files 91 | Enumerator.new do |y| 92 | y.yield("#{@cdk_dir}/lib/#{@cdk_dir}-init-stack.ts", init_stack) 93 | @services.each do |svc| 94 | y.yield("#{@cdk_dir}/lib/#{svc.path_prefix}-fargate-stack.ts", 95 | svc.fargate_stack) 96 | y.yield("#{@cdk_dir}/lib/#{svc.path_prefix}-pipeline-stack.ts", 97 | svc.pipeline_stack) if svc.enable_cicd 98 | @packages.merge(svc.packages) 99 | end 100 | 101 | y.yield("#{@cdk_dir}/bin/#{@cdk_dir}.ts", project) 102 | end 103 | end 104 | 105 | def default_stack 106 | # CDK init cmd default empty stack 107 | "#{@cdk_dir}/lib/#{@cdk_dir}-stack.ts" 108 | end 109 | 110 | def default_test 111 | # CDK init cmd default test file 112 | "#{@cdk_dir}/test/#{@cdk_dir}.test.ts" 113 | end 114 | 115 | # @api private 116 | def project 117 | Aws::RailsProvisioner::Views::Project.new( 118 | stack_prefix: @stack_prefix, 119 | path_prefix: @cdk_dir, 120 | services: @services 121 | ).render 122 | end 123 | 124 | # @api private 125 | def init_stack 126 | init = Aws::RailsProvisioner::Views::InitStack.new( 127 | vpc: @vpc, 128 | stack_prefix: @stack_prefix 129 | ) 130 | @packages.merge(init.packages) 131 | init.render 132 | end 133 | 134 | private 135 | 136 | def _stack_prefix 137 | dir = @cdk_dir.dup 138 | dir.split('-').map do |part| 139 | part[0] = part[0].upcase 140 | part 141 | end.join 142 | end 143 | 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/cdk_deployer.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Aws::RailsProvisioner 4 | class CDKDeployer 5 | 6 | def initialize(options = {}) 7 | @cdk_dir = options[:cdk_dir] 8 | @services = options[:services] 9 | @stack_prefix = options[:stack_prefix] 10 | 11 | both = options[:fargate].nil? && options[:cicd].nil? 12 | @fargate_stack = both || !!options[:fargate] 13 | @pipeline_stack = both || !!options[:cicd] 14 | 15 | @init_stack_only = !!options[:init] 16 | 17 | @svc_name = options[:service_name] 18 | @profile = options[:profile] 19 | end 20 | 21 | def run 22 | Dir.chdir(@cdk_dir) do 23 | `cdk bootstrap` 24 | deploy_init_stack 25 | unless @init_stack_only 26 | if @svc_name 27 | deploy_svc(@services[@svc_name].stack_prefix) 28 | else 29 | @services.each do |svc| 30 | deploy_svc(svc.stack_prefix) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | 37 | private 38 | 39 | def deploy_init_stack 40 | opts = @profile ? " --profile #{@profile}" : '' 41 | # disable prompts in deploy command 42 | opts += " --require-approval never" 43 | cmd = "cdk deploy #{@stack_prefix}InitStack#{opts}" 44 | Open3.popen3(cmd) do |_, stdout, stderr, _| 45 | puts stdout.read 46 | puts stderr.read 47 | end 48 | end 49 | 50 | def deploy_svc(stack_prefix) 51 | opts = @profile ? " --profile #{@profile}" : '' 52 | # disable prompts in deploy command 53 | opts += " --require-approval never" 54 | if @fargate_stack 55 | `cdk deploy #{stack_prefix}FargateStack#{opts}` 56 | end 57 | if @pipeline_stack 58 | `cdk deploy #{stack_prefix}PipelineStack#{opts}` 59 | end 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/code_build.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class CodeBuild 3 | 4 | def initialize(options = {}) 5 | @project_name = options[:project_name] 6 | @description = options[:description] 7 | @buildspec = options[:buildspec] 8 | @image = options[:build_image].upcase 9 | @timeout = options[:timeout] 10 | end 11 | 12 | # @return [String] 13 | attr_reader :project_name 14 | 15 | # @return [String] 16 | attr_reader :description 17 | 18 | # @return [String] 19 | attr_reader :buildspec 20 | 21 | # @return [Integer] 22 | attr_reader :timeout 23 | 24 | # @return [String] 25 | attr_reader :image 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/db_cluster.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-rds' 2 | 3 | module Aws::RailsProvisioner 4 | class DBCluster 5 | 6 | # Configuration value under :db_cluster 7 | # 8 | # @param [Hash] options 9 | # 10 | # @option options [String] :username DB username, default 11 | # to `SERVICE_NAMEDBAdminUser` 12 | # 13 | # @option options [required, String] :engine provide the engine for 14 | # the database cluster: `aurora`, `aurora-mysql` or `aurora-postgresql` 15 | # 16 | # @option options [String] :engine_version version of the database to start, 17 | # when not provided, default for the engine is used. 18 | # 19 | # @option options [String] :instance_type type of instance to start 20 | # for the replicas, if not provided, a default :instance_type will 21 | # be populated responding to the type of engine provided. 22 | # 23 | # @option options [String] :instance_subnet where to place the instances 24 | # within the VPC, default to `isolated` subnet (recommend) 25 | # 26 | # @option options [Hash] :backup backup config for DB databases 27 | # 28 | # @example: at `aws-rails-provisioner.yml` 29 | # backup: 30 | # retention_days: 7 31 | # preferred_window: '01:00-02:00' 32 | # @see {Aws::RailsProvisioner::DBCluster::BackUp} 33 | # 34 | # @option options [String] :cluster_identifier An optional identifier for 35 | # the cluster, If not supplied, a name is automatically generated 36 | # 37 | # @option options [required, String] :db_name name for the DB inside the cluster 38 | # 39 | # @option options [String] :removal_policy policy to apply when 40 | # the cluster and its instances are removed from the stack or replaced during an update. 41 | # `retain`, `destroy` available, default to `retain` 42 | # 43 | # @option options [String] :instance_identifier every replica is named 44 | # by appending the replica number to this string, default to 'Instance' 45 | # 46 | # @option options [Integer] :instances how many replicas/instances 47 | # to create, defaults to 2 48 | # 49 | # @option options [Integer] :port if not provided, default value is 50 | # based on :engine 51 | # 52 | # @option options [String] :kms_key_arn the he KMS key arn for storage encryption 53 | # 54 | # @option options [String] :preferred_maintenance_window daily time 55 | # range in 24-hours UTC format in which backups preferably execute 56 | # 57 | # @option oprions [Hash] :parameter_group default value is based on engine 58 | # following example shows default for aurora postgres 59 | # 60 | # @example: at `aws-rails-provisioner.yml` 61 | # parameter_group: 62 | # family: 'aurora-postgresql11' 63 | # description: 'created by AWS RailsProvisioner' 64 | # parameters: 65 | # key: value 66 | # @see {Aws::RailsProvisioner::DBCluster::ParameterGroup} 67 | # 68 | # @see AWS CDK DatabaseClusterProps 69 | def initialize(options = {}) 70 | @username = options[:username] || 'DBAdminUser' 71 | 72 | @engine = _engine_type(options[:engine]) 73 | @engine_version = options[:engine_version] 74 | @postgres = @engine == 'AURORA_POSTGRESQL' 75 | unless @postgres 76 | # MySql require username between 1 to 16 77 | @username = options[:username][0..15] 78 | end 79 | 80 | @instance_type = options[:instance_type] || _default_instance_type 81 | @instance_subnet = Aws::RailsProvisioner::Utils.subnet_type( 82 | options[:instance_subnet] || 'isolated') 83 | 84 | @backup = BackUp.new(options[:backup]) if options[:backup] 85 | @db_name = options.fetch(:db_name) 86 | @cluster_identifier = options[:cluster_identifier] 87 | @removal_policy = Aws::RailsProvisioner::Utils.removal_policy( 88 | options[:removal_policy] || 'retain') 89 | 90 | @instance_identifier = options[:instance_identifier] 91 | @instances = options[:instances] || 2 92 | 93 | @kms_key = options[:kms_key_arn] 94 | 95 | @port = options[:port] 96 | @preferred_maintenance_window = options[:preferred_maintenance_window] 97 | 98 | pg_opts = options[:parameter_group] || {} 99 | pg_opts[:profile] = options[:profile] if options[:profile] 100 | pg_opts[:stub_client] = options[:stub_client] # test only 101 | @parameter_group = ParameterGroup.new(@engine, pg_opts) 102 | @db_port = @port || _default_db_port 103 | end 104 | 105 | # @return [Boolean] 106 | attr_reader :postgres 107 | 108 | # @return [String] 109 | attr_reader :username 110 | 111 | # @return [String] 112 | attr_reader :engine 113 | 114 | # @return [String] 115 | attr_reader :engine_version 116 | 117 | # @return [String] 118 | attr_reader :instance_type 119 | 120 | # @return [String] 121 | attr_reader :instance_subnet 122 | 123 | # @return [String] 124 | attr_reader :instance_identifier 125 | 126 | # @return [Integer] 127 | attr_reader :instances 128 | 129 | # @return [String] 130 | attr_reader :db_name 131 | 132 | # @return [String] 133 | attr_reader :cluster_identifier 134 | 135 | # @return [String] 136 | attr_reader :removal_policy 137 | 138 | # @return [String] 139 | attr_reader :kms_key 140 | 141 | # @return [Integer] 142 | attr_reader :port 143 | 144 | # @return [String] 145 | attr_reader :preferred_maintenance_window 146 | 147 | # @return [ParameterGroup] 148 | attr_reader :parameter_group 149 | 150 | # @return [BackUp] 151 | attr_reader :backup 152 | 153 | # @return [Integer] 154 | attr_reader :db_port 155 | 156 | class BackUp 157 | 158 | # @param [Hash] options 159 | # 160 | # @option options [required, Integer] :retention_days 161 | # days to retain the backup 162 | # 163 | # @option options [String] :preferred_window A daily 164 | # time range in 24-hours UTC format in which backups 165 | # preferably execute 166 | # 167 | def initialize(options = {}) 168 | @retention_days = options[:retention_days] 169 | @preferred_window = options[:preferred_window] 170 | end 171 | 172 | # @return [Integer] 173 | attr_reader :retention_days 174 | 175 | # @return [String] 176 | attr_reader :preferred_window 177 | 178 | end 179 | 180 | class ParameterGroup 181 | 182 | # @param [Hash] options 183 | # 184 | # @option options [String] :family 185 | # 186 | # @option options [String] :description 187 | # 188 | # @option options [Hash] :parameters 189 | # 190 | def initialize(engine, options = {}) 191 | # client 192 | @profile = options[:profile] 193 | 194 | @engine = engine 195 | @family = options[:family] || _default_family 196 | @description = options[:description] || 'created by AWS RailsProvisioner' 197 | @cfn = !!options[:parameters] 198 | unless @cfn 199 | suffix = @engine.downcase.gsub(/_/, '-') 200 | @name = "aws-rails-provisioner-default-#{suffix}" 201 | _create_default_pg(options[:stub_client]) 202 | else 203 | @parameters = Aws::RailsProvisioner::Utils.to_pairs(options[:parameters]) 204 | end 205 | end 206 | 207 | # @return [Boolean] 208 | attr_reader :cfn 209 | 210 | # @return [String] 211 | attr_reader :name 212 | 213 | # @return [String] 214 | attr_reader :family 215 | 216 | # @return [String] 217 | attr_reader :description 218 | 219 | # @return [Array] 220 | attr_reader :parameters 221 | 222 | private 223 | 224 | def _default_family 225 | case @engine 226 | when 'AURORA_POSTGRESQL' then 'aurora-postgresql11' 227 | when 'AURORA_MYSQL' then 'aurora-mysql5.7' 228 | when 'AURORA' then 'aurora5.6' 229 | else 230 | msg = 'Failed to locate a default family type for :engine'\ 231 | ' provided, please provide :family for :parameter_group' 232 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 233 | end 234 | end 235 | 236 | # CDK creation requires parameters input 237 | def _create_default_pg(stub_client) 238 | if stub_client 239 | rds = Aws::RDS::Client.new(stub_responses: true) 240 | else 241 | rds = @profile ? Aws::RDS::Client.new(profile: @profile) : 242 | Aws::RDS::Client.new 243 | end 244 | 245 | begin 246 | rds.create_db_cluster_parameter_group( 247 | db_parameter_group_family: @family, 248 | description: @description, 249 | db_cluster_parameter_group_name: @name 250 | ) 251 | rescue Aws::RDS::Errors::DBParameterGroupAlreadyExists 252 | # Cluster Parameter Group already exists 253 | # do nothing 254 | end 255 | end 256 | 257 | end 258 | 259 | private 260 | 261 | def _engine_type(engine) 262 | type = engine.dup 263 | case type.downcase 264 | when 'aurora-postgresql' then 'AURORA_POSTGRESQL' 265 | when 'aurora-mysql' then 'AURORA_MYSQL' 266 | when 'aurora' then 'AURORA' 267 | else 268 | msg = "DB engine: #{engine.inspect} not supported" 269 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 270 | end 271 | end 272 | 273 | def _default_instance_type 274 | case @engine 275 | when 'AURORA_POSTGRESQL' then 'r4.large' 276 | when 'AURORA_MYSQL' then 'r5.large' 277 | when 'AURORA' then 'r5.large' 278 | else 279 | msg = 'Failed to locate a default instance type for :engine'\ 280 | ' provided, please provide :instance_type' 281 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 282 | end 283 | end 284 | 285 | def _default_db_port 286 | case @engine 287 | when 'AURORA_POSTGRESQL' then 3306 288 | when 'AURORA_MYSQL' then 3306 289 | when 'AURORA' then 3306 290 | else 291 | msg = 'Failed to locate a default db port for :engine'\ 292 | ' provided, please provide :port' 293 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 294 | end 295 | 296 | end 297 | 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/errors.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | module Errors 3 | 4 | class ValidationError < RuntimeError; end 5 | 6 | class InvalidCommandOption < RuntimeError 7 | 8 | def initialize(type, option) 9 | msg = "invalid option: #{option}, #{option} is valid for `#{type}` command." 10 | super(msg) 11 | end 12 | 13 | end 14 | 15 | class InvalidYAMLFile < RuntimeError 16 | 17 | def initialize 18 | msg = "Invalid `aws-rails-provisioner.yml` file provided." 19 | super(msg) 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/fargate.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class Fargate 3 | 4 | # Configuration value under :fargate 5 | # 6 | # @param [Hash] options 7 | # 8 | # @option options [Integer] :desired_count number of 9 | # desired copies of running tasks, default to 2 10 | # 11 | # @option options [Boolean] :public when `true` (default) 12 | # Application Load Balancer will be internet-facing 13 | # 14 | # @option options [String] :domain_name domain name for the service, 15 | # e.g. api.example.com 16 | # 17 | # @option options [String] :domain_zone route53 hosted zone for the domain, 18 | # e.g. "example.com.". 19 | # 20 | # @option options [String] :service_name name for the Fargate service 21 | # 22 | # @option options [Integer] :memory default to 512 (MB) 23 | # 24 | # @option options [Integer] :cpu default to 256 (units) 25 | # 26 | # @option options [Hash] :envs environment variable pairs 27 | # for the container used by this Fargate task 28 | # 29 | # @option options [String] :container_name defaults to `FargateTaskContainer` 30 | # 31 | # @option options [Integer] :container_port defaults to 80 32 | # 33 | # @option options [String] :certificate certificate arn. Certificate Manager 34 | # certificate to associate with the load balancer. Setting this option 35 | # will set the load balancer port to 443. 36 | # 37 | def initialize(options = {}) 38 | # code gen only 39 | @has_db = !!options[:has_db] 40 | 41 | @service_name = options[:service_name] 42 | @desired_count = options[:desired_count] || 2 43 | @public = !!options[:public] 44 | @domain_name = options[:domain_name] 45 | @domain_zone = options[:domain_zone] 46 | @certificate = options[:certificate] 47 | 48 | @memory = options[:memory] || 512 49 | @cpu = options[:cpu] || 256 50 | @envs = Aws::RailsProvisioner::Utils.to_pairs(options[:envs]) if options[:envs] 51 | @container_port = options[:container_port] || 80 52 | @container_name = options[:container_name] || 'FargateTaskContainer' 53 | end 54 | 55 | # @api private 56 | # @return [Boolean] 57 | attr_reader :has_db 58 | 59 | # @return [Integer] 60 | attr_reader :desired_count 61 | 62 | # @return [String] 63 | attr_reader :service_name 64 | 65 | # @return [Boolean] 66 | attr_reader :public 67 | 68 | # @return [String] 69 | attr_reader :domain_name 70 | 71 | # @return [String] 72 | attr_reader :domain_zone 73 | 74 | # @return [String] 75 | attr_reader :certificate 76 | 77 | # @return [Array] 78 | attr_reader :envs 79 | 80 | # @return [Integer] 81 | attr_reader :memory 82 | 83 | # @return [Integer] 84 | attr_reader :cpu 85 | 86 | # @return [Integer] 87 | attr_reader :container_port 88 | 89 | # @return [String] 90 | attr_reader :container_name 91 | 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/migration.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class Migration < Aws::RailsProvisioner::CodeBuild 3 | 4 | # An AWS CodeBuild Project that runs DB migration 5 | # for the Ruby on Rails App inside private subnet of 6 | # the VPC 7 | # 8 | # configuration for :migration 9 | # 10 | # @param [Hash] options 11 | # 12 | # @option options [String] :project_name name for 13 | # the CodeBuild project, default to 'SERVICE_NAMEDBMigration' 14 | # 15 | # @option options [String] :description description for this 16 | # CodeBuild project, default to 'running DB Migration for 17 | # the rails app inside private subnet' 18 | # 19 | # @option options [String] :buildspec buildspec.yml file path, default 20 | # to `buildspec-db.yml` under root directory, using template under 21 | # `buildspecs/` 22 | # 23 | # @option options [String] :build_image default to codebuild `standard_4_0` 24 | # full list of supported images see: 25 | # https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-codebuild.LinuxBuildImage.html 26 | # 27 | # @option options [Integer] :timeout number of minutes after which 28 | # CodeBuild stops the build if it’s not complete 29 | # 30 | def initialize(options = {}) 31 | unless options[:description] 32 | options[:description] = 'running DB Migration for'\ 33 | ' the rails app inside private subnet' 34 | end 35 | unless options[:buildspec] 36 | options[:buildspec] = 'buildspec-db.yml' 37 | end 38 | unless options[:build_image] 39 | options[:build_image] = 'standard_4_0' 40 | end 41 | # TODO envs support? 42 | super(options) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/parser.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Aws::RailsProvisioner 4 | module Parser 5 | 6 | def parse(file_path) 7 | config = YAML.load(File.read(file_path)) 8 | symbolize_keys(config) 9 | end 10 | 11 | private 12 | 13 | def symbolize_keys(hash) 14 | hash.inject({}) do |h, (k,v)| 15 | v = symbolize_keys(v) if v.respond_to?(:keys) 16 | h[k.to_sym] = v 17 | h 18 | end 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/scaling.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class Scaling 3 | 4 | # Configuration for Fargate service scaling 5 | # @param [Hash] options 6 | # 7 | # @option options [required, Integer] :max_capacity maximum capacity to scale to 8 | # 9 | # @option options [Integer] :min_capacity minimum capacity to scale to 10 | # default to 1 11 | # 12 | # @option options [Hash] :on_cpu scale out or in to achieve a 13 | # target CPU utilization 14 | # @example: at `aws-rails-provisioner.yml` 15 | # scaling: 16 | # on_cpu: 17 | # target_util_percent: 80 18 | # scale_in_cool_down: 300 19 | # @see {Aws::RailsProvisioner::Scaling::BaseScaling} 20 | # 21 | # @option options [Hash] :on_memory scale out or in to achieve a 22 | # target memory utilization 23 | # @example: at `aws-rails-provisioner.yml` 24 | # scaling: 25 | # on_memory: 26 | # target_util_percent: 80 27 | # scale_out_cool_down: 200 28 | # @see {Aws::RailsProvisioner::Scaling::BaseScaling} 29 | # 30 | # @option options [Hash] :on_metric scale out or in based on a 31 | # metric value 32 | # @example: at `aws-rails-provisioner.yml` 33 | # on_metric: 34 | # adjustment_type: percentchangeincapacity 35 | # min_adjustment_magnitude: 10 36 | # cooldown: 300 37 | # metric: 38 | # name: foo 39 | # @see {Aws::RailsProvisioner::Scaling::MetricScaling} 40 | # 41 | # @option options [Hash] :on_custom_metric scale out or in to track 42 | # a custom metric 43 | # @example: at `aws-rails-provisioner.yml` 44 | # scaling: 45 | # on_custom_metric: 46 | # target_value: 100 47 | # scale_in_cooldown: 300 48 | # scale_out_cooldown: 500 49 | # metric: 50 | # name: foo 51 | # @see {Aws::RailsProvisioner::Scaling::MetricScaling} 52 | # 53 | # @option options [Hash] :on_request scale out or in to achieve a 54 | # target ALB request count per target 55 | # @example: at `aws-rails-provisioner.yml` 56 | # scaling: 57 | # on_request: 58 | # requests_per_target: 100000 59 | # disable_scale_in: true 60 | # @see {Aws::RailsProvisioner::Scaling::BaseScaling} 61 | # 62 | # @option options [Hash] :on_schedule scale out or in based on time 63 | # @example: at `aws-rails-provisioner.yml` 64 | # scaling: 65 | # on_schedule: 66 | # schedule: 'at(yyyy-mm-ddThh:mm:ss)' 67 | # max_capacity: 10 68 | # min_capacity: 5 69 | # @see {Aws::RailsProvisioner::Scaling::ScheduleScaling} 70 | # 71 | def initialize(options = {}) 72 | @max_capacity = options.fetch(:max_capacity) 73 | @min_capacity = options[:min_capacity] 74 | 75 | @on_cpu = _scaling_props(:cpu, options[:cpu]) 76 | @on_memory = _scaling_props(:memory, options[:memory]) 77 | @on_metric = _scaling_props(:metric, options[:metric]) 78 | @on_request = _scaling_props(:request, options[:request]) 79 | @on_schedule = _scaling_props(:schedule, options[:schedule]) 80 | @to_track_custome_metric = _scaling_props(:custom, options[:custom_metric]) 81 | end 82 | 83 | # @return [Integer] 84 | attr_reader :max_capacity 85 | 86 | # @return [Integer] 87 | attr_reader :min_capacity 88 | 89 | # @return [BaseScaling] 90 | attr_reader :on_cpu 91 | 92 | # @return [BaseScaling] 93 | attr_reader :on_memory 94 | 95 | # @return [BaseScaling] 96 | attr_reader :on_request 97 | 98 | # @return [MetricScaling] 99 | attr_reader :on_metric 100 | 101 | # @return [MetricScaling] 102 | attr_reader :to_track_custom_metric 103 | 104 | # @return [ScheduleScaling] 105 | attr_reader :on_schedule 106 | 107 | private 108 | 109 | def _scaling_props(type, opts) 110 | return if opts.nil? 111 | case type 112 | when :cpu, :memory, :request 113 | BaseScaling.new(type, opts) 114 | when :metric, :custom 115 | MetricScaling.new(type, opts) 116 | when :schedule 117 | ScheduleScaling.new(type, opts) 118 | else 119 | raise Aws::RailsProvisioner::Errors::ValidationError.new( 120 | 'Unsupported Scaling type.') 121 | end 122 | end 123 | 124 | class MetricScaling 125 | 126 | # Configuration for metric scaling policy 127 | # @param [Hash] options 128 | # 129 | # @option options [String] :adjustment_type how the adjustment numbers 130 | # inside 'intervals' are interpreted, available for `:on_metric`, supporting 131 | # types: `change_in_capacity`, `percent_change_in_capacity` or `exact_capacity` 132 | # 133 | # @option options [Integer] :min_adjustment_magnitude available for 134 | # `:on_metric`, when :adjustment_type set to `percentchangeincapacity` 135 | # minimum absolute number to adjust capacity with as result of percentage scaling. 136 | # 137 | # @option options [Integer] :cooldown grace period after scaling activity 138 | # in seconds, available for `:on_metric` 139 | # 140 | # @option options [Array] :scaling_steps intervals for scaling, array of 141 | # Aws::RailsProvisioner::Scaling::MetricScaling::ScalingInterval, available for 142 | # `:on_metric` 143 | # @example: at `aws-rails-provisioner.yml` 144 | # on_metric: 145 | # scaling_steps: 146 | # - 147 | # change: 10 148 | # lower: 30 149 | # upper: 60 150 | # - 151 | # change: 20 152 | # lower: 0 153 | # upper: 20 154 | # @see {Aws::RailsProvisioner::Scaling::MetricScaling::ScalingInterval} 155 | # 156 | # @option options [Hash] :metric 157 | # @example: at `aws-rails-provisioner.yml` 158 | # on_metric: 159 | # metric: 160 | # name: foo 161 | # namespace: bar 162 | # dimensions: 163 | # key:value 164 | # on_custom_metric: 165 | # metric: 166 | # name: baz 167 | # @see {Aws::RailsProvisioner::Scaling::MetricScaling::Metric} 168 | # 169 | # @option options [Integer] :target_value the target value to achieve for the metric 170 | # available for :custom_metric 171 | # 172 | # @option options [Boolean] :disable_scale_in whether scale in by the 173 | # target tracking policy is disabled, available for :custom_metric 174 | # 175 | # @option options [Integer] :scale_in_cooldown period (in seconds) after a scale in activity 176 | # completes before another scale in activity can start, available for :custom_metric 177 | # 178 | # @option options [Integer] :scale_out_cooldown period (in seconds) after a scale out activity 179 | # completes before another scale out activity can start, available for :custom_metric 180 | # 181 | def initialize(type, options) 182 | @metric = Metric.new(type, options) 183 | if type == :custom 184 | @target_value = options[:target_value] 185 | @disable_scale_in = !!options[:disable_scale_in] 186 | @scale_in_cooldown = options[:scale_in_cooldown] 187 | @scale_out_cooldown = options[:scale_out_cooldown] 188 | else # :metric 189 | @scaling_steps = _scaling_steps(options[:scaling_steps] || []) 190 | @cooldown_sec = options[:cooldown] 191 | @adjustment_type = Aws::RailsProvisioner::Utils.adjustment_type( 192 | options[:adjustment_type]) if options[:adjustment_type] 193 | if @adjustment_type == 'PercentChangeInCapacity' 194 | @min_adjustment_magnitude = options[:min_adjustment_magnitude] 195 | end 196 | end 197 | end 198 | 199 | # @return [Metric] 200 | attr_reader :metric 201 | 202 | # @return [Integer] 203 | attr_reader :target_value 204 | 205 | # @return [Boolean] 206 | attr_reader :disable_scale_in 207 | 208 | # @return [Integer] 209 | attr_reader :scale_in_cooldown 210 | 211 | # @return [Integer] 212 | attr_reader :scale_out_cooldown 213 | 214 | # @return [Array] 215 | attr_reader :scaling_steps 216 | 217 | # @return [String] 218 | attr_reader :adjustment_type 219 | 220 | # @return [Integer] 221 | attr_reader :cooldown_sec 222 | 223 | # @return [Integer] 224 | attr_reader :min_adjustment_magnitude 225 | 226 | class ScalingInterval 227 | 228 | # Configuration for each scaling interval in 229 | # scaling steps 230 | # @param [Hash] options 231 | # 232 | # @option options [Integer] :change the capacity adjustment 233 | # to apply in this interval, interpreted differently based on :adjustment_type 234 | # * `changeincapacity` - add the adjustment to the current capacity. The number 235 | # can be positive or negative 236 | # * `percentchangeincapacity` - add or remove the given percentage of the 237 | # current capacity to itself. The number can be in the range [-100..100] 238 | # * `exactcapacity` - set the capacity to this number. The number must be positive 239 | # 240 | # @option options [Integer] :lower lower bound of the interval, scaling 241 | # adjustment will be applied if the metric is higher than this value 242 | # 243 | # @option options [Integer] :upper upper bound of the interval, scaling 244 | # adjustment will be applied if the metric is lower than this value 245 | # 246 | def initialize(options = {}) 247 | @change = options[:change] 248 | @lower = options[:lower] 249 | @upper = options[:upper] 250 | end 251 | 252 | # @return [Integer] 253 | attr_reader :change 254 | 255 | # @return [Integer] 256 | attr_reader :lower 257 | 258 | # @return [Integer] 259 | attr_reader :upper 260 | 261 | end 262 | 263 | class Metric 264 | 265 | # Metric to scale on 266 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html 267 | # @param [Hash] options 268 | # 269 | # @option options [String] :name 270 | # 271 | # @option options [String] :namespace 272 | # 273 | # @option options [Integer] :period 274 | # 275 | # @option options [String] :statistic 276 | # 277 | # @option options [String] :color 278 | # 279 | # @option options [String] :label 280 | # 281 | # @option options [Hash] :dimensions 282 | # 283 | # @option options [String] :unit available unit: 284 | # `Seconds`, `Microseconds`, `Milliseconds` 285 | # `Bytes`, `Kilobytes`, `Megabytes`, `Gigabytes`, `Terabytes` 286 | # `Bits`, `Kilobits`, `Megabits`, `Gigabits`, `Terabits`, 287 | # `Percent`, `Count`, `None`, 288 | # `BytesPerSecond`, `KilobytesPerSecond`, `MegabytesPerSecond`, 289 | # `GigabytesPerSecond`, `TerabytesPerSecond`, `BitsPerSecond`, 290 | # `KilobitsPerSecond`, `MegabitsPerSecond`, `GigabitsPerSecond`, 291 | # `TerabitsPerSecond`, `CountPerSecond` 292 | # 293 | def initialize(type, options = {}) 294 | @name = options.fetch(:name) 295 | @namespace = options.fetch(:namespace) 296 | @color = options[:color] 297 | @dimensions = Aws::RailsProvisioner::Utils.to_pairs( 298 | options[:dimensions]) if options[:dimensions] 299 | @label = options[:label] 300 | @period_sec = options[:period] 301 | @statistic = options[:statistics] 302 | @unit = options[:unit] 303 | end 304 | 305 | # @return [String] 306 | attr_reader :name 307 | 308 | # @return [String] 309 | attr_reader :namespace 310 | 311 | # @return [String] 312 | attr_reader :color 313 | 314 | # @return [String] 315 | attr_reader :dimensions 316 | 317 | # @return [String] 318 | attr_reader :label 319 | 320 | # @return [Integer] 321 | attr_reader :period_sec 322 | 323 | # @return [String] 324 | attr_reader :statistic 325 | 326 | # @return [String] 327 | attr_reader :unit 328 | 329 | end 330 | 331 | def scaling_steps? 332 | @scaling_steps && !@scaling_steps.empty? 333 | end 334 | 335 | private 336 | 337 | def _scaling_steps(steps) 338 | steps.map {|step| ScalingInterval.new(step)} 339 | end 340 | 341 | end 342 | 343 | class BaseScaling 344 | 345 | # Configuration for scaling policy 346 | # @param [Hash] options 347 | # 348 | # @option options [Boolean] :disable_scale_in whether scale in 349 | # by the target tracking policy is disabled, default as `false` 350 | # 351 | # @option options [Integer] :scale_in_cooldown period in seconds 352 | # after a scale in activity completes before another scale in activity 353 | # can start 354 | # 355 | # @option options [Integer] :scale_out_cooldown period in seconds 356 | # after a scale in activity completes before another scale in activity 357 | # can start 358 | # 359 | # @option options [Integer] :target_util_percent available for 360 | # * :on_cpu , target average CPU utilization across the task 361 | # * :on_memory , target average memory utilization across the task 362 | # 363 | # @option options [Integer] :requests_per_target available for 364 | # :on_request, ALB requests per target 365 | # 366 | def initialize(type, options = {}) 367 | @disable_scale_in = !!options[:disable_scale_in] 368 | @scale_in_cooldown = options[:scale_in_cooldown] 369 | @scale_out_cooldown = options[:scale_out_cooldown] 370 | var_name = _type_2_var(type) 371 | instance_variable_set("@#{var_name}", options[var_name.to_sym]) 372 | end 373 | 374 | # @return [Integer] 375 | attr_reader :target_util_percent 376 | 377 | # @return [Integer] 378 | attr_reader :requests_per_target 379 | 380 | # @return [Boolean] 381 | attr_reader :disable_scale_in 382 | 383 | # @return [Integer] 384 | attr_reader :scale_in_cooldown 385 | 386 | # @return [Integer] 387 | attr_reader :scale_out_cooldown 388 | 389 | private 390 | 391 | def _type_2_var(type) 392 | case type 393 | when :cpu, :memory then 'target_util_percent' 394 | when :request then 'requests_per_target' 395 | end 396 | end 397 | 398 | end 399 | 400 | class ScheduleScaling 401 | 402 | # Configurations for scaling policy based on time 403 | # @param [Hash] options 404 | # 405 | # @option options [required, String] :schedule when to perform this action 406 | # support formats: 407 | # * 'at(yyyy-mm-ddThh:mm:ss)' 408 | # * 'rate(value unit)' 409 | # * 'cron(fields)' 410 | # 411 | # @option options [Integer] :max_capacity the new maximum capacity 412 | # 413 | # @option options [Integer] :min_capacity the new minimum capacity 414 | # 415 | # @option options [Integer] :start_time when this scheduled action becomes active 416 | # milliseconds since Epoch time 417 | # 418 | # @option options [Integer] :end_time when this scheduled action expires 419 | # milliseconds since Epoch time 420 | # 421 | def initialize(options = {}) 422 | @schedule = options.fetch(:schedule) 423 | @max_capacity = options[:max_capacity] 424 | @min_capacity = options[:min_capacity] 425 | @start_time = options[:start_time] 426 | @end_time = options[:end_time] 427 | end 428 | 429 | # @return [String] 430 | attr_reader :schedule 431 | 432 | # @return [Integer] 433 | attr_reader :max_capacity 434 | 435 | # @return [Integer] 436 | attr_reader :min_capacity 437 | 438 | # @return [Integer] 439 | attr_reader :start_time 440 | 441 | # @return [Integer] 442 | attr_reader :end_time 443 | 444 | end 445 | 446 | end 447 | 448 | end 449 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/service.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Aws::RailsProvisioner 4 | # @api private 5 | class Service 6 | 7 | def initialize(name, options = {}) 8 | @name = name.to_s 9 | @file_path = options[:file_path] || 'aws-rails-provisioner.yml' 10 | @stack_prefix = _camel_case(@name) 11 | @path_prefix = _path_prefix(@name) 12 | @const_prefix = _const_prefix(@stack_prefix) 13 | @enable_cicd = !!options[:enable_cicd] 14 | @profile = options[:profile] 15 | @packages = Set.new 16 | 17 | # fargate stack 18 | @source_path = options.fetch(:source_path) 19 | @fargate = options[:fargate] || {} 20 | 21 | @db_cluster = options[:db_cluster] 22 | @scaling = options[:scaling] 23 | 24 | # pipeline stack 25 | @cicd = options[:cicd] || {} 26 | end 27 | 28 | # @return [String] 29 | attr_reader :name 30 | 31 | # @return [String] 32 | attr_reader :stack_prefix 33 | 34 | # @return [String] 35 | attr_reader :path_prefix 36 | 37 | # @return [String] 38 | attr_reader :const_prefix 39 | 40 | # @return [Boolean] 41 | attr_reader :enable_cicd 42 | 43 | # @return [Set] 44 | attr_reader :packages 45 | 46 | def fargate_stack 47 | stack = Aws::RailsProvisioner::Views::FargateStack.new( 48 | file_path: @file_path, 49 | service_name: @name, 50 | path_prefix: @path_prefix, 51 | stack_prefix: @stack_prefix, 52 | profile: @profile, 53 | source_path: @source_path, 54 | db_cluster: @db_cluster, 55 | fargate: @fargate, 56 | scaling: @scaling, 57 | ) 58 | @packages.merge(stack.packages) 59 | stack.render 60 | end 61 | 62 | def pipeline_stack 63 | if @db_cluster.nil? || @db_cluster.empty? 64 | @cicd[:skip_migration] = true 65 | end 66 | @cicd[:source_path] = @source_path 67 | @cicd[:stack_prefix] = @stack_prefix 68 | stack = Aws::RailsProvisioner::Views::PipelineStack.new(@cicd) 69 | @packages.merge(stack.packages) 70 | stack.render 71 | end 72 | 73 | private 74 | 75 | def _camel_case(str) 76 | str.split('_').collect(&:capitalize).join 77 | end 78 | 79 | def _path_prefix(str) 80 | str.downcase.gsub('_', '-') 81 | end 82 | 83 | def _const_prefix(str) 84 | dup = str.dup 85 | dup[0] = dup[0].downcase 86 | dup 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/services.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | # @api private 3 | class ServiceEnumerator 4 | 5 | include Enumerable 6 | 7 | def initialize(options = {}) 8 | @configs = options || {} 9 | end 10 | 11 | # @param [String] name 12 | def [](name) 13 | if services.key?(name) 14 | services[name] 15 | else 16 | msg = "unknown service #{name} under :services" 17 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 18 | end 19 | end 20 | alias service [] 21 | 22 | def each(&block) 23 | services.values.each(&block) 24 | end 25 | 26 | private 27 | 28 | def services 29 | @service ||= begin 30 | @configs.inject({}) do |hash, (name, config)| 31 | hash[name] = build_service(name, config) 32 | hash 33 | end 34 | end 35 | end 36 | 37 | def build_service(name, config) 38 | Aws::RailsProvisioner::Service.new(name, config) 39 | end 40 | 41 | end 42 | 43 | Services = ServiceEnumerator.new 44 | end 45 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/subnet_selection.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class SubnetSelection 3 | 4 | def initialize(options = {}) 5 | @name = options[:name] 6 | @type = Aws::RailsProvisioner::Util.subnet_type(options[:type]) if options[:type] 7 | if @name && @type 8 | msg = "At most one of :type and :name can be supplied." 9 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 10 | end 11 | end 12 | 13 | # @return [String] 14 | attr_reader :name 15 | 16 | # @return [String] 17 | attr_reader :type 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/utils.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Aws::RailsProvisioner 4 | # @api private 5 | module Utils 6 | 7 | def self.ip_address_type(str) 8 | types = %w(ipv4, dualstack) 9 | if types.include?(str.downcase) 10 | str.upcase 11 | else 12 | msg = "Unsupported ip address type, please choose from #{types}" 13 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 14 | end 15 | end 16 | 17 | def self.subnet_type(str) 18 | types = %w(isolated private public) 19 | if types.include?(str.downcase) 20 | str.upcase 21 | else 22 | msg = "Unsupported subnet type, please choose from #{types}" 23 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 24 | end 25 | end 26 | 27 | def self.removal_policy(str) 28 | types = %w(retain destroy) 29 | if types.include?(str.downcase) 30 | str.upcase 31 | else 32 | msg = "Unsupported removal policy, please choose from #{types}" 33 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 34 | end 35 | end 36 | 37 | def self.protocol(str) 38 | types = %w(https http tcp tls) 39 | if types.include?(str.downcase) 40 | str.upcase 41 | else 42 | msg = "Unsupported protocol type, please choose from #{types}" 43 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 44 | end 45 | end 46 | 47 | def self.adjustment_type(str) 48 | types = %w(change_in_capacity percent_change_in_capacity exact_capacity) 49 | if types.include?(str.downcase) 50 | str.upcase 51 | else 52 | msg = "Unsupported adjustment type, please choose from #{types}" 53 | raise Aws::RailsProvisioner::Errors::ValidationError, msg 54 | end 55 | end 56 | 57 | def self.parse(file_path) 58 | config = YAML.load(File.read(file_path)) 59 | symbolize_keys(config) 60 | rescue 61 | raise Aws::RailsProvisioner::Errors::InvalidYAMLFile.new 62 | end 63 | 64 | def self.to_pairs(hash) 65 | hash.inject([]) do |arr, (key, value)| 66 | arr << "'#{key}': '#{value}'," 67 | arr 68 | end 69 | end 70 | 71 | def self.to_pkgs(services) 72 | services.inject([]) do |pkgs, svc| 73 | pkgs << "@aws-cdk/aws-#{svc}" 74 | pkgs 75 | end 76 | end 77 | 78 | private 79 | 80 | def self.symbolize_keys(hash) 81 | hash.inject({}) do |h, (k,v)| 82 | v = symbolize_keys(v) if v.respond_to?(:keys) 83 | h[k.to_sym] = v 84 | h 85 | end 86 | end 87 | 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/version.rb: -------------------------------------------------------------------------------- 1 | module Aws 2 | module RailsProvisioner 3 | VERSION = '0.0.1.rc2' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/view.rb: -------------------------------------------------------------------------------- 1 | require 'mustache' 2 | 3 | module Aws::RailsProvisioner 4 | # @api private 5 | class View < Mustache 6 | 7 | TEMPLATE_DIR = File.expand_path('../../../templates', __FILE__) 8 | 9 | def self.inherited(subclass) 10 | parts = subclass.name.split('::') 11 | parts.shift #=> remove AWS 12 | parts.shift #=> remove RailsProvisioner 13 | parts.shift #=> remove Views 14 | path = parts.map { |part| underscore(part) }.join('/') 15 | subclass.template_path = TEMPLATE_DIR 16 | subclass.template_file = "#{TEMPLATE_DIR}/#{path}.mustache" 17 | subclass.raise_on_context_miss = true 18 | end 19 | 20 | private 21 | 22 | def underscore(str) 23 | string = str.dup 24 | string.scan(/[a-z0-9]+|\d+|[A-Z0-9]+[a-z]*/).join('_'.freeze) 25 | string.downcase! 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/views/fargate_stack.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | module Views 3 | 4 | class FargateStack < View 5 | 6 | # Fargate Stack Generation 7 | # 8 | # @param [Hash] options 9 | # 10 | # @option options [String] :source_path relative path from `aws-rails-provisioner.yml` 11 | # to the directory containing Rails Application source 12 | # 13 | # @option options [Hash] :db_cluster 14 | # @see {Aws::RailsProvisioner::DBCluster} 15 | # 16 | # @option options [Hash] :fargate configurations for 17 | # fargate service. 18 | # @see {Aws::RailsProvisioner::Fargate} 19 | # 20 | # @option options [Hash] :scaling configurations for 21 | # scaling setting for Fargate service 22 | # @see {Aws::RailsProvisioner::Scaling} 23 | # 24 | def initialize(options = {}) 25 | # code gen only 26 | @service_name = options[:service_name] 27 | @path_prefix = options[:path_prefix] 28 | @stack_prefix = options[:stack_prefix] 29 | @profile = options[:profile] 30 | 31 | dir = File.dirname(File.expand_path(options[:file_path])) 32 | @source_path = File.join(dir, options[:source_path]) 33 | @rds_config = options[:db_cluster] || {} 34 | @fargate_config = options[:fargate] || {} 35 | @scaling_config = options[:scaling] || {} 36 | end 37 | 38 | # @return [String] 39 | attr_reader :stack_prefix 40 | 41 | # @return [String] 42 | attr_reader :source_path 43 | 44 | def services 45 | base = [ 46 | { abbr: 'ec2', value: 'ec2' }, 47 | { abbr: 'ecs', value: 'ecs' }, 48 | { abbr: 'ecs_patterns', value: 'ecs-patterns' }, 49 | { abbr: 'ecr_assets', value: 'ecr-assets' }, 50 | { abbr: 'rds', value: 'rds' } 51 | ] 52 | if @fargate_config && @fargate_config[:certificate] 53 | base << { abbr: 'certificatemanager', value: 'certificatemanager' } 54 | end 55 | if @scaling_config && 56 | (@scaling_config[:on_metric] || @scaling_config[:on_custom_metric]) 57 | base << { abbr: 'cloudwatch', value: 'cloudwatch' } 58 | end 59 | if @rds_config && !@rds_config.empty? 60 | base << { abbr: 'secretsmanager', value: 'secretsmanager' } 61 | if @rds_config[:kms_key_arn] 62 | base << { abbr: 'kms', value: 'kms'} 63 | end 64 | end 65 | base 66 | end 67 | 68 | def packages 69 | keys = services.map {|svc| svc[:value] } 70 | Aws::RailsProvisioner::Utils.to_pkgs(keys) 71 | end 72 | 73 | def db_cluster 74 | if @rds_config && !@rds_config.empty? 75 | unless @rds_config[:username] 76 | @rds_config[:username] = "#{stack_prefix}DBAdminUser" 77 | end 78 | @rds_config[:profile] = @profile if @profile 79 | Aws::RailsProvisioner::DBCluster.new(@rds_config) 80 | end 81 | end 82 | 83 | def fargate 84 | if @rds_config && !@rds_config.empty? 85 | @fargate_config[:has_db] = true 86 | end 87 | @fargate_config[:service_name] = @stack_prefix 88 | Aws::RailsProvisioner::Fargate.new(@fargate_config) 89 | end 90 | 91 | def scaling 92 | if @scaling_config && !@scaling_config.empty? 93 | Aws::RailsProvisioner::Scaling.new(@scaling_config) 94 | end 95 | end 96 | 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/views/init_stack.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | module Views 3 | class InitStack < View 4 | 5 | # For InitStack Generation 6 | # 7 | # @param [Hash] options 8 | # 9 | # @option options [Hash] :vpc 10 | # @see {Aws::RailsProvisioner::Vpc} 11 | # 12 | def initialize(options = {}) 13 | @vpc_config = options[:vpc] 14 | @stack_prefix = options[:stack_prefix] 15 | end 16 | 17 | # @api private 18 | # @return [String] 19 | attr_reader :stack_prefix 20 | 21 | def services 22 | ['ec2', 'ecs'] 23 | end 24 | 25 | def packages 26 | Aws::RailsProvisioner::Utils.to_pkgs(services) 27 | end 28 | 29 | def vpc 30 | Aws::RailsProvisioner::Vpc.new(@vpc_config) 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/views/pipeline_stack.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | module Views 3 | class PipelineStack < View 4 | 5 | # Pipeline (CICD) Generation under :cicd 6 | # 7 | # @param [Hash] options 8 | # 9 | # @option options [String] :pipeline_name Name for the 10 | # AWS CodePipeline generated 11 | # 12 | # @option options [String] :source_repo CodeCommit Repo 13 | # name for holding your rails app source, default to 14 | # folder name where your rails app lives 15 | # 16 | # @option options [String] :source_description Description for 17 | # the CodeCommit Repo, default to 'created by aws-rails-provisioner' 18 | # 19 | # @option options [Hash] :build configurations for codebuild project 20 | # for building images 21 | # @see {Aws::RailsProvisioner::Build} 22 | # 23 | # @option options [Hash] :migration configuration for db migration 24 | # codebuild, available if :db_cluster is configured 25 | # @see {Aws::RailsProvisioner::Migration} 26 | # 27 | def initialize(options = {}) 28 | @stack_prefix = options[:stack_prefix] 29 | 30 | @pipeline_name = options[:pipeline_name] || "#{@stack_prefix}Pipeline" 31 | @source_repo = options[:source_repo] || _extract_repo_name(options[:source_path]) 32 | @source_description = options[:source_description] || "created by aws-rails-provisioner with AWS CDK for #{@stack_prefix}" 33 | 34 | @build_config = options[:build] || {} 35 | unless @build_config[:project_name] 36 | @build_config[:project_name] = "#{@stack_prefix}ImageBuild" 37 | end 38 | 39 | @skip_migration = options[:skip_migration] || false 40 | unless @skip_migration 41 | @migration_config = options[:migration] || {} 42 | unless @migration_config[:project_name] 43 | @migration_config[:project_name] = "#{@stack_prefix}DBMigration" 44 | end 45 | end 46 | end 47 | 48 | def services 49 | [ 50 | { abbr: 'iam', value: 'iam'}, 51 | { abbr: 'ec2', value: 'ec2'}, 52 | { abbr: 'ecr', value: 'ecr' }, 53 | { abbr: 'ecs', value: 'ecs' }, 54 | { abbr: 'rds', value: 'rds' }, 55 | { abbr: 'codebuild', value: 'codebuild'}, 56 | { abbr: 'codecommit', value: 'codecommit'}, 57 | { abbr: 'codepipeline', value: 'codepipeline' }, 58 | { abbr: 'pipelineactions', value: 'codepipeline-actions'} 59 | ] 60 | end 61 | 62 | def packages 63 | keys = services.map {|svc| svc[:value] } 64 | Aws::RailsProvisioner::Utils.to_pkgs(keys) 65 | end 66 | 67 | # @return [String] 68 | attr_reader :stack_prefix 69 | 70 | # @return [String] 71 | attr_reader :pipeline_name 72 | 73 | # @return [String] 74 | attr_reader :source_repo 75 | 76 | # @return [String] 77 | attr_reader :source_description 78 | 79 | # @return [Aws::RailsProvisioner::Build] 80 | attr_reader :build 81 | 82 | # @return [Aws::RailsProvisioner::Migration] 83 | attr_reader :migration 84 | 85 | # @return [Boolean] 86 | attr_reader :skip_migration 87 | 88 | def build 89 | Aws::RailsProvisioner::Build.new(@build_config) 90 | end 91 | 92 | def migration 93 | if @migration_config 94 | Aws::RailsProvisioner::Migration.new(@migration_config) 95 | end 96 | end 97 | 98 | private 99 | 100 | def _extract_repo_name(path) 101 | path.split('/')[-1] || 'AwsRailsProvisionerRailsAppSource' 102 | end 103 | 104 | end 105 | end 106 | end 107 | 108 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/views/project.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | module Views 3 | # @api private 4 | class Project < View 5 | 6 | def initialize(options = {}) 7 | @services = options[:services] 8 | @stack_prefix = options[:stack_prefix] 9 | @path_prefix = options[:path_prefix] 10 | end 11 | 12 | # @return [String] 13 | attr_reader :stack_prefix 14 | 15 | # @return [String] 16 | attr_reader :path_prefix 17 | 18 | def stacks 19 | stacks = [] 20 | @services.each do |svc| 21 | stacks << { 22 | name: svc.name, 23 | const_prefix: svc.const_prefix, 24 | path_prefix: svc.path_prefix, 25 | stack_prefix: svc.stack_prefix, 26 | enable_cicd: svc.enable_cicd 27 | } 28 | end 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/aws-rails-provisioner/vpc.rb: -------------------------------------------------------------------------------- 1 | module Aws::RailsProvisioner 2 | class Vpc 3 | 4 | SUBNETS_DEFAULTS = { 5 | application: { 6 | cidr_mask: 24, 7 | type: 'private' 8 | }, 9 | ingress: { 10 | cidr_mask: 24, 11 | type: 'public' 12 | }, 13 | database: { 14 | cidr_mask: 28, 15 | type: 'isolated' 16 | } 17 | } 18 | 19 | # Configuration value under :vpc 20 | # @param [Hash] options 21 | # 22 | # @option options [Integer] :max_azs maximum number 23 | # of AZs to use in this region, default to 3 24 | # 25 | # @option options [String] :cidr CIDR range to use for 26 | # the VPC, default to '10.0.0.0/21' 27 | # 28 | # @option options [Hash] :subnets subnets configuration 29 | # to build for each AZ, default to following example: 30 | # 31 | # @example: at `aws-rails-provisioner.yml` 32 | # subnets: 33 | # application: 34 | # cidr_mask: 24 35 | # type: private 36 | # ingress: 37 | # cidr_mask: 24 38 | # type: public 39 | # database: 40 | # cidr_mask: 28 41 | # type: isolate 42 | # 43 | # @option options [Boolean] :enable_dns whether the DNS 44 | # resolution is supported for the VPC, default to `true` 45 | # 46 | # @option options [Integer] :nat_gateways number of NAT Gateways 47 | # to create, default to :maxAz value 48 | # 49 | # @option options [Hash] :nat_gateway_subnets choose the subnets 50 | # that will have NAT Gateway attached, default to public: 51 | # 52 | # @example: at `aws-rails-provisioner.yml` 53 | # nat_gateway_subnets: 54 | # type: public 55 | # 56 | # Note: Either subnet `:type` or `:name` can be provided 57 | # @see {Aws::RailsProvisioner::SubnetSelection} 58 | # 59 | # @see AWS CDK VpcNetworkProps 60 | def initialize(options = {}) 61 | @max_azs = options[:max_azs] || 3 62 | @cidr = options[:cidr] || '10.0.0.0/21' 63 | subnets_config = options[:subnets] || SUBNETS_DEFAULTS 64 | @subnets = subnets_config.map do |name, config| 65 | Subnet.new( 66 | cidr_mask: config[:cidr_mask], 67 | subnet_name: name, 68 | type: config[:type] 69 | ) 70 | end 71 | @enable_dns = options[:enable_dns].nil? ? true : !!options[:enable_dns] 72 | @nat_gateways = options[:nat_gateways] || @max_azs 73 | @nat_gateway_subnets = Aws::RailsProvisioner::SubnetSelection.new(options[:nat_gateway_subnets]) if options[:nat_gateway_subnets] 74 | end 75 | 76 | # @return [Integer] 77 | attr_reader :max_azs 78 | 79 | # @return [Integer] 80 | attr_reader :nat_gateways 81 | 82 | # @return [Aws::RailsProvisioner::SubnetSelection | nil] 83 | attr_reader :nat_gateway_subnets 84 | 85 | # @return [String] 86 | attr_reader :cidr 87 | 88 | # @return [Boolean] 89 | attr_reader :enable_dns 90 | 91 | # @return [Array|nil] 92 | attr_reader :subnets 93 | 94 | class Subnet 95 | 96 | def initialize(options) 97 | @subnet_name = options.fetch(:subnet_name) 98 | @cidr_mask = options.fetch(:cidr_mask) 99 | @type = Aws::RailsProvisioner::Utils.subnet_type(options.fetch(:type)) 100 | end 101 | 102 | attr_reader :subnet_name 103 | 104 | attr_reader :cidr_mask 105 | 106 | attr_reader :type 107 | 108 | end 109 | 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/cdk_code_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | module Aws::RailsProvisioner 4 | describe CDKCodeBuilder do 5 | 6 | it 'supports providing :cdk_dir' do 7 | builder = CDKCodeBuilder.new 8 | expect(builder.cdk_dir).to eq('cdk-sample') 9 | 10 | builder = CDKCodeBuilder.new(cdk_dir: 'foo') 11 | expect(builder.cdk_dir).to eq('foo') 12 | end 13 | 14 | it 'caculates required cdk :packages' do 15 | builder = CDKCodeBuilder.new(SpecHelper.single_service) 16 | builder.source_files.each {|_|} 17 | expect(builder.packages).to contain_exactly( 18 | "@aws-cdk/aws-ec2", "@aws-cdk/aws-ecs", 19 | "@aws-cdk/aws-ecr", "@aws-cdk/aws-ecr-assets", 20 | "@aws-cdk/aws-ecs-patterns", "@aws-cdk/aws-rds", 21 | "@aws-cdk/aws-secretsmanager", "@aws-cdk/aws-codebuild", 22 | "@aws-cdk/aws-codecommit", "@aws-cdk/aws-codepipeline", 23 | "@aws-cdk/aws-codepipeline-actions", "@aws-cdk/aws-iam" 24 | ) 25 | end 26 | 27 | it 'supports single fargate service' do 28 | allow(File).to receive(:expand_path).with('aws-rails-provisioner.yml').and_return('/absolute/path/to/dir/aws-rails-provisioner.yml') 29 | builder = CDKCodeBuilder.new(SpecHelper.single_service) 30 | expected = SpecHelper.cdk_single_service 31 | 32 | expect(builder.services.to_a.size).to eq(1) 33 | generate_code_as_expected(builder, expected) 34 | end 35 | 36 | it 'supports rails without db' do 37 | allow(File).to receive(:expand_path).with('aws-rails-provisioner.yml').and_return('/absolute/path/to/dir/aws-rails-provisioner.yml') 38 | builder = CDKCodeBuilder.new(SpecHelper.no_db) 39 | expected = SpecHelper.cdk_no_db 40 | generate_code_as_expected(builder, expected) 41 | end 42 | 43 | it 'supports multiple fargate services' do 44 | allow(File).to receive(:expand_path).with('aws-rails-provisioner.yml').and_return('/absolute/path/to/dir/aws-rails-provisioner.yml') 45 | builder = CDKCodeBuilder.new(SpecHelper.multi_service) 46 | expected = SpecHelper.cdk_multi_service 47 | 48 | expect(builder.services.to_a.size).to eq(2) 49 | generate_code_as_expected(builder, expected) 50 | end 51 | 52 | private 53 | 54 | def generate_code_as_expected(builder, expected) 55 | expect(builder.project).to eq(expected[:app]) 56 | expect(builder.init_stack).to eq(expected[:init]) 57 | builder.services.each do |svc| 58 | expect(svc.fargate_stack).to eq( 59 | expected[:services][svc.path_prefix][:fargate]) 60 | expect(svc.pipeline_stack).to eq( 61 | expected[:services][svc.path_prefix][:pipeline]) 62 | end 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/cdk-sample-init-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | 5 | export class CdkSampleInitStack extends cdk.Stack { 6 | public readonly vpc: ec2.IVpc; 7 | public readonly cluster: ecs.ICluster; 8 | 9 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | // Setting up VPC with subnets 13 | const vpc = new ec2.Vpc(this, 'Vpc', { 14 | maxAzs: 2, 15 | cidr: '10.0.0.0/21', 16 | enableDnsSupport: true, 17 | natGateways: 2, 18 | subnetConfiguration: [ 19 | { 20 | cidrMask: 24, 21 | name: 'application', 22 | subnetType: ec2.SubnetType.PRIVATE 23 | }, 24 | { 25 | cidrMask: 24, 26 | name: 'ingress', 27 | subnetType: ec2.SubnetType.PUBLIC 28 | }, 29 | { 30 | cidrMask: 28, 31 | name: 'database', 32 | subnetType: ec2.SubnetType.ISOLATED 33 | }, 34 | ] 35 | }); 36 | this.vpc = vpc; 37 | 38 | this.cluster = new ecs.Cluster(this, 'FargateCluster', { 39 | vpc: vpc 40 | }); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/cdk-sample.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cdk = require('@aws-cdk/core'); 4 | import { CdkSampleInitStack } from '../lib/cdk-sample-init-stack'; 5 | import { RailsFooFargateStack } from '../lib/rails-foo-fargate-stack'; 6 | import { RailsFooPipelineStack } from '../lib/rails-foo-pipeline-stack'; 7 | import { RailsNoDbFargateStack } from '../lib/rails-no-db-fargate-stack'; 8 | import { RailsNoDbPipelineStack } from '../lib/rails-no-db-pipeline-stack'; 9 | 10 | const app = new cdk.App(); 11 | const initStack = new CdkSampleInitStack(app, 'CdkSampleInitStack'); 12 | 13 | // for service :rails_foo 14 | const railsFooFargateStack = new RailsFooFargateStack(app, 'RailsFooFargateStack', { 15 | vpc: initStack.vpc, 16 | cluster: initStack.cluster 17 | }); 18 | 19 | new RailsFooPipelineStack(app, 'RailsFooPipelineStack', { 20 | vpc: initStack.vpc, 21 | dbUrl: railsFooFargateStack.dbUrl, 22 | db: railsFooFargateStack.db, 23 | repoName: railsFooFargateStack.repoName, 24 | service: railsFooFargateStack.service 25 | }); 26 | 27 | // for service :rails_no_db 28 | const railsNoDbFargateStack = new RailsNoDbFargateStack(app, 'RailsNoDbFargateStack', { 29 | vpc: initStack.vpc, 30 | cluster: initStack.cluster 31 | }); 32 | 33 | new RailsNoDbPipelineStack(app, 'RailsNoDbPipelineStack', { 34 | vpc: initStack.vpc, 35 | dbUrl: railsNoDbFargateStack.dbUrl, 36 | db: railsNoDbFargateStack.db, 37 | repoName: railsNoDbFargateStack.repoName, 38 | service: railsNoDbFargateStack.service 39 | }); 40 | 41 | app.synth(); 42 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/rails-foo-fargate-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | import ecs_patterns = require('@aws-cdk/aws-ecs-patterns'); 5 | import ecr_assets = require('@aws-cdk/aws-ecr-assets'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import secretsmanager = require('@aws-cdk/aws-secretsmanager'); 8 | 9 | interface RailsFooFargateStackProps { 10 | vpc: ec2.IVpc, 11 | cluster: ecs.ICluster, 12 | } 13 | 14 | export class RailsFooFargateStack extends cdk.Stack { 15 | public readonly service: ecs.FargateService; 16 | public readonly repoName: string; 17 | public readonly dbUrl: string; 18 | public readonly db: rds.DatabaseCluster; 19 | 20 | constructor(scope: cdk.App, id: string, props: RailsFooFargateStackProps) { 21 | super(scope, id); 22 | 23 | // import resources 24 | const cluster = props.cluster; 25 | const vpc = props.vpc; 26 | 27 | // Create secret from SecretsManager 28 | const username = 'RailsFooDBAdminU'; 29 | const secret = new secretsmanager.Secret(this, 'Secret', { 30 | generateSecretString: { 31 | excludePunctuation: true 32 | } 33 | }); 34 | const password = secret.secretValue; 35 | 36 | // Import DB cluster ParameterGroup 37 | const parameterGroup = rds.ParameterGroup.fromParameterGroupName( 38 | this, 'DBClusterPG', 'aws-rails-provisioner-default-aurora-mysql'); 39 | // Create DB Cluster 40 | const db = new rds.DatabaseCluster(this, 'DBCluster', { 41 | engine: rds.DatabaseClusterEngine.AURORA_MYSQL, 42 | masterUser: { 43 | username: username, 44 | password: password 45 | }, 46 | instanceProps: { 47 | instanceType: new ec2.InstanceType('r5.large'), 48 | vpc: vpc, 49 | vpcSubnets: { 50 | subnetType: ec2.SubnetType.ISOLATED 51 | } 52 | }, 53 | defaultDatabaseName: 'app_development', 54 | removalPolicy: cdk.RemovalPolicy.RETAIN, 55 | instances: 2, 56 | parameterGroup: parameterGroup 57 | }); 58 | const dbUrl = "mysql2://" + username + ":" + password + "@" + db.clusterEndpoint.socketAddress + "/app_development"; 59 | this.dbUrl = dbUrl; 60 | 61 | const asset = new ecr_assets.DockerImageAsset(this, 'ImageAssetBuild', { 62 | directory: '/absolute/path/to/dir/path/to/rails_foo' 63 | }); 64 | 65 | // compute repo name from asset image 66 | const parts = asset.imageUri.split("@")[0].split("/"); 67 | const repoName = parts.slice(1, parts.length).join("/").split(":")[0]; 68 | this.repoName = repoName; 69 | 70 | const image = ecs.ContainerImage.fromDockerImageAsset(asset); 71 | 72 | // Fargate service 73 | const lbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LBFargate', { 74 | serviceName: 'RailsFoo', 75 | cluster: cluster, 76 | taskImageOptions: { 77 | image: image, 78 | containerName: 'FargateTaskContainer', 79 | containerPort: 80, 80 | environment: { 81 | 'DATABASE_URL': dbUrl, 82 | 'PORT': '80', 83 | }, 84 | enableLogging: true, 85 | }, 86 | memoryLimitMiB: 512, 87 | cpu: 256, 88 | desiredCount: 1, 89 | publicLoadBalancer: true, 90 | assignPublicIp: true 91 | }); 92 | db.connections.allowDefaultPortFrom(lbFargate.service, 'From Fargate'); 93 | this.db = db; 94 | 95 | const scaling = lbFargate.service.autoScaleTaskCount({ 96 | maxCapacity: 2, 97 | }); 98 | 99 | this.service = lbFargate.service; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/rails-foo-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import iam = require('@aws-cdk/aws-iam'); 3 | import ec2 = require('@aws-cdk/aws-ec2'); 4 | import ecr = require('@aws-cdk/aws-ecr'); 5 | import ecs = require('@aws-cdk/aws-ecs'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import codebuild = require('@aws-cdk/aws-codebuild'); 8 | import codecommit = require('@aws-cdk/aws-codecommit'); 9 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 10 | import pipelineactions = require('@aws-cdk/aws-codepipeline-actions'); 11 | 12 | interface RailsFooPipelineStackProps { 13 | vpc: ec2.IVpc, 14 | dbUrl: string, 15 | repoName: string, 16 | service: ecs.FargateService, 17 | db: rds.DatabaseCluster 18 | } 19 | 20 | export class RailsFooPipelineStack extends cdk.Stack { 21 | constructor(scope: cdk.App, id: string, props: RailsFooPipelineStackProps) { 22 | super(scope, id); 23 | 24 | const pipeline = new codepipeline.Pipeline(this, 'FargatePipeline', { 25 | pipelineName: 'RailsFooPipeline', 26 | }); 27 | 28 | const repo = new codecommit.Repository(this, 'CodeCommitRepo', { 29 | repositoryName: 'rails_foo', 30 | description: 'created by aws-rails-provisioner with AWS CDK for RailsFoo' 31 | }); 32 | 33 | const sourceOutput = new codepipeline.Artifact(); 34 | const sourceStage = pipeline.addStage({ 35 | stageName: 'Source', 36 | actions: [ 37 | new pipelineactions.CodeCommitSourceAction({ 38 | actionName: 'SourceAction', 39 | repository: repo, 40 | output: sourceOutput 41 | }) 42 | ] 43 | }); 44 | 45 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'ImageRepo', props.repoName); 46 | 47 | const role = new iam.Role(this, 'ImageBuildRole', { 48 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com') 49 | }); 50 | const policy = new iam.PolicyStatement(); 51 | policy.addAllResources(); 52 | policy.addActions( 53 | "ecr:BatchCheckLayerAvailability", 54 | "ecr:CompleteLayerUpload", 55 | "ecr:GetAuthorizationToken", 56 | "ecr:InitiateLayerUpload", 57 | "ecr:PutImage", 58 | "ecr:UploadLayerPart" 59 | ); 60 | role.addToPolicy(policy); 61 | 62 | const build = new codebuild.PipelineProject(this, 'ImageBuildToECR', { 63 | projectName: 'RailsFooImageBuild', 64 | description: 'build, tag and push image to ECR', 65 | environmentVariables: { 66 | 'REPO_NAME': { 67 | value: ecrRepo.repositoryName, 68 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 69 | }, 70 | 'REPO_PREFIX': { 71 | value: ecrRepo.repositoryUri, 72 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 73 | }, 74 | }, 75 | environment: { 76 | buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_DOCKER_18_09_0, 77 | privileged: true 78 | }, 79 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-ecr.yml'), 80 | role: role 81 | }); 82 | 83 | const buildOutput = new codepipeline.Artifact(); 84 | const buildStage = pipeline.addStage({ 85 | stageName: 'Build', 86 | placement: { 87 | justAfter: sourceStage 88 | }, 89 | actions: [ 90 | new pipelineactions.CodeBuildAction({ 91 | actionName: 'ImageBuildAction', 92 | input: sourceOutput, 93 | outputs: [ buildOutput ], 94 | project: build 95 | }) 96 | ] 97 | }); 98 | 99 | const migration = new codebuild.PipelineProject(this, 'DBMigration', { 100 | projectName: 'RailsFooDBMigration', 101 | description: 'running DB Migration for the rails app inside private subnet', 102 | environmentVariables: { 103 | 'DATABASE_URL': { 104 | value: props.dbUrl, 105 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 106 | } 107 | }, 108 | environment:{ 109 | buildImage: codebuild.LinuxBuildImage.STANDARD_4_0 110 | }, 111 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-db.yml'), 112 | vpc: props.vpc, 113 | subnetSelection: { 114 | subnetType: ec2.SubnetType.PRIVATE 115 | } 116 | }); 117 | migration.connections.allowToDefaultPort(props.db, 'DB Migration CodeBuild'); 118 | 119 | const migrationStage = pipeline.addStage({ 120 | stageName: 'DBMigration', 121 | placement: { 122 | justAfter: buildStage 123 | }, 124 | actions: [ 125 | new pipelineactions.CodeBuildAction({ 126 | actionName: 'DBMigrationAction', 127 | project: migration, 128 | input: sourceOutput 129 | }) 130 | ] 131 | }); 132 | 133 | pipeline.addStage({ 134 | stageName: 'Deploy', 135 | placement: { 136 | justAfter: migrationStage 137 | }, 138 | actions: [ 139 | new pipelineactions.EcsDeployAction({ 140 | actionName: 'FargateDeployAction', 141 | service: props.service, 142 | input: buildOutput 143 | }) 144 | ] 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/rails-no-db-fargate-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | import ecs_patterns = require('@aws-cdk/aws-ecs-patterns'); 5 | import ecr_assets = require('@aws-cdk/aws-ecr-assets'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | 8 | interface RailsNoDbFargateStackProps { 9 | vpc: ec2.IVpc, 10 | cluster: ecs.ICluster, 11 | } 12 | 13 | export class RailsNoDbFargateStack extends cdk.Stack { 14 | public readonly service: ecs.FargateService; 15 | public readonly repoName: string; 16 | public readonly dbUrl: string; 17 | public readonly db: rds.DatabaseCluster; 18 | 19 | constructor(scope: cdk.App, id: string, props: RailsNoDbFargateStackProps) { 20 | super(scope, id); 21 | 22 | // import resources 23 | const cluster = props.cluster; 24 | 25 | const asset = new ecr_assets.DockerImageAsset(this, 'ImageAssetBuild', { 26 | directory: '/absolute/path/to/dir/path/to/no_db' 27 | }); 28 | 29 | // compute repo name from asset image 30 | const parts = asset.imageUri.split("@")[0].split("/"); 31 | const repoName = parts.slice(1, parts.length).join("/").split(":")[0]; 32 | this.repoName = repoName; 33 | 34 | const image = ecs.ContainerImage.fromDockerImageAsset(asset); 35 | 36 | // Fargate service 37 | const lbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LBFargate', { 38 | serviceName: 'RailsNoDb', 39 | cluster: cluster, 40 | taskImageOptions: { 41 | image: image, 42 | containerName: 'FargateTaskContainer', 43 | containerPort: 80, 44 | environment: { 45 | 'PORT': '80', 46 | }, 47 | enableLogging: true, 48 | }, 49 | memoryLimitMiB: 512, 50 | cpu: 256, 51 | desiredCount: 1, 52 | publicLoadBalancer: true, 53 | assignPublicIp: true 54 | }); 55 | this.service = lbFargate.service; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/multi_service/rails-no-db-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import iam = require('@aws-cdk/aws-iam'); 3 | import ec2 = require('@aws-cdk/aws-ec2'); 4 | import ecr = require('@aws-cdk/aws-ecr'); 5 | import ecs = require('@aws-cdk/aws-ecs'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import codebuild = require('@aws-cdk/aws-codebuild'); 8 | import codecommit = require('@aws-cdk/aws-codecommit'); 9 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 10 | import pipelineactions = require('@aws-cdk/aws-codepipeline-actions'); 11 | 12 | interface RailsNoDbPipelineStackProps { 13 | vpc: ec2.IVpc, 14 | dbUrl: string, 15 | repoName: string, 16 | service: ecs.FargateService, 17 | db: rds.DatabaseCluster 18 | } 19 | 20 | export class RailsNoDbPipelineStack extends cdk.Stack { 21 | constructor(scope: cdk.App, id: string, props: RailsNoDbPipelineStackProps) { 22 | super(scope, id); 23 | 24 | const pipeline = new codepipeline.Pipeline(this, 'FargatePipeline', { 25 | pipelineName: 'RailsNoDbPipeline', 26 | }); 27 | 28 | const repo = new codecommit.Repository(this, 'CodeCommitRepo', { 29 | repositoryName: 'no_db', 30 | description: 'created by aws-rails-provisioner with AWS CDK for RailsNoDb' 31 | }); 32 | 33 | const sourceOutput = new codepipeline.Artifact(); 34 | const sourceStage = pipeline.addStage({ 35 | stageName: 'Source', 36 | actions: [ 37 | new pipelineactions.CodeCommitSourceAction({ 38 | actionName: 'SourceAction', 39 | repository: repo, 40 | output: sourceOutput 41 | }) 42 | ] 43 | }); 44 | 45 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'ImageRepo', props.repoName); 46 | 47 | const role = new iam.Role(this, 'ImageBuildRole', { 48 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com') 49 | }); 50 | const policy = new iam.PolicyStatement(); 51 | policy.addAllResources(); 52 | policy.addActions( 53 | "ecr:BatchCheckLayerAvailability", 54 | "ecr:CompleteLayerUpload", 55 | "ecr:GetAuthorizationToken", 56 | "ecr:InitiateLayerUpload", 57 | "ecr:PutImage", 58 | "ecr:UploadLayerPart" 59 | ); 60 | role.addToPolicy(policy); 61 | 62 | const build = new codebuild.PipelineProject(this, 'ImageBuildToECR', { 63 | projectName: 'RailsNoDbImageBuild', 64 | description: 'build, tag and push image to ECR', 65 | environmentVariables: { 66 | 'REPO_NAME': { 67 | value: ecrRepo.repositoryName, 68 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 69 | }, 70 | 'REPO_PREFIX': { 71 | value: ecrRepo.repositoryUri, 72 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 73 | }, 74 | }, 75 | environment: { 76 | buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_DOCKER_18_09_0, 77 | privileged: true 78 | }, 79 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-ecr.yml'), 80 | role: role 81 | }); 82 | 83 | const buildOutput = new codepipeline.Artifact(); 84 | const buildStage = pipeline.addStage({ 85 | stageName: 'Build', 86 | placement: { 87 | justAfter: sourceStage 88 | }, 89 | actions: [ 90 | new pipelineactions.CodeBuildAction({ 91 | actionName: 'ImageBuildAction', 92 | input: sourceOutput, 93 | outputs: [ buildOutput ], 94 | project: build 95 | }) 96 | ] 97 | }); 98 | 99 | pipeline.addStage({ 100 | stageName: 'Deploy', 101 | placement: { 102 | justAfter: buildStage 103 | }, 104 | actions: [ 105 | new pipelineactions.EcsDeployAction({ 106 | actionName: 'FargateDeployAction', 107 | service: props.service, 108 | input: buildOutput 109 | }) 110 | ] 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/no_db/cdk-sample-init-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | 5 | export class CdkSampleInitStack extends cdk.Stack { 6 | public readonly vpc: ec2.IVpc; 7 | public readonly cluster: ecs.ICluster; 8 | 9 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | // Setting up VPC with subnets 13 | const vpc = new ec2.Vpc(this, 'Vpc', { 14 | maxAzs: 2, 15 | cidr: '10.0.0.0/21', 16 | enableDnsSupport: true, 17 | natGateways: 2, 18 | subnetConfiguration: [ 19 | { 20 | cidrMask: 24, 21 | name: 'application', 22 | subnetType: ec2.SubnetType.PRIVATE 23 | }, 24 | { 25 | cidrMask: 24, 26 | name: 'ingress', 27 | subnetType: ec2.SubnetType.PUBLIC 28 | }, 29 | { 30 | cidrMask: 28, 31 | name: 'database', 32 | subnetType: ec2.SubnetType.ISOLATED 33 | }, 34 | ] 35 | }); 36 | this.vpc = vpc; 37 | 38 | this.cluster = new ecs.Cluster(this, 'FargateCluster', { 39 | vpc: vpc 40 | }); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/no_db/cdk-sample.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cdk = require('@aws-cdk/core'); 4 | import { CdkSampleInitStack } from '../lib/cdk-sample-init-stack'; 5 | import { RailsNoDbFargateStack } from '../lib/rails-no-db-fargate-stack'; 6 | import { RailsNoDbPipelineStack } from '../lib/rails-no-db-pipeline-stack'; 7 | 8 | const app = new cdk.App(); 9 | const initStack = new CdkSampleInitStack(app, 'CdkSampleInitStack'); 10 | 11 | // for service :rails_no_db 12 | const railsNoDbFargateStack = new RailsNoDbFargateStack(app, 'RailsNoDbFargateStack', { 13 | vpc: initStack.vpc, 14 | cluster: initStack.cluster 15 | }); 16 | 17 | new RailsNoDbPipelineStack(app, 'RailsNoDbPipelineStack', { 18 | vpc: initStack.vpc, 19 | dbUrl: railsNoDbFargateStack.dbUrl, 20 | db: railsNoDbFargateStack.db, 21 | repoName: railsNoDbFargateStack.repoName, 22 | service: railsNoDbFargateStack.service 23 | }); 24 | 25 | app.synth(); 26 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/no_db/rails-no-db-fargate-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | import ecs_patterns = require('@aws-cdk/aws-ecs-patterns'); 5 | import ecr_assets = require('@aws-cdk/aws-ecr-assets'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | 8 | interface RailsNoDbFargateStackProps { 9 | vpc: ec2.IVpc, 10 | cluster: ecs.ICluster, 11 | } 12 | 13 | export class RailsNoDbFargateStack extends cdk.Stack { 14 | public readonly service: ecs.FargateService; 15 | public readonly repoName: string; 16 | public readonly dbUrl: string; 17 | public readonly db: rds.DatabaseCluster; 18 | 19 | constructor(scope: cdk.App, id: string, props: RailsNoDbFargateStackProps) { 20 | super(scope, id); 21 | 22 | // import resources 23 | const cluster = props.cluster; 24 | 25 | const asset = new ecr_assets.DockerImageAsset(this, 'ImageAssetBuild', { 26 | directory: '/absolute/path/to/dir/path/to/rails' 27 | }); 28 | 29 | // compute repo name from asset image 30 | const parts = asset.imageUri.split("@")[0].split("/"); 31 | const repoName = parts.slice(1, parts.length).join("/").split(":")[0]; 32 | this.repoName = repoName; 33 | 34 | const image = ecs.ContainerImage.fromDockerImageAsset(asset); 35 | 36 | // Fargate service 37 | const lbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LBFargate', { 38 | serviceName: 'RailsNoDb', 39 | cluster: cluster, 40 | taskImageOptions: { 41 | image: image, 42 | containerName: 'FargateTaskContainer', 43 | containerPort: 80, 44 | environment: { 45 | 'RAILS_LOG_TO_STDOUT': 'true', 46 | }, 47 | enableLogging: true, 48 | }, 49 | memoryLimitMiB: 512, 50 | cpu: 256, 51 | desiredCount: 1, 52 | }); 53 | this.service = lbFargate.service; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/no_db/rails-no-db-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import iam = require('@aws-cdk/aws-iam'); 3 | import ec2 = require('@aws-cdk/aws-ec2'); 4 | import ecr = require('@aws-cdk/aws-ecr'); 5 | import ecs = require('@aws-cdk/aws-ecs'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import codebuild = require('@aws-cdk/aws-codebuild'); 8 | import codecommit = require('@aws-cdk/aws-codecommit'); 9 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 10 | import pipelineactions = require('@aws-cdk/aws-codepipeline-actions'); 11 | 12 | interface RailsNoDbPipelineStackProps { 13 | vpc: ec2.IVpc, 14 | dbUrl: string, 15 | repoName: string, 16 | service: ecs.FargateService, 17 | db: rds.DatabaseCluster 18 | } 19 | 20 | export class RailsNoDbPipelineStack extends cdk.Stack { 21 | constructor(scope: cdk.App, id: string, props: RailsNoDbPipelineStackProps) { 22 | super(scope, id); 23 | 24 | const pipeline = new codepipeline.Pipeline(this, 'FargatePipeline', { 25 | pipelineName: 'RailsNoDbPipeline', 26 | }); 27 | 28 | const repo = new codecommit.Repository(this, 'CodeCommitRepo', { 29 | repositoryName: 'rails', 30 | description: 'created by aws-rails-provisioner with AWS CDK for RailsNoDb' 31 | }); 32 | 33 | const sourceOutput = new codepipeline.Artifact(); 34 | const sourceStage = pipeline.addStage({ 35 | stageName: 'Source', 36 | actions: [ 37 | new pipelineactions.CodeCommitSourceAction({ 38 | actionName: 'SourceAction', 39 | repository: repo, 40 | output: sourceOutput 41 | }) 42 | ] 43 | }); 44 | 45 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'ImageRepo', props.repoName); 46 | 47 | const role = new iam.Role(this, 'ImageBuildRole', { 48 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com') 49 | }); 50 | const policy = new iam.PolicyStatement(); 51 | policy.addAllResources(); 52 | policy.addActions( 53 | "ecr:BatchCheckLayerAvailability", 54 | "ecr:CompleteLayerUpload", 55 | "ecr:GetAuthorizationToken", 56 | "ecr:InitiateLayerUpload", 57 | "ecr:PutImage", 58 | "ecr:UploadLayerPart" 59 | ); 60 | role.addToPolicy(policy); 61 | 62 | const build = new codebuild.PipelineProject(this, 'ImageBuildToECR', { 63 | projectName: 'RailsNoDbImageBuild', 64 | description: 'build, tag and push image to ECR', 65 | environmentVariables: { 66 | 'REPO_NAME': { 67 | value: ecrRepo.repositoryName, 68 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 69 | }, 70 | 'REPO_PREFIX': { 71 | value: ecrRepo.repositoryUri, 72 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 73 | }, 74 | }, 75 | environment: { 76 | buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_DOCKER_18_09_0, 77 | privileged: true 78 | }, 79 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-ecr.yml'), 80 | role: role 81 | }); 82 | 83 | const buildOutput = new codepipeline.Artifact(); 84 | const buildStage = pipeline.addStage({ 85 | stageName: 'Build', 86 | placement: { 87 | justAfter: sourceStage 88 | }, 89 | actions: [ 90 | new pipelineactions.CodeBuildAction({ 91 | actionName: 'ImageBuildAction', 92 | input: sourceOutput, 93 | outputs: [ buildOutput ], 94 | project: build 95 | }) 96 | ] 97 | }); 98 | 99 | pipeline.addStage({ 100 | stageName: 'Deploy', 101 | placement: { 102 | justAfter: buildStage 103 | }, 104 | actions: [ 105 | new pipelineactions.EcsDeployAction({ 106 | actionName: 'FargateDeployAction', 107 | service: props.service, 108 | input: buildOutput 109 | }) 110 | ] 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/single_service/cdk-sample-init-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | 5 | export class CdkSampleInitStack extends cdk.Stack { 6 | public readonly vpc: ec2.IVpc; 7 | public readonly cluster: ecs.ICluster; 8 | 9 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | // Setting up VPC with subnets 13 | const vpc = new ec2.Vpc(this, 'Vpc', { 14 | maxAzs: 2, 15 | cidr: '10.0.0.0/21', 16 | enableDnsSupport: true, 17 | natGateways: 2, 18 | subnetConfiguration: [ 19 | { 20 | cidrMask: 24, 21 | name: 'application', 22 | subnetType: ec2.SubnetType.PRIVATE 23 | }, 24 | { 25 | cidrMask: 24, 26 | name: 'ingress', 27 | subnetType: ec2.SubnetType.PUBLIC 28 | }, 29 | { 30 | cidrMask: 28, 31 | name: 'database', 32 | subnetType: ec2.SubnetType.ISOLATED 33 | }, 34 | ] 35 | }); 36 | this.vpc = vpc; 37 | 38 | this.cluster = new ecs.Cluster(this, 'FargateCluster', { 39 | vpc: vpc 40 | }); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/single_service/cdk-sample.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cdk = require('@aws-cdk/core'); 4 | import { CdkSampleInitStack } from '../lib/cdk-sample-init-stack'; 5 | import { RailsFooFargateStack } from '../lib/rails-foo-fargate-stack'; 6 | import { RailsFooPipelineStack } from '../lib/rails-foo-pipeline-stack'; 7 | 8 | const app = new cdk.App(); 9 | const initStack = new CdkSampleInitStack(app, 'CdkSampleInitStack'); 10 | 11 | // for service :rails_foo 12 | const railsFooFargateStack = new RailsFooFargateStack(app, 'RailsFooFargateStack', { 13 | vpc: initStack.vpc, 14 | cluster: initStack.cluster 15 | }); 16 | 17 | new RailsFooPipelineStack(app, 'RailsFooPipelineStack', { 18 | vpc: initStack.vpc, 19 | dbUrl: railsFooFargateStack.dbUrl, 20 | db: railsFooFargateStack.db, 21 | repoName: railsFooFargateStack.repoName, 22 | service: railsFooFargateStack.service 23 | }); 24 | 25 | app.synth(); 26 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/single_service/rails-foo-fargate-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import ec2 = require('@aws-cdk/aws-ec2'); 3 | import ecs = require('@aws-cdk/aws-ecs'); 4 | import ecs_patterns = require('@aws-cdk/aws-ecs-patterns'); 5 | import ecr_assets = require('@aws-cdk/aws-ecr-assets'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import secretsmanager = require('@aws-cdk/aws-secretsmanager'); 8 | 9 | interface RailsFooFargateStackProps { 10 | vpc: ec2.IVpc, 11 | cluster: ecs.ICluster, 12 | } 13 | 14 | export class RailsFooFargateStack extends cdk.Stack { 15 | public readonly service: ecs.FargateService; 16 | public readonly repoName: string; 17 | public readonly dbUrl: string; 18 | public readonly db: rds.DatabaseCluster; 19 | 20 | constructor(scope: cdk.App, id: string, props: RailsFooFargateStackProps) { 21 | super(scope, id); 22 | 23 | // import resources 24 | const cluster = props.cluster; 25 | const vpc = props.vpc; 26 | 27 | // Create secret from SecretsManager 28 | const username = 'RailsFooDBAdminUser'; 29 | const secret = new secretsmanager.Secret(this, 'Secret', { 30 | generateSecretString: { 31 | excludePunctuation: true 32 | } 33 | }); 34 | const password = secret.secretValue; 35 | 36 | // Import DB cluster ParameterGroup 37 | const parameterGroup = rds.ParameterGroup.fromParameterGroupName( 38 | this, 'DBClusterPG', 'aws-rails-provisioner-default-aurora-postgresql'); 39 | // Create DB Cluster 40 | const db = new rds.DatabaseCluster(this, 'DBCluster', { 41 | engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL, 42 | masterUser: { 43 | username: username, 44 | password: password 45 | }, 46 | instanceProps: { 47 | instanceType: new ec2.InstanceType('r4.large'), 48 | vpc: vpc, 49 | vpcSubnets: { 50 | subnetType: ec2.SubnetType.ISOLATED 51 | } 52 | }, 53 | defaultDatabaseName: 'app_development', 54 | removalPolicy: cdk.RemovalPolicy.RETAIN, 55 | instances: 2, 56 | parameterGroup: parameterGroup 57 | }); 58 | const dbUrl = "postgres://" + username + ":" + password + "@" + db.clusterEndpoint.socketAddress + "/app_development"; 59 | this.dbUrl = dbUrl; 60 | 61 | const asset = new ecr_assets.DockerImageAsset(this, 'ImageAssetBuild', { 62 | directory: '/absolute/path/to/dir/path/to/rails_foo' 63 | }); 64 | 65 | // compute repo name from asset image 66 | const parts = asset.imageUri.split("@")[0].split("/"); 67 | const repoName = parts.slice(1, parts.length).join("/").split(":")[0]; 68 | this.repoName = repoName; 69 | 70 | const image = ecs.ContainerImage.fromDockerImageAsset(asset); 71 | 72 | // Fargate service 73 | const lbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LBFargate', { 74 | serviceName: 'RailsFoo', 75 | cluster: cluster, 76 | taskImageOptions: { 77 | image: image, 78 | containerName: 'FargateTaskContainer', 79 | containerPort: 80, 80 | environment: { 81 | 'DATABASE_URL': dbUrl, 82 | }, 83 | enableLogging: true, 84 | }, 85 | memoryLimitMiB: 512, 86 | cpu: 256, 87 | desiredCount: 5, 88 | publicLoadBalancer: true, 89 | assignPublicIp: true 90 | }); 91 | db.connections.allowDefaultPortFrom(lbFargate.service, 'From Fargate'); 92 | this.db = db; 93 | this.service = lbFargate.service; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /spec/fixtures/cdk/single_service/rails-foo-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | import iam = require('@aws-cdk/aws-iam'); 3 | import ec2 = require('@aws-cdk/aws-ec2'); 4 | import ecr = require('@aws-cdk/aws-ecr'); 5 | import ecs = require('@aws-cdk/aws-ecs'); 6 | import rds = require('@aws-cdk/aws-rds'); 7 | import codebuild = require('@aws-cdk/aws-codebuild'); 8 | import codecommit = require('@aws-cdk/aws-codecommit'); 9 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 10 | import pipelineactions = require('@aws-cdk/aws-codepipeline-actions'); 11 | 12 | interface RailsFooPipelineStackProps { 13 | vpc: ec2.IVpc, 14 | dbUrl: string, 15 | repoName: string, 16 | service: ecs.FargateService, 17 | db: rds.DatabaseCluster 18 | } 19 | 20 | export class RailsFooPipelineStack extends cdk.Stack { 21 | constructor(scope: cdk.App, id: string, props: RailsFooPipelineStackProps) { 22 | super(scope, id); 23 | 24 | const pipeline = new codepipeline.Pipeline(this, 'FargatePipeline', { 25 | pipelineName: 'RailsFoo', 26 | }); 27 | 28 | const repo = new codecommit.Repository(this, 'CodeCommitRepo', { 29 | repositoryName: 'rails_foo', 30 | description: 'created by aws-rails-provisioner with AWS CDK for RailsFoo' 31 | }); 32 | 33 | const sourceOutput = new codepipeline.Artifact(); 34 | const sourceStage = pipeline.addStage({ 35 | stageName: 'Source', 36 | actions: [ 37 | new pipelineactions.CodeCommitSourceAction({ 38 | actionName: 'SourceAction', 39 | repository: repo, 40 | output: sourceOutput 41 | }) 42 | ] 43 | }); 44 | 45 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'ImageRepo', props.repoName); 46 | 47 | const role = new iam.Role(this, 'ImageBuildRole', { 48 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com') 49 | }); 50 | const policy = new iam.PolicyStatement(); 51 | policy.addAllResources(); 52 | policy.addActions( 53 | "ecr:BatchCheckLayerAvailability", 54 | "ecr:CompleteLayerUpload", 55 | "ecr:GetAuthorizationToken", 56 | "ecr:InitiateLayerUpload", 57 | "ecr:PutImage", 58 | "ecr:UploadLayerPart" 59 | ); 60 | role.addToPolicy(policy); 61 | 62 | const build = new codebuild.PipelineProject(this, 'ImageBuildToECR', { 63 | projectName: 'RailsFooImageBuild', 64 | description: 'build, tag and push image to ECR', 65 | environmentVariables: { 66 | 'REPO_NAME': { 67 | value: ecrRepo.repositoryName, 68 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 69 | }, 70 | 'REPO_PREFIX': { 71 | value: ecrRepo.repositoryUri, 72 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 73 | }, 74 | }, 75 | environment: { 76 | buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_DOCKER_18_09_0, 77 | privileged: true 78 | }, 79 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-ecr.yml'), 80 | role: role 81 | }); 82 | 83 | const buildOutput = new codepipeline.Artifact(); 84 | const buildStage = pipeline.addStage({ 85 | stageName: 'Build', 86 | placement: { 87 | justAfter: sourceStage 88 | }, 89 | actions: [ 90 | new pipelineactions.CodeBuildAction({ 91 | actionName: 'ImageBuildAction', 92 | input: sourceOutput, 93 | outputs: [ buildOutput ], 94 | project: build 95 | }) 96 | ] 97 | }); 98 | 99 | const migration = new codebuild.PipelineProject(this, 'DBMigration', { 100 | projectName: 'RailsFooDBMigration', 101 | description: 'running DB Migration for the rails app inside private subnet', 102 | environmentVariables: { 103 | 'DATABASE_URL': { 104 | value: props.dbUrl, 105 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 106 | } 107 | }, 108 | environment:{ 109 | buildImage: codebuild.LinuxBuildImage.STANDARD_4_0 110 | }, 111 | buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec-db.yml'), 112 | vpc: props.vpc, 113 | subnetSelection: { 114 | subnetType: ec2.SubnetType.PRIVATE 115 | } 116 | }); 117 | migration.connections.allowToDefaultPort(props.db, 'DB Migration CodeBuild'); 118 | 119 | const migrationStage = pipeline.addStage({ 120 | stageName: 'DBMigration', 121 | placement: { 122 | justAfter: buildStage 123 | }, 124 | actions: [ 125 | new pipelineactions.CodeBuildAction({ 126 | actionName: 'DBMigrationAction', 127 | project: migration, 128 | input: sourceOutput 129 | }) 130 | ] 131 | }); 132 | 133 | pipeline.addStage({ 134 | stageName: 'Deploy', 135 | placement: { 136 | justAfter: migrationStage 137 | }, 138 | actions: [ 139 | new pipelineactions.EcsDeployAction({ 140 | actionName: 'FargateDeployAction', 141 | service: props.service, 142 | input: buildOutput 143 | }) 144 | ] 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /spec/fixtures/yml/multi_service.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | 3 | vpc: 4 | max_azs: 2 5 | enable_dns: true 6 | services: 7 | rails_foo: 8 | source_path: /path/to/rails_foo 9 | fargate: 10 | desired_count: 1 11 | public: true 12 | envs: 13 | PORT: 80 14 | db_cluster: 15 | stub_client: true 16 | engine: aurora-mysql 17 | db_name: app_development 18 | scaling: 19 | max_capacity: 2 20 | on_cpu: 21 | target_util_percent: 80 22 | scale_in_cool_down: 300 23 | rails_no_db: 24 | source_path: /path/to/no_db 25 | fargate: 26 | desired_count: 1 27 | public: true 28 | envs: 29 | PORT: 80 30 | -------------------------------------------------------------------------------- /spec/fixtures/yml/no_db.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | vpc: 3 | max_azs: 2 4 | services: 5 | rails_no_db: 6 | source_path: /path/to/rails 7 | fargate: 8 | desired_count: 1 9 | envs: 10 | RAILS_LOG_TO_STDOUT: true 11 | -------------------------------------------------------------------------------- /spec/fixtures/yml/single_service.yml: -------------------------------------------------------------------------------- 1 | version: '0' 2 | 3 | vpc: 4 | max_azs: 2 5 | enable_dns: true 6 | services: 7 | rails_foo: 8 | source_path: /path/to/rails_foo 9 | fargate: 10 | desired_count: 5 11 | public: true 12 | db_cluster: 13 | stub_client: true 14 | engine: aurora-postgresql 15 | db_name: app_development 16 | cicd: 17 | pipeline_name: RailsFoo 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | $:.unshift(File.expand_path('../../lib', __FILE__)) 4 | require 'aws-rails-provisioner' 5 | 6 | module SpecHelper 7 | class << self 8 | 9 | YML_DIR = File.join([ 10 | File.dirname(__FILE__), 11 | 'fixtures', 12 | 'yml' 13 | ]) 14 | 15 | CDK_DIR = File.join([ 16 | File.dirname(__FILE__), 17 | 'fixtures', 18 | 'cdk' 19 | ]) 20 | 21 | def single_service 22 | config = yml_fixtures('single_service') 23 | config[:services][:rails_foo][:enable_cicd] = true 24 | config 25 | end 26 | 27 | def cdk_single_service 28 | cdk_fixtures('single_service') 29 | end 30 | 31 | def multi_service 32 | config = yml_fixtures('multi_service') 33 | config[:services][:rails_foo][:enable_cicd] = true 34 | config[:services][:rails_no_db][:enable_cicd] = true 35 | config 36 | end 37 | 38 | def cdk_multi_service 39 | cdk_fixtures('multi_service') 40 | end 41 | 42 | def no_db 43 | config = yml_fixtures('no_db') 44 | config[:services][:rails_no_db][:enable_cicd] = true 45 | config 46 | end 47 | 48 | def cdk_no_db 49 | cdk_fixtures('no_db') 50 | end 51 | 52 | private 53 | 54 | def yml_fixtures(suite) 55 | Aws::RailsProvisioner::Utils.parse("#{YML_DIR}/#{suite}.yml") 56 | end 57 | 58 | def cdk_fixtures(suite) 59 | Dir.glob("#{CDK_DIR}/#{suite}/*").inject({}) do |h, path| 60 | file = path.split("#{suite}/")[-1] 61 | if file == "cdk-sample.ts" 62 | h[:app] = File.read(path) 63 | elsif file == "cdk-sample-init-stack.ts" 64 | h[:init] = File.read(path) 65 | else 66 | h[:services] ||= {} 67 | if file.include?("fargate-stack.ts") 68 | svc = file.split("-fargate-stack.ts")[0] 69 | h[:services][svc] ||= {} 70 | h[:services][svc][:fargate] = File.read(path) 71 | else 72 | svc = file.split("-pipeline-stack.ts")[0] 73 | h[:services][svc] ||= {} 74 | h[:services][svc][:pipeline] = File.read(path) 75 | end 76 | end 77 | 78 | h 79 | end 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /templates/fargate_stack.mustache: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | {{#services}} 3 | import {{abbr}} = require('@aws-cdk/aws-{{value}}'); 4 | {{/services}} 5 | 6 | interface {{stack_prefix}}FargateStackProps { 7 | vpc: ec2.IVpc, 8 | cluster: ecs.ICluster, 9 | } 10 | 11 | export class {{stack_prefix}}FargateStack extends cdk.Stack { 12 | public readonly service: ecs.FargateService; 13 | public readonly repoName: string; 14 | public readonly dbUrl: string; 15 | public readonly db: rds.DatabaseCluster; 16 | 17 | constructor(scope: cdk.App, id: string, props: {{stack_prefix}}FargateStackProps) { 18 | super(scope, id); 19 | 20 | // import resources 21 | const cluster = props.cluster; 22 | {{#db_cluster}} 23 | const vpc = props.vpc; 24 | 25 | // Create secret from SecretsManager 26 | const username = '{{username}}'; 27 | const secret = new secretsmanager.Secret(this, 'Secret', { 28 | generateSecretString: { 29 | excludePunctuation: true 30 | } 31 | }); 32 | const password = secret.secretValue; 33 | 34 | {{#parameter_group}} 35 | {{#cfn}} 36 | // Create DB Cluster ParameterGroup 37 | const parameterGroup = new rds.ParameterGroup(this, 'DBClusterPG', { 38 | description: '{{description}}', 39 | family: '{{family}}', 40 | parameters: { 41 | {{#parameters}} 42 | {{{.}}} 43 | {{/parameters}} 44 | } 45 | }); 46 | {{/cfn}} 47 | {{#name}} 48 | // Import DB cluster ParameterGroup 49 | const parameterGroup = rds.ParameterGroup.fromParameterGroupName( 50 | this, 'DBClusterPG', '{{.}}'); 51 | {{/name}} 52 | {{/parameter_group}} 53 | // Create DB Cluster 54 | const db = new rds.DatabaseCluster(this, 'DBCluster', { 55 | engine: rds.DatabaseClusterEngine.{{engine}}, 56 | {{#engine_version}} 57 | engineVersion: '{{.}}', 58 | {{/engine_version}} 59 | masterUser: { 60 | username: username, 61 | password: password 62 | }, 63 | instanceProps: { 64 | instanceType: new ec2.InstanceType('{{instance_type}}'), 65 | vpc: vpc, 66 | vpcSubnets: { 67 | subnetType: ec2.SubnetType.{{instance_subnet}} 68 | } 69 | }, 70 | {{#kms_key}} 71 | kmsKey: kms.Key.fromKeyArn(this, 'DBKMSKey', '{{.}}'), 72 | {{/kms_key}} 73 | {{#backup}} 74 | backup: { 75 | {{#retention_days}} 76 | retentionDays: {{.}}, 77 | {{/retention_days}} 78 | {{#preferred_window}} 79 | preferred_window: '{{.}}', 80 | {{/preferred_window}} 81 | }, 82 | {{/backup}} 83 | defaultDatabaseName: '{{db_name}}', 84 | {{#cluster_identifier}} 85 | clusterIdentifier: '{{.}}', 86 | {{/cluster_identifier}} 87 | {{#removal_policy}} 88 | removalPolicy: cdk.RemovalPolicy.{{.}}, 89 | {{/removal_policy}} 90 | {{#instance_identifier}} 91 | instanceIdentifierBase: '{{.}}', 92 | {{/instance_identifier}} 93 | instances: {{instances}}, 94 | parameterGroup: parameterGroup 95 | }); 96 | const dbUrl = {{#postgres}}"postgres://"{{/postgres}}{{^postgres}}"mysql2://"{{/postgres}} + username + ":" + password + "@" + db.clusterEndpoint.socketAddress + "/{{db_name}}"; 97 | this.dbUrl = dbUrl; 98 | {{/db_cluster}} 99 | 100 | const asset = new ecr_assets.DockerImageAsset(this, 'ImageAssetBuild', { 101 | directory: '{{source_path}}' 102 | }); 103 | 104 | // compute repo name from asset image 105 | const parts = asset.imageUri.split("@")[0].split("/"); 106 | const repoName = parts.slice(1, parts.length).join("/").split(":")[0]; 107 | this.repoName = repoName; 108 | 109 | const image = ecs.ContainerImage.fromDockerImageAsset(asset); 110 | 111 | {{#fargate}} 112 | // Fargate service 113 | const lbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'LBFargate', { 114 | {{#service_name}} 115 | serviceName: '{{.}}', 116 | {{/service_name}} 117 | cluster: cluster, 118 | taskImageOptions: { 119 | image: image, 120 | containerName: '{{container_name}}', 121 | containerPort: {{container_port}}, 122 | environment: { 123 | {{#has_db}} 124 | 'DATABASE_URL': dbUrl, 125 | {{/has_db}} 126 | {{#envs}} 127 | {{{.}}} 128 | {{/envs}} 129 | }, 130 | enableLogging: true, 131 | }, 132 | {{#certificate}} 133 | certificate: certificatemanager.Certificate.fromCertificateArn(this, 'Certificate', '{{.}}'), 134 | {{/certificate}} 135 | {{#memory}}memoryLimitMiB: {{.}},{{/memory}} 136 | {{#cpu}}cpu: {{.}},{{/cpu}} 137 | desiredCount: {{desired_count}}, 138 | {{#domain_name}} 139 | domainName: '{{.}}', 140 | {{/domain_name}} 141 | {{#domain_zone}} 142 | domainZone: '{{.}}', 143 | {{/domain_zone}} 144 | {{#public}} 145 | publicLoadBalancer: true, 146 | assignPublicIp: true 147 | {{/public}} 148 | }); 149 | {{#has_db}} 150 | db.connections.allowDefaultPortFrom(lbFargate.service, 'From Fargate'); 151 | this.db = db; 152 | {{/has_db}} 153 | {{/fargate}} 154 | {{#scaling}} 155 | 156 | const scaling = lbFargate.service.autoScaleTaskCount({ 157 | maxCapacity: {{max_capacity}}, 158 | {{#min_capacity}} 159 | minCapacity: {{.}} 160 | {{/min_capacity}} 161 | }); 162 | {{#on_cpu}} 163 | scaling.scaleOnCpuUtilization('FargateScalingOnCpu', { 164 | targetUtilizationPercent: {{target_util_percent}}, 165 | {{#disable_scale_in}} 166 | disableScaleIn: true, 167 | {{/disable_scale_in}} 168 | {{#scale_in_cooldown}} 169 | scaleInCooldownSec: {{.}}, 170 | {{/scale_in_cooldown}} 171 | {{#scale_out_cooldown}} 172 | scaleOutCooldownSec: {{.}}, 173 | {{/scale_out_cooldown}} 174 | }); 175 | {{/on_cpu}} 176 | {{#on_memory}} 177 | scaling.scaleOnMemoryUtilization('FargateScalingOnMemory', { 178 | targetUtilizationPercent: {{target_util_percent}}, 179 | {{#disable_scale_in}} 180 | disableScaleIn: true, 181 | {{/disable_scale_in}} 182 | {{#scale_in_cooldown}} 183 | scaleInCooldownSec: {{.}}, 184 | {{/scale_in_cooldown}} 185 | {{#scale_out_cooldown}} 186 | scaleOutCooldownSec: {{.}}, 187 | {{/scale_out_cooldown}} 188 | }); 189 | {{/on_memory}} 190 | {{#on_metric}} 191 | {{#metric}} 192 | const metric = new cloudwatch.Metric('Metric', { 193 | metricName: {{name}}, 194 | metricNamespace: {{namespace}}, 195 | {{#color}} 196 | color: '{{.}}', 197 | {{/color}} 198 | dimensions: { 199 | {{#dimensions}} 200 | {{{.}}} 201 | {{/dimensions}} 202 | } 203 | {{#label}} 204 | label: '{{.}}', 205 | {{/label}} 206 | {{#period_sec}} 207 | periodSec: {{.}}, 208 | {{/period_sec}} 209 | {{#statistic}} 210 | statistic: {{.}}, 211 | {{/statistic}} 212 | {{#unit}} 213 | unit: cloudwatch.Unit.{{.}}, 214 | {{/unit}} 215 | }); 216 | {{/metric}} 217 | scaling.scaleOnMetric('FargateScalingOnMetric', { 218 | metric: metric, 219 | {{#scaling_steps?}} 220 | scalingSteps: [ 221 | {{#scaling_steps}} 222 | { 223 | {{#change}}change: {{.}},{{/change}} 224 | {{#lower}}lower: {{.}},{{/lower}} 225 | {{#upper}}upper: {{.}}{{/upper}} 226 | }, 227 | {{/scaling_steps}} 228 | ], 229 | {{/scaling_steps?}} 230 | {{#adjustment_type}} 231 | adjustmentType: applicationautoscaling.{{.}}, 232 | {{/adjustment_type}} 233 | {{#cooldown_sec}} 234 | cooldownSec: {{.}}, 235 | {{/cooldown_sec}} 236 | {{#min_adjustment_magnitude}} 237 | minAdjustmentMagnitude: {{.}}, 238 | {{/min_adjustment_magnitude}} 239 | }); 240 | {{/on_metric}} 241 | {{#on_request}} 242 | scaling.scaleOnRequestCount('FargateScalingOnRequest', { 243 | requestsPerTarget: {{requests_per_target}}, 244 | targetGroup: target, 245 | {{#disable_scale_in}} 246 | disableScaleIn: true, 247 | {{/disable_scale_in}} 248 | {{#scale_in_cooldown}} 249 | scaleInCooldownSec: {{.}}, 250 | {{/scale_in_cooldown}} 251 | {{#scale_out_cooldown}} 252 | scaleOutCooldownSec: {{.}}, 253 | {{/scale_out_cooldown}} 254 | }); 255 | {{/on_request}} 256 | {{#on_schedule}} 257 | scaling.scaleOnSchedule('FargateScalingOnSchedule', { 258 | schedule: {{{schedule}}}, 259 | {{#max_capacity}} 260 | maxCapacity: {{.}}, 261 | {{/max_capacity}} 262 | {{#min_capacity}} 263 | minCapacity: {{.}}, 264 | {{/min_capacity}} 265 | {{#start_time}} 266 | startTime: new Date({{.}}), 267 | {{/start_time}} 268 | {{#end_time}} 269 | endTime: new Date({{.}}), 270 | {{/end_time}} 271 | }); 272 | {{/on_schedule}} 273 | {{#to_track_custom_metric}} 274 | {{#metric}} 275 | const custom_metric = new cloudwatch.Metric('CustomMetric', { 276 | metricName: {{name}}, 277 | metricNamespace: {{namespace}}, 278 | {{#color}} 279 | color: '{{.}}', 280 | {{/color}} 281 | dimensions: { 282 | {{#dimensions}} 283 | {{{.}}} 284 | {{/dimensions}} 285 | }, 286 | {{#label}} 287 | label: '{{.}}', 288 | {{/label}} 289 | {{#period_sec}} 290 | periodSec: {{.}}, 291 | {{/period_sec}} 292 | {{#statistic}} 293 | statistic: '{{.}}', 294 | {{/statistic}} 295 | {{#unit}} 296 | unit: cloudwatch.Unit.{{.}}, 297 | {{/unit}} 298 | }); 299 | {{/metric}} 300 | scaling.scaleToTrackCustomMetric('FargateScalingOnCustomMetric', { 301 | metric: custom_metric, 302 | {{#disable_scale_in}} 303 | disableScaleIn: true, 304 | {{/disable_scale_in}} 305 | {{#scale_in_cooldown}} 306 | scaleInCooldownSec: {{.}}, 307 | {{/scale_in_cooldown}} 308 | {{#scale_out_cooldown}} 309 | scaleOutCooldownSec: {{.}}, 310 | {{/scale_out_cooldown}} 311 | }); 312 | {{/to_track_custom_metric}} 313 | 314 | {{/scaling}} 315 | this.service = lbFargate.service; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /templates/init_stack.mustache: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | {{#services}} 3 | import {{.}} = require('@aws-cdk/aws-{{.}}'); 4 | {{/services}} 5 | 6 | export class {{stack_prefix}}InitStack extends cdk.Stack { 7 | public readonly vpc: ec2.IVpc; 8 | public readonly cluster: ecs.ICluster; 9 | 10 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | {{#vpc}} 14 | // Setting up VPC with subnets 15 | const vpc = new ec2.Vpc(this, 'Vpc', { 16 | maxAzs: {{max_azs}}, 17 | cidr: '{{cidr}}', 18 | {{#enable_dns}} 19 | enableDnsSupport: true, 20 | {{/enable_dns}} 21 | natGateways: {{nat_gateways}}, 22 | {{#nat_gateway_subnets}} 23 | natGatewaySubnets: { 24 | {{#name}} 25 | subnetName: '{{.}}' 26 | {{/name}} 27 | {{#type}} 28 | subnetType: ec2.SubnetType.{{.}} 29 | {{/type}} 30 | }, 31 | {{/nat_gateway_subnets}} 32 | subnetConfiguration: [ 33 | {{#subnets}} 34 | { 35 | cidrMask: {{cidr_mask}}, 36 | name: '{{subnet_name}}', 37 | subnetType: ec2.SubnetType.{{type}} 38 | }, 39 | {{/subnets}} 40 | ] 41 | }); 42 | this.vpc = vpc; 43 | {{/vpc}} 44 | 45 | this.cluster = new ecs.Cluster(this, 'FargateCluster', { 46 | vpc: vpc 47 | }); 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /templates/pipeline_stack.mustache: -------------------------------------------------------------------------------- 1 | import cdk = require('@aws-cdk/core'); 2 | {{#services}} 3 | import {{abbr}} = require('@aws-cdk/aws-{{value}}'); 4 | {{/services}} 5 | 6 | interface {{stack_prefix}}PipelineStackProps { 7 | vpc: ec2.IVpc, 8 | dbUrl: string, 9 | repoName: string, 10 | service: ecs.FargateService, 11 | db: rds.DatabaseCluster 12 | } 13 | 14 | export class {{stack_prefix}}PipelineStack extends cdk.Stack { 15 | constructor(scope: cdk.App, id: string, props: {{stack_prefix}}PipelineStackProps) { 16 | super(scope, id); 17 | 18 | const pipeline = new codepipeline.Pipeline(this, 'FargatePipeline', { 19 | pipelineName: '{{pipeline_name}}', 20 | }); 21 | 22 | const repo = new codecommit.Repository(this, 'CodeCommitRepo', { 23 | repositoryName: '{{source_repo}}', 24 | description: '{{source_description}}' 25 | }); 26 | 27 | const sourceOutput = new codepipeline.Artifact(); 28 | const sourceStage = pipeline.addStage({ 29 | stageName: 'Source', 30 | actions: [ 31 | new pipelineactions.CodeCommitSourceAction({ 32 | actionName: 'SourceAction', 33 | repository: repo, 34 | output: sourceOutput 35 | }) 36 | ] 37 | }); 38 | 39 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'ImageRepo', props.repoName); 40 | 41 | const role = new iam.Role(this, 'ImageBuildRole', { 42 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com') 43 | }); 44 | const policy = new iam.PolicyStatement(); 45 | policy.addAllResources(); 46 | policy.addActions( 47 | "ecr:BatchCheckLayerAvailability", 48 | "ecr:CompleteLayerUpload", 49 | "ecr:GetAuthorizationToken", 50 | "ecr:InitiateLayerUpload", 51 | "ecr:PutImage", 52 | "ecr:UploadLayerPart" 53 | ); 54 | role.addToPolicy(policy); 55 | 56 | {{#build}} 57 | const build = new codebuild.PipelineProject(this, 'ImageBuildToECR', { 58 | projectName: '{{project_name}}', 59 | description: '{{description}}', 60 | environmentVariables: { 61 | 'REPO_NAME': { 62 | value: ecrRepo.repositoryName, 63 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 64 | }, 65 | 'REPO_PREFIX': { 66 | value: ecrRepo.repositoryUri, 67 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 68 | }, 69 | }, 70 | environment: { 71 | buildImage: codebuild.LinuxBuildImage.{{image}}, 72 | privileged: true 73 | }, 74 | buildSpec: codebuild.BuildSpec.fromSourceFilename('{{buildspec}}'), 75 | {{#timeout}} 76 | timeout: {{.}}, 77 | {{/timeout}} 78 | role: role 79 | }); 80 | {{/build}} 81 | 82 | const buildOutput = new codepipeline.Artifact(); 83 | const buildStage = pipeline.addStage({ 84 | stageName: 'Build', 85 | placement: { 86 | justAfter: sourceStage 87 | }, 88 | actions: [ 89 | new pipelineactions.CodeBuildAction({ 90 | actionName: 'ImageBuildAction', 91 | input: sourceOutput, 92 | outputs: [ buildOutput ], 93 | project: build 94 | }) 95 | ] 96 | }); 97 | {{#migration}} 98 | 99 | const migration = new codebuild.PipelineProject(this, 'DBMigration', { 100 | projectName: '{{project_name}}', 101 | description: '{{description}}', 102 | environmentVariables: { 103 | 'DATABASE_URL': { 104 | value: props.dbUrl, 105 | type: codebuild.BuildEnvironmentVariableType.PLAINTEXT 106 | } 107 | }, 108 | environment:{ 109 | buildImage: codebuild.LinuxBuildImage.{{image}} 110 | }, 111 | buildSpec: codebuild.BuildSpec.fromSourceFilename('{{buildspec}}'), 112 | {{#timeout}} 113 | timeout: {{.}}, 114 | {{/timeout}} 115 | vpc: props.vpc, 116 | subnetSelection: { 117 | subnetType: ec2.SubnetType.PRIVATE 118 | } 119 | }); 120 | migration.connections.allowToDefaultPort(props.db, 'DB Migration CodeBuild'); 121 | 122 | const migrationStage = pipeline.addStage({ 123 | stageName: 'DBMigration', 124 | placement: { 125 | justAfter: buildStage 126 | }, 127 | actions: [ 128 | new pipelineactions.CodeBuildAction({ 129 | actionName: 'DBMigrationAction', 130 | project: migration, 131 | input: sourceOutput 132 | }) 133 | ] 134 | }); 135 | {{/migration}} 136 | 137 | pipeline.addStage({ 138 | stageName: 'Deploy', 139 | placement: { 140 | {{#migration}} 141 | justAfter: migrationStage 142 | {{/migration}} 143 | {{#skip_migration}} 144 | justAfter: buildStage 145 | {{/skip_migration}} 146 | }, 147 | actions: [ 148 | new pipelineactions.EcsDeployAction({ 149 | actionName: 'FargateDeployAction', 150 | service: props.service, 151 | input: buildOutput 152 | }) 153 | ] 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /templates/project.mustache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cdk = require('@aws-cdk/core'); 4 | import { {{stack_prefix}}InitStack } from '../lib/{{path_prefix}}-init-stack'; 5 | {{#stacks}} 6 | import { {{stack_prefix}}FargateStack } from '../lib/{{path_prefix}}-fargate-stack'; 7 | {{#enable_cicd}} 8 | import { {{stack_prefix}}PipelineStack } from '../lib/{{path_prefix}}-pipeline-stack'; 9 | {{/enable_cicd}} 10 | {{/stacks}} 11 | 12 | const app = new cdk.App(); 13 | const initStack = new {{stack_prefix}}InitStack(app, '{{stack_prefix}}InitStack'); 14 | {{#stacks}} 15 | 16 | // for service :{{name}} 17 | {{#enable_cicd}}const {{const_prefix}}FargateStack = {{/enable_cicd}}new {{stack_prefix}}FargateStack(app, '{{stack_prefix}}FargateStack', { 18 | vpc: initStack.vpc, 19 | cluster: initStack.cluster 20 | }); 21 | 22 | {{#enable_cicd}} 23 | new {{stack_prefix}}PipelineStack(app, '{{stack_prefix}}PipelineStack', { 24 | vpc: initStack.vpc, 25 | dbUrl: {{const_prefix}}FargateStack.dbUrl, 26 | db: {{const_prefix}}FargateStack.db, 27 | repoName: {{const_prefix}}FargateStack.repoName, 28 | service: {{const_prefix}}FargateStack.service 29 | }); 30 | {{/enable_cicd}} 31 | {{/stacks}} 32 | 33 | app.synth(); 34 | --------------------------------------------------------------------------------