diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..e279a6f --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,90 @@ +# Docs: https://docs.coderabbit.ai/configure-coderabbit +# Schema: https://coderabbit.ai/integrations/schema.v2.json +# Support: https://discord.gg/GsXnASn26c + +language: en + +tone_instructions: | + Provide feedback in a professional, friendly, constructive, and concise tone. + Offer clear, specific suggestions and best practices to help enhance the code quality and promote learning. + +early_access: true + +knowledge_base: + # The scope of learnings to use for the knowledge base. + # `local` uses the repository's learnings, + # `global` uses the organization's learnings, + # `auto` uses repository's learnings for public repositories and organization's learnings for private repositories. + # Default value: `auto` + learnings: + scope: global + issues: + scope: global + pull_requests: + scope: global + +reviews: + profile: chill + auto_review: + # Ignore reviewing if the title of the pull request contains any of these keywords (case-insensitive) + ignore_title_keywords: + - wip + - draft + - test + # Set the commit status to 'pending' when the review is in progress and 'success' when it is complete. + commit_status: false + # Post review details on each review. Additionally, post a review status when a review is skipped in certain cases. + review_status: false + path_instructions: + - path: "**/*.tf" + instructions: | + You're a Terraform expert who has thoroughly studied all the documentation from Hashicorp https://developer.hashicorp.com/terraform/docs and OpenTofu https://opentofu.org/docs/. + You have a strong grasp of Terraform syntax and prioritize providing accurate and insightful code suggestions. + As a fan of the Cloud Posse / SweetOps ecosystem, you incorporate many of their best practices https://docs.cloudposse.com/best-practices/terraform/ while balancing them with general Terraform guidelines. + tools: + # By default, all tools are enabled. + # Masterpoint uses Trunk (https://trunk.io) so we do not need a lot of this feedback due to overlap. + shellcheck: + enabled: false + ruff: + enabled: false + markdownlint: + enabled: false + github-checks: + enabled: false + languagetool: + enabled: false + biome: + enabled: false + hadolint: + enabled: false + swiftlint: + enabled: false + phpstan: + enabled: false + golangci-lint: + enabled: false + yamllint: + enabled: false + gitleaks: + enabled: false + checkov: + enabled: false + detekt: + enabled: false + eslint: + enabled: false + rubocop: + enabled: false + buf: + enabled: false + regal: + enabled: false + actionlint: + enabled: false + pmd: + enabled: false + cppcheck: + enabled: false + circleci: + enabled: false diff --git a/.github/workflows/trunk-upgrade.yaml b/.github/workflows/trunk-upgrade.yaml index 764843e..d67ed23 100644 --- a/.github/workflows/trunk-upgrade.yaml +++ b/.github/workflows/trunk-upgrade.yaml @@ -1,8 +1,8 @@ -name: Weekly Trunk Upgrade +name: Monthly Trunk Upgrade on: schedule: - # Every Monday @ 5am - - cron: 0 5 * * 1 + # On the first day of every month @ 8am + - cron: 0 8 1 * * # Allows us to manually run the workflow from Actions UI workflow_dispatch: {} permissions: read-all @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 - name: Trunk Upgrade - uses: trunk-io/trunk-action/upgrade@d5b1b61d0beee562512f530a278b6a2931fba857 + uses: trunk-io/trunk-action/upgrade@2eaee169140ec559bd556208f9f99cdfdf468da8 # v1.1.18 with: base: main reviewers: "@masterpointio/masterpoint-internal" diff --git a/.gitignore b/.gitignore index 1c768ad..73df5ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ .terraform .terraform.tfstate.lock.info -**/.idea -**/*.iml - # Cloud Posse Build Harness https://github.com/cloudposse/build-harness **/.build-harness **/build-harness @@ -17,3 +14,8 @@ # Crash log files crash.log test.log + +# Random +**/.idea +**/*.iml +.DS_Store diff --git a/README.md b/README.md index d351bb6..1cdbc0d 100644 --- a/README.md +++ b/README.md @@ -71,22 +71,26 @@ Here is an example of using this module: ## Providers -| Name | Version | -| ------------------------------------------------------------------ | --------- | -| [tailscale](#provider_tailscale) | >= 0.13.7 | +| Name | Version | +| ------------------------------------------------------------------ | ------- | +| [aws](#provider_aws) | 5.76.0 | +| [tailscale](#provider_tailscale) | 0.17.2 | ## Modules -| Name | Source | Version | -| -------------------------------------------------------------------------------------------------------- | --------------------------- | ------- | -| [tailscale_subnet_router](#module_tailscale_subnet_router) | masterpointio/ssm-agent/aws | 1.2.0 | -| [this](#module_this) | cloudposse/label/null | 0.25.0 | +| Name | Source | Version | +| -------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- | +| [ssm_policy](#module_ssm_policy) | cloudposse/iam-policy/aws | 2.0.1 | +| [ssm_state](#module_ssm_state) | cloudposse/ssm-parameter-store/aws | 0.13.0 | +| [tailscale_subnet_router](#module_tailscale_subnet_router) | masterpointio/ssm-agent/aws | 1.2.0 | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -| ------------------------------------------------------------------------------------------------------------------------------ | -------- | -| [tailscale_tailnet_key.default](https://registry.terraform.io/providers/tailscale/tailscale/latest/docs/resources/tailnet_key) | resource | +| Name | Type | +| ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [tailscale_tailnet_key.default](https://registry.terraform.io/providers/tailscale/tailscale/latest/docs/resources/tailnet_key) | resource | ## Inputs @@ -103,6 +107,7 @@ Here is an example of using this module: | [create_run_shell_document](#input_create_run_shell_document) | Whether or not to create the SSM-SessionManagerRunShell SSM Document. | `bool` | `true` | no | | [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [desired_capacity](#input_desired_capacity) | Desired number of instances in the Auto Scaling Group | `number` | `1` | no | | [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [ephemeral](#input_ephemeral) | Indicates if the key is ephemeral. | `bool` | `false` | no | @@ -115,6 +120,8 @@ Here is an example of using this module: | [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [max_size](#input_max_size) | Maximum number of instances in the Auto Scaling Group. Must be >= desired_capacity. | `number` | `2` | no | +| [min_size](#input_min_size) | Minimum number of instances in the Auto Scaling Group | `number` | `1` | no | | [monitoring_enabled](#input_monitoring_enabled) | Enable detailed monitoring of instances | `bool` | `true` | no | | [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | @@ -126,6 +133,7 @@ Here is an example of using this module: | [session_logging_kms_key_alias](#input_session_logging_kms_key_alias) | Alias name for `session_logging` KMS Key.
This is only applied if 2 conditions are met: (1) `session_logging_kms_key_arn` is unset,
(2) `session_logging_encryption_enabled` = true. | `string` | `"alias/session_logging"` | no | | [session_logging_ssm_document_name](#input_session_logging_ssm_document_name) | Name for `session_logging` SSM document.
This is only applied if 2 conditions are met: (1) `session_logging_enabled` = true,
(2) `create_run_shell_document` = true. | `string` | `"SSM-SessionManagerRunShell-Tailscale"` | no | | [ssh_enabled](#input_ssh_enabled) | Enable SSH access to the Tailscale Subnet Router EC2 instance. Defaults to true. | `bool` | `true` | no | +| [ssm_state_enabled](#input_ssm_state_enabled) | Control is tailscaled state (including preferences and keys) is stored in AWS SSM.
See more in the [docs](https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled). | `bool` | `false` | no | | [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subnet_ids](#input_subnet_ids) | The Subnet IDs which the Tailscale Subnet Router EC2 instance will run in. These _should_ be private subnets. | `list(string)` | n/a | yes | | [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/examples/complete/fixtures.us-east-2.tfvars b/examples/complete/fixtures.us-east-2.tfvars index f760a30..21ea325 100644 --- a/examples/complete/fixtures.us-east-2.tfvars +++ b/examples/complete/fixtures.us-east-2.tfvars @@ -8,6 +8,8 @@ region = "us-east-1" availability_zones = ["us-east-1a", "us-east-1b"] ipv4_primary_cidr_block = "172.16.0.0/16" +ssm_state_enabled = true + # Replace these values with your own tailnet = "orgname.org.github" oauth_client_id = "OAUTH_CLIENT_ID" diff --git a/main.tf b/main.tf index 10af105..c6e0235 100644 --- a/main.tf +++ b/main.tf @@ -4,9 +4,14 @@ locals { prefixed_primary_tag = "tag:${local.primary_tag}" prefixed_additional_tags = [for tag in var.additional_tags : "tag:${tag}"] + ssm_state_param_name = var.ssm_state_enabled ? "/tailscale/${module.this.id}/state" : null + ssm_state_flag = var.ssm_state_enabled ? "--state=${module.ssm_state[0].arn_map[local.ssm_state_param_name]}" : "" + tailscale_tags = concat([local.prefixed_primary_tag], local.prefixed_additional_tags) - tailscaled_extra_flags_enabled = length(var.tailscaled_extra_flags) > 0 + tailscaled_extra_flags = join(" ", compact(concat(var.tailscaled_extra_flags, [local.ssm_state_flag]))) + tailscaled_extra_flags_enabled = length(local.tailscaled_extra_flags) > 0 + tailscale_up_extra_flags_enabled = length(var.tailscale_up_extra_flags) > 0 userdata = templatefile("${path.module}/userdata.sh.tmpl", { @@ -18,7 +23,7 @@ locals { tags = join(",", local.tailscale_tags) tailscaled_extra_flags_enabled = local.tailscaled_extra_flags_enabled - tailscaled_extra_flags = join(" ", var.tailscaled_extra_flags) + tailscaled_extra_flags = local.tailscaled_extra_flags tailscale_up_extra_flags_enabled = local.tailscale_up_extra_flags_enabled tailscale_up_extra_flags = join(" ", var.tailscale_up_extra_flags) }) @@ -45,8 +50,11 @@ module "tailscale_subnet_router" { session_logging_enabled = var.session_logging_enabled session_logging_ssm_document_name = var.session_logging_ssm_document_name - ami = var.ami - instance_type = var.instance_type + ami = var.ami + instance_type = var.instance_type + max_size = var.max_size + min_size = var.min_size + desired_capacity = var.desired_capacity monitoring_enabled = var.monitoring_enabled associate_public_ip_address = var.associate_public_ip_address @@ -63,3 +71,53 @@ resource "tailscale_tailnet_key" "default" { # A device is automatically tagged when it is authenticated with this key. tags = local.tailscale_tags } + +module "ssm_state" { + count = var.ssm_state_enabled ? 1 : 0 + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + ignore_value_changes = true + + parameter_write = [ + { + name = local.ssm_state_param_name + type = "SecureString" + overwrite = "true" + value = "{}" + description = "Tailscaled state of ${module.this.id} subnet router." + } + ] + context = module.this.context + tags = module.this.tags +} + +module "ssm_policy" { + count = var.ssm_state_enabled ? 1 : 0 + source = "cloudposse/iam-policy/aws" + version = "2.0.1" + + name = "ssm" + description = "Additional SSM access for SSM Agent" + + iam_policy_enabled = true + iam_policy = [{ + statements = [ + { + sid = "SSMAgentPutParameter" + effect = "Allow" + actions = ["ssm:PutParameter"] + resources = [ + module.ssm_state[0].arn_map[local.ssm_state_param_name], + ] + }, + ] + }] + context = module.this.context + tags = module.this.tags +} + +resource "aws_iam_role_policy_attachment" "default" { + count = var.ssm_state_enabled ? 1 : 0 + role = module.tailscale_subnet_router.role_id + policy_arn = module.ssm_policy[0].policy_arn +} diff --git a/userdata.sh.tmpl b/userdata.sh.tmpl index 4a75a7f..e78c721 100644 --- a/userdata.sh.tmpl +++ b/userdata.sh.tmpl @@ -14,8 +14,8 @@ sudo yum-config-manager --add-repo https://pkgs.tailscale.com/stable/amazon-linu sudo yum install -y tailscale %{ if tailscaled_extra_flags_enabled == true } -echo "Exporting FLAGS to environment variable..." -export FLAGS=${tailscaled_extra_flags}% +echo "Exporting FLAGS to /etc/default/tailscaled..." +sudo sed -i "s|^FLAGS=.*|FLAGS=\"${tailscaled_extra_flags}\"|" /etc/default/tailscaled %{ endif } # Setup tailscale diff --git a/variables.tf b/variables.tf index 77532c0..509e9ec 100644 --- a/variables.tf +++ b/variables.tf @@ -43,7 +43,6 @@ variable "session_logging_kms_key_alias" { EOF } - variable "session_logging_ssm_document_name" { default = "SSM-SessionManagerRunShell-Tailscale" type = string @@ -98,6 +97,24 @@ variable "associate_public_ip_address" { default = null } +variable "max_size" { + description = "Maximum number of instances in the Auto Scaling Group. Must be >= desired_capacity." + type = number + default = 2 +} + +variable "min_size" { + description = "Minimum number of instances in the Auto Scaling Group" + type = number + default = 1 +} + +variable "desired_capacity" { + description = "Desired number of instances in the Auto Scaling Group" + type = number + default = 1 +} + ################ ## Tailscale ## ############## @@ -180,3 +197,12 @@ variable "tailscale_up_extra_flags" { See more in the [docs](https://tailscale.com/kb/1241/tailscale-up). EOT } + +variable "ssm_state_enabled" { + default = false + type = bool + description = <<-EOT + Control if tailscaled state is stored in AWS SSM (including preferences and keys). This tells the Tailscale daemon to write + read state from SSM, which unlocks important features like retaining the existing tailscale machine name. + See more in the [docs](https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled). + EOT +}