├── .circleci ├── config.yml └── gpg.private.enc ├── .envrc ├── .git-crypt ├── .gitattributes └── keys │ └── default │ └── 0 │ ├── 41D2606F66C3FF28874362B61A16916844CE9D82.gpg │ ├── 6FBE564C2B75F0C9A85C335C94ABC6ADB428736E.gpg │ ├── 933E3994686DC15C99D1369844037399AEDB1D8D.gpg │ └── D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg ├── .gitattributes ├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── asg.tf ├── capacity_provider.tf ├── cloudwatch.tf ├── cluster.tf ├── config ├── defaults.yaml ├── gpg │ ├── jonas.gpg.public │ ├── liam.gpg.public │ └── toby.gpg.public ├── hiera.yaml ├── roles │ ├── full.yaml │ ├── prerequisites.yaml │ └── root.yaml └── secrets │ ├── .unlocked │ ├── ci │ ├── aws-credentials.sh │ ├── encryption.passphrase │ ├── gpg.private │ ├── gpg.public │ ├── ssh.private │ └── ssh.public │ ├── circle_ci │ └── config.yaml │ ├── cluster │ ├── ssh.private │ └── ssh.public │ └── github │ └── config.yaml ├── docs ├── architecture.graffle │ ├── data.plist │ ├── image1.pdf │ ├── image11.pdf │ ├── image12.pdf │ ├── image13.pdf │ ├── image14.pdf │ ├── image15.pdf │ ├── image16.pdf │ ├── image3.pdf │ └── image4.pdf └── architecture.png ├── examples └── full │ ├── .terraform.lock.hcl │ ├── cluster.tf │ ├── outputs.tf │ ├── prerequisites.tf │ ├── provider.tf │ ├── terraform.tf │ └── variables.tf ├── go ├── iam.tf ├── key.tf ├── lib ├── paths.rb └── version.rb ├── locals.tf ├── main.tf ├── outputs.tf ├── policies ├── cluster-instance-policy.json ├── cluster-instance-role.json ├── cluster-service-policy.json └── cluster-service-role.json ├── scripts └── ci │ ├── common │ ├── configure-git.sh │ ├── install-git-crypt.sh │ ├── install-gpg-key.sh │ └── install-orb-deps.sh │ └── steps │ ├── build.sh │ ├── merge-pull-request.sh │ ├── prerelease.sh │ ├── release.sh │ └── test.sh ├── security_groups.tf ├── spec ├── integration │ ├── full_spec.rb │ └── spec_helper.rb └── unit │ ├── autoscaling_group_spec.rb │ ├── capacity_provider_spec.rb │ ├── cloudwatch_spec.rb │ ├── cluster_spec.rb │ ├── iam_spec.rb │ ├── infra │ ├── prerequisites │ │ ├── .terraform.lock.hcl │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ ├── security_groups.tf │ │ ├── terraform.tf │ │ └── variables.tf │ └── root │ │ ├── .terraform.lock.hcl │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ ├── terraform.tf │ │ └── variables.tf │ ├── launch_configuration_spec.rb │ ├── launch_template_spec.rb │ ├── security_group_spec.rb │ ├── spec_helper.rb │ └── support │ └── matchers.rb ├── terraform.tf ├── user-data └── cluster.tpl └── variables.tf /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | queue: eddiewebb/queue@1.6.4 5 | slack: circleci/slack@4.8.3 6 | 7 | defaults: &defaults 8 | docker: 9 | - image: ruby:3.1.1 10 | 11 | slack_context: &slack_context 12 | context: 13 | - slack 14 | 15 | only_main: &only_main 16 | filters: 17 | branches: 18 | only: 19 | - main 20 | 21 | only_dependabot: &only_dependabot 22 | filters: 23 | branches: 24 | only: 25 | - /^dependabot.*/ 26 | 27 | only_main_and_dependabot: &only_main_and_dependabot 28 | filters: 29 | branches: 30 | only: 31 | - main 32 | - /^dependabot.*/ 33 | 34 | commands: 35 | notify: 36 | steps: 37 | - when: 38 | condition: 39 | matches: 40 | pattern: "^dependabot.*" 41 | value: << pipeline.git.branch >> 42 | steps: 43 | - slack/notify: 44 | event: fail 45 | channel: builds-dependabot 46 | template: SLACK_FAILURE_NOTIFICATION 47 | - slack/notify: 48 | event: pass 49 | channel: builds-dependabot 50 | template: SLACK_SUCCESS_NOTIFICATION 51 | - when: 52 | condition: 53 | matches: 54 | pattern: "^(?!dependabot).*" 55 | value: << pipeline.git.branch >> 56 | steps: 57 | - slack/notify: 58 | event: fail 59 | channel: dev 60 | template: SLACK_FAILURE_NOTIFICATION 61 | - slack/notify: 62 | event: pass 63 | channel: builds 64 | template: SLACK_SUCCESS_NOTIFICATION 65 | configure_tools: 66 | steps: 67 | - run: ./scripts/ci/common/install-git-crypt.sh 68 | - run: ./scripts/ci/common/install-gpg-key.sh 69 | - run: ./scripts/ci/common/install-orb-deps.sh 70 | - run: ./scripts/ci/common/configure-git.sh 71 | 72 | jobs: 73 | build: 74 | <<: *defaults 75 | steps: 76 | - checkout 77 | - configure_tools 78 | - queue/until_front_of_line: 79 | consider-branch: false 80 | - run: ./scripts/ci/steps/build.sh 81 | - notify 82 | 83 | test: 84 | <<: *defaults 85 | steps: 86 | - checkout 87 | - configure_tools 88 | - run: 89 | no_output_timeout: 30m 90 | command: ./scripts/ci/steps/test.sh 91 | - store_artifacts: 92 | path: build/logs 93 | - store_artifacts: 94 | path: state 95 | - notify 96 | 97 | prerelease: 98 | <<: *defaults 99 | steps: 100 | - checkout 101 | - configure_tools 102 | - run: ./scripts/ci/steps/prerelease.sh 103 | - notify 104 | 105 | release: 106 | <<: *defaults 107 | steps: 108 | - checkout 109 | - configure_tools 110 | - run: ./scripts/ci/steps/release.sh 111 | - notify 112 | 113 | merge_pull_request: 114 | <<: *defaults 115 | steps: 116 | - checkout 117 | - configure_tools 118 | - run: ./scripts/ci/steps/merge-pull-request.sh 119 | - notify 120 | 121 | workflows: 122 | version: 2 123 | pipeline: 124 | jobs: 125 | - build: 126 | <<: *only_main_and_dependabot 127 | <<: *slack_context 128 | - test: 129 | <<: *only_main_and_dependabot 130 | <<: *slack_context 131 | requires: 132 | - build 133 | - merge_pull_request: 134 | <<: *only_dependabot 135 | <<: *slack_context 136 | requires: 137 | - test 138 | - prerelease: 139 | <<: *only_main 140 | <<: *slack_context 141 | requires: 142 | - test 143 | - slack/on-hold: 144 | <<: *only_main 145 | <<: *slack_context 146 | requires: 147 | - prerelease 148 | channel: release 149 | template: SLACK_ON_HOLD_NOTIFICATION 150 | - hold: 151 | <<: *only_main 152 | type: approval 153 | requires: 154 | - prerelease 155 | - slack/on-hold 156 | - release: 157 | <<: *only_main 158 | <<: *slack_context 159 | requires: 160 | - hold 161 | -------------------------------------------------------------------------------- /.circleci/gpg.private.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/.circleci/gpg.private.enc -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROJECT_DIR="$(pwd)" 4 | 5 | PATH_add "${PROJECT_DIR}" 6 | PATH_add "${PROJECT_DIR}"/vendor/**/bin 7 | 8 | if has asdf; then 9 | asdf install 10 | fi 11 | 12 | layout ruby 13 | layout node 14 | -------------------------------------------------------------------------------- /.git-crypt/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not edit this file. To specify the files to encrypt, create your own 2 | # .gitattributes file in the directory where your files are. 3 | * !filter !diff 4 | *.gpg binary 5 | -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/41D2606F66C3FF28874362B61A16916844CE9D82.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/.git-crypt/keys/default/0/41D2606F66C3FF28874362B61A16916844CE9D82.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/6FBE564C2B75F0C9A85C335C94ABC6ADB428736E.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/.git-crypt/keys/default/0/6FBE564C2B75F0C9A85C335C94ABC6ADB428736E.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/933E3994686DC15C99D1369844037399AEDB1D8D.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/.git-crypt/keys/default/0/933E3994686DC15C99D1369844037399AEDB1D8D.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/.git-crypt/keys/default/0/D164A61C69E23C0F74475FBE5FFE76AD095FCA07.gpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | config/secrets/** filter=git-crypt diff=git-crypt 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [tobyclemson] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "terraform" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "bundler" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *.swp 3 | *.swo 4 | 5 | # IntelliJ 6 | .idea/ 7 | *.ipr 8 | *.iml 9 | *.iws 10 | 11 | # Build 12 | vendor/ 13 | build/ 14 | dist/ 15 | .bundle 16 | .rakeTasks 17 | .direnv 18 | 19 | # OS 20 | .DS_Store 21 | 22 | # Temporary 23 | run/pids/ 24 | run/logs/ 25 | *.log 26 | .tmp 27 | 28 | # RSpec 29 | .rspec_status 30 | 31 | # Terraform 32 | state/ 33 | .terraform 34 | *.tfplan 35 | *.tfstate 36 | *.tfstate.backup 37 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | NewCops: enable 7 | 8 | Layout/LineLength: 9 | Max: 80 10 | AllowedPatterns: 11 | - ^\s+(?:context|it)\s["'][\w\s]+["']\s+do$ 12 | 13 | Metrics/BlockLength: 14 | AllowedMethods: 15 | - describe 16 | - context 17 | - shared_examples 18 | - it 19 | Exclude: 20 | - Rakefile 21 | 22 | Style/Documentation: 23 | Enabled: false 24 | 25 | RSpec/ExampleLength: 26 | Max: 40 27 | 28 | RSpec/DescribeClass: 29 | Enabled: false 30 | 31 | RSpec/InstanceVariable: 32 | Enabled: false 33 | 34 | RSpec/BeforeAfterAll: 35 | Enabled: false 36 | 37 | RSpec/MultipleMemoizedHelpers: 38 | Max: 10 39 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.1 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.1.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | BACKWARDS INCOMPATIBILITIES / NOTES: 4 | 5 | * The `cluster_desired_capacity` is now ignored after the first `apply` of the 6 | module since, in the case of autoscaling or manual scaling, the value may have 7 | changed between `apply`s. 8 | 9 | IMPROVEMENTS: 10 | 11 | * A `cluster_instance_metadata_options` variable has been added which mirrors 12 | the [metadata_options](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template#metadata-options) 13 | exposed on the `aws_launch_template` resource. Among other things, this allows 14 | users of this module to require that IMDSv2 be used by containers in the 15 | cluster. By default, IMDSv2 is not required in this version of the module but 16 | a future major release of the module may enforce IMDSv2 usage. 17 | * The EBS volumes attached to container instances are now tagged with 18 | `Component`, `DeploymentIdentifier`, `Name` and `ClusterName` tags by default, 19 | as well as with any tags passed in the `tags` var when provided (resolves 20 | #94). 21 | 22 | ## 6.0.0 (February 22th 2023) 23 | 24 | BACKWARDS INCOMPATIBILITIES / NOTES: 25 | 26 | * This module is now compatible with Terraform 1.1 and higher. 27 | * This module now uses EBS volume encryption by default. This can be disabled 28 | using `cluster_instance_enable_ebs_volume_encryption = false`. 29 | * In line with Amazon's update of the default root block storage device, the 30 | default in this module is now `/dev/xvda`. 31 | * All variables previously using `"yes|no"` have been replaced with 32 | `true|false`. 33 | * The `allowed_cidrs` variable has been renamed to `default_ingress_cidrs`. 34 | * The `egress_cidrs` variable has been renamed to `default_egress_cidrs`. 35 | * The `cluster_instance_amis` variable has been replaced with the singular 36 | `cluster_instance_ami`, with default value of `null`. 37 | * The following variables have had their default value replaced from `""` to 38 | `null`: 39 | - `cluster_instance_user_data_template` 40 | - `cluster_instance_iam_policy_contents` 41 | - `cluster_service_iam_policy_contents` 42 | 43 | IMPROVEMENTS: 44 | 45 | * This module now uses the nullable feature to simplify variable defaults. 46 | 47 | ## 5.0.1 (February 2nd 2023) 48 | 49 | IMPROVEMENTS: 50 | 51 | * added option to specify log retention period for cluster 52 | * added option to disable enhanced instance monitoring (enabled by default) 53 | * added option to specify the path of the root block storage device as AWS 54 | default has changed from `/dev/sda1` to `/dev/xvda` 55 | 56 | ## 5.0.0 (December 22nd 2022) 57 | 58 | BACKWARDS INCOMPATIBILITIES / NOTES: 59 | 60 | * This module is now compatible with Terraform 1.0 and higher. 61 | * In line with Amazon's deprecation and pending removal of support for launch 62 | configurations, this module now creates a launch template for the autoscaling 63 | group. As a result, the `launch_configuration_name` output has been replaced 64 | by the `launch_template_name` and `launch_template_id` outputs. Upon upgrading 65 | this module, the launch configuration will be destroyed and an equivalent 66 | launch template will be created and associated with the autoscaling group. 67 | * The unused `launch_configuration_create_before_destroy` variable has been 68 | removed. 69 | 70 | ## 4.2.0 (June 20th 2022) 71 | 72 | IMPROVEMENTS: 73 | 74 | * The `aws` and `null` provider constraints have been loosened to allow this 75 | module to be used with the latest versions of each. This enables use of 76 | Terraform AWS provider v4. 77 | * The no longer supported `template` provider has been replaced with native 78 | terraform configuration language equivalents. 79 | 80 | ## 4.1.0 (March 19th 2022) 81 | 82 | IMPROVEMENTS: 83 | 84 | * The `aws_ecs_cluster_capacity_providers` resource is now used to associate 85 | capacity providers with the created ECS cluster. 86 | 87 | ## 4.0.0 (May 27th, 2021) 88 | 89 | BACKWARDS INCOMPATIBILITIES / NOTES: 90 | 91 | * This module is now compatible with Terraform 0.14 and higher. 92 | 93 | ## 0.2.6 (December 31st, 2017) 94 | 95 | IMPROVEMENTS: 96 | 97 | * The `associate_public_ip_addresses` variable allows public IPs to be 98 | associated to ECS container instances. By default its value is `no`. 99 | 100 | ## 0.2.5 (December 31st, 2017) 101 | 102 | IMPROVEMENTS: 103 | 104 | * Updated README with correct inputs, outputs and usage. 105 | 106 | ## 0.2.4 (December 30th, 2017) 107 | 108 | BACKWARDS INCOMPATIBILITIES / NOTES: 109 | 110 | * The cluster now uses the latest ECS optimised amazon linux image by default. 111 | This can be overridden using the `cluster_instance_amis` variable. 112 | * The `private_subnet_ids` variable has been renamed to `subnet_ids` as there 113 | is nothing requiring the subnets to be private 114 | * The `private_network_cidr` variable has been renamed to `allowed_cidrs` and 115 | its type has changed to list. 116 | 117 | IMPROVEMENTS: 118 | 119 | * The cluster now uses the latest ECS optimised amazon linux image by default. 120 | * The default security group ingress and egress rules are now optional and 121 | configurable. A list of CIDRs for both ingress and egress can be specified 122 | using `allowed_cidrs` and `egress_cidrs` respectively. The default rules 123 | can be disabled using `include_default_ingress_rule` and 124 | `include_default_egress_rule`. 125 | * The security group ID is now available via an output named 126 | `security_group_id` so that additional rules can be added outside of the 127 | module. 128 | 129 | ## 0.2.3 (December 29th, 2017) 130 | 131 | BACKWARDS INCOMPATIBILITIES / NOTES: 132 | 133 | * The configuration directory has changed from `/src` to `` to 134 | satisfy the Terraform standard module structure. 135 | 136 | IMPROVEMENTS: 137 | 138 | * All variables and outputs now have descriptions to satisfy the Terraform 139 | standard module structure. 140 | 141 | ## 0.2.0 (November 3th, 2017) 142 | 143 | BACKWARDS INCOMPATIBILITIES / NOTES: 144 | 145 | * The IAM roles and policies for instance and service now use randomly 146 | generated names. The value that was previously used for name can now be found 147 | in the description. 148 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at maintainers@infrablocks.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | 76 | [version]: http://contributor-covenant.org/version/1/4/ 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'awspec' 6 | gem 'confidante' 7 | gem 'git' 8 | gem 'nokogiri' 9 | gem 'rake' 10 | gem 'rake_circle_ci' 11 | gem 'rake_git' 12 | gem 'rake_git_crypt' 13 | gem 'rake_github' 14 | gem 'rake_gpg' 15 | gem 'rake_ssh' 16 | gem 'rake_terraform' 17 | gem 'rspec' 18 | gem 'rspec-terraform' 19 | gem 'rubocop' 20 | gem 'rubocop-rake' 21 | gem 'rubocop-rspec' 22 | gem 'rubyzip' 23 | gem 'semantic' 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 InfraBlocks Maintainers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Terraform AWS ECS Cluster 2 | ========================= 3 | 4 | [![CircleCI](https://circleci.com/gh/infrablocks/terraform-aws-ecs-cluster.svg?style=svg)](https://circleci.com/gh/infrablocks/terraform-aws-ecs-cluster) 5 | 6 | A Terraform module for building an ECS Cluster in AWS. 7 | 8 | The ECS cluster requires: 9 | 10 | * An existing VPC 11 | * Some existing subnets 12 | 13 | The ECS cluster consists of: 14 | 15 | * A cluster in ECS 16 | * A launch template and auto-scaling group for a cluster of ECS container 17 | instances 18 | * An SSH key to connect to the ECS container instances 19 | * A security group for the container instances optionally allowing: 20 | * Outbound internet access for all containers 21 | * Inbound TCP access on any port from the VPC network 22 | * An IAM role and policy for the container instances allowing: 23 | * ECS interactions 24 | * ECR image pulls 25 | * S3 object fetches 26 | * Logging to cloudwatch 27 | * An IAM role and policy for ECS services allowing: 28 | * Elastic load balancer registration / deregistration 29 | * EC2 describe actions and security group ingress rule creation 30 | * A CloudWatch log group 31 | 32 | ![Diagram of infrastructure managed by this module](https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/main/docs/architecture.png) 33 | 34 | Usage 35 | ----- 36 | 37 | To use the module, include something like the following in your Terraform 38 | configuration: 39 | 40 | ```hcl-terraform 41 | module "ecs_cluster" { 42 | source = "infrablocks/ecs-cluster/aws" 43 | version = "5.0.0" 44 | 45 | region = "eu-west-2" 46 | vpc_id = "vpc-fb7dc365" 47 | subnet_ids = [ 48 | "subnet-eb32c271", 49 | "subnet-64872d1f" 50 | ] 51 | 52 | component = "important-component" 53 | deployment_identifier = "production" 54 | 55 | cluster_name = "services" 56 | cluster_instance_ssh_public_key_path = "~/.ssh/id_rsa.pub" 57 | cluster_instance_type = "t3.small" 58 | 59 | cluster_minimum_size = 2 60 | cluster_maximum_size = 10 61 | cluster_desired_capacity = 4 62 | } 63 | ``` 64 | 65 | As mentioned above, the ECS cluster deploys into an existing base network. 66 | Whilst the base network can be created using any mechanism you like, the 67 | [AWS Base Networking](https://github.com/infrablocks/terraform-aws-base-networking) 68 | module will create everything you need. See the 69 | [docs](https://github.com/infrablocks/terraform-aws-base-networking/blob/main/README.md) 70 | for usage instructions. 71 | 72 | See the 73 | [Terraform registry entry](https://registry.terraform.io/modules/infrablocks/ecs-cluster/aws/latest) 74 | for more details. 75 | 76 | ### Inputs 77 | 78 | | Name | Description | Default | Required | 79 | |-----------------------------------------------------|--------------------------------------------------------------------------------------------------------|:-----------------:|:-------------------------------:| 80 | | region | The region into which to deploy the cluster | - | yes | 81 | | vpc_id | The ID of the VPC into which to deploy the cluster | - | yes | 82 | | subnet_ids | The IDs of the subnets for container instances | - | yes | 83 | | component | The component this cluster will contain | - | yes | 84 | | deployment_identifier | An identifier for this instantiation | - | yes | 85 | | tags | A map of additional tags to add to all resources | - | no | 86 | | cluster_name | The name of the cluster to create | default | yes | 87 | | cluster_instance_ssh_public_key_path | The path to the public key to use for the container instances | - | yes | 88 | | cluster_instance_type | The instance type of the container instances | t2.medium | yes | 89 | | cluster_instance_root_block_device_size | The size in GB of the root block device on cluster instances | 30 | yes | 90 | | cluster_instance_root_block_device_path | Path of the instance root block storage volume | /dev/xvda | yes | 91 | | cluster_instance_root_block_device_type | The type of the root block device on cluster instances ('standard', 'gp2', or 'io1') | standard | yes | 92 | | cluster_instance_user_data_template | The contents of a template for container instance user data | see user-data | no | 93 | | cluster_instance_ami | AMI for the container instances | ECS optimised AMI | yes | 94 | | cluster_instance_metadata_options | A map of metadata options for cluster instances. | - | no | 95 | | cluster_instance_iam_policy_contents | The contents of the cluster instance IAM policy | see policies | no | 96 | | cluster_service_iam_policy_contents | The contents of the cluster service IAM policy | see policies | no | 97 | | cluster_minimum_size | The minimum size of the ECS cluster | 1 | yes | 98 | | cluster_maximum_size | The maximum size of the ECS cluster | 10 | yes | 99 | | cluster_desired_capacity | The desired capacity of the ECS cluster | 3 | yes | 100 | | associate_public_ip_addresses | Whether or not to associate public IP addresses with ECS container instances | false | no | 101 | | include_default_ingress_rule | Whether or not to include the default ingress rule on the ECS container instances security group | true | no | 102 | | include_default_egress_rule | Whether or not to include the default egress rule on the ECS container instances security group | true | no | 103 | | default_ingress_cidrs | The CIDRs allowed access to containers | ["10.0.0.0/8"] | if include_default_ingress_rule | 104 | | default_egress_cidrs | The CIDRs accessible from containers | ["0.0.0.0/0"] | if include_default_egress_rule | 105 | | security_groups | The list of security group IDs to associate with the cluster in addition to the default security group | [] | no | 106 | | cluster_log_group_retention | The number of days logs will be retained in the CloudWatch log group of the cluster (0 = unlimited) | 0 | no | 107 | | enable_detailed_monitoring | Enable detailed monitoring of EC2 instance(s) | true | no | 108 | | enable_container_insights | Whether or not to enable container insights on the ECS cluster | false | no | 109 | | protect_cluster_instances_from_scale_in | Whether or not to protect cluster instances in the autoscaling group from scale in | false | no | 110 | | include_asg_capacity_provider | Whether or not to add the created ASG as a capacity provider for the ECS cluster | false | no | 111 | | asg_capacity_provider_manage_termination_protection | Whether or not to allow ECS to manage termination protection for the ASG capacity provider | true | no | 112 | | asg_capacity_provider_manage_scaling | Whether or not to allow ECS to manage scaling for the ASG capacity provider | true | no | 113 | | asg_capacity_provider_minimum_scaling_step_size | The minimum scaling step size for ECS managed scaling of the ASG capacity provider | 1 | no | 114 | | asg_capacity_provider_maximum_scaling_step_size | The maximum scaling step size for ECS managed scaling of the ASG capacity provider | 1000 | no | 115 | | asg_capacity_provider_target_capacity | The target capacity, as a percentage from 1 to 100, for the ASG capacity provider | 100 | no | 116 | | cluster_instance_enable_ebs_volume_encryption | Determines whether encryption is enabled on the EBS volume | true | no | 117 | | cluster_instance_ebs_volume_kms_key_id | KMS key to use for encryption of the EBS volume when enabled | alias/aws/ebs | no | 118 | 119 | Notes: 120 | 121 | * By default, the latest available Amazon Linux 2 AMI is used. 122 | * For Amazon Linux 1 AMIs use version <= 0.6.0 of this module for terraform 0.11 123 | or version = 1.0.0 for terraform 0.12. 124 | * When a specific AMI is provided via `cluster_instance_ami`, only the root 125 | block device can be customised, using the 126 | `cluster_instance_root_block_device_size` and 127 | `cluster_instance_root_block_device_type` variables. 128 | * The user data template will get the cluster name as `cluster_name`. If 129 | none is supplied, a default will be used. 130 | 131 | ### Outputs 132 | 133 | | Name | Description | 134 | |------------------------|----------------------------------------------------------------------------------| 135 | | cluster_id | The ID of the created ECS cluster | 136 | | cluster_name | The name of the created ECS cluster | 137 | | cluster_arn | The ARN of the created ECS cluster | 138 | | autoscaling_group_name | The name of the autoscaling group for the ECS container instances | 139 | | launch_template_name | The name of the launch template for the ECS container instances | 140 | | launch_template_id | The ID of the launch template for the ECS container instances | 141 | | security_group_id | The ID of the default security group associated with the ECS container instances | 142 | | instance_role_arn | The ARN of the container instance role | 143 | | instance_role_id | The ID of the container instance role | 144 | | instance_policy_arn | The ARN of the container instance policy | 145 | | instance_policy_id | The ID of the container instance policy | 146 | | service_role_arn | The ARN of the ECS service role | 147 | | service_role_id | The ID of the ECS service role | 148 | | service_policy_arn | The ARN of the ECS service policy | 149 | | service_policy_id | The ID of the ECS service policy | 150 | | log_group | The name of the default log group for the cluster | 151 | 152 | ### Compatibility 153 | 154 | This module is compatible with Terraform versions greater than or equal to 155 | Terraform 1.0. 156 | 157 | ### Required Permissions 158 | 159 | * iam:GetPolicy 160 | * iam:GetPolicyVersion 161 | * iam:ListPolicyVersions 162 | * iam:ListEntitiesForPolicy 163 | * iam:CreatePolicy 164 | * iam:DeletePolicy 165 | * iam:GetRole 166 | * iam:PassRole 167 | * iam:CreateRole 168 | * iam:DeleteRole 169 | * iam:ListRolePolicies 170 | * iam:AttachRolePolicy 171 | * iam:DetachRolePolicy 172 | * iam:GetInstanceProfile 173 | * iam:CreateInstanceProfile 174 | * iam:ListInstanceProfilesForRole 175 | * iam:AddRoleToInstanceProfile 176 | * iam:RemoveRoleFromInstanceProfile 177 | * iam:DeleteInstanceProfile 178 | * ec2:DescribeSecurityGroups 179 | * ec2:CreateSecurityGroup 180 | * ec2:DeleteSecurityGroup 181 | * ec2:AuthorizeSecurityGroupIngress 182 | * ec2:AuthorizeSecurityGroupEgress 183 | * ec2:RevokeSecurityGroupEgress 184 | * ec2:ImportKeyPair 185 | * ec2:DescribeKeyPairs 186 | * ec2:DeleteKeyPair 187 | * ec2:CreateTags 188 | * ec2:DescribeImages 189 | * ec2:DescribeNetworkInterfaces 190 | * ecs:DescribeClusters 191 | * ecs:CreateCluster 192 | * ecs:DeleteCluster 193 | * autoscaling:DescribeLaunchConfigurations 194 | * autoscaling:CreateLaunchConfiguration 195 | * autoscaling:DeleteLaunchConfiguration 196 | * autoscaling:DescribeScalingActivities 197 | * autoscaling:DescribeAutoScalingGroups 198 | * autoscaling:CreateAutoScalingGroup 199 | * autoscaling:UpdateAutoScalingGroup 200 | * autoscaling:DeleteAutoScalingGroup 201 | * logs:CreateLogGroup 202 | * logs:DescribeLogGroups 203 | * logs:ListTagsLogGroup 204 | * logs:DeleteLogGroup 205 | 206 | Development 207 | ----------- 208 | 209 | ### Machine Requirements 210 | 211 | In order for the build to run correctly, a few tools will need to be installed 212 | on your development machine: 213 | 214 | * Ruby (3.1) 215 | * Bundler 216 | * git 217 | * git-crypt 218 | * gnupg 219 | * direnv 220 | * aws-vault 221 | 222 | #### Mac OS X Setup 223 | 224 | Installing the required tools is best managed by [homebrew](http://brew.sh). 225 | 226 | To install homebrew: 227 | 228 | ```shell 229 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 230 | ``` 231 | 232 | Then, to install the required tools: 233 | 234 | ```shell 235 | # ruby 236 | brew install rbenv 237 | brew install ruby-build 238 | echo 'eval "$(rbenv init - bash)"' >> ~/.bash_profile 239 | echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc 240 | eval "$(rbenv init -)" 241 | rbenv install 3.1.1 242 | rbenv rehash 243 | rbenv local 3.1.1 244 | gem install bundler 245 | 246 | # git, git-crypt, gnupg 247 | brew install git 248 | brew install git-crypt 249 | brew install gnupg 250 | 251 | # aws-vault 252 | brew cask install 253 | 254 | # direnv 255 | brew install direnv 256 | echo "$(direnv hook bash)" >> ~/.bash_profile 257 | echo "$(direnv hook zsh)" >> ~/.zshrc 258 | eval "$(direnv hook $SHELL)" 259 | 260 | direnv allow 261 | ``` 262 | 263 | ### Running the build 264 | 265 | Running the build requires an AWS account and AWS credentials. You are free to 266 | configure credentials however you like as long as an access key ID and secret 267 | access key are available. These instructions utilise 268 | [aws-vault](https://github.com/99designs/aws-vault) which makes credential 269 | management easy and secure. 270 | 271 | To provision module infrastructure, run tests and then destroy that 272 | infrastructure, execute: 273 | 274 | ```bash 275 | aws-vault exec -- ./go 276 | ``` 277 | 278 | To provision the module prerequisites: 279 | 280 | ```bash 281 | aws-vault exec -- ./go deployment:prerequisites:provision[] 282 | ``` 283 | 284 | To provision the module contents: 285 | 286 | ```bash 287 | aws-vault exec -- ./go deployment:root:provision[] 288 | ``` 289 | 290 | To destroy the module contents: 291 | 292 | ```bash 293 | aws-vault exec -- ./go deployment:root:destroy[] 294 | ``` 295 | 296 | To destroy the module prerequisites: 297 | 298 | ```bash 299 | aws-vault exec -- ./go deployment:prerequisites:destroy[] 300 | ``` 301 | 302 | Configuration parameters can be overridden via environment variables: 303 | 304 | ```bash 305 | DEPLOYMENT_IDENTIFIER=testing aws-vault exec -- ./go 306 | ``` 307 | 308 | When a deployment identifier is provided via an environment variable, 309 | infrastructure will not be destroyed at the end of test execution. This can 310 | be useful during development to avoid lengthy provision and destroy cycles. 311 | 312 | ### Common Tasks 313 | 314 | #### Generating an SSH key pair 315 | 316 | To generate an SSH key pair: 317 | 318 | ```bash 319 | ssh-keygen -m PEM -t rsa -b 4096 -C integration-test@example.com -N '' -f config/secrets/keys/bastion/ssh 320 | ``` 321 | 322 | #### Generating a self-signed certificate 323 | 324 | To generate a self signed certificate: 325 | 326 | ```bash 327 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 328 | ``` 329 | 330 | To decrypt the resulting key: 331 | 332 | ```bash 333 | openssl rsa -in key.pem -out ssl.key 334 | ``` 335 | 336 | #### Managing CircleCI keys 337 | 338 | To encrypt a GPG key for use by CircleCI: 339 | 340 | ```bash 341 | openssl aes-256-cbc \ 342 | -e \ 343 | -md sha1 \ 344 | -in ./config/secrets/ci/gpg.private \ 345 | -out ./.circleci/gpg.private.enc \ 346 | -k "" 347 | ``` 348 | 349 | To check decryption is working correctly: 350 | 351 | ```bash 352 | openssl aes-256-cbc \ 353 | -d \ 354 | -md sha1 \ 355 | -in ./.circleci/gpg.private.enc \ 356 | -k "" 357 | ``` 358 | 359 | Contributing 360 | ------------ 361 | 362 | Bug reports and pull requests are welcome on GitHub at 363 | https://github.com/infrablocks/terraform-aws-assumable-roles-policy. 364 | This project is intended to be a safe, welcoming space for collaboration, and 365 | contributors are expected to adhere to 366 | the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 367 | 368 | License 369 | ------- 370 | 371 | The library is available as open source under the terms of the 372 | [MIT License](http://opensource.org/licenses/MIT). 373 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'confidante' 4 | require 'git' 5 | require 'rake_circle_ci' 6 | require 'rake_git' 7 | require 'rake_git_crypt' 8 | require 'rake_github' 9 | require 'rake_gpg' 10 | require 'rake_ssh' 11 | require 'rake_terraform' 12 | require 'rspec/core/rake_task' 13 | require 'rubocop/rake_task' 14 | require 'securerandom' 15 | require 'semantic' 16 | require 'yaml' 17 | 18 | require_relative 'lib/paths' 19 | require_relative 'lib/version' 20 | 21 | configuration = Confidante.configuration 22 | 23 | def repo 24 | Git.open(Pathname.new('.')) 25 | end 26 | 27 | def latest_tag 28 | repo.tags.map do |tag| 29 | Semantic::Version.new(tag.name) 30 | end.max 31 | end 32 | 33 | task default: %i[ 34 | test:code:fix 35 | test:unit 36 | test:integration 37 | ] 38 | 39 | RakeTerraform.define_installation_tasks( 40 | path: File.join(Dir.pwd, 'vendor', 'terraform'), 41 | version: '1.3.1' 42 | ) 43 | 44 | RakeGitCrypt.define_standard_tasks( 45 | namespace: :git_crypt, 46 | 47 | provision_secrets_task_name: :'secrets:provision', 48 | destroy_secrets_task_name: :'secrets:destroy', 49 | 50 | install_commit_task_name: :'git:commit', 51 | uninstall_commit_task_name: :'git:commit', 52 | 53 | gpg_user_key_paths: %w[ 54 | config/gpg 55 | config/secrets/ci/gpg.public 56 | ] 57 | ) 58 | 59 | namespace :git do 60 | RakeGit.define_commit_task( 61 | argument_names: [:message] 62 | ) do |t, args| 63 | t.message = args.message 64 | end 65 | end 66 | 67 | namespace :encryption do 68 | namespace :directory do 69 | desc 'Ensure CI secrets directory exists.' 70 | task :ensure do 71 | FileUtils.mkdir_p('config/secrets/ci') 72 | end 73 | end 74 | 75 | namespace :passphrase do 76 | desc 'Generate encryption passphrase for CI GPG key' 77 | task generate: ['directory:ensure'] do 78 | File.write( 79 | 'config/secrets/ci/encryption.passphrase', 80 | SecureRandom.base64(36) 81 | ) 82 | end 83 | end 84 | end 85 | 86 | namespace :keys do 87 | namespace :deploy do 88 | RakeSSH.define_key_tasks( 89 | path: 'config/secrets/ci/', 90 | comment: 'maintainers@infrablocks.io' 91 | ) 92 | end 93 | 94 | namespace :cluster do 95 | RakeSSH.define_key_tasks( 96 | path: 'config/secrets/cluster', 97 | comment: 'maintainers@infrablocks.io' 98 | ) 99 | end 100 | 101 | namespace :secrets do 102 | namespace :gpg do 103 | RakeGPG.define_generate_key_task( 104 | output_directory: 'config/secrets/ci', 105 | name_prefix: 'gpg', 106 | owner_name: 'InfraBlocks Maintainers', 107 | owner_email: 'maintainers@infrablocks.io', 108 | owner_comment: 'terraform-aws-ecs-cluster CI Key' 109 | ) 110 | end 111 | 112 | task generate: ['gpg:generate'] 113 | end 114 | end 115 | 116 | namespace :secrets do 117 | namespace :directory do 118 | desc 'Ensure secrets directory exists and is set up correctly' 119 | task :ensure do 120 | FileUtils.mkdir_p('config/secrets') 121 | unless File.exist?('config/secrets/.unlocked') 122 | File.write('config/secrets/.unlocked', 'true') 123 | end 124 | end 125 | end 126 | 127 | desc 'Generate all generatable secrets.' 128 | task generate: %w[ 129 | directory:ensure 130 | encryption:passphrase:generate 131 | keys:deploy:generate 132 | keys:cluster:generate 133 | keys:secrets:generate 134 | ] 135 | 136 | desc 'Provision all secrets.' 137 | task provision: [:generate] 138 | 139 | desc 'Delete all secrets.' 140 | task :destroy do 141 | rm_rf 'config/secrets' 142 | end 143 | 144 | desc 'Rotate all secrets.' 145 | task rotate: [:'git_crypt:reinstall'] 146 | end 147 | 148 | RakeCircleCI.define_project_tasks( 149 | namespace: :circle_ci, 150 | project_slug: 'github/infrablocks/terraform-aws-ecs-cluster' 151 | ) do |t| 152 | circle_ci_config = 153 | YAML.load_file('config/secrets/circle_ci/config.yaml') 154 | 155 | t.api_token = circle_ci_config['circle_ci_api_token'] 156 | t.environment_variables = { 157 | ENCRYPTION_PASSPHRASE: 158 | File.read('config/secrets/ci/encryption.passphrase') 159 | .chomp, 160 | CIRCLECI_API_KEY: 161 | YAML.load_file( 162 | 'config/secrets/circle_ci/config.yaml' 163 | )['circle_ci_api_token'] 164 | } 165 | t.checkout_keys = [] 166 | t.ssh_keys = [ 167 | { 168 | hostname: 'github.com', 169 | private_key: File.read('config/secrets/ci/ssh.private') 170 | } 171 | ] 172 | end 173 | 174 | RakeGithub.define_repository_tasks( 175 | namespace: :github, 176 | repository: 'infrablocks/terraform-aws-ecs-cluster' 177 | ) do |t| 178 | github_config = 179 | YAML.load_file('config/secrets/github/config.yaml') 180 | 181 | t.access_token = github_config['github_personal_access_token'] 182 | t.deploy_keys = [ 183 | { 184 | title: 'CircleCI', 185 | public_key: File.read('config/secrets/ci/ssh.public') 186 | } 187 | ] 188 | end 189 | 190 | namespace :pipeline do 191 | desc 'Prepare CircleCI Pipeline' 192 | task prepare: %i[ 193 | circle_ci:env_vars:ensure 194 | circle_ci:checkout_keys:ensure 195 | circle_ci:ssh_keys:ensure 196 | github:deploy_keys:ensure 197 | ] 198 | end 199 | 200 | RuboCop::RakeTask.new 201 | 202 | namespace :test do 203 | namespace :code do 204 | desc 'Run all checks on the test code' 205 | task check: [:rubocop] 206 | 207 | desc 'Attempt to automatically fix issues with the test code' 208 | task fix: [:'rubocop:autocorrect_all'] 209 | end 210 | 211 | desc 'Run module unit tests' 212 | RSpec::Core::RakeTask.new(unit: ['terraform:ensure']) do |t| 213 | t.pattern = 'spec/unit/**{,/*/**}/*_spec.rb' 214 | t.rspec_opts = '-I spec/unit' 215 | 216 | ENV['AWS_REGION'] = configuration.region 217 | end 218 | 219 | desc 'Run module integration tests' 220 | RSpec::Core::RakeTask.new(integration: ['terraform:ensure']) do |t| 221 | t.pattern = 'spec/integration/**{,/*/**}/*_spec.rb' 222 | t.rspec_opts = '-I spec/integration' 223 | 224 | ENV['AWS_REGION'] = configuration.region 225 | end 226 | end 227 | 228 | namespace :deployment do 229 | namespace :prerequisites do 230 | RakeTerraform.define_command_tasks( 231 | configuration_name: 'prerequisites', 232 | argument_names: [:seed] 233 | ) do |t, args| 234 | deployment_configuration = 235 | configuration 236 | .for_scope(role: :prerequisites) 237 | .for_overrides(args.to_h) 238 | 239 | t.source_directory = 'spec/unit/infra/prerequisites' 240 | t.work_directory = 'build/infra' 241 | 242 | t.state_file = deployment_configuration.state_file 243 | t.vars = deployment_configuration.vars 244 | end 245 | end 246 | 247 | namespace :root do 248 | RakeTerraform.define_command_tasks( 249 | configuration_name: 'root', 250 | argument_names: [:seed] 251 | ) do |t, args| 252 | deployment_configuration = 253 | configuration 254 | .for_scope(role: :root) 255 | .for_overrides(args.to_h) 256 | 257 | t.source_directory = 'spec/unit/infra/root' 258 | t.work_directory = 'build/infra' 259 | 260 | t.state_file = deployment_configuration.state_file 261 | t.vars = deployment_configuration.vars 262 | end 263 | end 264 | end 265 | 266 | namespace :version do 267 | desc 'Bump version for specified type (pre, major, minor, patch)' 268 | task :bump, [:type] do |_, args| 269 | next_tag = latest_tag.send("#{args.type}!") 270 | repo.add_tag(next_tag.to_s) 271 | repo.push('origin', 'main', tags: true) 272 | puts "Bumped version to #{next_tag}." 273 | end 274 | 275 | desc 'Release module' 276 | task :release do 277 | next_tag = latest_tag.release! 278 | repo.add_tag(next_tag.to_s) 279 | repo.push('origin', 'main', tags: true) 280 | puts "Released version #{next_tag}." 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /asg.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | ami_id = coalesce( 3 | var.cluster_instance_ami, 4 | data.aws_ami.amazon_linux_2.image_id) 5 | cluster_user_data_template = coalesce( 6 | var.cluster_instance_user_data_template, 7 | file("${path.module}/user-data/cluster.tpl")) 8 | cluster_user_data = replace( 9 | local.cluster_user_data_template, 10 | "$${cluster_name}", local.cluster_full_name) 11 | cluster_instance_metadata_options = var.cluster_instance_metadata_options == null ? {} : var.cluster_instance_metadata_options 12 | } 13 | 14 | data "aws_ami" "amazon_linux_2" { 15 | most_recent = true 16 | owners = ["amazon"] 17 | 18 | filter { 19 | name = "name" 20 | values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"] 21 | } 22 | } 23 | 24 | resource "aws_launch_template" "cluster" { 25 | count = var.include_cluster_instances ? 1 : 0 26 | 27 | name_prefix = "cluster-${var.component}-${var.deployment_identifier}-${var.cluster_name}-" 28 | image_id = local.ami_id 29 | instance_type = var.cluster_instance_type 30 | key_name = var.cluster_instance_ssh_public_key_path == null ? "" : element(concat(aws_key_pair.cluster.*.key_name, [""]), 0) 31 | 32 | iam_instance_profile { 33 | name = aws_iam_instance_profile.cluster[0].name 34 | } 35 | 36 | metadata_options { 37 | http_endpoint = lookup(local.cluster_instance_metadata_options, "http_endpoint", null) 38 | http_tokens = lookup(local.cluster_instance_metadata_options, "http_tokens", null) 39 | http_put_response_hop_limit = lookup(local.cluster_instance_metadata_options, "http_put_response_hop_limit", null) 40 | instance_metadata_tags = lookup(local.cluster_instance_metadata_options, "instance_metadata_tags", null) 41 | http_protocol_ipv6 = lookup(local.cluster_instance_metadata_options, "http_protocol_ipv6", null) 42 | } 43 | 44 | user_data = base64encode(local.cluster_user_data) 45 | 46 | network_interfaces { 47 | associate_public_ip_address = var.associate_public_ip_addresses 48 | security_groups = concat([aws_security_group.cluster[0].id], var.security_groups) 49 | } 50 | 51 | block_device_mappings { 52 | device_name = var.cluster_instance_root_block_device_path 53 | 54 | ebs { 55 | encrypted = var.cluster_instance_enable_ebs_volume_encryption 56 | kms_key_id = local.cluster_instance_ebs_volume_kms_key_id 57 | 58 | volume_size = var.cluster_instance_root_block_device_size 59 | volume_type = var.cluster_instance_root_block_device_type 60 | } 61 | } 62 | 63 | monitoring { 64 | enabled = var.enable_detailed_monitoring 65 | } 66 | 67 | tag_specifications { 68 | resource_type = "volume" 69 | tags = merge( 70 | local.tags, 71 | { 72 | Name = "cluster-worker-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 73 | ClusterName = var.cluster_name 74 | } 75 | ) 76 | } 77 | 78 | depends_on = [ 79 | null_resource.iam_wait 80 | ] 81 | } 82 | 83 | resource "aws_autoscaling_group" "cluster" { 84 | count = var.include_cluster_instances ? 1 : 0 85 | 86 | name_prefix = "asg-${var.component}-${var.deployment_identifier}-${var.cluster_name}-" 87 | 88 | vpc_zone_identifier = var.subnet_ids 89 | 90 | launch_template { 91 | id = aws_launch_template.cluster[0].id 92 | version = "$Latest" 93 | } 94 | 95 | min_size = var.cluster_minimum_size 96 | max_size = var.cluster_maximum_size 97 | desired_capacity = var.cluster_desired_capacity 98 | 99 | protect_from_scale_in = ((var.include_asg_capacity_provider && var.asg_capacity_provider_manage_termination_protection) || var.protect_cluster_instances_from_scale_in) 100 | 101 | tag { 102 | key = "Name" 103 | value = "cluster-worker-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 104 | propagate_at_launch = true 105 | } 106 | 107 | tag { 108 | key = "ClusterName" 109 | value = var.cluster_name 110 | propagate_at_launch = true 111 | } 112 | 113 | dynamic "tag" { 114 | for_each = var.include_asg_capacity_provider ? merge({ 115 | AmazonECSManaged : "" 116 | }, local.tags) : local.tags 117 | content { 118 | key = tag.key 119 | value = tag.value 120 | propagate_at_launch = true 121 | } 122 | } 123 | 124 | lifecycle { 125 | create_before_destroy = true 126 | ignore_changes = [ 127 | desired_capacity 128 | ] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /capacity_provider.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_capacity_provider" "autoscaling_group" { 2 | count = (var.include_cluster_instances && var.include_asg_capacity_provider) ? 1 : 0 3 | 4 | name = "cp-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 5 | 6 | auto_scaling_group_provider { 7 | auto_scaling_group_arn = aws_autoscaling_group.cluster[0].arn 8 | 9 | managed_termination_protection = var.asg_capacity_provider_manage_termination_protection ? "ENABLED" : "DISABLED" 10 | 11 | managed_scaling { 12 | status = var.asg_capacity_provider_manage_scaling ? "ENABLED" : "DISABLED" 13 | target_capacity = var.asg_capacity_provider_target_capacity 14 | minimum_scaling_step_size = var.asg_capacity_provider_minimum_scaling_step_size 15 | maximum_scaling_step_size = var.asg_capacity_provider_maximum_scaling_step_size 16 | } 17 | } 18 | } 19 | 20 | resource "aws_ecs_cluster_capacity_providers" "cluster_capacity_providers" { 21 | count = ((var.include_cluster_instances && var.include_asg_capacity_provider) || length(var.additional_capacity_providers) > 0) ? 1 : 0 22 | 23 | cluster_name = aws_ecs_cluster.cluster.name 24 | 25 | capacity_providers = var.include_asg_capacity_provider ? [aws_ecs_capacity_provider.autoscaling_group[0].name] : var.additional_capacity_providers 26 | } 27 | -------------------------------------------------------------------------------- /cloudwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "cluster" { 2 | name = "/${var.component}/${var.deployment_identifier}/ecs-cluster/${var.cluster_name}" 3 | 4 | retention_in_days = var.cluster_log_group_retention 5 | } 6 | -------------------------------------------------------------------------------- /cluster.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | cluster_full_name = "${var.component}-${var.deployment_identifier}-${var.cluster_name}" 3 | } 4 | 5 | resource "aws_ecs_cluster" "cluster" { 6 | name = local.cluster_full_name 7 | 8 | tags = local.tags 9 | 10 | setting { 11 | name = "containerInsights" 12 | value = var.enable_container_insights ? "enabled" : "disabled" 13 | } 14 | 15 | depends_on = [ 16 | null_resource.iam_wait 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /config/defaults.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | region: 'eu-west-2' 3 | availability_zones: 4 | - "eu-west-2a" 5 | - "eu-west-2b" 6 | 7 | component: 'test' 8 | deployment_identifier: "%{hiera('seed')}" 9 | 10 | tags: 11 | ImportantTag: "important-value" 12 | 13 | work_directory: 'build' 14 | configuration_directory: "%{hiera('work_directory')}/%{hiera('source_directory')}" 15 | 16 | vpc_cidr: "10.1.0.0/16" 17 | 18 | private_zone_vpc_id: 'vpc-a938ffc0' 19 | 20 | custom_ami_id: "ami-3fb6bc5b" 21 | 22 | cluster_name: 'test-cluster' 23 | cluster_instance_ssh_public_key_path: "%{hiera('project_directory')}/config/secrets/cluster/ssh.public" 24 | cluster_instance_type: 't2.medium' 25 | cluster_instance_root_block_device_size: 40 26 | cluster_instance_root_block_device_path: '/dev/xvda' 27 | 28 | cluster_minimum_size: 1 29 | cluster_maximum_size: 3 30 | cluster_desired_capacity: 2 31 | 32 | cluster_log_group_retention: 0 33 | 34 | enable_detailed_monitoring: true 35 | 36 | security_groups: [] 37 | 38 | include_default_ingress_rule: true 39 | include_default_egress_rule: true 40 | 41 | default_ingress_cidrs: 42 | - '10.1.0.0/16' 43 | default_egress_cidrs: 44 | - '10.1.0.0/16' 45 | 46 | enable_container_insights: false 47 | 48 | protect_cluster_instances_from_scale_in: false 49 | 50 | include_asg_capacity_provider: false 51 | asg_capacity_provider_manage_termination_protection: false 52 | asg_capacity_provider_manage_scaling: false 53 | asg_capacity_provider_minimum_scaling_step_size: 1 54 | asg_capacity_provider_maximum_scaling_step_size: 1000 55 | asg_capacity_provider_target_capacity: 100 56 | -------------------------------------------------------------------------------- /config/gpg/jonas.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQGNBGB29HsBDAC+dgoRBQ9PLCx/cgN+OoPN7ciscmSNEWKsmcm6fZk+Vp5PJfIg 4 | d603ect41PV7AGAxKiUTHNyXL9+gUj8Hcg+kdNvsuGD+UBhu7rdcDtLgVuqTO25/ 5 | bIpZ3QR2N6tCuwq11i5NgGxnm0Am1z1f7D80V4iIUje9+e8UgW/7vYjigqhg7IAO 6 | QH2tse6KyY2xaLjPYTIxx/cVqT+b3ieut838AhwZo1NJb1oDiMTHkbbsfPZ+DsO9 7 | oZE3kx3210o6gULVtLkJUGv9N8pUKr2wjEeIaXv8Vz5NpZDoZPlcEVjH45y2LoR5 8 | YZ7zHGAI/2GK49ILhhiYnpZjCvnQ70sdVmn7blpRztzJ2ZEPL/St6R/kc9retVUb 9 | 5FBLuCR3fcoePxvnw2Fyxi9zI8UpMsssfP5rEv/QFaArQAe3mX0mwUYd3G5zb1+7 10 | eAH35teCT1/Ys4X/foozBjOpMD9wrcybyNkU9vU99AcxSU8MFx4t1JnatU6+D7ld 11 | slYWYZHmWMqgFm0AEQEAAbQhSm9uYXMgU3ZhbGluIDxqb25hc0Bnby1hdG9taWMu 12 | aW8+iQHUBBMBCAA+FiEE0WSmHGniPA90R1++X/52rQlfygcFAmB29HsCGwMFCQPC 13 | ZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQX/52rQlfygcDOgv+P0QshF3E 14 | HXj5UFHN66Ls+ZhUGJh1+tf0Yw7kqdVCEio7ah66kOJuEbif1Czv+pHIuQYLYutY 15 | 1PkPfKvyvncUauhz9N4fdi2Y224solfPj+DVZ50SvULNfY+wMprq11F8odoxsULT 16 | o1J48ik9LYjkcJlGIFow81KmqSdCkru3C2JwFoDpeZOr/ZVQBQwspTv7qAGFlufY 17 | NCSkgpFiEp3WtlUvpLng9dPJYZee2+hiubHwMxH5q58Pj+TOEFFVpJvfseaJN9hn 18 | HryyBufF3nlZCy8q2u+8EF59D+/YsWdi5yKQOKWxB3n/SiXPMKPlORs8J+ltdDW5 19 | 9eKhCW1xd1cQGUp9ptBCom7kSPAei2beNxlu6ZsvDgHCh1mSwHgVVQbY6cKoIASj 20 | W3Ps6vxU1/ekakecwz7dlrQPQvF2hDBkHblgOc/Ir0XHmWhKNLx2A4m4OFXYWsxp 21 | LOiMzUjrEvkcabYL29p+8LLAxOU9RK4Q+hNUyb9xbXKxi4KPDuozxMMJiQHUBBMB 22 | CAA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0WSmHGniPA90R1++X/52 23 | rQlfygcFAmRBL8gFCQeMok0ACgkQX/52rQlfygfN9AwAnIc0SjA/fTzfpHgjWtTO 24 | x/K63k6hLnlOzr0el2nInLnPwjeUD/kTd4BmwIQaFOPymw1s1FzoZZi6mnZCI5Mh 25 | 6hn9x9+iWmkTiQ87Q82Svaqd80Wdqgp3/rAr0AS5YHPrj8OtRZ58Zn6ikkJN72iQ 26 | 86YI+g7voDJnVSanfYnef5SAPdo6RghVj5CwCRxJSqWWeelyi5Egtsy3Rz1ujX7+ 27 | 8MfxKXJV2lWYg7mkliotMnd4kqkEDY2fKVowOHekKyF1iQgKTxP1F7nqw1i56Ec/ 28 | PH9Z8y6Bsmctv+Ot3Igrvs7WFNS7+PVznJfqeHJkyW4+x1lwQ3tIT6JYq6jE+QPZ 29 | 98D4ZJO8zQr+TJ4fl/mL8aKYQy88yMDIJddmNnZOx3w93TGNkox7FUpkfn/nLqyX 30 | zPXYxM/48fYByedU8HWZa7KtGIm8nVNII/VUHAx4ENxfAVk8/7ln1/TTS43Bxcx7 31 | kOhCy3kVaqbpVvDSJJuTj+aP7BRFjFFJJ/Hjr9FwmRZ5uQGNBGB29HsBDACpMSmc 32 | 55Yt0qlOreidaGGcHY4acLnV9XPcZkLozqp+GE8NCw5doLvswyUBlhUPeaGhturE 33 | rmCMSJFJZw7pKXHtdkmY9RCJnQDQOoKJS/hYVuHPq0sTU4CE33ycBr28DfVlZ5vF 34 | fmOfLEVF7HlwAAmurt11KctlPCuZBli7mcuHumAD5M9fTfwB0YO6zPUT4VBn+1hh 35 | VQRHMucGkW8n8vYub/1/cOpLIcq++K98iPc26sTr9Z/0GZKhNowUU7YlPva/s5EK 36 | AnZy1oaIFmINPv4NG6W92MJuKZFwgVHdHMW7Qxa1O2Dha6JPKladZbWlYlzBnjQt 37 | pQInV7+4vtrTyQCOOngQ+F9FGpWIlIlVT+wq2Dz4SNken84eWtXsZiXccm9j9gh7 38 | Dc588tFICc9qW+4OETAZCz9ynQnrBrfSsOKkC22kWSt7IKt88ryZB0XPFtVjZPf1 39 | JkjbfE2luNcWKGsjdTr+dZDalAkzy8UoKyN//eevNuquFep50ad0j3ges90AEQEA 40 | AYkBvAQYAQgAJhYhBNFkphxp4jwPdEdfvl/+dq0JX8oHBQJgdvR7AhsMBQkDwmcA 41 | AAoJEF/+dq0JX8oH7d0L/3XvJiaH5Fxc18+/WGwT0VGlcMlLOfUDF0Tkv2PmjW1L 42 | eZ4UFObKYH+OgGmF03rlWltcOYTfIGQLcVultDZZbcqLatMyT+WMqyFryv6KPhAD 43 | EhZ8MI8X/a3Md2lDwWDKKaIqfW8HaaI9tpWnPg+Fn3yhA67zAdW89+meiaLPFSrQ 44 | iK3g8g6IlkveZHCbeMf/7CHhtzedblBdHFWlVwG9whP4aOlaUbBD8BTROCSNx5g0 45 | ESu+elINpBzNfKz5ageseifQJltMbsVo3llNM4iag4ndiAWogY+fHNmlsL804oEw 46 | VbWvAuyQpFhn3iqzURjPeoH/LxjsqcwHRiOCroNy/oa/bCmm3avHdfxPyco6O6oC 47 | WOHkC6abAO+OjbEmYFIrhqPTmjAI1699mosrkXX+X3ZWctpJ+jiXSAI1oednXJxo 48 | MdAzwxk9v43AK9t9qrsIyhvBlaPTt1e+6bDUTgEL/bvzZzzdRjB+q2FqHZjjQozc 49 | CB6QneDtkVVQsCRwyZy9ookBvAQYAQgAJgIbDBYhBNFkphxp4jwPdEdfvl/+dq0J 50 | X8oHBQJkQS/gBQkHjKJlAAoJEF/+dq0JX8oHE8sL/0EYl8eiuzxU3zp3/Praho8f 51 | nf+vNHNrghAQbYrsuRGOVj1ddDO4FiUIAsWNl9osMMdPhDUROqdQKTjR3JJ/ANHI 52 | V2d6UOjOeWkt3hgHncGengF+6kTVun8DgA1v0iru0IHbHb63aNHU5OE+jfcaxOZi 53 | o3uzyysTx8BAzi3h8u4nLVt57msN79E8WcYUwnQu3IjyBzbMZiRiuc//8cAy18IU 54 | jr3W4ASIdY/CaqYSt/m3rSREiLyB3iWlfQiCgpMhq/rl6jtzCXo8JDTv5I0dJ5ZW 55 | uSplBcvYDL6N8aJEvTEHVSBApCNPic6wzZh3q9bgB1GOdlj3u5Gz+cMZUfd+QBG8 56 | oZRlDiAce9PCQYqD4O4ofDVrRb9QdpWgAY6hmGgaAcFBIkjiNClhAU0AHy7S1LcA 57 | pMtmv7F1EFkw/Ox9YAIFKftVPZI31nxJN/de5EubHD0zrYVE1jdHQRws+RYiI2GN 58 | Rb54kkE9q5Bic8IcaVOf+stxdY785oXFd8pIideOVA== 59 | =e5EY 60 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /config/gpg/liam.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 40 | =0Zai 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /config/gpg/toby.gpg.public: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 105 | VryshT5m6VKxVqJv 106 | =vHmq 107 | -----END PGP PUBLIC KEY BLOCK----- 108 | -------------------------------------------------------------------------------- /config/hiera.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | :backends: 3 | - "env" 4 | - "overrides" 5 | - "yaml" 6 | :logger: "noop" 7 | :yaml: 8 | :datadir: "config" 9 | :hierarchy: 10 | - "roles/%{role}" 11 | - "defaults" 12 | -------------------------------------------------------------------------------- /config/roles/full.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | configuration_directory: "%{cwd}/examples/full" 3 | state_file: "%{cwd}/state/full.tfstate" 4 | vars: 5 | region: "%{hiera('region')}" 6 | 7 | component: "%{hiera('component')}" 8 | deployment_identifier: "%{hiera('deployment_identifier')}" 9 | 10 | vpc_cidr: "%{hiera('vpc_cidr')}" 11 | availability_zones: "%{hiera('availability_zones')}" 12 | -------------------------------------------------------------------------------- /config/roles/prerequisites.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | configuration_directory: "%{cwd}/spec/unit/infra/prerequisites" 3 | state_file: "%{cwd}/state/prerequisites.tfstate" 4 | vars: 5 | region: "%{hiera('region')}" 6 | 7 | vpc_cidr: "%{hiera('vpc_cidr')}" 8 | availability_zones: "%{hiera('availability_zones')}" 9 | 10 | component: "%{hiera('component')}" 11 | deployment_identifier: "%{hiera('deployment_identifier')}" 12 | 13 | private_zone_vpc_id: "%{hiera('private_zone_vpc_id')}" 14 | -------------------------------------------------------------------------------- /config/roles/root.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | configuration_directory: "%{cwd}/spec/unit/infra/root" 3 | state_file: "%{cwd}/state/root.tfstate" 4 | vars: 5 | region: "%{hiera('region')}" 6 | 7 | component: "%{hiera('component')}" 8 | deployment_identifier: "%{hiera('deployment_identifier')}" 9 | 10 | tags: "%{hiera('tags')}" 11 | -------------------------------------------------------------------------------- /config/secrets/.unlocked: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/.unlocked -------------------------------------------------------------------------------- /config/secrets/ci/aws-credentials.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/aws-credentials.sh -------------------------------------------------------------------------------- /config/secrets/ci/encryption.passphrase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/encryption.passphrase -------------------------------------------------------------------------------- /config/secrets/ci/gpg.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/gpg.private -------------------------------------------------------------------------------- /config/secrets/ci/gpg.public: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/gpg.public -------------------------------------------------------------------------------- /config/secrets/ci/ssh.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/ssh.private -------------------------------------------------------------------------------- /config/secrets/ci/ssh.public: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/ci/ssh.public -------------------------------------------------------------------------------- /config/secrets/circle_ci/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/circle_ci/config.yaml -------------------------------------------------------------------------------- /config/secrets/cluster/ssh.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/cluster/ssh.private -------------------------------------------------------------------------------- /config/secrets/cluster/ssh.public: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/cluster/ssh.public -------------------------------------------------------------------------------- /config/secrets/github/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/config/secrets/github/config.yaml -------------------------------------------------------------------------------- /docs/architecture.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/data.plist -------------------------------------------------------------------------------- /docs/architecture.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image1.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image11.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image12.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image12.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image13.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image13.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image14.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image14.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image15.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image15.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image16.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image16.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image3.pdf -------------------------------------------------------------------------------- /docs/architecture.graffle/image4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.graffle/image4.pdf -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/docs/architecture.png -------------------------------------------------------------------------------- /examples/full/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.33.0" 6 | constraints = ">= 3.74.0, 4.33.0" 7 | hashes = [ 8 | "h1:0S9ZXYg6K0CTOJUTQnoH94YrKuOYyJYEcc+hN5qGafA=", 9 | "h1:rLRYOeKvU17Tky5dleZwTPRoWtbdFTG/jOF/fTP2otY=", 10 | "zh:421b24e21d7fac4d65d97438d2c0a4effe71d3a1bd15820d6fde2879e49fe817", 11 | "zh:4378a84ca8e2a6990f47abc24367b801e884be928671b37ad7b8e7b656f73e48", 12 | "zh:54e0d7884edf3cefd096715794d32b6532138dca905f0b2fe84fb2117594293c", 13 | "zh:6269a7d0312057db5ded669e9f7f9bd80fb6dcb549b50d8d7f3f3b2a0361b8a5", 14 | "zh:67f57d16aa3db493a3174c3c5f30385c7af9767c4e3cdca14e5a4bf384ff59d9", 15 | "zh:7d4d4a1d963e431ffdc3348e3a578d3ba0fa782b1f4bf55fd5c0e527d24fed81", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:cd8e3d32485acb49c1b06f63916fec8e73a4caa6cf88ae9c4bf236d6f5d9b914", 18 | "zh:d586fd01195bd3775346495e61806e79b6012e745dc05e31a30b958acf968abe", 19 | "zh:d76122060f25ab87887a743096a42d47ba091c2c019ac13ce6b3973b2babe5a3", 20 | "zh:e917d36fe18eddc42ec743b3152b4dcb4853b75ea7a679abd19bdf271bc48221", 21 | "zh:eb780860d5c04f43a018aef564e76a2d84e9aa68984fa1f968ca8c09d23a611a", 22 | ] 23 | } 24 | 25 | provider "registry.terraform.io/hashicorp/null" { 26 | version = "3.2.0" 27 | constraints = ">= 3.0.0" 28 | hashes = [ 29 | "h1:6yiJqQ6JAJW3oMxuZrWoUgHYpkscorX40Q/LzOMzY+w=", 30 | "h1:ZbuTqXe8q7Z0IJ2wkF4nio7eZDQc02sezY0esJ5b1Bc=", 31 | "zh:1d88ea3af09dcf91ad0aaa0d3978ca8dcb49dc866c8615202b738d73395af6b5", 32 | "zh:3844db77bfac2aca43aaa46f3f698c8e5320a47e838ee1318408663449547e7e", 33 | "zh:538fadbd87c576a332b7524f352e6004f94c27afdd3b5d105820d328dc49c5e3", 34 | "zh:56def6f00fc2bc9c3c265b841ce71e80b77e319de7b0f662425b8e5e7eb26846", 35 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 36 | "zh:8fce56e5f1d13041d8047a1d0c93f930509704813a28f8d39c2b2082d7eebf9f", 37 | "zh:989e909a5eca96b8bdd4a0e8609f1bd525949fd226ae870acedf2da0c55b0451", 38 | "zh:99ddc34ad13e04e9c3477f5422fbec20fc13395ff940720c287bfa5c546d2fbc", 39 | "zh:b546666da4b4b60c0eec23faab7f94dc900e48f66b5436fc1ac0b87c6709ef04", 40 | "zh:d56643cb08cba6e074d70c4af37d5de2bd7c505f81d866d6d47c9e1d28ec65d1", 41 | "zh:f39ac5ff9e9d00e6a670bce6825529eded4b0b4966abba36a387db5f0712d7ba", 42 | "zh:fe102389facd09776502327352be99becc1ac09e80bc287db84a268172be641f", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/full/cluster.tf: -------------------------------------------------------------------------------- 1 | module "ecs_cluster" { 2 | source = "../../" 3 | 4 | region = "eu-west-2" 5 | vpc_id = module.base_network.vpc_id 6 | subnet_ids = module.base_network.private_subnet_ids 7 | 8 | component = var.component 9 | deployment_identifier = var.deployment_identifier 10 | 11 | cluster_name = "services" 12 | cluster_instance_type = "t2.small" 13 | 14 | cluster_minimum_size = 2 15 | cluster_maximum_size = 10 16 | cluster_desired_capacity = 4 17 | 18 | cluster_instance_root_block_device_size = 30 19 | cluster_instance_root_block_device_type = "standard" 20 | cluster_instance_root_block_device_path = "/dev/xvda" 21 | 22 | cluster_log_group_retention = 0 23 | 24 | enable_detailed_monitoring = true 25 | 26 | security_groups = aws_security_group.custom_security_group[*].id 27 | 28 | include_asg_capacity_provider = true 29 | } 30 | -------------------------------------------------------------------------------- /examples/full/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_id" { 2 | value = module.ecs_cluster.cluster_id 3 | } 4 | 5 | output "cluster_name" { 6 | value = module.ecs_cluster.cluster_name 7 | } 8 | 9 | output "cluster_arn" { 10 | value = module.ecs_cluster.cluster_arn 11 | } 12 | 13 | output "autoscaling_group_name" { 14 | value = module.ecs_cluster.autoscaling_group_name 15 | } 16 | 17 | output "autoscaling_group_arn" { 18 | value = module.ecs_cluster.autoscaling_group_arn 19 | } 20 | 21 | output "launch_template_name" { 22 | value = module.ecs_cluster.launch_template_name 23 | } 24 | 25 | output "launch_template_id" { 26 | value = module.ecs_cluster.launch_template_id 27 | } 28 | 29 | output "security_group_id" { 30 | value = module.ecs_cluster.security_group_id 31 | } 32 | 33 | output "custom_security_group_ids" { 34 | value = aws_security_group.custom_security_group.*.id 35 | } 36 | 37 | output "instance_role_arn" { 38 | value = module.ecs_cluster.instance_role_arn 39 | } 40 | 41 | output "instance_role_id" { 42 | value = module.ecs_cluster.instance_role_id 43 | } 44 | 45 | output "instance_policy_arn" { 46 | value = module.ecs_cluster.instance_policy_arn 47 | } 48 | 49 | output "instance_policy_id" { 50 | value = module.ecs_cluster.instance_policy_id 51 | } 52 | 53 | output "service_role_arn" { 54 | value = module.ecs_cluster.service_role_arn 55 | } 56 | 57 | output "service_role_id" { 58 | value = module.ecs_cluster.service_role_id 59 | } 60 | 61 | output "service_policy_arn" { 62 | value = module.ecs_cluster.service_policy_arn 63 | } 64 | 65 | output "service_policy_id" { 66 | value = module.ecs_cluster.service_policy_id 67 | } 68 | 69 | output "log_group" { 70 | value = module.ecs_cluster.log_group 71 | } 72 | 73 | output "asg_capacity_provider_name" { 74 | value = module.ecs_cluster.asg_capacity_provider_name 75 | } 76 | -------------------------------------------------------------------------------- /examples/full/prerequisites.tf: -------------------------------------------------------------------------------- 1 | module "base_network" { 2 | source = "infrablocks/base-networking/aws" 3 | version = "4.0.0" 4 | 5 | vpc_cidr = var.vpc_cidr 6 | region = var.region 7 | availability_zones = var.availability_zones 8 | 9 | component = var.component 10 | deployment_identifier = var.deployment_identifier 11 | 12 | private_zone_id = module.dns-zones.private_zone_id 13 | } 14 | 15 | resource "aws_default_vpc" "default" {} 16 | 17 | module "dns-zones" { 18 | source = "infrablocks/dns-zones/aws" 19 | version = "1.0.0" 20 | 21 | domain_name = "infrablocks-ecs-cluster-example.com" 22 | private_domain_name = "infrablocks-ecs-cluster-example.net" 23 | private_zone_vpc_id = aws_default_vpc.default.id 24 | private_zone_vpc_region = var.region 25 | } 26 | 27 | resource "aws_security_group" "custom_security_group" { 28 | count = 2 29 | name = "${var.component}-${var.deployment_identifier}-${count.index}" 30 | description = "Custom security group for component: ${var.component}, deployment: ${var.deployment_identifier}" 31 | vpc_id = module.base_network.vpc_id 32 | 33 | tags = { 34 | Name = "${var.component}-${var.deployment_identifier}-${count.index}" 35 | Component = var.component 36 | DeploymentIdentifier = var.deployment_identifier 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/full/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | -------------------------------------------------------------------------------- /examples/full/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "4.33" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/full/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | 3 | variable "component" {} 4 | variable "deployment_identifier" {} 5 | 6 | variable "vpc_cidr" {} 7 | 8 | variable "availability_zones" { 9 | type = list(string) 10 | } 11 | 12 | variable "security_groups" { 13 | default = [] 14 | } 15 | -------------------------------------------------------------------------------- /go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$GO_DEBUG" ] && set -x 4 | set -e 5 | 6 | project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | 8 | verbose="no" 9 | offline="no" 10 | skip_checks="no" 11 | 12 | missing_dependency="no" 13 | 14 | [ -n "$GO_DEBUG" ] && verbose="yes" 15 | [ -n "$GO_SKIP_CHECKS" ] && skip_checks="yes" 16 | [ -n "$GO_OFFLINE" ] && offline="yes" 17 | 18 | function loose_version() { 19 | local version="$1" 20 | 21 | IFS="." read -r -a version_parts <<<"$version" 22 | 23 | echo "${version_parts[0]}.${version_parts[1]}" 24 | } 25 | 26 | function read_version() { 27 | local tool="$1" 28 | local tool_versions 29 | 30 | tool_versions="$(cat "$project_dir"/.tool-versions)" 31 | 32 | echo "$tool_versions" | grep "$tool" | cut -d ' ' -f 2 33 | } 34 | 35 | ruby_full_version="$(read_version "ruby")" 36 | ruby_loose_version="$(loose_version "$ruby_full_version")" 37 | 38 | if [[ "$skip_checks" == "no" ]]; then 39 | if ! type ruby >/dev/null 2>&1 || ! ruby -v | grep -q "$ruby_loose_version"; then 40 | echo "This codebase requires Ruby $ruby_loose_version." 41 | missing_dependency="yes" 42 | fi 43 | 44 | if [[ "$missing_dependency" = "yes" ]]; then 45 | echo "Please install missing dependencies to continue." 46 | exit 1 47 | fi 48 | 49 | echo "All system dependencies present. Continuing." 50 | fi 51 | 52 | if [[ "$offline" = "no" ]]; then 53 | echo "Installing ruby dependencies." 54 | if [[ "$verbose" = "yes" ]]; then 55 | bundle install 56 | else 57 | bundle install >/dev/null 58 | fi 59 | fi 60 | 61 | echo "Starting rake." 62 | if [[ "$verbose" = "yes" ]]; then 63 | time bundle exec rake --verbose "$@" 64 | else 65 | time bundle exec rake "$@" 66 | fi 67 | -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | cluster_instance_policy_contents = coalesce( 3 | var.cluster_instance_iam_policy_contents, 4 | file("${path.module}/policies/cluster-instance-policy.json")) 5 | } 6 | 7 | resource "aws_iam_role" "cluster_instance_role" { 8 | count = var.include_cluster_instances ? 1 : 0 9 | 10 | description = "cluster-instance-role-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 11 | assume_role_policy = file("${path.module}/policies/cluster-instance-role.json") 12 | 13 | tags = local.tags 14 | } 15 | 16 | resource "aws_iam_policy" "cluster_instance_policy" { 17 | count = var.include_cluster_instances ? 1 : 0 18 | 19 | description = "cluster-instance-policy-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 20 | policy = local.cluster_instance_policy_contents 21 | } 22 | 23 | resource "aws_iam_policy_attachment" "cluster_instance_policy_attachment" { 24 | count = var.include_cluster_instances ? 1 : 0 25 | 26 | name = "cluster-instance-policy-attachment-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 27 | roles = [aws_iam_role.cluster_instance_role[0].id] 28 | policy_arn = aws_iam_policy.cluster_instance_policy[0].arn 29 | } 30 | 31 | resource "aws_iam_instance_profile" "cluster" { 32 | count = var.include_cluster_instances ? 1 : 0 33 | 34 | name = "cluster-instance-profile-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 35 | path = "/" 36 | role = aws_iam_role.cluster_instance_role[0].name 37 | } 38 | 39 | resource "aws_iam_role" "cluster_service_role" { 40 | description = "cluster-service-role-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 41 | assume_role_policy = file("${path.module}/policies/cluster-service-role.json") 42 | 43 | tags = local.tags 44 | } 45 | 46 | resource "aws_iam_policy" "cluster_service_policy" { 47 | description = "cluster-service-policy-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 48 | policy = coalesce(var.cluster_service_iam_policy_contents, file("${path.module}/policies/cluster-service-policy.json")) 49 | } 50 | 51 | resource "aws_iam_policy_attachment" "cluster_service_policy_attachment" { 52 | name = "cluster-instance-policy-attachment-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 53 | roles = [aws_iam_role.cluster_service_role.id] 54 | policy_arn = aws_iam_policy.cluster_service_policy.arn 55 | } 56 | 57 | resource "null_resource" "iam_wait" { 58 | depends_on = [ 59 | aws_iam_role.cluster_instance_role, 60 | aws_iam_policy.cluster_instance_policy, 61 | aws_iam_policy_attachment.cluster_instance_policy_attachment, 62 | aws_iam_instance_profile.cluster, 63 | aws_iam_role.cluster_service_role, 64 | aws_iam_policy.cluster_service_policy, 65 | aws_iam_policy_attachment.cluster_service_policy_attachment 66 | ] 67 | 68 | provisioner "local-exec" { 69 | command = "sleep 30" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /key.tf: -------------------------------------------------------------------------------- 1 | resource "aws_key_pair" "cluster" { 2 | count = var.cluster_instance_ssh_public_key_path == null ? 0 : 1 3 | key_name = "cluster-${var.component}-${var.deployment_identifier}-${var.cluster_name}" 4 | public_key = file(var.cluster_instance_ssh_public_key_path) 5 | } 6 | -------------------------------------------------------------------------------- /lib/paths.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Paths 4 | class << self 5 | def project_root_directory 6 | join_and_expand(self_directory, '..') 7 | end 8 | 9 | def from_project_root_directory(*segments) 10 | join_and_expand(project_root_directory, *segments) 11 | end 12 | 13 | def join_and_expand(*segments) 14 | File.expand_path(join(*segments)) 15 | end 16 | 17 | def join(*segments) 18 | File.join(*segments.compact) 19 | end 20 | 21 | def self_directory 22 | File.dirname(__FILE__) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Semantic 4 | module Extensions 5 | def release! 6 | unless prerelease? 7 | raise 'Error: no pre segment, ' \ 8 | 'this version is not a pre-release version.' 9 | end 10 | 11 | new_version = clone 12 | new_version.build = new_version.pre = nil 13 | new_version 14 | end 15 | 16 | def rc! 17 | return start_rc if release? 18 | return increment_rc if rc? 19 | 20 | raise "Error: pre segment '#{pre}' does not look like 'rc.n'." 21 | end 22 | 23 | private 24 | 25 | def start_rc 26 | new_version = clone 27 | new_version = new_version.increment!(:minor) 28 | new_version.pre = 'rc.1' 29 | new_version 30 | end 31 | 32 | def increment_rc 33 | new_version = clone 34 | new_version.pre = "rc.#{Integer(new_version.pre.delete('rc.')) + 1}" 35 | new_version 36 | end 37 | 38 | def release? 39 | pre.nil? 40 | end 41 | 42 | def prerelease? 43 | !release? 44 | end 45 | 46 | def rc? 47 | pre =~ /^rc\.\d+$/ 48 | end 49 | end 50 | 51 | class Version 52 | prepend Extensions 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | locals { 4 | base_tags = { 5 | Component = var.component 6 | DeploymentIdentifier = var.deployment_identifier 7 | } 8 | 9 | tags = merge(var.tags, local.base_tags) 10 | 11 | cluster_instance_ebs_volume_kms_key_id = var.cluster_instance_ebs_volume_kms_key_id == null ? "arn:aws:kms:${var.region}:${data.aws_caller_identity.current.account_id}:alias/aws/ebs" : var.cluster_instance_ebs_volume_kms_key_id 12 | } 13 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrablocks/terraform-aws-ecs-cluster/c92b68602ede9cbc7efc21ed5642e210f22df5d6/main.tf -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_id" { 2 | description = "The ID of the created ECS cluster." 3 | value = aws_ecs_cluster.cluster.id 4 | } 5 | 6 | output "cluster_name" { 7 | description = "The name of the created ECS cluster." 8 | value = aws_ecs_cluster.cluster.name 9 | } 10 | 11 | output "cluster_arn" { 12 | description = "The ARN of the created ECS cluster." 13 | value = aws_ecs_cluster.cluster.arn 14 | } 15 | 16 | output "autoscaling_group_name" { 17 | description = "The name of the autoscaling group for the ECS container instances." 18 | value = try(aws_autoscaling_group.cluster[0].name, "") 19 | } 20 | 21 | output "autoscaling_group_arn" { 22 | description = "The ARN of the autoscaling group for the ECS container instances." 23 | value = try(aws_autoscaling_group.cluster[0].arn, "") 24 | } 25 | 26 | output "launch_template_name" { 27 | description = "The name of the launch template for the ECS container instances." 28 | value = try(aws_launch_template.cluster[0].name, "") 29 | } 30 | 31 | output "launch_template_id" { 32 | description = "The id of the launch template for the ECS container instances." 33 | value = try(aws_launch_template.cluster[0].id, "") 34 | } 35 | 36 | output "security_group_id" { 37 | description = "The ID of the default security group associated with the ECS container instances." 38 | value = try(aws_security_group.cluster[0].id, "") 39 | } 40 | 41 | output "instance_role_arn" { 42 | description = "The ARN of the container instance role." 43 | value = try(aws_iam_role.cluster_instance_role[0].arn, "") 44 | } 45 | 46 | output "instance_role_id" { 47 | description = "The ID of the container instance role." 48 | value = try(aws_iam_role.cluster_instance_role[0].unique_id, "") 49 | } 50 | 51 | output "instance_policy_arn" { 52 | description = "The ARN of the container instance policy." 53 | value = try(aws_iam_policy.cluster_instance_policy[0].arn, "") 54 | } 55 | 56 | output "instance_policy_id" { 57 | description = "The ID of the container instance policy." 58 | value = try(aws_iam_policy.cluster_instance_policy[0].id, "") 59 | } 60 | 61 | output "service_role_arn" { 62 | description = "The ARN of the ECS service role." 63 | value = aws_iam_role.cluster_service_role.arn 64 | } 65 | 66 | output "service_role_id" { 67 | description = "The ID of the ECS service role." 68 | value = aws_iam_role.cluster_service_role.unique_id 69 | } 70 | 71 | output "service_policy_arn" { 72 | description = "The ARN of the ECS service policy." 73 | value = aws_iam_policy.cluster_service_policy.arn 74 | } 75 | 76 | output "service_policy_id" { 77 | description = "The ID of the ECS service policy." 78 | value = aws_iam_policy.cluster_service_policy.id 79 | } 80 | 81 | output "log_group" { 82 | description = "The name of the default log group for the cluster." 83 | value = aws_cloudwatch_log_group.cluster.name 84 | } 85 | 86 | output "asg_capacity_provider_name" { 87 | description = "The name of the ASG capacity provider associated with the cluster." 88 | value = try(aws_ecs_capacity_provider.autoscaling_group[0].name, "") 89 | } 90 | -------------------------------------------------------------------------------- /policies/cluster-instance-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ecs:CreateCluster", 8 | "ecs:DeregisterContainerInstance", 9 | "ecs:DiscoverPollEndpoint", 10 | "ecs:Poll", 11 | "ecs:RegisterContainerInstance", 12 | "ecs:StartTelemetrySession", 13 | "ecs:Submit*", 14 | "ecr:GetAuthorizationToken", 15 | "ecr:GetDownloadUrlForLayer", 16 | "ecr:BatchGetImage", 17 | "ecr:BatchCheckLayerAvailability", 18 | "s3:GetObject", 19 | "logs:CreateLogStream", 20 | "logs:PutLogEvents" 21 | ], 22 | "Resource": "*" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /policies/cluster-instance-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": ["ec2.amazonaws.com"] 8 | }, 9 | "Effect": "Allow" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /policies/cluster-service-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "elasticloadbalancing:Describe*", 8 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 9 | "elasticloadbalancing:DeregisterTargets", 10 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 11 | "elasticloadbalancing:RegisterTargets", 12 | "ec2:Describe*", 13 | "ec2:AuthorizeSecurityGroupIngress" 14 | ], 15 | "Resource": "*" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /policies/cluster-service-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": ["ecs.amazonaws.com"] 8 | }, 9 | "Effect": "Allow" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /scripts/ci/common/configure-git.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | git config --global user.email "circleci@infrablocks.io" 8 | git config --global user.name "Circle CI" 9 | -------------------------------------------------------------------------------- /scripts/ci/common/install-git-crypt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | apt-get update 8 | apt-get install -y --no-install-recommends git ssh git-crypt 9 | -------------------------------------------------------------------------------- /scripts/ci/common/install-gpg-key.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | set +e 13 | openssl version 14 | openssl aes-256-cbc \ 15 | -d \ 16 | -md sha1 \ 17 | -in ./.circleci/gpg.private.enc \ 18 | -k "${ENCRYPTION_PASSPHRASE}" | gpg --import - 19 | set -e 20 | -------------------------------------------------------------------------------- /scripts/ci/common/install-orb-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | apt-get update 8 | apt-get install -y --no-install-recommends jq 9 | -------------------------------------------------------------------------------- /scripts/ci/steps/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | ./go test:code:check 13 | -------------------------------------------------------------------------------- /scripts/ci/steps/merge-pull-request.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | git-crypt unlock 13 | 14 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 15 | 16 | ./go github:pull_requests:merge["$CURRENT_BRANCH","%s [skip ci]"] 17 | -------------------------------------------------------------------------------- /scripts/ci/steps/prerelease.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | set +e 13 | openssl version 14 | openssl aes-256-cbc \ 15 | -d \ 16 | -md sha1 \ 17 | -in ./.circleci/gpg.private.enc \ 18 | -k "${ENCRYPTION_PASSPHRASE}" | gpg --import - 19 | set -e 20 | 21 | git crypt unlock 22 | 23 | ./go version:bump[rc] 24 | -------------------------------------------------------------------------------- /scripts/ci/steps/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | set +e 13 | openssl version 14 | openssl aes-256-cbc \ 15 | -d \ 16 | -md sha1 \ 17 | -in ./.circleci/gpg.private.enc \ 18 | -k "${ENCRYPTION_PASSPHRASE}" | gpg --import - 19 | set -e 20 | 21 | git crypt unlock 22 | 23 | ./go version:release 24 | -------------------------------------------------------------------------------- /scripts/ci/steps/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$DEBUG" ] && set -x 4 | set -e 5 | set -o pipefail 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | PROJECT_DIR="$( cd "$SCRIPT_DIR/../../.." && pwd )" 9 | 10 | cd "$PROJECT_DIR" 11 | 12 | set +e 13 | openssl version 14 | openssl aes-256-cbc \ 15 | -d \ 16 | -md sha1 \ 17 | -in ./.circleci/gpg.private.enc \ 18 | -k "${ENCRYPTION_PASSPHRASE}" | gpg --import - 19 | set -e 20 | 21 | git crypt unlock 22 | 23 | source config/secrets/ci/aws-credentials.sh 24 | 25 | ./go test:unit 26 | ./go test:integration 27 | -------------------------------------------------------------------------------- /security_groups.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "cluster" { 2 | count = var.include_cluster_instances ? 1 : 0 3 | 4 | name = "${var.component}-${var.deployment_identifier}-${var.cluster_name}" 5 | description = "Container access for component: ${var.component}, deployment: ${var.deployment_identifier}, cluster: ${var.cluster_name}" 6 | vpc_id = var.vpc_id 7 | 8 | tags = merge(local.tags, { 9 | Name = "${var.component}-${var.deployment_identifier}-${var.cluster_name}" 10 | ClusterName = var.cluster_name 11 | }) 12 | } 13 | 14 | resource "aws_security_group_rule" "cluster_default_ingress" { 15 | count = (var.include_cluster_instances && var.include_default_ingress_rule) ? 1 : 0 16 | 17 | type = "ingress" 18 | 19 | security_group_id = aws_security_group.cluster[0].id 20 | 21 | protocol = "-1" 22 | from_port = 0 23 | to_port = 0 24 | 25 | cidr_blocks = var.default_ingress_cidrs 26 | } 27 | 28 | resource "aws_security_group_rule" "cluster_default_egress" { 29 | count = (var.include_cluster_instances && var.include_default_egress_rule) ? 1 : 0 30 | 31 | type = "egress" 32 | 33 | security_group_id = aws_security_group.cluster[0].id 34 | 35 | protocol = "-1" 36 | from_port = 0 37 | to_port = 0 38 | 39 | cidr_blocks = var.default_egress_cidrs 40 | } 41 | -------------------------------------------------------------------------------- /spec/integration/full_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | # rubocop:disable RSpec/MultipleMemoizedHelpers 6 | describe 'full example' do 7 | before(:context) do 8 | apply(role: :full) 9 | end 10 | 11 | after(:context) do 12 | destroy( 13 | role: :full, 14 | only_if: -> { !ENV['FORCE_DESTROY'].nil? || ENV['SEED'].nil? } 15 | ) 16 | end 17 | 18 | let(:component) do 19 | var(role: :full, name: 'component') 20 | end 21 | let(:deployment_identifier) do 22 | var(role: :full, name: 'deployment_identifier') 23 | end 24 | let(:cluster_name) do 25 | output(role: :full, name: 'cluster_name') 26 | end 27 | let(:autoscaling_group_name) do 28 | output(role: :full, name: 'autoscaling_group_name') 29 | end 30 | let(:autoscaling_group_arn) do 31 | output(role: :full, name: 'autoscaling_group_arn') 32 | end 33 | let(:launch_template_name) do 34 | output(role: :full, name: 'launch_template_name') 35 | end 36 | let(:launch_template_id) do 37 | output(role: :full, name: 'launch_template_id') 38 | end 39 | let(:log_group) do 40 | output(role: :full, name: 'log_group') 41 | end 42 | let(:instance_role_id) do 43 | output(role: :full, name: 'instance_role_id') 44 | end 45 | let(:instance_role_arn) do 46 | output(role: :full, name: 'instance_role_arn') 47 | end 48 | let(:instance_policy_id) do 49 | output(role: :full, name: 'instance_policy_id') 50 | end 51 | let(:instance_policy_arn) do 52 | output(role: :full, name: 'instance_policy_arn') 53 | end 54 | let(:service_role_id) do 55 | output(role: :full, name: 'service_role_id') 56 | end 57 | let(:service_role_arn) do 58 | output(role: :full, name: 'service_role_arn') 59 | end 60 | let(:service_policy_id) do 61 | output(role: :full, name: 'service_policy_id') 62 | end 63 | let(:service_policy_arn) do 64 | output(role: :full, name: 'service_policy_arn') 65 | end 66 | 67 | describe 'Autoscaling Group' do 68 | subject(:auto_scaling_group) do 69 | autoscaling_group(autoscaling_group_name) 70 | end 71 | 72 | it { is_expected.to exist } 73 | 74 | it 'has an associated launch template' do 75 | expect(auto_scaling_group.launch_template.launch_template_name) 76 | .to(eq(launch_template_name)) 77 | end 78 | end 79 | 80 | describe 'ASG Capacity Provider' do 81 | context 'when capacity provider included' do 82 | let(:asg) do 83 | autoscaling_group( 84 | output_for(:harness, 'autoscaling_group_name') 85 | ) 86 | end 87 | let(:capacity_providers) do 88 | ecs_client = Aws::ECS::Client.new(region: 'eu-west-2') 89 | 90 | ecs_cluster(cluster_name) 91 | .capacity_providers 92 | .map do |cp| 93 | ecs_client 94 | .describe_capacity_providers(capacity_providers: [cp]) 95 | .capacity_providers[0] 96 | end 97 | end 98 | 99 | # rubocop:disable RSpec/MultipleExpectations 100 | it 'attaches the ASG as a capacity provider for the ECS cluster' do 101 | expect(capacity_providers.length).to(eq(1)) 102 | 103 | capacity_provider = capacity_providers.first 104 | 105 | expect(capacity_provider 106 | .auto_scaling_group_provider 107 | .auto_scaling_group_arn) 108 | .to(eq(autoscaling_group_arn)) 109 | end 110 | # rubocop:enable RSpec/MultipleExpectations 111 | end 112 | end 113 | 114 | describe 'CloudWatch' do 115 | let(:cloudwatch_log_group) do 116 | log_group_name = 117 | "/#{component}/#{deployment_identifier}/ecs-cluster/services" 118 | 119 | cloudwatch_logs_client = 120 | Aws::CloudWatchLogs::Client.new(region: 'eu-west-2') 121 | cloudwatch_logs_client 122 | .describe_log_groups({ log_group_name_prefix: log_group_name }) 123 | .log_groups 124 | .first 125 | end 126 | 127 | describe 'outputs' do 128 | it 'outputs the log group name' do 129 | expect(log_group).to(eq(cloudwatch_log_group.log_group_name)) 130 | end 131 | end 132 | end 133 | 134 | describe 'IAM policies, profiles and roles' do 135 | describe 'cluster instance profile' do 136 | subject(:instance_profile) do 137 | instance_profile_name = 138 | "cluster-instance-profile-#{cluster_name}" 139 | 140 | iam_client = Aws::IAM::Client.new(region: 'eu-west-2') 141 | iam_client 142 | .get_instance_profile({ instance_profile_name: }) 143 | .instance_profile 144 | end 145 | 146 | it 'has the cluster instance role' do 147 | expect(instance_profile.roles.first.role_name) 148 | .to(eq(iam_role(instance_role_id).name)) 149 | end 150 | end 151 | 152 | describe 'cluster instance role' do 153 | subject(:role) do 154 | iam_role(instance_role_id) 155 | end 156 | 157 | it { 158 | expect(role).to have_iam_policy(instance_policy_id) 159 | } 160 | end 161 | 162 | describe 'outputs' do 163 | let(:cluster_instance_iam_role) do 164 | iam_role(instance_role_id) 165 | end 166 | let(:cluster_service_iam_role) do 167 | iam_role(service_role_id) 168 | end 169 | let(:cluster_instance_iam_policy) do 170 | iam_policy(instance_policy_id) 171 | end 172 | let(:cluster_service_iam_policy) do 173 | iam_policy(service_policy_id) 174 | end 175 | 176 | it 'outputs instance role arn' do 177 | expect(instance_role_arn).to(eq(cluster_instance_iam_role.arn)) 178 | end 179 | 180 | it 'outputs instance role id' do 181 | expect(instance_role_id).to(eq(cluster_instance_iam_role.role_id)) 182 | end 183 | 184 | it 'outputs instance policy arn' do 185 | expect(instance_policy_arn).to(eq(cluster_instance_iam_policy.arn)) 186 | end 187 | 188 | it 'outputs service role arn' do 189 | expect(service_role_arn).to(eq(cluster_service_iam_role.arn)) 190 | end 191 | 192 | it 'outputs service role id' do 193 | expect(service_role_id).to(eq(cluster_service_iam_role.role_id)) 194 | end 195 | 196 | it 'outputs service policy arn' do 197 | expect(service_policy_arn).to(eq(cluster_service_iam_policy.arn)) 198 | end 199 | end 200 | end 201 | 202 | describe 'Launch Template' do 203 | let(:created_launch_template) do 204 | launch_template(launch_template_name) 205 | end 206 | 207 | let(:created_launch_template_version) do 208 | created_launch_template.launch_template_version 209 | end 210 | let(:created_launch_template_data) do 211 | created_launch_template_version.launch_template_data 212 | end 213 | 214 | let(:security_group_ids) do 215 | created_launch_template_data.network_interfaces[0].groups 216 | end 217 | 218 | it 'has id of launch_template_id output' do 219 | expect(created_launch_template.launch_template_id) 220 | .to(eq(launch_template_id)) 221 | end 222 | 223 | describe 'launch template name' do 224 | it 'contains the component' do 225 | expect(launch_template_name).to(match(/#{component}/)) 226 | end 227 | 228 | it 'contains the deployment identifier' do 229 | expect(launch_template_name) 230 | .to(match(/#{deployment_identifier}/)) 231 | end 232 | 233 | it 'contains the cluster name' do 234 | expect(launch_template_name).to(match(/#{cluster_name}/)) 235 | end 236 | end 237 | 238 | it 'does not add a docker block device' do 239 | expect(created_launch_template_data.block_device_mappings.size) 240 | .to(eq(1)) 241 | end 242 | 243 | it 'has correct number of security groups' do 244 | expect(security_group_ids.size).to(eq(3)) 245 | end 246 | 247 | it 'includes the custom security groups' do 248 | custom_security_group_ids = 249 | output(role: :full, name: 'custom_security_group_ids') 250 | expect(security_group_ids) 251 | .to(include(*custom_security_group_ids)) 252 | end 253 | 254 | it 'includes the default security group' do 255 | security_group_id = output(role: :full, name: 'security_group_id') 256 | expect(security_group_ids).to(include(security_group_id)) 257 | end 258 | 259 | it 'configures the ECS cluster in the user data script' do 260 | expect(created_launch_template_data.user_data) 261 | .to( 262 | eq(Base64.strict_encode64(<<~DOC)) 263 | #!/bin/bash 264 | echo "ECS_CLUSTER=#{cluster_name}" > /etc/ecs/ecs.config 265 | DOC 266 | ) 267 | end 268 | end 269 | end 270 | # rubocop:enable RSpec/MultipleMemoizedHelpers 271 | -------------------------------------------------------------------------------- /spec/integration/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | require 'aws-sdk' 6 | require 'awspec' 7 | require 'logger' 8 | require 'rspec' 9 | require 'rspec/terraform' 10 | require 'ruby_terraform' 11 | require 'stringio' 12 | 13 | Dir[File.join(__dir__, 'support', '**', '*.rb')] 14 | .each { |f| require f } 15 | 16 | RSpec.configure do |config| 17 | config.filter_run_when_matching :focus 18 | config.example_status_persistence_file_path = '.rspec_status' 19 | config.expect_with(:rspec) { |c| c.syntax = :expect } 20 | 21 | config.terraform_binary = 'vendor/terraform/bin/terraform' 22 | config.terraform_log_file_path = 'build/logs/integration.log' 23 | config.terraform_log_streams = [:file] 24 | config.terraform_configuration_provider = 25 | RSpec::Terraform::Configuration.chain_provider( 26 | providers: [ 27 | RSpec::Terraform::Configuration.seed_provider, 28 | RSpec::Terraform::Configuration.in_memory_provider( 29 | no_color: true 30 | ), 31 | RSpec::Terraform::Configuration.confidante_provider( 32 | parameters: %i[ 33 | configuration_directory 34 | state_file 35 | vars 36 | ], 37 | scope_selector: ->(o) { o.slice(:role) } 38 | ) 39 | ] 40 | ) 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/autoscaling_group_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'autoscaling group' do 6 | describe 'by default' do 7 | let(:component) do 8 | var(role: :root, name: 'component') 9 | end 10 | let(:deployment_identifier) do 11 | var(role: :root, name: 'deployment_identifier') 12 | end 13 | let(:cluster_name) do 14 | var(role: :root, name: 'cluster_name') 15 | end 16 | let(:private_subnet_ids) do 17 | output(role: :prerequisites, name: 'private_subnet_ids') 18 | end 19 | 20 | before(:context) do 21 | @plan = plan(role: :root) 22 | end 23 | 24 | it 'creates an autoscaling group' do 25 | expect(@plan) 26 | .to(include_resource_creation(type: 'aws_autoscaling_group') 27 | .once) 28 | end 29 | 30 | it 'uses a minimum cluster size of 1' do 31 | expect(@plan) 32 | .to(include_resource_creation(type: 'aws_autoscaling_group') 33 | .with_attribute_value(:min_size, 1)) 34 | end 35 | 36 | it 'uses all private subnets' do 37 | expect(@plan) 38 | .to(include_resource_creation(type: 'aws_autoscaling_group') 39 | .with_attribute_value( 40 | :vpc_zone_identifier, 41 | containing_exactly(*private_subnet_ids) 42 | )) 43 | end 44 | 45 | describe 'tags' do 46 | it 'has Name' do 47 | expect(@plan) 48 | .to(include_resource_creation(type: 'aws_autoscaling_group') 49 | .with_attribute_value( 50 | :tag, 51 | including({ 52 | key: 'Name', 53 | propagate_at_launch: true, 54 | value: "cluster-worker-#{component}-" \ 55 | "#{deployment_identifier}-default" 56 | }) 57 | )) 58 | end 59 | 60 | it 'has ClusterName' do 61 | expect(@plan) 62 | .to(include_resource_creation(type: 'aws_autoscaling_group') 63 | .with_attribute_value( 64 | :tag, 65 | including({ 66 | key: 'ClusterName', 67 | propagate_at_launch: true, 68 | value: 'default' 69 | }) 70 | )) 71 | end 72 | 73 | it 'has Component' do 74 | expect(@plan) 75 | .to(include_resource_creation(type: 'aws_autoscaling_group') 76 | .with_attribute_value( 77 | :tag, 78 | including({ 79 | key: 'Component', 80 | propagate_at_launch: true, 81 | value: component 82 | }) 83 | )) 84 | end 85 | 86 | it 'has DeploymentIdentifier' do 87 | expect(@plan) 88 | .to(include_resource_creation(type: 'aws_autoscaling_group') 89 | .with_attribute_value( 90 | :tag, 91 | including({ 92 | key: 'DeploymentIdentifier', 93 | propagate_at_launch: true, 94 | value: deployment_identifier 95 | }) 96 | )) 97 | end 98 | 99 | it 'has ImportantTag' do 100 | expect(@plan) 101 | .to(include_resource_creation(type: 'aws_autoscaling_group') 102 | .with_attribute_value( 103 | :tag, 104 | including({ 105 | key: 'ImportantTag', 106 | propagate_at_launch: true, 107 | value: 'important-value' 108 | }) 109 | )) 110 | end 111 | end 112 | end 113 | 114 | context 'when cluster sizes provided' do 115 | before(:context) do 116 | @plan = plan(role: :root) do |vars| 117 | vars.cluster_minimum_size = 2 118 | vars.cluster_maximum_size = 5 119 | vars.cluster_desired_capacity = 3 120 | end 121 | end 122 | 123 | it 'uses the provided minimum cluster size' do 124 | expect(@plan) 125 | .to(include_resource_creation(type: 'aws_autoscaling_group') 126 | .with_attribute_value(:min_size, 2)) 127 | end 128 | 129 | it 'uses the provided maximum cluster size' do 130 | expect(@plan) 131 | .to(include_resource_creation(type: 'aws_autoscaling_group') 132 | .with_attribute_value(:max_size, 5)) 133 | end 134 | 135 | it 'uses the provided desired cluster capacity' do 136 | expect(@plan) 137 | .to(include_resource_creation(type: 'aws_autoscaling_group') 138 | .with_attribute_value(:desired_capacity, 3)) 139 | end 140 | end 141 | 142 | context 'when scale in protection enabled' do 143 | before(:context) do 144 | @plan = plan(role: :root) do |vars| 145 | vars.protect_cluster_instances_from_scale_in = true 146 | end 147 | end 148 | 149 | it 'has protect_from_scale_in set to true' do 150 | expect(@plan) 151 | .to(include_resource_creation(type: 'aws_autoscaling_group') 152 | .with_attribute_value(:protect_from_scale_in, true)) 153 | end 154 | end 155 | 156 | context 'when scale in protection disabled' do 157 | before(:context) do 158 | @plan = plan(role: :root) do |vars| 159 | vars.protect_cluster_instances_from_scale_in = false 160 | end 161 | end 162 | 163 | it 'has protect_from_scale_in set to false' do 164 | expect(@plan) 165 | .to(include_resource_creation(type: 'aws_autoscaling_group') 166 | .with_attribute_value(:protect_from_scale_in, false)) 167 | end 168 | end 169 | 170 | context 'when include_cluster_instances is false' do 171 | before(:context) do 172 | @plan = plan(role: :root) do |vars| 173 | vars.include_cluster_instances = false 174 | end 175 | end 176 | 177 | it 'does not create an autoscaling group' do 178 | expect(@plan) 179 | .not_to(include_resource_creation(type: 'aws_autoscaling_group')) 180 | end 181 | end 182 | 183 | context 'when include_cluster_instances is true' do 184 | before(:context) do 185 | @plan = plan(role: :root) do |vars| 186 | vars.include_cluster_instances = true 187 | end 188 | end 189 | 190 | it 'creates an autoscaling group' do 191 | expect(@plan) 192 | .to(include_resource_creation(type: 'aws_autoscaling_group') 193 | .once) 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /spec/unit/capacity_provider_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'ASG Capacity Provider' do 6 | let(:component) do 7 | var(role: :root, name: 'component') 8 | end 9 | let(:dep_id) do 10 | var(role: :root, name: 'deployment_identifier') 11 | end 12 | 13 | before(:context) do 14 | @plan = plan(role: :root) 15 | end 16 | 17 | context 'when include_asg_capacity_provider is true and\ 18 | include_cluster_instances is true' do 19 | describe 'by default' do 20 | before(:context) do 21 | @plan = plan(role: :root) do |vars| 22 | vars.include_asg_capacity_provider = true 23 | vars.include_cluster_instances = true 24 | end 25 | end 26 | 27 | # rubocop:disable RSpec/MultipleExpectations 28 | it 'attaches the ASG as a capacity provider for the ECS cluster' do 29 | expect(@plan) 30 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 31 | .once) 32 | 33 | expect(@plan) 34 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 35 | .with_attribute_value( 36 | :name, "cp-#{component}-#{dep_id}-default" 37 | )) 38 | end 39 | # rubocop:enable RSpec/MultipleExpectations 40 | 41 | it 'includes the AmazonECSManaged tag on the ASG' do 42 | expect(@plan) 43 | .to(include_resource_creation(type: 'aws_autoscaling_group') 44 | .with_attribute_value( 45 | :tag, 46 | including({ 47 | key: 'AmazonECSManaged', 48 | propagate_at_launch: true, 49 | value: '' 50 | }) 51 | )) 52 | end 53 | end 54 | 55 | context 'with managed termination protection' do 56 | before(:context) do 57 | @plan = plan(role: :root) do |vars| 58 | vars.include_cluster_instances = true 59 | vars.include_asg_capacity_provider = true 60 | vars.asg_capacity_provider_manage_termination_protection = true 61 | end 62 | end 63 | 64 | it 'enables managed termination protection' do 65 | expect(@plan) 66 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 67 | .with_attribute_value( 68 | [ 69 | :auto_scaling_group_provider, 70 | 0, 71 | :managed_termination_protection 72 | ], 73 | 'ENABLED' 74 | )) 75 | end 76 | end 77 | 78 | context 'without managed termination protection' do 79 | before(:context) do 80 | @plan = plan(role: :root) do |vars| 81 | vars.include_cluster_instances = true 82 | vars.include_asg_capacity_provider = true 83 | vars.asg_capacity_provider_manage_termination_protection = false 84 | end 85 | end 86 | 87 | it 'disables managed termination protection' do 88 | expect(@plan) 89 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 90 | .with_attribute_value( 91 | [ 92 | :auto_scaling_group_provider, 93 | 0, 94 | :managed_termination_protection 95 | ], 96 | 'DISABLED' 97 | )) 98 | end 99 | end 100 | 101 | context 'with managed scaling' do 102 | before(:context) do 103 | @plan = plan(role: :root) do |vars| 104 | vars.include_cluster_instances = true 105 | vars.include_asg_capacity_provider = true 106 | vars.asg_capacity_provider_manage_scaling = true 107 | vars.asg_capacity_provider_minimum_scaling_step_size = 3 108 | vars.asg_capacity_provider_maximum_scaling_step_size = 300 109 | vars.asg_capacity_provider_target_capacity = 90 110 | end 111 | end 112 | 113 | it 'enables managed scaling' do 114 | expect(@plan) 115 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 116 | .with_attribute_value( 117 | [ 118 | :auto_scaling_group_provider, 119 | 0, 120 | :managed_scaling, 121 | 0, 122 | :status 123 | ], 124 | 'ENABLED' 125 | )) 126 | end 127 | 128 | it 'uses the provided minimum scaling step size' do 129 | expect(@plan) 130 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 131 | .with_attribute_value( 132 | [ 133 | :auto_scaling_group_provider, 134 | 0, 135 | :managed_scaling, 136 | 0, 137 | :minimum_scaling_step_size 138 | ], 139 | 3 140 | )) 141 | end 142 | 143 | it 'uses the provided maximum scaling step size' do 144 | expect(@plan) 145 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 146 | .with_attribute_value( 147 | [ 148 | :auto_scaling_group_provider, 149 | 0, 150 | :managed_scaling, 151 | 0, 152 | :maximum_scaling_step_size 153 | ], 154 | 300 155 | )) 156 | end 157 | 158 | it 'uses the provided target capacity' do 159 | expect(@plan) 160 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 161 | .with_attribute_value( 162 | [ 163 | :auto_scaling_group_provider, 164 | 0, 165 | :managed_scaling, 166 | 0, 167 | :target_capacity 168 | ], 169 | 90 170 | )) 171 | end 172 | end 173 | 174 | context 'without managed scaling' do 175 | before(:context) do 176 | @plan = plan(role: :root) do |vars| 177 | vars.include_cluster_instances = true 178 | vars.include_asg_capacity_provider = true 179 | vars.asg_capacity_provider_manage_scaling = false 180 | end 181 | end 182 | 183 | it 'disables managed scaling' do 184 | expect(@plan) 185 | .to(include_resource_creation(type: 'aws_ecs_capacity_provider') 186 | .with_attribute_value( 187 | [ 188 | :auto_scaling_group_provider, 189 | 0, 190 | :managed_scaling, 191 | 0, 192 | :status 193 | ], 194 | 'DISABLED' 195 | )) 196 | end 197 | end 198 | end 199 | 200 | context 'when include_asg_capacity_provider is false \ 201 | and include_cluster_instances is true' do 202 | before(:context) do 203 | @plan = plan(role: :root) do |vars| 204 | vars.include_cluster_instances = true 205 | vars.include_asg_capacity_provider = false 206 | end 207 | end 208 | 209 | it 'does not create a capacity provider for the ECS cluster' do 210 | expect(@plan) 211 | .not_to(include_resource_creation(type: 'aws_ecs_capacity_provider')) 212 | end 213 | 214 | it 'does not include the AmazonECSManaged tag on the ASG' do 215 | expect(@plan) 216 | .not_to(include_resource_creation(type: 'aws_autoscaling_group') 217 | .with_attribute_value( 218 | :tag, 219 | including({ 220 | key: 'AmazonECSManaged', 221 | propagate_at_launch: true, 222 | value: '' 223 | }) 224 | )) 225 | end 226 | end 227 | 228 | context 'when include_asg_capacity_provider is true and include_cluster_instances is false' do 229 | before(:context) do 230 | @plan = plan(role: :root) do |vars| 231 | vars.include_cluster_instances = false 232 | vars.include_asg_capacity_provider = true 233 | end 234 | end 235 | 236 | it 'does not create a capacity provider for the ECS cluster' do 237 | expect(@plan) 238 | .not_to(include_resource_creation(type: 'aws_ecs_capacity_provider')) 239 | end 240 | 241 | it 'does not include the AmazonECSManaged tag on the ASG' do 242 | expect(@plan) 243 | .not_to(include_resource_creation(type: 'aws_autoscaling_group') 244 | .with_attribute_value( 245 | :tag, 246 | including({ 247 | key: 'AmazonECSManaged', 248 | propagate_at_launch: true, 249 | value: '' 250 | }) 251 | )) 252 | end 253 | end 254 | 255 | context 'when include_asg_capacity_provider is false and include_cluster_instances is false' do 256 | before(:context) do 257 | @plan = plan(role: :root) do |vars| 258 | vars.include_cluster_instances = false 259 | vars.include_asg_capacity_provider = false 260 | end 261 | end 262 | 263 | it 'does not create a capacity provider for the ECS cluster' do 264 | expect(@plan) 265 | .not_to(include_resource_creation(type: 'aws_ecs_capacity_provider')) 266 | end 267 | 268 | it 'does not include the AmazonECSManaged tag on the ASG' do 269 | expect(@plan) 270 | .not_to(include_resource_creation(type: 'aws_autoscaling_group') 271 | .with_attribute_value( 272 | :tag, 273 | including({ 274 | key: 'AmazonECSManaged', 275 | propagate_at_launch: true, 276 | value: '' 277 | }) 278 | )) 279 | end 280 | end 281 | 282 | context 'when additional_capacity_providers are provided' do 283 | before(:context) do 284 | @plan = plan(role: :root) do |vars| 285 | vars.cluster_name = 'special-cluster' 286 | vars.include_cluster_instances = false 287 | vars.include_asg_capacity_provider = false 288 | vars.additional_capacity_providers = ['FARGATE'] 289 | end 290 | end 291 | 292 | it 'creates a cluster capacity providers resource' do 293 | expect(@plan) 294 | .to(include_resource_creation( 295 | type: 'aws_ecs_cluster_capacity_providers' 296 | ) 297 | .once) 298 | end 299 | 300 | it 'uses the correct cluster name on the cluster capacity providers resource' do 301 | expect(@plan) 302 | .to(include_resource_creation( 303 | type: 'aws_ecs_cluster_capacity_providers' 304 | ) 305 | .with_attribute_value( 306 | :cluster_name, 307 | "#{component}-#{dep_id}-special-cluster" 308 | )) 309 | end 310 | 311 | it 'adds the additional capacity providers to the cluster capacity providers' do 312 | expect(@plan) 313 | .to(include_resource_creation( 314 | type: 'aws_ecs_cluster_capacity_providers' 315 | ) 316 | .with_attribute_value( 317 | :capacity_providers, ['FARGATE'] 318 | )) 319 | end 320 | end 321 | end 322 | -------------------------------------------------------------------------------- /spec/unit/cloudwatch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'CloudWatch' do 6 | let(:component) do 7 | var(role: :root, name: 'component') 8 | end 9 | let(:dep_id) do 10 | var(role: :root, name: 'deployment_identifier') 11 | end 12 | let(:log_group_name) do 13 | "/#{component}/#{dep_id}/ecs-cluster/default" 14 | end 15 | 16 | before(:context) do 17 | @plan = plan(role: :root) 18 | end 19 | 20 | describe 'logging' do 21 | it 'creates log group' do 22 | expect(@plan) 23 | .to(include_resource_creation(type: 'aws_cloudwatch_log_group') 24 | .once) 25 | end 26 | 27 | it 'has log group name' do 28 | expect(@plan) 29 | .to(include_resource_creation(type: 'aws_cloudwatch_log_group') 30 | .with_attribute_value(:name, log_group_name)) 31 | end 32 | 33 | it 'uses log retention default of 0' do 34 | expect(@plan) 35 | .to(include_resource_creation(type: 'aws_cloudwatch_log_group') 36 | .with_attribute_value(:retention_in_days, 0)) 37 | end 38 | 39 | context 'when cluster log group retention is set' do 40 | cluster_log_group_retention = 3 41 | 42 | before(:context) do 43 | @plan = plan(role: :root) do |vars| 44 | vars.cluster_log_group_retention = cluster_log_group_retention 45 | end 46 | end 47 | 48 | it 'uses provided log group retention' do 49 | expect(@plan) 50 | .to(include_resource_creation(type: 'aws_cloudwatch_log_group') 51 | .with_attribute_value( 52 | :retention_in_days, 53 | cluster_log_group_retention 54 | )) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/unit/cluster_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'ECS Cluster' do 6 | before(:context) do 7 | @plan = plan(role: :root) 8 | end 9 | 10 | it 'exists' do 11 | expect(@plan) 12 | .to(include_resource_creation(type: 'aws_ecs_cluster') 13 | .once) 14 | end 15 | 16 | context 'when container insights enabled' do 17 | before(:context) do 18 | @plan = plan(role: :root) do |vars| 19 | vars.enable_container_insights = true 20 | end 21 | end 22 | 23 | it 'has container insights enabled on the cluster' do 24 | expect(@plan) 25 | .to(include_resource_creation(type: 'aws_ecs_cluster') 26 | .with_attribute_value([:setting, 0, :value], 'enabled')) 27 | end 28 | end 29 | 30 | context 'when container insights disabled' do 31 | before(:context) do 32 | @plan = plan(role: :root) do |vars| 33 | vars.enable_container_insights = false 34 | end 35 | end 36 | 37 | it 'has container insights disabled on the cluster' do 38 | expect(@plan) 39 | .to(include_resource_creation(type: 'aws_ecs_cluster') 40 | .with_attribute_value([:setting, 0, :value], 'disabled')) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/iam_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'json' 5 | 6 | describe 'IAM policies, profiles and roles' do 7 | let(:component) do 8 | var(role: :root, name: 'component') 9 | end 10 | let(:dep_id) do 11 | var(role: :root, name: 'deployment_identifier') 12 | end 13 | 14 | describe 'by default' do 15 | before(:context) do 16 | @plan = plan(role: :root) 17 | end 18 | 19 | describe 'cluster instance profile' do 20 | it 'exists' do 21 | expect(@plan) 22 | .to(include_resource_creation(type: 'aws_iam_instance_profile') 23 | .once) 24 | end 25 | 26 | it 'has path /' do 27 | expect(@plan) 28 | .to(include_resource_creation(type: 'aws_iam_instance_profile') 29 | .with_attribute_value(:path, '/')) 30 | end 31 | end 32 | 33 | describe 'cluster instance role' do 34 | it 'exists' do 35 | expect(@plan) 36 | .to(include_resource_creation( 37 | type: 'aws_iam_role', 38 | name: 'cluster_instance_role' 39 | ) 40 | .once) 41 | end 42 | 43 | it('has description') do 44 | expect(@plan) 45 | .to(include_resource_creation( 46 | type: 'aws_iam_role', 47 | name: 'cluster_instance_role' 48 | ) 49 | .with_attribute_value( 50 | :description, 51 | "cluster-instance-role-#{component}-#{dep_id}-default" 52 | )) 53 | end 54 | 55 | it 'allows assuming a role of ec2' do 56 | expect(@plan) 57 | .to(include_resource_creation( 58 | type: 'aws_iam_role', 59 | name: 'cluster_instance_role' 60 | ) 61 | .with_attribute_value( 62 | :assume_role_policy, 63 | a_policy_with_statement( 64 | Effect: 'Allow', 65 | Action: 'sts:AssumeRole', 66 | Principal: { Service: ['ec2.amazonaws.com'] } 67 | ) 68 | )) 69 | end 70 | end 71 | 72 | describe 'cluster instance policy' do 73 | it 'exists' do 74 | expect(@plan) 75 | .to(include_resource_creation( 76 | type: 'aws_iam_policy', 77 | name: 'cluster_instance_policy' 78 | ) 79 | .once) 80 | end 81 | 82 | it 'has correct description' do 83 | expect(@plan) 84 | .to(include_resource_creation( 85 | type: 'aws_iam_policy', 86 | name: 'cluster_instance_policy' 87 | ) 88 | .with_attribute_value( 89 | :description, 90 | "cluster-instance-policy-#{component}-#{dep_id}-default" 91 | )) 92 | end 93 | 94 | it 'allows ECS, ECR, S3 and log actions' do 95 | expect(@plan) 96 | .to(include_resource_creation( 97 | type: 'aws_iam_policy', 98 | name: 'cluster_instance_policy' 99 | ) 100 | .with_attribute_value( 101 | :policy, 102 | a_policy_with_statement( 103 | Resource: '*', 104 | Effect: 'Allow', 105 | Action: %w[ 106 | ecs:CreateCluster 107 | ecs:RegisterContainerInstance 108 | ecs:DeregisterContainerInstance 109 | ecs:DiscoverPollEndpoint 110 | ecs:Poll 111 | ecs:StartTelemetrySession 112 | ecs:Submit* 113 | ecr:GetAuthorizationToken 114 | ecr:GetDownloadUrlForLayer 115 | ecr:BatchGetImage 116 | ecr:BatchCheckLayerAvailability 117 | s3:GetObject 118 | logs:CreateLogStream 119 | logs:PutLogEvents 120 | ] 121 | ) 122 | )) 123 | end 124 | end 125 | 126 | describe 'cluster service role' do 127 | it 'exists' do 128 | expect(@plan) 129 | .to(include_resource_creation( 130 | type: 'aws_iam_role', 131 | name: 'cluster_service_role' 132 | ) 133 | .once) 134 | end 135 | 136 | it 'has correct description' do 137 | expect(@plan) 138 | .to(include_resource_creation( 139 | type: 'aws_iam_role', 140 | name: 'cluster_service_role' 141 | ) 142 | .with_attribute_value( 143 | :description, 144 | "cluster-service-role-#{component}-#{dep_id}-default" 145 | )) 146 | end 147 | 148 | it 'allows assuming a role of ecs' do 149 | expect(@plan) 150 | .to(include_resource_creation( 151 | type: 'aws_iam_role', 152 | name: 'cluster_service_role' 153 | ) 154 | .with_attribute_value( 155 | :assume_role_policy, 156 | a_policy_with_statement( 157 | Effect: 'Allow', 158 | Action: 'sts:AssumeRole', 159 | Principal: { Service: ['ecs.amazonaws.com'] } 160 | ) 161 | )) 162 | end 163 | end 164 | 165 | describe 'cluster service policy' do 166 | it 'exists' do 167 | expect(@plan) 168 | .to(include_resource_creation( 169 | type: 'aws_iam_policy', 170 | name: 'cluster_service_policy' 171 | ) 172 | .once) 173 | end 174 | 175 | it 'has correct description' do 176 | expect(@plan) 177 | .to(include_resource_creation( 178 | type: 'aws_iam_policy', 179 | name: 'cluster_service_policy' 180 | ) 181 | .with_attribute_value( 182 | :description, 183 | "cluster-service-policy-#{component}-#{dep_id}-default" 184 | )) 185 | end 186 | 187 | it 'allows ELB, EC2 ingress and describe actions' do 188 | expect(@plan) 189 | .to(include_resource_creation( 190 | type: 'aws_iam_policy', 191 | name: 'cluster_service_policy' 192 | ) 193 | .with_attribute_value( 194 | :policy, 195 | a_policy_with_statement( 196 | Resource: '*', 197 | Effect: 'Allow', 198 | Action: %w[ 199 | elasticloadbalancing:RegisterInstancesWithLoadBalancer 200 | elasticloadbalancing:DeregisterInstancesFromLoadBalancer 201 | elasticloadbalancing:Describe* 202 | elasticloadbalancing:RegisterTargets 203 | elasticloadbalancing:DeregisterTargets 204 | ec2:Describe* 205 | ec2:AuthorizeSecurityGroupIngress 206 | ] 207 | ) 208 | )) 209 | end 210 | end 211 | end 212 | 213 | describe 'when include_cluster_instances is false' do 214 | before(:context) do 215 | @plan = plan(role: :root) do |vars| 216 | vars.include_cluster_instances = false 217 | end 218 | end 219 | 220 | describe 'cluster instance profile' do 221 | it 'does not exist' do 222 | expect(@plan) 223 | .not_to(include_resource_creation(type: 'aws_iam_instance_profile')) 224 | end 225 | end 226 | 227 | describe 'cluster instance role' do 228 | it 'does not exist' do 229 | expect(@plan) 230 | .not_to(include_resource_creation( 231 | type: 'aws_iam_role', 232 | name: 'cluster_instance_role' 233 | )) 234 | end 235 | end 236 | 237 | describe 'cluster instance policy' do 238 | it 'does not exist' do 239 | expect(@plan) 240 | .not_to(include_resource_creation( 241 | type: 'aws_iam_policy', 242 | name: 'cluster_instance_policy' 243 | )) 244 | end 245 | end 246 | end 247 | 248 | describe 'when include_cluster_instances is true' do 249 | before(:context) do 250 | @plan = plan(role: :root) do |vars| 251 | vars.include_cluster_instances = true 252 | end 253 | end 254 | 255 | describe 'cluster instance profile' do 256 | it 'exists' do 257 | expect(@plan) 258 | .to(include_resource_creation(type: 'aws_iam_instance_profile') 259 | .once) 260 | end 261 | end 262 | 263 | describe 'cluster instance role' do 264 | it 'exists' do 265 | expect(@plan) 266 | .to(include_resource_creation( 267 | type: 'aws_iam_role', 268 | name: 'cluster_instance_role' 269 | ).once) 270 | end 271 | end 272 | 273 | describe 'cluster instance policy' do 274 | it 'exists' do 275 | expect(@plan) 276 | .to(include_resource_creation( 277 | type: 'aws_iam_policy', 278 | name: 'cluster_instance_policy' 279 | ).once) 280 | end 281 | end 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.33.0" 6 | constraints = "4.33.0" 7 | hashes = [ 8 | "h1:0S9ZXYg6K0CTOJUTQnoH94YrKuOYyJYEcc+hN5qGafA=", 9 | "h1:rLRYOeKvU17Tky5dleZwTPRoWtbdFTG/jOF/fTP2otY=", 10 | "zh:421b24e21d7fac4d65d97438d2c0a4effe71d3a1bd15820d6fde2879e49fe817", 11 | "zh:4378a84ca8e2a6990f47abc24367b801e884be928671b37ad7b8e7b656f73e48", 12 | "zh:54e0d7884edf3cefd096715794d32b6532138dca905f0b2fe84fb2117594293c", 13 | "zh:6269a7d0312057db5ded669e9f7f9bd80fb6dcb549b50d8d7f3f3b2a0361b8a5", 14 | "zh:67f57d16aa3db493a3174c3c5f30385c7af9767c4e3cdca14e5a4bf384ff59d9", 15 | "zh:7d4d4a1d963e431ffdc3348e3a578d3ba0fa782b1f4bf55fd5c0e527d24fed81", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:cd8e3d32485acb49c1b06f63916fec8e73a4caa6cf88ae9c4bf236d6f5d9b914", 18 | "zh:d586fd01195bd3775346495e61806e79b6012e745dc05e31a30b958acf968abe", 19 | "zh:d76122060f25ab87887a743096a42d47ba091c2c019ac13ce6b3973b2babe5a3", 20 | "zh:e917d36fe18eddc42ec743b3152b4dcb4853b75ea7a679abd19bdf271bc48221", 21 | "zh:eb780860d5c04f43a018aef564e76a2d84e9aa68984fa1f968ca8c09d23a611a", 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/main.tf: -------------------------------------------------------------------------------- 1 | module "base_network" { 2 | source = "infrablocks/base-networking/aws" 3 | version = "4.0.0" 4 | 5 | vpc_cidr = var.vpc_cidr 6 | region = var.region 7 | availability_zones = var.availability_zones 8 | 9 | component = var.component 10 | deployment_identifier = var.deployment_identifier 11 | 12 | private_zone_id = module.dns-zones.private_zone_id 13 | } 14 | 15 | resource "aws_default_vpc" "default" {} 16 | 17 | module "dns-zones" { 18 | source = "infrablocks/dns-zones/aws" 19 | version = "1.0.0" 20 | 21 | domain_name = "infrablocks-ecs-cluster-example.com" 22 | private_domain_name = "infrablocks-ecs-cluster-example.net" 23 | private_zone_vpc_id = aws_default_vpc.default.id 24 | private_zone_vpc_region = var.region 25 | } 26 | 27 | data "aws_caller_identity" "current" {} 28 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = module.base_network.vpc_id 3 | } 4 | 5 | output "vpc_cidr" { 6 | value = module.base_network.vpc_cidr 7 | } 8 | 9 | output "private_subnet_ids" { 10 | value = module.base_network.private_subnet_ids 11 | } 12 | 13 | output "security_group_ids" { 14 | value = aws_security_group.custom_security_group.*.id 15 | } 16 | 17 | output "account_id" { 18 | value = data.aws_caller_identity.current.account_id 19 | } 20 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/security_groups.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "custom_security_group" { 2 | count = 2 3 | name = "${var.component}-${var.deployment_identifier}-${count.index}" 4 | description = "Custom security group for component: ${var.component}, deployment: ${var.deployment_identifier}" 5 | vpc_id = module.base_network.vpc_id 6 | 7 | tags = { 8 | Name = "${var.component}-${var.deployment_identifier}-${count.index}" 9 | Component = var.component 10 | DeploymentIdentifier = var.deployment_identifier 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "4.33" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/unit/infra/prerequisites/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "vpc_cidr" {} 3 | variable "availability_zones" { 4 | type = list(string) 5 | } 6 | 7 | variable "component" {} 8 | variable "deployment_identifier" {} 9 | 10 | variable "private_zone_vpc_id" {} 11 | -------------------------------------------------------------------------------- /spec/unit/infra/root/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.33.0" 6 | constraints = ">= 3.74.0, 4.33.0" 7 | hashes = [ 8 | "h1:0S9ZXYg6K0CTOJUTQnoH94YrKuOYyJYEcc+hN5qGafA=", 9 | "h1:rLRYOeKvU17Tky5dleZwTPRoWtbdFTG/jOF/fTP2otY=", 10 | "zh:421b24e21d7fac4d65d97438d2c0a4effe71d3a1bd15820d6fde2879e49fe817", 11 | "zh:4378a84ca8e2a6990f47abc24367b801e884be928671b37ad7b8e7b656f73e48", 12 | "zh:54e0d7884edf3cefd096715794d32b6532138dca905f0b2fe84fb2117594293c", 13 | "zh:6269a7d0312057db5ded669e9f7f9bd80fb6dcb549b50d8d7f3f3b2a0361b8a5", 14 | "zh:67f57d16aa3db493a3174c3c5f30385c7af9767c4e3cdca14e5a4bf384ff59d9", 15 | "zh:7d4d4a1d963e431ffdc3348e3a578d3ba0fa782b1f4bf55fd5c0e527d24fed81", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:cd8e3d32485acb49c1b06f63916fec8e73a4caa6cf88ae9c4bf236d6f5d9b914", 18 | "zh:d586fd01195bd3775346495e61806e79b6012e745dc05e31a30b958acf968abe", 19 | "zh:d76122060f25ab87887a743096a42d47ba091c2c019ac13ce6b3973b2babe5a3", 20 | "zh:e917d36fe18eddc42ec743b3152b4dcb4853b75ea7a679abd19bdf271bc48221", 21 | "zh:eb780860d5c04f43a018aef564e76a2d84e9aa68984fa1f968ca8c09d23a611a", 22 | ] 23 | } 24 | 25 | provider "registry.terraform.io/hashicorp/null" { 26 | version = "3.2.0" 27 | constraints = ">= 3.0.0, 3.2.0" 28 | hashes = [ 29 | "h1:6yiJqQ6JAJW3oMxuZrWoUgHYpkscorX40Q/LzOMzY+w=", 30 | "h1:ZbuTqXe8q7Z0IJ2wkF4nio7eZDQc02sezY0esJ5b1Bc=", 31 | "zh:1d88ea3af09dcf91ad0aaa0d3978ca8dcb49dc866c8615202b738d73395af6b5", 32 | "zh:3844db77bfac2aca43aaa46f3f698c8e5320a47e838ee1318408663449547e7e", 33 | "zh:538fadbd87c576a332b7524f352e6004f94c27afdd3b5d105820d328dc49c5e3", 34 | "zh:56def6f00fc2bc9c3c265b841ce71e80b77e319de7b0f662425b8e5e7eb26846", 35 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 36 | "zh:8fce56e5f1d13041d8047a1d0c93f930509704813a28f8d39c2b2082d7eebf9f", 37 | "zh:989e909a5eca96b8bdd4a0e8609f1bd525949fd226ae870acedf2da0c55b0451", 38 | "zh:99ddc34ad13e04e9c3477f5422fbec20fc13395ff940720c287bfa5c546d2fbc", 39 | "zh:b546666da4b4b60c0eec23faab7f94dc900e48f66b5436fc1ac0b87c6709ef04", 40 | "zh:d56643cb08cba6e074d70c4af37d5de2bd7c505f81d866d6d47c9e1d28ec65d1", 41 | "zh:f39ac5ff9e9d00e6a670bce6825529eded4b0b4966abba36a387db5f0712d7ba", 42 | "zh:fe102389facd09776502327352be99becc1ac09e80bc287db84a268172be641f", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /spec/unit/infra/root/main.tf: -------------------------------------------------------------------------------- 1 | data "terraform_remote_state" "prerequisites" { 2 | backend = "local" 3 | 4 | config = { 5 | path = "${path.module}/../../../../state/prerequisites.tfstate" 6 | } 7 | } 8 | 9 | module "ecs_cluster" { 10 | source = "../../../.." 11 | 12 | region = var.region 13 | vpc_id = data.terraform_remote_state.prerequisites.outputs.vpc_id 14 | subnet_ids = data.terraform_remote_state.prerequisites.outputs.private_subnet_ids 15 | 16 | component = var.component 17 | 18 | deployment_identifier = var.deployment_identifier 19 | 20 | tags = var.tags 21 | 22 | cluster_name = var.cluster_name 23 | 24 | include_cluster_instances = var.include_cluster_instances 25 | 26 | cluster_instance_ssh_public_key_path = var.cluster_instance_ssh_public_key_path 27 | cluster_instance_type = var.cluster_instance_type 28 | cluster_instance_ami = var.cluster_instance_ami 29 | cluster_instance_root_block_device_size = var.cluster_instance_root_block_device_size 30 | cluster_instance_root_block_device_path = var.cluster_instance_root_block_device_path 31 | 32 | cluster_instance_enable_ebs_volume_encryption = var.cluster_instance_enable_ebs_volume_encryption 33 | cluster_instance_ebs_volume_kms_key_id = var.cluster_instance_ebs_volume_kms_key_id 34 | 35 | cluster_instance_metadata_options = var.cluster_instance_metadata_options 36 | 37 | cluster_minimum_size = var.cluster_minimum_size 38 | cluster_maximum_size = var.cluster_maximum_size 39 | cluster_desired_capacity = var.cluster_desired_capacity 40 | 41 | cluster_log_group_retention = var.cluster_log_group_retention 42 | 43 | enable_detailed_monitoring = var.enable_detailed_monitoring 44 | 45 | security_groups = var.security_groups 46 | 47 | include_default_ingress_rule = var.include_default_ingress_rule 48 | include_default_egress_rule = var.include_default_egress_rule 49 | 50 | default_ingress_cidrs = var.default_ingress_cidrs 51 | default_egress_cidrs = var.default_egress_cidrs 52 | 53 | enable_container_insights = var.enable_container_insights 54 | 55 | protect_cluster_instances_from_scale_in = var.protect_cluster_instances_from_scale_in 56 | 57 | include_asg_capacity_provider = var.include_asg_capacity_provider 58 | asg_capacity_provider_manage_termination_protection = var.asg_capacity_provider_manage_termination_protection 59 | asg_capacity_provider_manage_scaling = var.asg_capacity_provider_manage_scaling 60 | asg_capacity_provider_minimum_scaling_step_size = var.asg_capacity_provider_minimum_scaling_step_size 61 | asg_capacity_provider_maximum_scaling_step_size = var.asg_capacity_provider_maximum_scaling_step_size 62 | asg_capacity_provider_target_capacity = var.asg_capacity_provider_target_capacity 63 | additional_capacity_providers = var.additional_capacity_providers 64 | } 65 | -------------------------------------------------------------------------------- /spec/unit/infra/root/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_id" { 2 | value = module.ecs_cluster.cluster_id 3 | } 4 | 5 | output "cluster_name" { 6 | value = module.ecs_cluster.cluster_name 7 | } 8 | 9 | output "cluster_arn" { 10 | value = module.ecs_cluster.cluster_arn 11 | } 12 | 13 | output "autoscaling_group_name" { 14 | value = module.ecs_cluster.autoscaling_group_name 15 | } 16 | 17 | output "autoscaling_group_arn" { 18 | value = module.ecs_cluster.autoscaling_group_arn 19 | } 20 | 21 | output "launch_template_name" { 22 | value = module.ecs_cluster.launch_template_name 23 | } 24 | 25 | output "security_group_id" { 26 | value = module.ecs_cluster.security_group_id 27 | } 28 | 29 | output "instance_role_arn" { 30 | value = module.ecs_cluster.instance_role_arn 31 | } 32 | 33 | output "instance_role_id" { 34 | value = module.ecs_cluster.instance_role_id 35 | } 36 | 37 | output "instance_policy_arn" { 38 | value = module.ecs_cluster.instance_policy_arn 39 | } 40 | 41 | output "instance_policy_id" { 42 | value = module.ecs_cluster.instance_policy_id 43 | } 44 | 45 | output "service_role_arn" { 46 | value = module.ecs_cluster.service_role_arn 47 | } 48 | 49 | output "service_role_id" { 50 | value = module.ecs_cluster.service_role_id 51 | } 52 | 53 | output "service_policy_arn" { 54 | value = module.ecs_cluster.service_policy_arn 55 | } 56 | 57 | output "service_policy_id" { 58 | value = module.ecs_cluster.service_policy_id 59 | } 60 | 61 | output "log_group" { 62 | value = module.ecs_cluster.log_group 63 | } 64 | -------------------------------------------------------------------------------- /spec/unit/infra/root/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | -------------------------------------------------------------------------------- /spec/unit/infra/root/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "4.33" 8 | } 9 | null = { 10 | source = "hashicorp/null" 11 | version = "3.2.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spec/unit/infra/root/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | 3 | variable "component" {} 4 | variable "deployment_identifier" {} 5 | 6 | variable "tags" { 7 | type = map(string) 8 | default = null 9 | } 10 | 11 | variable "cluster_name" { 12 | default = null 13 | } 14 | variable "include_cluster_instances" { 15 | default = null 16 | } 17 | variable "cluster_instance_ssh_public_key_path" { 18 | default = null 19 | } 20 | variable "cluster_instance_type" { 21 | default = null 22 | } 23 | variable "cluster_instance_ami" { 24 | type = string 25 | default = null 26 | } 27 | variable "cluster_instance_root_block_device_size" { 28 | default = null 29 | } 30 | variable "cluster_instance_root_block_device_path" { 31 | default = null 32 | } 33 | 34 | variable "cluster_instance_metadata_options" { 35 | type = map 36 | default = null 37 | } 38 | 39 | variable "cluster_minimum_size" { 40 | default = null 41 | } 42 | variable "cluster_maximum_size" { 43 | default = null 44 | } 45 | variable "cluster_desired_capacity" { 46 | default = null 47 | } 48 | variable "cluster_log_group_retention" { 49 | default = null 50 | } 51 | variable "cluster_instance_enable_ebs_volume_encryption" { 52 | default = null 53 | } 54 | variable "cluster_instance_ebs_volume_kms_key_id" { 55 | default = null 56 | } 57 | 58 | variable "enable_detailed_monitoring" { 59 | default = null 60 | } 61 | 62 | variable "include_default_ingress_rule" { 63 | default = null 64 | } 65 | variable "include_default_egress_rule" { 66 | default = null 67 | } 68 | 69 | variable "default_ingress_cidrs" { 70 | type = list(string) 71 | default = null 72 | } 73 | variable "default_egress_cidrs" { 74 | type = list(string) 75 | default = null 76 | } 77 | 78 | variable "security_groups" { 79 | type = list(string) 80 | default = [] 81 | } 82 | 83 | variable "enable_container_insights" { 84 | default = null 85 | } 86 | 87 | variable "protect_cluster_instances_from_scale_in" { 88 | default = null 89 | } 90 | 91 | variable "include_asg_capacity_provider" { 92 | default = null 93 | } 94 | variable "asg_capacity_provider_manage_termination_protection" { 95 | default = null 96 | } 97 | variable "asg_capacity_provider_manage_scaling" { 98 | default = null 99 | } 100 | variable "asg_capacity_provider_minimum_scaling_step_size" { 101 | default = null 102 | } 103 | variable "asg_capacity_provider_maximum_scaling_step_size" { 104 | default = null 105 | } 106 | variable "asg_capacity_provider_target_capacity" { 107 | default = null 108 | } 109 | variable "additional_capacity_providers" { 110 | type = list(string) 111 | default = [] 112 | } -------------------------------------------------------------------------------- /spec/unit/launch_configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Launch Configuration' do 6 | let(:component) do 7 | var(role: :root, name: 'component') 8 | end 9 | let(:dep_id) do 10 | var(role: :root, name: 'deployment_identifier') 11 | end 12 | let(:region) do 13 | var(role: :root, name: 'region') 14 | end 15 | 16 | before(:context) do 17 | @plan = plan(role: :root) 18 | end 19 | 20 | it 'does not exist' do 21 | expect(@plan) 22 | .not_to(include_resource_creation(type: 'aws_launch_configuration')) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/unit/launch_template_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Launch Template' do 6 | let(:component) do 7 | var(role: :root, name: 'component') 8 | end 9 | let(:dep_id) do 10 | var(role: :root, name: 'deployment_identifier') 11 | end 12 | let(:cluster_name) do 13 | 'default' 14 | end 15 | let(:tags) do 16 | var(role: :root, name: 'tags') 17 | end 18 | let(:region) do 19 | var(role: :root, name: 'region') 20 | end 21 | let(:account_id) do 22 | output(role: :prerequisites, name: 'account_id') 23 | end 24 | 25 | describe 'by default' do 26 | before(:context) do 27 | @plan = plan(role: :root) 28 | end 29 | 30 | it 'exists' do 31 | expect(@plan) 32 | .to(include_resource_creation(type: 'aws_launch_template') 33 | .once) 34 | end 35 | 36 | it 'has default instance type' do 37 | expect(@plan) 38 | .to(include_resource_creation(type: 'aws_launch_template') 39 | .with_attribute_value(:instance_type, 't2.medium')) 40 | end 41 | 42 | it 'has instance profile' do 43 | expect(@plan) 44 | .to(include_resource_creation(type: 'aws_launch_template') 45 | .with_attribute_value( 46 | [:iam_instance_profile, 0, :name], 47 | "cluster-instance-profile-#{component}-#{dep_id}-default" 48 | )) 49 | end 50 | 51 | context 'when using latest amazon linux 2 optimised for ECS' do 52 | let(:latest_amazon_linux_2_ecs_optimised_ami_id) do 53 | ec2_client = Aws::EC2::Client.new(region:) 54 | 55 | response = ec2_client.describe_images( 56 | { 57 | owners: ['amazon'], 58 | filters: [ 59 | { 60 | name: 'name', 61 | values: ['amzn2-ami-ecs-hvm-*-x86_64-ebs'] 62 | } 63 | ] 64 | } 65 | ) 66 | most_recent_image = response.images.max_by do |image| 67 | DateTime.parse(image.creation_date) 68 | end 69 | 70 | most_recent_image.image_id 71 | end 72 | 73 | it 'uses latest image id' do 74 | expect(@plan) 75 | .to(include_resource_creation(type: 'aws_launch_template') 76 | .with_attribute_value( 77 | :image_id, 78 | latest_amazon_linux_2_ecs_optimised_ami_id 79 | )) 80 | end 81 | end 82 | 83 | describe 'launch template name prefix' do 84 | it 'contains component, deployment identifier and cluster name' do 85 | expect(@plan) 86 | .to(include_resource_creation(type: 'aws_launch_template') 87 | .with_attribute_value( 88 | :name_prefix, 89 | "cluster-#{component}-#{dep_id}-default-" 90 | )) 91 | end 92 | end 93 | 94 | describe 'monitoring' do 95 | it 'is enabled by default' do 96 | expect(@plan) 97 | .to(include_resource_creation(type: 'aws_launch_template') 98 | .with_attribute_value( 99 | [:monitoring, 0, :enabled], 100 | true 101 | )) 102 | end 103 | end 104 | 105 | describe 'root block device' do 106 | it 'uses the default specified size' do 107 | expect(@plan) 108 | .to(include_resource_creation(type: 'aws_launch_template') 109 | .with_attribute_value( 110 | [:block_device_mappings, 0, :ebs, 0, :volume_size], 111 | 30 112 | )) 113 | end 114 | 115 | it 'uses the default specified type' do 116 | expect(@plan) 117 | .to(include_resource_creation(type: 'aws_launch_template') 118 | .with_attribute_value( 119 | [:block_device_mappings, 0, :ebs, 0, :volume_type], 120 | 'standard' 121 | )) 122 | end 123 | 124 | it 'uses the default specified device name' do 125 | expect(@plan) 126 | .to(include_resource_creation(type: 'aws_launch_template') 127 | .with_attribute_value( 128 | [:block_device_mappings, 0, :device_name], 129 | '/dev/xvda' 130 | )) 131 | end 132 | 133 | it 'enables encryption by default' do 134 | expect(@plan) 135 | .to(include_resource_creation(type: 'aws_launch_template') 136 | .with_attribute_value( 137 | [:block_device_mappings, 0, :ebs, 0, :encrypted], 138 | 'true' 139 | )) 140 | end 141 | 142 | it 'uses default kms key for encryption' do 143 | expect(@plan) 144 | .to(include_resource_creation(type: 'aws_launch_template') 145 | .with_attribute_value( 146 | [:block_device_mappings, 0, :ebs, 0, :kms_key_id], 147 | "arn:aws:kms:#{region}:#{account_id}:alias/aws/ebs" 148 | )) 149 | end 150 | end 151 | 152 | describe 'tag specifications' do 153 | it 'sets default and provided tags on volumes' do 154 | expect(@plan) 155 | .to(include_resource_creation(type: 'aws_launch_template') 156 | .with_attribute_value( 157 | [:tag_specifications, 0], 158 | { 159 | resource_type: 'volume', 160 | tags: { 161 | 'Component' => component, 162 | 'DeploymentIdentifier' => dep_id, 163 | 'Name' => 164 | "cluster-worker-#{component}-#{dep_id}-#{cluster_name}", 165 | 'ClusterName' => cluster_name 166 | }.merge(tags) 167 | } 168 | )) 169 | end 170 | end 171 | 172 | describe 'metadata options' do 173 | it 'disables http_protocol_ipv6 and instance_metadata_tags by default' do 174 | expect(@plan) 175 | .to(include_resource_creation(type: 'aws_launch_template') 176 | .with_attribute_value( 177 | :metadata_options, 178 | including( 179 | including({ 180 | http_protocol_ipv6: 'disabled', 181 | instance_metadata_tags: 'disabled' 182 | }) 183 | ) 184 | )) 185 | end 186 | end 187 | end 188 | 189 | describe 'when AMIs specified' do 190 | ami_id = 'custom_ami_id' 191 | 192 | before(:context) do 193 | @plan = plan(role: :root) do |vars| 194 | vars.cluster_instance_ami = ami_id 195 | end 196 | end 197 | 198 | it 'uses provided image ID' do 199 | expect(@plan) 200 | .to(include_resource_creation(type: 'aws_launch_template') 201 | .with_attribute_value( 202 | :image_id, 203 | ami_id 204 | )) 205 | end 206 | end 207 | 208 | describe 'when root block device path is specified' do 209 | device_path = '/custom/path' 210 | 211 | before(:context) do 212 | @plan = plan(role: :root) do |vars| 213 | vars.cluster_instance_root_block_device_path = device_path 214 | end 215 | end 216 | 217 | it 'uses provided device path' do 218 | expect(@plan) 219 | .to(include_resource_creation(type: 'aws_launch_template') 220 | .with_attribute_value( 221 | [:block_device_mappings, 0, :device_name], 222 | device_path 223 | )) 224 | end 225 | end 226 | 227 | describe 'when enable detailed monitoring is specified' do 228 | enable_detailed_monitoring = false 229 | 230 | before(:context) do 231 | @plan = plan(role: :root) do |vars| 232 | vars.enable_detailed_monitoring = enable_detailed_monitoring 233 | end 234 | end 235 | 236 | it 'uses provided enable monitoring' do 237 | expect(@plan) 238 | .to(include_resource_creation(type: 'aws_launch_template') 239 | .with_attribute_value( 240 | [:monitoring, 0, :enabled], 241 | enable_detailed_monitoring 242 | )) 243 | end 244 | end 245 | 246 | describe 'when ebs volume encryption is disabled' do 247 | encryption_enabled = false 248 | 249 | before(:context) do 250 | @plan = plan(role: :root) do |vars| 251 | vars.cluster_instance_enable_ebs_volume_encryption = encryption_enabled 252 | end 253 | end 254 | 255 | it 'disables encryption' do 256 | expect(@plan) 257 | .to(include_resource_creation(type: 'aws_launch_template') 258 | .with_attribute_value( 259 | [:block_device_mappings, 0, :ebs, 0, :encrypted], 260 | encryption_enabled.to_s 261 | )) 262 | end 263 | end 264 | 265 | describe 'when encryption kms key is set' do 266 | kms_key_id = 'arn:aws:kms:eu-west-2:111111111111:some/other/key' 267 | 268 | before(:context) do 269 | @plan = plan(role: :root) do |vars| 270 | vars.cluster_instance_ebs_volume_kms_key_id = kms_key_id 271 | end 272 | end 273 | 274 | it 'uses provided kms key' do 275 | expect(@plan) 276 | .to(include_resource_creation(type: 'aws_launch_template') 277 | .with_attribute_value( 278 | [:block_device_mappings, 0, :ebs, 0, :kms_key_id], 279 | kms_key_id 280 | )) 281 | end 282 | end 283 | 284 | describe 'when cluster_instance_metadata_options is provided' do 285 | before(:context) do 286 | @plan = plan(role: :root) do |vars| 287 | vars.cluster_instance_metadata_options = { 288 | http_endpoint: 'enabled', 289 | http_tokens: 'required', 290 | http_protocol_ipv6: 'enabled', 291 | instance_metadata_tags: 'enabled', 292 | http_put_response_hop_limit: 15 293 | } 294 | end 295 | end 296 | 297 | it 'uses provided metadata options' do 298 | expect(@plan) 299 | .to(include_resource_creation(type: 'aws_launch_template') 300 | .with_attribute_value( 301 | :metadata_options, 302 | including(including({ 303 | http_endpoint: 'enabled', 304 | http_tokens: 'required', 305 | http_protocol_ipv6: 'enabled', 306 | instance_metadata_tags: 'enabled', 307 | http_put_response_hop_limit: 15 308 | })) 309 | )) 310 | end 311 | end 312 | 313 | describe 'when include_cluster_instances is false' do 314 | before(:context) do 315 | @plan = plan(role: :root) do |vars| 316 | vars.include_cluster_instances = false 317 | end 318 | end 319 | 320 | it 'does not create an aws_launch_template' do 321 | expect(@plan) 322 | .not_to(include_resource_creation(type: 'aws_launch_template')) 323 | end 324 | end 325 | 326 | describe 'when include_cluster_instances is true' do 327 | before(:context) do 328 | @plan = plan(role: :root) do |vars| 329 | vars.include_cluster_instances = true 330 | end 331 | end 332 | 333 | it 'creates an aws_launch_template' do 334 | expect(@plan) 335 | .to(include_resource_creation(type: 'aws_launch_template') 336 | .once) 337 | end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /spec/unit/security_group_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Security Group' do 6 | let(:component) do 7 | var(role: :root, name: 'component') 8 | end 9 | let(:dep_id) do 10 | var(role: :root, name: 'deployment_identifier') 11 | end 12 | let(:vpc_id) do 13 | output(role: :prerequisites, name: 'vpc_id') 14 | end 15 | 16 | describe 'by default' do 17 | before(:context) do 18 | @plan = plan(role: :root) 19 | end 20 | 21 | it 'exists' do 22 | expect(@plan) 23 | .to(include_resource_creation(type: 'aws_security_group') 24 | .once) 25 | end 26 | 27 | describe 'tags' do 28 | it 'has Component' do 29 | expect(@plan) 30 | .to(include_resource_creation(type: 'aws_security_group') 31 | .with_attribute_value( 32 | :tags, 33 | including({ Component: component }) 34 | )) 35 | end 36 | 37 | it 'has DeploymentIdentifier' do 38 | expect(@plan) 39 | .to(include_resource_creation(type: 'aws_security_group') 40 | .with_attribute_value( 41 | :tags, 42 | including({ DeploymentIdentifier: dep_id }) 43 | )) 44 | end 45 | 46 | it 'has ImportantTag' do 47 | expect(@plan) 48 | .to(include_resource_creation(type: 'aws_security_group') 49 | .with_attribute_value( 50 | :tags, 51 | including({ ImportantTag: 'important-value' }) 52 | )) 53 | end 54 | end 55 | 56 | it 'uses given vpc_id' do 57 | expect(@plan) 58 | .to(include_resource_creation(type: 'aws_security_group') 59 | .with_attribute_value(:vpc_id, vpc_id)) 60 | end 61 | 62 | context 'when default ingress and egress are included' do 63 | it('allows inbound TCP and UDP connectivity on all ports from any ' \ 64 | 'address within the VPC') do 65 | expect(@plan) 66 | .to(include_resource_creation( 67 | type: 'aws_security_group_rule', 68 | name: 'cluster_default_ingress' 69 | ) 70 | .with_attribute_value(:from_port, 0) 71 | .with_attribute_value(:to_port, 0) 72 | .with_attribute_value(:protocol, '-1') 73 | .with_attribute_value(:cidr_blocks, ['10.0.0.0/8'])) 74 | end 75 | 76 | it 'allows outbound TCP connectivity on all ports and protocols anywhere' do 77 | expect(@plan) 78 | .to(include_resource_creation( 79 | type: 'aws_security_group_rule', 80 | name: 'cluster_default_egress' 81 | ) 82 | .with_attribute_value(:from_port, 0) 83 | .with_attribute_value(:to_port, 0) 84 | .with_attribute_value(:protocol, '-1') 85 | .with_attribute_value(:cidr_blocks, ['0.0.0.0/0'])) 86 | end 87 | end 88 | end 89 | 90 | describe 'when default ingress and egress are not included' do 91 | before(:context) do 92 | @plan = plan(role: :root) do |vars| 93 | vars.include_default_ingress_rule = false 94 | vars.include_default_egress_rule = false 95 | end 96 | end 97 | 98 | it 'has no ingress rules' do 99 | expect(@plan) 100 | .not_to(include_resource_creation( 101 | type: 'aws_security_group_rule', 102 | name: 'cluster_default_ingress' 103 | )) 104 | end 105 | 106 | it 'has no egress rules' do 107 | expect(@plan) 108 | .not_to(include_resource_creation( 109 | type: 'aws_security_group_rule', 110 | name: 'cluster_default_egress' 111 | )) 112 | end 113 | end 114 | 115 | describe 'when include_cluster_instances is false' do 116 | before(:context) do 117 | @plan = plan(role: :root) do |vars| 118 | vars.include_cluster_instances = false 119 | end 120 | end 121 | 122 | it 'does not create an aws_security_group' do 123 | expect(@plan) 124 | .not_to(include_resource_creation(type: 'aws_security_group')) 125 | end 126 | 127 | it 'does not create any aws_security_group_rules' do 128 | expect(@plan) 129 | .not_to(include_resource_creation(type: 'aws_security_group_rule')) 130 | end 131 | end 132 | 133 | describe 'when include_cluster_instances is true' do 134 | before(:context) do 135 | @plan = plan(role: :root) do |vars| 136 | vars.include_cluster_instances = true 137 | end 138 | end 139 | 140 | it 'creates an aws_security_group' do 141 | expect(@plan) 142 | .to(include_resource_creation(type: 'aws_security_group') 143 | .once) 144 | end 145 | 146 | it 'creates aws_security_group_rules' do 147 | expect(@plan) 148 | .to(include_resource_creation(type: 'aws_security_group_rule')) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/unit/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | require 'aws-sdk' 6 | require 'rspec' 7 | require 'ruby_terraform' 8 | require 'rspec/terraform' 9 | require 'logger' 10 | require 'stringio' 11 | 12 | Dir[File.join(__dir__, 'support', '**', '*.rb')] 13 | .each { |f| require f } 14 | 15 | RSpec.configure do |config| 16 | config.filter_run_when_matching :focus 17 | config.example_status_persistence_file_path = '.rspec_status' 18 | config.expect_with(:rspec) { |c| c.syntax = :expect } 19 | 20 | config.terraform_binary = 'vendor/terraform/bin/terraform' 21 | config.terraform_log_file_path = 'build/logs/unit.log' 22 | config.terraform_log_streams = [:file] 23 | config.terraform_log_level = Logger::DEBUG 24 | config.terraform_configuration_provider = 25 | RSpec::Terraform::Configuration.chain_provider( 26 | providers: [ 27 | RSpec::Terraform::Configuration.seed_provider( 28 | generator: -> { SecureRandom.hex[0, 8] } 29 | ), 30 | RSpec::Terraform::Configuration.in_memory_provider( 31 | no_color: true 32 | ), 33 | RSpec::Terraform::Configuration.confidante_provider( 34 | parameters: %i[ 35 | configuration_directory 36 | state_file 37 | vars 38 | ], 39 | scope_selector: ->(o) { o.slice(:role) } 40 | ) 41 | ] 42 | ) 43 | 44 | config.before(:suite) do 45 | apply( 46 | role: :prerequisites 47 | ) 48 | end 49 | config.after(:suite) do 50 | destroy( 51 | role: :prerequisites, 52 | only_if: -> { !ENV['FORCE_DESTROY'].nil? || ENV['SEED'].nil? } 53 | ) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/unit/support/matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'set' 5 | 6 | RSpec::Matchers 7 | .define( 8 | :a_policy_with_statement 9 | ) do |expected_statement, options = {}| 10 | def normalise(statement) 11 | %i[Resource NotResource Action NotAction Principal] 12 | .each_with_object(statement) do |section, s| 13 | s[section] = s[section].to_set if s[section].is_a?(Array) 14 | end 15 | end 16 | 17 | match do |actual| 18 | return false unless actual 19 | 20 | expected_statement = normalise(expected_statement) 21 | policy = JSON.parse(actual, symbolize_names: true) 22 | statements = policy[:Statement] 23 | statement = statements.find do |target_statement| 24 | expected_statement <= normalise(target_statement) 25 | end 26 | present = !statement.nil? 27 | 28 | if present && options[:without_keys] 29 | options[:without_keys].each do |key| 30 | return false if statement.key?(key) 31 | end 32 | end 33 | 34 | present 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.1" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 3.74" 8 | } 9 | null = { 10 | source = "hashicorp/null" 11 | version = ">= 3.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /user-data/cluster.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "ECS_CLUSTER=${cluster_name}" > /etc/ecs/ecs.config 3 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | description = "The region into which to deploy the cluster." 3 | type = string 4 | } 5 | variable "vpc_id" { 6 | description = "The ID of the VPC into which to deploy the cluster." 7 | type = string 8 | } 9 | variable "subnet_ids" { 10 | description = "The IDs of the subnets for container instances." 11 | type = list(string) 12 | } 13 | 14 | variable "component" { 15 | description = "The component this cluster will contain." 16 | type = string 17 | } 18 | variable "deployment_identifier" { 19 | description = "An identifier for this instantiation." 20 | type = string 21 | } 22 | 23 | variable "cluster_name" { 24 | description = "The name of the cluster to create." 25 | type = string 26 | default = "default" 27 | nullable = false 28 | } 29 | 30 | variable "include_cluster_instances" { 31 | description = "Whether or not to provision an ASG to create ECS cluster container instances." 32 | type = bool 33 | default = true 34 | nullable = false 35 | } 36 | variable "cluster_instance_type" { 37 | description = "The instance type of the container instances." 38 | type = string 39 | default = "t2.medium" 40 | nullable = false 41 | } 42 | variable "cluster_instance_ssh_public_key_path" { 43 | description = "The path to the public key to use for the container instances." 44 | type = string 45 | default = null 46 | } 47 | 48 | variable "cluster_instance_root_block_device_size" { 49 | description = "The size in GB of the root block device on cluster instances." 50 | type = number 51 | default = 30 52 | nullable = false 53 | } 54 | variable "cluster_instance_root_block_device_type" { 55 | description = "The type of the root block device on cluster instances ('standard', 'gp2', or 'io1')." 56 | type = string 57 | default = "standard" 58 | nullable = false 59 | } 60 | 61 | variable "cluster_instance_root_block_device_path" { 62 | description = "Path of the instance root block storage volume." 63 | type = string 64 | default = "/dev/xvda" 65 | nullable = false 66 | } 67 | 68 | variable "cluster_instance_user_data_template" { 69 | description = "The contents of a template for container instance user data." 70 | type = string 71 | default = null 72 | } 73 | 74 | variable "cluster_instance_ami" { 75 | description = "AMI for the container instances." 76 | type = string 77 | default = null 78 | } 79 | 80 | variable "cluster_instance_iam_policy_contents" { 81 | description = "The contents of the cluster instance IAM policy." 82 | type = string 83 | default = null 84 | } 85 | 86 | variable "cluster_instance_metadata_options" { 87 | description = "The metadata_options for cluster instances." 88 | type = map 89 | default = null 90 | } 91 | variable "cluster_service_iam_policy_contents" { 92 | description = "The contents of the cluster service IAM policy." 93 | type = string 94 | default = null 95 | } 96 | 97 | variable "cluster_minimum_size" { 98 | description = "The minimum size of the ECS cluster." 99 | type = string 100 | default = 1 101 | nullable = false 102 | } 103 | variable "cluster_maximum_size" { 104 | description = "The maximum size of the ECS cluster." 105 | type = string 106 | default = 10 107 | nullable = false 108 | } 109 | variable "cluster_desired_capacity" { 110 | description = "The desired capacity of the ECS cluster." 111 | type = string 112 | default = 3 113 | nullable = false 114 | } 115 | 116 | variable "associate_public_ip_addresses" { 117 | description = "Whether or not to associate public IP addresses with ECS container instances." 118 | type = string 119 | default = false 120 | nullable = false 121 | } 122 | 123 | variable "security_groups" { 124 | description = "The list of security group IDs to associate with the cluster." 125 | type = list(string) 126 | default = [] 127 | nullable = false 128 | } 129 | 130 | variable "include_default_ingress_rule" { 131 | description = "Whether or not to include the default ingress rule on the ECS container instances security group." 132 | type = string 133 | default = true 134 | nullable = false 135 | } 136 | variable "include_default_egress_rule" { 137 | description = "Whether or not to include the default egress rule on the ECS container instances security group." 138 | type = string 139 | default = true 140 | nullable = false 141 | } 142 | variable "default_ingress_cidrs" { 143 | description = "The CIDRs allowed access to containers." 144 | type = list(string) 145 | default = ["10.0.0.0/8"] 146 | nullable = false 147 | } 148 | variable "default_egress_cidrs" { 149 | description = "The CIDRs accessible from containers." 150 | type = list(string) 151 | default = ["0.0.0.0/0"] 152 | nullable = false 153 | } 154 | 155 | variable "tags" { 156 | description = "Map of tags to be applied to all resources in cluster." 157 | type = map(string) 158 | default = {} 159 | nullable = false 160 | } 161 | 162 | variable "enable_container_insights" { 163 | description = "Whether or not to enable container insights on the ECS cluster." 164 | type = string 165 | default = false 166 | nullable = false 167 | } 168 | 169 | variable "protect_cluster_instances_from_scale_in" { 170 | description = "Whether or not to protect cluster instances in the autoscaling group from scale in." 171 | type = string 172 | default = false 173 | nullable = false 174 | } 175 | 176 | variable "include_asg_capacity_provider" { 177 | description = "Whether or not to add the created ASG as a capacity provider for the ECS cluster." 178 | type = string 179 | default = false 180 | nullable = false 181 | } 182 | variable "asg_capacity_provider_manage_termination_protection" { 183 | description = "Whether or not to allow ECS to manage termination protection for the ASG capacity provider." 184 | type = string 185 | default = true 186 | nullable = false 187 | } 188 | variable "asg_capacity_provider_manage_scaling" { 189 | description = "Whether or not to allow ECS to manage scaling for the ASG capacity provider." 190 | type = string 191 | default = true 192 | nullable = false 193 | } 194 | variable "asg_capacity_provider_minimum_scaling_step_size" { 195 | description = "The minimum scaling step size for ECS managed scaling of the ASG capacity provider." 196 | type = number 197 | default = 1 198 | nullable = false 199 | } 200 | variable "asg_capacity_provider_maximum_scaling_step_size" { 201 | description = "The maximum scaling step size for ECS managed scaling of the ASG capacity provider." 202 | type = number 203 | default = 1000 204 | nullable = false 205 | } 206 | variable "asg_capacity_provider_target_capacity" { 207 | description = "The target capacity, as a percentage from 1 to 100, for the ASG capacity provider." 208 | type = number 209 | default = 100 210 | nullable = false 211 | } 212 | variable "additional_capacity_providers" { 213 | description = "Additional capacity providers to associate with the ECS cluster. Supports \"FARGATE\" and \"FARGATE_SPOT\"." 214 | type = list(string) 215 | default = [] 216 | nullable = false 217 | } 218 | 219 | variable "cluster_log_group_retention" { 220 | description = "The number of days logs will be retained in the CloudWatch log group of the cluster." 221 | type = number 222 | default = 0 223 | nullable = false 224 | } 225 | 226 | variable "enable_detailed_monitoring" { 227 | description = "Enable detailed monitoring of EC2 instance(s)." 228 | type = bool 229 | default = true 230 | nullable = false 231 | } 232 | 233 | variable "cluster_instance_enable_ebs_volume_encryption" { 234 | description = "Determines whether encryption is enabled on the EBS volume." 235 | type = bool 236 | default = true 237 | nullable = false 238 | } 239 | 240 | variable "cluster_instance_ebs_volume_kms_key_id" { 241 | description = "KMS key to use for encryption of the EBS volume when enabled." 242 | type = string 243 | default = null 244 | } 245 | --------------------------------------------------------------------------------