diff --git a/.config/.checkov.yml b/.config/.checkov.yml index a6266ab..59fa647 100644 --- a/.config/.checkov.yml +++ b/.config/.checkov.yml @@ -1,15 +1,13 @@ download-external-modules: False evaluate-variables: true -file: -- 'tf.json' +directory: +- ./ framework: -- terraform_plan +- terraform skip-check: - CKV2_GCP* - CKV_AZURE* - CKV2_AZURE* -repo-root-for-plan-enrichment: -- '.' summary-position: bottom output: 'cli' compact: True diff --git a/.config/functional_tests/post-entrypoint-helpers.sh b/.config/functional_tests/post-entrypoint-helpers.sh index 9ed42cd..5bb1b31 100644 --- a/.config/functional_tests/post-entrypoint-helpers.sh +++ b/.config/functional_tests/post-entrypoint-helpers.sh @@ -1,6 +1,6 @@ #!/bin/bash ## NOTE: this script runs at the end of functional test -## Use this to load any configurations after the functional test +## Use this to load any configurations after the functional test ## TIPS: avoid modifying the .project_automation/functional_test/entrypoint.sh ## migrate any customization you did on entrypoint.sh to this helper script echo "Executing Post-Entrypoint Helpers" \ No newline at end of file diff --git a/.config/functional_tests/pre-entrypoint-helpers.sh b/.config/functional_tests/pre-entrypoint-helpers.sh index a1bc78c..baa7ca5 100644 --- a/.config/functional_tests/pre-entrypoint-helpers.sh +++ b/.config/functional_tests/pre-entrypoint-helpers.sh @@ -1,6 +1,20 @@ #!/bin/bash ## NOTE: this script runs at the start of functional test -## use this to load any configuration before the functional test +## use this to load any configuration before the functional test ## TIPS: avoid modifying the .project_automation/functional_test/entrypoint.sh ## migrate any customization you did on entrypoint.sh to this helper script -echo "Executing Pre-Entrypoint Helpers" \ No newline at end of file +echo "Executing Pre-Entrypoint Helpers" + +#********** TFC Env Vars ************* +echo "Load env vars" +export AWS_DEFAULT_REGION=us-west-2 +export TFE_TOKEN=`aws secretsmanager get-secret-value --secret-id abp/hcp/token | jq -r ".SecretString"` + +#********** Get tfvars from SSM ************* +echo "Get *.tfvars from SSM parameter" +aws ssm get-parameter \ + --name "/abp/hcp/functional/terraform_test.tfvars" \ + --with-decryption \ + --query "Parameter.Value" \ + --output "text" \ + --region "us-west-2" > ./tests/terraform.auto.tfvars diff --git a/.config/static_tests/post-entrypoint-helpers.sh b/.config/static_tests/post-entrypoint-helpers.sh index 25fbae1..d63be05 100644 --- a/.config/static_tests/post-entrypoint-helpers.sh +++ b/.config/static_tests/post-entrypoint-helpers.sh @@ -1,6 +1,6 @@ #!/bin/bash ## NOTE: this script runs at the end of static test -## Use this to load any configurations after the static test +## Use this to load any configurations after the static test ## TIPS: avoid modifying the .project_automation/static_test/entrypoint.sh ## migrate any customization you did on entrypoint.sh to this helper script echo "Executing Post-Entrypoint Helpers" \ No newline at end of file diff --git a/.config/static_tests/pre-entrypoint-helpers.sh b/.config/static_tests/pre-entrypoint-helpers.sh index d24d7a6..87ab74d 100644 --- a/.config/static_tests/pre-entrypoint-helpers.sh +++ b/.config/static_tests/pre-entrypoint-helpers.sh @@ -1,6 +1,6 @@ #!/bin/bash ## NOTE: this script runs at the start of static test -## use this to load any configuration before the static test +## use this to load any configuration before the static test ## TIPS: avoid modifying the .project_automation/static_test/entrypoint.sh ## migrate any customization you did on entrypoint.sh to this helper script echo "Executing Pre-Entrypoint Helpers" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 10712dc..4a1f3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ plan.out.json # Local .terraform directories **/.terraform/* +**/.terraform # .tfstate files *.tfstate diff --git a/.header.md b/.header.md index 5e652f5..857d67c 100644 --- a/.header.md +++ b/.header.md @@ -1,7 +1,20 @@ -# Terraform Module Project +# Terraform Cloud Agent on Amazon ECS -:no_entry_sign: Do not edit this readme.md file. To learn how to change this content and work with this repository, refer to CONTRIBUTING.md +This solution creates self-hosted HashiCorp Cloud Platform (HCP) Terraform agent on Amazon ECS cluster. HCP Terraform allows you to manage isolated, private, or on-premises infrastructure using self-hosted HCP Terraform agents. The agent polls HCP Terraform or HCP Terraform Enterprise for any changes to your configuration and executes the changes locally, so you do not need to allow public ingress traffic to your resources. Agents allow you to control infrastructure in private environments without modifying your network perimeter. -## Readme Content +## Architecture -This file will contain any instructional information about this module. +![Terraform Cloud Agent on Amazon ECS architecture](/assets/architecture.png) + +## Prerequisites + +To use this module you need to have the following: + +1. [HashiCorp Cloud Platform (HCP) Terraform](https://www.hashicorp.com/products/terraform) subscription +2. Terraform API token with permission to create Terraform agent pool +3. AWS account and credentials to provision resources as mentioned below. +4. VPC with private subnets + +## Getting Started + +Please refer to the [examples](./examples/basic) on how to get started. diff --git a/.project_automation/functional_tests/entrypoint.sh b/.project_automation/functional_tests/entrypoint.sh index 3e9d64b..f6f9a01 100755 --- a/.project_automation/functional_tests/entrypoint.sh +++ b/.project_automation/functional_tests/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash ## WARNING: DO NOT modify the content of entrypoint.sh -# Use ./config/functional_tests/pre-entrypoint-helpers.sh or ./config/functional_tests/post-entrypoint-helpers.sh +# Use ./config/functional_tests/pre-entrypoint-helpers.sh or ./config/functional_tests/post-entrypoint-helpers.sh # to load any customizations or additional configurations ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between @@ -22,7 +22,7 @@ pre_entrypoint() { post_entrypoint() { if [ -f ${PROJECT_PATH}/.config/functional_tests/post-entrypoint-helpers.sh ]; then echo "Post-entrypoint helper found" - source ${PROJECT_PATH}/.config/functional_tests/post-entrypoint-helpers.sh + source ${PROJECT_PATH}/.config/functional_tests/post-entrypoint-helpers.sh echo "Post-entrypoint helper loaded" else echo "Post-entrypoint helper not found - skipped" @@ -32,12 +32,16 @@ post_entrypoint() { #********** Pre-entrypoint helper ************* pre_entrypoint -#********** Checkov Analysis ************* -echo "Running Checkov Analysis" -terraform init -terraform plan -out tf.plan -terraform show -json tf.plan > tf.json -checkov --config-file ${PROJECT_PATH}/.config/checkov.yml +#********** Functional Test ************* +/bin/bash ${PROJECT_PATH}/.project_automation/functional_tests/functional_tests.sh +if [ $? -eq 0 ] +then + echo "Functional test completed" + EXIT_CODE=0 +else + echo "Functional test failed" + EXIT_CODE=1 +fi #********** Post-entrypoint helper ************* post_entrypoint diff --git a/.project_automation/functional_tests/functional_tests.sh b/.project_automation/functional_tests/functional_tests.sh index f18c8ac..deb3135 100644 --- a/.project_automation/functional_tests/functional_tests.sh +++ b/.project_automation/functional_tests/functional_tests.sh @@ -20,7 +20,7 @@ if test -f ${MANDATORY_TEST_PATH}; then else echo "File ${MANDATORY_TEST_PATH} not found. You must include at least one test run in file ${MANDATORY_TEST_PATH}" (exit 1) -fi +fi if [ $? -eq 0 ]; then echo "Terraform Test Successfull" diff --git a/.project_automation/static_tests/entrypoint.sh b/.project_automation/static_tests/entrypoint.sh index fe97343..abfe382 100755 --- a/.project_automation/static_tests/entrypoint.sh +++ b/.project_automation/static_tests/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash ## WARNING: DO NOT modify the content of entrypoint.sh -# Use ./config/static_tests/pre-entrypoint-helpers.sh or ./config/static_tests/post-entrypoint-helpers.sh +# Use ./config/static_tests/pre-entrypoint-helpers.sh or ./config/static_tests/post-entrypoint-helpers.sh # to load any customizations or additional configurations ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between @@ -22,7 +22,7 @@ pre_entrypoint() { post_entrypoint() { if [ -f ${PROJECT_PATH}/.config/static_tests/post-entrypoint-helpers.sh ]; then echo "Post-entrypoint helper found" - source ${PROJECT_PATH}/.config/static_tests/post-entrypoint-helpers.sh + source ${PROJECT_PATH}/.config/static_tests/post-entrypoint-helpers.sh echo "Post-entrypoint helper loaded" else echo "Post-entrypoint helper not found - skipped" diff --git a/CODEOWNERS b/CODEOWNERS index b713cc0..7468ed5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,4 @@ -* @aws-ia/aws-ia +gautam.baghel@hashicorp.com +dan.barr@hashicorp.com +manuchn@amazon.com +wellsiau@amazon.com \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f992c36..35e54f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ For best practices and information on developing with Terraform, see the [I&A Mo ## Contributing Code -In order to contibute code to this repository, you must submit a *[Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)*. To do so, you must *[fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo)* this repostiory, make your changes in your forked version and submit a *Pull Request*. +In order to contribute code to this repository, you must submit a *[Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)*. To do so, you must *[fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo)* this repository, make your changes in your forked version and submit a *Pull Request*. ## Writing Documentation @@ -20,7 +20,7 @@ README.md is automatically generated by pulling in content from other files. For Pull Requests (PRs) submitted against this repository undergo a series of static and functional checks. -> :exclamation: Note: Failures during funtional or static checks will prevent a pull request from being accepted. +> :exclamation: Note: Failures during functional or static checks will prevent a pull request from being accepted. It is a best practice to perform these checks locally prior to submitting a pull request. @@ -32,7 +32,7 @@ It is a best practice to perform these checks locally prior to submitting a pull - Checkov - Terratest -> :bangbang: The readme.md file will be created after all checks have completed successfuly, it is recommended that you install terraform-docs locally in order to preview your readme.md file prior to publication. +> :bangbang: The README.md file will be created after all checks have completed successfully, it is recommended that you install terraform-docs locally in order to preview your readme.md file prior to publication. ## Install the required tools @@ -40,7 +40,7 @@ Prerequisites: - [Python](https://docs.python.org/3/using/index.html) - [Pip](https://pip.pypa.io/en/stable/installation/) -- [golang](https://go.dev/doc/install) (for macos you can use `brew`) +- [golang](https://go.dev/doc/install) (for macOS you can use `brew`) - [tflint](https://github.com/terraform-linters/tflint) - [tfsec](https://aquasecurity.github.io/tfsec/v1.0.11/) - [Markdown Lint](https://github.com/markdownlint/markdownlint) diff --git a/README.md b/README.md index 56f16e4..7c7696e 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,45 @@ -# Terraform Module Project +# Terraform Cloud Agent on Amazon ECS -:no\_entry\_sign: Do not edit this readme.md file. To learn how to change this content and work with this repository, refer to CONTRIBUTING.md +This solution creates self-hosted HashiCorp Cloud Platform (HCP) Terraform agent on Amazon ECS cluster. HCP Terraform allows you to manage isolated, private, or on-premises infrastructure using self-hosted HCP Terraform agents. The agent polls HCP Terraform or HCP Terraform Enterprise for any changes to your configuration and executes the changes locally, so you do not need to allow public ingress traffic to your resources. Agents allow you to control infrastructure in private environments without modifying your network perimeter. -## Readme Content +## Architecture -This file will contain any instructional information about this module. +![Terraform Cloud Agent on Amazon ECS architecture](/assets/architecture.png) + +## Prerequisites + +To use this module you need to have the following: + +1. [HashiCorp Cloud Platform (HCP) Terraform](https://www.hashicorp.com/products/terraform) subscription +2. Terraform API token with permission to create Terraform agent pool +3. AWS account and credentials to provision resources as mentioned below. +4. VPC with private subnets + +## Getting Started + +Please refer to the [examples](./examples/basic) on how to get started. ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.0 | -| [aws](#requirement\_aws) | >= 5.47.0 | +| [aws](#requirement\_aws) | >= 5.0.0 | | [tfe](#requirement\_tfe) | >= 0.54 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.47.0 | +| [aws](#provider\_aws) | >= 5.0.0 | | [tfe](#provider\_tfe) | >= 0.54 | ## Modules | Name | Source | Version | |------|--------|---------| -| [ecs\_cluster](#module\_ecs\_cluster) | terraform-aws-modules/ecs/aws | ~> 5.0 | +| [ecs\_cluster](#module\_ecs\_cluster) | git::https://github.com/terraform-aws-modules/terraform-aws-ecs | 6b52c965734d95767d8e20d965afcd0db29dae5e | ## Resources @@ -38,24 +51,29 @@ This file will contain any instructional information about this module. | [aws_iam_role.ecs_task_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.ecs_task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.agent_init_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.ssm_access_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy_attachment.ecs_task_execution_role_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.ecs_task_role_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_kms_key.log_ssm_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | | [aws_security_group.hcp_terraform_agent](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group_rule.allow_egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_ssm_parameter.agent_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [tfe_agent_pool.ecs_agent_pool](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/agent_pool) | resource | | [tfe_agent_token.ecs_agent_token](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/agent_token) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.agent_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.agent_init_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_key_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssm_access_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [hcp\_terraform\_org\_name](#input\_hcp\_terraform\_org\_name) | The name of the HCP Terraform or HCP Terraform enterprise organization where the agent pool will be configured. The combination of `hcp_terraform_org_name` and `name` must be unique within an AWS account. | `string` | n/a | yes | +| [hcp\_terraform\_org\_name](#input\_hcp\_terraform\_org\_name) | The name of the HCP Terraform or HCP Terraform Enterprise organization where the agent pool will be configured. The combination of `hcp_terraform_org_name` and `name` must be unique within an AWS account. | `string` | n/a | yes | | [name](#input\_name) | A name to apply to resources. The combination of `name` and `hcp_terraform_org_name` must be unique within an AWS account. | `string` | n/a | yes | -| [subnet\_ids](#input\_subnet\_ids) | IDs of the subnet(s) where agents can be deployed (public subnets required) | `list(string)` | n/a | yes | +| [subnet\_ids](#input\_subnet\_ids) | IDs of the subnet(s) where agents can be deployed | `list(string)` | n/a | yes | | [vpc\_id](#input\_vpc\_id) | ID of the VPC where the cluster is running. | `string` | n/a | yes | | [agent\_auto\_update](#input\_agent\_auto\_update) | Whether the agent should auto-update. Valid values are minor, patch, and disabled. | `string` | `"minor"` | no | | [agent\_cidr\_blocks](#input\_agent\_cidr\_blocks) | CIDR blocks to allow the agent to communicate with the HCP Terraform instance. | `list(string)` |
[
"0.0.0.0/0"
]
| no | @@ -65,15 +83,17 @@ This file will contain any instructional information about this module. | [agent\_log\_level](#input\_agent\_log\_level) | The logging verbosity for the agent. Valid values are trace, debug, info (default), warn, and error. | `string` | `"info"` | no | | [agent\_memory](#input\_agent\_memory) | The amount of memory, in MB, allocated to the agent container(s). | `number` | `512` | no | | [agent\_single\_execution](#input\_agent\_single\_execution) | Whether to use single-execution mode. | `bool` | `true` | no | -| [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | The name of the CloudWatch log group where agent logs will be sent. | `string` | `"/ecs/hcp-terraform-agent"` | no | -| [cloudwatch\_log\_group\_retention](#input\_cloudwatch\_log\_group\_retention) | The number of days to retain logs in the CloudWatch log group. | `number` | `7` | no | -| [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | The name of the CloudWatch log group where agent logs will be sent. | `bool` | `true` | no | +| [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | The name of the CloudWatch log group where agent logs will be sent. | `string` | `"/hcp/hcp-terraform-agent"` | no | +| [cloudwatch\_log\_group\_retention](#input\_cloudwatch\_log\_group\_retention) | The number of days to retain logs in the CloudWatch log group. | `number` | `365` | no | +| [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Whether the CloudWatch log group should be created. | `bool` | `true` | no | | [create\_ecs\_cluster](#input\_create\_ecs\_cluster) | Whether to create a new ECS cluster for the agent. | `bool` | `true` | no | -| [create\_tfe\_agent\_pool](#input\_create\_tfe\_agent\_pool) | Option to omit agent pool/token creation | `bool` | `true` | no | +| [create\_tfe\_agent\_pool](#input\_create\_tfe\_agent\_pool) | Whether to omit agent pool/token creation | `bool` | `true` | no | | [ecs\_cluster\_arn](#input\_ecs\_cluster\_arn) | ARN of the ECS cluster where the agent will be deployed. | `string` | `"arn:aws:ecs:us-west-2:000000000000:cluster/ecs-basic"` | no | | [extra\_env\_vars](#input\_extra\_env\_vars) | Extra environment variables to pass to the agent container. |
list(object({
name = string
value = string
}))
| `[]` | no | -| [hcp\_terraform\_address](#input\_hcp\_terraform\_address) | The HTTPS address of the HCP Terraform or HCP Terraform enterprise instance. | `string` | `"https://app.terraform.io"` | no | +| [hcp\_terraform\_address](#input\_hcp\_terraform\_address) | The HTTPS address of the HCP Terraform or HCP Terraform Enterprise instance. | `string` | `"https://app.terraform.io"` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN of the KMS key to create. If empty, a new key will be created. | `string` | `""` | no | | [num\_agents](#input\_num\_agents) | The number of agent containers to run. | `number` | `1` | no | +| [tags](#input\_tags) | Map of tags to apply to resources deployed by this solution. | `map(any)` | `null` | no | | [task\_policy\_arns](#input\_task\_policy\_arns) | ARN(s) of IAM policies to attach to the agent task. Determines what actions the agent can take without requiring additional AWS credentials. | `list(string)` | `[]` | no | | [tfe\_agent\_pool\_name](#input\_tfe\_agent\_pool\_name) | Terraform agent pool name to be used when agent creation is omitted | `string` | `""` | no | | [tfe\_agent\_token](#input\_tfe\_agent\_token) | Terraform agent token to be used when agent creation is omitted | `string` | `""` | no | @@ -88,6 +108,7 @@ This file will contain any instructional information about this module. | [ecs\_service\_arn](#output\_ecs\_service\_arn) | ARN of the ECS service. | | [ecs\_task\_arn](#output\_ecs\_task\_arn) | ARN of the ECS task definition. | | [ecs\_task\_revision](#output\_ecs\_task\_revision) | Revision number of the ECS task definition. | +| [kms\_key\_arn](#output\_kms\_key\_arn) | The ARN of the created KMS key | | [log\_stream\_prefix](#output\_log\_stream\_prefix) | Prefix for the CloudWatch log stream. | | [security\_group\_id](#output\_security\_group\_id) | ID of the VPC security group attached to the service. | | [security\_group\_name](#output\_security\_group\_name) | Name of the VPC security group attached to the service. | diff --git a/VERSION b/VERSION index ae39fab..45c7a58 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.0 +v0.0.1 diff --git a/assets/architecture.png b/assets/architecture.png new file mode 100644 index 0000000..b2a90a9 Binary files /dev/null and b/assets/architecture.png differ diff --git a/examples/basic/.header.md b/examples/basic/.header.md index e69de29..15dcb86 100644 --- a/examples/basic/.header.md +++ b/examples/basic/.header.md @@ -0,0 +1,5 @@ +# Create Terraform Cloud Agent and ECS Cluster + +This example show how you can run the module to launch dedicated ECS cluster for Terraform Cloud Agent. You need to specify VPC ID which is created via separate VPC module. + +On this example, the ECS Task role will use `arn:aws:iam::aws:policy/ReadOnlyAccess` as it's managed policy. You need to modify this to fit your requirements. diff --git a/examples/basic/README.md b/examples/basic/README.md index dfd8216..8085011 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,4 +1,10 @@ +# Create Terraform Cloud Agent and ECS Cluster + +This example show how you can run the module to launch dedicated ECS cluster for Terraform Cloud Agent. You need to specify VPC ID which is created via separate VPC module. + +On this example, the ECS Task role will use `arn:aws:iam::aws:policy/ReadOnlyAccess` as it's managed policy. You need to modify this to fit your requirements. + ## Requirements | Name | Version | @@ -17,7 +23,7 @@ | Name | Source | Version | |------|--------|---------| | [agent\_pool](#module\_agent\_pool) | ../../ | n/a | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | +| [vpc](#module\_vpc) | git::https://github.com/terraform-aws-modules/terraform-aws-vpc | 25322b6b6be69db6cca7f167d7b0e5327156a595 | ## Resources diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 052e522..845fd1d 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -14,6 +14,7 @@ locals { tags = { Terraform = "true" Environment = "dev" + ManagedBy = "aws-ia/terraform-aws-tf-cloud-agents" } } @@ -28,9 +29,12 @@ module "agent_pool" { agent_image = "hashicorp/tfc-agent:latest" use_spot_instances = true agent_cpu = 512 - agent_memory = 1024 + agent_memory = 2048 + num_agents = 1 vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets + task_policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"] + tags = local.tags } ##################################################################################### @@ -38,8 +42,7 @@ module "agent_pool" { ##################################################################################### module "vpc" { - source = "terraform-aws-modules/vpc/aws" - version = "~> 5.0" + source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc?ref=25322b6b6be69db6cca7f167d7b0e5327156a595" # v5.8.1 name = local.name cidr = local.vpc_cidr diff --git a/examples/basic/variables.tf b/examples/basic/variables.tf index 1f7aa3f..d2c7306 100644 --- a/examples/basic/variables.tf +++ b/examples/basic/variables.tf @@ -6,5 +6,4 @@ variable "hcp_terraform_org_name" { variable "tfe_token" { type = string description = "Terraform token to be used to create the agent pool." - } \ No newline at end of file diff --git a/main.tf b/main.tf index fa694c7..9b32cb7 100644 --- a/main.tf +++ b/main.tf @@ -1,9 +1,11 @@ data "aws_region" "current" {} +data "aws_caller_identity" "current" {} resource "tfe_agent_pool" "ecs_agent_pool" { - count = var.create_tfe_agent_pool ? 1 : 0 - name = "${var.name}-agent-pool" - organization = var.hcp_terraform_org_name + count = var.create_tfe_agent_pool ? 1 : 0 + name = "${var.name}-agent-pool" + organization = var.hcp_terraform_org_name + organization_scoped = false # always explicitly specify the workspace or override by customer } resource "tfe_agent_token" "ecs_agent_token" { @@ -17,11 +19,68 @@ resource "aws_ssm_parameter" "agent_token" { description = "HCP Terraform agent token" type = "SecureString" value = var.create_tfe_agent_pool ? tfe_agent_token.ecs_agent_token[0].token : var.tfe_agent_token + key_id = local.key_id + tags = var.tags } +resource "aws_kms_key" "log_ssm_key" { + count = var.kms_key_arn == "" ? 1 : 0 + description = "KMS key to encrypt the Log groups and SSM parameters" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = data.aws_iam_policy_document.kms_key_policy[0].json +} + +data "aws_iam_policy_document" "kms_key_policy" { + count = var.kms_key_arn == "" ? 1 : 0 + statement { + sid = "Allow access to the key" + effect = "Allow" + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + actions = [ + "kms:*", + ] + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + } + statement { + sid = "Allow CloudWatch logs and SSM access to the key" + effect = "Allow" + principals { + type = "Service" + identifiers = [ + "logs.${data.aws_region.current.name}.amazonaws.com", + "ssm.amazonaws.com" + ] + } + actions = [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ] + resources = [ + "arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:key/*" + ] + } +} + +locals { + key_id = var.kms_key_arn == "" ? aws_kms_key.log_ssm_key[0].arn : var.kms_key_arn +} + + resource "aws_cloudwatch_log_group" "cloudwatch" { - name = "/ecs/hcp-terraform-agents/${var.name}" + count = var.create_cloudwatch_log_group ? 1 : 0 + name = "/hcp/hcp-terraform-agents/ecs/${var.name}" retention_in_days = var.cloudwatch_log_group_retention + kms_key_id = local.key_id + tags = var.tags } resource "aws_ecs_task_definition" "hcp_terraform_agent" { @@ -29,6 +88,7 @@ resource "aws_ecs_task_definition" "hcp_terraform_agent" { cpu = var.agent_cpu memory = var.agent_memory execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] @@ -42,13 +102,11 @@ resource "aws_ecs_task_definition" "hcp_terraform_agent" { name : "hcp-terraform" image : var.agent_image essential : true - cpu : 0 - memory : 256 logConfiguration : { logDriver : "awslogs", options : { awslogs-create-group : "true", - awslogs-group : var.create_cloudwatch_log_group ? aws_cloudwatch_log_group.cloudwatch.name : var.cloudwatch_log_group_name + awslogs-group : var.create_cloudwatch_log_group ? aws_cloudwatch_log_group.cloudwatch[0].name : var.cloudwatch_log_group_name awslogs-region : data.aws_region.current.name awslogs-stream-prefix : "hcp-tf-${var.hcp_terraform_org_name}-${var.name}" } @@ -84,6 +142,7 @@ resource "aws_ecs_task_definition" "hcp_terraform_agent" { } ] ) + tags = var.tags } resource "aws_ecs_service" "hcp_terraform_agent" { @@ -103,7 +162,7 @@ resource "aws_ecs_service" "hcp_terraform_agent" { } network_configuration { - assign_public_ip = "true" + assign_public_ip = "false" security_groups = [aws_security_group.hcp_terraform_agent.id] subnets = var.subnet_ids } @@ -115,14 +174,11 @@ resource "aws_ecs_service" "hcp_terraform_agent" { } } - tags = { - Name = "hcp-terraform-agent-${var.hcp_terraform_org_name}-${var.name}" - } -} - -moved { - from = aws_ecs_service.hcp-terraform-agent - to = aws_ecs_service.hcp_terraform_agent + tags = merge(var.tags, + { + Name = "hcp-terraform-agent-${var.hcp_terraform_org_name}-${var.name}" + } + ) } resource "aws_security_group" "hcp_terraform_agent" { @@ -132,6 +188,7 @@ resource "aws_security_group" "hcp_terraform_agent" { lifecycle { create_before_destroy = true } + tags = var.tags } #tfsec:ignore:no-public-egress-sgr @@ -151,9 +208,8 @@ resource "aws_security_group_rule" "allow_egress" { ##################################################################################### module "ecs_cluster" { - count = var.create_ecs_cluster ? 1 : 0 - source = "terraform-aws-modules/ecs/aws" - version = "~> 5.0" + count = var.create_ecs_cluster ? 1 : 0 + source = "git::https://github.com/terraform-aws-modules/terraform-aws-ecs?ref=6b52c965734d95767d8e20d965afcd0db29dae5e" # v5.11.2 cluster_name = var.name @@ -161,7 +217,6 @@ module "ecs_cluster" { FARGATE = { default_capacity_provider_strategy = { weight = 50 - base = 20 } } FARGATE_SPOT = { @@ -171,9 +226,11 @@ module "ecs_cluster" { } } - tags = { - Name = var.name - } + tags = merge(var.tags, + { + Name = var.name + } + ) } ##################################################################################### @@ -196,9 +253,13 @@ data "aws_iam_policy_document" "agent_assume_role_policy" { resource "aws_iam_role" "ecs_task_execution_role" { name = "${var.name}-ecsTaskExecutionRole" assume_role_policy = data.aws_iam_policy_document.agent_assume_role_policy.json - tags = { - Name = "hcp-terraform-${var.hcp_terraform_org_name}-${var.name}-ecsTaskExecutionRole" - } + + tags = merge(var.tags, + { + Name = "hcp-terraform-${var.hcp_terraform_org_name}-${var.name}-ecsTaskExecutionRole" + } + ) + } resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" { @@ -206,6 +267,24 @@ resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attach policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } +data "aws_iam_policy_document" "ssm_access_policy" { + statement { + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [ + local.key_id + ] + } +} + +resource "aws_iam_role_policy" "ssm_access_policy" { + name = "ssm-access-policy" + role = aws_iam_role.ecs_task_execution_role.name + policy = data.aws_iam_policy_document.ssm_access_policy.json +} + data "aws_iam_policy_document" "agent_init_policy" { statement { effect = "Allow" @@ -223,9 +302,13 @@ resource "aws_iam_role_policy" "agent_init_policy" { resource "aws_iam_role" "ecs_task_role" { name = "${var.name}-ecsTaskRole" assume_role_policy = data.aws_iam_policy_document.agent_assume_role_policy.json - tags = { - Name = "hcp-terraform-${var.hcp_terraform_org_name}-${var.name}-ecsTaskRole" - } + + tags = merge(var.tags, + { + Name = "hcp-terraform-${var.hcp_terraform_org_name}-${var.name}-ecsTaskRole" + } + ) + } resource "aws_iam_role_policy_attachment" "ecs_task_role_policy_attachment" { diff --git a/outputs.tf b/outputs.tf index 5ffce0c..2b36135 100644 --- a/outputs.tf +++ b/outputs.tf @@ -47,3 +47,8 @@ output "task_role_arn" { description = "ARN of the IAM role attached to the task containers." value = aws_iam_role.ecs_task_role.arn } + +output "kms_key_arn" { + description = "The ARN of the created KMS key" + value = try(aws_kms_key.log_ssm_key[0].arn, null) +} diff --git a/providers.tf b/providers.tf index fb33e87..8c81c1c 100644 --- a/providers.tf +++ b/providers.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.47.0" + version = ">= 5.0.0" } tfe = { diff --git a/tests/02_integration.tftest.hcl b/tests/02_integration.tftest.hcl new file mode 100644 index 0000000..3d376a2 --- /dev/null +++ b/tests/02_integration.tftest.hcl @@ -0,0 +1,37 @@ +run "agent_is_setup" { + command = apply + module { + source = "./tests/setup_agent" + } + # agent pool id should start with apool-xxxxx + assert { + condition = substr(module.agent_pool.agent_pool_id, 0, 5) == "apool" + error_message = "Invalid agent pool id" + } +} + +run "validate_terraform_apply" { + command = apply + module { + source = "./tests/validate_apply" + } + + variables { + tfe_agent_pool = run.agent_is_setup.agent_pool_id + } + + assert { + condition = data.tfe_workspace.test.runs_count > 0 + error_message = "Workspace run count is zero, indicating plan / apply did not run" + } + + assert { + condition = data.tfe_workspace.test.run_failures == 0 + error_message = "Workspace run failure is bigger than zero" + } + + assert { + condition = data.tfe_workspace.test.resource_count > 0 + error_message = "No resources created, indicating apply is failing" + } +} \ No newline at end of file diff --git a/tests/setup_agent/main.tf b/tests/setup_agent/main.tf new file mode 100644 index 0000000..94affea --- /dev/null +++ b/tests/setup_agent/main.tf @@ -0,0 +1,55 @@ +##################################################################################### +# Terraform module examples are meant to show an _example_ on how to use a module +# per use-case. The code below should not be copied directly but referenced in order +# to build your own root module that invokes this module +##################################################################################### + +data "aws_availability_zones" "available" {} + +locals { + region = "us-west-2" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + name = "ecs-${basename(path.cwd)}" + vpc_cidr = "10.0.0.0/16" + tags = { + Terraform = "true" + Environment = "dev" + } +} + +##################################################################################### +# MODULE INVOCATION +##################################################################################### + +module "agent_pool" { + source = "../../" + name = local.name + hcp_terraform_org_name = var.hcp_terraform_org_name + agent_image = "hashicorp/tfc-agent:latest" + use_spot_instances = true + agent_cpu = 512 + agent_memory = 2048 + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + task_policy_arns = ["arn:aws:iam::aws:policy/AmazonS3FullAccess"] +} + +##################################################################################### +# VPC +##################################################################################### + +module "vpc" { + source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc?ref=25322b6b6be69db6cca7f167d7b0e5327156a595" # v5.8.1 + + name = local.name + cidr = local.vpc_cidr + + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] + + enable_nat_gateway = true + single_nat_gateway = true + + tags = local.tags +} diff --git a/tests/setup_agent/outputs.tf b/tests/setup_agent/outputs.tf new file mode 100644 index 0000000..1628ed3 --- /dev/null +++ b/tests/setup_agent/outputs.tf @@ -0,0 +1,49 @@ +output "agent_pool_name" { + description = "Name of the HCP Terraform or HCP Terraform Enterprise agent pool." + value = module.agent_pool.agent_pool_name +} + +output "agent_pool_id" { + description = "ID of the HCP Terraform or HCP Terraform Enterprise agent pool." + value = module.agent_pool.agent_pool_id +} + +output "ecs_service_arn" { + description = "ARN of the ECS service." + value = module.agent_pool.ecs_service_arn +} + +output "ecs_task_arn" { + description = "ARN of the ECS task definition." + value = module.agent_pool.ecs_task_arn +} + +output "ecs_task_revision" { + description = "Revision number of the ECS task definition." + value = module.agent_pool.ecs_task_revision +} + +output "log_stream_prefix" { + description = "Prefix for the CloudWatch log stream." + value = module.agent_pool.log_stream_prefix +} + +output "security_group_name" { + description = "Name of the VPC security group attached to the service." + value = module.agent_pool.security_group_name +} + +output "security_group_id" { + description = "ID of the VPC security group attached to the service." + value = module.agent_pool.security_group_id +} + +output "task_role_name" { + description = "Name of the IAM role attached to the task containers." + value = module.agent_pool.task_role_name +} + +output "task_role_arn" { + description = "ARN of the IAM role attached to the task containers." + value = module.agent_pool.task_role_arn +} diff --git a/tests/setup_agent/providers.tf b/tests/setup_agent/providers.tf new file mode 100644 index 0000000..af605ba --- /dev/null +++ b/tests/setup_agent/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.47.0" + } + } +} + + +provider "aws" { + region = local.region + default_tags { + tags = { + managed_by = "abp-tf-cloud-agents-test" + } + } +} + +provider "tfe" { + token = var.tfe_token +} \ No newline at end of file diff --git a/tests/setup_agent/variables.tf b/tests/setup_agent/variables.tf new file mode 100644 index 0000000..1f7aa3f --- /dev/null +++ b/tests/setup_agent/variables.tf @@ -0,0 +1,10 @@ +variable "hcp_terraform_org_name" { + type = string + description = "The name of the HCP Terraform organization." +} + +variable "tfe_token" { + type = string + description = "Terraform token to be used to create the agent pool." + +} \ No newline at end of file diff --git a/tests/validate_apply/main.tf b/tests/validate_apply/main.tf new file mode 100644 index 0000000..cbcf8ce --- /dev/null +++ b/tests/validate_apply/main.tf @@ -0,0 +1,59 @@ +data "tfe_organization" "test" { + name = var.hcp_terraform_org_name +} + +resource "tfe_oauth_client" "test" { + organization = data.tfe_organization.test.name + api_url = "https://api.github.com" + http_url = "https://github.com" + oauth_token = var.github_oauth_token_id + service_provider = "github" +} + +resource "tfe_workspace" "test" { + name = "test-agent-workspace" + organization = data.tfe_organization.test.name + queue_all_runs = false + vcs_repo { + branch = "main" + identifier = var.vcs_repo_identifier + oauth_token_id = tfe_oauth_client.test.oauth_token_id + } +} + +resource "tfe_agent_pool_allowed_workspaces" "test" { + agent_pool_id = var.tfe_agent_pool + allowed_workspace_ids = [tfe_workspace.test.id] +} + +resource "tfe_workspace_settings" "test" { + depends_on = [tfe_agent_pool_allowed_workspaces.test] + workspace_id = tfe_workspace.test.id + agent_pool_id = var.tfe_agent_pool + execution_mode = "agent" +} + +resource "tfe_workspace_run" "test" { + depends_on = [tfe_workspace_settings.test] + workspace_id = tfe_workspace.test.id + + apply { + manual_confirm = false + wait_for_run = true + retry_attempts = 3 + retry_backoff_min = 3 + } + + destroy { + manual_confirm = false + wait_for_run = true + retry_attempts = 3 + retry_backoff_min = 10 + } +} + +data "tfe_workspace" "test" { + depends_on = [tfe_workspace_run.test] + name = tfe_workspace.test.name + organization = data.tfe_organization.test.name +} \ No newline at end of file diff --git a/tests/validate_apply/outputs.tf b/tests/validate_apply/outputs.tf new file mode 100644 index 0000000..81988fc --- /dev/null +++ b/tests/validate_apply/outputs.tf @@ -0,0 +1,11 @@ +output "tfe_workspace_run_failure" { + value = data.tfe_workspace.test.run_failures +} + +output "tfe_workspace_run_count" { + value = data.tfe_workspace.test.runs_count +} + +output "tfe_workspace_resource_count" { + value = data.tfe_workspace.test.resource_count +} \ No newline at end of file diff --git a/tests/validate_apply/variables.tf b/tests/validate_apply/variables.tf new file mode 100644 index 0000000..21c6268 --- /dev/null +++ b/tests/validate_apply/variables.tf @@ -0,0 +1,20 @@ +variable "hcp_terraform_org_name" { + type = string + description = "The name of the HCP Terraform organization." +} + +variable "github_oauth_token_id" { + type = string + description = "GitHub OAuth token for connecting the VCS" + sensitive = true +} + +variable "vcs_repo_identifier" { + type = string + description = "GitHub repo identifier" +} + +variable "tfe_agent_pool" { + type = string + description = "Terraform Cloud Agent pool id" +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index e6ed190..7cc0d61 100644 --- a/variables.tf +++ b/variables.tf @@ -1,11 +1,21 @@ +### General + variable "name" { type = string description = "A name to apply to resources. The combination of `name` and `hcp_terraform_org_name` must be unique within an AWS account." } +variable "tags" { + description = "Map of tags to apply to resources deployed by this solution." + type = map(any) + default = null +} + +### HashiCorp Cloud Platform + variable "hcp_terraform_address" { type = string - description = "The HTTPS address of the HCP Terraform or HCP Terraform enterprise instance." + description = "The HTTPS address of the HCP Terraform or HCP Terraform Enterprise instance." default = "https://app.terraform.io" validation { condition = startswith(var.hcp_terraform_address, "https://") @@ -15,9 +25,30 @@ variable "hcp_terraform_address" { variable "hcp_terraform_org_name" { type = string - description = "The name of the HCP Terraform or HCP Terraform enterprise organization where the agent pool will be configured. The combination of `hcp_terraform_org_name` and `name` must be unique within an AWS account." + description = "The name of the HCP Terraform or HCP Terraform Enterprise organization where the agent pool will be configured. The combination of `hcp_terraform_org_name` and `name` must be unique within an AWS account." } +variable "create_tfe_agent_pool" { + type = bool + default = true + description = "Whether to omit agent pool/token creation" +} + +variable "tfe_agent_token" { + type = string + default = "" + description = "Terraform agent token to be used when agent creation is omitted" + sensitive = true +} + +variable "tfe_agent_pool_name" { + type = string + default = "" + description = "Terraform agent pool name to be used when agent creation is omitted" +} + +### Terraform Agent + variable "agent_cpu" { type = number description = "The CPU units allocated to the agent container(s). See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html#fargate-tasks-size" @@ -97,24 +128,32 @@ variable "num_agents" { default = 1 } +### CloudWatch configuration + variable "create_cloudwatch_log_group" { type = bool - description = "The name of the CloudWatch log group where agent logs will be sent." + description = "Whether the CloudWatch log group should be created." default = true } variable "cloudwatch_log_group_retention" { type = number description = "The number of days to retain logs in the CloudWatch log group." - default = 7 + default = 365 + validation { + condition = contains([1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, 0], var.cloudwatch_log_group_retention) + error_message = "Valid values for var: cloudwatch_log_group_retention are (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0)." + } } variable "cloudwatch_log_group_name" { type = string description = "The name of the CloudWatch log group where agent logs will be sent." - default = "/ecs/hcp-terraform-agent" + default = "/hcp/hcp-terraform-agent" } +### ECS Cluster configuration + variable "create_ecs_cluster" { type = bool description = "Whether to create a new ECS cluster for the agent." @@ -148,7 +187,7 @@ variable "vpc_id" { variable "subnet_ids" { type = list(string) - description = "IDs of the subnet(s) where agents can be deployed (public subnets required)" + description = "IDs of the subnet(s) where agents can be deployed" validation { condition = alltrue([ for i in var.subnet_ids : can(regex("^subnet-[a-zA-Z0-9]+$", i)) @@ -163,21 +202,9 @@ variable "task_policy_arns" { default = [] } -variable "create_tfe_agent_pool" { - type = bool - default = true - description = "Option to omit agent pool/token creation" -} - -variable "tfe_agent_token" { - type = string - default = "" - description = "Terraform agent token to be used when agent creation is omitted" - sensitive = true -} -variable "tfe_agent_pool_name" { +variable "kms_key_arn" { + description = "The ARN of the KMS key to create. If empty, a new key will be created." type = string default = "" - description = "Terraform agent pool name to be used when agent creation is omitted" }