diff --git a/.gitignore b/.gitignore index 3aa5e24..e3b4830 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,12 @@ bin # terraform **/.terraform/* +*.auto.tfvars +*.terraform.lock.hcl *.tfstate *.tfstate.* -*.auto.tfvars +*_override.tf +*_override.tf.json crash.log override.tf override.tf.json -*_override.tf -*_override.tf.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..31dd5e5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: git://github.com/antonbabenko/pre-commit-terraform + rev: v1.47.0 + hooks: + - id: terraform_docs + - id: terraform_fmt + - repo: git://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-merge-conflict diff --git a/README.md b/README.md index a9ae516..3707128 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### Moot - A Serverless Release Dashboard +# Moot - A Serverless Release Dashboard AWS Serverless solution deployed with Terraform which implements a single-page-application dashboard. This dashboard creates releases that are intended to trigger continuous integration (CI) production deploy pipelines. All that is needed to kick off a release is a version number. @@ -17,13 +17,26 @@ This solution utilises the following services: - S3 + Cloudfront (frontend) - SSM Parameter Store (secrets management) -#### Installation +## Requirements + +The following tools must be installed in order to fully deploy Moot + +- [yarn](https://yarnpkg.com/getting-started/install) -- used to build the frontend locally +- [awscli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) -- used to aws s3 sync the frontend assets +- [go](https://golang.org/doc/install#download) -- requires at least version 16 because of `go modules` + +## Installation + +The below snippet will fully deploy the dashboard (backend + frontend). In this instance, I am deploying to a cheap Route53 domain I purchased for testing purposes (`moot.link`). + +If your AWS account does not have a Route53 hosted zone, remove the `hosted_zone_name` and `fqdn_alias` lines to use Cloudfront's default certificate and dns. ```hcl module "moot" { source = "github.com/seanturner026/moot.git" name = "moot" + aws_profile = "default" admin_user_email = var.admin_user_email enable_delete_admin_user = false github_token = var.github_token @@ -36,24 +49,30 @@ module "moot" { } ``` -#### Workflows +## Workflows - Standard Deploy: Merges the HEAD branch into the BASE (e.g. main) branch, creates release based on BASE branch - Hotfix Deploy: Creates release based on the BASE branch - -#### Repositories View +## Repositories View ![alt text](https://github.com/seanturner026/moot/blob/main/assets/repositories.png?raw=true) -#### Add Repository View +## Add Repository View ![alt text](https://github.com/seanturner026/moot/blob/main/assets/repositories-add.png?raw=true) -#### Users View +## Users View ![alt text](https://github.com/seanturner026/moot/blob/main/assets/users.png?raw=true) -## Terraform Providers +## Terraform Information + + +## Requirements + +No requirements. + +## Providers | Name | Version | |------|---------| @@ -62,23 +81,26 @@ module "moot" { | external | n/a | | null | n/a | -## Terraform Inputs +## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | admin\_user\_email | Controls the creation of an admin user that is required to initially gain access to the
dashboard.

If access to the dashboard is completely lost, do the following
• `var.enable_delete_admin_user = true`
• `terraform apply`
• `var.enable_delete_admin_user = false`
• `terraform apply`

If the initial admin user should no longer be able to access the dashboard, revoke access by
setting `var.enable_delete_admin_user = true` and running `terraform apply` | `string` | `""` | no | +| aws\_profile | AWS Profile Name from `~/.aws/config that can be used for local execution. This profile is used
to preform the following actions:

• `aws s3 sync`: Sync bundle produced by `yarn` to build to s3
• `cognito-idp admin-create-user`: Creates an admin cognito user for dashboard access
• `cognito-idp admin-delete-user`: Deletes an admin cognito user if the user should not
have access to the dashboard anymore, OR, if there is no way for the user to regain access.
• `cognito-idp list-users`: Obtains the admin user's ID in order to write the ID to the
DynamodDB table.
` | `string` | `""` | no | | enable\_api\_gateway\_access\_logs | Enables API Gateway access logging to cloudwatch for the default stage. | `bool` | `false` | no | | enable\_delete\_admin\_user | Destroys the admin user.

