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
+
+[](https://github.com/your-org/Terraform-module-base-template/actions/workflows/ci.yml)
+[](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"
+ }
+ }
+}