├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── Taskfile.yml ├── examples └── basic │ ├── .terraform.lock.hcl │ ├── README.md │ ├── keys │ ├── authorized_worker_keys │ ├── session_signing_key │ ├── session_signing_key.pub │ ├── tsa_host_key │ ├── tsa_host_key.pub │ ├── worker_key │ └── worker_key.pub │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── go.mod ├── go.sum ├── modules ├── atc │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── cloud-init │ ├── atc.yml │ ├── shared.yml │ └── worker.yml ├── dashboard │ ├── README.md │ ├── dashboard.json.template │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf └── worker │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── packer └── template.json └── test ├── module.go └── module_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.* text eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default. Unless we have a more specific match. 2 | * @telia-oss/terraform-owners 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Is something not working as expected? 4 | labels: bug 5 | --- 6 | 7 | ### Bug report 8 | 9 | > What is the problem? 10 | 11 | ### Steps to reproduce 12 | 13 | > Please post the relevant parts of the failing terraform code here (remember to remove sensitive information): 14 | 15 | ```hcl 16 | module "template" { 17 | name_prefix = "template-basic-example" 18 | } 19 | ``` 20 | 21 | ### Terraform version 22 | 23 | > Run `terraform version` and post the output here: 24 | 25 | ```bash 26 | $ terraform version 27 | 28 | Terraform v0.12.7 29 | + provider.aws v2.27.0 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | about: I have a suggestion! 4 | --- 5 | 6 | ### Feature request 7 | 8 | > How can we improve the module? 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: workflow 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Install Go 13 | uses: actions/setup-go@v1 14 | with: { go-version: 1.14 } 15 | 16 | - name: Install Terraform 17 | uses: hashicorp/setup-terraform@v1 18 | with: { terraform_version: 0.14.9 } 19 | 20 | - name: Install Taskfile 21 | run: curl -sL https://taskfile.dev/install.sh | sh 22 | 23 | - name: Run tests 24 | run: ./bin/task test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Terraform 2 | **/.terraform 3 | **/*.tfstate* 4 | crash.log 5 | 6 | # IntelliJ IDE 7 | .idea/ 8 | 9 | # Taskfile 10 | .task/ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Telia Company 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Concourse CI 2 | 3 | [![workflow](https://github.com/telia-oss/terraform-aws-concourse/workflows/workflow/badge.svg)](https://github.com/telia-oss/terraform-aws-concourse/actions) 4 | 5 | A Terraform module for deploying Concourse CI. 6 | 7 | ## Prerequisites 8 | 9 | 1. Use [Packer](https://www.packer.io/) to create an AMI with Concourse (and related tooling installed) installed: 10 | 11 | ```bash 12 | # From the project root, using task: 13 | task ami 14 | ``` 15 | 16 | 2. Generate key pairs for Concourse: 17 | 18 | ```bash 19 | # Create folder 20 | mkdir -p keys 21 | 22 | ssh-keygen -t rsa -f ./keys/tsa_host_key -N '' 23 | ssh-keygen -t rsa -f ./keys/worker_key -N '' 24 | ssh-keygen -t rsa -f ./keys/session_signing_key -N '' 25 | 26 | # Authorized workers 27 | cp ./keys/worker_key.pub ./keys/authorized_worker_keys 28 | ``` 29 | 30 | ### Required for HTTPS 31 | 32 | Route53 hosted zone, domain and ACM certificate. 33 | 34 | ### Required for Github authentication 35 | 36 | Github Oauth application, with an encrypted password: 37 | 38 | ```bash 39 | aws kms encrypt \ 40 | --key-id \ 41 | --plaintext \ 42 | --output text \ 43 | --query CiphertextBlob \ 44 | --profile default 45 | ``` 46 | 47 | Or you can add it to SSM Parameter store/Secrets Manager and [aws-env](https://github.com/telia-oss/aws-env) will populate the environment at runtime: 48 | 49 | ```hcl 50 | module "concourse_atc" { 51 | # ... other configuration 52 | 53 | github_client_id = "sm:///concourse-deployment/github-oauth-client-id" 54 | github_client_secret = "sm:///concourse-deployment/github-oauth-client-secret" 55 | } 56 | ``` 57 | 58 | By default the ATC will have permissions to read secrets from `/concourse-deployment/*` in secrets manager (in addition to `/concourse/*` for the secrets backend). 59 | 60 | ## Usage 61 | 62 | See example. If you want to learn more about how to use Concourse, 63 | check out the [official documentation](https://concourse-ci.org). 64 | 65 | ## Related projects 66 | 67 | - [concourse-images](https://github.com/telia-oss/concourse-images): A collection of docker images for use in Concourse tasks. 68 | - [concourse-tasks](https://github.com/telia-oss/concourse-tasks): A very small collection of Concourse tasks :) 69 | - [concourse-sts-lambda](https://github.com/telia-oss/concourse-sts-lambda): Lambda for managing temporary AWS credentials stored in Secrets Manager. 70 | - [concourse-github-lambda](https://github.com/telia-oss/concourse-github-lambda): Lambda for managing Github deploy keys. 71 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | env: 4 | TERM: screen-256color 5 | GO111MODULE: on 6 | AWS_DEFAULT_REGION: eu-west-1 7 | 8 | tasks: 9 | default: 10 | cmds: 11 | - task: test 12 | 13 | test: 14 | desc: Run tests. 15 | cmds: 16 | - task: test-go 17 | - task: test-terraform 18 | 19 | test-go: 20 | desc: Run tests for all Go code. 21 | silent: true 22 | cmds: 23 | - gofmt -s -l -w . 24 | - go vet -v ./... 25 | 26 | test-terraform: 27 | desc: Run tests for all terraform directories. 28 | silent: true 29 | env: 30 | DIRECTORIES: 31 | sh: find . -type f -name '*.tf' -not -path "**/.terraform/*" -print0 | xargs -0I {} dirname {} | sort -u 32 | cmds: 33 | - | 34 | BOLD=$(tput bold) 35 | NORM=$(tput sgr0) 36 | 37 | CWD=$PWD 38 | 39 | for d in $DIRECTORIES; do 40 | cd $d 41 | echo "${BOLD}$PWD:${NORM}" 42 | 43 | if ! terraform fmt -check=true -list=false -recursive=true; then 44 | echo " ✗ terraform fmt" && exit 1 45 | else 46 | echo " √ terraform fmt" 47 | fi 48 | 49 | if ! terraform init -backend=false -input=false -get=true -get-plugins=true -no-color > /dev/null; then 50 | echo " ✗ terraform init" && exit 1 51 | else 52 | echo " √ terraform init" 53 | fi 54 | 55 | if ! terraform validate > /dev/null; then 56 | echo " ✗ terraform validate" && exit 1 57 | else 58 | echo " √ terraform validate" 59 | fi 60 | 61 | cd $CWD 62 | done 63 | 64 | e2e: 65 | desc: Run the end 2 end test suite. 66 | silent: true 67 | cmds: 68 | - go test -v ./... -timeout=1h 69 | 70 | ami: 71 | desc: Build the packer template to create a Concourse AMI. 72 | silent: true 73 | cmds: 74 | - packer validate -var="template_version={{.VERSION}}" packer/template.json 75 | - packer build -var="template_version={{.VERSION}}" packer/template.json 76 | vars: 77 | VERSION: 78 | sh: git describe --tags --candidates=1 --dirty 79 | -------------------------------------------------------------------------------- /examples/basic/.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 = "3.33.0" 6 | constraints = ">= 3.0.0" 7 | hashes = [ 8 | "h1:UJcZV5+xJmHHDCsm+s8+xMonccZvVD0jdGwHAoi7nJg=", 9 | "zh:0e89b10323a59de9dd6f286423cc172cb1733683d654c886493c3bd4e43e6290", 10 | "zh:288df55f0f4fac1e920cfa61616ac42a4e4414bd7a637902db03d0c7101f14ca", 11 | "zh:303c9136c5bf97e6c1deda6e27f0d0931fe0eaaab547bf219b996623fb0ad522", 12 | "zh:457a5da9f323e2781942df534153d000ea81727798ee0771177009d84b04aad7", 13 | "zh:857fa3e29cc25ace76556a5edfded41628a3380cebf457e627576a83084852f8", 14 | "zh:85e1eb383372f834630fac7b02ec9ae1e33d24d61cf5a7d832583a16e6b5add4", 15 | "zh:9dd01eb05ac73146ac5f25421b7683fe4bffec23e408162887e1265f9bfe8462", 16 | "zh:b1561e1335754ec93a54f45c18dc1cab70f38bc08adf244d793791134f5641ef", 17 | "zh:bb96f57b80e3d94ee4bc05a5450fdd796424272b46cfc67ff9d094d5316c5fac", 18 | "zh:e4ce241d8b5dd1124dc0f1da6c0840ab777de8717dac6e76afbbad9883f5ce34", 19 | "zh:f2b292e813844d6d611db89017fc420ac05f2e3b25324e3c893481d375e23396", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/random" { 24 | version = "3.1.0" 25 | hashes = [ 26 | "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", 27 | "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", 28 | "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", 29 | "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", 30 | "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", 31 | "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", 32 | "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", 33 | "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", 34 | "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", 35 | "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", 36 | "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", 37 | "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", 38 | ] 39 | } 40 | 41 | provider "registry.terraform.io/hashicorp/template" { 42 | version = "2.2.0" 43 | hashes = [ 44 | "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", 45 | "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386", 46 | "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53", 47 | "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603", 48 | "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16", 49 | "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776", 50 | "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451", 51 | "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae", 52 | "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde", 53 | "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d", 54 | "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2", 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | ## examples/basic 2 | 3 | An example which shows _basic_ usage of the module. 4 | -------------------------------------------------------------------------------- /examples/basic/keys/authorized_worker_keys: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDoXKw99KjAA+zwa2HwpR7plIVVypvV2PQKpZI6HthVFfMNAvwseMVyeHjXlKF2qvBmQ2LGW0rX00tcxbwjcn+WVn0fajDuB2PWYERZbsreRJ8kY0+3XKnfJvDwdjgKdZRGLxzPtxqre7Ne/muRNsdZn+ZC+mffH9Ph5/Gn9YZe23MmB/MmfBbiILFyof477rfOT+0xTPbyr/t2esZtVWwu9buCtLBVOEM1jC4QkB80axSDgaJBbznvapyZPjsRINrBucIs5N/8fJmVYlkz/FlGcVdVVE0bJ/i8vUWm9xjWL3esVJuxLj00hxzxHNTGUBEUwubZwjLujpqmzUhlOBk1 dalmo@dalmobook 2 | -------------------------------------------------------------------------------- /examples/basic/keys/session_signing_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEA9JCSC5kNbYgdif7aw5l3gAaYUsXediot4pSGgRAvCK9Y0v+g 3 | ioxFlu80s29vcHhy2JP2pWySXRj3SGkjKqtMIgHzcCTH4oS98Rg6Ren/BpG+0Nt3 4 | ADvLj5D3YZcAW8dwDZHDB6KatlWT+QKN7BPoxa+qNNpZLcajB64iiUyfBTU3BrKP 5 | HTXgeRHYsHDob27dFK1Ym+iAv2gg65/H38o+o7DxuJYkKv7PYUKGNsmE3Q1zv/tv 6 | TY+YdcHU95IYHpPHggkFIaekNhamlMdz+iQ+LnFV/Y8gb2ZLmRkyxgRZiBRVvSfa 7 | XGB/utNBQEd6jj5irO/QAGyB4H05d8qesHgVfwIDAQABAoIBAQDeiz+adieV6CqZ 8 | C+Qd4WSwh0/M4mlQtYkTiXvrrcJe8NCvEBYSfRpGAl2+ekS536ECG4JxfZ6iQLmk 9 | lqd9iGq693OCNLX/FjORVC6SuB9s5urwOwTKnZ7agVva3gFhgtYgQudp5zlJeg4w 10 | pXZnwKYsXXFQrvA/i2WG85pRvftmTALgKTlOiBA1On78jHarEu2x1RZcJP45UgQk 11 | hjtT7suvNFKuYasSutI7/VXXtKi4LO4Qnn/+3HtcrxRNkZcdcjr3cfG9mGF5An8n 12 | IN5ZMjPjltbZVd0gDS13dUEZQyKvKT6EMKDyJAnfLzZmF4DyhKgxkpBB75Xz9QBB 13 | 8BsWqwvhAoGBAP1JBnC1ic1950M2TK9iXQZkhaC379RE1/MF1/XmmG7WAtV+9wH5 14 | pQbiMQC3c0G1dWnFmfVDZmZr7Nw2jWyTpXSos+QKxkoztWOtimHhpaXw45WmsGyA 15 | ZU8zP1KiJAUnBDGGDjHTB6SsxdXOkFYdBHiGJ+S5GG7136ihNT78R8yrAoGBAPcv 16 | nhnfd5sAwJ8qYqRntSoRAP9MedKWjPZJmgJRNALgyo3iaNDjHLyaIaveqKEGJ7tq 17 | MeBjotdfExvUynml/IWq3UWP7PInc69Y6M6a/ZGAdO3S+kksnr68EkMehJptImvU 18 | IZYeE59w6t18v4I4gQqypIoOIg5D373C/XEez3J9AoGBAKxKlFru4kIqNrn9ocRb 19 | wLOshUmCVV2rRspFW1Yl+eLLKTpZF2T8kElSa8r0/y3fZXMBu2ye4HUjTQevBByr 20 | go0MhPHGcoPfELAxSES03Z03c1hE/xWPcVqinZx0NtRaafvWGKnDxxs5e4mo7X3m 21 | Vzg06cYoMnqoPe/TWQjzS1PpAoGBAOqxsopcTALG/xzgsJHqye8r/+dmpFBXk3WQ 22 | woKquRh2eLuomd6jlLhaG9hE65Uf8/+VP9AOqiGVat5E9w3zlMURS1Bt7AqrfMKj 23 | R0BLlrBc6Cia5fsuO3dbuHcz9bFuJ318B3uyCO2c19L+TBMHNTaEEYfnDVzAAyL4 24 | jYynNU49AoGBAK26o6B5QDln+dVbJDvnvWNEbfiPzrnZOOEbQLu/Ly6Zw4aLYnqi 25 | kfrAOIT65dP5AJ74Wj9Rc/La5pbRHMFJhNVTCQjkFxtzlzz+ee8Y3KGFWsyeAqGz 26 | ajJkPmP0Z/wWLcn7nj3AdqQwbV4Q/g95F71bO8Sdf3mRQKKe6Up7VjYs 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/basic/keys/session_signing_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0kJILmQ1tiB2J/trDmXeABphSxd52Ki3ilIaBEC8Ir1jS/6CKjEWW7zSzb29weHLYk/albJJdGPdIaSMqq0wiAfNwJMfihL3xGDpF6f8Gkb7Q23cAO8uPkPdhlwBbx3ANkcMHopq2VZP5Ao3sE+jFr6o02lktxqMHriKJTJ8FNTcGso8dNeB5EdiwcOhvbt0UrVib6IC/aCDrn8ffyj6jsPG4liQq/s9hQoY2yYTdDXO/+29Nj5h1wdT3khgek8eCCQUhp6Q2FqaUx3P6JD4ucVX9jyBvZkuZGTLGBFmIFFW9J9pcYH+600FAR3qOPmKs79AAbIHgfTl3yp6weBV/ dalmo@dalmobook 2 | -------------------------------------------------------------------------------- /examples/basic/keys/tsa_host_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA0gPOnQ6c65NveGUXvSYL7CoYqfM6d+mTDYyTs2GyA2S65Xan 3 | ZfOxn8mcOaBECtBy7tGvdaWYpIn8CG8HgAnBXK01v4OznyJhxOK5+zSlGHKbGo1U 4 | L1yQl6CKt5fxZ/IMkKqn9zTkherfJiNQ1qUoArVS4P3CgmJbChrWZq7XzeXKWvjq 5 | g1x65ne9xoCZA9D0+C68ZDHluprAWZWGCwImX9XzQ40FSYEPrWDawvNSFGOCdhUp 6 | 7npEJWfbxgEg8G6q1OgHszWeSydbFvYP/YDein2Nejam+N2HSYQG6DTPE3vxRClI 7 | 8EhGYWFZxCsVIBfXGd9zsWefiO52J0oOOaBcqQIDAQABAoIBAQC3SgVPw0omu5Uk 8 | wS33sbXkFlVSNepIbz0dLRxXCSOgnJAU5fpxGz9hkkZbcMkRmx1D2xNEHRNcPuUM 9 | w0ILd/gfFzh6fcoNT6d+etYQLMEN1jAR+1iGwUWcX8vRMbXJn1FiDN7s/GeZjQPW 10 | OCo8OwXxXykHHdFdk9OavRNzqJLFqXuWs54GI1Ve83WJXAlvhJB6pOF5AEFAYtd2 11 | CNwU3Nd2q0xPsqtcOO8MnB8jz1b1Sc6uIZf+6Qe7ZLMWyn3kJLbXLi9ylcih2R0b 12 | AN2Ad51FMTfyHtFhfBmdrSrhv8PEQr0gXFdXI5Vytjbuzvbb+qiYqH1w70s+FdO6 13 | rIYEHwxFAoGBAO3DmIWo+g8A8XYl3Xz5SBm+/HgPEoYz0PCT1p3sn0whw/I+aLFk 14 | muIvBHSjn5g82qTDV1VyQEALndi7cFN4mDQZkRDAJ+7mT5Rqx7P5h5ESCEhShI7/ 15 | Ct8bCVcNqhoq3osWmjPA4Tqbwl4gy6/Sku+2xdCjbe+JvJgsDKFaUiGHAoGBAOIf 16 | XflK7017hqBw16kMJnz1Yeyd+u0IolhVOB+IyMObM/dUgG3728QQjUjGbo3/wmN7 17 | wElhgZgN0/RxkXLToN3c2qSlTdOGHecII+JHIdBbJOcdBaUXOfmVTB+D6kMG/nje 18 | SDy/rKsTioDj75ji3S0627NwPef9yydb9uXMyNxPAoGBANNF2fLzGvhYEze0H+Mc 19 | M5hFIFyisAg2ZPRRugRsrYReAJryH995kTNpKiFm/7qsYHDF74XK1xLR/7oFnKNf 20 | ZoftK/1hclqTpqUwWTIwiek7x8ZUJNxX/tYPVTZYuw0ziLq5I97XBowougcgRz59 21 | 8/k0RQTHJDoU/1OM2NBLzO1jAoGBAJC8nS09vtEsuS6nLBOjmFsxwf1v3bataST7 22 | X1In/sHd7TRqHU7JAJGOOrETep5f0DBXfOw7gnUunLYBn6UNOxHoFPeAa8FyPl+s 23 | QTPMbiNsw8E/PWa/6DcOTkx55pACwQ3i0gA4fDCA8I2x2KJWzFXwL8K6nJWLSOcn 24 | oXDUVNlXAoGAemUbsHmu9oKMjRRaxOU5rzZwbBQ1aqfi+6+KGXYNU1sfqpnt19jg 25 | pWo6K7SEKbE9wFcFASdltr0dWimUkSKRX+5lRlqie347zSsbFrPd8UR6fMwkr/5i 26 | T591OpfLIsmBk5DwEaJyHj2IP/vVTmYUymrRBrGWYi6vin/5qD/vB2s= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/basic/keys/tsa_host_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSA86dDpzrk294ZRe9JgvsKhip8zp36ZMNjJOzYbIDZLrldqdl87GfyZw5oEQK0HLu0a91pZikifwIbweACcFcrTW/g7OfImHE4rn7NKUYcpsajVQvXJCXoIq3l/Fn8gyQqqf3NOSF6t8mI1DWpSgCtVLg/cKCYlsKGtZmrtfN5cpa+OqDXHrmd73GgJkD0PT4LrxkMeW6msBZlYYLAiZf1fNDjQVJgQ+tYNrC81IUY4J2FSnuekQlZ9vGASDwbqrU6AezNZ5LJ1sW9g/9gN6KfY16Nqb43YdJhAboNM8Te/FEKUjwSEZhYVnEKxUgF9cZ33OxZ5+I7nYnSg45oFyp dalmo@dalmobook 2 | -------------------------------------------------------------------------------- /examples/basic/keys/worker_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA6FysPfSowAPs8Gth8KUe6ZSFVcqb1dj0CqWSOh7YVRXzDQL8 3 | LHjFcnh415ShdqrwZkNixltK19NLXMW8I3J/llZ9H2ow7gdj1mBEWW7K3kSfJGNP 4 | t1yp3ybw8HY4CnWURi8cz7caq3uzXv5rkTbHWZ/mQvpn3x/T4efxp/WGXttzJgfz 5 | JnwW4iCxcqH+O+63zk/tMUz28q/7dnrGbVVsLvW7grSwVThDNYwuEJAfNGsUg4Gi 6 | QW8572qcmT47ESDawbnCLOTf/HyZlWJZM/xZRnFXVVRNGyf4vL1FpvcY1i93rFSb 7 | sS49NIcc8RzUxlARFMLm2cIy7o6aps1IZTgZNQIDAQABAoIBAClocm1sDzKAwJWr 8 | nT2EP3kCtawvOgwm6H6JOQDQhF7NVY3pDUVjlFQs8eQBIbEDD2o58f1FQZYqmlCD 9 | EF8ExYXmDdAuXV/dw0Xty+BgJRjtA6s5Y3hatA8HYoKHnr8GaxECzlCZ4c/TcIiq 10 | MEMljusC2sbu1tnlUx379o2m7HY0sGRowYTKViPGrk9ctOLHiCyMCkprO9e1hdXs 11 | XEoe3p4vzyB8YiiogfaDcPIG0GwXZeX0VdgTzsHD+pCxBNv5a13MRppbmYUMsFbY 12 | 58sLuX3znJjY8iPpSM68b51xNyVBGQq0qFsvnTCWvM0O7EN1IRFvWINAmaWo2trv 13 | bV+T9uECgYEA+FPhYgpYkp0mDDTWGFX9fhuxt+mXcRuLdkRwMxx2WgE77JoVK+1z 14 | DptW9Ns23zx429B37NP46kIC2IoNXJTvbU8K5sC0DgYTDp4JxEMHejIIU71mOjh9 15 | qIF4kWqj78lXBP6z9W+N7JXRTObaGx8CYjDkGYTYHxG9eQqIrdQM43kCgYEA74qD 16 | jT11t48diittazP9N2qV0QJo0fJhfSnAWcnBRBaoLY0puoYuJLAvOrXpsNXG/v/A 17 | b4REKaJXeOdqcKVO7IiWsV3lBkri3Bni1k6Kz+scevrdEHo1bADY2fvoF3tyZ8Q7 18 | utelLEAxsbUsqr1jz36Gy7JPyYFbY65S2EVXWJ0CgYEArFBoQMO1Gmd5k1bGiTSC 19 | JhPJijjJIW9for1yrcS+S0436sIwlr657BTWjinfNaCcrMtHrEqamtMbbm69PtUK 20 | HuuOQPjO+Dw21RnM5Sct9RjqtlDistuoNllA9IbvIuCvRYQIE/NIpDaBeb9m8RLf 21 | cItEfIC2BzkkJO2uUhCmsOECgYAWv8DemQek9yKHDLjHhZh2utifkOsDhVFc4aoy 22 | 3MZOARjXuqBL5pJbSaAyozQeZy8O6JsN/DG9An3sLY4eRJTKQe6Nya0Ge/YvkQXJ 23 | 9IuaU2nRBCIafoC0USBHE3VnRYIH+MrzY6d8HFyb0+j6DiJ8gjerALQzHuXfAZ88 24 | itlQwQKBgAoSTkkkX9MZ8Un/MwYsNY48Y3QDtdzVYNBMeQn3NJouMQw8JJ7+Rhdy 25 | uLTrCKzDPikwbS2ccojiWqWB//v7B2I68xtZiu8BcGXPNOnMl2aRiqHmN2Yp2sms 26 | r2UcYo79w4Fh48NzHoqAd5YY+/nJ4AROCEcRDzUfVKuUS3y9sIdj 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/basic/keys/worker_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDoXKw99KjAA+zwa2HwpR7plIVVypvV2PQKpZI6HthVFfMNAvwseMVyeHjXlKF2qvBmQ2LGW0rX00tcxbwjcn+WVn0fajDuB2PWYERZbsreRJ8kY0+3XKnfJvDwdjgKdZRGLxzPtxqre7Ne/muRNsdZn+ZC+mffH9Ph5/Gn9YZe23MmB/MmfBbiILFyof477rfOT+0xTPbyr/t2esZtVWwu9buCtLBVOEM1jC4QkB80axSDgaJBbznvapyZPjsRINrBucIs5N/8fJmVYlkz/FlGcVdVVE0bJ/i8vUWm9xjWL3esVJuxLj00hxzxHNTGUBEUwubZwjLujpqmzUhlOBk1 dalmo@dalmobook 2 | -------------------------------------------------------------------------------- /examples/basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.14" 3 | } 4 | 5 | provider "aws" { 6 | region = var.region 7 | } 8 | 9 | data "aws_vpc" "main" { 10 | default = true 11 | } 12 | 13 | data "aws_subnet_ids" "main" { 14 | vpc_id = data.aws_vpc.main.id 15 | } 16 | 17 | module "postgres" { 18 | source = "telia-oss/rds-cluster/aws" 19 | version = "4.0.0" 20 | 21 | name_prefix = var.name_prefix 22 | username = "superuser" 23 | password = var.postgres_password 24 | engine = "aurora-postgresql" 25 | port = 5439 26 | vpc_id = data.aws_vpc.main.id 27 | subnet_ids = data.aws_subnet_ids.main.ids 28 | 29 | tags = { 30 | environment = "dev" 31 | terraform = "True" 32 | } 33 | } 34 | 35 | module "concourse_atc" { 36 | source = "../../modules/atc" 37 | 38 | name_prefix = var.name_prefix 39 | web_protocol = "HTTP" 40 | web_port = "80" 41 | authorized_cidr = ["0.0.0.0/0"] 42 | concourse_keys = "${path.root}/keys" 43 | vpc_id = data.aws_vpc.main.id 44 | public_subnet_ids = data.aws_subnet_ids.main.ids 45 | private_subnet_ids = data.aws_subnet_ids.main.ids 46 | postgres_host = module.postgres.endpoint 47 | postgres_port = module.postgres.port 48 | postgres_username = module.postgres.username 49 | postgres_password = var.postgres_password 50 | postgres_database = module.postgres.database_name 51 | encryption_key = "" 52 | instance_ami = var.packer_ami 53 | github_client_id = "sm:///concourse-deployment/github-oauth-client-id" 54 | github_client_secret = "sm:///concourse-deployment/github-oauth-client-secret" 55 | github_users = ["itsdalmo"] 56 | github_teams = ["telia-oss:concourse-owners"] 57 | local_user = "admin:${var.concourse_admin_password}" 58 | local_admin_user = "admin" 59 | 60 | tags = { 61 | environment = "dev" 62 | terraform = "True" 63 | } 64 | } 65 | 66 | module "concourse_worker" { 67 | source = "../../modules/worker" 68 | 69 | name_prefix = var.name_prefix 70 | concourse_keys = "${path.root}/keys" 71 | vpc_id = data.aws_vpc.main.id 72 | private_subnet_ids = data.aws_subnet_ids.main.ids 73 | atc_sg = module.concourse_atc.security_group_id 74 | tsa_host = module.concourse_atc.tsa_host 75 | tsa_port = module.concourse_atc.tsa_port 76 | instance_ami = var.packer_ami 77 | 78 | tags = { 79 | environment = "dev" 80 | terraform = "True" 81 | } 82 | } 83 | 84 | # ATC ingress postgres 85 | resource "aws_security_group_rule" "atc_ingress_postgres" { 86 | security_group_id = module.postgres.security_group_id 87 | type = "ingress" 88 | protocol = "tcp" 89 | from_port = module.postgres.port 90 | to_port = module.postgres.port 91 | source_security_group_id = module.concourse_atc.security_group_id 92 | } 93 | 94 | module "concourse_dashboard" { 95 | source = "../../modules/dashboard" 96 | 97 | name_prefix = var.name_prefix 98 | atc_asg_name = module.concourse_atc.asg_id 99 | atc_log_group_name = module.concourse_atc.log_group_name 100 | worker_asg_name = module.concourse_worker.asg_id 101 | worker_log_group_name = module.concourse_worker.log_group_name 102 | internal_lb_arn = module.concourse_atc.internal_lb_arn 103 | external_lb_arn = module.concourse_atc.external_lb_arn 104 | rds_cluster_id = module.postgres.id 105 | } 106 | -------------------------------------------------------------------------------- /examples/basic/outputs.tf: -------------------------------------------------------------------------------- 1 | output "endpoint" { 2 | description = "The Concourse web interface." 3 | value = module.concourse_atc.endpoint 4 | } 5 | 6 | output "atc_asg_id" { 7 | description = "ID/name of the ATC autoscaling group." 8 | value = module.concourse_atc.asg_id 9 | } 10 | 11 | output "worker_asg_id" { 12 | description = "ID/name of the worker autoscaling group." 13 | value = module.concourse_worker.asg_id 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name_prefix" { 2 | type = string 3 | default = "concourse-basic-example" 4 | } 5 | 6 | variable "packer_ami" { 7 | type = string 8 | default = "ami-063d4ab14480ac177" 9 | } 10 | 11 | variable "concourse_admin_password" { 12 | type = string 13 | default = "dolphins" 14 | } 15 | 16 | variable "postgres_password" { 17 | type = string 18 | default = "dolphins" 19 | } 20 | 21 | variable "region" { 22 | type = string 23 | default = "eu-west-1" 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = ">= 3.0" 6 | } 7 | } 8 | required_version = ">= 0.14" 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/telia-oss/terraform-aws-concourse/v3 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.23.3 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/go-sql-driver/mysql v1.4.1 // indirect 9 | github.com/google/uuid v1.1.1 // indirect 10 | github.com/gruntwork-io/terratest v0.18.3 11 | github.com/kr/pretty v0.1.0 // indirect 12 | github.com/pquerna/otp v1.2.0 // indirect 13 | github.com/stretchr/testify v1.4.0 14 | github.com/telia-oss/terraform-aws-asg/v3 v3.2.0 15 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect 16 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect 17 | google.golang.org/appengine v1.6.1 // indirect 18 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.21.2 h1:CqbWrQzi7s8J2F0TRRdLvTr0+bt5Zxo2IDoFNGsAiUg= 2 | github.com/aws/aws-sdk-go v1.21.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 3 | github.com/aws/aws-sdk-go v1.23.3 h1:Ty/4P6tOFJkDnKDrFJWnveznvESblf8QOheD1CwQPDU= 4 | github.com/aws/aws-sdk-go v1.23.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 5 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= 6 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 12 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 13 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 15 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/gruntwork-io/terratest v0.17.6 h1:efnWnoz3GvM6VGHvRebGaO41ne4ZTKMvMqMf8V5vY58= 17 | github.com/gruntwork-io/terratest v0.17.6/go.mod h1:NjUn6YXA5Skxt8Rs20t3isYx5Rl+EgvGB8/+RRXddqk= 18 | github.com/gruntwork-io/terratest v0.18.3 h1:07C7q8dElpSmob6uo/5mZm1c++zsAQsVlDv3G98CeLs= 19 | github.com/gruntwork-io/terratest v0.18.3/go.mod h1:NjUn6YXA5Skxt8Rs20t3isYx5Rl+EgvGB8/+RRXddqk= 20 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 21 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 22 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 25 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 26 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 27 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 28 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= 32 | github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 33 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 36 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 37 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 38 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 39 | github.com/telia-oss/terraform-aws-asg/v3 v3.2.0 h1:68hi77fl4mNkCyjX//GEQF9CxrCDxkWNuNG2mHGWoMk= 40 | github.com/telia-oss/terraform-aws-asg/v3 v3.2.0/go.mod h1:Cy6Ie2Y3PN0wijAjxRrH9othe6VqpPMgqCVqeiWOOk4= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 43 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 44 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 47 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 48 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= 49 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 51 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs= 57 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 59 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 63 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 64 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 65 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 66 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 67 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 70 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 72 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 73 | -------------------------------------------------------------------------------- /modules/atc/README.md: -------------------------------------------------------------------------------- 1 | ## Concourse ATC 2 | 3 | A Terraform module for deploying a Concourse ATC. 4 | -------------------------------------------------------------------------------- /modules/atc/main.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Resources 3 | # ------------------------------------------------------------------------------- 4 | data "aws_region" "current" {} 5 | 6 | data "aws_caller_identity" "current" {} 7 | 8 | data "aws_vpc" "concourse" { 9 | id = var.vpc_id 10 | } 11 | 12 | resource "aws_security_group_rule" "lb_ingress_atc" { 13 | security_group_id = module.atc.security_group_id 14 | type = "ingress" 15 | protocol = "tcp" 16 | from_port = var.atc_port 17 | to_port = var.atc_port 18 | source_security_group_id = module.external_lb.security_group_id 19 | } 20 | 21 | resource "aws_security_group_rule" "workers_ingress_atc" { 22 | security_group_id = module.atc.security_group_id 23 | type = "ingress" 24 | protocol = "tcp" 25 | from_port = var.atc_port 26 | to_port = var.atc_port 27 | cidr_blocks = [data.aws_vpc.concourse.cidr_block] 28 | } 29 | 30 | resource "aws_security_group_rule" "workers_ingress_tsa" { 31 | security_group_id = module.atc.security_group_id 32 | type = "ingress" 33 | protocol = "tcp" 34 | from_port = var.tsa_port 35 | to_port = var.tsa_port 36 | cidr_blocks = [data.aws_vpc.concourse.cidr_block] 37 | } 38 | 39 | resource "aws_security_group_rule" "tsa_ingress_peers" { 40 | security_group_id = module.atc.security_group_id 41 | type = "ingress" 42 | protocol = "-1" 43 | from_port = 0 44 | to_port = 0 45 | self = true 46 | } 47 | 48 | resource "aws_autoscaling_attachment" "external_lb" { 49 | autoscaling_group_name = module.atc.id 50 | alb_target_group_arn = aws_lb_target_group.external.arn 51 | } 52 | 53 | resource "aws_autoscaling_attachment" "internal_lb" { 54 | autoscaling_group_name = module.atc.id 55 | alb_target_group_arn = aws_lb_target_group.internal.arn 56 | } 57 | 58 | module "atc" { 59 | source = "telia-oss/asg/aws" 60 | version = "4.0.0" 61 | 62 | name_prefix = "${var.name_prefix}-atc" 63 | user_data_base64 = data.template_cloudinit_config.atc.rendered 64 | vpc_id = var.vpc_id 65 | subnet_ids = var.private_subnet_ids 66 | min_size = var.min_size 67 | max_size = var.max_size 68 | instance_type = var.instance_type 69 | instance_ami = var.instance_ami 70 | instance_key = var.instance_key 71 | instance_policy = data.aws_iam_policy_document.atc.json 72 | instance_volume_size = 8 73 | await_signal = true 74 | pause_time = "PT5M" 75 | health_check_type = "ELB" 76 | tags = var.tags 77 | } 78 | 79 | locals { 80 | shared_cloud_init = templatefile("${path.module}/../cloud-init/shared.yml", { 81 | region = data.aws_region.current.name 82 | cloudwatch_namespace = var.name_prefix 83 | log_group_name = aws_cloudwatch_log_group.atc.name 84 | prometheus_enabled = var.prometheus_enabled 85 | }) 86 | 87 | atc_cloud_init = templatefile("${path.module}/../cloud-init/atc.yml", { 88 | region = data.aws_region.current.name 89 | stack_name = "${var.name_prefix}-atc-asg" 90 | target_group = aws_lb_target_group.internal.arn 91 | atc_port = var.atc_port 92 | tsa_port = var.tsa_port 93 | local_user = var.local_user 94 | local_admin_user = var.local_admin_user 95 | github_client_id = var.github_client_id 96 | github_client_secret = var.github_client_secret 97 | github_users = var.github_users 98 | github_teams = var.github_teams 99 | prometheus_enabled = var.prometheus_enabled 100 | prometheus_bind_port = var.prometheus_port 101 | placement_strategy = var.placement_strategy 102 | secret_cache_enabled = var.secret_cache_enabled 103 | concourse_web_host = "${lower(var.web_protocol)}://${var.domain != "" ? var.domain : module.external_lb.dns_name}:${var.web_port}" 104 | postgres_host = var.postgres_host 105 | postgres_port = var.postgres_port 106 | postgres_username = var.postgres_username 107 | postgres_password = var.postgres_password 108 | postgres_database = var.postgres_database 109 | log_level = var.log_level 110 | tsa_host_key = file("${var.concourse_keys}/tsa_host_key") 111 | session_signing_key = file("${var.concourse_keys}/session_signing_key") 112 | authorized_worker_keys = file("${var.concourse_keys}/authorized_worker_keys") 113 | encryption_key = var.encryption_key 114 | old_encryption_key = var.old_encryption_key 115 | }) 116 | } 117 | 118 | data "template_cloudinit_config" "atc" { 119 | gzip = true 120 | base64_encode = true 121 | 122 | part { 123 | content_type = "text/cloud-config" 124 | content = local.shared_cloud_init 125 | } 126 | 127 | part { 128 | content_type = "text/cloud-config" 129 | merge_type = "list(append)+dict(recurse_array)+str()" 130 | content = local.atc_cloud_init 131 | } 132 | } 133 | 134 | resource "aws_cloudwatch_log_group" "atc" { 135 | name = "${var.name_prefix}-atc" 136 | } 137 | 138 | data "aws_iam_policy_document" "atc" { 139 | statement { 140 | effect = "Allow" 141 | 142 | resources = ["*"] 143 | 144 | actions = [ 145 | "cloudwatch:PutMetricData", 146 | "cloudwatch:GetMetricStatistics", 147 | "cloudwatch:ListMetrics", 148 | "logs:DescribeLogStreams", 149 | "logs:DescribeLogGroups", 150 | "ec2:DescribeTags", 151 | ] 152 | } 153 | 154 | statement { 155 | effect = "Allow" 156 | 157 | resources = [ 158 | aws_cloudwatch_log_group.atc.arn, 159 | ] 160 | 161 | actions = [ 162 | "logs:CreateLogStream", 163 | "logs:CreateLogGroup", 164 | "logs:PutLogEvents", 165 | ] 166 | } 167 | 168 | # Used for cfn-signal 169 | statement { 170 | effect = "Allow" 171 | 172 | resources = ["*"] 173 | 174 | actions = [ 175 | "elasticloadbalancing:DescribeTargetHealth", 176 | ] 177 | } 178 | 179 | statement { 180 | effect = "Allow" 181 | 182 | actions = [ 183 | "secretsmanager:ListSecrets", 184 | ] 185 | 186 | resources = ["*"] 187 | } 188 | 189 | statement { 190 | effect = "Allow" 191 | 192 | actions = [ 193 | "secretsmanager:GetSecretValue", 194 | "secretsmanager:DescribeSecret", 195 | ] 196 | 197 | resources = [ 198 | "arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:/concourse/*", 199 | "arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:/concourse-deployment/*", 200 | ] 201 | } 202 | } 203 | 204 | resource "aws_security_group_rule" "ingress" { 205 | security_group_id = module.external_lb.security_group_id 206 | type = "ingress" 207 | protocol = "tcp" 208 | from_port = var.web_port 209 | to_port = var.web_port 210 | cidr_blocks = var.authorized_cidr 211 | } 212 | 213 | resource "aws_route53_record" "main" { 214 | count = var.domain == "" ? 0 : 1 215 | zone_id = var.zone_id 216 | name = var.domain 217 | type = "A" 218 | 219 | alias { 220 | name = module.external_lb.dns_name 221 | zone_id = module.external_lb.zone_id 222 | evaluate_target_health = false 223 | } 224 | } 225 | 226 | module "external_lb" { 227 | source = "telia-oss/loadbalancer/aws" 228 | version = "4.0.0" 229 | 230 | name_prefix = var.name_prefix 231 | vpc_id = var.vpc_id 232 | subnet_ids = var.public_subnet_ids 233 | type = "application" 234 | internal = false 235 | tags = var.tags 236 | } 237 | 238 | resource "aws_lb_listener" "external" { 239 | load_balancer_arn = module.external_lb.arn 240 | port = var.web_port 241 | protocol = upper(var.web_protocol) 242 | certificate_arn = var.web_certificate_arn 243 | ssl_policy = var.web_certificate_arn == "" ? "" : "ELBSecurityPolicy-TLS-1-2-2017-01" 244 | 245 | default_action { 246 | target_group_arn = aws_lb_target_group.external.arn 247 | type = "forward" 248 | } 249 | } 250 | 251 | resource "aws_lb_target_group" "external" { 252 | vpc_id = var.vpc_id 253 | port = var.atc_port 254 | protocol = "HTTP" 255 | 256 | health_check { 257 | protocol = "HTTP" 258 | port = "traffic-port" 259 | path = "/" 260 | interval = 30 261 | timeout = 5 262 | healthy_threshold = 2 263 | unhealthy_threshold = 2 264 | matcher = 200 265 | } 266 | 267 | # NOTE: TF is unable to destroy a target group while a listener is attached, 268 | # therefor we have to create a new one before destroying the old. This also means 269 | # we have to let it have a random name, and then tag it with the desired name. 270 | lifecycle { 271 | create_before_destroy = true 272 | } 273 | 274 | tags = merge( 275 | var.tags, 276 | { 277 | "Name" = "${var.name_prefix}-target-${var.atc_port}" 278 | }, 279 | ) 280 | } 281 | 282 | module "internal_lb" { 283 | source = "telia-oss/loadbalancer/aws" 284 | version = "4.0.0" 285 | 286 | name_prefix = var.name_prefix 287 | vpc_id = var.vpc_id 288 | subnet_ids = var.private_subnet_ids 289 | type = "network" 290 | internal = true 291 | tags = var.tags 292 | } 293 | 294 | resource "aws_lb_listener" "internal" { 295 | load_balancer_arn = module.internal_lb.arn 296 | port = var.tsa_port 297 | protocol = "TCP" 298 | 299 | default_action { 300 | target_group_arn = aws_lb_target_group.internal.arn 301 | type = "forward" 302 | } 303 | } 304 | 305 | resource "aws_lb_target_group" "internal" { 306 | vpc_id = var.vpc_id 307 | port = var.tsa_port 308 | protocol = "TCP" 309 | 310 | # Since the TSA attempts to handshake TCP health checks which generates INFO log entries 311 | # with "error: EOF" we are instead health checking the ATC which is part of the same binary. 312 | health_check { 313 | protocol = "TCP" 314 | port = var.atc_port 315 | interval = 30 316 | healthy_threshold = 2 317 | unhealthy_threshold = 2 318 | } 319 | 320 | # NOTE: TF is unable to destroy a target group while a listener is attached, 321 | # therefor we have to create a new one before destroying the old. This also means 322 | # we have to let it have a random name, and then tag it with the desired name. 323 | lifecycle { 324 | create_before_destroy = true 325 | } 326 | 327 | tags = merge( 328 | var.tags, 329 | { 330 | "Name" = "${var.name_prefix}-target-${var.tsa_port}" 331 | }, 332 | ) 333 | } 334 | 335 | -------------------------------------------------------------------------------- /modules/atc/outputs.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Output 3 | # ------------------------------------------------------------------------------- 4 | output "asg_id" { 5 | value = module.atc.id 6 | } 7 | 8 | output "role_arn" { 9 | value = module.atc.role_arn 10 | } 11 | 12 | output "role_name" { 13 | value = module.atc.role_name 14 | } 15 | 16 | output "security_group_id" { 17 | value = module.atc.security_group_id 18 | } 19 | 20 | output "log_group_name" { 21 | value = aws_cloudwatch_log_group.atc.name 22 | } 23 | 24 | output "external_lb_arn" { 25 | value = module.external_lb.arn 26 | } 27 | 28 | output "external_lb_sg" { 29 | value = module.external_lb.security_group_id 30 | } 31 | 32 | output "internal_lb_arn" { 33 | value = module.internal_lb.arn 34 | } 35 | 36 | output "internal_lb_sg" { 37 | value = module.internal_lb.security_group_id 38 | } 39 | 40 | output "tsa_host" { 41 | value = module.internal_lb.dns_name 42 | } 43 | 44 | output "tsa_port" { 45 | value = var.tsa_port 46 | } 47 | 48 | output "endpoint" { 49 | value = "${lower(var.web_protocol)}://${var.domain == "" ? module.external_lb.dns_name : var.domain}:${var.web_port}" 50 | } 51 | 52 | -------------------------------------------------------------------------------- /modules/atc/variables.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Variables 3 | # ------------------------------------------------------------------------------ 4 | variable "name_prefix" { 5 | description = "A prefix used for naming resources." 6 | type = string 7 | } 8 | 9 | variable "vpc_id" { 10 | description = "The VPC ID." 11 | type = string 12 | } 13 | 14 | variable "public_subnet_ids" { 15 | description = "ID of subnets where public resources (public LB) can be provisioned." 16 | type = list(string) 17 | } 18 | 19 | variable "private_subnet_ids" { 20 | description = "ID of subnets where private resources (ATC and private LB) can be provisioned." 21 | type = list(string) 22 | } 23 | 24 | variable "authorized_cidr" { 25 | description = "List of authorized CIDR blocks which can reach the Concourse web interface." 26 | type = list(string) 27 | } 28 | 29 | variable "min_size" { 30 | description = "The minimum (and desired) size of the auto scale group." 31 | type = number 32 | default = 1 33 | } 34 | 35 | variable "max_size" { 36 | description = "The maximum size of the auto scale group." 37 | type = number 38 | default = 2 39 | } 40 | 41 | variable "instance_type" { 42 | description = "Type of instance to provision for the Concourse ATC." 43 | type = string 44 | default = "t3.small" 45 | } 46 | 47 | variable "instance_ami" { 48 | description = "The EC2 image ID to launch. See the include packer image." 49 | type = string 50 | } 51 | 52 | variable "instance_key" { 53 | description = "The key name that should be used for the ATC instances." 54 | type = string 55 | default = "" 56 | } 57 | 58 | variable "concourse_keys" { 59 | description = "Path to a directory containing the Concourse SSH keys. (See README.md)." 60 | type = string 61 | } 62 | 63 | variable "postgres_host" { 64 | description = "The DNS address of the postgres DB." 65 | type = string 66 | } 67 | 68 | variable "postgres_port" { 69 | description = "The port on which the DB accepts connections." 70 | type = number 71 | } 72 | 73 | variable "postgres_username" { 74 | description = "The master username for the database." 75 | type = string 76 | } 77 | 78 | variable "postgres_password" { 79 | description = "Password for the master DB user." 80 | type = string 81 | } 82 | 83 | variable "postgres_database" { 84 | description = "Name for the automatically created database." 85 | type = string 86 | } 87 | 88 | variable "github_client_id" { 89 | description = "Application client ID for enabling GitLab OAuth." 90 | type = string 91 | default = "" 92 | } 93 | 94 | variable "github_client_secret" { 95 | description = "Application client secret for enabling GitLab OAuth." 96 | type = string 97 | default = "" 98 | } 99 | 100 | variable "github_users" { 101 | description = "GitHub user to permit admin access." 102 | type = list(string) 103 | default = [] 104 | } 105 | 106 | variable "github_teams" { 107 | description = "GitHub team whose members will have admin access (:)." 108 | type = list(string) 109 | default = [] 110 | } 111 | 112 | variable "local_user" { 113 | description = "Create a local user (format: username:password)." 114 | type = string 115 | default = "" 116 | } 117 | 118 | variable "local_admin_user" { 119 | description = "Add the local user to the main team to grant admin privileges (format: username)." 120 | type = string 121 | default = "" 122 | } 123 | 124 | variable "domain" { 125 | description = "The (domain) name of the record." 126 | type = string 127 | default = "" 128 | } 129 | 130 | variable "zone_id" { 131 | description = "The ID of the hosted zone to contain this record." 132 | type = string 133 | default = "" 134 | } 135 | 136 | variable "web_protocol" { 137 | description = "The protocol for connections from clients to the external load balancer (Concourse web interface)." 138 | type = string 139 | default = "HTTP" 140 | } 141 | 142 | variable "web_port" { 143 | description = "The port on which the external load balancer is listening (Concourse web interface)" 144 | type = number 145 | default = 80 146 | } 147 | 148 | variable "web_certificate_arn" { 149 | description = "The ARN of the default SSL server certificate. Exactly one certificate is required if the protocol (Concourse web interface) is HTTPS." 150 | type = string 151 | default = "" 152 | } 153 | 154 | variable "atc_port" { 155 | description = "Port specification for the Concourse ATC." 156 | type = number 157 | default = 8080 158 | } 159 | 160 | variable "tsa_port" { 161 | description = "Port specification for the Concourse TSA." 162 | type = number 163 | default = 2222 164 | } 165 | 166 | variable "prometheus_enabled" { 167 | description = "Enable exporting of prometheus metrics." 168 | type = bool 169 | default = false 170 | } 171 | 172 | variable "prometheus_port" { 173 | description = "Port where prometheus metrics can be scraped." 174 | type = number 175 | default = 9391 176 | } 177 | 178 | variable "placement_strategy" { 179 | description = "Concourse container placement strategy." 180 | type = string 181 | default = "volume-locality" 182 | } 183 | 184 | variable "secret_cache_enabled" { 185 | description = "Enable Concourse secret cache." 186 | type = bool 187 | default = true 188 | } 189 | 190 | variable "encryption_key" { 191 | description = "A 16 or 32 length key used to encrypt sensitive information before storing it in the database." 192 | type = string 193 | } 194 | 195 | variable "old_encryption_key" { 196 | description = "Encryption key previously used for encrypting sensitive information. If provided without a new key, data is encrypted. If provided with a new key, data is re-encrypted." 197 | type = string 198 | default = "" 199 | } 200 | 201 | variable "log_level" { 202 | description = "Minimum level of logs to see (options: debug|info|error|fatal)." 203 | type = string 204 | default = "info" 205 | } 206 | 207 | variable "tags" { 208 | description = "A map of tags (key-value pairs) passed to resources." 209 | type = map(string) 210 | default = {} 211 | } 212 | 213 | -------------------------------------------------------------------------------- /modules/atc/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.14" 4 | } 5 | -------------------------------------------------------------------------------- /modules/cloud-init/atc.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: "/concourse/keys/web/tsa_host_key" 4 | permissions: "0644" 5 | owner: "root" 6 | encoding: base64 7 | content: ${base64encode(tsa_host_key)} 8 | - path: "/concourse/keys/web/session_signing_key" 9 | permissions: "0644" 10 | owner: "root" 11 | encoding: base64 12 | content: ${base64encode(session_signing_key)} 13 | - path: "/concourse/keys/web/authorized_worker_keys" 14 | permissions: "0644" 15 | owner: "root" 16 | encoding: base64 17 | content: ${base64encode(authorized_worker_keys)} 18 | - path: "/etc/systemd/system/concourse.service" 19 | permissions: "0644" 20 | owner: "root" 21 | content: | 22 | [Unit] 23 | Description=Service for Concourse ATC/TSA 24 | Requires=network-online.target 25 | After=network-online.target 26 | 27 | [Service] 28 | Type=simple 29 | Restart=always 30 | RestartSec=30s 31 | TimeoutStartSec=5m 32 | TimeoutStopSec=1h 33 | 34 | Environment="CONCOURSE_GITHUB_CLIENT_ID=${github_client_id}" 35 | Environment="CONCOURSE_GITHUB_CLIENT_SECRET=${github_client_secret}" 36 | Environment="CONCOURSE_POSTGRES_HOST=${postgres_host}" 37 | Environment="CONCOURSE_POSTGRES_PORT=${postgres_port}" 38 | Environment="CONCOURSE_POSTGRES_USER=${postgres_username}" 39 | Environment="CONCOURSE_POSTGRES_PASSWORD=${postgres_password}" 40 | Environment="CONCOURSE_POSTGRES_DATABASE=${postgres_database}" 41 | Environment="CONCOURSE_EXTERNAL_URL=${concourse_web_host}" 42 | Environment="CONCOURSE_LOG_LEVEL=${log_level}" 43 | Environment="CONCOURSE_TSA_LOG_LEVEL=${log_level}" 44 | Environment="CONCOURSE_TSA_HOST_KEY=/concourse/keys/web/tsa_host_key" 45 | Environment="CONCOURSE_TSA_AUTHORIZED_KEYS=/concourse/keys/web/authorized_worker_keys" 46 | Environment="CONCOURSE_SESSION_SIGNING_KEY=/concourse/keys/web/session_signing_key" 47 | Environment="CONCOURSE_ENCRYPTION_KEY=${encryption_key}" 48 | Environment="CONCOURSE_OLD_ENCRYPTION_KEY=${old_encryption_key}" 49 | Environment="CONCOURSE_AWS_SECRETSMANAGER_REGION=${region}" 50 | Environment="CONCOURSE_CONTAINER_PLACEMENT_STRATEGY=${placement_strategy}" 51 | Environment="CONCOURSE_SECRET_CACHE_ENABLED=${secret_cache_enabled}" 52 | 53 | %{ if local_user != "" }Environment="CONCOURSE_ADD_LOCAL_USER=${local_user}"%{ endif } 54 | %{ if local_admin_user != "" }Environment="CONCOURSE_MAIN_TEAM_LOCAL_USER=${local_admin_user}"%{ endif } 55 | %{ if length(github_users) > 0 }Environment="CONCOURSE_MAIN_TEAM_GITHUB_USER=${join(",", github_users)}"%{ endif } 56 | %{ if length(github_teams) > 0 }Environment="CONCOURSE_MAIN_TEAM_GITHUB_TEAM=${join(",", github_teams)}"%{ endif } 57 | %{ if prometheus_enabled }Environment="CONCOURSE_PROMETHEUS_BIND_IP=0.0.0.0"%{ endif } 58 | %{ if prometheus_enabled }Environment="CONCOURSE_PROMETHEUS_BIND_PORT=${prometheus_bind_port}"%{ endif } 59 | 60 | ExecStartPre=/bin/bash -c "/bin/systemctl set-environment CONCOURSE_PEER_ADDRESS=$(curl -L http://169.254.169.254/latest/meta-data/local-ipv4)" 61 | ExecStart=/usr/local/bin/aws-env exec -- /usr/local/concourse/bin/concourse web 62 | 63 | [Install] 64 | WantedBy=multi-user.target 65 | - path: "/usr/local/scripts/cloudformation-signal.sh" 66 | permissions: "0744" 67 | owner: "root" 68 | content: | 69 | #! /usr/bin/bash 70 | 71 | set -euo pipefail 72 | 73 | state="" 74 | INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) 75 | 76 | until [ "$state" == "\"healthy\"" ]; do 77 | sleep 10 78 | state=$(aws --region ${region} elbv2 describe-target-health \ 79 | --targets Id=$${INSTANCE_ID},Port=${tsa_port} \ 80 | --target-group-arn ${target_group} \ 81 | --query TargetHealthDescriptions[0].TargetHealth.State) 82 | echo "State is $${state}" 83 | done 84 | runcmd: 85 | - | 86 | /usr/local/scripts/cloudformation-signal.sh 87 | /opt/aws/bin/cfn-signal -e $? --stack ${stack_name} --resource AutoScalingGroup --region ${region} 88 | -------------------------------------------------------------------------------- /modules/cloud-init/shared.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: "/opt/aws/amazon-cloudwatch-agent.json" 4 | permissions: "0644" 5 | owner: "root" 6 | content: | 7 | { 8 | "agent": { 9 | "region": "${region}", 10 | "logfile": "/var/log/amazon-cloudwatch-agent.log", 11 | "metrics_collection_interval": 60 12 | }, 13 | "metrics": { 14 | "namespace": "${cloudwatch_namespace}", 15 | "append_dimensions": { 16 | "InstanceId": "$${aws:InstanceId}", 17 | "AutoScalingGroupName": "$${aws:AutoScalingGroupName}" 18 | }, 19 | "aggregation_dimensions": [ 20 | [ 21 | "InstanceId" 22 | ], 23 | [ 24 | "AutoScalingGroupName" 25 | ] 26 | ], 27 | "metrics_collected": { 28 | "disk": { 29 | "resources": [ 30 | "/" 31 | ], 32 | "measurement": [ 33 | "disk_used_percent" 34 | ] 35 | }, 36 | "swap": { 37 | "measurement": [ 38 | "swap_used_percent" 39 | ] 40 | }, 41 | "mem": { 42 | "measurement": [ 43 | "mem_used_percent" 44 | ] 45 | } 46 | } 47 | }, 48 | "logs": { 49 | "logs_collected": { 50 | "files": { 51 | "collect_list": [ 52 | { 53 | "file_path": "/var/log/concourse.log*", 54 | "log_group_name": "${log_group_name}", 55 | "log_stream_name": "{instance_id}", 56 | "timezone": "UTC" 57 | } 58 | ] 59 | } 60 | }, 61 | "log_stream_name": "{instance_id}/unknown-log-stream" 62 | } 63 | } 64 | - path: "/etc/systemd/system/node_exporter.service" 65 | permissions: "0644" 66 | owner: "root" 67 | content: | 68 | [Unit] 69 | Description=Node exporter for Prometheus to scrape 70 | Requires=network-online.target 71 | After=network-online.target 72 | 73 | [Service] 74 | Type=simple 75 | Restart=always 76 | ExecStart=/usr/local/bin/node_exporter 77 | 78 | [Install] 79 | WantedBy=multi-user.target 80 | - path: "/etc/systemd/system/concourse-logging.service" 81 | permissions: "0644" 82 | owner: "root" 83 | content: | 84 | [Unit] 85 | Description=Service for Concourse logging 86 | After=rc-local.service 87 | 88 | [Service] 89 | Type=simple 90 | Restart=always 91 | TimeoutSec=infinity 92 | 93 | ExecStart=/bin/bash -c '/usr/bin/journalctl -u concourse -f -o cat > /var/log/concourse.log' 94 | 95 | [Install] 96 | WantedBy=multi-user.target 97 | - path: "/etc/logrotate.d/concourse" 98 | permissions: "0644" 99 | owner: "root" 100 | content: | 101 | /var/log/concourse.log { 102 | create 0644 root root 103 | daily 104 | rotate 1 105 | size 100M 106 | postrotate 107 | systemctl restart concourse-logging awslogsd 108 | endscript 109 | } 110 | runcmd: 111 | - | 112 | /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent.json 113 | - | 114 | systemctl enable concourse-logging.service --now 115 | systemctl enable amazon-cloudwatch-agent.service --now 116 | systemctl enable concourse.service --now 117 | %{if prometheus_enabled } systemctl enable node_exporter.service --now %{ endif } 118 | -------------------------------------------------------------------------------- /modules/cloud-init/worker.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: "/concourse/keys/worker/tsa_host_key.pub" 4 | permissions: "0644" 5 | owner: "root" 6 | encoding: base64 7 | content: ${base64encode(pub_tsa_host_key)} 8 | - path: "/concourse/keys/worker/worker_key" 9 | permissions: "0644" 10 | owner: "root" 11 | encoding: base64 12 | content: ${base64encode(worker_key)} 13 | - path: "/concourse/keys/worker/worker_key.pub" 14 | permissions: "0644" 15 | owner: "root" 16 | encoding: base64 17 | content: ${base64encode(pub_worker_key)} 18 | - path: "/etc/systemd/system/concourse.service" 19 | permissions: "0644" 20 | owner: "root" 21 | content: | 22 | [Unit] 23 | Description=Service for Concourse Worker 24 | Requires=network-online.target 25 | After=network-online.target 26 | 27 | [Service] 28 | Type=simple 29 | Restart=always 30 | RestartSec=30s 31 | TimeoutStartSec=5m 32 | TimeoutStopSec=1h 33 | KillMode=process 34 | 35 | Environment="CONCOURSE_TEAM=${worker_team}" 36 | Environment="CONCOURSE_BIND_IP=0.0.0.0" 37 | Environment="CONCOURSE_LOG_LEVEL=${log_level}" 38 | Environment="CONCOURSE_WORK_DIR=/concourse" 39 | Environment="CONCOURSE_TSA_HOST=${tsa_host}:${tsa_port}" 40 | Environment="CONCOURSE_BAGGAGECLAIM_BIND_IP=0.0.0.0" 41 | Environment="CONCOURSE_BAGGAGECLAIM_LOG_LEVEL=${log_level}" 42 | Environment="CONCOURSE_TSA_PUBLIC_KEY=/concourse/keys/worker/tsa_host_key.pub" 43 | Environment="CONCOURSE_TSA_WORKER_PRIVATE_KEY=/concourse/keys/worker/worker_key" 44 | Environment="CONCOURSE_REBALANCE_INTERVAL=30m" 45 | 46 | ExecStartPre=/bin/bash -c "/bin/systemctl set-environment CONCOURSE_NAME=$(curl -L http://169.254.169.254/latest/meta-data/instance-id)" 47 | ExecStart=/usr/local/concourse/bin/concourse worker 48 | 49 | ExecStop=/usr/local/concourse/bin/concourse retire-worker 50 | ExecStop=/bin/bash -c "while pgrep concourse >> /dev/null; do echo draining worker... && sleep 5; done; echo done draining!" 51 | 52 | [Install] 53 | WantedBy=multi-user.target 54 | - path: "/etc/systemd/system/lifecycled.service" 55 | permissions: "0644" 56 | owner: "root" 57 | content: | 58 | [Unit] 59 | Description=Service for Autoscaling lifecycle 60 | Requires=network-online.target 61 | After=network-online.target 62 | 63 | [Service] 64 | Type=simple 65 | Restart=on-failure 66 | RestartSec=30s 67 | TimeoutSec=infinity 68 | 69 | Environment="AWS_REGION=${region}" 70 | ExecStart=/usr/local/bin/lifecycled \ 71 | --sns-topic=${lifecycle_topic} \ 72 | --handler=/usr/local/scripts/lifecycle-handler.sh \ 73 | --cloudwatch-group=${lifecycled_log_group_name} \ 74 | --json 75 | 76 | [Install] 77 | WantedBy=multi-user.target 78 | - path: "/usr/local/scripts/cloudformation-signal.sh" 79 | permissions: "0744" 80 | owner: "root" 81 | content: | 82 | #! /usr/bin/bash 83 | 84 | set -euo pipefail 85 | 86 | function await_unit() { 87 | echo -n "Waiting for $1..." 88 | while ! systemctl is-active $1 > /dev/null; do 89 | sleep 1 90 | done 91 | echo "Done!" 92 | } 93 | 94 | await_unit lifecycled.service 95 | await_unit concourse.service 96 | - path: "/usr/local/scripts/lifecycle-handler.sh" 97 | permissions: "0744" 98 | owner: "root" 99 | content: | 100 | #! /usr/bin/bash 101 | 102 | set -euo pipefail 103 | 104 | systemctl stop concourse.service 105 | runcmd: 106 | - | 107 | systemctl enable lifecycled.service --now 108 | - | 109 | /usr/local/scripts/cloudformation-signal.sh 110 | /opt/aws/bin/cfn-signal -e $? --stack ${stack_name} --resource AutoScalingGroup --region ${region} 111 | 112 | -------------------------------------------------------------------------------- /modules/dashboard/README.md: -------------------------------------------------------------------------------- 1 | ## Concourse Cloudwatch Dashboard 2 | 3 | A cloudwatch dashboard for the Concourse deployment. 4 | -------------------------------------------------------------------------------- /modules/dashboard/dashboard.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "metric", 5 | "x": 0, 6 | "y": 0, 7 | "width": 6, 8 | "height": 9, 9 | "properties": { 10 | "metrics": [ 11 | [ 12 | "AWS/EC2", 13 | "CPUUtilization", 14 | "AutoScalingGroupName", 15 | "${atc_asg_name}", 16 | { 17 | "label": "ATC" 18 | } 19 | ], 20 | [ 21 | "AWS/EC2", 22 | "CPUUtilization", 23 | "AutoScalingGroupName", 24 | "${worker_asg_name}", 25 | { 26 | "label": "Worker" 27 | } 28 | ] 29 | ], 30 | "view": "timeSeries", 31 | "stacked": false, 32 | "region": "${region}", 33 | "title": "CPU utilization", 34 | "period": ${period} 35 | } 36 | }, 37 | { 38 | "type": "metric", 39 | "x": 6, 40 | "y": 0, 41 | "width": 6, 42 | "height": 6, 43 | "properties": { 44 | "metrics": [ 45 | [ 46 | "${cloudwatch_namespace}", 47 | "mem_used_percent", 48 | "AutoScalingGroupName", 49 | "${atc_asg_name}", 50 | { 51 | "label": "ATC" 52 | } 53 | ], 54 | [ 55 | "${cloudwatch_namespace}", 56 | "mem_used_percent", 57 | "AutoScalingGroupName", 58 | "${worker_asg_name}", 59 | { 60 | "label": "Worker" 61 | } 62 | ] 63 | ], 64 | "view": "timeSeries", 65 | "region": "${region}", 66 | "title": "Memory usage", 67 | "period": ${period}, 68 | "stacked": false 69 | } 70 | }, 71 | { 72 | "type": "metric", 73 | "x": 6, 74 | "y": 6, 75 | "width": 6, 76 | "height": 3, 77 | "properties": { 78 | "metrics": [ 79 | [ 80 | "${cloudwatch_namespace}", 81 | "swap_used_percent", 82 | "AutoScalingGroupName", 83 | "${atc_asg_name}", 84 | { 85 | "label": "ATC" 86 | } 87 | ], 88 | [ 89 | "${cloudwatch_namespace}", 90 | "swap_used_percent", 91 | "AutoScalingGroupName", 92 | "${worker_asg_name}", 93 | { 94 | "label": "Worker" 95 | } 96 | ] 97 | ], 98 | "view": "timeSeries", 99 | "region": "${region}", 100 | "title": "Swap usage", 101 | "period": ${period}, 102 | "stacked": false 103 | } 104 | }, 105 | { 106 | "type": "metric", 107 | "x": 12, 108 | "y": 0, 109 | "width": 6, 110 | "height": 9, 111 | "properties": { 112 | "metrics": [ 113 | [ 114 | "${cloudwatch_namespace}", 115 | "disk_used_percent", 116 | "AutoScalingGroupName", 117 | "${atc_asg_name}", 118 | { 119 | "label": "ATC" 120 | } 121 | ], 122 | [ 123 | "${cloudwatch_namespace}", 124 | "disk_used_percent", 125 | "AutoScalingGroupName", 126 | "${worker_asg_name}", 127 | { 128 | "label": "Worker" 129 | } 130 | ] 131 | ], 132 | "view": "timeSeries", 133 | "region": "${region}", 134 | "title": "Disk usage", 135 | "period": ${period}, 136 | "stacked": false 137 | } 138 | }, 139 | { 140 | "type": "metric", 141 | "x": 18, 142 | "y": 0, 143 | "width": 6, 144 | "height": 6, 145 | "properties": { 146 | "metrics": [ 147 | [ 148 | "AWS/Logs", 149 | "IncomingLogEvents", 150 | "LogGroupName", 151 | "${atc_log_group_name}", 152 | { 153 | "label": "ATC" 154 | } 155 | ], 156 | [ 157 | "AWS/Logs", 158 | "IncomingLogEvents", 159 | "LogGroupName", 160 | "${worker_log_group_name}", 161 | { 162 | "label": "Worker" 163 | } 164 | ] 165 | ], 166 | "view": "timeSeries", 167 | "stacked": false, 168 | "region": "${region}", 169 | "title": "Log events (ATC/Worker)", 170 | "period": ${period} 171 | } 172 | }, 173 | { 174 | "type": "metric", 175 | "x": 18, 176 | "y": 6, 177 | "width": 6, 178 | "height": 3, 179 | "properties": { 180 | "metrics": [ 181 | [ 182 | "AWS/AutoScaling", 183 | "GroupTotalInstances", 184 | "AutoScalingGroupName", 185 | "${atc_asg_name}", 186 | { 187 | "label": "ATC" 188 | } 189 | ], 190 | [ 191 | "AWS/AutoScaling", 192 | "GroupTotalInstances", 193 | "AutoScalingGroupName", 194 | "${worker_asg_name}", 195 | { 196 | "label": "Worker" 197 | } 198 | ] 199 | ], 200 | "view": "timeSeries", 201 | "region": "${region}", 202 | "title": "Instance count", 203 | "period": ${period}, 204 | "stacked": false 205 | } 206 | }, 207 | { 208 | "type": "metric", 209 | "x": 0, 210 | "y": 9, 211 | "width": 6, 212 | "height": 9, 213 | "properties": { 214 | "metrics": [ 215 | [ 216 | "AWS/EC2", 217 | "NetworkIn", 218 | "AutoScalingGroupName", 219 | "${atc_asg_name}", 220 | { 221 | "label": "Inbound" 222 | } 223 | ], 224 | [ 225 | "AWS/EC2", 226 | "NetworkOut", 227 | "AutoScalingGroupName", 228 | "${atc_asg_name}", 229 | { 230 | "label": "Outbound" 231 | } 232 | ] 233 | ], 234 | "view": "timeSeries", 235 | "stacked": false, 236 | "region": "${region}", 237 | "title": "ATC network traffic (ENI in/out)", 238 | "period": ${period} 239 | } 240 | }, 241 | { 242 | "type": "metric", 243 | "x": 6, 244 | "y": 9, 245 | "width": 6, 246 | "height": 9, 247 | "properties": { 248 | "metrics": [ 249 | [ 250 | "AWS/EC2", 251 | "NetworkIn", 252 | "AutoScalingGroupName", 253 | "${worker_asg_name}", 254 | { 255 | "label": "Inbound" 256 | } 257 | ], 258 | [ 259 | "AWS/EC2", 260 | "NetworkOut", 261 | "AutoScalingGroupName", 262 | "${worker_asg_name}", 263 | { 264 | "label": "Outbound" 265 | } 266 | ] 267 | ], 268 | "view": "timeSeries", 269 | "stacked": false, 270 | "region": "${region}", 271 | "title": "Worker network traffic (ENI in/out)", 272 | "period": ${period} 273 | } 274 | }, 275 | { 276 | "type": "metric", 277 | "x": 12, 278 | "y": 9, 279 | "width": 6, 280 | "height": 9, 281 | "properties": { 282 | "metrics": [ 283 | [ 284 | "AWS/ApplicationELB", 285 | "ProcessedBytes", 286 | "LoadBalancer", 287 | "${external_lb}", 288 | { 289 | "label": "External ALB" 290 | } 291 | ], 292 | [ 293 | "AWS/NetworkELB", 294 | "ProcessedBytes", 295 | "LoadBalancer", 296 | "${internal_lb}", 297 | { 298 | "label": "Internal NLB" 299 | } 300 | ] 301 | ], 302 | "view": "timeSeries", 303 | "stacked": false, 304 | "region": "${region}", 305 | "period": ${period}, 306 | "title": "Internal/external LB (processed bytes)" 307 | } 308 | }, 309 | { 310 | "type": "metric", 311 | "x": 18, 312 | "y": 9, 313 | "width": 6, 314 | "height": 9, 315 | "properties": { 316 | "metrics": [ 317 | %{ if length(nat_gateway_metrics) > 0 } 318 | [ 319 | { 320 | "expression": "SUM(${jsonencode(nat_inbound_ids)})", 321 | "label": "Inbound", 322 | "id": "inbound" 323 | } 324 | ], 325 | [ 326 | { 327 | "expression": "SUM(${jsonencode(nat_outbound_ids)})", 328 | "label": "Outbound", 329 | "id": "outbound" 330 | } 331 | ], 332 | ${jsonencode(nat_gateway_metrics)} 333 | %{ endif } 334 | ], 335 | "view": "timeSeries", 336 | "stacked": false, 337 | "region": "${region}", 338 | "title": "NAT Gateway (in/out)", 339 | "period": ${period} 340 | } 341 | %{ if rds_cluster_id == "" } 342 | } 343 | %{ else } 344 | }, 345 | { 346 | "type": "metric", 347 | "x": 0, 348 | "y": 18, 349 | "width": 6, 350 | "height": 9, 351 | "properties": { 352 | "metrics": [ 353 | [ 354 | "AWS/RDS", 355 | "DatabaseConnections", 356 | "DBClusterIdentifier", 357 | "${rds_cluster_id}", 358 | { 359 | "stat": "Maximum", 360 | "period": ${period} 361 | } 362 | ] 363 | ], 364 | "view": "timeSeries", 365 | "stacked": false, 366 | "region": "${region}", 367 | "title": "RDS Database connections (max)" 368 | } 369 | }, 370 | { 371 | "type": "metric", 372 | "x": 6, 373 | "y": 18, 374 | "width": 6, 375 | "height": 9, 376 | "properties": { 377 | "metrics": [ 378 | [ 379 | "AWS/RDS", 380 | "CPUUtilization", 381 | "DBClusterIdentifier", 382 | "${rds_cluster_id}", 383 | { 384 | "stat": "Maximum", 385 | "period": ${period} 386 | } 387 | ] 388 | ], 389 | "view": "timeSeries", 390 | "stacked": false, 391 | "region": "${region}", 392 | "title": "RDS CPU Utilisation (max)" 393 | } 394 | }, 395 | { 396 | "type": "metric", 397 | "x": 12, 398 | "y": 18, 399 | "width": 6, 400 | "height": 9, 401 | "properties": { 402 | "metrics": [ 403 | [ 404 | "AWS/RDS", 405 | "FreeableMemory", 406 | "DBClusterIdentifier", 407 | "${rds_cluster_id}", 408 | { 409 | "stat": "Minimum", 410 | "period": ${period} 411 | } 412 | ] 413 | ], 414 | "view": "timeSeries", 415 | "stacked": false, 416 | "region": "${region}", 417 | "title": "RDS Freeable memory (min)" 418 | } 419 | } 420 | %{ endif } 421 | ] 422 | } 423 | -------------------------------------------------------------------------------- /modules/dashboard/main.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Resources 3 | # ------------------------------------------------------------------------------- 4 | data "aws_region" "current" {} 5 | 6 | locals { 7 | nat_inbound = [ 8 | for i, id in var.nat_gateway_ids : [ 9 | "AWS/NATGateway", 10 | "BytesInFromDestination", 11 | "NatGatewayId", 12 | id, 13 | { 14 | stat = "Average" 15 | id = "nat-${i + 1}-inbound" 16 | visible = false 17 | } 18 | ] 19 | ] 20 | 21 | nat_outbound = [ 22 | for i, id in var.nat_gateway_ids : [ 23 | "AWS/NATGateway", 24 | "BytesInFromSource", 25 | "NatGatewayId", 26 | id, 27 | { 28 | stat = "Average" 29 | id = "nat-${i + 1}-outbound" 30 | visible = false 31 | } 32 | ] 33 | ] 34 | } 35 | 36 | resource "aws_cloudwatch_dashboard" "main" { 37 | dashboard_name = var.name_prefix 38 | dashboard_body = templatefile("${path.module}/dashboard.json.template", { 39 | cloudwatch_namespace = var.name_prefix 40 | atc_asg_name = var.atc_asg_name 41 | atc_log_group_name = var.atc_log_group_name 42 | worker_asg_name = var.worker_asg_name 43 | worker_log_group_name = var.worker_log_group_name 44 | rds_cluster_id = var.rds_cluster_id 45 | external_lb = join("/", slice(split("/", var.external_lb_arn), 1, 4)) 46 | internal_lb = join("/", slice(split("/", var.internal_lb_arn), 1, 4)) 47 | nat_gateway_metrics = concat(local.nat_inbound, local.nat_outbound) 48 | nat_inbound_ids = [for i, _ in var.nat_gateway_ids : "nat-${i + 1}-inbound"] 49 | nat_outbound_ids = [for i, _ in var.nat_gateway_ids : "nat-${i + 1}-outbound"] 50 | period = var.period 51 | region = data.aws_region.current.name 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /modules/dashboard/outputs.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Output 3 | # ------------------------------------------------------------------------------- 4 | output "arn" { 5 | value = aws_cloudwatch_dashboard.main.dashboard_arn 6 | } 7 | -------------------------------------------------------------------------------- /modules/dashboard/variables.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Variables 3 | # ------------------------------------------------------------------------------ 4 | variable "name_prefix" { 5 | description = "A prefix used for naming resources." 6 | type = string 7 | } 8 | 9 | variable "atc_asg_name" { 10 | description = "Name of the ATC autoscaling group." 11 | type = string 12 | } 13 | 14 | variable "atc_log_group_name" { 15 | description = "Name of the ATC log group." 16 | type = string 17 | } 18 | 19 | variable "worker_asg_name" { 20 | description = "Name of the worker autoscaling group." 21 | type = string 22 | } 23 | 24 | variable "worker_log_group_name" { 25 | description = "Name of the worker log group." 26 | type = string 27 | } 28 | 29 | variable "rds_cluster_id" { 30 | description = "ID/Name of the RDS cluster." 31 | type = string 32 | } 33 | 34 | variable "external_lb_arn" { 35 | description = "ARN of the external load balancer." 36 | type = string 37 | } 38 | 39 | variable "internal_lb_arn" { 40 | description = "ARN of the external load balancer." 41 | type = string 42 | } 43 | 44 | variable "nat_gateway_ids" { 45 | description = "A list of NAT gateways for which to include metrics." 46 | type = list(string) 47 | default = [] 48 | } 49 | 50 | variable "period" { 51 | description = "The default period, in seconds, for all metrics in this widget. The period is the length of time represented by one data point on the graph." 52 | type = number 53 | default = 60 54 | } 55 | 56 | 57 | variable "tags" { 58 | description = "A map of tags (key-value pairs) passed to resources." 59 | type = map(string) 60 | default = {} 61 | } 62 | 63 | -------------------------------------------------------------------------------- /modules/dashboard/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.14" 4 | } 5 | -------------------------------------------------------------------------------- /modules/worker/README.md: -------------------------------------------------------------------------------- 1 | ## Concourse Worker 2 | 3 | A Terraform module for deploying a Concourse worker cluster. 4 | -------------------------------------------------------------------------------- /modules/worker/main.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Resources 3 | # ------------------------------------------------------------------------------- 4 | data "aws_region" "current" {} 5 | 6 | data "aws_caller_identity" "current" {} 7 | 8 | resource "aws_security_group_rule" "atc_ingress_garbage_collection" { 9 | security_group_id = module.worker.security_group_id 10 | type = "ingress" 11 | protocol = "tcp" 12 | from_port = 7799 13 | to_port = 7799 14 | source_security_group_id = var.atc_sg 15 | } 16 | 17 | resource "aws_security_group_rule" "atc_ingress_baggageclaim" { 18 | security_group_id = module.worker.security_group_id 19 | type = "ingress" 20 | protocol = "tcp" 21 | from_port = 7788 22 | to_port = 7788 23 | source_security_group_id = var.atc_sg 24 | } 25 | 26 | resource "aws_security_group_rule" "atc_ingress_garden" { 27 | security_group_id = module.worker.security_group_id 28 | type = "ingress" 29 | protocol = "tcp" 30 | from_port = 7777 31 | to_port = 7777 32 | source_security_group_id = var.atc_sg 33 | } 34 | 35 | module "worker" { 36 | source = "telia-oss/asg/aws" 37 | version = "4.0.0" 38 | 39 | name_prefix = "${var.name_prefix}-worker" 40 | user_data_base64 = data.template_cloudinit_config.worker.rendered 41 | vpc_id = var.vpc_id 42 | subnet_ids = var.private_subnet_ids 43 | min_size = var.min_size 44 | max_size = var.max_size 45 | instance_type = var.instance_type 46 | instance_ami = var.instance_ami 47 | instance_key = var.instance_key 48 | instance_policy = data.aws_iam_policy_document.worker.json 49 | instance_volume_size = var.instance_volume_size 50 | await_signal = true 51 | pause_time = "PT5M" 52 | health_check_type = "EC2" 53 | tags = var.tags 54 | 55 | } 56 | 57 | locals { 58 | shared_cloud_init = templatefile("${path.module}/../cloud-init/shared.yml", { 59 | region = data.aws_region.current.name 60 | cloudwatch_namespace = var.name_prefix 61 | log_group_name = aws_cloudwatch_log_group.worker.name 62 | prometheus_enabled = var.prometheus_enabled 63 | }) 64 | 65 | worker_cloud_init = templatefile("${path.module}/../cloud-init/worker.yml", { 66 | region = data.aws_region.current.name 67 | stack_name = "${var.name_prefix}-worker-asg" 68 | tsa_host = var.tsa_host 69 | tsa_port = var.tsa_port 70 | log_level = var.log_level 71 | worker_team = var.worker_team 72 | worker_key = file("${var.concourse_keys}/worker_key") 73 | pub_worker_key = file("${var.concourse_keys}/worker_key.pub") 74 | pub_tsa_host_key = file("${var.concourse_keys}/tsa_host_key.pub") 75 | lifecycle_topic = aws_sns_topic.worker.arn 76 | lifecycled_log_group_name = aws_cloudwatch_log_group.worker_lifecycled.name 77 | }) 78 | } 79 | 80 | data "template_cloudinit_config" "worker" { 81 | gzip = true 82 | base64_encode = true 83 | 84 | part { 85 | content_type = "text/cloud-config" 86 | content = local.shared_cloud_init 87 | } 88 | 89 | part { 90 | content_type = "text/cloud-config" 91 | merge_type = "list(append)+dict(recurse_array)+str()" 92 | content = local.worker_cloud_init 93 | } 94 | } 95 | 96 | resource "aws_cloudwatch_log_group" "worker" { 97 | name = "${var.name_prefix}-worker" 98 | } 99 | 100 | resource "aws_cloudwatch_log_group" "worker_lifecycled" { 101 | name = "${var.name_prefix}-worker-lifecycled" 102 | } 103 | 104 | data "aws_iam_policy_document" "worker" { 105 | statement { 106 | effect = "Allow" 107 | 108 | resources = ["*"] 109 | 110 | actions = [ 111 | "cloudwatch:PutMetricData", 112 | "cloudwatch:GetMetricStatistics", 113 | "cloudwatch:ListMetrics", 114 | "logs:DescribeLogStreams", 115 | "logs:DescribeLogGroups", 116 | "ec2:DescribeTags", 117 | ] 118 | } 119 | 120 | statement { 121 | effect = "Allow" 122 | 123 | resources = [ 124 | aws_cloudwatch_log_group.worker.arn, 125 | aws_cloudwatch_log_group.worker_lifecycled.arn, 126 | ] 127 | 128 | actions = [ 129 | "logs:CreateLogStream", 130 | "logs:CreateLogGroup", 131 | "logs:PutLogEvents", 132 | ] 133 | } 134 | 135 | statement { 136 | effect = "Allow" 137 | 138 | resources = [ 139 | aws_sns_topic.worker.arn, 140 | ] 141 | 142 | actions = [ 143 | "sns:Subscribe", 144 | "sns:Unsubscribe", 145 | ] 146 | } 147 | 148 | statement { 149 | effect = "Allow" 150 | 151 | resources = ["arn:aws:sqs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:lifecycled-*"] 152 | 153 | actions = [ 154 | "sqs:*", 155 | ] 156 | } 157 | 158 | statement { 159 | effect = "Allow" 160 | 161 | resources = ["*"] 162 | 163 | actions = [ 164 | "autoscaling:RecordLifecycleActionHeartbeat", 165 | "autoscaling:CompleteLifecycleAction", 166 | ] 167 | } 168 | } 169 | 170 | resource "aws_sns_topic" "worker" { 171 | name = "${var.name_prefix}-worker-lifecycle" 172 | } 173 | 174 | resource "aws_autoscaling_lifecycle_hook" "worker" { 175 | name = "${var.name_prefix}-worker-lifecycle" 176 | autoscaling_group_name = module.worker.id 177 | lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING" 178 | default_result = "CONTINUE" 179 | heartbeat_timeout = 300 180 | notification_target_arn = aws_sns_topic.worker.arn 181 | role_arn = aws_iam_role.lifecycle.arn 182 | } 183 | 184 | resource "aws_iam_role" "lifecycle" { 185 | name = "${var.name_prefix}-lifecycle-role" 186 | assume_role_policy = data.aws_iam_policy_document.asg_assume.json 187 | } 188 | 189 | resource "aws_iam_role_policy" "lifecycle" { 190 | name = "${var.name_prefix}-lifecycle-permissions" 191 | role = aws_iam_role.lifecycle.id 192 | policy = data.aws_iam_policy_document.asg_permissions.json 193 | } 194 | 195 | data "aws_iam_policy_document" "asg_assume" { 196 | statement { 197 | effect = "Allow" 198 | actions = ["sts:AssumeRole"] 199 | 200 | principals { 201 | type = "Service" 202 | identifiers = ["autoscaling.amazonaws.com"] 203 | } 204 | } 205 | } 206 | 207 | data "aws_iam_policy_document" "asg_permissions" { 208 | statement { 209 | effect = "Allow" 210 | 211 | resources = [ 212 | aws_sns_topic.worker.arn, 213 | ] 214 | 215 | actions = [ 216 | "sns:Publish", 217 | ] 218 | } 219 | } 220 | 221 | -------------------------------------------------------------------------------- /modules/worker/outputs.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------- 2 | # Output 3 | # ------------------------------------------------------------------------------- 4 | output "asg_id" { 5 | value = module.worker.id 6 | } 7 | 8 | output "role_arn" { 9 | value = module.worker.role_arn 10 | } 11 | 12 | output "role_name" { 13 | value = module.worker.role_name 14 | } 15 | 16 | output "security_group_id" { 17 | value = module.worker.security_group_id 18 | } 19 | 20 | output "log_group_name" { 21 | value = aws_cloudwatch_log_group.worker.name 22 | } 23 | 24 | output "lifecycled_log_group_name" { 25 | value = aws_cloudwatch_log_group.worker_lifecycled.name 26 | } 27 | -------------------------------------------------------------------------------- /modules/worker/variables.tf: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Variables 3 | # ------------------------------------------------------------------------------ 4 | variable "name_prefix" { 5 | description = "A prefix used for naming resources." 6 | type = string 7 | } 8 | 9 | variable "vpc_id" { 10 | description = "The VPC ID." 11 | type = string 12 | } 13 | 14 | variable "private_subnet_ids" { 15 | description = "ID of subnets where private resources (Workers) can be provisioned." 16 | type = list(string) 17 | } 18 | 19 | variable "min_size" { 20 | description = "The minimum (and desired) size of the auto scale group." 21 | type = number 22 | default = 1 23 | } 24 | 25 | variable "max_size" { 26 | description = "The maximum size of the auto scale group." 27 | type = number 28 | default = 3 29 | } 30 | 31 | variable "instance_type" { 32 | description = "Type of instance to provision for the Concourse workers." 33 | type = string 34 | default = "t3.large" 35 | } 36 | 37 | variable "instance_ami" { 38 | description = "The EC2 image ID to launch. See the include packer image." 39 | type = string 40 | } 41 | 42 | variable "instance_key" { 43 | description = "The key name that should be used for the worker instances." 44 | type = string 45 | default = "" 46 | } 47 | 48 | variable "instance_volume_size" { 49 | description = "The size of the worker volumes in gigabytes." 50 | type = number 51 | default = 50 52 | } 53 | 54 | variable "concourse_keys" { 55 | description = "Path to a directory containing the Concourse SSH keys. (See README.md)." 56 | type = string 57 | } 58 | 59 | variable "atc_sg" { 60 | description = "The ID of the security group created for the Concourse ATC." 61 | type = string 62 | } 63 | 64 | variable "tsa_host" { 65 | description = "TSA host to forward the worker through (i.e. the address of the internal load balancer for the ATC)." 66 | type = string 67 | } 68 | 69 | variable "tsa_port" { 70 | description = "The port used to reach the TSA host." 71 | type = number 72 | } 73 | 74 | variable "prometheus_enabled" { 75 | description = "Enable exporting of prometheus metrics." 76 | type = bool 77 | default = false 78 | } 79 | 80 | variable "worker_team" { 81 | description = "The name of the team that this worker will be assigned to." 82 | type = string 83 | default = "" 84 | } 85 | 86 | variable "log_level" { 87 | description = "Minimum level of logs to see (options: debug|info|error|fatal)." 88 | type = string 89 | default = "info" 90 | } 91 | 92 | variable "tags" { 93 | description = "A map of tags (key-value pairs) passed to resources." 94 | type = map(string) 95 | default = {} 96 | } 97 | 98 | -------------------------------------------------------------------------------- /modules/worker/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /packer/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "source_ami": "", 4 | "ami_users": "", 5 | "template_version": null, 6 | "concourse_version": "5.6.0", 7 | "lifecycled_version": "3.0.2", 8 | "aws_env_version": "1.0.0" 9 | }, 10 | "builders": [ 11 | { 12 | "type": "amazon-ebs", 13 | "region": "eu-west-1", 14 | "source_ami": "{{user `source_ami`}}", 15 | "source_ami_filter": { 16 | "filters": { 17 | "name": "amzn2-ami-hvm*gp2", 18 | "architecture": "x86_64", 19 | "virtualization-type": "hvm", 20 | "root-device-type": "ebs" 21 | }, 22 | "owners": [ 23 | "137112412989" 24 | ], 25 | "most_recent": true 26 | }, 27 | "instance_type": "m3.medium", 28 | "ssh_username": "ec2-user", 29 | "ami_name": "concourse-{{user `concourse_version`}}-{{timestamp}}", 30 | "ami_users": "{{user `ami_users`}}", 31 | "tags": { 32 | "source_ami": "{{ .SourceAMI }}", 33 | "template_version": "{{user `template_version`}}", 34 | "concourse_version": "{{user `concourse_version`}}", 35 | "lifecycled_version": "{{user `lifecycled_version`}}", 36 | "aws_env_version": "{{user `aws_env_version`}}" 37 | } 38 | } 39 | ], 40 | "provisioners": [ 41 | { 42 | "type": "shell", 43 | "inline": [ 44 | "sleep 30", 45 | "sudo yum update -y", 46 | "sudo yum install -y aws-cfn-bootstrap" 47 | ] 48 | }, 49 | { 50 | "type": "shell", 51 | "inline": [ 52 | "curl -L https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm -o /tmp/amazon-cloudwatch-agent.rpm", 53 | "sudo rpm -U /tmp/amazon-cloudwatch-agent.rpm" 54 | ] 55 | }, 56 | { 57 | "type": "shell", 58 | "inline": [ 59 | "curl -L https://github.com/concourse/concourse/releases/download/v{{user `concourse_version`}}/concourse-{{user `concourse_version`}}-linux-amd64.tgz -o /tmp/concourse-linux-amd64.tgz", 60 | "sudo tar -xvzf /tmp/concourse-linux-amd64.tgz -C /usr/local" 61 | ] 62 | }, 63 | { 64 | "type": "shell", 65 | "inline": [ 66 | "curl -L https://github.com/buildkite/lifecycled/releases/download/v{{user `lifecycled_version`}}/lifecycled-linux-amd64 -o lifecycled", 67 | "sudo chmod +x lifecycled", 68 | "sudo chown root:root lifecycled", 69 | "sudo mv lifecycled /usr/local/bin/lifecycled" 70 | ] 71 | }, 72 | { 73 | "type": "shell", 74 | "inline": [ 75 | "curl -L https://github.com/telia-oss/aws-env/releases/download/v{{user `aws_env_version`}}/aws-env-linux-amd64 -o aws-env", 76 | "sudo chmod +x aws-env", 77 | "sudo chown root:root aws-env", 78 | "sudo mv aws-env /usr/local/bin/aws-env" 79 | ] 80 | }, 81 | { 82 | "type": "shell", 83 | "inline": [ 84 | "curl -L https://github.com/prometheus/node_exporter/releases/download/v0.16.0/node_exporter-0.16.0.linux-amd64.tar.gz -o node_exporter.tar.gz", 85 | "tar xvzf node_exporter.tar.gz --strip=1 */node_exporter", 86 | "rm node_exporter.tar.gz", 87 | "sudo chmod +x node_exporter", 88 | "sudo chown root:root node_exporter", 89 | "sudo mv node_exporter /usr/local/bin/node_exporter" 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /test/module.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | "runtime" 15 | "testing" 16 | "time" 17 | 18 | "github.com/aws/aws-sdk-go/aws" 19 | "github.com/aws/aws-sdk-go/aws/session" 20 | "github.com/aws/aws-sdk-go/service/autoscaling" 21 | "github.com/stretchr/testify/assert" 22 | 23 | asg "github.com/telia-oss/terraform-aws-asg/v3/test" 24 | ) 25 | 26 | type Expectations struct { 27 | Version string 28 | WorkerVersion string 29 | ATCAutoscaling asg.Expectations 30 | WorkerAutoscaling asg.Expectations 31 | } 32 | 33 | func RunTestSuite(t *testing.T, endpoint, atcASGName, workerASGName, adminUser, adminPassword, region string, expected Expectations) { 34 | // Run test suites for the autoscaling groups. 35 | asg.RunTestSuite(t, atcASGName, region, expected.ATCAutoscaling) 36 | asg.RunTestSuite(t, workerASGName, region, expected.WorkerAutoscaling) 37 | 38 | // Wait for ATC to register as healthy in the target groups (max 10min wait) 39 | sess := NewSession(t, region) 40 | WaitForHealthyTargets(t, sess, atcASGName, 1*time.Minute, 15*time.Minute) 41 | 42 | info := GetConcourseInfo(t, endpoint) 43 | assert.Equal(t, expected.Version, info.Version) 44 | assert.Equal(t, expected.WorkerVersion, info.WorkerVersion) 45 | 46 | // Download and install fly binary. 47 | tempDir, err := ioutil.TempDir("", "terraform-aws-concourse") 48 | if err != nil { 49 | t.Fatalf("failed to create temporary directory for fly binary: %s", err) 50 | } 51 | defer os.RemoveAll(tempDir) 52 | 53 | fly := &Fly{ 54 | Endpoint: endpoint, 55 | Directory: tempDir, 56 | Target: "terraform-aws-concourse", 57 | } 58 | 59 | fly.Setup(t, adminUser, adminPassword) 60 | 61 | workers := fly.Workers(t) 62 | assert.Equal(t, int(expected.WorkerAutoscaling.MinSize), len(workers)) 63 | for _, worker := range workers { 64 | assert.Equal(t, "linux", worker.Platform) 65 | assert.Equal(t, "running", worker.State) 66 | } 67 | } 68 | 69 | func parseURL(t *testing.T, endpoint string) *url.URL { 70 | u, err := url.Parse(endpoint) 71 | if err != nil { 72 | t.Fatalf("failed to parse url from endpoint: %s", endpoint) 73 | } 74 | return u 75 | } 76 | 77 | func GetConcourseInfo(t *testing.T, endpoint string) ConcourseInfo { 78 | u := parseURL(t, endpoint) 79 | u.Path = path.Join(u.Path, "api", "v1", "info") 80 | 81 | r, err := http.Get(u.String()) 82 | if err != nil { 83 | t.Fatalf("get-request error: %s", err) 84 | } 85 | defer r.Body.Close() 86 | 87 | if r.StatusCode != http.StatusOK { 88 | t.Errorf("got non-200 response: %d", r.StatusCode) 89 | } 90 | 91 | var info ConcourseInfo 92 | err = json.NewDecoder(r.Body).Decode(&info) 93 | if err != nil { 94 | t.Fatalf("failed to deserialize JSON response: %s", err) 95 | } 96 | return info 97 | } 98 | 99 | type ConcourseInfo struct { 100 | Version string `json:"version"` 101 | WorkerVersion string `json:"worker_version"` 102 | } 103 | 104 | func NewSession(t *testing.T, region string) *session.Session { 105 | sess, err := session.NewSession(&aws.Config{ 106 | Region: aws.String(region), 107 | }) 108 | if err != nil { 109 | t.Fatalf("failed to create new AWS session: %s", err) 110 | } 111 | return sess 112 | } 113 | 114 | func DescribeTargetGroups(t *testing.T, sess *session.Session, asgName string) []*autoscaling.LoadBalancerTargetGroupState { 115 | c := autoscaling.New(sess) 116 | 117 | out, err := c.DescribeLoadBalancerTargetGroups(&autoscaling.DescribeLoadBalancerTargetGroupsInput{ 118 | AutoScalingGroupName: aws.String(asgName), 119 | }) 120 | if err != nil { 121 | t.Fatalf("failed to describe load balancer target groups: %s", err) 122 | } 123 | return out.LoadBalancerTargetGroups 124 | } 125 | 126 | func WaitForHealthyTargets(t *testing.T, sess *session.Session, asgName string, checkInterval time.Duration, timeoutLimit time.Duration) { 127 | interval := time.NewTicker(checkInterval) 128 | defer interval.Stop() 129 | 130 | timeout := time.NewTimer(timeoutLimit) 131 | defer timeout.Stop() 132 | 133 | WaitLoop: 134 | for { 135 | select { 136 | case <-interval.C: 137 | targetGroups := DescribeTargetGroups(t, sess, asgName) 138 | for _, group := range targetGroups { 139 | if aws.StringValue(group.State) != "InService" { 140 | t.Logf("target group not ready: %s", aws.StringValue(group.LoadBalancerTargetGroupARN)) 141 | continue WaitLoop 142 | } 143 | } 144 | break WaitLoop 145 | case <-timeout.C: 146 | t.Fatal("timeout reached while waiting for target group health checks") 147 | } 148 | } 149 | } 150 | 151 | type Fly struct { 152 | Endpoint string 153 | Directory string 154 | Target string 155 | bin string 156 | } 157 | 158 | func (f *Fly) Setup(t *testing.T, username, password string) { 159 | u := parseURL(t, f.Endpoint) 160 | q := u.Query() 161 | 162 | q.Set("arch", "amd64") 163 | q.Set("platform", runtime.GOOS) 164 | 165 | u.Path = path.Join(u.Path, "api", "v1", "cli") 166 | u.RawQuery = q.Encode() 167 | 168 | f.bin = filepath.Join(f.Directory, "fly") 169 | file, err := os.Create(f.bin) 170 | if err != nil { 171 | t.Fatalf("failed to create new file: %s", err) 172 | } 173 | defer file.Close() 174 | 175 | resp, err := http.Get(u.String()) 176 | if err != nil { 177 | t.Fatalf("failed to get fly: %s", err) 178 | } 179 | defer resp.Body.Close() 180 | 181 | _, err = io.Copy(file, resp.Body) 182 | if err != nil { 183 | t.Fatalf("failed to write fly to disk: %s", err) 184 | } 185 | 186 | err = file.Chmod(0755) 187 | if err != nil { 188 | t.Fatalf("failed to change fly permissions: %s", err) 189 | } 190 | 191 | cmd := exec.Command(f.bin, "--target", f.Target, "login", "--team-name", "main", "--concourse-url", f.Endpoint, "--username", username, "--password", password) 192 | _, err = cmd.CombinedOutput() 193 | if err != nil { 194 | t.Errorf("failed to login to concourse: %s", err) 195 | } 196 | } 197 | 198 | func (f *Fly) Workers(t *testing.T) []*ConcourseWorker { 199 | cmd := exec.Command(f.bin, "--target", f.Target, "workers", "--json") 200 | out, err := cmd.CombinedOutput() 201 | if err != nil { 202 | t.Errorf("failed to list workers: %s", err) 203 | } 204 | 205 | r := bytes.NewReader(out) 206 | 207 | var workers []*ConcourseWorker 208 | err = json.NewDecoder(r).Decode(&workers) 209 | if err != nil { 210 | t.Fatalf("failed to deserialize workers: %s", err) 211 | } 212 | return workers 213 | } 214 | 215 | type ConcourseWorker struct { 216 | Platform string `json:"platform"` 217 | State string `json:"state"` 218 | } 219 | -------------------------------------------------------------------------------- /test/module_test.go: -------------------------------------------------------------------------------- 1 | package module_test 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | asg "github.com/telia-oss/terraform-aws-asg/v3/test" 10 | concourse "github.com/telia-oss/terraform-aws-concourse/v3/test" 11 | 12 | "github.com/gruntwork-io/terratest/modules/aws" 13 | "github.com/gruntwork-io/terratest/modules/packer" 14 | "github.com/gruntwork-io/terratest/modules/random" 15 | "github.com/gruntwork-io/terratest/modules/terraform" 16 | ) 17 | 18 | var amiID = flag.String("ami-id", "", "Concourse AMI ID.") 19 | 20 | func TestModule(t *testing.T) { 21 | tests := []struct { 22 | description string 23 | directory string 24 | name string 25 | password string 26 | region string 27 | expected concourse.Expectations 28 | }{ 29 | { 30 | description: "basic example", 31 | directory: "../examples/basic", 32 | name: fmt.Sprintf("concourse-basic-test-%s", random.UniqueId()), 33 | password: random.UniqueId(), 34 | region: "eu-west-1", 35 | expected: concourse.Expectations{ 36 | Version: "5.6.0", 37 | WorkerVersion: "2.2", 38 | ATCAutoscaling: asg.Expectations{ 39 | MinSize: 1, 40 | MaxSize: 2, 41 | DesiredCapacity: 1, 42 | InstanceType: "t3.small", 43 | InstanceTags: map[string]string{ 44 | "terraform": "True", 45 | "environment": "dev", 46 | }, 47 | UserData: []string{ 48 | `Environment="CONCOURSE_GITHUB_CLIENT_ID=sm:///concourse-deployment/github-oauth-client-id"`, 49 | `Environment="CONCOURSE_GITHUB_CLIENT_SECRET=sm:///concourse-deployment/github-oauth-client-secret"`, 50 | `Environment="CONCOURSE_MAIN_TEAM_GITHUB_USER=itsdalmo"`, 51 | `Environment="CONCOURSE_MAIN_TEAM_GITHUB_TEAM=telia-oss:concourse-owners"`, 52 | `Environment="CONCOURSE_MAIN_TEAM_LOCAL_USER=admin"`, 53 | `Environment="CONCOURSE_POSTGRES_PORT=5439"`, 54 | `Environment="CONCOURSE_POSTGRES_USER=superuser"`, 55 | `Environment="CONCOURSE_POSTGRES_PASSWORD=dolphins"`, 56 | `Environment="CONCOURSE_POSTGRES_DATABASE=main"`, 57 | `Environment="CONCOURSE_LOG_LEVEL=info"`, 58 | `Environment="CONCOURSE_TSA_LOG_LEVEL=info"`, 59 | `Environment="CONCOURSE_TSA_HOST_KEY=/concourse/keys/web/tsa_host_key"`, 60 | `Environment="CONCOURSE_TSA_AUTHORIZED_KEYS=/concourse/keys/web/authorized_worker_keys"`, 61 | `Environment="CONCOURSE_SESSION_SIGNING_KEY=/concourse/keys/web/session_signing_key"`, 62 | `Environment="CONCOURSE_ENCRYPTION_KEY="`, 63 | `Environment="CONCOURSE_OLD_ENCRYPTION_KEY="`, 64 | `Environment="CONCOURSE_AWS_SECRETSMANAGER_REGION=eu-west-1"`, 65 | `Environment="CONCOURSE_SECRET_CACHE_ENABLED=true"`, 66 | }, 67 | IsGzippedUserData: true, 68 | }, 69 | WorkerAutoscaling: asg.Expectations{ 70 | MinSize: 1, 71 | MaxSize: 3, 72 | DesiredCapacity: 1, 73 | InstanceType: "t3.large", 74 | InstanceTags: map[string]string{ 75 | "terraform": "True", 76 | "environment": "dev", 77 | }, 78 | UserData: []string{ 79 | `Environment="CONCOURSE_TEAM="`, 80 | `Environment="CONCOURSE_BIND_IP=0.0.0.0"`, 81 | `Environment="CONCOURSE_LOG_LEVEL=info"`, 82 | `Environment="CONCOURSE_WORK_DIR=/concourse"`, 83 | `Environment="CONCOURSE_BAGGAGECLAIM_BIND_IP=0.0.0.0"`, 84 | `Environment="CONCOURSE_BAGGAGECLAIM_LOG_LEVEL=info"`, 85 | `Environment="CONCOURSE_TSA_PUBLIC_KEY=/concourse/keys/worker/tsa_host_key.pub"`, 86 | `Environment="CONCOURSE_TSA_WORKER_PRIVATE_KEY=/concourse/keys/worker/worker_key"`, 87 | `Environment="CONCOURSE_REBALANCE_INTERVAL=30m"`, 88 | `ExecStartPre=/bin/bash -c "/bin/systemctl set-environment CONCOURSE_NAME=$(curl -L http://169.254.169.254/latest/meta-data/instance-id)"`, 89 | `ExecStart=/usr/local/concourse/bin/concourse worker`, 90 | `ExecStop=/usr/local/concourse/bin/concourse retire-worker`, 91 | `ExecStop=/bin/bash -c "while pgrep concourse >> /dev/null; do echo draining worker... && sleep 5; done; echo done draining!"`, 92 | }, 93 | IsGzippedUserData: true, 94 | }, 95 | }, 96 | }, 97 | } 98 | 99 | for _, tc := range tests { 100 | tc := tc // Source: https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca008721 101 | t.Run(tc.description, func(t *testing.T) { 102 | t.Parallel() 103 | 104 | amiID := *amiID 105 | if amiID == "" { 106 | amiID = packer.BuildArtifact(t, &packer.Options{ 107 | Template: "../packer/template.json", 108 | 109 | Vars: map[string]string{ 110 | "template_version": "dev", 111 | }, 112 | 113 | Only: "amazon-ebs", 114 | }) 115 | defer aws.DeleteAmiAndAllSnapshots(t, tc.region, amiID) 116 | } 117 | 118 | options := &terraform.Options{ 119 | TerraformDir: tc.directory, 120 | 121 | Vars: map[string]interface{}{ 122 | // aws_db_subnet_group requires a lowercase name. 123 | "name_prefix": strings.ToLower(tc.name), 124 | "concourse_admin_password": tc.password, 125 | "packer_ami": amiID, 126 | "region": tc.region, 127 | }, 128 | 129 | EnvVars: map[string]string{ 130 | "AWS_DEFAULT_REGION": tc.region, 131 | }, 132 | } 133 | 134 | defer terraform.Destroy(t, options) 135 | terraform.InitAndApply(t, options) 136 | 137 | concourse.RunTestSuite(t, 138 | terraform.Output(t, options, "endpoint"), 139 | terraform.Output(t, options, "atc_asg_id"), 140 | terraform.Output(t, options, "worker_asg_id"), 141 | "admin", 142 | tc.password, 143 | tc.region, 144 | tc.expected, 145 | ) 146 | }) 147 | } 148 | } 149 | --------------------------------------------------------------------------------