Set this value to true to destroy the user, and to false to recreate the user. | `bool` | `false` | no | | fqdn\_alias | ALIAS for the Cloudfront distribution, S3, Cognito and API Gateway. Must be in the form of
`example.com`. | `string` | `""` | no | -| github\_token | Token for Github. | `string` | `""` | no | -| gitlab\_token | Token for Gitlab. | `string` | `""` | no | +| github\_token | Token for Github. | `string` | `"42"` | no | +| gitlab\_token | Token for Gitlab. | `string` | `"42"` | no | | hosted\_zone\_name | Name of AWS Route53 Hosted Zone for DNS. | `string` | `""` | no | | name | Name to be applied to all resources. | `string` | `"release_dashboard"` | no | -| slack\_webhook\_url | URL to send slack message payloads to. | `string` | `""` | no | +| slack\_webhook\_url | URL to send slack message payloads to. | `string` | `"42"` | no | | tags | Map of tags to be applied to resources. | `map(string)` | `{}` | no | -## Terraform Outputs +## Outputs | Name | Description | |------|-------------| | cloudfront\_domain\_name | FQDN of Cloudfront Distribution that can be used for DNS. | + + diff --git a/locals.tf b/locals.tf index b0b16b7..a680dd6 100644 --- a/locals.tf +++ b/locals.tf @@ -1,4 +1,11 @@ locals { + + aws_profile = var.aws_profile != "" ? var.aws_profile : "default" + frontend_module_comprehension = [for module in jsondecode(file("${path.root}/.terraform/modules/modules.json"))["Modules"] : module if length(regexall("vuejs_frontend", module.Key)) > 0][0] + frontend_module_path = "${path.root}/${local.frontend_module_comprehension.Dir}" + main_module_name = split(".terraform/modules/", path.module)[1] + main_module_path = "./.terraform/modules/${local.main_module_name}" + ssm_parameters = { client_pool_secret = { description = "Cognito User Pool client secret." @@ -22,11 +29,6 @@ locals { lambda_binary_exists = { for key, _ in local.lambdas : key => fileexists("${path.module}/bin/${key}") } } - frontend_module_comprehension = [for module in jsondecode(file("${path.root}/.terraform/modules/modules.json"))["Modules"] : module if length(regexall("vuejs_frontend", module.Key)) > 0][0] - frontend_module_path = "${path.root}/${local.frontend_module_comprehension.Dir}" - main_module_path = "./.terraform/modules/${local.main_module_name}" - main_module_name = split(".terraform/modules/", path.module)[1] - lambdas = { auth = { description = "Administrates user login, token refreshes, and password resets." diff --git a/r_cognito.tf b/r_cognito.tf index 15c6651..6a6d654 100644 --- a/r_cognito.tf +++ b/r_cognito.tf @@ -63,19 +63,3 @@ resource "aws_cognito_identity_pool" "this" { tags = var.tags } - -resource "null_resource" "create_admin_user" { - count = var.admin_user_email != "" && !var.enable_delete_admin_user ? 1 : 0 - - provisioner "local-exec" { - command = "aws --region ${data.aws_region.current.name} cognito-idp admin-create-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email} --user-attributes Name=email,Value=${var.admin_user_email}" - } -} - -resource "null_resource" "delete_admin_user" { - count = var.admin_user_email != "" && var.enable_delete_admin_user ? 1 : 0 - - provisioner "local-exec" { - command = "aws --region ${data.aws_region.current.name} cognito-idp admin-delete-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email}" - } -} diff --git a/r_lambda.tf b/r_lambda.tf index 32db45d..dc423eb 100644 --- a/r_lambda.tf +++ b/r_lambda.tf @@ -1,62 +1,3 @@ -resource "null_resource" "go_setup" { - - triggers = { - hash_go_mod = filemd5("${local.main_module_path}/go.mod") - hash_go_sum = filemd5("${local.main_module_path}/go.sum") - } - - provisioner "local-exec" { - command = "cp -f ${local.main_module_path}/go.mod ." - } - - provisioner "local-exec" { - command = "cp -f ${local.main_module_path}/go.sum ." - } -} - -resource "null_resource" "lambda_build" { - for_each = local.lambdas - depends_on = [null_resource.go_setup] - - triggers = { - binary_exists = local.null.lambda_binary_exists[each.key] - - hash_main = join("", [ - for file in fileset("${path.module}/cmd/${each.key}", "*.go") : filemd5("${path.module}/cmd/${each.key}/${file}") - ]) - - hash_util = join("", [ - for file in fileset("${path.module}/internal/util", "*.go") : filemd5("${path.module}/internal/util/${file}") - ]) - } - - provisioner "local-exec" { - command = "export GO111MODULE=on" - } - - provisioner "local-exec" { - command = "cd ${local.main_module_path} && GOOS=linux go build -ldflags '-s -w' -o ./bin/${each.key} ./cmd/${each.key}/." - } -} - -resource "null_resource" "lambda_test" { - for_each = local.lambdas - - triggers = { - hash_main = join("", [ - for file in fileset("${path.module}/cmd/${each.key}", "*.go") : filemd5("${path.module}/cmd/${each.key}/${file}") - ]) - - hash_util = join("", [ - for file in fileset("${path.module}/internal/util", "*.go") : filemd5("${path.module}/internal/util/${file}") - ]) - } - - provisioner "local-exec" { - command = "cd ${local.main_module_path} && go test ./cmd/${each.key}" - } -} - resource "aws_lambda_function" "this" { depends_on = [null_resource.lambda_build, null_resource.lambda_test] for_each = local.lambdas diff --git a/r_null.tf b/r_null.tf index 5e2fd28..abe3876 100644 --- a/r_null.tf +++ b/r_null.tf @@ -5,18 +5,102 @@ resource "null_resource" "build_frontend" { } provisioner "local-exec" { - command = "cd ${local.frontend_module_path} && yarn install" + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.frontend_module_path} && yarn install" } provisioner "local-exec" { - command = "cd ${local.frontend_module_path} && echo \"VUE_APP_API_GATEWAY_ENDPOINT=${aws_apigatewayv2_api.this.api_endpoint}\" > .env" + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.frontend_module_path} && echo \"VUE_APP_API_GATEWAY_ENDPOINT=${aws_apigatewayv2_api.this.api_endpoint}\" > .env" } provisioner "local-exec" { - command = "cd ${local.frontend_module_path} && yarn build" + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.frontend_module_path} && yarn build" } provisioner "local-exec" { - command = "cd ${local.frontend_module_path} && aws s3 sync --cache-control 'max-age=604800' dist/ s3://${aws_s3_bucket.this.id}" + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.frontend_module_path} && AWS_DEFAULT_PROFILE=${local.aws_profile} aws s3 sync --cache-control 'max-age=604800' dist/ s3://${aws_s3_bucket.this.id}" + } +} + +resource "null_resource" "go_setup" { + + triggers = { + hash_go_mod = filemd5("${local.main_module_path}/go.mod") + hash_go_sum = filemd5("${local.main_module_path}/go.sum") + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "cp -f ${local.main_module_path}/go.mod ." + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "cp -f ${local.main_module_path}/go.sum ." + } +} + +resource "null_resource" "lambda_build" { + for_each = local.lambdas + depends_on = [null_resource.go_setup] + + triggers = { + binary_exists = local.null.lambda_binary_exists[each.key] + + hash_main = join("", [ + for file in fileset("${path.module}/cmd/${each.key}", "*.go") : filemd5("${path.module}/cmd/${each.key}/${file}") + ]) + + hash_util = join("", [ + for file in fileset("${path.module}/internal/util", "*.go") : filemd5("${path.module}/internal/util/${file}") + ]) + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "export GO111MODULE=on" + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.main_module_path} && GOOS=linux go build -ldflags '-s -w' -o ./bin/${each.key} ./cmd/${each.key}/." + } +} + +resource "null_resource" "lambda_test" { + for_each = local.lambdas + + triggers = { + hash_main = join("", [ + for file in fileset("${path.module}/cmd/${each.key}", "*.go") : filemd5("${path.module}/cmd/${each.key}/${file}") + ]) + + hash_util = join("", [ + for file in fileset("${path.module}/internal/util", "*.go") : filemd5("${path.module}/internal/util/${file}") + ]) + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "cd ${local.main_module_path} && go test ./cmd/${each.key}" + } +} + +resource "null_resource" "create_admin_user" { + count = var.admin_user_email != "" && !var.enable_delete_admin_user ? 1 : 0 + + provisioner "local-exec" { + command = "aws --region ${data.aws_region.current.name} cognito-idp admin-create-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email} --user-attributes Name=email,Value=${var.admin_user_email}" + } +} + +resource "null_resource" "delete_admin_user" { + count = var.admin_user_email != "" && var.enable_delete_admin_user ? 1 : 0 + + provisioner "local-exec" { + command = "aws --region ${data.aws_region.current.name} cognito-idp admin-delete-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email}" } } diff --git a/terraform_examples/cloudfront_dns/.terraform.lock.hcl b/terraform_examples/cloudfront_dns/.terraform.lock.hcl deleted file mode 100644 index 80c6ea3..0000000 --- a/terraform_examples/cloudfront_dns/.terraform.lock.hcl +++ /dev/null @@ -1,75 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/archive" { - version = "2.2.0" - hashes = [ - "h1:2K5LQkuWRS2YN1/YoNaHn9MAzjuTX8Gaqy6i8Mbfv8Y=", - "zh:06bd875932288f235c16e2237142b493c2c2b6aba0e82e8c85068332a8d2a29e", - "zh:0c681b481372afcaefddacc7ccdf1d3bb3a0c0d4678a526bc8b02d0c331479bc", - "zh:100fc5b3fc01ea463533d7bbfb01cb7113947a969a4ec12e27f5b2be49884d6c", - "zh:55c0d7ddddbd0a46d57c51fcfa9b91f14eed081a45101dbfc7fd9d2278aa1403", - "zh:73a5dd68379119167934c48afa1101b09abad2deb436cd5c446733e705869d6b", - "zh:841fc4ac6dc3479981330974d44ad2341deada8a5ff9e3b1b4510702dfbdbed9", - "zh:91be62c9b41edb137f7f835491183628d484e9d6efa82fcb75cfa538c92791c5", - "zh:acd5f442bd88d67eb948b18dc2ed421c6c3faee62d3a12200e442bfff0aa7d8b", - "zh:ad5720da5524641ad718a565694821be5f61f68f1c3c5d2cfa24426b8e774bef", - "zh:e63f12ea938520b3f83634fc29da28d92eed5cfbc5cc8ca08281a6a9c36cca65", - "zh:f6542918faa115df46474a36aabb4c3899650bea036b5f8a5e296be6f8f25767", - ] -} - -provider "registry.terraform.io/hashicorp/aws" { - version = "3.42.0" - constraints = ">= 3.37.0" - hashes = [ - "h1:C6/yDp6BhuDFx0qdkBuJj/OWUJpAoraHTJaU6ac38Rw=", - "zh:126c856a6eedddd8571f161a826a407ba5655a37a6241393560a96b8c4beca1a", - "zh:1a4868e6ac734b5fc2e79a4a889d176286b66664aad709435aa6acee5871d5b0", - "zh:40fed7637ab8ddeb93bef06aded35d970f0628025b97459ae805463e8aa0a58a", - "zh:68def3c0a5a1aac1db6372c51daef858b707f03052626d3427ac24cba6f2014d", - "zh:6db7ec9c8d1803a0b6f40a664aa892e0f8894562de83061fa7ac1bc51ff5e7e5", - "zh:7058abaad595930b3f97dc04e45c112b2dbf37d098372a849081f7081da2fb52", - "zh:8c25adb15a19da301c478aa1f4a4d8647cabdf8e5dae8331d4490f80ea718c26", - "zh:8e129b847401e39fcbc54817726dab877f36b7f00ff5ed76f7b43470abe99ff9", - "zh:d268bb267a2d6b39df7ddee8efa7c1ef7a15cf335dfa5f2e64c9dae9b623a1b8", - "zh:d6eeb3614a0ab50f8e9ab5666ae5754ea668ce327310e5b21b7f04a18d7611a8", - "zh:f5d3c58055dff6e38562b75d3edc908cb2f1e45c6914f6b00f4773359ce49324", - ] -} - -provider "registry.terraform.io/hashicorp/external" { - version = "2.1.0" - hashes = [ - "h1:LTl5CGW8wiIEe16AC4MtXN/95xWWNDbap70zJsBTk0w=", - "zh:0d83ffb72fbd08986378204a7373d8c43b127049096eaf2765bfdd6b00ad9853", - "zh:7577d6edc67b1e8c2cf62fe6501192df1231d74125d90e51d570d586d95269c5", - "zh:9c669ded5d5affa4b2544952c4b6588dfed55260147d24ced02dca3a2829f328", - "zh:a404d46f2831f90633947ab5d57e19dbfe35b3704104ba6ec80bcf50b058acfd", - "zh:ae1caea1c936d459ceadf287bb5c5bd67b5e2a7819df6f5c4114b7305df7f822", - "zh:afb4f805477694a4b9dde86b268d2c0821711c8aab1c6088f5f992228c4c06fb", - "zh:b993b4a1de8a462643e78f4786789e44ce5064b332fee1cb0d6250ed085561b8", - "zh:c84b2c13fa3ea2c0aa7291243006d560ce480a5591294b9001ce3742fc9c5791", - "zh:c8966f69b7eccccb771704fd5335923692eccc9e0e90cb95d14538fe2e92a3b8", - "zh:d5fe68850d449b811e633a300b114d0617df6d450305e8251643b4d143dc855b", - "zh:ddebfd1e674ba336df09b1f27bbaa0e036c25b7a7087dc8081443f6e5954028b", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.1.0" - hashes = [ - "h1:xhbHC6in3nQryvTQBWKxebi3inG5OCgHgc4fRxL0ymc=", - "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", - "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", - "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", - "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", - "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", - "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", - "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", - "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", - "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", - "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", - "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", - ] -} diff --git a/terraform_examples/cloudfront_dns/main.tf b/terraform_examples/cloudfront_dns/main.tf index a16e0f1..ee7bf1b 100644 --- a/terraform_examples/cloudfront_dns/main.tf +++ b/terraform_examples/cloudfront_dns/main.tf @@ -2,6 +2,7 @@ module "moot" { source = "github.com/seanturner026/moot.git" name = "moot" + aws_profile = "default" admin_user_email = var.admin_user_email enable_delete_admin_user = false github_token = var.github_token diff --git a/terraform_examples/complete/main.tf b/terraform_examples/complete/main.tf index 3913786..419dd75 100644 --- a/terraform_examples/complete/main.tf +++ b/terraform_examples/complete/main.tf @@ -2,6 +2,7 @@ module "moot" { source = "github.com/seanturner026/moot.git" name = "moot" + aws_profile = "default" admin_user_email = var.admin_user_email enable_delete_admin_user = false github_token = var.github_token diff --git a/variables.tf b/variables.tf index 87facc6..e2eb4bd 100644 --- a/variables.tf +++ b/variables.tf @@ -10,6 +10,22 @@ variable "tags" { default = {} } +variable "aws_profile" { + type = string + description = <<-DESC + AWS Profile Name from `~/.aws/config that can be used for local execution. This profile is used + to preform the following actions: + + • `aws s3 sync`: Sync bundle produced by `yarn` to build to s3 + • `cognito-idp admin-create-user`: Creates an admin cognito user for dashboard access + • `cognito-idp admin-delete-user`: Deletes an admin cognito user if the user should not + have access to the dashboard anymore, OR, if there is no way for the user to regain access. + • `cognito-idp list-users`: Obtains the admin user's ID in order to write the ID to the + DynamodDB table. + DESC + default = "" +} + variable "admin_user_email" { type = string description = <<-DESC