├── .coderabbit.yaml
├── .editorconfig
├── .github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── renovate.json5
└── workflows
│ ├── lint.yaml
│ ├── release-please.yaml
│ ├── test.yaml
│ └── trunk-upgrade.yaml
├── .gitignore
├── .terraform-docs.yaml
├── .trunk
├── .gitignore
├── configs
│ ├── .checkov.yaml
│ ├── .markdownlint.yaml
│ ├── .shellcheckrc
│ └── .yamllint.yaml
└── trunk.yaml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── aqua.yaml
├── context.tf
├── examples
└── complete
│ ├── context.tf
│ ├── fixtures.us-east-2.tfvars
│ ├── main.tf
│ ├── outputs.tf
│ ├── variables.tf
│ └── versions.tf
├── main.tf
├── outputs.tf
├── userdata.sh.tmpl
├── variables.tf
└── versions.tf
/.coderabbit.yaml:
--------------------------------------------------------------------------------
1 | # Docs: https://docs.coderabbit.ai/configure-coderabbit
2 | # Schema: https://coderabbit.ai/integrations/schema.v2.json
3 | # Support: https://discord.gg/GsXnASn26c
4 |
5 | language: en
6 |
7 | tone_instructions: |
8 | Provide feedback in a professional, friendly, constructive, and concise tone.
9 | Offer clear, specific suggestions and best practices to help enhance the code quality and promote learning.
10 | Be concise and only comment on significant issues.
11 |
12 | early_access: true
13 |
14 | knowledge_base:
15 | # The scope of learnings to use for the knowledge base.
16 | # `local` uses the repository's learnings,
17 | # `global` uses the organization's learnings,
18 | # `auto` uses repository's learnings for public repositories and organization's learnings for private repositories.
19 | # Default value: `auto`
20 | learnings:
21 | scope: global
22 | issues:
23 | scope: global
24 | pull_requests:
25 | scope: global
26 |
27 | reviews:
28 | profile: chill
29 | auto_review:
30 | # Disable incremental code review on each push
31 | auto_incremental_review: false
32 | # The keywords are case-insensitive
33 | ignore_title_keywords:
34 | - wip
35 | - draft
36 | - test
37 | commit_status: false
38 | path_instructions:
39 | - path: "**/*.tf"
40 | instructions: |
41 | 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/.
42 | You have a strong grasp of Terraform syntax and prioritize providing accurate and insightful code suggestions.
43 | 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.
44 | changed_files_summary: false
45 | poem: false
46 | # Don't post review details on each review.
47 | review_status: false
48 | sequence_diagrams: false
49 | tools:
50 | # By default, all tools are enabled.
51 | # Masterpoint uses Trunk (https://trunk.io) so we do not need a lot of this feedback due to overlap.
52 | shellcheck:
53 | enabled: false
54 | ruff:
55 | enabled: false
56 | markdownlint:
57 | enabled: false
58 | github-checks:
59 | enabled: false
60 | languagetool:
61 | enabled: false
62 | biome:
63 | enabled: false
64 | hadolint:
65 | enabled: false
66 | swiftlint:
67 | enabled: false
68 | phpstan:
69 | enabled: false
70 | golangci-lint:
71 | enabled: false
72 | yamllint:
73 | enabled: false
74 | gitleaks:
75 | enabled: false
76 | checkov:
77 | enabled: false
78 | detekt:
79 | enabled: false
80 | eslint:
81 | enabled: false
82 | rubocop:
83 | enabled: false
84 | buf:
85 | enabled: false
86 | regal:
87 | enabled: false
88 | actionlint:
89 | enabled: false
90 | pmd:
91 | enabled: false
92 | cppcheck:
93 | enabled: false
94 | circleci:
95 | enabled: false
96 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Unix-style newlines with a newline ending every file
2 | [*]
3 | charset = utf-8
4 | end_of_line = lf
5 | indent_size = 2
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.md]
11 | max_line_length = 0
12 |
13 | [COMMIT_EDITMSG]
14 | max_line_length = 0
15 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Use this file to define individuals or teams that are responsible for code in a repository.
2 | # Read more:
3 | #
4 | # Order is important: the last matching pattern takes the most precedence
5 |
6 | # These owners will be the default owners for everything
7 | * @masterpointio/masterpoint-open-source
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## what
2 |
3 | - Describe high-level what changed as a result of these commits (i.e. in plain-english, what do these changes mean?)
4 | - Use bullet points to be concise and to the point.
5 |
6 | ## why
7 |
8 | - Provide the justifications for the changes (e.g. business case).
9 | - Describe why these changes were made (e.g. why do these commits fix the problem?)
10 | - Use bullet points to be concise and to the point.
11 |
12 | ## references
13 |
14 | - Link to any supporting GitHub issues or helpful documentation to add some context (e.g. Stackoverflow).
15 | - Use `closes #123`, if this PR closes a GitHub issue `#123`
16 |
--------------------------------------------------------------------------------
/.github/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:best-practices",
4 | "github>aquaproj/aqua-renovate-config#2.7.5"
5 | ],
6 | "enabledManagers": [
7 | "terraform",
8 | "github-actions"
9 | ],
10 | "terraform": {
11 | "ignorePaths": [
12 | "**/context.tf" // Mixin file https://github.com/cloudposse/terraform-null-label/blob/main/exports/context.tf
13 | ],
14 | "fileMatch": [
15 | "\\.tf$",
16 | "\\.tofu$"
17 | ]
18 | },
19 | "schedule": [
20 | "after 9am on the first day of the month"
21 | ],
22 | "assigneesFromCodeOwners": true,
23 | "dependencyDashboardAutoclose": true,
24 | "addLabels": ["{{manager}}"],
25 | "packageRules": [
26 | {
27 | "matchManagers": ["github-actions"],
28 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
29 | "automerge": true,
30 | "automergeType": "branch",
31 | "groupName": "github-actions-auto-upgrade",
32 | "addLabels": ["auto-upgrade"]
33 | },
34 | {
35 | "matchManagers": ["github-actions"],
36 | "matchUpdateTypes": ["major"],
37 | "groupName": "github-actions-needs-review",
38 | "addLabels": ["needs-review"]
39 | },
40 | {
41 | "matchManagers": ["terraform"],
42 | "groupName": "tf",
43 | "addLabels": ["needs-review"]
44 | },
45 | {
46 | "matchFileNames": ["**/*.tofu", "**/*.tf"],
47 | "matchDatasources": ["terraform-provider", "terraform-module"],
48 | "registryUrls": ["https://registry.opentofu.org"],
49 | "groupName": "tf"
50 | },
51 | {
52 | "matchFileNames": ["**/*.tofu"],
53 | "matchDepTypes": ["required_version"],
54 | "registryUrls": ["https://registry.opentofu.org"],
55 | "groupName": "tf"
56 | },
57 | {
58 | "matchFileNames": ["**/*.tf"],
59 | "matchDepTypes": ["required_version"],
60 | "registryUrls": ["https://registry.terraform.io"],
61 | "groupName": "tf"
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | concurrency:
4 | group: lint-${{ github.head_ref || github.run_id }}
5 | cancel-in-progress: true
6 |
7 | on: pull_request
8 |
9 | permissions:
10 | actions: read
11 | checks: write
12 | contents: read
13 | pull-requests: read
14 |
15 | jobs:
16 | lint:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Check out Git repository
20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 | - name: Trunk Check
22 | uses: trunk-io/trunk-action@4d5ecc89b2691705fd08c747c78652d2fc806a94 # v1.1.19
23 |
24 | conventional-title:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yaml:
--------------------------------------------------------------------------------
1 | name: Release Please
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 | issues: write
12 |
13 | jobs:
14 | release-please:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f #v4.1.3
18 | with:
19 | release-type: terraform-module
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: TF Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | permissions:
10 | actions: read
11 | checks: write
12 | contents: read
13 | id-token: write
14 | pull-requests: read
15 |
16 | jobs:
17 | tf-test:
18 | name: 🧪 ${{ matrix.tf }} test
19 | runs-on: ubuntu-latest
20 | strategy:
21 | matrix:
22 | tf: [tofu, terraform]
23 | steps:
24 | - uses: masterpointio/github-action-tf-test@c3b619f3bca9e4f482b9e0fb3166ab3f02d9d54c # v1.0.0
25 | with:
26 | tf_type: ${{ matrix.tf }}
27 | aws_role_arn: ${{ vars.TF_TEST_AWS_ROLE_ARN }}
28 | github_token: ${{ secrets.GITHUB_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/trunk-upgrade.yaml:
--------------------------------------------------------------------------------
1 | name: Trunk Upgrade
2 |
3 | on:
4 | schedule:
5 | # On the first day of every month @ 8am
6 | - cron: 0 8 1 * *
7 | workflow_dispatch: {}
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | trunk-upgrade:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | # For trunk to create PRs
16 | contents: write
17 | pull-requests: write
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 |
22 | - name: Create Token for MasterpointBot App
23 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
24 | id: generate-token
25 | with:
26 | app_id: ${{ secrets.MP_BOT_APP_ID }}
27 | private_key: ${{ secrets.MP_BOT_APP_PRIVATE_KEY }}
28 |
29 | - name: Upgrade
30 | id: trunk-upgrade
31 | uses: trunk-io/trunk-action/upgrade@4d5ecc89b2691705fd08c747c78652d2fc806a94 # v1.1.19
32 | with:
33 | github-token: ${{ steps.generate-token.outputs.token }}
34 | reviewers: "@masterpointio/masterpoint-internal"
35 | prefix: "chore: "
36 |
37 | - name: Merge PR automatically
38 | if: steps.trunk-upgrade.outputs.pull-request-number != ''
39 | env:
40 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
41 | PR_NUMBER: ${{ steps.trunk-upgrade.outputs.pull-request-number }}
42 | run: |
43 | gh pr merge "$PR_NUMBER" --squash --auto --delete-branch
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore override files as they are usually used to override resources locally
2 | *override.tf
3 | *override.tf.json
4 |
5 | # .tfstate files
6 | *.tfstate
7 | *.tfstate.*
8 |
9 | # Local .terraform directories
10 | **/.terraform/*
11 |
12 | # Ignore the root .terraform.lock.hcl file (Child modules don't want this)
13 | .terraform.lock.hcl
14 | !examples/**/.terraform.lock.hcl
15 |
16 | # IDE/Editor settings
17 | **/.idea
18 | **/*.iml
19 | .vscode/
20 | *.orig
21 | *.draft
22 | *~
23 |
24 | # Build Harness https://github.com/cloudposse/build-harness
25 | **/.build-harness
26 | **/build-harness
27 |
28 | # Log files
29 | *.log
30 |
31 | # Output from other tools that might be used alongside Terraform/OpenTofu
32 | *.tfvars.json
33 | backend.tf.json
34 |
35 | # Taskit files
36 | .taskit/
37 | .task/
38 | .env.taskit-secrets
39 |
40 | # Other
41 | **/*.backup
42 | ***/*.tmp
43 | **/*.temp
44 | **/*.bak
45 | **/*.*swp
46 | **/.DS_Store
47 |
--------------------------------------------------------------------------------
/.terraform-docs.yaml:
--------------------------------------------------------------------------------
1 | version: 0.20.0
2 | formatter: markdown table
3 |
4 | recursive:
5 | enabled: false
6 |
7 | settings:
8 | lockfile: false
9 |
10 | output:
11 | file: README.md
12 | mode: inject
13 | template: |-
14 |
15 | {{ .Content }}
16 |
17 |
--------------------------------------------------------------------------------
/.trunk/.gitignore:
--------------------------------------------------------------------------------
1 | *out
2 | *logs
3 | *actions
4 | *notifications
5 | *tools
6 | plugins
7 | user_trunk.yaml
8 | user.yaml
9 | tmp
10 |
--------------------------------------------------------------------------------
/.trunk/configs/.checkov.yaml:
--------------------------------------------------------------------------------
1 | skip-check:
2 | - CKV_TF_1 # Ensure module references are pinned to a commit SHA.
3 |
--------------------------------------------------------------------------------
/.trunk/configs/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | # Autoformatter friendly markdownlint config (all formatting rules disabled)
2 | default: true
3 | blank_lines: false
4 | bullet: false
5 | html: false
6 | indentation: false
7 | line_length: false
8 | spaces: false
9 | url: false
10 | whitespace: false
11 |
12 | # Ignore MD041/first-line-heading/first-line-h1
13 | # Error: First line in a file should be a top-level heading
14 | MD041: false
15 |
16 | # Ignore MD013/line-length
17 | MD013:
18 | strict: false
19 | line_length: 350
20 |
--------------------------------------------------------------------------------
/.trunk/configs/.shellcheckrc:
--------------------------------------------------------------------------------
1 | enable=all
2 | source-path=SCRIPTDIR
3 | disable=SC2154
4 |
5 | # If you're having issues with shellcheck following source, disable the errors via:
6 | # disable=SC1090
7 | # disable=SC1091
8 |
--------------------------------------------------------------------------------
/.trunk/configs/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | quoted-strings:
3 | required: only-when-needed
4 | extra-allowed: ["{|}"]
5 | key-duplicates: {}
6 | octal-values:
7 | forbid-implicit-octal: true
8 |
--------------------------------------------------------------------------------
/.trunk/trunk.yaml:
--------------------------------------------------------------------------------
1 | # This file controls the behavior of Trunk: https://docs.trunk.io/cli
2 | # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
3 | version: 0.1
4 | cli:
5 | version: 1.24.0
6 | # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
7 | plugins:
8 | sources:
9 | - id: trunk
10 | ref: v1.7.0
11 | uri: https://github.com/trunk-io/plugins
12 | # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
13 | runtimes:
14 | enabled:
15 | - node@22.16.0
16 | - python@3.10.8
17 | # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
18 | lint:
19 | disabled:
20 | # Incompatible with some Terraform features: https://github.com/tenable/terrascan/issues/1331
21 | - terrascan
22 | enabled:
23 | - renovate@40.36.2
24 | - tofu@1.9.1
25 | - terraform@1.1.0
26 | - actionlint@1.7.7
27 | - checkov@3.2.435
28 | - git-diff-check
29 | - markdownlint@0.45.0
30 | - prettier@3.5.3
31 | - tflint@0.58.0
32 | - trivy@0.63.0
33 | - trufflehog@3.88.35
34 | - yamllint@1.37.1
35 | ignore:
36 | - linters: [tofu]
37 | paths:
38 | - "**/backend.tf.json"
39 | # Ignore CHANGELOG.md as release-please manages this file
40 | - linters: [ALL]
41 | paths:
42 | - "**/CHANGELOG.md"
43 | actions:
44 | enabled:
45 | - terraform-docs
46 | - trunk-announce
47 | - trunk-check-pre-push
48 | - trunk-fmt-pre-commit
49 | - trunk-upgrade-available
50 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [1.8.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.7.0...v1.8.0) (2025-05-22)
4 |
5 |
6 | ### Features
7 |
8 | * Expose the "architecture" variable ([#58](https://github.com/masterpointio/terraform-aws-tailscale/issues/58)) ([8662e72](https://github.com/masterpointio/terraform-aws-tailscale/commit/8662e722bccd056a64fa720ba2fecbec7f5bb51d))
9 |
10 | ## [1.7.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.6.0...v1.7.0) (2025-05-16)
11 |
12 |
13 | ### Features
14 |
15 | * allow configuring additional security group rules ([#56](https://github.com/masterpointio/terraform-aws-tailscale/issues/56)) ([e854ea0](https://github.com/masterpointio/terraform-aws-tailscale/commit/e854ea03f2fe100ed9a4e6de7ece462dccc9c485))
16 |
17 | ## [1.6.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.5.1...v1.6.0) (2025-01-06)
18 |
19 |
20 | ### Features
21 |
22 | * enable log rotation + install CW Agent ([#48](https://github.com/masterpointio/terraform-aws-tailscale/issues/48)) ([560774b](https://github.com/masterpointio/terraform-aws-tailscale/commit/560774b0c2a4e5a0a4bcdc06d4c060dd16db4678))
23 |
24 | ## [1.5.1](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.5.0...v1.5.1) (2024-11-25)
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * avoid RPM lock issue ([#44](https://github.com/masterpointio/terraform-aws-tailscale/issues/44)) ([30b0aca](https://github.com/masterpointio/terraform-aws-tailscale/commit/30b0acaba65aa95bee257cb46b76ddc7d8071a1b))
30 |
31 | ## [1.5.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.4.0...v1.5.0) (2024-11-21)
32 |
33 |
34 | ### Features
35 |
36 | * support AWS SSM tailscaled state ([#41](https://github.com/masterpointio/terraform-aws-tailscale/issues/41)) ([4e9ef78](https://github.com/masterpointio/terraform-aws-tailscale/commit/4e9ef782a5e2f6460c9150e78972bb7e8560dd52))
37 |
38 | ## [1.4.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/v1.3.0...v1.4.0) (2024-08-20)
39 |
40 |
41 | ### Features
42 |
43 | * support extra arguments ([#28](https://github.com/masterpointio/terraform-aws-tailscale/issues/28)) ([6ff5059](https://github.com/masterpointio/terraform-aws-tailscale/commit/6ff5059a5c4a1efa0b3c81b6f92a42ee5f165e3d))
44 |
45 | ## [1.3.0](https://github.com/masterpointio/terraform-aws-tailscale/compare/1.2.0...v1.3.0) (2024-08-13)
46 |
47 |
48 | ### Features
49 |
50 | * adds conventional-title lint check ([73abd18](https://github.com/masterpointio/terraform-aws-tailscale/commit/73abd184189ce062cba882d79ab10b183a1f117c))
51 | * adds release-please for automated releases ([c08d7bb](https://github.com/masterpointio/terraform-aws-tailscale/commit/c08d7bbdffba9038e4e111e984dfbe2e78e1512c))
52 | * allow configuring router as an exit node ([#24](https://github.com/masterpointio/terraform-aws-tailscale/issues/24)) ([3c30878](https://github.com/masterpointio/terraform-aws-tailscale/commit/3c30878166fc694c27cd77ace3879e2a19168556))
53 |
54 |
55 | ### Bug Fixes
56 |
57 | * update to use kebab-case name ([f8006ff](https://github.com/masterpointio/terraform-aws-tailscale/commit/f8006ff056060edab3c2b311b014b548156d6204))
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2025 Masterpoint
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Banner][banner-image]](https://masterpoint.io/)
2 |
3 | # terraform-aws-tailscale
4 |
5 | [![Release][release-badge]][latest-release]
6 |
7 | 💡 Learn more about Masterpoint [below](#who-we-are-𐦂𖨆𐀪𖠋).
8 |
9 | ## Purpose and Functionality
10 |
11 | This is a Terraform Module to create a simple, autoscaled [Tailscale Subnet Router](https://tailscale.com/kb/1019/subnets/) on EC2 instance along with generated auth key, and its corresponding IAM resources. The instance should cycle itself on a schedule.
12 |
13 | ## Usage
14 |
15 | Here's how to invoke this example module in your projects
16 |
17 | ```hcl
18 | module "vpc" {
19 | source = "cloudposse/vpc/aws"
20 | version = "2.1.1"
21 |
22 | namespace = "eg"
23 | stage = "test"
24 | name = "tailscale"
25 |
26 | ipv4_primary_cidr_block = "172.16.0.0/16"
27 | }
28 |
29 | module "subnets" {
30 | source = "cloudposse/dynamic-subnets/aws"
31 | version = "2.4.1"
32 |
33 | namespace = "eg"
34 | stage = "test"
35 | name = "tailscale"
36 |
37 | availability_zones = ["us-east-1a", "us-east-1b"]
38 |
39 | vpc_id = module.vpc.vpc_id
40 | igw_id = [module.vpc.igw_id]
41 | ipv4_cidr_block = [module.vpc.vpc_cidr_block]
42 | }
43 |
44 | module "tailscale" {
45 | source = "masterpointio/tailscale/aws"
46 | version = "X.X.X"
47 |
48 | namespace = "eg"
49 | stage = "test"
50 | name = "tailscale"
51 |
52 | vpc_id = module.vpc.vpc_id
53 | subnet_ids = module.subnets.private_subnet_ids
54 | advertise_routes = [module.vpc.vpc_cidr_block]
55 |
56 | ephemeral = true
57 | }
58 | ```
59 |
60 | ## Examples
61 |
62 | Here is an example of using this module:
63 |
64 | - [`examples/complete`](https://github.com/masterpointio/terraform-aws-tailscale/) - complete example of using this module
65 |
66 | ## System Logging and Monitoring Setup
67 |
68 | On Linux and other Unix-like systems, Tailscale typically runs as a systemd service, which by default does not rotate logs - potentially allowing system logs to grow until the disk fills.
69 |
70 | To address this, our user data script configures both a maximum journal size and a retention period to ensure logs are periodically purged. We also install the [CloudWatch Agent](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Install-CloudWatch-Agent.html) with its default configuration so that filesystem usage metrics are reported to AWS.
71 |
72 | 👀 To view these metrics, navigate in the AWS Console to “CWAgent” → “AutoScalingGroupName, ImageId, InstanceId, InstanceType, device, fstype, path” → “disk_used_percent” for the root path “/”.
73 |
74 | ## Direct and Relayed Connections
75 |
76 | Tailscale supports two primary types of [connection types](https://tailscale.com/kb/1257/connection-types) for subnet routers:
77 |
78 | - **Direct (peer-to-peer)**: Nodes communicate directly with each other when possible, offering better performance and reliability.
79 | - **Relayed**: Traffic is routed through Tailscale's DERP (Designated Encrypted Relay for Packets) servers when direct connectivity isn't possible (e.g. when the subnet router is in a private VPC subnet).
80 |
81 | ### Addressing Connection Stability Issues
82 |
83 | We've been using relayed connections for our subnet routers, but we've observed that relayed connections can sometimes cause intermittent connectivity issues, particularly when working with database connections through the Tailscale proxy (see [this issue](https://github.com/cyrilgdn/terraform-provider-postgresql/issues/495) for an example).
84 |
85 | These issues appear as connection timeouts or SOCKS server errors:
86 |
87 | ```sh
88 | │ Error: Error connecting to PostgreSQL server dev.example.com (scheme: postgres): socks connect tcp localhost:1055->dev.example.com:5432: unknown error general SOCKS server failure
89 | │
90 | │ with data.postgresql_schemas.schemas["example"],
91 | │ on main.tf line 65, in data "postgresql_schemas" "schemas":
92 | │ 65: data "postgresql_schemas" "schemas" {
93 | │
94 | ╵
95 | netstack: decrementing connsInFlightByClient[100.0.108.92] because the packet was not handled; new value is 0
96 | [RATELIMIT] format("netstack: decrementing connsInFlightByClient[%v] because the packet was not handled; new value is %d")
97 | ```
98 |
99 | ### Configuring Direct Connections
100 |
101 | To optimize for direct connections in your Tailscale subnet router, follow this example:
102 |
103 | ```hcl
104 | locals {
105 | public_subnets = ["subnet-1234567890", "subnet-0987654321"]
106 | vpc_id = "vpc-1234567890"
107 | direct_port = "41641"
108 | }
109 |
110 | module "tailscale" {
111 | source = "masterpointio/tailscale/aws"
112 | version = "1.6.0" # Or later
113 | ...
114 | # Direct connection configuration
115 | subnet_ids = local.public_subnets # Ensure subnet router is in a public subnet
116 |
117 | additional_security_group_ids = [module.direct_sg.id] # Attach the security group to the subnet router
118 | tailscaled_extra_flags = ["--port=${local.direct_port}"] # Ensure `tailscaled` listens on the same port as the security group is configured
119 |
120 | context = module.this.context
121 | }
122 |
123 | module "direct_sg" {
124 | source = "cloudposse/security-group/aws"
125 | version = "2.2.0"
126 | enabled = true
127 |
128 | vpc_id = local.vpc_id
129 | attributes = ["tailscale", "direct"]
130 |
131 | rules = [{
132 | key = "direct_ingress"
133 | type = "ingress"
134 | from_port = local.direct_port
135 | to_port = local.direct_port
136 | protocol = "udp"
137 | cidr_blocks = ["0.0.0.0/0"]
138 | description = "Allow a direct Tailscale connection from any peer."
139 | }]
140 |
141 | context = module.this.context
142 | }
143 | ```
144 |
145 | The above configuration ensures that the subnet router can establish direct connections with other Tailscale nodes:
146 |
147 | 1. It is in a public subnet and gets a public IP address.
148 | 2. The security group is attached and configured to listen on a fixed port.
149 | 3. The `tailscaled` daemon is configured to listen on the same port as the security group is configured to listen on.
150 | 4. The outgoing UDP and TCP packets on port `443` are permitted. In our example, [`cloudposse/security-group/aws`](https://github.com/cloudposse/terraform-aws-security-group) module allows all egress.
151 |
152 |
153 |
154 |
155 | ## Requirements
156 |
157 | | Name | Version |
158 | |------|---------|
159 | | [terraform](#requirement\_terraform) | >= 1.0 |
160 | | [aws](#requirement\_aws) | >= 4.0 |
161 | | [tailscale](#requirement\_tailscale) | >= 0.13.7 |
162 |
163 | ## Providers
164 |
165 | | Name | Version |
166 | |------|---------|
167 | | [aws](#provider\_aws) | >= 4.0 |
168 | | [tailscale](#provider\_tailscale) | >= 0.13.7 |
169 |
170 | ## Modules
171 |
172 | | Name | Source | Version |
173 | | -------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- |
174 | | [ssm_policy](#module_ssm_policy) | cloudposse/iam-policy/aws | 2.0.1 |
175 | | [ssm_state](#module_ssm_state) | cloudposse/ssm-parameter-store/aws | 0.13.0 |
176 | | [tailscale_subnet_router](#module_tailscale_subnet_router) | masterpointio/ssm-agent/aws | 1.4.0 |
177 | | [this](#module_this) | cloudposse/label/null | 0.25.0 |
178 |
179 | ## Resources
180 |
181 | | Name | Type |
182 | |------|------|
183 | | [aws_iam_role_policy_attachment.cw_agent](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
184 | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
185 | | [tailscale_tailnet_key.default](https://registry.terraform.io/providers/tailscale/tailscale/latest/docs/resources/tailnet_key) | resource |
186 |
187 | ## Inputs
188 |
189 | | Name | Description | Type | Default | Required |
190 | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: |
191 | | [additional_security_group_ids](#input_additional_security_group_ids) | Additional Security Group IDs to associate with the Tailscale Subnet Router EC2 instance. | `list(string)` | `[]` | no |
192 | | [additional_security_group_rules](#input_additional_security_group_rules) | Additional security group rules that will be attached to the primary security group |
map(object({
type = string
from_port = number
to_port = number
protocol = string
description = optional(string)
cidr_blocks = optional(list(string))
ipv6_cidr_blocks = optional(list(string))
prefix_list_ids = optional(list(string))
self = optional(bool)
}))
| `{}` | no |
193 | | [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
194 | | [additional_tags](#input_additional_tags) | Additional Tailscale tags to apply to the Tailscale Subnet Router machine in addition to `primary_tag`. These should not include the `tag:` prefix. | `list(string)` | `[]` | no |
195 | | [advertise_routes](#input_advertise_routes) | The routes (expressed as CIDRs) to advertise as part of the Tailscale Subnet Router.
Example: ["10.0.2.0/24", "0.0.1.0/24"] | `list(string)` | `[]` | no |
196 | | [ami](#input_ami) | The AMI to use for the Tailscale Subnet Router EC2 instance.
If not provided, the latest Amazon Linux 2 AMI will be used.
Note: This will update periodically as AWS releases updates to their AL2 AMI.
Pin to a specific AMI if you would like to avoid these updates. | `string` | `""` | no |
197 | | [associate_public_ip_address](#input_associate_public_ip_address) | Associate public IP address with subnet router | `bool` | `null` | no |
198 | | [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no |
199 | | [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
200 | | [create_run_shell_document](#input_create_run_shell_document) | Whether or not to create the SSM-SessionManagerRunShell SSM Document. | `bool` | `true` | no |
201 | | [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
202 | | [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 |
203 | | [desired_capacity](#input_desired_capacity) | Desired number of instances in the Auto Scaling Group | `number` | `1` | no |
204 | | [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
205 | | [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no |
206 | | [ephemeral](#input_ephemeral) | Indicates if the key is ephemeral. | `bool` | `false` | no |
207 | | [exit_node_enabled](#input_exit_node_enabled) | Advertise Tailscale Subnet Router EC2 instance as exit node. Defaults to false. | `bool` | `false` | no |
208 | | [expiry](#input_expiry) | The expiry of the auth key in seconds. | `number` | `7776000` | no |
209 | | [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
210 | | [instance_type](#input_instance_type) | The instance type to use for the Tailscale Subnet Router EC2 instance. | `string` | `"t4g.nano"` | no |
211 | | [journald_max_retention_sec](#input_journald_max_retention_sec) | The maximum time to store journal entries. | `string` | `"7d"` | no |
212 | | [journald_system_max_use](#input_journald_system_max_use) | Disk space the journald may use up at most | `string` | `"200M"` | no |
213 | | [key_pair_name](#input_key_pair_name) | The name of the key-pair to associate with the Tailscale Subnet Router EC2 instance. | `string` | `null` | no |
214 | | [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no |
215 | | [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 |
216 | | [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 |
217 | | [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 |
218 | | [max_size](#input_max_size) | Maximum number of instances in the Auto Scaling Group. Must be >= desired_capacity. | `number` | `2` | no |
219 | | [min_size](#input_min_size) | Minimum number of instances in the Auto Scaling Group | `number` | `1` | no |
220 | | [monitoring_enabled](#input_monitoring_enabled) | Enable detailed monitoring of instances | `bool` | `true` | no |
221 | | [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 |
222 | | [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 |
223 | | [preauthorized](#input_preauthorized) | Determines whether or not the machines authenticated by the key will be authorized for the tailnet by default. | `bool` | `true` | no |
224 | | [primary_tag](#input_primary_tag) | The primary tag to apply to the Tailscale Subnet Router machine. Do not include the `tag:` prefix. This must match the OAuth client's tag. If not provided, the module will use the module's ID as the primary tag, which is configured in context.tf | `string` | `null` | no |
225 | | [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
226 | | [reusable](#input_reusable) | Indicates if the key is reusable or single-use. | `bool` | `true` | no |
227 | | [session_logging_enabled](#input_session_logging_enabled) | To enable CloudWatch and S3 session logging or not.
Note this does not apply to SSH sessions as AWS cannot log those sessions. | `bool` | `true` | no |
228 | | [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 |
229 | | [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 |
230 | | [ssh_enabled](#input_ssh_enabled) | Enable SSH access to the Tailscale Subnet Router EC2 instance. Defaults to true. | `bool` | `true` | no |
231 | | [ssm_state_enabled](#input_ssm_state_enabled) | 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). | `bool` | `false` | no |
232 | | [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
233 | | [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 |
234 | | [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 |
235 | | [tailscale_up_extra_flags](#input_tailscale_up_extra_flags) | Extra flags to pass to `tailscale up` for advanced configuration.
See more in the [docs](https://tailscale.com/kb/1241/tailscale-up). | `list(string)` | `[]` | no |
236 | | [tailscaled_extra_flags](#input_tailscaled_extra_flags) | Extra flags to pass to Tailscale daemon for advanced configuration. Example: ["--state=mem:"]
See more in the [docs](https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled). | `list(string)` | `[]` | no |
237 | | [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
238 | | [user_data](#input_user_data) | The user_data to use for the Tailscale Subnet Router EC2 instance.
You can use this to automate installation of all the required command line tools. | `string` | `""` | no |
239 | | [vpc_id](#input_vpc_id) | The ID of the VPC which the Tailscale Subnet Router EC2 instance will run in. | `string` | n/a | yes |
240 |
241 | ## Outputs
242 |
243 | | Name | Description |
244 | |------|-------------|
245 | | [autoscaling\_group\_id](#output\_autoscaling\_group\_id) | The ID of the Tailscale Subnet Router EC2 instance Autoscaling Group. |
246 | | [instance\_name](#output\_instance\_name) | The name tag value of the Tailscale Subnet Router EC2 instance. |
247 | | [launch\_template\_id](#output\_launch\_template\_id) | The ID of the Tailscale Subnet Router EC2 instance Launch Template. |
248 | | [security\_group\_id](#output\_security\_group\_id) | The ID of the Tailscale Subnet Router EC2 instance Security Group. |
249 |
250 |
251 |
252 |
253 | ## Built By
254 |
255 | Powered by the [Masterpoint team](https://masterpoint.io/who-we-are/) and driven forward by contributions from the community ❤️
256 |
257 | [![Contributors][contributors-image]][contributors-url]
258 |
259 | ## Contribution Guidelines
260 |
261 | Contributions are welcome and appreciated!
262 |
263 | Found an issue or want to request a feature? [Open an issue][issues-url]
264 |
265 | Want to fix a bug you found or add some functionality? Fork, clone, commit, push, and PR — we'll check it out.
266 |
267 | ## Who We Are 𐦂𖨆𐀪𖠋
268 |
269 | Established in 2016, Masterpoint is a team of experienced software and platform engineers specializing in Infrastructure as Code (IaC). We provide expert guidance to organizations of all sizes, helping them leverage the latest IaC practices to accelerate their engineering teams.
270 |
271 | ### Our Mission
272 |
273 | Our mission is to simplify cloud infrastructure so developers can innovate faster, safer, and with greater confidence. By open-sourcing tools and modules that we use internally, we aim to contribute back to the community, promoting consistency, quality, and security.
274 |
275 | ### Our Commitments
276 |
277 | - 🌟 **Open Source**: We live and breathe open source, contributing to and maintaining hundreds of projects across multiple organizations.
278 | - 🌎 **1% for the Planet**: Demonstrating our commitment to environmental sustainability, we are proud members of [1% for the Planet](https://www.onepercentfortheplanet.org), pledging to donate 1% of our annual sales to environmental nonprofits.
279 | - 🇺🇦 **1% Towards Ukraine**: With team members and friends affected by the ongoing [Russo-Ukrainian war](https://en.wikipedia.org/wiki/Russo-Ukrainian_War), we donate 1% of our annual revenue to invasion relief efforts, supporting organizations providing aid to those in need. [Here's how you can help Ukraine with just a few clicks](https://masterpoint.io/updates/supporting-ukraine/).
280 |
281 | ## Connect With Us
282 |
283 | We're active members of the community and are always publishing content, giving talks, and sharing our hard earned expertise. Here are a few ways you can see what we're up to:
284 |
285 | [![LinkedIn][linkedin-badge]][linkedin-url] [![Newsletter][newsletter-badge]][newsletter-url] [![Blog][blog-badge]][blog-url] [![YouTube][youtube-badge]][youtube-url]
286 |
287 | ... and be sure to connect with our founder, [Matt Gowie](https://www.linkedin.com/in/gowiem/).
288 |
289 | ## License
290 |
291 | [Apache License, Version 2.0][license-url].
292 |
293 | [![Open Source Initiative][osi-image]][license-url]
294 |
295 | Copyright © 2016-2025 [Masterpoint Consulting LLC](https://masterpoint.io/)
296 |
297 |
298 |
299 | [banner-image]: https://masterpoint-public.s3.us-west-2.amazonaws.com/v2/standard-long-fullcolor.png
300 | [license-url]: https://opensource.org/license/apache-2-0
301 | [osi-image]: https://i0.wp.com/opensource.org/wp-content/uploads/2023/03/cropped-OSI-horizontal-large.png?fit=250%2C229&ssl=1
302 | [linkedin-badge]: https://img.shields.io/badge/LinkedIn-Follow-0A66C2?style=for-the-badge&logoColor=white
303 | [linkedin-url]: https://www.linkedin.com/company/masterpoint-consulting
304 | [blog-badge]: https://img.shields.io/badge/Blog-IaC_Insights-55C1B4?style=for-the-badge&logoColor=white
305 | [blog-url]: https://masterpoint.io/updates/
306 | [newsletter-badge]: https://img.shields.io/badge/Newsletter-Subscribe-ECE295?style=for-the-badge&logoColor=222222
307 | [newsletter-url]: https://newsletter.masterpoint.io/
308 | [youtube-badge]: https://img.shields.io/badge/YouTube-Subscribe-D191BF?style=for-the-badge&logo=youtube&logoColor=white
309 | [youtube-url]: https://www.youtube.com/channel/UCeeDaO2NREVlPy9Plqx-9JQ
310 | [release-badge]: https://img.shields.io/github/v/release/masterpointio/terraform-aws-tailscale?color=0E383A&label=Release&style=for-the-badge&logo=github&logoColor=white
311 | [latest-release]: https://github.com/masterpointio/terraform-aws-tailscale/releases/latest
312 | [contributors-image]: https://contrib.rocks/image?repo=masterpointio/terraform-aws-tailscale
313 | [contributors-url]: https://github.com/masterpointio/terraform-aws-tailscale/graphs/contributors
314 | [issues-url]: https://github.com/masterpointio/terraform-aws-tailscale/issues
315 |
--------------------------------------------------------------------------------
/aqua.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # aqua - Declarative CLI Version Manager
3 | # https://aquaproj.github.io/
4 | # checksum:
5 | # enabled: true
6 | # require_checksum: true
7 | # supported_envs:
8 | # - all
9 | registries:
10 | - type: standard
11 | ref: v4.355.0 # renovate: depName=aquaproj/aqua-registry
12 | packages:
13 | - name: terraform-docs/terraform-docs@v0.20.0
14 | - name: hashicorp/terraform@v1.11.4
15 | tags: [terraform]
16 | - name: opentofu/opentofu@v1.9.1
17 | tags: [tofu]
18 |
--------------------------------------------------------------------------------
/context.tf:
--------------------------------------------------------------------------------
1 | #
2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label
3 | # All other instances of this file should be a copy of that one
4 | #
5 | #
6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf
7 | # and then place it in your Terraform module to automatically get
8 | # Cloud Posse's standard configuration inputs suitable for passing
9 | # to Cloud Posse modules.
10 | #
11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf
12 | #
13 | # Modules should access the whole context as `module.this.context`
14 | # to get the input variables with nulls for defaults,
15 | # for example `context = module.this.context`,
16 | # and access individual variables as `module.this.`,
17 | # with final values filled in.
18 | #
19 | # For example, when using defaults, `module.this.context.delimiter`
20 | # will be null, and `module.this.delimiter` will be `-` (hyphen).
21 | #
22 |
23 | module "this" {
24 | source = "cloudposse/label/null"
25 | version = "0.25.0" # requires Terraform >= 0.13.0
26 |
27 | enabled = var.enabled
28 | namespace = var.namespace
29 | tenant = var.tenant
30 | environment = var.environment
31 | stage = var.stage
32 | name = var.name
33 | delimiter = var.delimiter
34 | attributes = var.attributes
35 | tags = var.tags
36 | additional_tag_map = var.additional_tag_map
37 | label_order = var.label_order
38 | regex_replace_chars = var.regex_replace_chars
39 | id_length_limit = var.id_length_limit
40 | label_key_case = var.label_key_case
41 | label_value_case = var.label_value_case
42 | descriptor_formats = var.descriptor_formats
43 | labels_as_tags = var.labels_as_tags
44 |
45 | context = var.context
46 | }
47 |
48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here
49 |
50 | variable "context" {
51 | type = any
52 | default = {
53 | enabled = true
54 | namespace = null
55 | tenant = null
56 | environment = null
57 | stage = null
58 | name = null
59 | delimiter = null
60 | attributes = []
61 | tags = {}
62 | additional_tag_map = {}
63 | regex_replace_chars = null
64 | label_order = []
65 | id_length_limit = null
66 | label_key_case = null
67 | label_value_case = null
68 | descriptor_formats = {}
69 | # Note: we have to use [] instead of null for unset lists due to
70 | # https://github.com/hashicorp/terraform/issues/28137
71 | # which was not fixed until Terraform 1.0.0,
72 | # but we want the default to be all the labels in `label_order`
73 | # and we want users to be able to prevent all tag generation
74 | # by setting `labels_as_tags` to `[]`, so we need
75 | # a different sentinel to indicate "default"
76 | labels_as_tags = ["unset"]
77 | }
78 | description = <<-EOT
79 | Single object for setting entire context at once.
80 | See description of individual variables for details.
81 | Leave string and numeric variables as `null` to use default value.
82 | Individual variable settings (non-null) override settings in context object,
83 | except for attributes, tags, and additional_tag_map, which are merged.
84 | EOT
85 |
86 | validation {
87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"])
88 | error_message = "Allowed values: `lower`, `title`, `upper`."
89 | }
90 |
91 | validation {
92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"])
93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
94 | }
95 | }
96 |
97 | variable "enabled" {
98 | type = bool
99 | default = null
100 | description = "Set to false to prevent the module from creating any resources"
101 | }
102 |
103 | variable "namespace" {
104 | type = string
105 | default = null
106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique"
107 | }
108 |
109 | variable "tenant" {
110 | type = string
111 | default = null
112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for"
113 | }
114 |
115 | variable "environment" {
116 | type = string
117 | default = null
118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'"
119 | }
120 |
121 | variable "stage" {
122 | type = string
123 | default = null
124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'"
125 | }
126 |
127 | variable "name" {
128 | type = string
129 | default = null
130 | description = <<-EOT
131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
132 | This is the only ID element not also included as a `tag`.
133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input.
134 | EOT
135 | }
136 |
137 | variable "delimiter" {
138 | type = string
139 | default = null
140 | description = <<-EOT
141 | Delimiter to be used between ID elements.
142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all.
143 | EOT
144 | }
145 |
146 | variable "attributes" {
147 | type = list(string)
148 | default = []
149 | description = <<-EOT
150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
151 | in the order they appear in the list. New attributes are appended to the
152 | end of the list. The elements of the list are joined by the `delimiter`
153 | and treated as a single ID element.
154 | EOT
155 | }
156 |
157 | variable "labels_as_tags" {
158 | type = set(string)
159 | default = ["default"]
160 | description = <<-EOT
161 | Set of labels (ID elements) to include as tags in the `tags` output.
162 | Default is to include all labels.
163 | Tags with empty values will not be included in the `tags` output.
164 | Set to `[]` to suppress all generated tags.
165 | **Notes:**
166 | The value of the `name` tag, if included, will be the `id`, not the `name`.
167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
168 | changed in later chained modules. Attempts to change it will be silently ignored.
169 | EOT
170 | }
171 |
172 | variable "tags" {
173 | type = map(string)
174 | default = {}
175 | description = <<-EOT
176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
177 | Neither the tag keys nor the tag values will be modified by this module.
178 | EOT
179 | }
180 |
181 | variable "additional_tag_map" {
182 | type = map(string)
183 | default = {}
184 | description = <<-EOT
185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
186 | This is for some rare cases where resources want additional configuration of tags
187 | and therefore take a list of maps with tag key, value, and additional configuration.
188 | EOT
189 | }
190 |
191 | variable "label_order" {
192 | type = list(string)
193 | default = null
194 | description = <<-EOT
195 | The order in which the labels (ID elements) appear in the `id`.
196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"].
197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present.
198 | EOT
199 | }
200 |
201 | variable "regex_replace_chars" {
202 | type = string
203 | default = null
204 | description = <<-EOT
205 | Terraform regular expression (regex) string.
206 | Characters matching the regex will be removed from the ID elements.
207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits.
208 | EOT
209 | }
210 |
211 | variable "id_length_limit" {
212 | type = number
213 | default = null
214 | description = <<-EOT
215 | Limit `id` to this many characters (minimum 6).
216 | Set to `0` for unlimited length.
217 | Set to `null` for keep the existing setting, which defaults to `0`.
218 | Does not affect `id_full`.
219 | EOT
220 | validation {
221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0
222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length."
223 | }
224 | }
225 |
226 | variable "label_key_case" {
227 | type = string
228 | default = null
229 | description = <<-EOT
230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
231 | Does not affect keys of tags passed in via the `tags` input.
232 | Possible values: `lower`, `title`, `upper`.
233 | Default value: `title`.
234 | EOT
235 |
236 | validation {
237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case)
238 | error_message = "Allowed values: `lower`, `title`, `upper`."
239 | }
240 | }
241 |
242 | variable "label_value_case" {
243 | type = string
244 | default = null
245 | description = <<-EOT
246 | Controls the letter case of ID elements (labels) as included in `id`,
247 | set as tag values, and output by this module individually.
248 | Does not affect values of tags passed in via the `tags` input.
249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation).
250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
251 | Default value: `lower`.
252 | EOT
253 |
254 | validation {
255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case)
256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
257 | }
258 | }
259 |
260 | variable "descriptor_formats" {
261 | type = any
262 | default = {}
263 | description = <<-EOT
264 | Describe additional descriptors to be output in the `descriptors` output map.
265 | Map of maps. Keys are names of descriptors. Values are maps of the form
266 | `{
267 | format = string
268 | labels = list(string)
269 | }`
270 | (Type is `any` so the map values can later be enhanced to provide additional options.)
271 | `format` is a Terraform format string to be passed to the `format()` function.
272 | `labels` is a list of labels, in order, to pass to `format()` function.
273 | Label values will be normalized before being passed to `format()` so they will be
274 | identical to how they appear in `id`.
275 | Default is `{}` (`descriptors` output will be empty).
276 | EOT
277 | }
278 |
279 | #### End of copy of cloudposse/terraform-null-label/variables.tf
280 |
--------------------------------------------------------------------------------
/examples/complete/context.tf:
--------------------------------------------------------------------------------
1 | #
2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label
3 | # All other instances of this file should be a copy of that one
4 | #
5 | #
6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf
7 | # and then place it in your Terraform module to automatically get
8 | # Cloud Posse's standard configuration inputs suitable for passing
9 | # to Cloud Posse modules.
10 | #
11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf
12 | #
13 | # Modules should access the whole context as `module.this.context`
14 | # to get the input variables with nulls for defaults,
15 | # for example `context = module.this.context`,
16 | # and access individual variables as `module.this.`,
17 | # with final values filled in.
18 | #
19 | # For example, when using defaults, `module.this.context.delimiter`
20 | # will be null, and `module.this.delimiter` will be `-` (hyphen).
21 | #
22 |
23 | module "this" {
24 | source = "cloudposse/label/null"
25 | version = "0.25.0" # requires Terraform >= 0.13.0
26 |
27 | enabled = var.enabled
28 | namespace = var.namespace
29 | tenant = var.tenant
30 | environment = var.environment
31 | stage = var.stage
32 | name = var.name
33 | delimiter = var.delimiter
34 | attributes = var.attributes
35 | tags = var.tags
36 | additional_tag_map = var.additional_tag_map
37 | label_order = var.label_order
38 | regex_replace_chars = var.regex_replace_chars
39 | id_length_limit = var.id_length_limit
40 | label_key_case = var.label_key_case
41 | label_value_case = var.label_value_case
42 | descriptor_formats = var.descriptor_formats
43 | labels_as_tags = var.labels_as_tags
44 |
45 | context = var.context
46 | }
47 |
48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here
49 |
50 | variable "context" {
51 | type = any
52 | default = {
53 | enabled = true
54 | namespace = null
55 | tenant = null
56 | environment = null
57 | stage = null
58 | name = null
59 | delimiter = null
60 | attributes = []
61 | tags = {}
62 | additional_tag_map = {}
63 | regex_replace_chars = null
64 | label_order = []
65 | id_length_limit = null
66 | label_key_case = null
67 | label_value_case = null
68 | descriptor_formats = {}
69 | # Note: we have to use [] instead of null for unset lists due to
70 | # https://github.com/hashicorp/terraform/issues/28137
71 | # which was not fixed until Terraform 1.0.0,
72 | # but we want the default to be all the labels in `label_order`
73 | # and we want users to be able to prevent all tag generation
74 | # by setting `labels_as_tags` to `[]`, so we need
75 | # a different sentinel to indicate "default"
76 | labels_as_tags = ["unset"]
77 | }
78 | description = <<-EOT
79 | Single object for setting entire context at once.
80 | See description of individual variables for details.
81 | Leave string and numeric variables as `null` to use default value.
82 | Individual variable settings (non-null) override settings in context object,
83 | except for attributes, tags, and additional_tag_map, which are merged.
84 | EOT
85 |
86 | validation {
87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"])
88 | error_message = "Allowed values: `lower`, `title`, `upper`."
89 | }
90 |
91 | validation {
92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"])
93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
94 | }
95 | }
96 |
97 | variable "enabled" {
98 | type = bool
99 | default = null
100 | description = "Set to false to prevent the module from creating any resources"
101 | }
102 |
103 | variable "namespace" {
104 | type = string
105 | default = null
106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique"
107 | }
108 |
109 | variable "tenant" {
110 | type = string
111 | default = null
112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for"
113 | }
114 |
115 | variable "environment" {
116 | type = string
117 | default = null
118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'"
119 | }
120 |
121 | variable "stage" {
122 | type = string
123 | default = null
124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'"
125 | }
126 |
127 | variable "name" {
128 | type = string
129 | default = null
130 | description = <<-EOT
131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
132 | This is the only ID element not also included as a `tag`.
133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input.
134 | EOT
135 | }
136 |
137 | variable "delimiter" {
138 | type = string
139 | default = null
140 | description = <<-EOT
141 | Delimiter to be used between ID elements.
142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all.
143 | EOT
144 | }
145 |
146 | variable "attributes" {
147 | type = list(string)
148 | default = []
149 | description = <<-EOT
150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
151 | in the order they appear in the list. New attributes are appended to the
152 | end of the list. The elements of the list are joined by the `delimiter`
153 | and treated as a single ID element.
154 | EOT
155 | }
156 |
157 | variable "labels_as_tags" {
158 | type = set(string)
159 | default = ["default"]
160 | description = <<-EOT
161 | Set of labels (ID elements) to include as tags in the `tags` output.
162 | Default is to include all labels.
163 | Tags with empty values will not be included in the `tags` output.
164 | Set to `[]` to suppress all generated tags.
165 | **Notes:**
166 | The value of the `name` tag, if included, will be the `id`, not the `name`.
167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
168 | changed in later chained modules. Attempts to change it will be silently ignored.
169 | EOT
170 | }
171 |
172 | variable "tags" {
173 | type = map(string)
174 | default = {}
175 | description = <<-EOT
176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
177 | Neither the tag keys nor the tag values will be modified by this module.
178 | EOT
179 | }
180 |
181 | variable "additional_tag_map" {
182 | type = map(string)
183 | default = {}
184 | description = <<-EOT
185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
186 | This is for some rare cases where resources want additional configuration of tags
187 | and therefore take a list of maps with tag key, value, and additional configuration.
188 | EOT
189 | }
190 |
191 | variable "label_order" {
192 | type = list(string)
193 | default = null
194 | description = <<-EOT
195 | The order in which the labels (ID elements) appear in the `id`.
196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"].
197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present.
198 | EOT
199 | }
200 |
201 | variable "regex_replace_chars" {
202 | type = string
203 | default = null
204 | description = <<-EOT
205 | Terraform regular expression (regex) string.
206 | Characters matching the regex will be removed from the ID elements.
207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits.
208 | EOT
209 | }
210 |
211 | variable "id_length_limit" {
212 | type = number
213 | default = null
214 | description = <<-EOT
215 | Limit `id` to this many characters (minimum 6).
216 | Set to `0` for unlimited length.
217 | Set to `null` for keep the existing setting, which defaults to `0`.
218 | Does not affect `id_full`.
219 | EOT
220 | validation {
221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0
222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length."
223 | }
224 | }
225 |
226 | variable "label_key_case" {
227 | type = string
228 | default = null
229 | description = <<-EOT
230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
231 | Does not affect keys of tags passed in via the `tags` input.
232 | Possible values: `lower`, `title`, `upper`.
233 | Default value: `title`.
234 | EOT
235 |
236 | validation {
237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case)
238 | error_message = "Allowed values: `lower`, `title`, `upper`."
239 | }
240 | }
241 |
242 | variable "label_value_case" {
243 | type = string
244 | default = null
245 | description = <<-EOT
246 | Controls the letter case of ID elements (labels) as included in `id`,
247 | set as tag values, and output by this module individually.
248 | Does not affect values of tags passed in via the `tags` input.
249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation).
250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
251 | Default value: `lower`.
252 | EOT
253 |
254 | validation {
255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case)
256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
257 | }
258 | }
259 |
260 | variable "descriptor_formats" {
261 | type = any
262 | default = {}
263 | description = <<-EOT
264 | Describe additional descriptors to be output in the `descriptors` output map.
265 | Map of maps. Keys are names of descriptors. Values are maps of the form
266 | `{
267 | format = string
268 | labels = list(string)
269 | }`
270 | (Type is `any` so the map values can later be enhanced to provide additional options.)
271 | `format` is a Terraform format string to be passed to the `format()` function.
272 | `labels` is a list of labels, in order, to pass to `format()` function.
273 | Label values will be normalized before being passed to `format()` so they will be
274 | identical to how they appear in `id`.
275 | Default is `{}` (`descriptors` output will be empty).
276 | EOT
277 | }
278 |
279 | #### End of copy of cloudposse/terraform-null-label/variables.tf
280 |
--------------------------------------------------------------------------------
/examples/complete/fixtures.us-east-2.tfvars:
--------------------------------------------------------------------------------
1 | enabled = true
2 |
3 | namespace = "eg"
4 | stage = "test"
5 | name = "tailscale"
6 |
7 | region = "us-east-1"
8 | availability_zones = ["us-east-1a", "us-east-1b"]
9 | ipv4_primary_cidr_block = "172.16.0.0/16"
10 |
11 | ssm_state_enabled = true
12 |
13 | # Replace these values with your own
14 | tailnet = "orgname.org.github"
15 | oauth_client_id = "OAUTH_CLIENT_ID"
16 | oauth_client_secret = "OAUTH_CLIENT_SECRET"
17 |
--------------------------------------------------------------------------------
/examples/complete/main.tf:
--------------------------------------------------------------------------------
1 | # trunk-ignore-all(trivy/AVD-AWS-0178): We don't need have VPC Flow logs.
2 | provider "aws" {
3 | region = var.region
4 | }
5 |
6 | provider "tailscale" {
7 | tailnet = var.tailnet
8 | oauth_client_id = var.oauth_client_id
9 | oauth_client_secret = var.oauth_client_secret
10 | }
11 |
12 | module "vpc" {
13 | source = "cloudposse/vpc/aws"
14 | version = "2.1.1"
15 |
16 | ipv4_primary_cidr_block = "172.16.0.0/16"
17 |
18 | context = module.this.context
19 | }
20 |
21 | module "subnets" {
22 | source = "cloudposse/dynamic-subnets/aws"
23 | version = "2.4.1"
24 |
25 | availability_zones = var.availability_zones
26 |
27 | vpc_id = module.vpc.vpc_id
28 | igw_id = [module.vpc.igw_id]
29 | ipv4_cidr_block = [module.vpc.vpc_cidr_block]
30 |
31 | context = module.this.context
32 | }
33 |
34 | module "tailscale" {
35 | source = "../.."
36 |
37 | vpc_id = module.vpc.vpc_id
38 | subnet_ids = module.subnets.private_subnet_ids
39 | advertise_routes = [module.vpc.vpc_cidr_block]
40 |
41 | context = module.this.context
42 | }
43 |
--------------------------------------------------------------------------------
/examples/complete/outputs.tf:
--------------------------------------------------------------------------------
1 | output "instance_name" {
2 | value = module.tailscale.instance_name
3 | description = "The name tag value of the Bastion instance."
4 | }
5 |
6 | output "security_group_id" {
7 | value = module.tailscale.security_group_id
8 | description = "The ID of the SSM Agent Security Group."
9 | }
10 |
11 | output "launch_template_id" {
12 | value = module.tailscale.launch_template_id
13 | description = "The ID of the SSM Agent Launch Template."
14 | }
15 |
16 | output "autoscaling_group_id" {
17 | value = module.tailscale.autoscaling_group_id
18 | description = "The ID of the SSM Agent Autoscaling Group."
19 | }
20 |
--------------------------------------------------------------------------------
/examples/complete/variables.tf:
--------------------------------------------------------------------------------
1 | variable "availability_zones" {
2 | type = list(string)
3 | description = "List of Availability Zones where subnets will be created"
4 | }
5 |
6 | variable "oauth_client_id" {
7 | type = string
8 | description = <<-EOF
9 | The OAuth application's ID when using OAuth client credentials.
10 | Can be set via the TAILSCALE_OAUTH_CLIENT_ID environment variable.
11 | Both 'oauth_client_id' and 'oauth_client_secret' must be set.
12 | Conflicts with 'api_key'.
13 | EOF
14 | }
15 |
16 | variable "oauth_client_secret" {
17 | type = string
18 | description = <<-EOF
19 | (Sensitive) The OAuth application's secret when using OAuth client credentials.
20 | Can be set via the TAILSCALE_OAUTH_CLIENT_SECRET environment variable.
21 | Both 'oauth_client_id' and 'oauth_client_secret' must be set.
22 | Conflicts with 'api_key'.
23 | EOF
24 | }
25 |
26 | variable "region" {
27 | type = string
28 | description = "The AWS Region to deploy these resources to."
29 | }
30 |
31 | variable "tailnet" {
32 | type = string
33 | description = <<-EOF
34 | The organization name of the Tailnet in which to perform actions.
35 | Can be set via the TAILSCALE_TAILNET environment variable.
36 | Default is the tailnet that owns API credentials passed to the provider.
37 | EOF
38 | }
39 |
--------------------------------------------------------------------------------
/examples/complete/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.5"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = "~> 5.0"
8 | }
9 | tailscale = {
10 | source = "tailscale/tailscale"
11 | version = "~> 0.13"
12 | }
13 | local = {
14 | source = "hashicorp/local"
15 | version = "~> 2.4"
16 | }
17 | null = {
18 | source = "hashicorp/null"
19 | version = "~> 3.0"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/main.tf:
--------------------------------------------------------------------------------
1 | locals {
2 |
3 | primary_tag = coalesce(var.primary_tag, module.this.id)
4 | prefixed_primary_tag = "tag:${local.primary_tag}"
5 | prefixed_additional_tags = [for tag in var.additional_tags : "tag:${tag}"]
6 |
7 | ssm_state_param_name = var.ssm_state_enabled ? "/tailscale/${module.this.id}/state" : null
8 | ssm_state_flag = var.ssm_state_enabled ? "--state=${module.ssm_state[0].arn_map[local.ssm_state_param_name]}" : ""
9 |
10 | tailscale_tags = concat([local.prefixed_primary_tag], local.prefixed_additional_tags)
11 |
12 | tailscaled_extra_flags = join(" ", compact(concat(var.tailscaled_extra_flags, [local.ssm_state_flag])))
13 | tailscaled_extra_flags_enabled = length(local.tailscaled_extra_flags) > 0
14 |
15 | tailscale_up_extra_flags_enabled = length(var.tailscale_up_extra_flags) > 0
16 |
17 | userdata = templatefile("${path.module}/userdata.sh.tmpl", {
18 | authkey = tailscale_tailnet_key.default.key
19 | exit_node_enabled = var.exit_node_enabled
20 | hostname = module.this.id
21 | routes = join(",", var.advertise_routes)
22 | ssh_enabled = var.ssh_enabled
23 | tags = join(",", local.tailscale_tags)
24 |
25 | tailscaled_extra_flags_enabled = local.tailscaled_extra_flags_enabled
26 | tailscaled_extra_flags = local.tailscaled_extra_flags
27 | tailscale_up_extra_flags_enabled = local.tailscale_up_extra_flags_enabled
28 | tailscale_up_extra_flags = join(" ", var.tailscale_up_extra_flags)
29 |
30 | journald_system_max_use = var.journald_system_max_use
31 | journald_max_retention_sec = var.journald_max_retention_sec
32 | })
33 | }
34 |
35 | # Note: `trunk` ignores that this rule is already listed in `.trivyignore` file.
36 | # Bucket does not have versioning enabled
37 | # trivy:ignore:AVD-AWS-0090
38 | module "tailscale_subnet_router" {
39 | source = "masterpointio/ssm-agent/aws"
40 | version = "1.4.0"
41 |
42 | context = module.this.context
43 | tags = module.this.tags
44 |
45 | vpc_id = var.vpc_id
46 | subnet_ids = var.subnet_ids
47 | key_pair_name = var.key_pair_name
48 | create_run_shell_document = var.create_run_shell_document
49 |
50 | additional_security_group_ids = var.additional_security_group_ids
51 | additional_security_group_rules = var.additional_security_group_rules
52 |
53 | session_logging_kms_key_alias = var.session_logging_kms_key_alias
54 | session_logging_enabled = var.session_logging_enabled
55 | session_logging_ssm_document_name = var.session_logging_ssm_document_name
56 |
57 | ami = var.ami
58 | architecture = var.architecture
59 | instance_type = var.instance_type
60 | max_size = var.max_size
61 | min_size = var.min_size
62 | desired_capacity = var.desired_capacity
63 |
64 | monitoring_enabled = var.monitoring_enabled
65 | associate_public_ip_address = var.associate_public_ip_address
66 |
67 | user_data = base64encode(length(var.user_data) > 0 ? var.user_data : local.userdata)
68 | }
69 |
70 | resource "tailscale_tailnet_key" "default" {
71 | reusable = var.reusable
72 | ephemeral = var.ephemeral
73 | preauthorized = var.preauthorized
74 | expiry = var.expiry
75 |
76 | # A device is automatically tagged when it is authenticated with this key.
77 | tags = local.tailscale_tags
78 | }
79 |
80 | module "ssm_state" {
81 | count = var.ssm_state_enabled ? 1 : 0
82 | source = "cloudposse/ssm-parameter-store/aws"
83 | version = "0.13.0"
84 | ignore_value_changes = true
85 |
86 | parameter_write = [
87 | {
88 | name = local.ssm_state_param_name
89 | type = "SecureString"
90 | overwrite = "true"
91 | value = "{}"
92 | description = "Tailscaled state of ${module.this.id} subnet router."
93 | }
94 | ]
95 | context = module.this.context
96 | tags = module.this.tags
97 | }
98 |
99 | module "ssm_policy" {
100 | count = var.ssm_state_enabled ? 1 : 0
101 | source = "cloudposse/iam-policy/aws"
102 | version = "2.0.1"
103 |
104 | name = "ssm"
105 | description = "Additional SSM access for SSM Agent"
106 |
107 | iam_policy_enabled = true
108 | iam_policy = [{
109 | statements = [
110 | {
111 | sid = "SSMAgentPutParameter"
112 | effect = "Allow"
113 | actions = ["ssm:PutParameter"]
114 | resources = [
115 | module.ssm_state[0].arn_map[local.ssm_state_param_name],
116 | ]
117 | },
118 | ]
119 | }]
120 | context = module.this.context
121 | tags = module.this.tags
122 | }
123 |
124 | resource "aws_iam_role_policy_attachment" "default" {
125 | count = var.ssm_state_enabled ? 1 : 0
126 | role = module.tailscale_subnet_router.role_id
127 | policy_arn = module.ssm_policy[0].policy_arn
128 | }
129 |
130 | resource "aws_iam_role_policy_attachment" "cw_agent" {
131 | role = module.tailscale_subnet_router.role_id
132 | policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
133 | }
134 |
--------------------------------------------------------------------------------
/outputs.tf:
--------------------------------------------------------------------------------
1 | output "instance_name" {
2 | value = module.this.id
3 | description = "The name tag value of the Tailscale Subnet Router EC2 instance."
4 | }
5 |
6 | output "security_group_id" {
7 | value = module.tailscale_subnet_router.security_group_id
8 | description = "The ID of the Tailscale Subnet Router EC2 instance Security Group."
9 | }
10 |
11 | output "launch_template_id" {
12 | value = module.tailscale_subnet_router.launch_template_id
13 | description = "The ID of the Tailscale Subnet Router EC2 instance Launch Template."
14 | }
15 |
16 | output "autoscaling_group_id" {
17 | value = module.tailscale_subnet_router.autoscaling_group_id
18 | description = "The ID of the Tailscale Subnet Router EC2 instance Autoscaling Group."
19 | }
20 |
--------------------------------------------------------------------------------
/userdata.sh.tmpl:
--------------------------------------------------------------------------------
1 | #!/bin/bash -ex
2 | exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1
3 |
4 | echo "Starting user-data script..."
5 |
6 | echo "Enabling IP forwarding..."
7 | echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
8 | echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf
9 | sysctl -p /etc/sysctl.conf
10 |
11 | # In systemd, Administrator drop-ins should reside in /etc/systemd/, ensuring they
12 | # are preserved across updates and have higher precedence than vendor defaults.
13 | #
14 | # We name our file 99-custom.conf so it loads last among any .conf files.
15 | # That way, it overrides any settings that come earlier.
16 |
17 | # Create the journald configs directory if it doesn't already exist
18 | mkdir -p /etc/systemd/journald.conf.d
19 |
20 | cat < /etc/systemd/journald.conf.d/99-custom.conf
21 | [Journal]
22 | SystemMaxUse=${journald_system_max_use}
23 | MaxRetentionSec=${journald_max_retention_sec}
24 | EOF
25 |
26 | # Restart journald so it picks up the new configuration
27 | systemctl restart systemd-journald
28 |
29 | # Function to retry a command up to a maximum number of attempts
30 | retry_command() {
31 | local cmd="$1"
32 | local max_attempts="$2"
33 | local attempt=1
34 | local exit_code=0
35 |
36 | while [ $attempt -le $max_attempts ]; do
37 | echo "Attempt $attempt of $max_attempts: $cmd"
38 | eval "$cmd"
39 | exit_code=$?
40 | if [ $exit_code -eq 0 ]; then
41 | echo "Command succeeded: $cmd"
42 | return 0
43 | else
44 | echo "Command failed with exit code $exit_code: $cmd"
45 | attempt=$((attempt + 1))
46 | if [ $attempt -le $max_attempts ]; then
47 | echo "Retrying in 2 seconds..."
48 | sleep 2
49 | fi
50 | fi
51 | done
52 |
53 | echo "Command failed after $max_attempts attempts: $cmd"
54 | return $exit_code
55 | }
56 |
57 | # Install CloudWatch Agent
58 | echo "Installing CloudWatch Agent..."
59 | retry_command "dnf install -y amazon-cloudwatch-agent" 5
60 | amazon-cloudwatch-agent-ctl -a start -m ec2
61 |
62 | # Install Tailscale
63 | echo "Installing Tailscale..."
64 | retry_command "dnf install -y dnf-utils" 5
65 | retry_command "dnf config-manager --add-repo https://pkgs.tailscale.com/stable/amazon-linux/2/tailscale.repo" 5
66 | retry_command "dnf install -y tailscale" 5
67 |
68 | %{ if tailscaled_extra_flags_enabled == true }
69 | echo "Exporting FLAGS to /etc/default/tailscaled..."
70 | sed -i "s|^FLAGS=.*|FLAGS=\"${tailscaled_extra_flags}\"|" /etc/default/tailscaled
71 | %{ endif }
72 |
73 | # Setup Tailscale
74 | echo "Enabling and starting tailscaled service..."
75 | systemctl enable --now tailscaled
76 |
77 | echo "Waiting for tailscaled to initialize..."
78 | sleep 5
79 |
80 | # Start tailscale
81 | # We pass --advertise-tags below even though the authkey being created with those tags should result
82 | # in the same effect. This is to be more explicit because tailscale tags are a complicated topic.
83 | tailscale up \
84 | %{ if ssh_enabled == true }--ssh%{ endif } \
85 | %{ if exit_node_enabled == true }--advertise-exit-node%{ endif } \
86 | %{ if tailscale_up_extra_flags_enabled == true }${tailscale_up_extra_flags}%{ endif } \
87 | --advertise-routes=${routes} \
88 | --advertise-tags=${tags} \
89 | --hostname=${hostname} \
90 | --authkey=${authkey}
91 |
92 | echo "Tailscale setup completed."
93 |
--------------------------------------------------------------------------------
/variables.tf:
--------------------------------------------------------------------------------
1 | #################################
2 | ## Subnet Router EC2 Instance ##
3 | ###############################
4 |
5 | variable "vpc_id" {
6 | type = string
7 | description = "The ID of the VPC which the Tailscale Subnet Router EC2 instance will run in."
8 | }
9 |
10 | variable "subnet_ids" {
11 | type = list(string)
12 | description = "The Subnet IDs which the Tailscale Subnet Router EC2 instance will run in. These *should* be private subnets."
13 | }
14 |
15 | variable "additional_security_group_ids" {
16 | default = []
17 | type = list(string)
18 | description = "Additional Security Group IDs to associate with the Tailscale Subnet Router EC2 instance."
19 | }
20 |
21 | variable "additional_security_group_rules" {
22 | description = "Additional security group rules that will be attached to the primary security group"
23 | type = map(object({
24 | type = string
25 | from_port = number
26 | to_port = number
27 | protocol = string
28 |
29 | description = optional(string)
30 | cidr_blocks = optional(list(string))
31 | ipv6_cidr_blocks = optional(list(string))
32 | prefix_list_ids = optional(list(string))
33 | self = optional(bool)
34 | }))
35 | default = {}
36 | }
37 |
38 | variable "create_run_shell_document" {
39 | default = true
40 | type = bool
41 | description = "Whether or not to create the SSM-SessionManagerRunShell SSM Document."
42 | }
43 |
44 | variable "session_logging_enabled" {
45 | default = true
46 | type = bool
47 | description = <