diff --git a/.ci/bin/generatedocs.sh b/.ci/bin/generatedocs.sh new file mode 100755 index 0000000..77a8e2b --- /dev/null +++ b/.ci/bin/generatedocs.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +GIT_REF=${GIT_REF:-develop} + +# script to auto-generate terraform documentation + +# pandoc -v &> /dev/null || { echo >&2 "ERROR: Pandoc not installed" ; exit 1 ; } +terraform-docs --version &> /dev/null || { echo >&2 "ERROR: terraform-docs not installed" ; exit 1 ; } + +IFS=$'\n' +# create an array of all unique directories containing .tf files +arr=($(find . -name '*.tf' | xargs -I % sh -c 'dirname %' | sort -u)) +unset IFS + +for i in "${arr[@]}" +do + # check for _docs folder + docs_dir=$i/_docs + echo $docs_dir + if [[ -d "$docs_dir" ]]; then + + if ! test -f $docs_dir/README.md; then + echo "ERROR: _docs dir found with no README.md"; exit 1 + fi + + # generate the tf documentation + echo "generating docs for: $i" + .ci/bin/terraform-docs.sh markdown $i > $docs_dir/TF_MODULE.md + INPUT_OUTPUT=$(.ci/bin/terraform-docs.sh markdown $i | sed -e 's/ /\'$'\n/g') + + # merge the tf docs with the main readme + # pandoc --wrap=none -f gfm -t gfm $docs_dir/README.md -A $docs_dir/TF_MODULE.md > $i/README.md + # sed -e '/___TF_INPUT_OUTPUT_VARS___/ {' -e 'r $docs_dir/TF_MODULE.md' -e 'd' -e '}' -i $i/README.md + # cp $docs_dir/README.md $i/README.md + # sed -i '' '/___TF_INPUT_OUTPUT_VARS___/ {' -e 'r $docs_dir/TF_MODULE.md' -e 'd' -e '}' $i/README.md + # sed -i -e '/___TF_INPUT_OUTPUT_VARS___/{r $docs_dir/TF_MODULE.md' -e 'd}' $i/README.md + # sed -e "/___TF_INPUT_OUTPUT_VARS___/r $docs_dir/TF_MODULE.md" -e "/___TF_INPUT_OUTPUT_VARS___/d" $i/README.md + + #sed "s/___TF_INPUT_OUTPUT_VARS___/${INPUT_OUTPUT}/g" $docs_dir/README.md + PATTERN=___TF_INPUT_OUTPUT_VARS___ \ + .ci/bin/var-replace.awk \ + $docs_dir/README.md \ + $docs_dir/TF_MODULE.md + + # # Create a absolute link for terraform registry + # sed -i ".bak" -e "s|__GIT_REF__|${GIT_REF}|" $i/README.md + # rm -rf $i/README.md.bak + + # do some cleanup + # because sed on macOS is special.. + # if [[ "$OSTYPE" == "darwin"* ]]; then + # sed -i '' '//d' $i/README.md # quirk of pandoc + # else + # sed -i -e '//d' $i/README.md # quirk of pandoc + # fi + + elif [[ ! -d "$docs_dir" && $i != *".terraform"* ]]; then + # .ci/bin/terraform-docs.sh markdown $i > $i/README.md + echo test + fi +done diff --git a/.ci/bin/install.sh b/.ci/bin/install.sh new file mode 100755 index 0000000..9d08242 --- /dev/null +++ b/.ci/bin/install.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source $(dirname "${BASH_SOURCE[0]}")/terraform.sh + +installTerraform diff --git a/.ci/bin/run-local.sh b/.ci/bin/run-local.sh new file mode 100755 index 0000000..b652e17 --- /dev/null +++ b/.ci/bin/run-local.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +docker run --entrypoint="/bin/sh" -it --rm -w /build -v $(pwd):/build hashicorp/terraform:0.12.7 ./.ci/bin/verify.sh +docker run --entrypoint="/bin/sh" -it --rm -w /build -v $(pwd):/build hashicorp/terraform:0.12.7 ./.ci/bin/verify-examples.sh diff --git a/.ci/bin/terraform-docs.awk b/.ci/bin/terraform-docs.awk new file mode 100644 index 0000000..9d552cc --- /dev/null +++ b/.ci/bin/terraform-docs.awk @@ -0,0 +1,81 @@ +# sources copied from https://github.com/cloudposse/build-harness + +# This script converts Terraform 0.12 variables/outputs to something suitable for `terraform-docs` +# As of terraform-docs v0.6.0, HCL2 is not supported. This script is a *dirty hack* to get around it. +# https://github.com/segmentio/terraform-docs/ +# https://github.com/segmentio/terraform-docs/issues/62 + +{ + if ( /\{/ ) { + braceCnt++ + } + + if ( /\}/ ) { + braceCnt-- + } + + # [START] variable or output block started + if ($0 ~ /(variable|output) "(.*?)"/) { + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + blockCnt++ + print $0 + } + + # [START] multiline default statement started + if (blockCnt > 0) { + if ($1 == "default") { + print $0 + if ($NF ~ /[\[\(\{]/) { + blockDefCnt++ + blockDefStart=1 + } + } + } + + # [PRINT] single line "description" + if (blockDefCnt == 0) { + if ($1 == "description") { + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + print $0 + } + } + + # [PRINT] single line "type" + if (blockCnt > 0) { + if ($1 == "type" ) { + # [CLOSE] "default" block + if (blockDefCnt > 0) { + blockDefCnt = 0 + } + type=$3 + if (type ~ "object") { + print " type = \"object\"" + } else { + print " type = \"" $3 "\"" + } + } + } + + # [CLOSE] variable/output block + if (blockCnt > 0) { + if (braceCnt == 0 && blockCnt > 0) { + blockCnt-- + print $0 + } + } + + # [PRINT] Multiline "default" statement + if (blockCnt > 0 && blockDefCnt > 0) { + if (blockDefStart == 1) { + blockDefStart = 0 + } else { + print $0 + } + } +} diff --git a/.ci/bin/terraform-docs.sh b/.ci/bin/terraform-docs.sh new file mode 100755 index 0000000..2189c0a --- /dev/null +++ b/.ci/bin/terraform-docs.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +which awk 2>&1 >/dev/null || ( echo "awk not available"; exit 1) +which terraform 2>&1 >/dev/null || ( echo "terraform not available"; exit 1) +which terraform-docs 2>&1 >/dev/null || ( echo "terraform-docs not available"; exit 1) + +if [[ "`terraform version | head -1`" =~ 0\.12 ]]; then + TMP_FILE="$(mktemp /tmp/terraform-docs.XXXXXXXXXX)" + awk -f ${PWD}/.ci/bin/terraform-docs.awk $2/*.tf > ${TMP_FILE} + terraform-docs $1 ${TMP_FILE} + exit 1 + #rm -f ${TMP_FILE} +else + terraform-docs $1 $2 +fi diff --git a/.ci/bin/terraform.sh b/.ci/bin/terraform.sh new file mode 100755 index 0000000..d64a092 --- /dev/null +++ b/.ci/bin/terraform.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +TARGET_DIR=/opt +PATH=${PATH}:${TARGET_DIR} + +TERRAFORM_VERSION=${1:-"0.12.7"} +OS=${2:-"linux"} +TERRAFORM_URL="https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_${OS}_amd64.zip" + +installTerraform() { + echo "Downloading terraform: ${TERRAFORM_URL}" + + curl '-#' -fL -o ${TARGET_DIR}/terraform.zip ${TERRAFORM_URL} && + unzip -q -d ${TARGET_DIR}/ ${TARGET_DIR}/terraform.zip && + terraform --version +} + +verifyModulesAndPlugins() { + echo "Verify plugins and modules can be resolved in $PWD" + terraform init -get -backend=false -input=false +} + +formatCheck() { + RESULT=$(terraform fmt -write=false) + if [[ ! -z ${RESULT} ]]; then + echo The following files are formatted incorrectly: $RESULT + exit 1 + fi +} + +validate() { + echo "Validating and checking format of terraform code in $PWD" + terraform validate + formatCheck +} diff --git a/.ci/bin/var-replace.awk b/.ci/bin/var-replace.awk new file mode 100755 index 0000000..8af33e2 --- /dev/null +++ b/.ci/bin/var-replace.awk @@ -0,0 +1,107 @@ +#!/usr/bin/awk -f + +# templater - takes a file and replaces a variable +# in a given template with the contents of another file. + +# err() - Prints a supplied `text` to standard error. +# @text: Text to be printed to stderr. +function err (text) { + print text > "/dev/stderr" +} + + +# The BEGIN matcher is a special type of matcher that +# gets executed whenever the AWK program is starting +# and no records have been matched yet. +BEGIN { + if (ARGC != 3) { + err("Error: not enough arguments.") + err("") + + err("Usage: ./templater ") + err("Aborting.") + exit 1 + } + + if (length(ENVIRON["PATTERN"]) == 0) { + err("Error: no pattern specified.") + err("") + + err("Specify a pattern via the `PATTERN` environment variable.") + err("For example: ") + err(" PATTERN=__CONTENT__ templater contents.txt template.txt") + err("Aborting.") + exit 1 + } +} + +# By using the `NR=FNR` pattern we're able to specify +# an action that we want to perform only on the first +# file that we supply via the command line. +# +# FNR is a counter that keeps track of the current line +# in the current file that is being processed. +# +# NR is a counter that keeps track of the total number +# of lines that have been processed so far. +# +# By trying to match `NR==FNR` we can perform an action +# in the very first file. To visualize that, we can set +# up an experiment: +# +# $ cat file1 +# a +# b +# c +# +# $ cat file2 +# d +# e +# +# $ awk '{print FILENAME, NR, FNR, $0}' file1 file2 +# file1 1 1 a +# file1 2 2 b +# file1 3 3 c +# file2 4 1 d -> not equal -> starts the second one +# file2 5 2 e -> not equal +# +# In the action we can then store all the lines from +# the first file in memory so that we can use it later +# when we find the string to replace. +# +# By specifying the `next` statement, no further matching +# is performed for this record (line). +# +# ps.: we could also check `FILENAME`, like: +# FILENAME==ARGV[1] +NR==FNR { + content_lines[n++]=$0; + next; +} + +# Once we find the string to replace, we iterate over +# all the lines that we stored (from the first file) +# and then once we're done, we force AWK to immediately +# stop processing the current record so that it doesn't +# print `__CONTENT__` and don't proceed with performing +# further matches for this record (line). +# +# ps.: if you didn't want to take a variable here, for +# instance, have a fixed pattern to replace, you could +# simply use `/PATTERN/ { ... }`. +$0 ~ ENVIRON["PATTERN"] { + for (i = 0; i < n; i++) { + print content_lines[i]; + } + next +} + +# Given that 1 always evaluates to `true`, this is a match +# that will always occur. +# +# As we can either omit an action or a match (not both!), +# we can use a catch-all match (1) and let awk use the +# default action (print current line). +# +# This has the effect of printing all lines that didn't +# match the other matches that we specified above. \ No newline at end of file diff --git a/.ci/bin/verify-examples.sh b/.ci/bin/verify-examples.sh new file mode 100755 index 0000000..5836c9d --- /dev/null +++ b/.ci/bin/verify-examples.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +DIR=${1:-examples} + +source $(dirname $0)/terraform.sh + +EXAMPLES="$(find ${DIR} -maxdepth 1 -mindepth 1 -type d 2> /dev/null )" +if [[ -z $EXAMPLES || "$($(echo $EXAMPLES) | wc -l)" -gt 0 ]] ; then + echo "No example(s) directories found." + exit 1 +fi + +for example in ${EXAMPLES} ; do + echo Verifying example $example + if [[ $(find ${example} -type f | grep "*.tf" | wc -l) -gt 0 ]] ; then + echo no tf files + exit 1 + fi + verifyModulesAndPlugins ${example} + validate ${example} +done diff --git a/.ci/bin/verify.sh b/.ci/bin/verify.sh new file mode 100755 index 0000000..80a3356 --- /dev/null +++ b/.ci/bin/verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source $(dirname $0)/terraform.sh + +verifyModulesAndPlugins +validate diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..a0ad21f --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,27 @@ +name: CI + +on: [push] + +jobs: + verify: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: verify + run: | + ./.ci/bin/install.sh + ./.ci/bin/verify.sh + + verify-examples: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: verify-examples + run: | + ./.ci/bin/install.sh + ./.ci/bin/verify-examples.sh + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ead811..62f3578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 2.0.0 - 2019-09-03 +- Upgrade to Terraform 0.12 +- Add github actions + ## 1.4.0 - 2019-08-09 - Changed: health_check_path is always '/' upon aws_alb_target_group creation #22 @tminuss @@ -42,7 +46,8 @@ https://github.com/philips-software/terraform-aws-ecs-service/tags/1.0.0 - Refactor outputs to support terraform 0.11 - Add support to mount volumes -[Unreleased]: https://github.com/philips-software/terraform-aws-ecs-service/compare/1.4.0...HEAD +[Unreleased]: https://github.com/philips-software/terraform-aws-ecs-service/compare/2.0.0...HEAD +[2.0.0]: https://github.com/philips-software/terraform-aws-ecs-service/compare/1.4.0...2.0.0 [1.4.0]: https://github.com/philips-software/terraform-aws-ecs-service/compare/1.3.0...1.4.0 [1.3.0]: https://github.com/philips-software/terraform-aws-ecs-service/compare/1.2.1...1.3.0 [1.2.1]: https://github.com/philips-software/terraform-aws-ecs-service/compare/1.2.0...1.2.1 diff --git a/README.md b/README.md index a53a665..84b435b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ Terraform module for creating a ECS docker service with optional load balancer and DNS record +## Terraform version + +- Terraform 0.12: Pin module to `~> 2+`, submit pull request to branch `develop` +- Terraform 0.11: Pin module to `~> 1.x`, submit pull request to branch `terrafomr011` + + ### Deprecated - `enable_alb` : Since release 1.3.0 the load balancer can be controlled externally, load balancers can be create via a separate module. In the next major release the embedded alb in this module will be removed. @@ -11,198 +17,79 @@ Terraform module for creating a ECS docker service with optional load balancer a + when using default monitoring metrics make sure that you specify the ecs clustername!!!! ## Example usages: - -### Without Load Balancing -``` - -resource "aws_sns_topic" "monitoring" { - name = "${var.environment}-monitoring" -} - -module "service" { - source = "philips-software/ecs-service/aws" - version = "1.0.0" - - # Or via github - # source = "github.com/philips-software/terraform-aws-ecs-service?ref=1.0.0" - - environment = "${var.environment}" - aws_region = "${var.aws_region}" - - ecs_cluster_id = "${module.ecs-cluster.ecs_cluster_id}" - ecs_cluster_name = "${module.ecs_cluster.name}" - - docker_image = "npalm/docker-introduction" - container_port = "80" - service_name = "test" - - // Monitoring settings - monitoring_sns_topic_arn = "${aws_sns_topic.monitoring.arn}" - - // All settings below are optional - container_cpu = "1024" - container_memory = "2048" - - desired_count = "1" - - docker_environment_vars = <` | no | -| enable_alb | If true an ALB is created. | string | `false` | no | -| enable_dns | Enable creation of DNS record. | string | `true` | no | -| enable_load_balanced | Creates a services that can be load balanced, a ecs-services, target group and listener rule. | string | `false` | no | -| enable_monitoring | If true monitoring alerts will be created if needed. | string | `true` | no | -| enable_target_group_connection | In case `true` a load balanceer is created for the services which will be connected to the target group specified in `target_group_arn`. Creating a load balancer for an ecs service requires a target group with a connected load balancer. To ensure ther right order of creation provide a list of depended arn in `ecs_services_dependencies` | string | `false` | no | -| environment | Name of the environment (e.g. project-dev); will be prefixed to all resources. | string | - | yes | -| health_check | Health check for the target group, will overwrite the defaults (merged). Defaults: `protocol=HTTP or HTTPS` depends on `container_ssl`, `path=/`, `matcher=200-399` and `interval=30`. | map | `` | no | -| health_check_grace_period_seconds | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 1800. Only valid for services configured to use load balancers. | string | `0` | no | -| health_check_interval | The approximate amount of time, in seconds, between health checks of an individual target. Minimum value 5 seconds, Maximum value 300 seconds. Default 30 seconds. | string | `30` | no | -| health_check_matcher | HTTP result code used for health validation. | string | `200-399` | no | -| health_check_path | The url path part for the health check endpoint. | string | `/` | no | -| internal_alb | If true this ALB is only available within the VPC, default (false) is publicly accessable (internetfacing). | string | `false` | no | -| lb_listener_rule_condition | The condtion fot he LB listener rule created when `enable_load_balanced` is set. | map | `` | no | -| listener_arn | Required for `enable_load_balanced`, provide the arn of the listener connected to a load balancer. By default a rule to the root of the listener will be created. | string | `` | no | -| monitoring_sns_topic_arn | ARN for the SNS topic to send alerts to. | string | `` | no | -| project | Project cost center / cost allocation. | string | - | yes | -| service_name | Name of the service to be created. | string | - | yes | -| ssl_policy | SSL policy applied to an SSL enabled ALB, see https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html | string | `ELBSecurityPolicy-TLS-1-2-2017-01` | no | -| subnet_ids | Comma separated list with subnet itd. | string | `` | no | -| tags | A map of tags to add to the resources | map | `` | no | -| target_group_arn | Required for `enable_target_group_connection` provides the target group arn to be connected to the ecs load balancer. Ensure you provide the arn's of th listeners or listeners rule conntected to the target group as `ecs_services_dependencies`. | string | `` | no | -| task_role_arn | The ARN of IAM role that allows your Amazon ECS container task to make calls to other AWS services. | string | `` | no | -| volumes | Defines the volumes that can be mounted to a container. | list | `` | no | -| vpc_id | The VPC to launch the ALB in in (e.g. vpc-66ecaa02). | string | `` | no | +| alb\_certificate\_arn | The AWS certificate ARN, required for an ALB via HTTPS. The certificate should be available in the same zone. | string | `""` | no | +| alb\_port | Defines to port for the ALB. | number | `"443"` | no | +| alb\_protocol | Defines the ALB protocol to be used. | string | `"HTTPS"` | no | +| alb\_timeout | The idle timeout in seconds of the ALB | number | `"60"` | no | +| container\_cpu | CPU shares to be assigned to the container. | string | `""` | no | +| container\_memory | Memory to be assigned to the container. | number | `"400"` | no | +| container\_port | The container port to be exported to the host. | string | n/a | yes | +| container\_ssl\_enabled | Set to true if container has SSL enabled. This requires that the container can handle HTTPS traffic. | bool | `"false"` | no | +| desired\_count | The number of desired tasks | number | `"1"` | no | +| dns\_name | The name DNS name. | string | `""` | no | +| dns\_zone\_id | The ID of the DNS zone. | string | `""` | no | +| docker\_environment\_vars | A JSON formated array of tuples of docker enviroment variables. | string | `""` | no | +| docker\_image | Name of te docker image. | string | n/a | yes | +| docker\_image\_tag | The docker image version (e.g. 1.0.0 or latest). | string | `"latest"` | no | +| docker\_logging\_config | The configuration for docker container logging | string | `""` | no | +| docker\_mount\_points | Defines the the mount point for the container. | string | `""` | no | +| docker\_repository | The location of the docker repository (e.g. 123456789.dkr.ecr.eu-west-1.amazonaws.com). | string | `"docker.io"` | no | +| ecs\_cluster\_id | The id of the ECS cluster where this service will be launched. | string | n/a | yes | +| ecs\_cluster\_name | The name of the ECS cluster where this service will be launched. | string | n/a | yes | +| ecs\_service\_role | ECS service role. | string | `""` | no | +| ecs\_services\_dependencies | A list of arns can be provided to which the creation of the ecs service is depended. | list(string) | `` | no | +| enable\_alb | If true an ALB is created. | bool | `"false"` | no | +| enable\_dns | Enable creation of DNS record. | bool | `"true"` | no | +| enable\_load\_balanced | Enables load balancing for a service by creating a target group and listener rule. This option should NOT be used together with `enable_target_group_connection` delegates the creation of the target group to component that use this module. | bool | `"false"` | no | +| enable\_monitoring | If true monitoring alerts will be created if needed. | bool | `"true"` | no | +| enable\_target\_group\_connection | If `true` a load balancer is created for the service which will be connected to the target group specified in `target_group_arn`. Creating a load balancer for an ecs service requires a target group with a connected load balancer. To ensure the right order of creation, provide a list of depended arns in `ecs_services_dependencies` | bool | `"false"` | no | +| environment | Name of the environment (e.g. project-dev); will be prefixed to all resources. | string | n/a | yes | +| health\_check | Health check for the target group, will overwrite the defaults (merged). Defaults: `protocol=HTTP or HTTPS` depends on `container_ssl`, `path=/`, `matcher=200-399` and `interval=30`. | map(string) | `` | no | +| health\_check\_grace\_period\_seconds | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 1800. Only valid for services configured to use load balancers. | string | `"0"` | no | +| health\_check\_interval | The approximate amount of time, in seconds, between health checks of an individual target. Minimum value 5 seconds, Maximum value 300 seconds. Default 30 seconds. | string | `"30"` | no | +| health\_check\_matcher | HTTP result code used for health validation. | string | `"200-399"` | no | +| health\_check\_path | The url path part for the health check endpoint. | string | `"/"` | no | +| internal\_alb | If true this ALB is only available within the VPC, default (false) is publicly accessable (internetfacing). | bool | `"false"` | no | +| lb\_listener\_rule\_condition | The condition for the LB listener rule which is created when `enable_load_balanced` is set. | map(string) | `` | no | +| listener\_arn | Required for `enable_load_balanced`, provide the arn of the listener connected to a load balancer. By default a rule to the root of the listener will be created. | string | `""` | no | +| monitoring\_sns\_topic\_arn | ARN for the SNS topic to send alerts to. | string | `""` | no | +| project | Project cost center / cost allocation. | string | n/a | yes | +| service\_name | Name of the service to be created. | string | n/a | yes | +| ssl\_policy | SSL policy applied to an SSL enabled ALB, see https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html | string | `"ELBSecurityPolicy-TLS-1-2-2017-01"` | no | +| subnet\_ids | List of subnet itd to deploy the ALB. | list(string) | `` | no | +| tags | A map of tags to add to the resources | map(string) | `` | no | +| target\_group\_arn | Required for `enable_target_group_connection` provides the target group arn to be connected to the ecs load balancer. Ensure you provide the arns of the listeners or listeners rule conntected to the target group as `ecs_services_dependencies`. | string | `""` | no | +| task\_role\_arn | The ARN of IAM role that allows your Amazon ECS container task to make calls to other AWS services. | string | `""` | no | +| volumes | Defines the volumes that can be mounted to a container. | list(map(string)) | `` | no | +| vpc\_id | The VPC to launch the ALB in in (e.g. vpc-66ecaa02). | string | `""` | no | ## Outputs | Name | Description | |------|-------------| -| alb_dns_name | DNS address of the load balancer, if created. | -| alb_route53_dns_name | Route 53 DNS name, if created. | -| aws_alb_target_group_arn | ARN of the loadbalancer target group. | +| alb\_dns\_name | DNS address of the load balancer, if created. | +| alb\_route53\_dns\_name | Route 53 DNS name, if created. | +| aws\_alb\_target\_group\_arn | ARN of the loadbalancer target group. | + +## Automated checks +Currently the automated checks are limited. In CI the following checks are done for the root and each example. +- lint: `terraform validate` and `terraform fmt` +- basic init / get check: `terraform init -get -backend=false -input=false` + +## Generation variable documentation +A markdown table for variables can be generated as follow. Generation requires awk and terraform-docs installed. + +``` + .ci/bin/terraform-docs.sh markdown +``` ## Philips Forest diff --git a/alb.tf b/alb.tf index ea58bec..08f3d99 100644 --- a/alb.tf +++ b/alb.tf @@ -1,21 +1,21 @@ data "aws_vpc" "selected" { - count = "${var.enable_alb ? 1 : 0}" - id = "${var.vpc_id}" + count = var.enable_alb ? 1 : 0 + id = var.vpc_id } resource "aws_security_group" "security_group_alb" { // Only enable if LB is required - count = "${var.enable_alb ? 1 : 0}" + count = var.enable_alb ? 1 : 0 name_prefix = "${var.environment}-${var.service_name}" - vpc_id = "${var.vpc_id}" + vpc_id = var.vpc_id # allow all incoming traffic - ingress = { - from_port = "${var.alb_port}" - to_port = "${var.alb_port}" + ingress { + from_port = var.alb_port + to_port = var.alb_port protocol = "tcp" - cidr_blocks = ["${var.internal_alb ? data.aws_vpc.selected.cidr_block : "0.0.0.0/0"}"] + cidr_blocks = [var.internal_alb ? data.aws_vpc.selected[0].cidr_block : "0.0.0.0/0"] } # allow all outgoing traffic @@ -23,14 +23,24 @@ resource "aws_security_group" "security_group_alb" { from_port = 0 to_port = 0 protocol = "-1" - cidr_blocks = ["${data.aws_vpc.selected.cidr_block}"] + cidr_blocks = [data.aws_vpc.selected[0].cidr_block] } - tags = "${merge(map("Name", format("%s", "${var.environment}-${var.service_name}")), - map("Environment", format("%s", var.environment)), - map("Project", format("%s", var.project)), - map("Application", format("%s", var.service_name)), - var.tags)}" + tags = merge( + { + "Name" = format("%s", "${var.environment}-${var.service_name}") + }, + { + "Environment" = format("%s", var.environment) + }, + { + "Project" = format("%s", var.project) + }, + { + "Application" = format("%s", var.service_name) + }, + var.tags, + ) lifecycle { create_before_destroy = true @@ -39,75 +49,121 @@ resource "aws_security_group" "security_group_alb" { resource "aws_alb" "alb" { // Only enable if LB is required - count = "${var.enable_alb ? 1 : 0}" + count = var.enable_alb ? 1 : 0 - internal = "${var.internal_alb}" - security_groups = ["${aws_security_group.security_group_alb.id}"] - subnets = ["${split(",", var.subnet_ids)}"] + internal = var.internal_alb + security_groups = [aws_security_group.security_group_alb[0].id] + subnets = var.subnet_ids - idle_timeout = "${var.alb_timeout}" + idle_timeout = var.alb_timeout enable_deletion_protection = false - tags = "${merge(map("Name", format("%s", "${var.environment}-${var.service_name}")), - map("Environment", format("%s", var.environment)), - map("Project", format("%s", var.project)), - map("Application", format("%s", var.service_name)), - var.tags)}" + tags = merge( + { + "Name" = format("%s", "${var.environment}-${var.service_name}") + }, + { + "Environment" = format("%s", var.environment) + }, + { + "Project" = format("%s", var.project) + }, + { + "Application" = format("%s", var.service_name) + }, + var.tags, + ) } resource "aws_alb_target_group" "target_group" { - count = "${var.enable_alb || var.enable_load_balanced ? 1 : 0}" - - port = "${var.container_port}" - protocol = "${var.container_ssl_enabled ? "HTTPS" : "HTTP" }" - vpc_id = "${var.vpc_id}" - - health_check = ["${merge( - map("protocol", format("%s", var.container_ssl_enabled ? "HTTPS" : "HTTP")), - map("path", format("%s", var.health_check_path)), - map("matcher", format("%s", var.health_check_matcher)), - map("interval", format("%s", var.health_check_interval)), - var.health_check - )}"] + count = var.enable_alb || var.enable_load_balanced ? 1 : 0 + + port = var.container_port + protocol = var.container_ssl_enabled ? "HTTPS" : "HTTP" + vpc_id = var.vpc_id + + dynamic "health_check" { + for_each = [merge( + { + "protocol" = format("%s", var.container_ssl_enabled ? "HTTPS" : "HTTP") + }, + { + "path" = format("%s", var.health_check_path) + }, + { + "matcher" = format("%s", var.health_check_matcher) + }, + { + "interval" = format("%s", var.health_check_interval) + }, + var.health_check, + )] + content { + enabled = lookup(health_check.value, "enabled", null) + healthy_threshold = lookup(health_check.value, "healthy_threshold", null) + interval = lookup(health_check.value, "interval", null) + matcher = lookup(health_check.value, "matcher", null) + path = lookup(health_check.value, "path", null) + port = lookup(health_check.value, "port", null) + protocol = lookup(health_check.value, "protocol", null) + timeout = lookup(health_check.value, "timeout", null) + unhealthy_threshold = lookup(health_check.value, "unhealthy_threshold", null) + } + } lifecycle { create_before_destroy = true } # ALB id is added as tag to ensure the LB exists before creating the service - tags = "${merge(map("Name", format("%s", "${var.environment}-${var.service_name}")), - map("Environment", format("%s", var.environment)), - map("Project", format("%s", var.project)), - var.tags)}" + tags = merge( + { + "Name" = format("%s", "${var.environment}-${var.service_name}") + }, + { + "Environment" = format("%s", var.environment) + }, + { + "Project" = format("%s", var.project) + }, + var.tags, + ) } resource "aws_lb_listener_rule" "default" { - count = "${var.enable_load_balanced ? 1 : 0}" + count = var.enable_load_balanced ? 1 : 0 - listener_arn = "${var.listener_arn}" + listener_arn = var.listener_arn priority = 100 action { type = "forward" - target_group_arn = "${aws_alb_target_group.target_group.arn}" + target_group_arn = aws_alb_target_group.target_group[0].arn } - condition = ["${var.lb_listener_rule_condition}"] + + dynamic "condition" { + for_each = [var.lb_listener_rule_condition] + content { + field = condition.value["field"] + values = list(condition.value["values"]) + } + } } resource "aws_alb_listener" "listener" { // Only enable if ALB is required - count = "${var.enable_alb ? 1 : 0}" + count = var.enable_alb ? 1 : 0 - load_balancer_arn = "${aws_alb.alb.arn}" - protocol = "${var.alb_protocol}" - port = "${var.alb_port}" - certificate_arn = "${var.alb_certificate_arn}" - ssl_policy = "${var.alb_protocol == "HTTPS" ? "${var.ssl_policy}": ""}" + load_balancer_arn = aws_alb.alb[0].arn + protocol = var.alb_protocol + port = var.alb_port + certificate_arn = var.alb_certificate_arn + ssl_policy = var.alb_protocol == "HTTPS" ? var.ssl_policy : "" default_action { - target_group_arn = "${aws_alb_target_group.target_group.arn}" + target_group_arn = aws_alb_target_group.target_group[0].arn type = "forward" } } @@ -115,15 +171,16 @@ resource "aws_alb_listener" "listener" { resource "aws_route53_record" "dns_record" { // Only enable if ALB is required and dns_name is given // Note: checking if dns_name == "" did not work here... - count = "${var.enable_alb ? var.enable_dns : 0}" + count = var.enable_alb && var.enable_dns ? 1 : 0 - name = "${var.dns_name}" - zone_id = "${var.dns_zone_id}" + name = var.dns_name + zone_id = var.dns_zone_id type = "A" alias { - name = "${aws_alb.alb.dns_name}" - zone_id = "${aws_alb.alb.zone_id}" + name = aws_alb.alb[0].dns_name + zone_id = aws_alb.alb[0].zone_id evaluate_target_health = true } } + diff --git a/examples/default/.terraform-version b/examples/default/.terraform-version new file mode 100644 index 0000000..e2e3067 --- /dev/null +++ b/examples/default/.terraform-version @@ -0,0 +1 @@ +0.12.7 diff --git a/examples/public-alb-no-dns-no-ssl/README.md b/examples/default/README.md similarity index 80% rename from examples/public-alb-no-dns-no-ssl/README.md rename to examples/default/README.md index e1bd500..db30207 100644 --- a/examples/public-alb-no-dns-no-ssl/README.md +++ b/examples/default/README.md @@ -1,6 +1,8 @@ # Test ECS service -This directory contains a test setup for an ECS service. The service will run via HTTP. SSL, and DNS are disabled. +This directory contains a test setup for an ECS service. +- service default: ALB via HTTP +- service features: based on default, custom healthcheck and EFS ## Prerequisites for running the example diff --git a/examples/default/bastion.tf b/examples/default/bastion.tf new file mode 100644 index 0000000..29a5c68 --- /dev/null +++ b/examples/default/bastion.tf @@ -0,0 +1,13 @@ +module "bastion" { + source = "git::https://github.com/philips-software/terraform-aws-bastion.git?ref=2.0.0" + + enable_bastion = "false" + + environment = var.environment + project = var.project + + aws_region = var.aws_region + key_name = aws_key_pair.key.key_name + subnet_id = element(module.vpc.public_subnets, 0) + vpc_id = module.vpc.vpc_id +} diff --git a/examples/default/ecs.tf b/examples/default/ecs.tf new file mode 100644 index 0000000..6fbe9b8 --- /dev/null +++ b/examples/default/ecs.tf @@ -0,0 +1,62 @@ + +resource "aws_cloudwatch_log_group" "log_group" { + name = var.environment + + tags = { + Name = var.environment + Environment = var.environment + } +} + +resource "aws_key_pair" "key" { + key_name = var.key_name + public_key = file(var.ssh_key_file_ecs) +} + +data "template_file" "ecs_user_data_ecs" { + template = file("${path.module}/user-data-ecs-cluster-instance.tpl") + + vars = { + ecs_cluster_name = module.ecs_cluster.name + } +} + +data "template_cloudinit_config" "config" { + gzip = false + base64_encode = false + + part { + content_type = "text/x-shellscript" + content = data.template_file.ecs_user_data_ecs.rendered + } + + part { + content_type = module.efs.amazon_linux_cloudinit_config_part["content_type"] + content = module.efs.amazon_linux_cloudinit_config_part["content"] + } +} + +module "ecs_cluster" { + source = "git::https://github.com/philips-software/terraform-aws-ecs.git?ref=2.0.0" + + user_data = "${data.template_cloudinit_config.config.rendered}" + + aws_region = var.aws_region + environment = var.environment + + key_name = var.key_name + + vpc_id = module.vpc.vpc_id + vpc_cidr = module.vpc.vpc_cidr + + min_instance_count = 1 + max_instance_count = 1 + desired_instance_count = 1 + + instance_type = "t2.micro" + + subnet_ids = join(",", module.vpc.private_subnets) + + project = var.project +} + diff --git a/examples/default/efs.tf b/examples/default/efs.tf new file mode 100644 index 0000000..ac1b31f --- /dev/null +++ b/examples/default/efs.tf @@ -0,0 +1,8 @@ +module "efs" { + source = "git::https://github.com/philips-software/terraform-aws-efs.git?ref=2.0.0" + environment = var.environment + project = var.project + subnet_ids = module.vpc.private_subnets + vpc_id = module.vpc.vpc_id +} + diff --git a/examples/public-alb-no-dns-no-ssl/generate-ssh-key.sh b/examples/default/generate-ssh-key.sh similarity index 100% rename from examples/public-alb-no-dns-no-ssl/generate-ssh-key.sh rename to examples/default/generate-ssh-key.sh diff --git a/examples/default/outputs.tf b/examples/default/outputs.tf new file mode 100644 index 0000000..8e2aab9 --- /dev/null +++ b/examples/default/outputs.tf @@ -0,0 +1,9 @@ +output "url-default" { + value = "http://${lower(module.service.alb_dns_name)}" +} + + +output "url-feature" { + value = "http://${lower(module.service-features.alb_dns_name)}" +} + diff --git a/examples/default/providers.tf b/examples/default/providers.tf new file mode 100644 index 0000000..7ac49de --- /dev/null +++ b/examples/default/providers.tf @@ -0,0 +1,21 @@ +provider "aws" { + region = var.aws_region + version = "2.26" +} + +provider "template" { + version = "2.1" +} + +provider "local" { + version = "1.3" +} + +provider "null" { + version = "2.1" +} + +provider "tls" { + version = "2.1" +} + diff --git a/examples/public-alb-no-dns-no-ssl/service.tf b/examples/default/service-default.tf similarity index 54% rename from examples/public-alb-no-dns-no-ssl/service.tf rename to examples/default/service-default.tf index 702f6c6..c7dabed 100644 --- a/examples/public-alb-no-dns-no-ssl/service.tf +++ b/examples/default/service-default.tf @@ -1,24 +1,23 @@ module "service" { source = "../../" - environment = "${var.environment}" - project = "${var.project}" + environment = var.environment + project = var.project - ecs_cluster_id = "${module.ecs_cluster.id}" - ecs_cluster_name = "${module.ecs_cluster.name}" - docker_image = "npalm/docker-introduction" - service_name = "${var.service_name}" + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.public_subnets + ecs_cluster_id = module.ecs_cluster.id + ecs_cluster_name = module.ecs_cluster.name + docker_image = "nginx" + service_name = "service-default" // ALB part, over http without dns entry - ecs_service_role = "${module.ecs_cluster.service_role_name}" + ecs_service_role = module.ecs_cluster.service_role_name enable_alb = true - internal_alb = false - vpc_id = "${module.vpc.vpc_id}" - subnet_ids = "${join(",", module.vpc.public_subnets)}" - alb_port = 80 alb_protocol = "HTTP" + alb_port = 80 container_ssl_enabled = false - container_port = "80" + container_port = 80 // DNS specifc settings for the ALB, disalbed enable_dns = false @@ -34,8 +33,11 @@ module "service" { "options": { "awslogs-group": "${var.environment}", "awslogs-region": "${var.aws_region}", - "awslogs-stream-prefix": "${var.service_name}" + "awslogs-stream-prefix": "service-default" } } - EOF + +EOF + } + diff --git a/examples/default/service-features.tf b/examples/default/service-features.tf new file mode 100644 index 0000000..136f978 --- /dev/null +++ b/examples/default/service-features.tf @@ -0,0 +1,77 @@ +locals { + volumes = [ + { + name = "static_html" + host_path = "/efs/html" + }, + ] +} + +data "template_file" "volumes_mounts" { + template = <= 80%" + alarm_name = "LOW ${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} - CPUUtilization >= 80%" comparison_operator = "GreaterThanOrEqualToThreshold" metric_name = "CPUUtilization" namespace = "AWS/ECS" @@ -49,28 +53,32 @@ resource "aws_cloudwatch_metric_alarm" "service_cpu_usage_low" { statistic = "Maximum" threshold = "80" - alarm_description = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} cpu utilization to high" + alarm_description = "${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} cpu utilization to high" treat_missing_data = "ignore" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - ClusterName = "${var.ecs_cluster_name}" - ServiceName = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)}" + dimensions = { + ClusterName = var.ecs_cluster_name + ServiceName = var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name) } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } resource "aws_cloudwatch_metric_alarm" "service_cpu_usage_high" { - count = "${var.enable_monitoring ? 1 : 0}" + count = var.enable_monitoring ? 1 : 0 - alarm_name = "HIGH ${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} - CPUUtilization >= 95%" + alarm_name = "HIGH ${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} - CPUUtilization >= 95%" comparison_operator = "GreaterThanOrEqualToThreshold" metric_name = "CPUUtilization" namespace = "AWS/ECS" @@ -79,21 +87,25 @@ resource "aws_cloudwatch_metric_alarm" "service_cpu_usage_high" { statistic = "Maximum" threshold = "95" - alarm_description = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} cpu utilization to high" + alarm_description = "${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} cpu utilization to high" treat_missing_data = "ignore" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - ClusterName = "${var.ecs_cluster_name}" - ServiceName = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)}" + dimensions = { + ClusterName = var.ecs_cluster_name + ServiceName = var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name) } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } @@ -101,9 +113,9 @@ resource "aws_cloudwatch_metric_alarm" "service_cpu_usage_high" { # Memory # -------------------------------------------------------------------------------- resource "aws_cloudwatch_metric_alarm" "service_memory_usage_high" { - count = "${var.enable_monitoring ? 1 : 0}" + count = var.enable_monitoring ? 1 : 0 - alarm_name = "HIGH ${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} - MemoryUtilization >= 95%" + alarm_name = "HIGH ${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} - MemoryUtilization >= 95%" comparison_operator = "GreaterThanOrEqualToThreshold" metric_name = "MemoryUtilization" @@ -113,28 +125,32 @@ resource "aws_cloudwatch_metric_alarm" "service_memory_usage_high" { statistic = "Maximum" threshold = "95" - alarm_description = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} memory utilization to high" + alarm_description = "${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} memory utilization to high" treat_missing_data = "ignore" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - ClusterName = "${var.ecs_cluster_name}" - ServiceName = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)}" + dimensions = { + ClusterName = var.ecs_cluster_name + ServiceName = var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name) } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } resource "aws_cloudwatch_metric_alarm" "service_memory_usage_low" { - count = "${var.enable_monitoring ? 1 : 0}" + count = var.enable_monitoring ? 1 : 0 - alarm_name = "LOW ${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} - MemoryUtilization >= 80%" + alarm_name = "LOW ${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} - MemoryUtilization >= 80%" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = 5 metric_name = "MemoryUtilization" @@ -143,21 +159,25 @@ resource "aws_cloudwatch_metric_alarm" "service_memory_usage_low" { statistic = "Maximum" threshold = "80" - alarm_description = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)} memory utilization to high" + alarm_description = "${var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name)} memory utilization to high" treat_missing_data = "ignore" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - ClusterName = "${var.ecs_cluster_name}" - ServiceName = "${var.enable_alb ? join("",aws_ecs_service.service_alb.*.name) : join("",aws_ecs_service.service.*.name)}" + dimensions = { + ClusterName = var.ecs_cluster_name + ServiceName = var.enable_alb ? join("", aws_ecs_service.service_alb.*.name) : join("", aws_ecs_service.service.*.name) } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } @@ -165,9 +185,9 @@ resource "aws_cloudwatch_metric_alarm" "service_memory_usage_low" { # ALB Target Groups # -------------------------------------------------------------------------------- resource "aws_cloudwatch_metric_alarm" "alb_healthy_hostcount_high" { - count = "${var.enable_monitoring * var.enable_alb}" + count = var.enable_monitoring && var.enable_alb ? 1 : 0 - alarm_name = "HIGH ${aws_ecs_service.service_alb.name} - ALB HealthyHostCount too low < 1" + alarm_name = "HIGH ${aws_ecs_service.service_alb[0].name} - ALB HealthyHostCount too low < 1" comparison_operator = "LessThanThreshold" metric_name = "HealthyHostCount" namespace = "AWS/ApplicationELB" @@ -176,28 +196,32 @@ resource "aws_cloudwatch_metric_alarm" "alb_healthy_hostcount_high" { statistic = "Average" threshold = "1" - alarm_description = "${aws_ecs_service.service_alb.name} HealthyHostCount too low" + alarm_description = "${aws_ecs_service.service_alb[0].name} HealthyHostCount too low" treat_missing_data = "breaching" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - LoadBalancer = "${aws_alb.alb.arn_suffix}" - TargetGroup = "${aws_alb_target_group.target_group.arn_suffix}" + dimensions = { + LoadBalancer = aws_alb.alb[0].arn_suffix + TargetGroup = aws_alb_target_group.target_group[0].arn_suffix } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } resource "aws_cloudwatch_metric_alarm" "alb_healthy_hostcount_low" { - count = "${var.enable_monitoring * var.enable_alb}" + count = var.enable_monitoring && var.enable_alb ? 1 : 0 - alarm_name = "LOW ${aws_ecs_service.service_alb.name} - ALB HealthyHostCount too low < desired" + alarm_name = "LOW ${aws_ecs_service.service_alb[0].name} - ALB HealthyHostCount too low < desired" comparison_operator = "LessThanThreshold" metric_name = "HealthyHostCount" namespace = "AWS/ApplicationELB" @@ -205,21 +229,26 @@ resource "aws_cloudwatch_metric_alarm" "alb_healthy_hostcount_low" { period = "60" statistic = "Average" - threshold = "${aws_ecs_service.service_alb.desired_count}" - alarm_description = "${aws_ecs_service.service_alb.name} HealthyHostCount too low" + threshold = aws_ecs_service.service_alb[0].desired_count + alarm_description = "${aws_ecs_service.service_alb[0].name} HealthyHostCount too low" treat_missing_data = "ignore" actions_enabled = true - alarm_actions = ["${var.monitoring_sns_topic_arn}"] - ok_actions = ["${var.monitoring_sns_topic_arn}"] - insufficient_data_actions = ["${var.monitoring_sns_topic_arn}"] + alarm_actions = [var.monitoring_sns_topic_arn] + ok_actions = [var.monitoring_sns_topic_arn] + insufficient_data_actions = [var.monitoring_sns_topic_arn] - dimensions { - LoadBalancer = "${aws_alb.alb.arn_suffix}" - TargetGroup = "${aws_alb_target_group.target_group.arn_suffix}" + dimensions = { + LoadBalancer = aws_alb.alb[0].arn_suffix + TargetGroup = aws_alb_target_group.target_group[0].arn_suffix } lifecycle { - ignore_changes = ["threshold", "period", "evaluation_periods"] + ignore_changes = [ + threshold, + period, + evaluation_periods, + ] } } + diff --git a/outputs.tf b/outputs.tf index 1894ab0..b5ce379 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,15 @@ output "alb_dns_name" { description = "DNS address of the load balancer, if created." - value = "${element(concat(aws_alb.alb.*.dns_name, list("")), 0)}" + value = element(concat(aws_alb.alb.*.dns_name, [""]), 0) } output "aws_alb_target_group_arn" { description = "ARN of the loadbalancer target group." - value = "${element(concat(aws_alb_target_group.target_group.*.arn, list("")), 0)}" + value = element(concat(aws_alb_target_group.target_group.*.arn, [""]), 0) } output "alb_route53_dns_name" { description = "Route 53 DNS name, if created." - value = "${element(concat(aws_route53_record.dns_record.*.name, list("")), 0)}" + value = element(concat(aws_route53_record.dns_record.*.name, [""]), 0) } + diff --git a/variables.tf b/variables.tf index 125f530..c5e5f66 100644 --- a/variables.tf +++ b/variables.tf @@ -1,74 +1,74 @@ variable "environment" { description = "Name of the environment (e.g. project-dev); will be prefixed to all resources." - type = "string" + type = string } variable "project" { description = "Project cost center / cost allocation." - type = "string" + type = string } variable "ecs_cluster_id" { - type = "string" description = "The id of the ECS cluster where this service will be launched." + type = string } variable "docker_repository" { - type = "string" - default = "docker.io" description = "The location of the docker repository (e.g. 123456789.dkr.ecr.eu-west-1.amazonaws.com)." + type = string + default = "docker.io" } variable "docker_image_tag" { - type = "string" - default = "latest" description = "The docker image version (e.g. 1.0.0 or latest)." + type = string + default = "latest" } variable "docker_image" { - type = "string" description = "Name of te docker image." + type = string } variable "container_memory" { - default = "400" - type = "string" description = "Memory to be assigned to the container." + type = number + default = 400 } variable "container_cpu" { - default = "" - type = "string" description = "CPU shares to be assigned to the container." + type = string + default = "" } variable "docker_environment_vars" { description = "A JSON formated array of tuples of docker enviroment variables." - type = "string" + type = string default = "" } variable "service_name" { description = "Name of the service to be created." - type = "string" + type = string } variable "docker_logging_config" { - type = "string" - default = "" description = "The configuration for docker container logging" + type = string + default = "" } variable "desired_count" { - type = "string" - default = "1" description = "The number of desired tasks" + type = number + default = 1 } variable "task_role_arn" { - type = "string" - default = "" description = "The ARN of IAM role that allows your Amazon ECS container task to make calls to other AWS services." + type = string + default = "" } // ------ @@ -77,6 +77,7 @@ variable "task_role_arn" { variable "enable_alb" { description = "If true an ALB is created." + type = bool default = false } @@ -87,99 +88,108 @@ variable "alb_protocol" { variable "alb_port" { description = "Defines to port for the ALB." + type = number default = 443 } variable "alb_certificate_arn" { description = "The AWS certificate ARN, required for an ALB via HTTPS. The certificate should be available in the same zone." - type = "string" + type = string default = "" } variable "alb_timeout" { description = "The idle timeout in seconds of the ALB" + type = number default = 60 } variable "health_check_matcher" { description = "HTTP result code used for health validation." + type = string default = "200-399" } variable "health_check_path" { description = "The url path part for the health check endpoint." + type = string default = "/" } variable "health_check_interval" { description = "The approximate amount of time, in seconds, between health checks of an individual target. Minimum value 5 seconds, Maximum value 300 seconds. Default 30 seconds." + type = string default = "30" } variable "health_check_grace_period_seconds" { description = "Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 1800. Only valid for services configured to use load balancers." + type = string default = "0" } variable "ecs_service_role" { description = "ECS service role." - type = "string" + type = string default = "" } variable "container_ssl_enabled" { - default = false description = "Set to true if container has SSL enabled. This requires that the container can handle HTTPS traffic." + type = bool + default = false } variable "container_port" { description = "The container port to be exported to the host." - type = "string" + type = string } variable "enable_dns" { description = "Enable creation of DNS record." + type = bool default = true } variable "dns_zone_id" { - type = "string" description = "The ID of the DNS zone." + type = string default = "" } variable "dns_name" { - type = "string" description = "The name DNS name." + type = string default = "" } variable "vpc_id" { - type = "string" description = "The VPC to launch the ALB in in (e.g. vpc-66ecaa02)." + type = string default = "" } variable "subnet_ids" { - type = "string" - description = "Comma separated list with subnet itd." - default = "" + description = "List of subnet itd to deploy the ALB." + type = list(string) + default = [] } variable "internal_alb" { description = "If true this ALB is only available within the VPC, default (false) is publicly accessable (internetfacing)." + type = bool default = false } variable "docker_mount_points" { description = "Defines the the mount point for the container." - type = "string" + type = string default = "" } variable "volumes" { description = "Defines the volumes that can be mounted to a container." - type = "list" + type = list(map(string)) default = [] } @@ -188,72 +198,76 @@ variable "volumes" { // ------ variable "enable_monitoring" { description = "If true monitoring alerts will be created if needed." + type = bool default = true } variable "monitoring_sns_topic_arn" { - type = "string" description = "ARN for the SNS topic to send alerts to." + type = string default = "" } variable "ecs_cluster_name" { - type = "string" description = "The name of the ECS cluster where this service will be launched." + type = string } variable "tags" { - type = "map" description = "A map of tags to add to the resources" + type = map(string) default = {} } variable "ssl_policy" { - type = "string" description = "SSL policy applied to an SSL enabled ALB, see https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html" + type = string default = "ELBSecurityPolicy-TLS-1-2-2017-01" } variable "enable_target_group_connection" { description = "If `true` a load balancer is created for the service which will be connected to the target group specified in `target_group_arn`. Creating a load balancer for an ecs service requires a target group with a connected load balancer. To ensure the right order of creation, provide a list of depended arns in `ecs_services_dependencies`" + type = bool default = false } variable "enable_load_balanced" { description = "Enables load balancing for a service by creating a target group and listener rule. This option should NOT be used together with `enable_target_group_connection` delegates the creation of the target group to component that use this module." + type = bool default = false } variable "target_group_arn" { - type = "string" description = "Required for `enable_target_group_connection` provides the target group arn to be connected to the ecs load balancer. Ensure you provide the arns of the listeners or listeners rule conntected to the target group as `ecs_services_dependencies`." + type = string default = "" } variable "listener_arn" { - type = "string" description = "Required for `enable_load_balanced`, provide the arn of the listener connected to a load balancer. By default a rule to the root of the listener will be created." + type = string default = "" } variable "health_check" { - type = "map" description = "Health check for the target group, will overwrite the defaults (merged). Defaults: `protocol=HTTP or HTTPS` depends on `container_ssl`, `path=/`, `matcher=200-399` and `interval=30`." + type = map(string) default = {} } variable "lb_listener_rule_condition" { - type = "map" description = "The condition for the LB listener rule which is created when `enable_load_balanced` is set." + type = map(string) default = { field = "path-pattern" - values = ["/*"] + values = "/*" } } variable "ecs_services_dependencies" { - type = "list" description = "A list of arns can be provided to which the creation of the ecs service is depended." + type = list(string) default = [] } + diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..ac97c6a --- /dev/null +++ b/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +}