├── .github ├── dependabot.yml └── workflows │ └── cookiecutter.yml ├── .gitignore ├── LICENSE ├── README.md ├── cookiecutter.json ├── makefile ├── tests └── settings.json └── {{cookiecutter.name}} ├── .github ├── dependabot.yml └── workflows │ ├── checkov.yml │ ├── documentation.yml │ ├── formatting.yml │ ├── terraform_tests.yml │ ├── terratest.yml │ ├── tflint.yml │ ├── trivy.yml │ └── validation.yml ├── .gitignore ├── .opentofu-version ├── .pre-commit-config.yaml ├── .terraform-docs.yml ├── .terraform-version ├── .tflint.hcl ├── LICENSE ├── README.md ├── examples └── basic │ ├── main.tf │ └── tests │ └── basic.tftest.hcl ├── main.tf ├── makefile ├── outputs.tf ├── providers.tf ├── terratest └── basic_test.go ├── tests └── basic.tftest.hcl └── variables.tf /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/cookiecutter.yml: -------------------------------------------------------------------------------- 1 | name: Test Build Template 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | rebuild-templates: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.12" 18 | 19 | - name: "Install Cookiecutter" 20 | run: pip install --user cookiecutter 21 | 22 | - name: "Build Template" 23 | run: make test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Robert Hafner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Module Cookiecutter 2 | 3 | This CookieCutter Template generates Terraform Modules with all the best practices. 4 | 5 | To use this template install [CookieCutter](https://www.cookiecutter.io/) and run this command: 6 | 7 | ```bash 8 | cookiecutter gh:TerraformInDepth/terraform-module-cookiecutter 9 | ``` 10 | 11 | You will then be asked a few questions, such as what provider you are using, which will be used to generate a customized project. 12 | 13 | If your project has custom (but repeated) needs you can always fork this template to add any customizations your team might want. 14 | 15 | ## Features 16 | 17 | * Security Scanning with Checkov and Trivy. 18 | * Quality Control with TFLint. 19 | * Formatting and Validation with Terraform and OpenTofu. 20 | * CI using Github Actions Workflows. 21 | * Git Hooks with the Pre-Commit Framework. 22 | * Terraform and OpenTofu version management with tenv. 23 | * Testing with Terratest and Terraform Testing Framework. 24 | 25 | ## Terraform in Depth 26 | 27 | This template was developed alongside [Terraform in Depth](https://mng.bz/QR21), which was published in February 2025. The version of this template released with the book was tagged [v1.0.0](https://github.com/TerraformInDepth/terraform-module-cookiecutter/tree/v1.0.0), but as an actively maintained project additional changes have been made since then. 28 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "license": [ 4 | "All Rights Reserved", 5 | "MIT license", 6 | "BSD license", 7 | "Apache Software License 2.0", 8 | "GNU General Public License v3" 9 | ], 10 | "author": "", 11 | "minimum_terraform_version": "1.5", 12 | "primary_provider": "hashicorp/aws", 13 | "provider_min_version": "5.0", 14 | "private_registry_url": "", 15 | "__short_primary_provider": "{{ cookiecutter.primary_provider.split('/')[-1] }}", 16 | "_copy_without_render": [ 17 | "*workflows" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | build: 4 | mkdir -p build 5 | 6 | test: build 7 | rm -Rf build/terraform-example-module 8 | cookiecutter --no-input --config-file=cookiecutter.json --output-dir build/terraform-example-module . 9 | -------------------------------------------------------------------------------- /tests/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terraform-example-module", 3 | "license": "MIT license", 4 | "author": "Robert Hafner" 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/checkov.yml: -------------------------------------------------------------------------------- 1 | name: Checkov 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tflint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | name: Checkout source code 16 | 17 | - name: Run Checkov action 18 | id: checkov 19 | uses: bridgecrewio/checkov-action@v12 20 | with: 21 | directory: . 22 | framework: terraform 23 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Teraform-Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tflint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | name: Checkout source code 16 | 17 | - name: Run Terraform-Docs 18 | run: make test_documentation 19 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/formatting.yml: -------------------------------------------------------------------------------- 1 | name: Formatting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | formatting: 11 | strategy: 12 | matrix: 13 | engine: ["opentofu", "terraform"] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Terraform 19 | uses: hashicorp/setup-terraform@v3 20 | if: ${{ matrix.engine == 'terraform' }} 21 | 22 | - name: Install OpenTofu 23 | uses: opentofu/setup-opentofu@v1 24 | if: ${{ matrix.engine == 'opentofu' }} 25 | 26 | - name: Test Formatting 27 | run: make test_formatting TF_ENGINE=${{matrix.engine}} 28 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/terraform_tests.yml: -------------------------------------------------------------------------------- 1 | name: Terraform Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | terraform_test: 11 | strategy: 12 | matrix: 13 | engine: ["opentofu", "terraform"] 14 | version: ["1.6", "1.7", "1.8", "1.9"] 15 | experimental: [false] 16 | include: 17 | - version: "1.10" 18 | engine: "terraform" 19 | experimental: false 20 | 21 | continue-on-error: ${{ matrix.experimental }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install Terraform 27 | uses: hashicorp/setup-terraform@v3 28 | if: ${{ matrix.engine == 'terraform' }} 29 | with: 30 | terraform_version: ${{ matrix.version }} 31 | 32 | - name: Install OpenTofu 33 | uses: opentofu/setup-opentofu@v1 34 | if: ${{ matrix.engine == 'opentofu' }} 35 | with: 36 | tofu_version: ${{ matrix.version }} 37 | 38 | - name: Run Terraform Tests 39 | run: make terraform_test TF_ENGINE=${{matrix.engine}} 40 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/terratest.yml: -------------------------------------------------------------------------------- 1 | name: Terratest 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | terratest: 11 | strategy: 12 | matrix: 13 | engine: ["opentofu", "terraform"] 14 | version: ["1.6", "1.7", "1.8", "1.9"] 15 | experimental: [false] 16 | include: 17 | - version: "1.10" 18 | engine: "terraform" 19 | experimental: false 20 | 21 | continue-on-error: ${{ matrix.experimental }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install Terraform 27 | uses: hashicorp/setup-terraform@v3 28 | if: ${{ matrix.engine == 'terraform' }} 29 | with: 30 | terraform_version: ${{ matrix.version }} 31 | terraform_wrapper: false 32 | 33 | - name: Install OpenTofu 34 | uses: opentofu/setup-opentofu@v1 35 | if: ${{ matrix.engine == 'opentofu' }} 36 | with: 37 | tofu_version: ${{ matrix.version }} 38 | tofu_wrapper: false 39 | 40 | - name: Run Terratest 41 | run: make terratest TF_ENGINE=${{matrix.engine}} 42 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/tflint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tflint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | name: Checkout source code 14 | 15 | - uses: terraform-linters/setup-tflint@v3 16 | name: Setup TFLint 17 | 18 | - name: Run TFLint 19 | run: make test_tflint 20 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: Trivy 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tflint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | name: Checkout source code 14 | 15 | - name: Trivy 16 | uses: aquasecurity/trivy-action@master 17 | with: 18 | scan-type: "config" 19 | hide-progress: true 20 | exit-code: "1" 21 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Validation 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | validation: 9 | strategy: 10 | matrix: 11 | engine: ["opentofu", "terraform"] 12 | version: ["1.6", "1.7", "1.8", "1.9"] 13 | experimental: [false] 14 | include: 15 | - version: "1.10" 16 | engine: "terraform" 17 | experimental: false 18 | 19 | continue-on-error: ${{ matrix.experimental }} 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Install Terraform 25 | uses: hashicorp/setup-terraform@v3 26 | if: ${{ matrix.engine == 'terraform' }} 27 | with: 28 | terraform_version: ${{ matrix.version }} 29 | 30 | - name: Install OpenTofu 31 | uses: opentofu/setup-opentofu@v1 32 | if: ${{ matrix.engine == 'opentofu' }} 33 | with: 34 | tofu_version: ${{ matrix.version }} 35 | 36 | - name: Test Validation 37 | run: make test_validation TF_ENGINE=${{matrix.engine}} 38 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | *.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.opentofu-version: -------------------------------------------------------------------------------- 1 | latest-allowed 2 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: format 5 | name: Test format 6 | entry: make test_format 7 | language: system 8 | pass_filenames: false 9 | - id: validation 10 | name: Test validation 11 | entry: make test_validation 12 | language: system 13 | pass_filenames: false 14 | - id: documentation 15 | name: Test documentation 16 | entry: make test_documentation 17 | language: system 18 | pass_filenames: false 19 | - id: lint 20 | name: Test lint 21 | entry: make test_tflint 22 | language: system 23 | pass_filenames: false 24 | - id: security 25 | name: Test security 26 | entry: make test_security 27 | language: system 28 | pass_filenames: false 29 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | formatter: "markdown" 2 | 3 | output: 4 | file: "README.md" 5 | mode: inject 6 | 7 | sort: 8 | enabled: true 9 | by: required 10 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.terraform-version: -------------------------------------------------------------------------------- 1 | latest-allowed 2 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/.tflint.hcl: -------------------------------------------------------------------------------- 1 | plugin "terraform" { 2 | enabled = true 3 | preset = "all" 4 | } 5 | 6 | plugin "opa" { 7 | enabled = false 8 | version = "0.7.0" 9 | source = "github.com/terraform-linters/tflint-ruleset-opa" 10 | } 11 | 12 | {% if cookiecutter.__short_primary_provider == "aws" %} 13 | plugin "aws" { 14 | enabled = true 15 | version = "0.38.0" 16 | source = "github.com/terraform-linters/tflint-ruleset-aws" 17 | } 18 | {% endif %} 19 | 20 | {% if cookiecutter.__short_primary_provider == "gcp" %} 21 | plugin "google" { 22 | enabled = true 23 | version = "0.31.0" 24 | source = "github.com/terraform-linters/tflint-ruleset-google" 25 | } 26 | {% endif %} 27 | 28 | {% if cookiecutter.__short_primary_provider == "azurerm" %} 29 | plugin "azurerm" { 30 | enabled = true 31 | version = "0.28.0" 32 | source = "github.com/terraform-linters/tflint-ruleset-azurerm" 33 | } 34 | {% endif %} 35 | 36 | 37 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/LICENSE: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.license == 'MIT license' -%} 2 | MIT License 3 | 4 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | {% elif cookiecutter.license == 'BSD license' %} 24 | 25 | BSD License 26 | 27 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} 28 | All rights reserved. 29 | 30 | Redistribution and use in source and binary forms, with or without modification, 31 | are permitted provided that the following conditions are met: 32 | 33 | * Redistributions of source code must retain the above copyright notice, this 34 | list of conditions and the following disclaimer. 35 | 36 | * Redistributions in binary form must reproduce the above copyright notice, this 37 | list of conditions and the following disclaimer in the documentation and/or 38 | other materials provided with the distribution. 39 | 40 | * Neither the name of the copyright holder nor the names of its 41 | contributors may be used to endorse or promote products derived from this 42 | software without specific prior written permission. 43 | 44 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 45 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 46 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 47 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 48 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 49 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 50 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 51 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 52 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 53 | OF THE POSSIBILITY OF SUCH DAMAGE. 54 | {% elif cookiecutter.license == 'Apache Software License 2.0' -%} 55 | Apache Software License 2.0 56 | 57 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} 58 | 59 | Licensed under the Apache License, Version 2.0 (the "License"); 60 | you may not use this file except in compliance with the License. 61 | You may obtain a copy of the License at 62 | 63 | 64 | 65 | Unless required by applicable law or agreed to in writing, software 66 | distributed under the License is distributed on an "AS IS" BASIS, 67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 68 | See the License for the specific language governing permissions and 69 | limitations under the License. 70 | {% elif cookiecutter.license == 'GNU General Public License v3' -%} 71 | GNU GENERAL PUBLIC LICENSE 72 | Version 3, 29 June 2007 73 | 74 | {{ cookiecutter.project_short_description }} 75 | Copyright (C) {% now 'local', '%Y' %} {{ cookiecutter.author }} 76 | 77 | This program is free software: you can redistribute it and/or modify 78 | it under the terms of the GNU General Public License as published by 79 | the Free Software Foundation, either version 3 of the License, or 80 | (at your option) any later version. 81 | 82 | This program is distributed in the hope that it will be useful, 83 | but WITHOUT ANY WARRANTY; without even the implied warranty of 84 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 85 | GNU General Public License for more details. 86 | 87 | You should have received a copy of the GNU General Public License 88 | along with this program. If not, see . 89 | 90 | Also add information on how to contact you by electronic and paper mail. 91 | 92 | You should also get your employer (if you work as a programmer) or school, 93 | if any, to sign a "copyright disclaimer" for the program, if necessary. 94 | For more information on this, and how to apply and follow the GNU GPL, see 95 | . 96 | 97 | The GNU General Public License does not permit incorporating your program 98 | into proprietary programs. If your program is a subroutine library, you 99 | may consider it more useful to permit linking proprietary applications with 100 | the library. If this is what you want to do, use the GNU Lesser General 101 | Public License instead of this License. But first, please read 102 | . 103 | {% elif cookiecutter.license == 'All Rights Reserved' -%} 104 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }}, All Rights Reserved 105 | {% endif -%} 106 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.name}} 2 | 3 | This package was automatically generated by the [Terraform in Depth Cookiecutter Template](https://github.com/TerraformInDepth/terraform-module-cookiecutter). You should replace this description with some information about your project. 4 | 5 | ## API 6 | 7 | 8 | This section will be automatically generated with Terraform-Docs. Run `make documentation` after making changes. 9 | 10 | 11 | ## Development 12 | 13 | ### Dependencies 14 | 15 | This project can install most dependencies automatically using a package manager, so please make sure they are installed. 16 | 17 | * Windows: [Chocolatey](https://chocolatey.org/) 18 | * MacOS: [Homebrew](https://brew.sh/) 19 | 20 | Now run `make install` and most tools will be installed for you. 21 | 22 | > [!WARNING] 23 | > [pre-commit](https://pre-commit.com/#install) and [Checkov](https://www.checkov.io/2.Basics/Installing%20Checkov.html) need to be installed manually on Windows. 24 | 25 | ### Pre Commit 26 | 27 | The Pre-Commit framework is used to manage and install pre-commit hooks on your local machine. After cloning this repository you can run `make precommit_install` to initialize the hooks. This only needs to be done once after cloning. 28 | 29 | ### Running Chores 30 | 31 | The `make chores` command will automatically update documentation using Terraform-Docs, and will run automatic formatting. 32 | 33 | ### Security Checks 34 | 35 | This project uses Trivy and Checkov for security scanning. You can run `make test_security` to run both tools, while `make test_trivy` and `make test_checkov` run each component on its own. 36 | 37 | ### Linting 38 | 39 | To run TFLint use the command `make test_tflint`. 40 | 41 | It is possible to automatically apply some fixes, but these should be reviewed before running. If you are comfortable with all of the results from `make test_tflint` being fixed automatically then run `make fix_tflint`. 42 | 43 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/examples/basic/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | {{cookiecutter.__short_primary_provider}} = { 4 | source = "{{cookiecutter.primary_provider}}" 5 | version = "~> {{cookiecutter.provider_min_version}}" 6 | } 7 | } 8 | required_version = "~> {{cookiecutter.minimum_terraform_version}}" 9 | } 10 | 11 | # Replace these variables with the ones for your tests. 12 | variable "test_input" { 13 | type = string 14 | default = "test" 15 | } 16 | 17 | # Pass in any variables that the module requires. 18 | # If your module has a `name` field don't forget to add some randomness. 19 | module "basic_example" { 20 | source = "../../" 21 | input = var.test_input 22 | } 23 | 24 | # Replace this output with the one for your tests. 25 | output "test_output" { 26 | value = module.basic_example.output 27 | } 28 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/examples/basic/tests/basic.tftest.hcl: -------------------------------------------------------------------------------- 1 | variables { 2 | test_input = "test" 3 | } 4 | 5 | run "input_and_output_match" { 6 | 7 | assert { 8 | condition = output.test_output == "test" 9 | error_message = "The output does not match the input." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "terraform_data" "this" { 3 | input = { 4 | example = var.input 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/makefile: -------------------------------------------------------------------------------- 1 | # Get the set versions of Terraform and OpenTofu. 2 | TERRAFORM_VERSION:=$(shell cat .terraform-version) 3 | TOFU_VERSION:=$(shell cat .terraform-version) 4 | 5 | # Select the engine: opentofu or terraform. 6 | # make validate TF_ENGINE=opentofu 7 | # This will switch the engine for all actions, including testing. 8 | TF_ENGINE:=terraform 9 | ifeq ($(TF_ENGINE), terraform) 10 | TF_BINARY:=terraform 11 | TF_VERSION:=$(TERRAFORM_VERSION) 12 | else ifeq ($(TF_ENGINE), opentofu) 13 | TF_BINARY:=tofu 14 | TF_VERSION:=$(TOFU_VERSION) 15 | endif 16 | 17 | ifeq ($(CI), ) 18 | TFENV_COMMAND:=tenv use $(TF_BINARY) $(TF_VERSION) 19 | else 20 | TFENV_COMMAND:=echo "skipping tenv use in CI" 21 | endif 22 | 23 | 24 | # Packages to install based on different package managers. 25 | BREW_PACKAGES := cosign tenv terraform-docs tflint checkov trivy pre-commit golang 26 | CHOCOLATEY_PACKAGES := cosign tenv terraform-docs tflint trivy golang 27 | APT_PACKAGES := 28 | APK_PACKAGES := cosign 29 | 30 | # Autogenerated based off on the system itself. 31 | # Github Actions installs homebrew on linux machines, so we check for apt first. 32 | INSTALLER_PATH := $(shell { command -v apt || command -v brew || command -v choco ; } 2>/dev/null) 33 | INSTALLER := $(shell { basename $(INSTALLER_PATH) ; } 2>/dev/null) 34 | MODULE_DIRECTORY_NAME := $(shell { pwd | rev | cut -d"/" -f 1 | rev ; } 2>/dev/null) 35 | TERRATEST_FILES:=$(wildcard tests/*_test.go) 36 | 37 | # Empty variables primarily used to allow users to pass in their own options. 38 | CHECKOV_OPTS:= 39 | GO_TEST_OPTS:= 40 | 41 | # General 42 | all: 43 | 44 | chores: documentation formatting 45 | 46 | test: test_documentation test_tflint test_security test_validation test_formatting 47 | 48 | # 49 | # Install 50 | # 51 | 52 | install: install_$(INSTALLER) 53 | 54 | install_brew: 55 | brew tap tofuutils/tap 56 | brew install $(BREW_PACKAGES) 57 | 58 | install_choco: 59 | choco install $(CHOCOLATEY_PACKAGES) 60 | # checkov 61 | 62 | 63 | # 64 | # Testing Workspace Setup 65 | # 66 | .terraform: 67 | $(TF_BINARY) init -backend=false #ANNO This command creates the .terraform directory. 68 | 69 | 70 | # 71 | # Terraform Formatting 72 | # 73 | 74 | .PHONY: formatting 75 | formatting: 76 | $(TF_BINARY) fmt -recursive . 77 | 78 | .PHONY: test_formatting 79 | test_formatting: 80 | $(TF_BINARY) fmt -check -recursive . 81 | 82 | 83 | # 84 | # Terraform Docs 85 | # 86 | 87 | .PHONY: documentation 88 | documentation: 89 | terraform-docs -c .terraform-docs.yml . 90 | 91 | .PHONY: test_documentation 92 | test_documentation: 93 | terraform-docs -c .terraform-docs.yml --output-check . 94 | 95 | 96 | # 97 | # Linting 98 | # 99 | 100 | .PHONY: fix_tflint 101 | fix_tflint: 102 | tflint --init 103 | tflint --fix 104 | 105 | .PHONY: test_tflint 106 | test_tflint: 107 | tflint --init 108 | tflint 109 | 110 | 111 | # 112 | # Security 113 | # 114 | 115 | .PHONY: test_security 116 | test_security: test_checkov test_trivy 117 | 118 | .PHONY: test_checkov 119 | test_checkov: 120 | checkov --directory . $(CHECKOV_OPTS) 121 | 122 | .PHONY: test_trivy 123 | test_trivy: 124 | trivy config . 125 | 126 | 127 | # 128 | # Terratest 129 | # 130 | 131 | tests/go.mod: 132 | cd tests && \ 133 | go mod init "testing_terraform" 134 | 135 | tests/go.sum: tests/go.mod $(TERRATEST_FILES) 136 | cd tests && \ 137 | go mod tidy 138 | 139 | .PHONY: terratest 140 | terratest: tests/go.sum 141 | cd tests && \ 142 | TERRATEST_BINARY=$(TF_BINARY) go test -v -timeout 60m $(GO_TEST_OPTS) 143 | 144 | 145 | # 146 | # Terraform Test Framework 147 | # 148 | 149 | TERRAFORM_EXAMPLES:=$(wildcard examples/*) 150 | 151 | 152 | .PHONY: $(TERRAFORM_EXAMPLES) 153 | $(TERRAFORM_EXAMPLES): 154 | @echo "Testing $@" 155 | cd $@ && \ 156 | $(TFENV_COMMAND) && \ 157 | $(TF_BINARY) init -backend=false && \ 158 | $(TF_BINARY) test $(TF_TEST_OPTS) 159 | 160 | .PHONY: terraform_test 161 | terraform_test: $(TERRAFORM_EXAMPLES) 162 | @echo "Testing Root Module" 163 | $(TFENV_COMMAND) && \ 164 | $(TF_BINARY) test $(TF_TEST_OPTS) 165 | 166 | # 167 | # Validation 168 | # 169 | 170 | .PHONY: test_validation 171 | test_validation: .terraform 172 | $(TF_BINARY) validate 173 | 174 | 175 | # 176 | # Local Tools 177 | # 178 | 179 | .PHONY: precommit_install 180 | precommit_install: 181 | pre-commit install 182 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/outputs.tf: -------------------------------------------------------------------------------- 1 | output "output" { 2 | value = terraform_data.this.output 3 | description = "This is an example of an output." 4 | } 5 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | {{cookiecutter.__short_primary_provider}} = { 4 | source = "{{cookiecutter.primary_provider}}" 5 | version = "~> {{cookiecutter.provider_min_version}}" 6 | } 7 | } 8 | required_version = "~> {{cookiecutter.minimum_terraform_version}}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/terratest/basic_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/gruntwork-io/terratest/modules/terraform" 8 | ) 9 | 10 | func TestTerraformBasicExample(t *testing.T) { 11 | t.Parallel() 12 | 13 | // Allow the TerraformBinary option to be changed by users. 14 | // This makes it possible to switch to tofu easily. 15 | terraformBinary := os.Getenv("TERRATEST_BINARY") 16 | if len(terraformBinary) <= 0 { 17 | terraformBinary = "terraform" 18 | } 19 | 20 | terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ 21 | 22 | // Point this at the specific module or example to test. 23 | TerraformDir: "../examples/basic", 24 | 25 | // Switch between Terraform binaries. 26 | TerraformBinary: terraformBinary, 27 | 28 | Vars: map[string]interface{}{ 29 | // Input Variables go here. 30 | // "test_input": testInput, 31 | }, 32 | }) 33 | 34 | // Run terraform destroy after all other test code has run, even with errors. 35 | defer terraform.Destroy(t, terraformOptions) 36 | 37 | // Run terraform apply immediately. 38 | terraform.InitAndApply(t, terraformOptions) 39 | 40 | // Get any outputs from Terraform. 41 | // testOutput := terraform.Output(t, terraformOptions, "test_output") 42 | 43 | // Run assertions to conform that outputs are what you expect. 44 | //assert.Equal(t, testInput, testOutput) 45 | } 46 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/tests/basic.tftest.hcl: -------------------------------------------------------------------------------- 1 | variables { 2 | input = "test" 3 | } 4 | 5 | run "input_and_output_match" { 6 | 7 | assert { 8 | condition = output.output == "test" 9 | error_message = "The output does not match the input." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /{{cookiecutter.name}}/variables.tf: -------------------------------------------------------------------------------- 1 | variable "input" { 2 | description = "This is an example of an input." 3 | type = string 4 | default = "test" 5 | } 6 | --------------------------------------------------------------------------------