diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100644 index 0000000..0c49a92 --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,26 @@ +{{ if .Versions -}} +# Changelog + +{{ range .Versions }} + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) + +{{ range .CommitGroups -}} +### {{ .Title }} + +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} ([{{ .Hash.Short }}]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }})) +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} + +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100644 index 0000000..dc73080 --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,34 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/your-org/Terraform-module-base-template +options: + commits: + filters: + Type: + - feat + - fix + - perf + - refactor + - docs + - test + - chore + commit_groups: + title_maps: + feat: Features + fix: Bug Fixes + perf: Performance + refactor: Code Refactoring + docs: Documentation + test: Tests + chore: Maintenance + header: + pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" + pattern_maps: + - Type + - Scope + - Subject + notes: + keywords: + - BREAKING CHANGE diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c305712 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +{ + "name": "Terraform Module Development", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers/features/terraform:1": { + "version": "1.11.3", + "tflint": "latest", + "terragrunt": "none" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + } + }, + "postCreateCommand": "make dev-setup && make hooks", + "customizations": { + "vscode": { + "extensions": [ + "hashicorp.terraform", + "hashicorp.hcl", + "github.copilot", + "github.copilot-chat", + "github.vscode-github-actions", + "ms-azuretools.vscode-docker", + "editorconfig.editorconfig", + "timonwong.shellcheck", + "redhat.vscode-yaml" + ], + "settings": { + "terraform.languageServer.enable": true, + "terraform.validation.enableEnhancedValidation": true, + "editor.formatOnSave": true, + "[terraform]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": true + }, + "[terraform-vars]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": true + } + } + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d54fe7e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.tf] +indent_size = 2 + +[*.tfvars] +indent_size = 2 + +[*.hcl] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 + +[*.yaml] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.sh] +indent_size = 4 +indent_style = space + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d43cfae --- /dev/null +++ b/.env.example @@ -0,0 +1,28 @@ +# ============================================================================= +# Terraform Module — Environment Configuration +# ============================================================================= +# Copy this file to .env and customise for your local environment. +# Values here override Makefile defaults. The .env file is git-ignored. + +# Terraform version (must match .terraform-version for tfenv) +TF_VERSION=1.11.3 + +# TFLint version +TFLINT_VERSION=v0.53.0 + +# Trivy version +TRIVY_VERSION=0.58.0 + +# Terraform init: upgrade providers/modules on init (true/false) +TF_UPGRADE=false + +# Terraform plan/apply: refresh state before operations (true/false) +# Set to false in CI or when state is known-fresh for faster runs +TF_REFRESH=true + +# AWS Region (used by examples and integration tests) +# AWS_DEFAULT_REGION=eu-west-1 + +# Artifactory settings (for publishing — see release workflow) +# ARTIFACTORY_URL=https://myorg.jfrog.io/artifactory +# ARTIFACTORY_REPO=terraform-modules diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b2d9033 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Default owners for everything +* @your-org/platform-engineering + +# Module-specific owners +modules/ @your-org/platform-engineering +examples/ @your-org/platform-engineering + +# CI/CD and security +.github/ @your-org/platform-engineering +.pre-commit-config.yaml @your-org/platform-engineering +.tflint.hcl @your-org/platform-engineering diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0b22127 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,54 @@ +--- +name: Bug Report +about: Report a bug in this Terraform module +title: "bug: " +labels: bug +assignees: "" +--- + +## Description + +A clear and concise description of the bug. + +## Module + +Which module is affected? (e.g., `modules/example`) + +## Terraform Version + +``` +terraform version output +``` + +## Provider Version + +``` +terraform providers output +``` + +## Steps to Reproduce + +1. Configure module with... +2. Run `terraform plan` +3. See error... + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. Include error output if applicable. + +## Configuration + +```hcl +# Minimal terraform configuration to reproduce +module "example" { + source = "..." +} +``` + +## Additional Context + +Any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..892ff6f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,36 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement for this module +title: "feat: " +labels: enhancement +assignees: "" +--- + +## Description + +A clear and concise description of the feature you'd like. + +## Use Case + +Describe the problem this feature would solve. + +## Proposed Solution + +How you think this should work. Include example HCL if applicable: + +```hcl +module "example" { + source = "..." + + # New feature configuration + new_feature = true +} +``` + +## Alternatives Considered + +Any alternative solutions or features you've considered. + +## Additional Context + +Any other context, screenshots, or references. diff --git a/.github/actions/terraform-check/action.yml b/.github/actions/terraform-check/action.yml new file mode 100644 index 0000000..e236f2a --- /dev/null +++ b/.github/actions/terraform-check/action.yml @@ -0,0 +1,54 @@ +name: Terraform Check +description: Reusable composite action for terraform format, validate, and lint + +inputs: + terraform_version: + description: Terraform version to install + required: false + default: "1.11.3" + tflint_version: + description: TFLint version to install + required: false + default: "v0.53.0" + working_directory: + description: Directory containing terraform files + required: false + default: "." + +runs: + using: composite + steps: + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ inputs.terraform_version }} + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: ${{ inputs.tflint_version }} + + - name: Format Check + shell: bash + working-directory: ${{ inputs.working_directory }} + run: terraform fmt -check -recursive -diff + + - name: Init + shell: bash + working-directory: ${{ inputs.working_directory }} + run: terraform init -backend=false + + - name: Validate + shell: bash + working-directory: ${{ inputs.working_directory }} + run: terraform validate + + - name: Init TFLint + shell: bash + working-directory: ${{ inputs.working_directory }} + run: tflint --init + + - name: Lint + shell: bash + working-directory: ${{ inputs.working_directory }} + run: tflint diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a5460dd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +## Description + +Brief description of the changes. + +## Type of Change + +- [ ] `feat`: New feature or module capability +- [ ] `fix`: Bug fix +- [ ] `docs`: Documentation update +- [ ] `refactor`: Code change that neither fixes a bug nor adds a feature +- [ ] `test`: Adding or updating tests +- [ ] `chore`: Maintenance (CI, dependencies, tooling) + +## Module(s) Affected + +- [ ] `modules/example` +- [ ] `examples/complete` +- [ ] CI/CD workflows +- [ ] Other: ___ + +## Checklist + +- [ ] `make fmt-check` passes +- [ ] `make validate` passes +- [ ] `make lint` passes +- [ ] `make test` passes +- [ ] `make security` passes +- [ ] `make docs` — terraform-docs are up to date +- [ ] Variables have descriptions and types +- [ ] Outputs have descriptions +- [ ] Examples updated (if applicable) +- [ ] Tests added/updated for new functionality +- [ ] CHANGELOG entry (auto-generated from commit message) + +## Breaking Changes + + + +None. + +## Additional Notes + + diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..8a957b4 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,37 @@ +name: Changelog + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + changelog: + name: Update Changelog + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install git-chglog + run: | + curl -sSL https://github.com/git-chglog/git-chglog/releases/download/v0.15.4/git-chglog_0.15.4_linux_amd64.tar.gz | tar xz + sudo mv git-chglog /usr/local/bin/ + + - name: Generate Changelog + run: git-chglog -o CHANGELOG.md + + - name: Commit Changelog + run: | + if [[ -n "$(git diff --name-only CHANGELOG.md)" ]]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + git commit -m "docs(changelog): update CHANGELOG.md [skip ci]" + git push + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..20137c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,194 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +permissions: + contents: read + security-events: write + +env: + TF_VERSION: "1.11.3" + TFLINT_VERSION: "v0.53.0" + TRIVY_VERSION: "0.58.0" + +jobs: + # ----------------------------------------------------------------------- + # Format Check + # ----------------------------------------------------------------------- + format-check: + name: Terraform Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + + - name: Format Check + run: terraform fmt -check -recursive -diff + + # ----------------------------------------------------------------------- + # Validate + # ----------------------------------------------------------------------- + validate: + name: Terraform Validate + runs-on: ubuntu-latest + needs: format-check + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + + - name: Validate Module + run: | + terraform init -backend=false + terraform validate + + - name: Validate Examples + run: | + for dir in examples/*/; do + if [[ -f "${dir}versions.tf" ]]; then + echo "::group::Validating ${dir}" + cd "$dir" + terraform init -backend=false + terraform validate + cd "$GITHUB_WORKSPACE" + echo "::endgroup::" + fi + done + + # ----------------------------------------------------------------------- + # Lint + # ----------------------------------------------------------------------- + lint: + name: TFLint + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: ${{ env.TFLINT_VERSION }} + + - name: Init TFLint + run: tflint --init + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint Module + run: tflint + + # ----------------------------------------------------------------------- + # Test + # ----------------------------------------------------------------------- + test: + name: Terraform Test + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + + - name: Run Unit Tests + run: | + terraform init -backend=false + terraform test -filter=tests/unit/ -verbose + + # ----------------------------------------------------------------------- + # Security Scan + # ----------------------------------------------------------------------- + security: + name: Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Trivy IaC Scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: config + scan-ref: . + severity: HIGH,CRITICAL + limit-severities-for-sarif: true + exit-code: 1 + format: sarif + output: trivy-results.sarif + trivyignores: .trivyignore + + - name: Upload Trivy SARIF + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif + + # ----------------------------------------------------------------------- + # Commit Lint (PR only) + # ----------------------------------------------------------------------- + commit-lint: + name: Conventional Commits + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate PR Title + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .{3,}" + if [[ ! "$PR_TITLE" =~ $PATTERN ]]; then + echo "::error::PR title does not follow Conventional Commits format." + echo "Expected: type(scope): description" + echo "Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert" + echo "Got: $PR_TITLE" + exit 1 + fi + + # ----------------------------------------------------------------------- + # Documentation Check + # ----------------------------------------------------------------------- + docs-check: + name: Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup terraform-docs + run: | + mkdir -p /tmp/terraform-docs + curl -sSLo /tmp/terraform-docs/terraform-docs.tar.gz \ + https://github.com/terraform-docs/terraform-docs/releases/download/v0.19.0/terraform-docs-v0.19.0-linux-amd64.tar.gz + tar -xzf /tmp/terraform-docs/terraform-docs.tar.gz -C /tmp/terraform-docs + chmod +x /tmp/terraform-docs/terraform-docs + sudo mv /tmp/terraform-docs/terraform-docs /usr/local/bin/ + + - name: Check docs are up to date + run: | + terraform-docs markdown table --output-file README.md --output-mode inject . + if [[ -n "$(git diff --name-only)" ]]; then + echo "::error::terraform-docs are out of date. Run 'make docs' and commit." + git diff + exit 1 + fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4140da8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,28 @@ +name: CodeQL Analysis + +on: + push: + branches: [main] + schedule: + - cron: "0 6 * * 1" # Weekly on Monday at 06:00 UTC + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python # For any helper scripts; HCL scanned via Trivy + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 0000000..cf370f6 --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,72 @@ +name: Dependency Updates + +on: + schedule: + - cron: "0 8 * * 1" # Weekly on Monday at 08:00 UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +env: + TF_VERSION: "1.11.3" + +jobs: + update-providers: + name: Check Provider Updates + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + + - name: Check for provider updates + id: updates + run: | + UPDATES_FOUND=false + for dir in modules/*/; do + if [[ -f "${dir}versions.tf" ]]; then + cd "$dir" + terraform init -backend=false -upgrade + if [[ -n "$(git diff --name-only .terraform.lock.hcl 2>/dev/null)" ]]; then + UPDATES_FOUND=true + fi + cd "$GITHUB_WORKSPACE" + fi + done + echo "found=$UPDATES_FOUND" >> "$GITHUB_OUTPUT" + + - name: Validate after update + if: steps.updates.outputs.found == 'true' + run: | + for dir in modules/*/; do + if [[ -f "${dir}versions.tf" ]]; then + cd "$dir" + terraform init -backend=false + terraform validate + cd "$GITHUB_WORKSPACE" + fi + done + + - name: Create PR for updates + if: steps.updates.outputs.found == 'true' + run: | + BRANCH="chore/provider-updates-$(date +%Y%m%d)" + git checkout -b "$BRANCH" + git add -A + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "chore(deps): update terraform provider lock files" + git push origin "$BRANCH" + gh pr create \ + --title "chore(deps): update terraform provider lock files" \ + --body "Automated provider lock file update. Please review and merge." \ + --base main \ + --head "$BRANCH" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fc8e008 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,223 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + packages: write + +env: + TF_VERSION: "1.11.3" + +jobs: + # ----------------------------------------------------------------------- + # Validate before release + # ----------------------------------------------------------------------- + validate: + name: Pre-Release Validation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + + - name: Format Check + run: terraform fmt -check -recursive + + - name: Validate Module + run: | + terraform init -backend=false + terraform validate + + - name: Run Tests + run: | + terraform init -backend=false + terraform test -filter=tests/unit/ + + # ----------------------------------------------------------------------- + # Create GitHub Release + # ----------------------------------------------------------------------- + release: + name: Create Release + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Create module archive + run: | + MODULE_NAME=$(basename "$GITHUB_REPOSITORY") + tar -czf "${MODULE_NAME}-${{ steps.version.outputs.version }}.tar.gz" \ + --exclude='.git' \ + --exclude='.github' \ + --exclude='tests' \ + --exclude='scripts' \ + --exclude='.devcontainer' \ + --exclude='.chglog' \ + --exclude='Makefile' \ + --exclude='.pre-commit-config.yaml' \ + --exclude='.editorconfig' \ + --exclude='.trivyignore' \ + . + + - name: Generate checksums + run: | + MODULE_NAME=$(basename "$GITHUB_REPOSITORY") + sha256sum "${MODULE_NAME}-${{ steps.version.outputs.version }}.tar.gz" > SHA256SUMS + + - name: Generate release notes + id: release_notes + run: | + # Get commits since last tag + PREV_TAG=$(git tag --sort=-version:refname | head -2 | tail -1) + if [[ -n "$PREV_TAG" ]]; then + NOTES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (%h)" --no-merges) + else + NOTES=$(git log --pretty=format:"- %s (%h)" --no-merges) + fi + { + echo "notes<> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "Release ${{ github.ref_name }}" + body: ${{ steps.release_notes.outputs.notes }} + files: | + *.tar.gz + SHA256SUMS + generate_release_notes: false + + # ----------------------------------------------------------------------- + # Publish to Terraform Registry (via GitHub) + # ----------------------------------------------------------------------- + # The Terraform Registry automatically picks up new releases from GitHub + # if the repository is connected. No additional action needed beyond + # creating the GitHub Release above. + # + # To publish to the public Terraform Registry: + # 1. Repository must be named: terraform-- + # (e.g., terraform-aws-ec2, terraform-aws-vpc) + # 2. Go to https://registry.terraform.io/github/create + # 3. Authorize and select this repository + # 4. Every GitHub Release (semver tag) is auto-published + # + # For private registries (Terraform Cloud/Enterprise): + # 1. Connect your VCS provider in TFC/TFE settings + # 2. Create a "Module" in the registry pointing to this repo + # 3. Tags trigger automatic version publication + # + # For GitLab Terraform Registry: + # Uncomment the job below and configure accordingly. + + # publish-gitlab: + # name: Publish to GitLab Registry + # runs-on: ubuntu-latest + # needs: release + # if: vars.GITLAB_REGISTRY_ENABLED == 'true' + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + # + # - name: Publish to GitLab + # env: + # GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} + # GITLAB_PROJECT_ID: ${{ vars.GITLAB_PROJECT_ID }} + # run: | + # VERSION="${GITHUB_REF_NAME#v}" + # MODULE_NAME=$(basename "$GITHUB_REPOSITORY" | sed 's/terraform-aws-//') + # curl --fail --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + # --upload-file "${MODULE_NAME}-${VERSION}.tar.gz" \ + # "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/packages/terraform/modules/${MODULE_NAME}/aws/${VERSION}/file" + + # ----------------------------------------------------------------------- + # Publish to Artifactory (JFrog) + # ----------------------------------------------------------------------- + # Uncomment to publish module archives to JFrog Artifactory. + publish-artifactory: + name: Publish to Artifactory + runs-on: ubuntu-latest + needs: release + if: vars.ARTIFACTORY_ENABLED == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get version + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Create module archive + run: | + MODULE_NAME=$(basename "$GITHUB_REPOSITORY") + tar -czf "${MODULE_NAME}-${{ steps.version.outputs.version }}.tar.gz" \ + --exclude='.git' \ + --exclude='.github' \ + --exclude='tests' \ + --exclude='scripts' \ + --exclude='.devcontainer' \ + --exclude='.chglog' \ + . + + - name: Upload to Artifactory + env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} + ARTIFACTORY_REPO: ${{ vars.ARTIFACTORY_REPO || 'terraform-modules' }} + run: | + MODULE_NAME=$(basename "$GITHUB_REPOSITORY") + VERSION="${{ steps.version.outputs.version }}" + ARCHIVE="${MODULE_NAME}-${VERSION}.tar.gz" + + curl --fail -H "Authorization: Bearer ${ARTIFACTORY_TOKEN}" \ + -T "${ARCHIVE}" \ + "${ARTIFACTORY_URL}/${ARTIFACTORY_REPO}/${MODULE_NAME}/${VERSION}/${ARCHIVE}" + + echo "✓ Published ${ARCHIVE} to Artifactory" + diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..db8949c --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,60 @@ +name: Version Bump + +on: + workflow_dispatch: + inputs: + bump: + description: "Version bump type" + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +jobs: + version-bump: + name: Bump Version + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get current version + id: current + run: | + CURRENT=$(git tag --sort=-version:refname | head -1 | sed 's/^v//') + if [[ -z "$CURRENT" ]]; then + CURRENT="0.0.0" + fi + echo "version=$CURRENT" >> "$GITHUB_OUTPUT" + + - name: Calculate new version + id: new + run: | + IFS='.' read -r major minor patch <<< "${{ steps.current.outputs.version }}" + case "${{ github.event.inputs.bump }}" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + echo "version=${major}.${minor}.${patch}" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v${{ steps.new.outputs.version }}" -m "Release v${{ steps.new.outputs.version }}" + git push origin "v${{ steps.new.outputs.version }}" + + - name: Summary + run: | + echo "## Version Bump" >> "$GITHUB_STEP_SUMMARY" + echo "- **Previous**: v${{ steps.current.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" + echo "- **New**: v${{ steps.new.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Type**: ${{ github.event.inputs.bump }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 78e7733..09f3824 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,38 @@ -# Local .terraform directories -.terraform/ - -# .tfstate files +# Terraform +**/.terraform/* *.tfstate *.tfstate.* - -# Crash log files +*.tfplan +*.tfplan.txt crash.log crash.*.log - -# Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject -# to change depending on the environment. -*.tfvars -*.tfvars.json - -# Ignore override files as they are usually used to override resources locally and so -# are not checked in override.tf override.tf.json *_override.tf *_override.tf.json +.terraform.lock.hcl +.terraformrc +terraform.rc -# Ignore transient lock info files created by terraform apply -.terraform.tfstate.lock.info - -# Include override files you do wish to add to version control using negated pattern -# !example_override.tf +# Plans +.plans/ -# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* +# Environment +.env +.env.* +!.env.example -# Ignore CLI configuration files -.terraformrc -terraform.rc +# OS +.DS_Store +Thumbs.db -# Optional: ignore graph output files generated by `terraform graph` -# *.dot +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ -# Optional: ignore plan files saved before destroying Terraform configuration -# Uncomment the line below if you want to ignore planout files. -# planout \ No newline at end of file +# Build artifacts +dist/ +bin/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a7ff347 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,61 @@ +repos: + # ----------------------------------------------------------------------- + # Commit Message Validation + # ----------------------------------------------------------------------- + - repo: https://github.com/jorisroovers/gitlint + rev: v0.19.1 + hooks: + - id: gitlint + args: [--contrib=contrib-title-conventional-commits] + stages: [commit-msg] + + # ----------------------------------------------------------------------- + # General File Checks + # ----------------------------------------------------------------------- + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + args: [--maxkb=1024] + - id: check-merge-conflict + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: check-yaml + args: [--allow-multiple-documents] + - id: check-json + - id: detect-private-key + + # ----------------------------------------------------------------------- + # Terraform Hooks + # ----------------------------------------------------------------------- + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.97.3 + hooks: + # Format + - id: terraform_fmt + + # Validate (requires init) + - id: terraform_validate + args: + - --hook-config=--retry-once-with-cleanup=true + - --tf-init-args=-backend=false + + # Lint with TFLint + - id: terraform_tflint + args: + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + + # Auto-generate documentation + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml + - --hook-config=--path-to-file=README.md + - --hook-config=--add-to-existing-file=true + - --hook-config=--create-file-if-not-exist=true + + # Security scanning with Trivy + - id: terraform_trivy + args: + - --args=--tf-exclude-downloaded-modules + - --args=--severity HIGH,CRITICAL diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..a2d8a0f --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,24 @@ +formatter: "markdown table" + +output: + file: "README.md" + mode: inject + template: |- + + {{ .Content }} + + +sort: + enabled: true + by: required + +settings: + indent: 3 + anchor: true + color: true + default: true + description: true + escape: true + required: true + sensitive: true + type: true diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..0a5af26 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.11.3 diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..19716bf --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,10 @@ +plugin "terraform" { + enabled = true + preset = "all" +} + +plugin "aws" { + enabled = true + version = "0.37.0" + source = "github.com/terraform-linters/tflint-ruleset-aws" +} diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..dbfed04 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,14 @@ +# Trivy Ignore File +# +# Add Trivy finding IDs to suppress known false positives. +# Always include a justification comment. +# +# Format: +# AVD-AWS-XXXX # Justification for suppression +# +# Example: +# AVD-AWS-0089 # S3 bucket logging intentionally disabled for cost savings + +# Template placeholder resources — suppress until replaced with real module code +AVD-AWS-0089 # S3 bucket logging not required for template placeholder resource +AVD-AWS-0090 # S3 bucket lifecycle not required for template placeholder resource diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8458276 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added +- Initial template with example S3 module +- Native Terraform testing with mock providers +- GitHub Actions CI/CD (6 workflows) +- Pre-commit hooks (fmt, validate, lint, docs, trivy) +- TFLint with AWS ruleset +- Trivy IaC security scanning +- terraform-docs auto-generation +- Makefile with 20+ automation targets +- DevContainer for VS Code +- Complete documentation (README, TEMPLATE_GUIDE, CONTRIBUTING, SECURITY, WORKFLOW) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..286ad30 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,163 @@ +# Contributing + +Thank you for contributing to this Terraform module! + +## Development Setup + +```bash +# Clone the repository +git clone git@github.com:your-org/Terraform-module-base-template.git +cd Terraform-module-base-template + +# Install tools +make dev-setup + +# Install git hooks +make hooks +``` + +## Development Workflow + +1. **Create a feature branch** + ```bash + git checkout -b feat/my-feature + ``` + +2. **Make your changes** + - Edit module files in `modules/` + - Update tests in `tests/` + - Update examples in `examples/` + +3. **Run all checks** + ```bash + make all + ``` + +4. **Commit with conventional format** + ```bash + git commit -m "feat(module): add support for lifecycle rules" + ``` + +5. **Push and create PR** + ```bash + git push origin feat/my-feature + ``` + +## Commit Message Format + +We use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +### Types + +| Type | Description | +|------|-------------| +| `feat` | New feature or capability | +| `fix` | Bug fix | +| `docs` | Documentation only | +| `style` | Formatting, whitespace | +| `refactor` | Code change (no new feature or fix) | +| `perf` | Performance improvement | +| `test` | Adding or updating tests | +| `chore` | Maintenance, dependencies | +| `build` | Build system changes | +| `ci` | CI/CD pipeline changes | +| `revert` | Revert a previous commit | + +### Examples + +``` +feat(s3): add support for intelligent tiering +fix(outputs): correct bucket_arn output when disabled +docs: update usage examples in README +test: add validation tests for environment variable +chore(deps): update AWS provider to ~> 6.0 +ci: add infracost estimation to PR +``` + +## Pre-Commit Hooks + +Hooks run automatically on every commit: + +- **terraform_fmt** — Formats HCL files +- **terraform_validate** — Validates syntax +- **terraform_tflint** — Lints against AWS best practices +- **terraform_docs** — Generates README documentation +- **terraform_trivy** — Scans for security issues +- **gitlint** — Validates commit message format + +To run manually: + +```bash +pre-commit run --all-files +``` + +## Testing + +### Unit Tests (No AWS credentials needed) + +```bash +make test-unit +``` + +Unit tests use `mock_provider` — they run entirely locally without API calls. + +### Integration Tests (AWS credentials required) + +```bash +make test-integration +``` + +Integration tests create real AWS resources. Ensure you have valid credentials and understand cost implications. + +### Writing Tests + +```hcl +# tests/unit/my_test.tftest.hcl + +mock_provider "aws" {} + +variables { + name = "test" + environment = "dev" +} + +run "my_test_case" { + command = plan + + assert { + condition = aws_s3_bucket.this[0].bucket == "dev-test" + error_message = "Unexpected bucket name." + } +} +``` + +## Code Quality + +Before submitting a PR, ensure: + +- [ ] `make fmt-check` passes +- [ ] `make validate` passes +- [ ] `make lint` passes +- [ ] `make test` passes +- [ ] `make security` passes +- [ ] `make docs` generates up-to-date documentation +- [ ] All variables have `description` and `type` +- [ ] All outputs have `description` +- [ ] Examples are updated + +## Release Process + +Releases are automated via GitHub Actions: + +1. Merge PR to `main` +2. Run `make release BUMP=patch` (or `minor`/`major`) +3. Push the tag: `git push origin v1.2.3` +4. Release workflow creates GitHub Release with artifacts diff --git a/LICENSE b/LICENSE index 457be2b..eee8ed4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 PE Stack Pulse +Copyright (c) 2026 PlatformStackPulse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..54e5bdf --- /dev/null +++ b/Makefile @@ -0,0 +1,263 @@ +# ============================================================================= +# Terraform Module — Makefile +# ============================================================================= + +SHELL := /bin/bash +.DEFAULT_GOAL := help + +# Load .env if present (does not override existing env vars) +-include .env +export + +# Terraform settings (override via .env or environment) +TF_VERSION ?= 1.11.3 +TFLINT_VERSION ?= v0.53.0 +TRIVY_VERSION ?= 0.58.0 +TF_REFRESH ?= true +TF_UPGRADE ?= false +EXAMPLES_DIR := examples +TESTS_DIR := tests + +# Computed flags from settings +TF_INIT_FLAGS := -backend=false -input=false +ifeq ($(TF_UPGRADE),true) + TF_INIT_FLAGS += -upgrade +endif + +TF_CMD_FLAGS := +ifeq ($(TF_REFRESH),false) + TF_CMD_FLAGS += -refresh=false +endif + +# Colors +GREEN := \033[0;32m +YELLOW := \033[0;33m +RED := \033[0;31m +CYAN := \033[0;36m +RESET := \033[0m + +# ============================================================================= +# Help +# ============================================================================= + +.PHONY: help +help: ## Show this help message + @echo "" + @echo "$(CYAN)Terraform Module$(RESET)" + @echo "" + @echo "$(GREEN)Core Targets:$(RESET)" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " $(CYAN)%-20s$(RESET) %s\n", $$1, $$2}' + @echo "" + +# ============================================================================= +# Core Targets +# ============================================================================= + +.PHONY: init +init: ## Initialize the module (honours TF_UPGRADE) + @echo "$(GREEN)Initializing module...$(RESET)" + @terraform init $(TF_INIT_FLAGS) > /dev/null 2>&1 + @echo "$(GREEN)✓ Module initialized$(RESET)" + +.PHONY: init-upgrade +init-upgrade: ## Initialize with -upgrade (force latest providers/modules) + @echo "$(GREEN)Initializing module (upgrade)...$(RESET)" + @terraform init -backend=false -input=false -upgrade + @echo "$(GREEN)✓ Module initialized (providers upgraded)$(RESET)" + +.PHONY: fmt +fmt: ## Format all Terraform files + @echo "$(GREEN)Formatting...$(RESET)" + @terraform fmt -recursive + @echo "$(GREEN)✓ Formatted$(RESET)" + +.PHONY: fmt-check +fmt-check: ## Check formatting (CI mode — fails on diff) + @echo "$(GREEN)Checking format...$(RESET)" + @terraform fmt -check -recursive -diff + @echo "$(GREEN)✓ Format OK$(RESET)" + +.PHONY: validate +validate: init ## Validate the module + @echo "$(GREEN)Validating module...$(RESET)" + @terraform validate + @echo "$(GREEN)✓ Module valid$(RESET)" + +.PHONY: lint +lint: ## Run TFLint + @echo "$(GREEN)Linting...$(RESET)" + @tflint --init > /dev/null 2>&1 + @tflint + @echo "$(GREEN)✓ Lint OK$(RESET)" + +.PHONY: test +test: test-unit ## Run all tests + +.PHONY: test-unit +test-unit: init ## Run unit tests + @echo "$(GREEN)Running unit tests...$(RESET)" + @terraform test -filter=$(TESTS_DIR)/unit/ -verbose + @echo "$(GREEN)✓ Unit tests passed$(RESET)" + +.PHONY: test-integration +test-integration: init ## Run integration tests (requires AWS credentials) + @echo "$(YELLOW)Running integration tests (requires AWS credentials)...$(RESET)" + @terraform test -filter=$(TESTS_DIR)/integration/ -verbose + @echo "$(GREEN)✓ Integration tests passed$(RESET)" + +.PHONY: security +security: ## Run Trivy IaC security scan + @echo "$(GREEN)Running security scan...$(RESET)" + @trivy config . --severity HIGH,CRITICAL --tf-exclude-downloaded-modules + @echo "$(GREEN)✓ Security scan passed$(RESET)" + +.PHONY: docs +docs: ## Generate terraform-docs + @echo "$(GREEN)Generating documentation...$(RESET)" + @terraform-docs markdown table --output-file README.md --output-mode inject . + @echo "$(GREEN)✓ Documentation generated$(RESET)" + +.PHONY: clean +clean: ## Remove .terraform dirs and plan files + @echo "$(GREEN)Cleaning...$(RESET)" + @find . -type d -name ".terraform" -exec rm -rf {} + 2>/dev/null || true + @find . -type f -name "*.tfplan" -delete 2>/dev/null || true + @find . -type f -name "*.tfplan.txt" -delete 2>/dev/null || true + @rm -rf .plans/ 2>/dev/null || true + @echo "$(GREEN)✓ Cleaned$(RESET)" + +# ============================================================================= +# Build Targets +# ============================================================================= + +.PHONY: all +all: fmt-check validate lint test security docs ## Run all checks (CI mode) + @echo "" + @echo "$(GREEN)═══════════════════════════════════════$(RESET)" + @echo "$(GREEN) ✓ All checks passed$(RESET)" + @echo "$(GREEN)═══════════════════════════════════════$(RESET)" + +.PHONY: ci +ci: all ## Alias for 'all' — CI optimized + +.PHONY: dev-setup +dev-setup: ## Install development tools (including tfenv) + @echo "$(GREEN)Installing development tools...$(RESET)" + @echo "" + @echo "$(CYAN)Checking tfenv...$(RESET)" + @if ! command -v tfenv &> /dev/null; then \ + echo " Installing tfenv..."; \ + git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv 2>/dev/null || true; \ + if [[ ! -L /usr/local/bin/tfenv ]]; then \ + sudo ln -sf ~/.tfenv/bin/* /usr/local/bin/; \ + fi; \ + else \ + echo " ✓ tfenv $$(tfenv --version 2>/dev/null || echo 'installed')"; \ + fi + @echo "" + @echo "$(CYAN)Installing terraform via tfenv...$(RESET)" + @tfenv install $(TF_VERSION) 2>/dev/null || true + @tfenv use $(TF_VERSION) + @echo " ✓ terraform $$(terraform version -json | jq -r '.terraform_version')" + @echo "" + @echo "$(CYAN)Checking tflint...$(RESET)" + @if ! command -v tflint &> /dev/null; then \ + echo " Installing tflint..."; \ + curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash; \ + else \ + echo " ✓ tflint $$(tflint --version | head -1)"; \ + fi + @echo "" + @echo "$(CYAN)Checking trivy...$(RESET)" + @if ! command -v trivy &> /dev/null; then \ + echo " Installing trivy..."; \ + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin; \ + else \ + echo " ✓ trivy $$(trivy --version | head -1)"; \ + fi + @echo "" + @echo "$(CYAN)Checking terraform-docs...$(RESET)" + @if ! command -v terraform-docs &> /dev/null; then \ + echo " Installing terraform-docs..."; \ + curl -sSLo /tmp/terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/latest/download/terraform-docs-v0.19.0-linux-amd64.tar.gz; \ + tar -xzf /tmp/terraform-docs.tar.gz -C /tmp; \ + sudo mv /tmp/terraform-docs /usr/local/bin/; \ + else \ + echo " ✓ terraform-docs $$(terraform-docs version)"; \ + fi + @echo "" + @echo "$(CYAN)Checking pre-commit...$(RESET)" + @if ! command -v pre-commit &> /dev/null; then \ + echo " Installing pre-commit..."; \ + pip3 install pre-commit; \ + else \ + echo " ✓ pre-commit $$(pre-commit --version)"; \ + fi + @echo "" + @echo "$(CYAN)Checking git-chglog...$(RESET)" + @if ! command -v git-chglog &> /dev/null; then \ + echo " Installing git-chglog..."; \ + curl -sSL https://github.com/git-chglog/git-chglog/releases/download/v0.15.4/git-chglog_0.15.4_linux_amd64.tar.gz | tar xz -C /tmp; \ + sudo mv /tmp/git-chglog /usr/local/bin/; \ + else \ + echo " ✓ git-chglog installed"; \ + fi + @echo "" + @echo "$(GREEN)✓ All tools installed$(RESET)" + +.PHONY: tf-install +tf-install: ## Install/switch terraform version via tfenv (reads .terraform-version) + @echo "$(GREEN)Installing terraform $(TF_VERSION) via tfenv...$(RESET)" + @tfenv install $(TF_VERSION) 2>/dev/null || true + @tfenv use $(TF_VERSION) + @echo "$(GREEN)✓ terraform $$(terraform version -json | jq -r '.terraform_version')$(RESET)" + +.PHONY: hooks +hooks: ## Install pre-commit hooks + @echo "$(GREEN)Installing pre-commit hooks...$(RESET)" + @pre-commit install + @pre-commit install --hook-type commit-msg + @echo "$(GREEN)✓ Hooks installed$(RESET)" + +.PHONY: changelog +changelog: ## Regenerate CHANGELOG.md from git history + @echo "$(GREEN)Generating changelog...$(RESET)" + @git-chglog -o CHANGELOG.md + @echo "$(GREEN)✓ Changelog updated$(RESET)" + +.PHONY: version +version: ## Show current version from git tags + @git tag --sort=-version:refname | head -1 || echo "v0.0.0 (no tags)" + +.PHONY: release +release: ## Create and push a version tag (use BUMP=patch|minor|major) + @BUMP=$${BUMP:-patch}; \ + CURRENT=$$(git tag --sort=-version:refname | head -1 | sed 's/^v//'); \ + if [[ -z "$$CURRENT" ]]; then CURRENT="0.0.0"; fi; \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + case "$$BUMP" in \ + major) major=$$((major + 1)); minor=0; patch=0 ;; \ + minor) minor=$$((minor + 1)); patch=0 ;; \ + patch) patch=$$((patch + 1)) ;; \ + esac; \ + NEW="$${major}.$${minor}.$${patch}"; \ + echo "$(GREEN)Releasing v$$NEW (was v$$CURRENT)$(RESET)"; \ + git tag -a "v$$NEW" -m "Release v$$NEW"; \ + echo "$(YELLOW)Tag created. Push with: git push origin v$$NEW$(RESET)" + +# ============================================================================= +# Example Targets +# ============================================================================= + +.PHONY: example-init +example-init: ## Initialize the complete example (honours TF_UPGRADE) + @cd $(EXAMPLES_DIR)/complete && terraform init $(TF_INIT_FLAGS) + +.PHONY: example-plan +example-plan: example-init ## Plan the complete example (honours TF_REFRESH) + @cd $(EXAMPLES_DIR)/complete && terraform plan $(TF_CMD_FLAGS) + +.PHONY: example-apply +example-apply: example-init ## Apply the complete example (honours TF_REFRESH) + @cd $(EXAMPLES_DIR)/complete && terraform apply $(TF_CMD_FLAGS) diff --git a/README.md b/README.md index 115d77d..5b2b680 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,257 @@ -# Terraform-module-base-template -A production-ready template for creating Terraform modules with built-in CI/CD, security scanning, testing, and documentation generation. +# Terraform Module Template + +[![CI](https://github.com/your-org/Terraform-module-base-template/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/Terraform-module-base-template/actions/workflows/ci.yml) +[![Release](https://github.com/your-org/Terraform-module-base-template/actions/workflows/release.yml/badge.svg)](https://github.com/your-org/Terraform-module-base-template/actions/workflows/release.yml) + +A production-ready template for creating Terraform modules following the **one module per repository** best practice, with built-in CI/CD, security scanning, testing, documentation generation, and publishing to public registries. + +## Features + +- **One Module Per Repo** — Module lives at the root; no nested `modules/` directory +- **Registry Publishing** — Auto-publish to Terraform Registry, Artifactory, or GitLab on release +- **Native Terraform Testing** — `terraform test` with mock providers (no external tools) +- **Security Scanning** — Trivy IaC scanning for HIGH/CRITICAL vulnerabilities +- **Linting** — TFLint with AWS ruleset (preset "all") +- **Auto Documentation** — terraform-docs generates README sections on every commit +- **GitHub Actions CI/CD** — Workflows for the full module lifecycle +- **Pre-Commit Hooks** — Format, validate, lint, docs, and security on every commit +- **Conventional Commits** — Enforced commit message format +- **Semantic Versioning** — Automated version management and releases +- **DevContainer** — VS Code remote development ready + +## Quick Start + +### Create a New Module + +```bash +# Create repo from template (name MUST follow: terraform--) +gh repo create my-org/terraform-aws-my-module --template my-org/Terraform-module-base-template --public + +# Clone +git clone git@github.com:my-org/terraform-aws-my-module.git +cd terraform-aws-my-module + +# Install tools and hooks +make dev-setup +make hooks + +# Run all checks +make all +``` + +### Customise the Template + +1. Replace the example S3 resources in `main.tf` with your actual resources +2. Update `variables.tf`, `outputs.tf`, and `versions.tf` +3. Write tests in `tests/unit/main_test.tftest.hcl` +4. Update `examples/complete/` with real usage +5. Update `.github/CODEOWNERS` +6. Update this `README.md` + +See [TEMPLATE_GUIDE.md](TEMPLATE_GUIDE.md) for detailed instructions. + +## Usage + +### From GitHub + +```hcl +module "this" { + source = "github.com/your-org/terraform-aws-my-module?ref=v1.0.0" + + name = "my-resource" + environment = "dev" + namespace = "myorg" + + tags = { + Project = "example" + Owner = "platform-engineering" + } +} +``` + +### From Terraform Registry + +```hcl +module "this" { + source = "your-org/my-module/aws" + version = "~> 1.0" + + name = "my-resource" + environment = "dev" + namespace = "myorg" + + tags = { + Project = "example" + Owner = "platform-engineering" + } +} +``` + +## Module Structure + +``` +├── main.tf # Primary resource definitions +├── variables.tf # Input variables +├── outputs.tf # Output values +├── versions.tf # Terraform and provider version constraints +├── locals.tf # Local values and naming conventions +├── data.tf # Data sources +├── examples/ # Usage examples for consumers +│ └── complete/ # Full-featured example +├── tests/ # Terraform native tests +│ ├── unit/ # Unit tests with mock providers +│ └── integration/ # Integration tests (real AWS) +├── .github/ # GitHub Actions + templates +├── scripts/ # Automation scripts +└── Makefile # Build automation +``` + +## Make Targets + +``` +make help Show all targets +make init Initialize the module +make fmt Format all Terraform files +make fmt-check Check formatting (CI mode) +make validate Validate the module +make lint Run TFLint +make test Run all tests +make test-unit Run unit tests only +make test-integration Run integration tests +make security Run Trivy security scan +make docs Generate terraform-docs +make clean Remove .terraform dirs +make all Run all checks +make dev-setup Install development tools +make hooks Install pre-commit hooks +make changelog Regenerate CHANGELOG.md +make version Show current version +make release Create version tag (BUMP=patch|minor|major) +``` + +## Publishing + +### Terraform Registry (Public) + +The [Terraform Registry](https://registry.terraform.io) automatically publishes new versions when you create a GitHub Release: + +1. **Name your repo** following the convention: `terraform--` (e.g., `terraform-aws-vpc`) +2. **Connect** at [registry.terraform.io/github/create](https://registry.terraform.io/github/create) +3. **Tag and release** — every semver tag (`v1.0.0`) is auto-published + +### Terraform Cloud / Enterprise (Private) + +1. Connect your VCS provider in TFC/TFE settings +2. Create a Module in the private registry pointing to this repo +3. Semver tags trigger automatic version publication + +### JFrog Artifactory + +Set these repository variables/secrets in GitHub: +- `ARTIFACTORY_ENABLED` = `true` (variable) +- `ARTIFACTORY_URL` — e.g., `https://myorg.jfrog.io/artifactory` (variable) +- `ARTIFACTORY_REPO` — e.g., `terraform-modules` (variable) +- `ARTIFACTORY_TOKEN` (secret) + +### GitLab Terraform Registry + +Uncomment the `publish-gitlab` job in `.github/workflows/release.yml` and set: +- `GITLAB_TOKEN` (secret) +- `GITLAB_PROJECT_ID` (variable) + +## CI/CD Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `ci.yml` | Push/PR to main | Format, validate, lint, test, security | +| `release.yml` | Tag v*.*.* | Create GitHub Release + publish to registries | +| `codeql.yml` | Weekly + push main | SAST security analysis | +| `dependencies.yml` | Weekly | Check for provider updates | +| `changelog.yml` | Push main | Auto-update CHANGELOG.md | +| `version-bump.yml` | Manual | Bump patch/minor/major version | + +## Pre-Commit Hooks + +Installed via `make hooks`. Runs on every commit: + +- `terraform_fmt` — Format check +- `terraform_validate` — Syntax validation +- `terraform_tflint` — Linting with AWS rules +- `terraform_docs` — Auto-generate documentation +- `terraform_trivy` — Security scanning (HIGH/CRITICAL) +- `gitlint` — Conventional commit message validation + +## Module Documentation + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.9.0 | +| [aws](#requirement\_aws) | >= 5.0.0 | + +### Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0.0 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [aws_s3_bucket.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_public_access_block.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_versioning.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [environment](#input\_environment) | Environment name (e.g., dev, staging, prod). | `string` | n/a | yes | +| [name](#input\_name) | Name of the resource. Used in naming convention. | `string` | n/a | yes | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources. | `bool` | `true` | no | +| [force\_destroy](#input\_force\_destroy) | Allow destruction of non-empty S3 bucket. | `bool` | `false` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | ARN of the KMS key for server-side encryption. If null, AES256 is used. | `string` | `null` | no | +| [namespace](#input\_namespace) | Namespace for resource naming (e.g., org name, team). | `string` | `""` | no | +| [tags](#input\_tags) | Additional tags to apply to all resources. | `map(string)` | `{}` | no | +| [versioning\_enabled](#input\_versioning\_enabled) | Enable S3 bucket versioning. | `bool` | `true` | no | + +### Outputs + +| Name | Description | +|------|-------------| +| [bucket\_arn](#output\_bucket\_arn) | The ARN of the S3 bucket. | +| [bucket\_domain\_name](#output\_bucket\_domain\_name) | The bucket domain name. | +| [bucket\_id](#output\_bucket\_id) | The ID of the S3 bucket. | +| [enabled](#output\_enabled) | Whether the module is enabled. | + + +## Learning Materials + +| Document | Description | +|----------|-------------| +| [docs/TERRAFORM_FLAGS.md](docs/TERRAFORM_FLAGS.md) | Terraform CLI flags reference (`-refresh`, `-upgrade`, etc.) | +| [docs/TFENV.md](docs/TFENV.md) | tfenv version manager guide | +| [docs/MAKEFILE_ENV.md](docs/MAKEFILE_ENV.md) | Makefile targets and `.env` configuration | +| [TEMPLATE_GUIDE.md](TEMPLATE_GUIDE.md) | Step-by-step guide to customise this template | +| [WORKFLOW.md](WORKFLOW.md) | Branching strategy and CI/CD pipeline | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Development workflow and guidelines | + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and guidelines. + +## Security + +See [SECURITY.md](SECURITY.md) for vulnerability reporting. + +## License + +[MIT](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1c075bd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,76 @@ +# Security Policy + +## Reporting Vulnerabilities + +If you discover a security vulnerability in this module, please report it responsibly. + +**Do NOT open a public issue.** + +Instead, email: **security@your-org.com** + +Include: +- Description of the vulnerability +- Steps to reproduce +- Impact assessment +- Suggested fix (if any) + +We will respond within 48 hours and work with you to resolve the issue. + +## Security Scanning + +This project uses multiple layers of security scanning: + +### Automated (CI/CD) + +| Tool | Scope | Trigger | +|------|-------|---------| +| **Trivy** | IaC vulnerability scanning | Every commit + PR | +| **TFLint** | AWS best practices enforcement | Every commit + PR | +| **CodeQL** | SAST analysis | Weekly + push to main | + +### Pre-Commit + +- `terraform_trivy` — Scans for HIGH/CRITICAL findings before commit +- `detect-private-key` — Prevents accidental key commits + +### Manual + +```bash +# Run security scan +make security + +# Run all checks including security +make all +``` + +## Suppressing Findings + +If a Trivy finding is a false positive, add it to `.trivyignore` with justification: + +``` +# .trivyignore +AVD-AWS-0089 # S3 logging intentionally disabled — this is a logging bucket itself +``` + +For inline suppressions in Terraform files: + +```hcl +resource "aws_s3_bucket" "logs" { + # trivy:ignore:AVD-AWS-0089 This IS the logging bucket + bucket = "my-logs-bucket" +} +``` + +## Best Practices Enforced + +- No hardcoded secrets in code or tfvars +- KMS or AES256 encryption on all storage resources +- Public access blocked on S3 buckets +- IAM policies follow least privilege +- All resources tagged with `ManagedBy = "terraform"` +- Provider versions pinned to prevent supply chain attacks +- Lock files (`.terraform.lock.hcl`) committed for reproducibility + +## Dependency Management + +Provider updates are checked weekly via the `dependencies.yml` workflow. Updates are submitted as PRs for review before merging. diff --git a/TEMPLATE_GUIDE.md b/TEMPLATE_GUIDE.md new file mode 100644 index 0000000..456df9e --- /dev/null +++ b/TEMPLATE_GUIDE.md @@ -0,0 +1,170 @@ +# Template Guide + +This guide explains how to use the Terraform Module Template to create a new module. + +Each module lives in its own repository at the root level (no nested `modules/` directory). This follows the [Terraform Registry convention](https://developer.hashicorp.com/terraform/registry/modules/publish#requirements) and best practices for module reusability. + +## Step 1: Create Repository + +Repository **must** follow the naming convention `terraform--`: + +```bash +# Create from template +gh repo create my-org/terraform-aws-vpc --template my-org/Terraform-module-base-template --public + +# Clone locally +git clone git@github.com:my-org/terraform-aws-vpc.git +cd terraform-aws-vpc +``` + +## Step 2: Install Tools + +```bash +make dev-setup # Install terraform, tflint, trivy, terraform-docs, pre-commit, git-chglog +make hooks # Install git pre-commit hooks +``` + +## Step 3: Define Your Resources + +Edit `main.tf` at the repository root: + +```hcl +# Replace the example S3 bucket with your resources +resource "aws_vpc" "this" { + count = var.enabled ? 1 : 0 + + cidr_block = var.cidr_block + + tags = local.tags +} +``` + +## Step 4: Define Variables + +Edit `variables.tf`: + +- Add `description` and `type` to every variable +- Use `validation {}` blocks for constraints +- Mark sensitive values with `sensitive = true` +- Group: required first, optional second, feature flags last + +## Step 5: Define Outputs + +Edit `outputs.tf`: + +- Add `description` to every output +- Use `try()` for conditional resources: `value = try(resource.this[0].arn, null)` + +## Step 6: Write Tests + +Edit `tests/unit/main_test.tftest.hcl`: + +```hcl +mock_provider "aws" {} + +variables { + name = "test" + environment = "dev" + enabled = true +} + +run "creates_resource" { + command = plan + + assert { + condition = length(aws_vpc.this) == 1 + error_message = "Expected VPC to be created." + } +} +``` + +## Step 7: Write Examples + +Edit `examples/complete/`: + +- Show full usage with all optional features +- Reference the root module: `source = "../.."` +- Include a `terraform.tfvars.example` with realistic values +- Make it copy-paste ready for consumers + +## Step 8: Validate + +```bash +make all # Run all checks: format, validate, lint, test, security, docs +``` + +## Step 9: Update Documentation + +- Update `README.md` with your module's description and usage +- Update `.github/CODEOWNERS` with your team +- Update `.chglog/config.yml` with your repository URL +- Remove this guide or update it for your module + +## Step 10: Publish + +### Push and Release + +```bash +git add -A +git commit -m "feat: initial module implementation" +git push origin main + +# Create first release +make release BUMP=minor # Creates v0.1.0 +git push origin v0.1.0 # Triggers release workflow +``` + +### Connect to Terraform Registry + +1. Go to [registry.terraform.io/github/create](https://registry.terraform.io/github/create) +2. Authorize with your GitHub account +3. Select the repository +4. Every new GitHub Release is auto-published to the registry + +### Private Registry (Terraform Cloud/Enterprise) + +1. In TFC/TFE → Registry → Publish Module +2. Select VCS provider and this repository +3. Tags auto-publish as new versions + +## Directory Structure + +``` +terraform-aws-my-module/ +├── main.tf # Primary resource definitions +├── variables.tf # Input variables +├── outputs.tf # Output values +├── versions.tf # Terraform and provider version constraints +├── locals.tf # Computed values and naming +├── data.tf # Data sources +├── README.md # Module documentation (auto-generated sections) +├── examples/ +│ └── complete/ # Full-featured usage example +│ ├── main.tf # Calls your module with source = "../.." +│ ├── variables.tf # Example inputs +│ ├── outputs.tf # Example outputs +│ └── versions.tf # Provider config +├── tests/ +│ ├── unit/ # Unit tests (no AWS needed) +│ │ └── main_test.tftest.hcl +│ └── integration/ # Integration tests (real AWS) +│ └── main_test.tftest.hcl +├── .github/ # CI/CD workflows +└── Makefile # Build automation +``` + +## Conventions + +| Convention | Detail | +|-----------|--------| +| Repo Naming | `terraform--` (required for registry) | +| Commits | Conventional Commits: `feat:`, `fix:`, `docs:`, etc. | +| Versions | Semantic Versioning: `vMAJOR.MINOR.PATCH` | +| Variables | Always have `description`, `type`, and `validation` | +| Outputs | Always have `description` | +| Resources | Use `count = var.enabled ? 1 : 0` for disable support | +| Tags | Include `ManagedBy = "terraform"` and `Environment` | +| Naming | `{namespace}-{environment}-{name}` pattern | +| Testing | Native `terraform test` with `mock_provider` | +| Security | Trivy HIGH/CRITICAL, no hardcoded secrets | +| Docs | terraform-docs with `BEGIN_TF_DOCS`/`END_TF_DOCS` markers | diff --git a/WORKFLOW.md b/WORKFLOW.md new file mode 100644 index 0000000..db260bb --- /dev/null +++ b/WORKFLOW.md @@ -0,0 +1,124 @@ +# Workflow Guide + +## Branch Strategy + +``` +main (protected) + ├── feat/add-lifecycle-rules + ├── fix/output-when-disabled + └── chore/update-providers +``` + +- `main` — Production branch, protected +- Feature branches — Created from `main`, merged via PR + +## Branch Protection + +Apply branch protection with: + +```bash +./scripts/apply-branch-protection.sh your-org/your-repo +``` + +### Rules Applied + +| Rule | Setting | +|------|---------| +| Required status checks | Format, Validate, TFLint, Test, Security | +| Strict status checks | Yes (branch must be up to date) | +| Required reviews | 1 approving review | +| Dismiss stale reviews | Yes | +| Code owner reviews | Required | +| Linear history | Required (no merge commits) | +| Force push | Disabled | +| Branch deletion | Disabled | + +## CI/CD Pipeline + +### On Every Push / PR + +``` +┌─────────────┐ ┌──────────┐ ┌────────┐ ┌──────┐ ┌──────────┐ +│ Format Check │───▶│ Validate │───▶│ TFLint │───▶│ Test │ │ Security │ +└─────────────┘ └──────────┘ └────────┘ └──────┘ └──────────┘ + │ │ + └────────┬────────────────┘ + │ + (parallel) +``` + +- **Format Check** — `terraform fmt -check -recursive` +- **Validate** — `terraform init -backend=false` + `terraform validate` +- **TFLint** — Lint with AWS ruleset +- **Test** — `terraform test` with mock providers +- **Security** — Trivy IaC scan (HIGH/CRITICAL) +- **Commit Lint** — PR title matches conventional commits +- **Docs Check** — terraform-docs are up to date + +### On Tag Push (v*.*.*) + +``` +┌──────────┐ ┌─────────┐ ┌─────────────────┐ +│ Validate │───▶│ Release │───▶│ GitHub Release │ +└──────────┘ └─────────┘ │ + Archive + SHA │ + └─────────────────┘ +``` + +### Weekly Automation + +- **CodeQL** — SAST security analysis (Monday 06:00 UTC) +- **Dependencies** — Check for provider updates (Monday 08:00 UTC) + +### On Push to Main + +- **Changelog** — Auto-update CHANGELOG.md from commits + +## Release Process + +### Automated (Recommended) + +```bash +# Bump version +make release BUMP=patch # v1.0.0 → v1.0.1 +make release BUMP=minor # v1.0.0 → v1.1.0 +make release BUMP=major # v1.0.0 → v2.0.0 + +# Push tag to trigger release +git push origin v1.0.1 +``` + +### Manual (GitHub UI) + +1. Go to Actions → Version Bump +2. Select bump type (patch/minor/major) +3. Run workflow + +### What Happens on Release + +1. Pre-release validation runs (format, validate, test) +2. Module archive created (`.tar.gz`) +3. SHA256 checksums generated +4. GitHub Release created with: + - Release notes (from conventional commits) + - Module archive + - Checksums + - Usage instructions + +## Consuming Released Modules + +```hcl +# Pin to specific version (recommended) +module "example" { + source = "github.com/your-org/your-module//modules/example?ref=v1.0.0" +} + +# Pin to major version +module "example" { + source = "github.com/your-org/your-module//modules/example?ref=v1" +} + +# Latest (not recommended for production) +module "example" { + source = "github.com/your-org/your-module//modules/example" +} +``` diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..4ea26ec --- /dev/null +++ b/data.tf @@ -0,0 +1,8 @@ +# Data sources for the module. +# +# Add data sources here when you need to reference existing +# AWS resources (e.g., IAM policies, VPCs, KMS keys). +# +# Example: +# data "aws_caller_identity" "current" {} +# data "aws_region" "current" {} diff --git a/docs/MAKEFILE_ENV.md b/docs/MAKEFILE_ENV.md new file mode 100644 index 0000000..7cabf70 --- /dev/null +++ b/docs/MAKEFILE_ENV.md @@ -0,0 +1,156 @@ +# Makefile & Environment Configuration + +This template uses a Makefile with environment variable overrides for flexible local and CI workflows. + +--- + +## Configuration Hierarchy + +``` +Environment variable (highest priority) + │ + ▼ + .env file (local overrides, git-ignored) + │ + ▼ + Makefile defaults (lowest priority) +``` + +Example: +```bash +# Makefile has: TF_VERSION ?= 1.11.3 +# .env has: TF_VERSION=1.12.0 +# Shell override: TF_VERSION=1.13.0 make init +# +# Result: 1.13.0 wins (env var > .env > Makefile default) +``` + +--- + +## The `.env` File + +Copy from the example and customise: + +```bash +cp .env.example .env +``` + +### Available Settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `TF_VERSION` | `1.11.3` | Terraform version (used by tfenv + Makefile) | +| `TFLINT_VERSION` | `v0.53.0` | TFLint version for linting | +| `TRIVY_VERSION` | `0.58.0` | Trivy version for security scanning | +| `TF_UPGRADE` | `false` | Add `-upgrade` to `terraform init` | +| `TF_REFRESH` | `true` | Control `-refresh` on plan/apply | +| `AWS_DEFAULT_REGION` | — | AWS region for examples/integration tests | +| `ARTIFACTORY_URL` | — | JFrog Artifactory URL (for publishing) | +| `ARTIFACTORY_REPO` | — | Artifactory repository name | + +--- + +## Make Targets Reference + +### Core Workflow + +```bash +make init # terraform init (uses TF_INIT_FLAGS) +make init-upgrade # terraform init -upgrade (always) +make fmt # terraform fmt -recursive +make fmt-check # terraform fmt -check (CI mode) +make validate # terraform validate +make lint # tflint +make test # Run unit tests +make test-unit # Run unit tests (explicit) +make test-integration # Run integration tests (needs AWS creds) +make security # trivy IaC scan +make docs # terraform-docs generation +make clean # Remove .terraform dirs +make all # Run everything (CI mode) +``` + +### Development + +```bash +make dev-setup # Install all tools (tfenv, tflint, trivy, etc.) +make tf-install # Install/switch terraform version via tfenv +make hooks # Install pre-commit hooks +``` + +### Versioning & Release + +```bash +make version # Show current version +make changelog # Regenerate CHANGELOG.md +make release # Create version tag (BUMP=patch|minor|major) +make release BUMP=minor # Create minor version bump +``` + +### Examples + +```bash +make example-init # Init the complete example +make example-plan # Plan (uses TF_CMD_FLAGS) +make example-apply # Apply (uses TF_CMD_FLAGS) +``` + +--- + +## Flag Combinations Cheat Sheet + +```bash +# Default: init normally, plan with refresh +make init && make example-plan + +# Fast CI: no refresh, no upgrade +TF_REFRESH=false make example-plan + +# Upgrade providers then plan +make init-upgrade && make example-plan + +# Full upgrade + no-refresh (testing provider bump) +TF_UPGRADE=true TF_REFRESH=false make example-plan + +# Set in .env for persistent config +echo "TF_REFRESH=false" >> .env +echo "TF_UPGRADE=true" >> .env +make example-plan # picks up .env settings +``` + +--- + +## CI vs Local Settings + +| Setting | Local Dev | CI | +|---------|-----------|-----| +| `TF_UPGRADE` | `false` (stable) | `false` (lock file pins) | +| `TF_REFRESH` | `true` (catch drift) | `false` (no cloud access in validate) | +| Backend | `-backend=false` | `-backend=false` (module testing) | +| Auto-approve | Never | Only after plan review | + +--- + +## Adding New Variables + +To add a new configurable flag: + +1. **Add to Makefile** with conditional default: + ```makefile + MY_FLAG ?= default_value + ``` + +2. **Add to `.env.example`** with documentation: + ```bash + # Description of what this does + MY_FLAG=default_value + ``` + +3. **Wire into computed flags** (if it affects terraform commands): + ```makefile + ifeq ($(MY_FLAG),true) + TF_CMD_FLAGS += --my-flag + endif + ``` + +4. **Document in this file** in the settings table above. diff --git a/docs/TERRAFORM_FLAGS.md b/docs/TERRAFORM_FLAGS.md new file mode 100644 index 0000000..e8415a8 --- /dev/null +++ b/docs/TERRAFORM_FLAGS.md @@ -0,0 +1,203 @@ +# Terraform Commands & Flags Reference + +A practical reference for the terraform CLI flags used in this template's Makefile and CI pipelines. + +--- + +## `terraform init` + +Initializes the working directory — downloads providers, modules, and configures backends. + +| Flag | Default | Description | +|------|---------|-------------| +| `-backend=false` | `true` | Skip backend configuration (used for local validation/testing) | +| `-input=false` | `true` | Disable interactive prompts (required for CI) | +| `-upgrade` | off | Force download of latest provider/module versions within constraints | +| `-reconfigure` | off | Reconfigure backend, ignoring any saved configuration | +| `-migrate-state` | off | Migrate state to a new backend configuration | + +### When to use `-upgrade` + +```bash +# Provider version bumped in versions.tf — need to pull new version +terraform init -upgrade + +# Or via Makefile +TF_UPGRADE=true make init +make init-upgrade +``` + +**Use when:** +- You've updated version constraints in `versions.tf` +- A provider released a bugfix you need +- Dependabot opened a PR bumping providers + +**Don't use when:** +- Running in CI (lock file should pin versions — use `terraform init` without upgrade) +- You want reproducible builds (the lock file `.terraform.lock.hcl` ensures this) + +--- + +## `terraform plan` / `terraform apply` + +### The `-refresh` Flag + +| Flag | Default | Description | +|------|---------|-------------| +| `-refresh=true` | `true` | Query cloud APIs to detect drift before planning | +| `-refresh=false` | — | Skip refresh, use cached state only | +| `-refresh-only` | — | Only refresh state, don't propose changes | + +### How Refresh Works + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ terraform plan │ +│ │ +│ 1. Read .tfstate ─────── What Terraform thinks exists │ +│ 2. Refresh (API calls) ─ What actually exists (drift detection) │ +│ 3. Compare with .tf ──── What you declared │ +│ 4. Generate plan ─────── Actions to reconcile │ +└─────────────────────────────────────────────────────────────────┘ +``` + +With `-refresh=false`: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Read .tfstate ─────── Trust state as-is (skip API calls) │ +│ 2. Compare with .tf ──── What you declared │ +│ 3. Generate plan ─────── Actions to reconcile │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### When to use `-refresh=false` + +```bash +# Fast plan in CI when state was just applied +terraform plan -refresh=false + +# Via Makefile +TF_REFRESH=false make example-plan +``` + +**Use when:** +- CI pipeline where state was just written (e.g., apply then plan for drift check) +- Large infrastructure where refresh takes minutes +- You only care about code changes, not drift + +**Don't use when:** +- You suspect drift (someone changed resources manually) +- Running `terraform apply` in production (always refresh in prod) +- First plan after importing resources + +### When to use `-refresh-only` + +```bash +# Detect drift without proposing any changes +terraform plan -refresh-only + +# Apply just the refresh (update state to match reality) +terraform apply -refresh-only +``` + +**Use when:** +- Investigating drift without making changes +- Syncing state after manual changes were intentionally made +- Baseline check before a big refactor + +--- + +## Flag Combinations in This Template + +### Via `.env` file + +```bash +# .env (git-ignored, local overrides) +TF_UPGRADE=false # default: don't upgrade on every init +TF_REFRESH=true # default: always refresh +``` + +### Via command line + +```bash +# Normal workflow +make init # init (no upgrade) +make example-plan # plan (with refresh) +make example-apply # apply (with refresh) + +# Fast CI mode +TF_REFRESH=false make example-plan # skip refresh + +# Update providers +make init-upgrade # explicit upgrade +TF_UPGRADE=true make init # upgrade via flag + +# Combined +TF_UPGRADE=true TF_REFRESH=false make example-plan +``` + +### How Makefile computes flags + +```makefile +# TF_INIT_FLAGS is used by: make init, make example-init +TF_INIT_FLAGS := -backend=false -input=false +ifeq ($(TF_UPGRADE),true) + TF_INIT_FLAGS += -upgrade # → -backend=false -input=false -upgrade +endif + +# TF_CMD_FLAGS is used by: make example-plan, make example-apply +TF_CMD_FLAGS := +ifeq ($(TF_REFRESH),false) + TF_CMD_FLAGS += -refresh=false # → -refresh=false +endif +``` + +--- + +## Other Useful Flags + +### `terraform plan` + +| Flag | Description | +|------|-------------| +| `-out=plan.tfplan` | Save plan to file for exact apply | +| `-target=resource.name` | Plan only specific resources (use sparingly) | +| `-destroy` | Plan a destroy operation | +| `-parallelism=N` | Limit concurrent operations (default 10) | +| `-compact-warnings` | Show warnings in compact form | + +### `terraform apply` + +| Flag | Description | +|------|-------------| +| `-auto-approve` | Skip interactive confirmation (CI only!) | +| `-parallelism=N` | Limit concurrent operations | +| `plan.tfplan` | Apply a saved plan file (recommended for prod) | + +### `terraform validate` + +| Flag | Description | +|------|-------------| +| `-no-color` | Disable colour output (CI log readability) | +| `-json` | Output in JSON format (machine parsing) | + +--- + +## Best Practices + +1. **Lock your providers** — Commit `.terraform.lock.hcl` so everyone gets identical versions +2. **Use `-upgrade` intentionally** — Don't auto-upgrade; review changes in lock file +3. **Refresh in production** — Always use `-refresh=true` for prod apply +4. **Skip refresh in CI validation** — Format/validate/lint don't need cloud access +5. **Save plans for apply** — Use `terraform plan -out=plan.tfplan` then `terraform apply plan.tfplan` +6. **Never `-auto-approve` locally** — Only in CI after plan review + +--- + +## Further Reading + +- [Terraform CLI docs — init](https://developer.hashicorp.com/terraform/cli/commands/init) +- [Terraform CLI docs — plan](https://developer.hashicorp.com/terraform/cli/commands/plan) +- [Terraform CLI docs — apply](https://developer.hashicorp.com/terraform/cli/commands/apply) +- [Dependency Lock File](https://developer.hashicorp.com/terraform/language/files/dependency-lock) +- [tfenv — Terraform version manager](https://github.com/tfutils/tfenv) diff --git a/docs/TFENV.md b/docs/TFENV.md new file mode 100644 index 0000000..2a28fe5 --- /dev/null +++ b/docs/TFENV.md @@ -0,0 +1,163 @@ +# tfenv — Terraform Version Manager + +[tfenv](https://github.com/tfutils/tfenv) manages multiple Terraform versions, similar to `rbenv` or `nvm`. + +--- + +## How It Works + +``` +.terraform-version ← Pin file at repo root (committed to git) + │ + ▼ + tfenv use ← Reads pin file, activates that version + │ + ▼ + ~/.tfenv/versions/ ← Installed versions live here +``` + +When you `cd` into this repo, tfenv automatically selects the version in `.terraform-version`. + +--- + +## Installation + +### Linux / macOS (manual) + +```bash +git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv +sudo ln -s ~/.tfenv/bin/* /usr/local/bin/ +``` + +### macOS (Homebrew) + +```bash +brew install tfenv +``` + +### Via this template + +```bash +make dev-setup # Installs tfenv + pins terraform version +``` + +--- + +## Common Commands + +```bash +# List available versions +tfenv list-remote + +# Install a specific version +tfenv install 1.11.3 + +# Install the version from .terraform-version +tfenv install + +# Switch to a version +tfenv use 1.11.3 + +# Show current version +tfenv version-name + +# List installed versions +tfenv list + +# Uninstall a version +tfenv uninstall 1.9.0 +``` + +--- + +## The `.terraform-version` File + +This repo includes a `.terraform-version` file at the root: + +``` +1.11.3 +``` + +**This file:** +- Is committed to git (team-wide version consistency) +- Is read by tfenv automatically when you enter the directory +- Should match `TF_VERSION` in `.env.example` and `required_version` in `versions.tf` +- Supports special values: + - `latest` — Always use latest stable + - `latest:^1.9` — Latest matching constraint + - `min-required` — Use minimum from `required_version` + +--- + +## Version Pinning Strategy + +| File | Purpose | Who reads it | +|------|---------|--------------| +| `.terraform-version` | Developer workstation version | tfenv | +| `versions.tf` → `required_version` | Minimum compatible version | terraform CLI | +| `.env` → `TF_VERSION` | Makefile override | Makefile | +| CI workflow → `TF_VERSION` env | CI pinned version | GitHub Actions | + +### Keeping them in sync + +When upgrading Terraform: + +1. Update `.terraform-version` → `1.12.0` +2. Update `versions.tf` → `required_version = ">= 1.12.0"` +3. Update `.env.example` → `TF_VERSION=1.12.0` +4. Update `.github/workflows/ci.yml` → `TF_VERSION: "1.12.0"` +5. Run `make dev-setup` to install new version +6. Run `make all` to validate everything passes +7. Commit all files together + +--- + +## tfenv vs. Direct Install + +| Aspect | tfenv | Direct binary | +|--------|-------|---------------| +| Multiple versions | ✅ Switch instantly | ❌ Manual swap | +| Team consistency | ✅ `.terraform-version` | ❌ Hope everyone matches | +| Upgrade path | `tfenv install X` | Download + replace | +| CI usage | Optional (CI uses setup-terraform action) | ✅ Direct download | +| Auto-switch on cd | ✅ Built-in | ❌ Not possible | + +--- + +## Troubleshooting + +### "tfenv: command not found" + +```bash +# Check if installed +ls ~/.tfenv/bin/tfenv + +# Add to PATH (add to ~/.zshrc or ~/.bashrc) +export PATH="$HOME/.tfenv/bin:$PATH" + +# Or create symlinks +sudo ln -sf ~/.tfenv/bin/* /usr/local/bin/ +``` + +### "Version X not installed" + +```bash +tfenv install # Installs version from .terraform-version +``` + +### Conflict with Homebrew terraform + +```bash +# Unlink the homebrew version +brew unlink terraform + +# tfenv takes over +tfenv use 1.11.3 +``` + +--- + +## Further Reading + +- [tfenv GitHub](https://github.com/tfutils/tfenv) +- [Terraform version constraints](https://developer.hashicorp.com/terraform/language/expressions/version-constraints) diff --git a/examples/complete/main.tf b/examples/complete/main.tf new file mode 100644 index 0000000..ff7c74d --- /dev/null +++ b/examples/complete/main.tf @@ -0,0 +1,18 @@ +# Complete Example +# +# This example demonstrates full usage of the module +# with all optional features enabled. + +module "example" { + source = "../.." + + name = var.name + environment = var.environment + namespace = var.namespace + + versioning_enabled = var.versioning_enabled + force_destroy = var.force_destroy + kms_key_arn = var.kms_key_arn + + tags = var.tags +} diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf new file mode 100644 index 0000000..9117908 --- /dev/null +++ b/examples/complete/outputs.tf @@ -0,0 +1,14 @@ +output "bucket_id" { + description = "The ID of the S3 bucket." + value = module.example.bucket_id +} + +output "bucket_arn" { + description = "The ARN of the S3 bucket." + value = module.example.bucket_arn +} + +output "enabled" { + description = "Whether the module is enabled." + value = module.example.enabled +} diff --git a/examples/complete/terraform.tfvars.example b/examples/complete/terraform.tfvars.example new file mode 100644 index 0000000..a8a7e58 --- /dev/null +++ b/examples/complete/terraform.tfvars.example @@ -0,0 +1,15 @@ +# Example tfvars for the complete example. +# Copy this file to terraform.tfvars and customise. + +name = "my-bucket" +environment = "dev" +namespace = "myorg" + +versioning_enabled = true +force_destroy = false +kms_key_arn = null + +tags = { + "Project" = "example" + "Owner" = "platform-engineering" +} diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf new file mode 100644 index 0000000..8f25f1d --- /dev/null +++ b/examples/complete/variables.tf @@ -0,0 +1,39 @@ +variable "name" { + description = "Name of the resource." + type = string +} + +variable "environment" { + description = "Environment name." + type = string +} + +variable "namespace" { + description = "Namespace for resource naming." + type = string + default = "" +} + +variable "versioning_enabled" { + description = "Enable S3 bucket versioning." + type = bool + default = true +} + +variable "force_destroy" { + description = "Allow destruction of non-empty S3 bucket." + type = bool + default = false +} + +variable "kms_key_arn" { + description = "ARN of the KMS key for encryption." + type = string + default = null +} + +variable "tags" { + description = "Additional tags." + type = map(string) + default = {} +} diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf new file mode 100644 index 0000000..00b5fc0 --- /dev/null +++ b/examples/complete/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0" + } + } +} + +provider "aws" { + region = "eu-west-1" +} diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..5ffc721 --- /dev/null +++ b/locals.tf @@ -0,0 +1,16 @@ +locals { + # Naming convention: {namespace}-{environment}-{name} + name_prefix = var.namespace != "" ? "${var.namespace}-${var.environment}" : var.environment + bucket_name = "${local.name_prefix}-${var.name}" + + # Standard tags applied to all resources + tags = merge( + { + "Name" = local.bucket_name + "Environment" = var.environment + "ManagedBy" = "terraform" + "Module" = "example" + }, + var.tags, + ) +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..4b9f895 --- /dev/null +++ b/main.tf @@ -0,0 +1,52 @@ +# Example Terraform Module +# +# This is a starter module demonstrating best practices. +# Replace with your actual resource definitions. +# +# Pattern: Keep main.tf focused on primary resources. +# Split complex modules into domain-specific files: +# iam.tf, networking.tf, monitoring.tf, etc. + +resource "aws_s3_bucket" "this" { + count = var.enabled ? 1 : 0 + + bucket = local.bucket_name + force_destroy = var.force_destroy + + tags = local.tags +} + +resource "aws_s3_bucket_versioning" "this" { + count = var.enabled ? 1 : 0 + + bucket = aws_s3_bucket.this[0].id + + versioning_configuration { + status = var.versioning_enabled ? "Enabled" : "Suspended" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + count = var.enabled ? 1 : 0 + + bucket = aws_s3_bucket.this[0].id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = var.kms_key_arn != null ? "aws:kms" : "AES256" + kms_master_key_id = var.kms_key_arn + } + bucket_key_enabled = var.kms_key_arn != null + } +} + +resource "aws_s3_bucket_public_access_block" "this" { + count = var.enabled ? 1 : 0 + + bucket = aws_s3_bucket.this[0].id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..962b5c2 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,19 @@ +output "bucket_id" { + description = "The ID of the S3 bucket." + value = try(aws_s3_bucket.this[0].id, null) +} + +output "bucket_arn" { + description = "The ARN of the S3 bucket." + value = try(aws_s3_bucket.this[0].arn, null) +} + +output "bucket_domain_name" { + description = "The bucket domain name." + value = try(aws_s3_bucket.this[0].bucket_domain_name, null) +} + +output "enabled" { + description = "Whether the module is enabled." + value = var.enabled +} diff --git a/scripts/apply-branch-protection.sh b/scripts/apply-branch-protection.sh new file mode 100755 index 0000000..baf104c --- /dev/null +++ b/scripts/apply-branch-protection.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Apply branch protection rules to the GitHub repository +# Requires: gh CLI authenticated with admin access +set -euo pipefail + +REPO="${1:-}" + +if [[ -z "$REPO" ]]; then + REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || true) + if [[ -z "$REPO" ]]; then + echo "Usage: $0 " + exit 1 + fi +fi + +echo "Applying branch protection to ${REPO}..." + +gh api -X PUT "repos/${REPO}/branches/main/protection" \ + --input - < /dev/null; then + echo "pre-commit not found. Installing..." + pip3 install pre-commit +fi + +cd "$PROJECT_ROOT" + +# Install pre-commit hooks +pre-commit install +pre-commit install --hook-type commit-msg + +echo "✓ Git hooks installed successfully" +echo "" +echo "Hooks will run automatically on every commit." +echo "To run manually: pre-commit run --all-files" diff --git a/scripts/update-changelog.sh b/scripts/update-changelog.sh new file mode 100755 index 0000000..aec557f --- /dev/null +++ b/scripts/update-changelog.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Update CHANGELOG.md from git history using git-chglog +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$PROJECT_ROOT" + +if ! command -v git-chglog &> /dev/null; then + echo "git-chglog not found. Install with: make dev-setup" + exit 1 +fi + +echo "Generating CHANGELOG.md..." +git-chglog -o CHANGELOG.md + +echo "✓ CHANGELOG.md updated" diff --git a/scripts/validate-module.sh b/scripts/validate-module.sh new file mode 100755 index 0000000..bc3bc61 --- /dev/null +++ b/scripts/validate-module.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Validate all Terraform modules locally +# Runs: fmt-check, validate, lint, test, security scan +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RESET='\033[0m' + +ERRORS=0 + +cd "$PROJECT_ROOT" + +echo "═══════════════════════════════════════" +echo " Terraform Module Validation" +echo "═══════════════════════════════════════" +echo "" + +# 1. Format check +echo -e "${GREEN}[1/5] Format check...${RESET}" +if terraform fmt -check -recursive > /dev/null 2>&1; then + echo -e " ${GREEN}✓ Formatting OK${RESET}" +else + echo -e " ${RED}✗ Formatting issues found. Run: terraform fmt -recursive${RESET}" + ERRORS=$((ERRORS + 1)) +fi +echo "" + +# 2. Validate +echo -e "${GREEN}[2/5] Validation...${RESET}" +terraform init -backend=false -input=false > /dev/null 2>&1 +if ! terraform validate > /dev/null 2>&1; then + echo -e " ${RED}✗ Module validation failed${RESET}" + terraform validate + ERRORS=$((ERRORS + 1)) +else + echo -e " ${GREEN}✓ Module valid${RESET}" +fi +echo "" + +# 3. Lint +echo -e "${GREEN}[3/5] Linting...${RESET}" +if command -v tflint &> /dev/null; then + tflint --init > /dev/null 2>&1 + if ! tflint > /dev/null 2>&1; then + echo -e " ${RED}✗ Lint issues found${RESET}" + tflint + ERRORS=$((ERRORS + 1)) + else + echo -e " ${GREEN}✓ Lint OK${RESET}" + fi +else + echo -e " ${YELLOW}⚠ tflint not installed, skipping${RESET}" +fi +echo "" + +# 4. Tests +echo -e "${GREEN}[4/5] Testing...${RESET}" +if [[ -d "tests/unit" ]]; then + terraform init -backend=false -input=false > /dev/null 2>&1 + if ! terraform test -filter=tests/unit/ > /dev/null 2>&1; then + echo -e " ${RED}✗ Tests failed${RESET}" + terraform test -filter=tests/unit/ -verbose + ERRORS=$((ERRORS + 1)) + else + echo -e " ${GREEN}✓ Tests passed${RESET}" + fi +else + echo -e " ${YELLOW}⚠ No unit tests found${RESET}" +fi +echo "" + +# 5. Security scan +echo -e "${GREEN}[5/5] Security scan...${RESET}" +if command -v trivy &> /dev/null; then + if ! trivy config . --severity HIGH,CRITICAL --tf-exclude-downloaded-modules --exit-code 1 > /dev/null 2>&1; then + echo -e " ${RED}✗ Security issues found${RESET}" + trivy config . --severity HIGH,CRITICAL --tf-exclude-downloaded-modules + ERRORS=$((ERRORS + 1)) + else + echo -e " ${GREEN}✓ No security issues${RESET}" + fi +else + echo -e " ${YELLOW}⚠ trivy not installed, skipping${RESET}" +fi +echo "" + +# Summary +echo "═══════════════════════════════════════" +if [[ "$ERRORS" -eq 0 ]]; then + echo -e " ${GREEN}✓ All validations passed${RESET}" +else + echo -e " ${RED}✗ ${ERRORS} check(s) failed${RESET}" +fi +echo "═══════════════════════════════════════" + +exit "$ERRORS" diff --git a/tests/integration/main_test.tftest.hcl b/tests/integration/main_test.tftest.hcl new file mode 100644 index 0000000..a997bb8 --- /dev/null +++ b/tests/integration/main_test.tftest.hcl @@ -0,0 +1,37 @@ +# Integration Tests for Example Module +# +# These tests run against a real AWS provider. +# Requires valid AWS credentials. +# +# Run with: terraform test -filter=tests/integration/ +# +# WARNING: These tests create real AWS resources. +# Costs may be incurred. Resources are cleaned up after tests. + +# Uncomment and configure when ready for integration testing: +# +# provider "aws" { +# region = "eu-west-1" +# } +# +# variables { +# name = "integration-test" +# environment = "dev" +# namespace = "test" +# enabled = true +# force_destroy = true # Allow cleanup +# } +# +# run "creates_real_bucket" { +# command = apply +# +# assert { +# condition = aws_s3_bucket.this[0].id != "" +# error_message = "S3 bucket should be created with a valid ID." +# } +# +# assert { +# condition = aws_s3_bucket.this[0].bucket == "test-dev-integration-test" +# error_message = "Bucket name should follow naming convention." +# } +# } diff --git a/tests/unit/main_test.tftest.hcl b/tests/unit/main_test.tftest.hcl new file mode 100644 index 0000000..9ea9c14 --- /dev/null +++ b/tests/unit/main_test.tftest.hcl @@ -0,0 +1,189 @@ +# Unit Tests for Example Module +# +# These tests use mock providers — no real AWS calls are made. +# Run with: terraform test +# Run verbose: terraform test -verbose +# Run specific: terraform test -run "test_name" + +mock_provider "aws" {} + +# --------------------------------------------------------------------------- +# Test: Module creates resources with valid inputs +# --------------------------------------------------------------------------- +variables { + name = "test-bucket" + environment = "dev" + namespace = "unit" + enabled = true +} + +run "creates_s3_bucket" { + command = plan + + assert { + condition = length(aws_s3_bucket.this) == 1 + error_message = "Expected one S3 bucket to be created." + } + + assert { + condition = aws_s3_bucket.this[0].bucket == "unit-dev-test-bucket" + error_message = "Bucket name should follow naming convention: {namespace}-{environment}-{name}." + } + + assert { + condition = aws_s3_bucket.this[0].tags["Environment"] == "dev" + error_message = "Bucket should have Environment tag set to 'dev'." + } + + assert { + condition = aws_s3_bucket.this[0].tags["ManagedBy"] == "terraform" + error_message = "Bucket should have ManagedBy tag set to 'terraform'." + } +} + +# --------------------------------------------------------------------------- +# Test: Module creates versioning configuration +# --------------------------------------------------------------------------- +run "enables_versioning" { + command = plan + + variables { + versioning_enabled = true + } + + assert { + condition = aws_s3_bucket_versioning.this[0].versioning_configuration[0].status == "Enabled" + error_message = "Versioning should be enabled." + } +} + +run "disables_versioning" { + command = plan + + variables { + versioning_enabled = false + } + + assert { + condition = aws_s3_bucket_versioning.this[0].versioning_configuration[0].status == "Suspended" + error_message = "Versioning should be suspended." + } +} + +# --------------------------------------------------------------------------- +# Test: Module enforces encryption +# --------------------------------------------------------------------------- +run "uses_aes256_by_default" { + command = plan + + variables { + kms_key_arn = null + } + + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.this[0].rule[0].apply_server_side_encryption_by_default[0].sse_algorithm == "AES256" + error_message = "Default encryption should be AES256 when no KMS key is provided." + } +} + +run "uses_kms_when_key_provided" { + command = plan + + variables { + kms_key_arn = "arn:aws:kms:eu-west-1:123456789012:key/test-key-id" + } + + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.this[0].rule[0].apply_server_side_encryption_by_default[0].sse_algorithm == "aws:kms" + error_message = "Encryption should use aws:kms when KMS key is provided." + } +} + +# --------------------------------------------------------------------------- +# Test: Module blocks public access +# --------------------------------------------------------------------------- +run "blocks_public_access" { + command = plan + + assert { + condition = aws_s3_bucket_public_access_block.this[0].block_public_acls == true + error_message = "Public ACLs should be blocked." + } + + assert { + condition = aws_s3_bucket_public_access_block.this[0].block_public_policy == true + error_message = "Public policies should be blocked." + } + + assert { + condition = aws_s3_bucket_public_access_block.this[0].restrict_public_buckets == true + error_message = "Public buckets should be restricted." + } +} + +# --------------------------------------------------------------------------- +# Test: Module is disabled when enabled = false +# --------------------------------------------------------------------------- +run "disabled_creates_nothing" { + command = plan + + variables { + enabled = false + } + + assert { + condition = length(aws_s3_bucket.this) == 0 + error_message = "No resources should be created when module is disabled." + } + + assert { + condition = length(aws_s3_bucket_public_access_block.this) == 0 + error_message = "No public access block should be created when module is disabled." + } +} + +# --------------------------------------------------------------------------- +# Test: Naming without namespace +# --------------------------------------------------------------------------- +run "naming_without_namespace" { + command = plan + + variables { + namespace = "" + } + + assert { + condition = aws_s3_bucket.this[0].bucket == "dev-test-bucket" + error_message = "Without namespace, bucket name should be: {environment}-{name}." + } +} + +# --------------------------------------------------------------------------- +# Test: Variable validation — invalid environment +# --------------------------------------------------------------------------- +run "rejects_invalid_environment" { + command = plan + + variables { + environment = "invalid" + } + + expect_failures = [ + var.environment, + ] +} + +# --------------------------------------------------------------------------- +# Test: Variable validation — empty name +# --------------------------------------------------------------------------- +run "rejects_empty_name" { + command = plan + + variables { + name = " " + } + + expect_failures = [ + var.name, + ] +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..4459b29 --- /dev/null +++ b/variables.tf @@ -0,0 +1,67 @@ +# ----------------------------------------------------------------------------- +# Required Variables +# ----------------------------------------------------------------------------- + +variable "name" { + description = "Name of the resource. Used in naming convention." + type = string + + validation { + condition = length(trimspace(var.name)) > 0 + error_message = "Name must not be empty." + } +} + +variable "environment" { + description = "Environment name (e.g., dev, staging, prod)." + type = string + + validation { + condition = contains(["dev", "staging", "uat", "preprod", "prod"], var.environment) + error_message = "Environment must be one of: dev, staging, uat, preprod, prod." + } +} + +# ----------------------------------------------------------------------------- +# Optional Variables +# ----------------------------------------------------------------------------- + +variable "enabled" { + description = "Set to false to prevent the module from creating any resources." + type = bool + default = true +} + +variable "namespace" { + description = "Namespace for resource naming (e.g., org name, team)." + type = string + default = "" +} + +variable "versioning_enabled" { + description = "Enable S3 bucket versioning." + type = bool + default = true +} + +variable "force_destroy" { + description = "Allow destruction of non-empty S3 bucket." + type = bool + default = false +} + +variable "kms_key_arn" { + description = "ARN of the KMS key for server-side encryption. If null, AES256 is used." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Tags +# ----------------------------------------------------------------------------- + +variable "tags" { + description = "Additional tags to apply to all resources." + type = map(string) + default = {} +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..f83d421 --- /dev/null +++ b/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0" + } + } +}