diff --git a/.github/workflows/commitmsg-conform.yml b/.github/workflows/commitmsg-conform.yml new file mode 100644 index 0000000..4940385 --- /dev/null +++ b/.github/workflows/commitmsg-conform.yml @@ -0,0 +1,14 @@ +name: Commit Message Conformance + +on: + pull_request: {} + +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + commitmsg-conform: + uses: actionsforge/actions/.github/workflows/commitmsg-conform.yml@main diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 0000000..034b809 --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,14 @@ +name: Markdown Lint + +on: + pull_request: {} + +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + markdown-lint: + uses: actionsforge/actions/.github/workflows/markdown-lint.yml@main diff --git a/.github/workflows/terraform-docs.yml b/.github/workflows/terraform-docs.yml new file mode 100644 index 0000000..56aa648 --- /dev/null +++ b/.github/workflows/terraform-docs.yml @@ -0,0 +1,13 @@ +name: Generate terraform docs + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + terraform-docs: + uses: actionsforge/actions/.github/workflows/terraform-docs.yml@main diff --git a/.github/workflows/terraform-lint-validate.yml b/.github/workflows/terraform-lint-validate.yml new file mode 100644 index 0000000..915f136 --- /dev/null +++ b/.github/workflows/terraform-lint-validate.yml @@ -0,0 +1,13 @@ +name: Terraform Lint & Validate + +on: + pull_request: {} +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + terraform-lint-validate: + uses: actionsforge/actions/.github/workflows/terraform-lint-validate.yml@main diff --git a/.github/workflows/terraform-tag-and-release.yml b/.github/workflows/terraform-tag-and-release.yml new file mode 100644 index 0000000..069a79e --- /dev/null +++ b/.github/workflows/terraform-tag-and-release.yml @@ -0,0 +1,12 @@ +name: Terraform Tag and Release +on: + workflow_run: + workflows: ["Generate terraform docs"] + types: + - completed + +permissions: + contents: write +jobs: + terraform-tag-and-release: + uses: actionsforge/actions/.github/workflows/terraform-tag-and-release.yml@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faeac57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +*tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Kubeconfig files +.terraform-kubeconfig-* + +# Secrets - never commit actual secrets +**/secret.yaml +!**/secret.yaml.example +**/*secret*.yaml +!**/*secret*.yaml.example diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..97ae20b --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,105 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.28.0" + constraints = ">= 6.0.0" + hashes = [ + "h1:wzZdGs0FFmNqIgPyo9tKnGKJ37BGNSgwRrEXayL29+0=", + "zh:0ba0d5eb6e0c6a933eb2befe3cdbf22b58fbc0337bf138f95bf0e8bb6e6df93e", + "zh:23eacdd4e6db32cf0ff2ce189461bdbb62e46513978d33c5de4decc4670870ec", + "zh:307b06a15fc00a8e6fd243abde2cbe5112e9d40371542665b91bec1018dd6e3c", + "zh:37a02d5b45a9d050b9642c9e2e268297254192280df72f6e46641daca52e40ec", + "zh:3da866639f07d92e734557d673092719c33ede80f4276c835bf7f231a669aa33", + "zh:480060b0ba310d0f6b6a14d60b276698cb103c48fd2f7e2802ae47c963995ec6", + "zh:57796453455c20db80d9168edbf125bf6180e1aae869de1546a2be58e4e405ec", + "zh:69139cba772d4df8de87598d8d8a2b1b4b254866db046c061dccc79edb14e6b9", + "zh:7312763259b859ff911c5452ca8bdf7d0be6231c5ea0de2df8f09d51770900ac", + "zh:8d2d6f4015d3c155d7eb53e36f019a729aefb46ebfe13f3a637327d3a1402ecc", + "zh:94ce589275c77308e6253f607de96919b840c2dd36c44aa798f693c9dd81af42", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:adaceec6a1bf4f5df1e12bd72cf52b72087c72efed078aef636f8988325b1a8b", + "zh:d37be1ce187d94fd9df7b13a717c219964cd835c946243f096c6b230cdfd7e92", + "zh:fe6205b5ca2ff36e68395cb8d3ae10a3728f405cdbcd46b206a515e1ebcf17a1", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.13" + hashes = [ + "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.30" + hashes = [ + "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=", + "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", + "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f", + "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b", + "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12", + "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2", + "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc", + "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15", + "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396", + "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d", + "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = "~> 0.11" + hashes = [ + "h1:+W+DMrVoVnoXo3f3M4W+OpZbkCrUn6PnqDF33D2Cuf0=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.1.0" + constraints = "~> 4.0" + hashes = [ + "h1:Ka8mEwRFXBabR33iN/WTIEW6RP0z13vFsDlwn11Pf2I=", + "zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2", + "zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8", + "zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc", + "zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc", + "zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac", + "zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882", + "zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d", + "zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298", + "zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297", + "zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54", + ] +} diff --git a/.vscode/cspell.json b/.vscode/cspell.json new file mode 100644 index 0000000..5711674 --- /dev/null +++ b/.vscode/cspell.json @@ -0,0 +1,4 @@ +{ + "words": [ + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4291bb0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "eamodio.gitlens", + "esbenp.prettier-vscode", + "foxundermoon.shell-format", + "Gruntfuggly.todo-tree", + "hashicorp.terraform", + "mhutchie.git-graph", + "ms-python.autopep8", + "ms-python.debugpy", + "ms-python.python", + "ms-python.black-formatter", + "ms-python.flake8", + "streetsidesoftware.code-spell-checker", + "usernamehw.errorlens", + "vscode-icons-team.vscode-icons" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f377d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,208 @@ +{ + "files.associations": { + "*.dockerfile": "dockerfile", + "*.sh.tpl": "shellscript", + "docker-compose*.yml": "yaml", + "Dockerfile*": "dockerfile", + "*.py.tpl": "python", + "*.yaml.tpl": "yaml", + "*.yml.tpl": "yaml", + "*.tf": "terraform", + "*.tfvars": "terraform" + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "__debug_bin": true, + "vendor/": true, + "go.sum": true, + "**/__pycache__": true, + "**/*.pyc": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/node_modules": true, + "**/.terraform": true, + "**/.terragrunt-cache": true, + "**/Thumbs.db": true, + "**/.ruff_cache": true, + "**/.coverage": true, + "**/htmlcov": true, + "**/*.tfstate": true, + "**/*.tfstate.*": true + }, + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "files.eol": "\n", + "remote.extensionKind": { + "ms-azuretools.vscode-docker": "ui", + "ms-vscode-remote.remote-containers": "ui" + }, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.rulers": [79], + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 79, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.trimAutoWhitespace": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.fixAll.ruff": "explicit" + }, + "prettier.requireConfig": true, + "workbench.iconTheme": "vscode-icons", + "workbench.colorTheme": "Visual Studio Dark", + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features", + "editor.foldingStrategy": "indentation" + }, + "[dockerfile]": { + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[json]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[terraform]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "hashicorp.terraform", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[yaml]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[yml]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + "[python]": { + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.fixAll.ruff": "explicit" + }, + "editor.rulers": [79], + "editor.wordWrapColumn": 79, + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + "python.formatting.provider": "none", + "python.formatting.blackArgs": [ + "--line-length=79", + "--target-version=py39", + "--skip-string-normalization", + "--config=lambdas/pyproject.toml" + ], + "python.linting.enabled": true, + "python.linting.lintOnSave": true, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + "--max-line-length=79", + "--extend-ignore=E203,W503,E501", + "--max-complexity=10" + ], + "ruff.enable": true, + "ruff.fixAll": true, + "ruff.organizeImports": true, + "ruff.lint.enable": true, + "ruff.format.enable": true, + "ruff.codeAction.fixViolation": { + "enable": true + }, + "ruff.codeAction.disableRuleComment": { + "enable": true + }, + "python.linting.mypyEnabled": true, + "python.linting.mypyArgs": [ + "--config-file=lambdas/pyproject.toml" + ], + "python.linting.pylintEnabled": false, + "isort.args": [ + "--settings-path=lambdas/pyproject.toml" + ], + "isort.check": true, + "python.analysis.typeCheckingMode": "off", + "python.analysis.autoImportCompletions": true, + "python.analysis.autoSearchPaths": true, + "python.analysis.diagnosticMode": "workspace", + "python.analysis.completeFunctionParens": true, + "python.analysis.autoFormatStrings": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "lambdas" + ], + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.terminal.activateEnvironment": false, + "autoDocstring.docstringFormat": "google", + "autoDocstring.startOnNewLine": false, + "autoDocstring.includeExtendedSummary": true, + "git.autofetch": true, + "git.confirmSync": false, + "git.enableSmartCommit": true, + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.copyOnSelection": true, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/__pycache__": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/.ruff_cache": true + }, + "markdown.extension.toc.levels": "2..6", + "markdown.extension.print.absoluteImgPath": false, + "yaml.format.enable": true, + "yaml.format.singleQuote": false, + "yaml.format.bracketSpacing": true, + "python.analysis.indexing": true, + "python.analysis.packageIndexDepths": [ + { + "name": "boto3", + "depth": 2 + }, + { + "name": "botocore", + "depth": 2 + } + ], + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/*/**": true, + "**/__pycache__/**": true, + "**/.pytest_cache/**": true, + "**/.mypy_cache/**": true, + "**/.terraform/**": true, + "**/.terragrunt-cache/**": true + }, + "terraform.format.enable": true, + "terraform.lint.enable": true +} diff --git a/README.md b/README.md index f9f84b8..2d80cce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,230 @@ # terraform-aws-eks-basic -Simple EKS cluster Terraform module + +A basic Terraform module for creating and managing Amazon EKS (Elastic Kubernetes Service) clusters. This module supports multiple compute modes: EC2, Fargate, and AutoMode, with EC2 as the primary focus. + +## Features + +- **Multi-Compute Support**: Supports EC2, Fargate, and AutoMode compute types +- **EC2 Managed Node Groups**: Full support for EC2 managed node groups with auto-scaling +- **Fargate Profiles**: Structure ready for Fargate profile configuration +- **AutoMode**: Structure ready for EKS AutoMode configuration +- **IRSA Support**: OIDC provider setup for IAM Roles for Service Accounts +- **EKS Capabilities**: Managed ACK, KRO, and ArgoCD capabilities (optional, default: disabled) + - **ACK**: AWS Controllers for Kubernetes - create AWS resources via Kubernetes manifests + - **KRO**: Kube Resource Orchestrator - platform engineering abstractions + - **ArgoCD**: GitOps capability for continuous deployment +- **Optional Addons**: + - EBS CSI Driver (optional, default: disabled) + - AWS Load Balancer Controller (optional, default: disabled) +- **Comprehensive Testing**: Includes Terraform test suite + +## Requirements + +| Name | Version | +| ---- | ------- | +| terraform | >= 1.6.0 | +| aws | >= 6.0 | +| kubernetes | ~> 2.30 | +| helm | ~> 2.13 | +| tls | ~> 4.0 | + +## Usage + +### Basic Example (EC2) + +```hcl +module "eks" { + source = "path/to/terraform-aws-eks-basic" + + cluster_name = "my-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + + # EC2 compute mode (default) + compute_mode = ["ec2"] + + # Node group configuration + node_instance_types = ["t3.medium"] + node_desired_size = 2 + node_min_size = 1 + node_max_size = 3 + node_disk_size = 20 + + tags = { + Environment = "production" + ManagedBy = "terraform" + } +} +``` + +### With Optional Addons + +```hcl +module "eks" { + source = "path/to/terraform-aws-eks-basic" + + cluster_name = "my-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + + compute_mode = ["ec2"] + + # Enable optional addons + enable_ebs_csi_driver = true + enable_aws_lb_controller = true + + tags = { + Environment = "production" + } +} +``` + +### With EKS Capabilities + +```hcl +module "eks" { + source = "path/to/terraform-aws-eks-basic" + + cluster_name = "my-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + + compute_mode = ["ec2"] + + # Enable EKS Capabilities for platform engineering + enable_ack_capability = true # AWS Controllers for Kubernetes + enable_kro_capability = true # Kube Resource Orchestrator + enable_argocd_capability = true # ArgoCD GitOps + + tags = { + Environment = "production" + } +} +``` + +### Fargate Example + +```hcl +module "eks" { + source = "path/to/terraform-aws-eks-basic" + + cluster_name = "my-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + + # Fargate compute mode + compute_mode = ["fargate"] + + fargate_profiles = { + default = { + subnet_ids = ["subnet-12345678"] + selectors = [ + { + namespace = "default" + labels = {} + } + ] + } + } + + tags = { + Environment = "production" + } +} +``` + +### Multiple Compute Modes + +```hcl +module "eks" { + source = "path/to/terraform-aws-eks-basic" + + cluster_name = "my-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + + # Use both EC2 and Fargate + compute_mode = ["ec2", "fargate"] + + # EC2 configuration + node_instance_types = ["t3.medium"] + node_desired_size = 2 + + # Fargate configuration + fargate_profiles = { + default = { + subnet_ids = ["subnet-12345678"] + selectors = [ + { + namespace = "default" + } + ] + } + } + + tags = { + Environment = "production" + } +} +``` + +## Examples + +- **[examples/basic](examples/basic/)** - Basic EKS cluster with EC2 node groups +- **[examples/ebs-web-app](examples/ebs-web-app/)** - Web application with EBS persistent volume +- **[examples/eks-capabilities](examples/eks-capabilities/)** - Complete platform engineering example with ACK, KRO, and ArgoCD capabilities + + + + +## Connecting to the Cluster + +After the cluster is created, configure `kubectl`: + +```bash +aws eks update-kubeconfig --name --region +``` + +Verify connection: + +```bash +kubectl get nodes +``` + +## Testing + +The module includes comprehensive tests using Terraform's test framework. Run tests with: + +```bash +terraform test +``` + +## Module Structure + +```plaintext +terraform-aws-eks-basic/ +├── main.tf # Core EKS cluster, IAM roles, OIDC provider +├── ec2.tf # EC2 managed node groups +├── fargate.tf # Fargate profiles +├── automode.tf # AutoMode configuration +├── capabilities.tf # EKS Capabilities (ACK, KRO, ArgoCD) +├── capabilities-iam.tf # IAM roles for EKS Capabilities +├── addons.tf # Optional addons (EBS CSI, ALB Controller) +├── variables.tf # Input variables +├── outputs.tf # Output values +├── versions.tf # Provider version constraints +├── README.md # This file +├── tests/ +│ └── eks_test.tftest.hcl # Test suite +└── examples/ + └── basic/ # Basic usage example +``` + +## License + +MIT License - see LICENSE file for details. diff --git a/addons.tf b/addons.tf new file mode 100644 index 0000000..052edfa --- /dev/null +++ b/addons.tf @@ -0,0 +1,352 @@ +# ============================================================================= +# EBS CSI Driver IAM (IRSA setup) +# ============================================================================= + +data "aws_iam_policy_document" "ebs_csi_driver_assume_role" { + count = var.enable_ebs_csi_driver ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.eks.arn] + } + + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "${replace(aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub" + values = ["system:serviceaccount:kube-system:ebs-csi-controller-sa"] + } + + condition { + test = "StringEquals" + variable = "${replace(aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:aud" + values = ["sts.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + + name = "${var.cluster_name}-ebs-csi-driver" + assume_role_policy = data.aws_iam_policy_document.ebs_csi_driver_assume_role[0].json + tags = var.tags + + # Explicit dependency to ensure OIDC provider exists before creating IRSA role + depends_on = [ + aws_iam_openid_connect_provider.eks + ] +} + +resource "aws_iam_role_policy" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + + name = "${var.cluster_name}-ebs-csi-driver-policy" + role = aws_iam_role.ebs_csi_driver[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "EBSCSIVolumeManagement" + Effect = "Allow" + Action = [ + "ec2:CreateVolume", + "ec2:DeleteVolume", + "ec2:AttachVolume", + "ec2:DetachVolume", + "ec2:ModifyVolume" + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:RequestedRegion" = data.aws_region.current.id + } + } + }, + { + Sid = "EBSCSISnapshotManagement" + Effect = "Allow" + Action = [ + "ec2:CreateSnapshot", + "ec2:DeleteSnapshot" + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:RequestedRegion" = data.aws_region.current.id + } + } + }, + { + Sid = "EBSCSIDescribeOperations" + Effect = "Allow" + Action = [ + "ec2:DescribeInstances", + "ec2:DescribeSnapshots", + "ec2:DescribeVolumes", + "ec2:DescribeAvailabilityZones" + ] + Resource = "*" + }, + { + Sid = "EBSCSITaggingOperations" + Effect = "Allow" + Action = [ + "ec2:CreateTags", + "ec2:DescribeTags" + ] + Resource = [ + "arn:aws:ec2:*:*:volume/*", + "arn:aws:ec2:*:*:snapshot/*" + ] + Condition = { + StringEquals = { + "ec2:CreateAction" = [ + "CreateVolume", + "CreateSnapshot" + ] + } + } + } + ] + }) +} + +resource "aws_eks_addon" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + addon_name = "aws-ebs-csi-driver" + addon_version = var.ebs_csi_driver_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "OVERWRITE" + service_account_role_arn = aws_iam_role.ebs_csi_driver[0].arn + + depends_on = [ + aws_iam_role_policy.ebs_csi_driver[0] + ] + + tags = var.tags +} + +resource "kubernetes_storage_class" "ebs_csi_default" { + count = var.enable_ebs_csi_driver ? 1 : 0 + + metadata { + name = "gp3" + annotations = { + "storageclass.kubernetes.io/is-default-class" = "true" + } + } + + storage_provisioner = "ebs.csi.aws.com" + volume_binding_mode = "WaitForFirstConsumer" + allow_volume_expansion = true + + parameters = { + type = "gp3" + fsType = "ext4" + } + + depends_on = [ + aws_eks_addon.ebs_csi_driver[0] + ] +} + +# ============================================================================= +# EKS Pod Identity Agent +# ============================================================================= + +resource "aws_eks_addon" "pod_identity_agent" { + count = var.enable_pod_identity_agent ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + addon_name = "eks-pod-identity-agent" + addon_version = var.pod_identity_agent_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "OVERWRITE" + + tags = var.tags +} + +# ============================================================================= +# AWS Load Balancer Controller IAM (IRSA setup) +# ============================================================================= + +data "aws_iam_policy_document" "aws_lb_controller_assume_role" { + count = var.enable_aws_lb_controller ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.eks.arn] + } + + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "${replace(aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub" + values = ["system:serviceaccount:kube-system:aws-load-balancer-controller"] + } + + condition { + test = "StringEquals" + variable = "${replace(aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:aud" + values = ["sts.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "aws_lb_controller" { + count = var.enable_aws_lb_controller ? 1 : 0 + + name = "${var.cluster_name}-aws-lb-controller" + assume_role_policy = data.aws_iam_policy_document.aws_lb_controller_assume_role[0].json + tags = var.tags + + # Explicit dependency to ensure OIDC provider exists before creating IRSA role + depends_on = [ + aws_iam_openid_connect_provider.eks + ] +} + +resource "aws_iam_role_policy_attachment" "aws_lb_controller" { + count = var.enable_aws_lb_controller ? 1 : 0 + + role = aws_iam_role.aws_lb_controller[0].name + policy_arn = "arn:aws:iam::aws:policy/ElasticLoadBalancingFullAccess" +} + +resource "aws_iam_role_policy_attachment" "aws_lb_controller_ec2" { + count = var.enable_aws_lb_controller ? 1 : 0 + + role = aws_iam_role.aws_lb_controller[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess" +} + +resource "aws_iam_role_policy" "aws_lb_controller_waf" { + count = var.enable_aws_lb_controller ? 1 : 0 + + name = "${var.cluster_name}-aws-lb-controller-waf" + role = aws_iam_role.aws_lb_controller[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "WAFv2Permissions" + Effect = "Allow" + Action = [ + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:AssociateWebACL", + "wafv2:DisassociateWebACL", + "wafv2:ListWebACLs" + ] + Resource = "*" + }, + { + Sid = "WAFRegionalPermissions" + Effect = "Allow" + Action = [ + "waf-regional:GetWebACL", + "waf-regional:GetWebACLForResource", + "waf-regional:AssociateWebACL", + "waf-regional:DisassociateWebACL", + "waf-regional:ListWebACLs" + ] + Resource = "*" + }, + { + Sid = "ShieldPermissions" + Effect = "Allow" + Action = [ + "shield:GetSubscriptionState", + "shield:DescribeProtection", + "shield:CreateProtection", + "shield:DeleteProtection" + ] + Resource = "*" + } + ] + }) +} + +resource "kubernetes_service_account" "aws_lb_controller" { + count = var.enable_aws_lb_controller ? 1 : 0 + + metadata { + name = "aws-load-balancer-controller" + namespace = "kube-system" + annotations = { + "eks.amazonaws.com/role-arn" = aws_iam_role.aws_lb_controller[0].arn + } + labels = { + "app.kubernetes.io/name" = "aws-load-balancer-controller" + "app.kubernetes.io/component" = "controller" + "app.kubernetes.io/managed-by" = "terraform" + } + } + + depends_on = [ + aws_eks_cluster.this, + aws_iam_role_policy_attachment.aws_lb_controller[0], + aws_iam_role_policy_attachment.aws_lb_controller_ec2[0], + aws_iam_role_policy.aws_lb_controller_waf[0] + ] +} + +resource "helm_release" "aws_load_balancer_controller" { + count = var.enable_aws_lb_controller ? 1 : 0 + + name = "aws-load-balancer-controller" + repository = "https://aws.github.io/eks-charts" + chart = "aws-load-balancer-controller" + namespace = "kube-system" + version = var.aws_lb_controller_helm_version + + set { + name = "clusterName" + value = aws_eks_cluster.this.name + } + + set { + name = "serviceAccount.create" + value = "false" + } + + set { + name = "serviceAccount.name" + value = "aws-load-balancer-controller" + } + + set { + name = "region" + value = data.aws_region.current.id + } + + set { + name = "vpcId" + value = var.vpc_id + } + + dynamic "set" { + for_each = var.aws_lb_controller_helm_values + content { + name = set.key + value = set.value + } + } + + depends_on = [ + kubernetes_service_account.aws_lb_controller[0] + ] +} diff --git a/automode.tf b/automode.tf new file mode 100644 index 0000000..2b8a54e --- /dev/null +++ b/automode.tf @@ -0,0 +1,16 @@ +# ============================================================================= +# EKS AutoMode Configuration +# ============================================================================= + +# Note: AutoMode is configured via the compute block in the EKS cluster resource +# This file is reserved for future AutoMode-specific resources and configurations +# when they become available in the AWS provider + +# AutoMode configuration is handled in main.tf within the aws_eks_cluster resource +# using the dynamic "compute" block. This structure allows for future expansion +# of AutoMode features as they become available. + +locals { + # Helper to determine if AutoMode is enabled + automode_enabled = contains(var.compute_mode, "automode") +} diff --git a/capabilities-iam.tf b/capabilities-iam.tf new file mode 100644 index 0000000..e2e696a --- /dev/null +++ b/capabilities-iam.tf @@ -0,0 +1,101 @@ +# ============================================================================= +# EKS Capabilities IAM Roles +# IAM roles required for EKS Capabilities (ACK, KRO, ArgoCD) +# ============================================================================= + +# ACK Capability Role +data "aws_iam_policy_document" "ack_capability_assume_role" { + count = var.enable_ack_capability ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["capabilities.eks.amazonaws.com"] + } + + actions = [ + "sts:AssumeRole", + "sts:TagSession" + ] + } +} + +resource "aws_iam_role" "ack_capability" { + count = var.enable_ack_capability ? 1 : 0 + + name = var.ack_capability_role_arn != null ? null : "${var.cluster_name}-ack-capability-role" + name_prefix = var.ack_capability_role_arn != null ? null : null + assume_role_policy = data.aws_iam_policy_document.ack_capability_assume_role[0].json + tags = var.tags +} + +# Attach IAM policies to ACK capability role +resource "aws_iam_role_policy_attachment" "ack_capability" { + for_each = var.enable_ack_capability ? var.ack_capability_iam_policy_arns : {} + + role = aws_iam_role.ack_capability[0].name + policy_arn = each.value +} + +# KRO Capability Role +data "aws_iam_policy_document" "kro_capability_assume_role" { + count = var.enable_kro_capability ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["capabilities.eks.amazonaws.com"] + } + + actions = [ + "sts:AssumeRole", + "sts:TagSession" + ] + } +} + +resource "aws_iam_role" "kro_capability" { + count = var.enable_kro_capability ? 1 : 0 + + name = var.kro_capability_role_arn != null ? null : "${var.cluster_name}-kro-capability-role" + name_prefix = var.kro_capability_role_arn != null ? null : null + assume_role_policy = data.aws_iam_policy_document.kro_capability_assume_role[0].json + tags = var.tags +} + +# Note: KRO capability roles don't require managed policies - AWS manages permissions internally + +# ArgoCD Capability Role +data "aws_iam_policy_document" "argocd_capability_assume_role" { + count = var.enable_argocd_capability ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["capabilities.eks.amazonaws.com"] + } + + actions = [ + "sts:AssumeRole", + "sts:TagSession" + ] + } +} + +resource "aws_iam_role" "argocd_capability" { + count = var.enable_argocd_capability ? 1 : 0 + + name = var.argocd_capability_role_arn != null ? null : "${var.cluster_name}-argocd-capability-role" + name_prefix = var.argocd_capability_role_arn != null ? null : null + assume_role_policy = data.aws_iam_policy_document.argocd_capability_assume_role[0].json + tags = var.tags +} + +# Note: ArgoCD capability roles don't require managed policies - AWS manages permissions internally +# ArgoCD also requires configuration which should be provided via the capability resource diff --git a/capabilities.tf b/capabilities.tf new file mode 100644 index 0000000..e187d89 --- /dev/null +++ b/capabilities.tf @@ -0,0 +1,69 @@ +# ============================================================================= +# EKS Capabilities +# Managed ACK, KRO, and ArgoCD capabilities running in AWS-managed infrastructure +# ============================================================================= + +resource "aws_eks_capability" "ack" { + count = var.enable_ack_capability ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + capability_name = "ACK" + type = "ACK" + + # ACK requires a role ARN for creating AWS resources + # Use provided role ARN or create one automatically + role_arn = var.ack_capability_role_arn != null ? var.ack_capability_role_arn : aws_iam_role.ack_capability[0].arn + + delete_propagation_policy = "RETAIN" + + depends_on = [ + aws_eks_cluster.this + ] + + tags = var.tags +} + +resource "aws_eks_capability" "kro" { + count = var.enable_kro_capability ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + capability_name = "KRO" + type = "KRO" + + # KRO requires a role ARN + # Use provided role ARN or create one automatically + role_arn = var.kro_capability_role_arn != null ? var.kro_capability_role_arn : aws_iam_role.kro_capability[0].arn + + delete_propagation_policy = "RETAIN" + + depends_on = [ + aws_eks_cluster.this + ] + + tags = var.tags +} + +resource "aws_eks_capability" "argocd" { + count = var.enable_argocd_capability ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + capability_name = "ARGOCD" + type = "ARGOCD" + + # ArgoCD requires a role ARN + # Use provided role ARN or create one automatically + role_arn = var.argocd_capability_role_arn != null ? var.argocd_capability_role_arn : aws_iam_role.argocd_capability[0].arn + + # Note: ArgoCD capability requires AWS Identity Center configuration + # This is typically done via AWS Console or requires additional setup + # For now, this resource will fail if Identity Center is not configured + # Users should configure Identity Center before enabling ArgoCD capability + + delete_propagation_policy = "RETAIN" + + depends_on = [ + aws_eks_cluster.this + ] + + tags = var.tags +} diff --git a/cluster-auth.tf b/cluster-auth.tf new file mode 100644 index 0000000..3959d02 --- /dev/null +++ b/cluster-auth.tf @@ -0,0 +1,7 @@ +# ============================================================================= +# EKS Cluster Authentication Data Source +# ============================================================================= + +data "aws_eks_cluster_auth" "this" { + name = aws_eks_cluster.this.name +} diff --git a/ec2.tf b/ec2.tf new file mode 100644 index 0000000..af6dea3 --- /dev/null +++ b/ec2.tf @@ -0,0 +1,92 @@ +# ============================================================================= +# EC2 Node IAM Role +# ============================================================================= + +data "aws_iam_policy_document" "eks_nodes_assume_role" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "eks_nodes" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + name = "${var.cluster_name}-eks-nodes-role" + assume_role_policy = data.aws_iam_policy_document.eks_nodes_assume_role[0].json + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "eks_nodes_worker" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + role = aws_iam_role.eks_nodes[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" +} + +resource "aws_iam_role_policy_attachment" "eks_nodes_cni" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + role = aws_iam_role.eks_nodes[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" +} + +resource "aws_iam_role_policy_attachment" "eks_nodes_ecr" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + role = aws_iam_role.eks_nodes[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" +} + +# ============================================================================= +# EC2 Managed Node Group +# ============================================================================= + +resource "aws_eks_node_group" "default" { + count = contains(var.compute_mode, "ec2") ? 1 : 0 + + cluster_name = aws_eks_cluster.this.name + node_group_name = "${var.cluster_name}-default" + node_role_arn = aws_iam_role.eks_nodes[0].arn + subnet_ids = var.node_subnet_ids != null ? var.node_subnet_ids : var.subnet_ids + + instance_types = var.node_instance_types + + disk_size = var.node_disk_size + + scaling_config { + desired_size = var.node_desired_size + min_size = var.node_min_size + max_size = var.node_max_size + } + + update_config { + max_unavailable = var.node_update_max_unavailable + } + + dynamic "remote_access" { + for_each = var.node_remote_access_enabled ? [1] : [] + content { + ec2_ssh_key = var.node_remote_access_ssh_key + source_security_group_ids = var.node_remote_access_security_groups + } + } + + labels = var.node_labels + + tags = var.tags + + depends_on = [ + aws_iam_role_policy_attachment.eks_nodes_worker[0], + aws_iam_role_policy_attachment.eks_nodes_cni[0], + aws_iam_role_policy_attachment.eks_nodes_ecr[0], + ] +} diff --git a/examples/basic/.terraform.lock.hcl b/examples/basic/.terraform.lock.hcl new file mode 100644 index 0000000..ebe2198 --- /dev/null +++ b/examples/basic/.terraform.lock.hcl @@ -0,0 +1,85 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.28.0" + constraints = ">= 6.0.0" + hashes = [ + "h1:wzZdGs0FFmNqIgPyo9tKnGKJ37BGNSgwRrEXayL29+0=", + "zh:0ba0d5eb6e0c6a933eb2befe3cdbf22b58fbc0337bf138f95bf0e8bb6e6df93e", + "zh:23eacdd4e6db32cf0ff2ce189461bdbb62e46513978d33c5de4decc4670870ec", + "zh:307b06a15fc00a8e6fd243abde2cbe5112e9d40371542665b91bec1018dd6e3c", + "zh:37a02d5b45a9d050b9642c9e2e268297254192280df72f6e46641daca52e40ec", + "zh:3da866639f07d92e734557d673092719c33ede80f4276c835bf7f231a669aa33", + "zh:480060b0ba310d0f6b6a14d60b276698cb103c48fd2f7e2802ae47c963995ec6", + "zh:57796453455c20db80d9168edbf125bf6180e1aae869de1546a2be58e4e405ec", + "zh:69139cba772d4df8de87598d8d8a2b1b4b254866db046c061dccc79edb14e6b9", + "zh:7312763259b859ff911c5452ca8bdf7d0be6231c5ea0de2df8f09d51770900ac", + "zh:8d2d6f4015d3c155d7eb53e36f019a729aefb46ebfe13f3a637327d3a1402ecc", + "zh:94ce589275c77308e6253f607de96919b840c2dd36c44aa798f693c9dd81af42", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:adaceec6a1bf4f5df1e12bd72cf52b72087c72efed078aef636f8988325b1a8b", + "zh:d37be1ce187d94fd9df7b13a717c219964cd835c946243f096c6b230cdfd7e92", + "zh:fe6205b5ca2ff36e68395cb8d3ae10a3728f405cdbcd46b206a515e1ebcf17a1", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.13" + hashes = [ + "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.30" + hashes = [ + "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=", + "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", + "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f", + "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b", + "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12", + "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2", + "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc", + "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15", + "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396", + "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d", + "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.1.0" + constraints = "~> 4.0" + hashes = [ + "h1:Ka8mEwRFXBabR33iN/WTIEW6RP0z13vFsDlwn11Pf2I=", + "zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2", + "zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8", + "zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc", + "zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc", + "zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac", + "zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882", + "zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d", + "zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298", + "zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297", + "zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54", + ] +} diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..a3a3de3 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,95 @@ +# Basic EKS Example + +This example demonstrates how to create a basic EKS cluster with EC2 managed node groups using the `terraform-aws-eks-basic` module. + +## Usage + +1. **Set required variables:** + + Create a `terraform.tfvars` file or set environment variables: + + ```hcl + aws_region = "ap-southeast-2" + cluster_name = "my-eks-cluster" + ``` + + Note: This example creates a VPC automatically using the `cloudbuildlab/vpc/aws` module. + You don't need to provide `vpc_id` or `subnet_ids` - they are created by the VPC module. + +2. **Initialize Terraform:** + + ```bash + terraform init + ``` + +3. **Review the plan:** + + ```bash + terraform plan + ``` + +4. **Apply the configuration:** + + ```bash + terraform apply + ``` + +## Configuration + +### Required Variables + +- `cluster_name`: Name of the EKS cluster (default: `cltest`) +- `aws_region`: AWS region for resources (default: `ap-southeast-2`) + +### VPC Configuration + +This example automatically creates a VPC with: + +- 2 availability zones +- Public and private subnets in each AZ +- Internet Gateway and NAT Gateway for internet access +- EKS-optimized tags for subnet discovery + +### Optional Variables + +- `cluster_name`: Name of the EKS cluster (default: `example-eks-cluster`) +- `cluster_version`: Kubernetes version (default: `1.28`) +- `node_instance_types`: EC2 instance types for nodes (default: `["t3.medium"]`) +- `node_desired_size`: Desired number of nodes (default: `2`) +- `node_min_size`: Minimum number of nodes (default: `1`) +- `node_max_size`: Maximum number of nodes (default: `3`) +- `node_disk_size`: Disk size in GiB (default: `20`) +- `enable_ebs_csi_driver`: Enable EBS CSI Driver addon (default: `false`) +- `enable_aws_lb_controller`: Enable AWS Load Balancer Controller (default: `false`) + +## Outputs + +After applying, you'll get outputs including: + +- `cluster_name`: Name of the EKS cluster +- `cluster_endpoint`: Endpoint URL for the Kubernetes API server +- `cluster_ca_data`: Base64 encoded certificate authority data +- `oidc_provider_arn`: ARN of the OIDC provider for IRSA +- `node_group_id`: ID of the managed node group +- `node_role_arn`: IAM role ARN for EC2 nodes + +## Connecting to the Cluster + +After the cluster is created, configure `kubectl`: + +```bash +aws eks update-kubeconfig --name --region +``` + +Verify connection: + +```bash +kubectl get nodes +``` + +## Next Steps + +- Configure AWS Auth to allow IAM users/roles to access the cluster +- Deploy applications to the cluster +- Enable additional addons (EBS CSI Driver, Load Balancer Controller) as needed +- Explore Fargate or AutoMode compute options diff --git a/examples/basic/main.tf b/examples/basic/main.tf new file mode 100644 index 0000000..54f4439 --- /dev/null +++ b/examples/basic/main.tf @@ -0,0 +1,97 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token + } +} + +locals { + base_name = var.cluster_name + name = var.cluster_name + tags = var.tags +} + +module "vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = local.base_name + vpc_cidr = "10.0.0.0/16" + availability_zones = ["${var.aws_region}a", "${var.aws_region}b"] + + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"] + + # Enable Internet Gateway & NAT Gateway + create_igw = true + nat_gateway_type = "single" + + enable_eks_tags = true + eks_cluster_name = local.name + + tags = local.tags +} + +# Example: Basic EKS cluster with EC2 node groups +module "eks" { + source = "../../" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + vpc_id = module.vpc.vpc_id + subnet_ids = concat(module.vpc.public_subnet_ids, module.vpc.private_subnet_ids) + node_subnet_ids = module.vpc.private_subnet_ids + + # Use EC2 compute mode (default) + compute_mode = ["ec2"] + + # EC2 Node Group Configuration + node_instance_types = var.node_instance_types + node_desired_size = var.node_desired_size + node_min_size = var.node_min_size + node_max_size = var.node_max_size + node_disk_size = var.node_disk_size + + # Optional: Enable addons + enable_ebs_csi_driver = var.enable_ebs_csi_driver + enable_aws_lb_controller = var.enable_aws_lb_controller + + # Optional: AWS Auth configuration + aws_auth_map_users = var.aws_auth_map_users + aws_auth_map_roles = var.aws_auth_map_roles + + tags = var.tags +} diff --git a/examples/basic/outputs.tf b/examples/basic/outputs.tf new file mode 100644 index 0000000..b663240 --- /dev/null +++ b/examples/basic/outputs.tf @@ -0,0 +1,52 @@ +# VPC Outputs +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "public_subnet_ids" { + description = "IDs of the public subnets" + value = module.vpc.public_subnet_ids +} + +output "private_subnet_ids" { + description = "IDs of the private subnets" + value = module.vpc.private_subnet_ids +} + +# EKS Cluster Outputs +output "cluster_name" { + description = "Name of the EKS cluster" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "Endpoint for EKS control plane" + value = module.eks.cluster_endpoint +} + +output "cluster_ca_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = module.eks.cluster_ca_data + sensitive = true +} + +output "cluster_version" { + description = "Kubernetes version of the EKS cluster" + value = module.eks.cluster_version +} + +output "oidc_provider_arn" { + description = "ARN of the EKS OIDC provider" + value = module.eks.oidc_provider_arn +} + +output "node_group_id" { + description = "ID of the EKS node group" + value = module.eks.node_group_id +} + +output "node_role_arn" { + description = "IAM role ARN for EC2 nodes" + value = module.eks.node_role_arn +} diff --git a/examples/basic/variables.tf b/examples/basic/variables.tf new file mode 100644 index 0000000..a2e06d0 --- /dev/null +++ b/examples/basic/variables.tf @@ -0,0 +1,88 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-southeast-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "cltest" +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS cluster" + type = string + default = "1.34" +} + +variable "node_instance_types" { + description = "List of EC2 instance types for the node group" + type = list(string) + default = ["t3.medium"] +} + +variable "node_desired_size" { + description = "Desired number of nodes in the node group" + type = number + default = 2 +} + +variable "node_min_size" { + description = "Minimum number of nodes in the node group" + type = number + default = 1 +} + +variable "node_max_size" { + description = "Maximum number of nodes in the node group" + type = number + default = 3 +} + +variable "node_disk_size" { + description = "Disk size in GiB for worker nodes" + type = number + default = 20 +} + +variable "enable_ebs_csi_driver" { + description = "Whether to enable the EBS CSI Driver addon" + type = bool + default = false +} + +variable "enable_aws_lb_controller" { + description = "Whether to enable the AWS Load Balancer Controller" + type = bool + default = false +} + +variable "aws_auth_map_users" { + description = "List of IAM users to add to aws-auth ConfigMap" + type = list(object({ + userarn = string + username = string + groups = list(string) + })) + default = [] +} + +variable "aws_auth_map_roles" { + description = "List of IAM roles to add to aws-auth ConfigMap" + type = list(object({ + rolearn = string + username = string + groups = list(string) + })) + default = [] +} + +variable "tags" { + description = "Map of tags to apply to all resources" + type = map(string) + default = { + Environment = "example" + ManagedBy = "terraform" + } +} diff --git a/examples/ebs-web-app/.gitignore b/examples/ebs-web-app/.gitignore new file mode 100644 index 0000000..1349f91 --- /dev/null +++ b/examples/ebs-web-app/.gitignore @@ -0,0 +1,24 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +.terraform.lock.hcl diff --git a/examples/ebs-web-app/README.md b/examples/ebs-web-app/README.md new file mode 100644 index 0000000..f13283c --- /dev/null +++ b/examples/ebs-web-app/README.md @@ -0,0 +1,517 @@ +# EBS Web Application Example + +This example demonstrates how to use the EBS CSI Driver with a web application that has an EBS persistent volume attached. You'll learn how persistent storage works in Kubernetes and how data persists across pod restarts. + +## What This Example Creates + +1. **EKS Cluster** with EBS CSI Driver addon enabled +2. **PersistentVolumeClaim (PVC)** using the `gp3` EBS storage class +3. **Web Application Deployment** (nginx) with the EBS volume mounted +4. **Kubernetes Service** to expose the web application + +## Features Demonstrated + +- ✅ EBS CSI Driver integration +- ✅ Persistent volume provisioning +- ✅ Pod with persistent storage +- ✅ Web application serving content from EBS volume +- ✅ Data persistence across pod restarts + +## Understanding EBS CSI Driver + +The **EBS CSI Driver** is a Container Storage Interface (CSI) driver that allows Kubernetes to provision and manage Amazon Elastic Block Store (EBS) volumes. When you create a PersistentVolumeClaim, the EBS CSI Driver automatically: + +- Creates an EBS volume in AWS +- Attaches it to your EC2 node +- Mounts it into your pod +- Ensures data persists even if the pod is deleted + +**Key Concepts:** + +- **StorageClass**: Defines the "class" of storage (e.g., `gp3` for General Purpose SSD) +- **PersistentVolumeClaim (PVC)**: A request for storage by a user +- **PersistentVolume (PV)**: The actual EBS volume provisioned by the driver +- **Volume Binding Mode**: `WaitForFirstConsumer` means the volume is only created when a pod needs it + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- Terraform >= 1.6.0 +- kubectl installed +- AWS account with permissions to create EKS clusters and EBS volumes + +## Step-by-Step Walkthrough + +### Step 1: Configure Variables + +Create a `terraform.tfvars` file: + +```hcl +aws_region = "ap-southeast-2" +cluster_name = "ebs-web-app" +``` + +### Step 2: Initialize and Apply + +```bash +terraform init +terraform plan +terraform apply +``` + +Wait for the cluster and EBS CSI Driver to be fully provisioned (this may take 10-15 minutes). + +**What happens during apply:** + +- EKS cluster is created +- EBS CSI Driver addon is installed +- VPC and networking resources are created +- Worker nodes are provisioned + +### Step 3: Configure kubectl + +```bash +aws eks update-kubeconfig --name ebs-web-app --region ap-southeast-2 +``` + +Verify you can connect: + +```bash +kubectl get nodes +``` + +**Expected output:** + +```plaintext +NAME STATUS ROLES AGE VERSION +ip-10-0-101-xxx.ap-southeast-2.compute.internal Ready 5m v1.34.x +ip-10-0-102-xxx.ap-southeast-2.compute.internal Ready 5m v1.34.x +``` + +### Step 4: Verify EBS CSI Driver + +The EBS CSI Driver is automatically installed by Terraform. Verify it's running: + +```bash +# Check EBS CSI Driver pods +kubectl get pods -n kube-system | grep ebs-csi + +# Check StorageClass +kubectl get storageclass gp3 +``` + +**Expected output:** + +```plaintext +NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE +gp3 ebs.csi.aws.com Delete WaitForFirstConsumer true 5m +``` + +**Key observation:** + +- `VOLUMEBINDINGMODE: WaitForFirstConsumer` means the EBS volume won't be created until a pod is scheduled that needs it +- This is more efficient than creating volumes immediately + +### Step 5: Verify Kubernetes Resources + +The Terraform deployment automatically creates: + +- **PersistentVolumeClaim** - Request for 10Gi of storage +- **Deployment** - Web application with nginx +- **Service** - ClusterIP service exposing the app + +Check the resources: + +```bash +# Check PVC status +kubectl get pvc web-app-storage + +# Check deployment +kubectl get deployment web-app + +# Check service +kubectl get service web-app + +# Check pods +kubectl get pods -l app=web-app +``` + +**Expected output:** + +```plaintext +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE +web-app-storage Pending gp3 2m + +NAME READY UP-TO-DATE AVAILABLE AGE +web-app 0/1 1 0 2m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +web-app ClusterIP 172.20.xxx.xxx 80/TCP 2m +``` + +**Important note:** + +- The PVC shows `STATUS: Pending` - this is **normal** with `WaitForFirstConsumer` mode +- The volume will be created when the pod is scheduled +- The deployment may show `0/1` initially while waiting for the volume + +### Step 6: Wait for Pod to be Ready + +The pod needs to: + +1. Be scheduled on a node +2. Trigger EBS volume creation +3. Wait for volume attachment +4. Start the container + +Watch the pod status: + +```bash +kubectl get pods -l app=web-app -w +``` + +**Expected progression:** + +```plaintext +NAME READY STATUS RESTARTS AGE +web-app-xxxxx-xxxxx 0/1 Pending 0 10s +web-app-xxxxx-xxxxx 0/1 ContainerCreating 0 30s +web-app-xxxxx-xxxxx 1/1 Running 0 2m +``` + +Once the pod is `Running`, verify the PVC is now bound: + +```bash +kubectl get pvc web-app-storage +``` + +**Expected output:** + +```plaintext +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE +web-app-storage Bound pvc-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx 10Gi RWO gp3 5m +``` + +**What happened:** + +- Pod was scheduled → EBS CSI Driver created the volume → Volume attached to node → Pod started + +### Step 7: Verify EBS Volume Details + +Check the PersistentVolume that was created: + +```bash +# List PersistentVolumes +kubectl get pv + +# Describe the PVC to see volume details +kubectl describe pvc web-app-storage +``` + +**Expected output:** + +```plaintext +Name: web-app-storage +Namespace: default +StorageClass: gp3 +Status: Bound +Volume: pvc-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx +Capacity: 10Gi +Access Modes: RWO +VolumeMode: Filesystem +``` + +Verify the volume is mounted in the pod: + +```bash +# Check volume mount +kubectl describe pod -l app=web-app | grep -A 5 "Mounts:" + +# Check disk usage +kubectl exec deployment/web-app -- df -h /usr/share/nginx/html +``` + +**Expected output:** + +```plaintext +Filesystem Size Used Avail Use% Mounted on +/dev/nvme1n1 10G 24K 10G 1% /usr/share/nginx/html +``` + +### Step 8: Access the Web Application + +The web application is now running and serving content from the EBS volume. + +#### Option 1: Port Forward (Recommended for Testing) + +```bash +# Port forward the service +kubectl port-forward service/web-app 8080:80 +``` + +In another terminal or browser: + +```bash +# Test with curl +curl http://localhost:8080 + +# Or open in browser +# http://localhost:8080 +``` + +**Expected output:** + +You should see an HTML page with: + +- Title: "EBS Persistent Volume Demo" +- Message: "Success! This content is stored on an EBS volume." +- Information about the volume mount path + +#### Option 2: Direct Pod Access + +```bash +# Get pod name +POD_NAME=$(kubectl get pod -l app=web-app -o jsonpath='{.items[0].metadata.name}') + +# Port forward to pod +kubectl port-forward pod/$POD_NAME 8080:80 +``` + +### Step 9: Test Data Persistence + +This is the key demonstration - proving that data persists across pod restarts. + +#### Test 1: Create a file and verify it persists + +```bash +# Create a test file on the persistent volume +kubectl exec deployment/web-app -- sh -c "echo 'This file persists!' > /usr/share/nginx/html/persistence-test.txt" + +# Verify the file exists +kubectl exec deployment/web-app -- cat /usr/share/nginx/html/persistence-test.txt +``` + +**Expected output:** + +```text +This file persists! +``` + +#### Test 2: Delete the pod and verify data remains + +```bash +# Delete the pod (it will be recreated automatically) +kubectl delete pod -l app=web-app + +# Wait for new pod to be ready +kubectl wait --for=condition=ready pod -l app=web-app --timeout=2m + +# Verify the file still exists +kubectl exec deployment/web-app -- cat /usr/share/nginx/html/persistence-test.txt +``` + +**Expected output:** + +```text +This file persists! +``` + +**What happened:** + +- Old pod was deleted +- New pod was created +- EBS volume was reattached to the new pod +- **The file is still there!** ✅ + +#### Test 3: Modify content and verify + +```bash +# Update the HTML content +kubectl exec deployment/web-app -- sh -c "echo '

Data Persists!

' > /usr/share/nginx/html/test.html" + +# Access the new content +kubectl port-forward service/web-app 8080:80 & +curl http://localhost:8080/test.html +``` + +### Step 10: Verify EBS Volume in AWS Console + +You can also verify the EBS volume was created in AWS: + +```bash +# Get the volume ID from the PVC +VOLUME_ID=$(kubectl get pv -o jsonpath='{.items[?(@.spec.claimRef.name=="web-app-storage")].spec.csi.volumeHandle}' | cut -d'/' -f4) + +# Describe the volume +aws ec2 describe-volumes --volume-ids $VOLUME_ID --query 'Volumes[0].{ID:VolumeId,Size:Size,Type:VolumeType,State:State}' --output table +``` + +**Expected output:** + +```text +| DescribeVolumes | ++--------+------+-------+--------+ +| ID | Size | State | Type | ++--------+------+-------+--------+ +| vol-xxx| 10 | in-use| gp3 | ++--------+------+-------+--------+ +``` + +## Understanding the Components + +### PersistentVolumeClaim + +The PVC in `kubernetes.tf` requests: + +- **10Gi** of storage +- **gp3** storage class (General Purpose SSD) +- **ReadWriteOnce** access mode (single pod can mount) + +**Key configuration:** + +```hcl +wait_until_bound = false +``` + +This is important because with `WaitForFirstConsumer` mode, the PVC won't bind until a pod is scheduled. Terraform shouldn't wait indefinitely. + +### Deployment + +The deployment includes: + +- **Init container**: Populates the EBS volume with HTML content +- **Main container**: nginx serving content from the mounted volume +- **Volume mount**: `/usr/share/nginx/html` on the EBS volume + +### Service + +The ClusterIP service exposes the web application internally within the cluster. + +## Configuration + +### Required Variables + +- `cluster_name`: Name of the EKS cluster (default: `ebs-web-app`) +- `aws_region`: AWS region for resources (default: `ap-southeast-2`) + +### Optional Variables + +- `cluster_version`: Kubernetes version (default: `1.34`) +- `node_instance_types`: EC2 instance types for nodes (default: `["t3.medium"]`) +- `node_desired_size`: Desired number of nodes (default: `2`) +- `node_min_size`: Minimum number of nodes (default: `1`) +- `node_max_size`: Maximum number of nodes (default: `3`) +- `node_disk_size`: Disk size in GiB (default: `20`) + +## Storage Configuration + +The example uses: + +- **Storage Class**: `gp3` (default EBS storage class created by the module) +- **Volume Size**: 10Gi +- **Access Mode**: ReadWriteOnce (single pod can mount) +- **Volume Type**: gp3 (General Purpose SSD) +- **Binding Mode**: WaitForFirstConsumer (volume created when pod is scheduled) + +## Cleanup + +To remove all resources: + +```bash +terraform destroy +``` + +This will delete: + +- The EKS cluster +- The VPC and networking resources +- **The EBS volumes (data will be lost)** +- All Kubernetes resources + +**Important:** Make sure to backup any important data before destroying! + +## Troubleshooting + +### Pod is in Pending state + +If the pod is stuck in Pending state: + +```bash +# Check pod events +kubectl describe pod -l app=web-app + +# Check if PVC is bound +kubectl get pvc + +# Check EBS CSI Driver pods +kubectl get pods -n kube-system | grep ebs-csi +``` + +**Common issues:** + +- EBS CSI Driver not ready → Wait a few minutes +- No nodes available → Check node group status +- Volume attachment failed → Check EBS CSI Driver logs + +### PVC is in Pending state + +If the PVC is not bound: + +1. **Verify EBS CSI Driver is installed:** + + ```bash + kubectl get pods -n kube-system | grep ebs-csi + ``` + +2. **Check storage class exists:** + + ```bash + kubectl get storageclass gp3 + ``` + +3. **Check EBS CSI Driver logs:** + + ```bash + kubectl logs -n kube-system -l app=ebs-csi-controller + ``` + +4. **Verify IAM permissions:** + The EBS CSI Driver needs IAM permissions to create volumes. The module automatically creates the required IAM role. + +### Volume attachment issues + +If the volume is created but not attached: + +```bash +# Check volume attachment status +kubectl describe pv + +# Check node events +kubectl get events --sort-by=.lastTimestamp | grep -i volume + +# Check EBS CSI Driver node plugin +kubectl get pods -n kube-system | grep ebs-csi-node +``` + +## Next Steps + +- **Scale the deployment** - Note: ReadWriteOnce volumes can only be mounted by one pod +- **Use ReadWriteMany storage** - Requires EFS CSI driver for multi-pod access +- **Add a Load Balancer** - Use AWS Load Balancer Controller to expose the app externally +- **Implement backup strategies** - Use AWS Backup or snapshot the EBS volumes +- **Monitor volume usage** - Set up CloudWatch alarms for volume metrics +- **Use different storage classes** - Try `io1` for high IOPS or `st1` for throughput-optimized + +## Key Takeaways + +1. **EBS CSI Driver** automatically provisions EBS volumes when you create a PVC +2. **WaitForFirstConsumer** mode delays volume creation until a pod needs it (more efficient) +3. **Data persists** across pod restarts because it's stored on EBS, not in the pod +4. **ReadWriteOnce** means only one pod can mount the volume at a time +5. **Terraform manages** the entire lifecycle - cluster, driver, and application + +## References + +- [EBS CSI Driver Documentation](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html) +- [Kubernetes Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) +- [AWS EBS Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEBS.html) diff --git a/examples/ebs-web-app/kubernetes.tf b/examples/ebs-web-app/kubernetes.tf new file mode 100644 index 0000000..72cb593 --- /dev/null +++ b/examples/ebs-web-app/kubernetes.tf @@ -0,0 +1,171 @@ +# ============================================================================= +# Web Application with EBS Persistent Volume +# This demonstrates the EBS CSI driver working with a web application +# ============================================================================= + +# PersistentVolumeClaim using EBS storage class +# Note: With WaitForFirstConsumer binding mode, PVC stays Pending until pod is scheduled +resource "kubernetes_persistent_volume_claim_v1" "web_app_storage" { + metadata { + name = "web-app-storage" + namespace = "default" + } + + spec { + access_modes = ["ReadWriteOnce"] + storage_class_name = "gp3" # Uses the default EBS storage class created by the module + + resources { + requests = { + storage = "10Gi" + } + } + } + + # Don't wait for binding - with WaitForFirstConsumer it won't bind until pod is scheduled + wait_until_bound = false + + depends_on = [ + module.eks + ] +} + +# Deployment with web application and EBS volume +resource "kubernetes_deployment_v1" "web_app" { + metadata { + name = "web-app" + namespace = "default" + labels = { + app = "web-app" + } + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "web-app" + } + } + + template { + metadata { + labels = { + app = "web-app" + } + } + + spec { + # Init container to populate the volume with content + init_container { + name = "init-content" + image = "busybox:latest" + command = ["/bin/sh", "-c"] + args = [ + <<-EOT + cat > /usr/share/nginx/html/index.html < + + + EBS Persistent Volume Demo + + + +
+

EBS Persistent Volume Demo

+
+

Success! This content is stored on an EBS volume.

+

The volume is mounted at: /usr/share/nginx/html

+

This data will persist even if the pod is deleted and recreated.

+
+

To verify persistence, create a file in this directory and restart the pod - the file will still be there!

+
+ + + EOF + EOT + ] + + volume_mount { + name = "web-storage" + mount_path = "/usr/share/nginx/html" + } + } + + container { + name = "web-server" + image = "nginx:alpine" + + port { + container_port = 80 + name = "http" + } + + volume_mount { + name = "web-storage" + mount_path = "/usr/share/nginx/html" + } + + resources { + requests = { + cpu = "100m" + memory = "128Mi" + } + limits = { + cpu = "500m" + memory = "256Mi" + } + } + } + + volume { + name = "web-storage" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim_v1.web_app_storage.metadata[0].name + } + } + } + } + } + + depends_on = [ + kubernetes_persistent_volume_claim_v1.web_app_storage + ] +} + +# Service to expose the web application +resource "kubernetes_service_v1" "web_app" { + metadata { + name = "web-app" + namespace = "default" + labels = { + app = "web-app" + } + } + + spec { + type = "ClusterIP" + + selector = { + app = "web-app" + } + + port { + port = 80 + target_port = 80 + protocol = "TCP" + name = "http" + } + } + + depends_on = [ + kubernetes_deployment_v1.web_app + ] +} diff --git a/examples/ebs-web-app/main.tf b/examples/ebs-web-app/main.tf new file mode 100644 index 0000000..98a56e3 --- /dev/null +++ b/examples/ebs-web-app/main.tf @@ -0,0 +1,96 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token + } +} + +locals { + base_name = var.cluster_name + name = var.cluster_name + tags = var.tags +} + +module "vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = local.base_name + vpc_cidr = "10.0.0.0/16" + availability_zones = ["${var.aws_region}a", "${var.aws_region}b"] + + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"] + + # Enable Internet Gateway & NAT Gateway + create_igw = true + nat_gateway_type = "single" + + enable_eks_tags = true + eks_cluster_name = local.name + + tags = local.tags +} + +# EKS cluster with EBS CSI Driver enabled +module "eks" { + source = "../../" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + vpc_id = module.vpc.vpc_id + subnet_ids = concat(module.vpc.public_subnet_ids, module.vpc.private_subnet_ids) + node_subnet_ids = module.vpc.private_subnet_ids + + # Use EC2 compute mode + compute_mode = ["ec2"] + + # EC2 Node Group Configuration + node_instance_types = var.node_instance_types + node_desired_size = var.node_desired_size + node_min_size = var.node_min_size + node_max_size = var.node_max_size + node_disk_size = var.node_disk_size + + # Enable EBS CSI Driver for persistent volumes + enable_ebs_csi_driver = true + + # Optional: AWS Auth configuration + aws_auth_map_users = var.aws_auth_map_users + aws_auth_map_roles = var.aws_auth_map_roles + + tags = var.tags +} diff --git a/examples/ebs-web-app/outputs.tf b/examples/ebs-web-app/outputs.tf new file mode 100644 index 0000000..52c4aac --- /dev/null +++ b/examples/ebs-web-app/outputs.tf @@ -0,0 +1,59 @@ +# VPC Outputs +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +# EKS Cluster Outputs +output "cluster_name" { + description = "Name of the EKS cluster" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "Endpoint for EKS control plane" + value = module.eks.cluster_endpoint +} + +output "cluster_ca_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = module.eks.cluster_ca_data + sensitive = true +} + +# Kubernetes Resources +output "web_app_service_name" { + description = "Name of the web app Kubernetes service" + value = kubernetes_service_v1.web_app.metadata[0].name +} + +output "web_app_pvc_name" { + description = "Name of the PersistentVolumeClaim" + value = kubernetes_persistent_volume_claim_v1.web_app_storage.metadata[0].name +} + +output "web_app_deployment_name" { + description = "Name of the web app deployment" + value = kubernetes_deployment_v1.web_app.metadata[0].name +} + +# Instructions +output "access_instructions" { + description = "Instructions to access the web application" + value = <<-EOT + To access the web application: + + 1. Port forward the service: + kubectl port-forward service/web-app 8080:80 + + 2. Open http://localhost:8080 in your browser + + 3. Verify the EBS volume is attached: + kubectl get pvc + kubectl describe pvc web-app-storage + + 4. Check the pod is using the volume: + kubectl get pods + kubectl describe pod -l app=web-app + EOT +} diff --git a/examples/ebs-web-app/terraform.tfvars.example b/examples/ebs-web-app/terraform.tfvars.example new file mode 100644 index 0000000..611333a --- /dev/null +++ b/examples/ebs-web-app/terraform.tfvars.example @@ -0,0 +1,11 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and update with your values + +aws_region = "ap-southeast-2" +cluster_name = "ebs-web-app" + +# Optional: Override default node configuration +# node_instance_types = ["t3.medium"] +# node_desired_size = 2 +# node_min_size = 1 +# node_max_size = 3 diff --git a/examples/ebs-web-app/variables.tf b/examples/ebs-web-app/variables.tf new file mode 100644 index 0000000..7d4c109 --- /dev/null +++ b/examples/ebs-web-app/variables.tf @@ -0,0 +1,76 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-southeast-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "ebs-web-app" +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS cluster" + type = string + default = "1.34" +} + +variable "node_instance_types" { + description = "List of EC2 instance types for the node group" + type = list(string) + default = ["t3.medium"] +} + +variable "node_desired_size" { + description = "Desired number of nodes in the node group" + type = number + default = 2 +} + +variable "node_min_size" { + description = "Minimum number of nodes in the node group" + type = number + default = 1 +} + +variable "node_max_size" { + description = "Maximum number of nodes in the node group" + type = number + default = 3 +} + +variable "node_disk_size" { + description = "Disk size in GiB for worker nodes" + type = number + default = 20 +} + +variable "aws_auth_map_users" { + description = "List of IAM users to add to aws-auth ConfigMap" + type = list(object({ + userarn = string + username = string + groups = list(string) + })) + default = [] +} + +variable "aws_auth_map_roles" { + description = "List of IAM roles to add to aws-auth ConfigMap" + type = list(object({ + rolearn = string + username = string + groups = list(string) + })) + default = [] +} + +variable "tags" { + description = "Map of tags to apply to all resources" + type = map(string) + default = { + Environment = "example" + ManagedBy = "terraform" + } +} diff --git a/examples/eks-capabilities/.gitignore b/examples/eks-capabilities/.gitignore new file mode 100644 index 0000000..1349f91 --- /dev/null +++ b/examples/eks-capabilities/.gitignore @@ -0,0 +1,24 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +.terraform.lock.hcl diff --git a/examples/eks-capabilities/README.md b/examples/eks-capabilities/README.md new file mode 100644 index 0000000..2d39efe --- /dev/null +++ b/examples/eks-capabilities/README.md @@ -0,0 +1,355 @@ +# EKS Capabilities Example + +This example demonstrates how to use EKS Capabilities (ACK, KRO, and ArgoCD) for platform engineering. It shows how platform teams can create reusable abstractions and how development teams can deploy applications with AWS resources using simple Kubernetes manifests. + +## What This Example Creates + +1. **EKS Cluster** with all three capabilities enabled: + - **ACK** (AWS Controllers for Kubernetes) - Create AWS resources via Kubernetes manifests + - **KRO** (Kube Resource Orchestrator) - Platform engineering abstractions + - **ArgoCD** - GitOps capability for continuous deployment + +2. **KRO Resource Graph Definition (RGD)** - Platform team abstraction template +3. **KRO Resource Group Instance** - Developer-facing application deployment +4. **KRO-managed AWS resources** - DynamoDB table and IAM role/policy created by the WebAppStack +5. **ACK example resources** - DynamoDB table and S3 bucket created via ACK manifests + +## Features Demonstrated + +- ✅ EKS Capabilities enablement (ACK, KRO, ArgoCD) +- ✅ Platform engineering with KRO Resource Graph Definitions +- ✅ Creating AWS resources (DynamoDB, S3, IAM) via ACK as part of the WebAppStack +- ✅ Creating additional ACK example resources via standalone manifests +- ✅ Pod Identity for secure AWS resource access +- ✅ Developer self-service with abstracted APIs + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- Terraform >= 1.6.0 +- kubectl installed +- AWS account with permissions to create EKS clusters and capabilities + +## Usage + +### Step 1: Configure Variables + +Create a `terraform.tfvars` file: + +```hcl +aws_region = "ap-southeast-2" +cluster_name = "eks-capabilities" +``` + +### Step 2: Initialize and Apply + +```bash +terraform init +terraform plan +``` + +Because this example uses the Kubernetes provider (which needs a live cluster), +apply it in two stages: + +```bash +# 1) Create the EKS cluster first +terraform apply -target=module.eks -auto-approve + +# 2) Apply the rest (KRO/ACK resources, RGD, etc.) +terraform apply -auto-approve +``` + +Wait for the cluster and capabilities to be fully provisioned (this may take 10-15 minutes). + +**Note:** The module automatically creates IAM roles for each capability (ACK, KRO, ArgoCD) with the appropriate managed policies. If you prefer to use existing roles, you can provide them via the `*_capability_role_arn` variables. + +### Step 3: Configure kubectl + +```bash +aws eks update-kubeconfig --name --region +``` + +### Step 4: Verify Kubernetes Resources + +The Terraform deployment automatically creates: + +- **KRO RBAC Configuration** - Grants the KRO capability role cluster-admin permissions +- **KRO Resource Graph Definition (RGD)** - Platform team abstraction template +- **ACK Example Resources** - DynamoDB table and S3 bucket + +These are automatically deployed as part of `terraform apply`. Verify they exist: + +```bash +# Check KRO RBAC +kubectl get clusterrolebinding eks-capabilities-kro-cluster-admin + +# Check KRO RGD +kubectl get resourcegraphdefinition eks-capabilities-appstack.kro.run +# Check ACK example resources +kubectl get table eks-capabilities-table +kubectl get bucket eks-capabilities-bucket +``` + +### Step 5: Verify Capabilities + +Check that capabilities are active: + +```bash +# Check available APIs +kubectl api-resources | grep -E "(resourcegraphdefinition|webappstack|podidentityassociation|table|bucket|role.iam.services.k8s.aws|policy.iam.services.k8s.aws)" + +# Verify KRO API +kubectl get resourcegraphdefinition +``` + +### Step 6: Verify Resource Graph Definition + +The RGD was automatically created by Terraform. Verify it: + +```bash +kubectl get resourcegraphdefinition eks-capabilities-appstack.kro.run +kubectl describe resourcegraphdefinition eks-capabilities-appstack.kro.run +``` + +**Expected output:** + +- The RGD should show `STATE: Inactive` initially (KRO is processing it) +- Once active, the `WebAppStack` API will be available for use +- The `describe` command shows the full RGD definition with all resources + +### Step 7: Deploy Application (Development Team) + +The development team uses the abstraction to deploy their application: + +```bash +kubectl apply -f kubernetes/dev-team/eks-capabilities-app-instance.yaml +``` + +Watch the resources being created: + +```bash +kubectl get webappstack eks-capabilities-app -w +``` + +Wait for the deployment to be ready (about 1-2 minutes), then test the application: + +```bash +# Port forward for quick testing +kubectl port-forward service/eks-capabilities-app 8080:80 +``` + +Then open in your browser. + +### Step 8: Verify WebAppStack Resources + +The WebAppStack creates the AWS resources needed by the demo app. Verify they exist: + +```bash +# DynamoDB table used by the app +kubectl get table eks-capabilities-app + +# IAM role and policy used by Pod Identity +kubectl get role.iam.services.k8s.aws eks-capabilities-app-role +kubectl get policy.iam.services.k8s.aws eks-capabilities-app-policy + +# Pod Identity Association +kubectl get podidentityassociation eks-capabilities-app + +# Optional S3 bucket (only if bucket.enabled=true) +kubectl get bucket eks-capabilities-bucket + +``` + +### Step 9: Verify ACK Example Resources + +The ACK example resources are created independently of the WebAppStack: + +```bash +# DynamoDB table and S3 bucket created via ACK manifests +kubectl get table eks-capabilities-table +kubectl get bucket eks-capabilities-bucket + +``` + +## Understanding the Components + +### KRO Resource Graph Definition (RGD) + +The RGD template in `kubernetes/platform-team/eks-capabilities-appstack-rgd.yaml.tpl` defines: + +- **Schema**: Developer-facing API (WebAppStack) +- **Resources**: Multiple Kubernetes and AWS resources bundled together +- **Dependencies**: Automatic dependency resolution +- **Conditional Resources**: S3 bucket and Ingress only created when enabled + +### KRO Resource Group Instance + +The instance in `kubernetes/dev-team/eks-capabilities-app-instance.yaml` shows: + +- Simple developer interface +- Single manifest deploys multiple resources +- Automatic resource creation and dependency management + +### KRO-managed AWS Resources + +The WebAppStack uses ACK-backed resources under the hood: + +- DynamoDB table for app state +- Optional S3 bucket when enabled +- IAM role/policy for Pod Identity +- DynamoDB table for app state + +## Verifying the Deployment + +### Check Application Status + +```bash +# Check the WebAppStack instance +kubectl get webappstack eks-capabilities-app -o yaml + +# Check deployment +kubectl get deployment eks-capabilities-app + +# Check service +kubectl get service eks-capabilities-app + +# Check DynamoDB table +kubectl get table eks-capabilities-app + +# Check IAM role and policy (ACK) +kubectl get role.iam.services.k8s.aws eks-capabilities-app-role +kubectl get policy.iam.services.k8s.aws eks-capabilities-app-policy +``` + +### Check Pod Identity + +```bash +# Verify Pod Identity Association +kubectl get podidentityassociation eks-capabilities-app + +# Check ServiceAccount +kubectl get serviceaccount eks-capabilities-app -o yaml +``` + +### Access the Application + +#### Option 1: Port Forward (Quick Test) + +```bash +kubectl port-forward service/eks-capabilities-app 8080:80 +``` + +Then open in your browser. + +#### Option 2: ALB (Production) + +```bash +kubectl get ingress eks-capabilities-app-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' +``` + +## ArgoCD Capability + +The ArgoCD capability is enabled but requires additional configuration for full GitOps setup. The capability provides: + +- Managed ArgoCD installation +- GitOps workflow support +- Application synchronization from Git repositories + +For full ArgoCD setup, refer to AWS documentation or future examples. + +## Cleanup + +To clean up all resources: + +```bash +# Delete the application instance (cascades to all resources) +kubectl delete webappstack eks-capabilities-app + +# Delete ACK example resources +kubectl delete -f kubernetes/ack-resources/ + +# Destroy Terraform resources (this will also delete RBAC and RGD) +terraform destroy +``` + +## Troubleshooting + +### Capabilities Not Active + +If capabilities show as "CREATING" for a long time: + +```bash +# Check capability status +aws eks describe-capability --cluster-name --capability-name ACK +aws eks describe-capability --cluster-name --capability-name KRO +aws eks describe-capability --cluster-name --capability-name ARGOCD +``` + +### KRO Resources Not Creating + +1. Verify RBAC is configured correctly: + + ```bash + kubectl get clusterrolebinding eks-capabilities-kro-cluster-admin + ``` + +2. Check KRO capability status: + + ```bash + kubectl get resourcegraphdefinition + ``` + +3. Check for errors: + + ```bash + kubectl describe resourcegraphdefinition eks-capabilities-appstack.kro.run + kubectl get webappstack eks-capabilities-app -o yaml + ``` + +### WebAppStack AWS Resources Not Creating + +1. Verify the WebAppStack is active: + + ```bash + kubectl get webappstack eks-capabilities-app -o yaml + ``` + +2. Check the RGD and KRO reconciliation: + + ```bash + kubectl describe resourcegraphdefinition eks-capabilities-appstack.kro.run + ``` + +3. Review ACK resource status: + + ```bash + kubectl describe table eks-capabilities-app + kubectl describe role.iam.services.k8s.aws eks-capabilities-app-role + kubectl describe policy.iam.services.k8s.aws eks-capabilities-app-policy + kubectl describe podidentityassociation eks-capabilities-app + kubectl describe bucket eks-capabilities-bucket + +### ACK Example Resources Not Creating + +1. Verify ACK capability is active +2. Check IAM permissions for the ACK capability role +3. Review ACK resource status: + + ```bash + kubectl describe table eks-capabilities-table + kubectl describe bucket eks-capabilities-bucket + ``` + +## Next Steps + +- Explore creating more complex RGDs with multiple AWS services +- Set up ArgoCD for GitOps workflows +- Implement namespace-specific IAM roles using IAMRoleSelector +- Create additional platform abstractions for different application types + +## References + +- [EKS Capabilities Documentation](https://docs.aws.amazon.com/eks/latest/userguide/capabilities.html) +- [ACK Documentation](https://aws-controllers-k8s.github.io/community/) +- [KRO Documentation](https://kro.dev/) +- [ArgoCD Documentation](https://argo-cd.readthedocs.io/) diff --git a/examples/eks-capabilities/kubernetes-resources.tf b/examples/eks-capabilities/kubernetes-resources.tf new file mode 100644 index 0000000..f735703 --- /dev/null +++ b/examples/eks-capabilities/kubernetes-resources.tf @@ -0,0 +1,210 @@ +# ============================================================================= +# Data Sources for Kubernetes Resource Generation +# ============================================================================= + +data "aws_caller_identity" "current" {} + +locals { + account_id = data.aws_caller_identity.current.account_id + oidc_provider_url = replace(module.eks.oidc_provider_url, "https://", "") + enable_kro = var.enable_kro_capability + # Use provided role ARN if set, otherwise default to module-created role name + kro_role_name = var.kro_capability_role_arn != null ? split("/", var.kro_capability_role_arn)[1] : "${var.cluster_name}-kro-capability-role" +} + +# ============================================================================= +# KRO RBAC Configuration +# ============================================================================= + +# ClusterRoleBinding to grant KRO role cluster-admin permissions +# cluster-admin includes permissions for all Kubernetes and ACK resources +resource "kubernetes_manifest" "kro_rbac" { + count = local.enable_kro ? 1 : 0 + + manifest = { + apiVersion = "rbac.authorization.k8s.io/v1" + kind = "ClusterRoleBinding" + metadata = { + name = "eks-capabilities-kro-cluster-admin" + } + subjects = [ + { + kind = "User" + name = "arn:aws:sts::${local.account_id}:assumed-role/${local.kro_role_name}/KRO" + apiGroup = "rbac.authorization.k8s.io" + } + ] + roleRef = { + kind = "ClusterRole" + name = "cluster-admin" + apiGroup = "rbac.authorization.k8s.io" + } + } + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks + ] +} + +# Allow EKS capabilities controller to list PodIdentityAssociations +resource "kubernetes_manifest" "capabilities_pod_identity_rbac" { + count = local.enable_kro ? 1 : 0 + + manifest = { + apiVersion = "rbac.authorization.k8s.io/v1" + kind = "ClusterRole" + metadata = { + name = "eks-capabilities-ack-resources-reader" + } + rules = [ + { + apiGroups = ["eks.services.k8s.aws"] + resources = ["podidentityassociations"] + verbs = ["get", "list", "watch"] + }, + { + apiGroups = ["dynamodb.services.k8s.aws"] + resources = ["tables"] + verbs = ["get", "list", "watch"] + }, + { + apiGroups = ["s3.services.k8s.aws"] + resources = ["buckets"] + verbs = ["get", "list", "watch"] + }, + { + apiGroups = ["iam.services.k8s.aws"] + resources = ["roles", "policies"] + verbs = ["get", "list", "watch"] + } + ] + } + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks + ] +} + +resource "kubernetes_manifest" "capabilities_pod_identity_rbac_binding" { + count = local.enable_kro ? 1 : 0 + + manifest = { + apiVersion = "rbac.authorization.k8s.io/v1" + kind = "ClusterRoleBinding" + metadata = { + name = "eks-capabilities-ack-resources-reader" + } + subjects = [ + { + kind = "User" + name = "capabilities.eks.amazonaws.com" + apiGroup = "rbac.authorization.k8s.io" + } + ] + roleRef = { + kind = "ClusterRole" + name = "eks-capabilities-ack-resources-reader" + apiGroup = "rbac.authorization.k8s.io" + } + } + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks, + kubernetes_manifest.capabilities_pod_identity_rbac + ] +} + +# ============================================================================= +# KRO Resource Graph Definition (RGD) +# ============================================================================= + +# Generate RGD content with proper values (no file output needed) +locals { + kro_rgd_content = local.enable_kro ? replace( + replace( + replace( + file("${path.module}/kubernetes/platform-team/eks-capabilities-appstack-rgd.yaml.tpl"), + "__ACCOUNT_ID__", + local.account_id + ), + "__OIDC_PROVIDER_URL__", + local.oidc_provider_url + ), + "__CLUSTER_NAME__", + var.cluster_name + ) : null + + kro_rgd_content_with_region = local.enable_kro ? replace( + local.kro_rgd_content, + "__AWS_REGION__", + var.aws_region + ) : null +} + +# Apply the RGD to Kubernetes +resource "kubernetes_manifest" "kro_rgd" { + count = local.enable_kro ? 1 : 0 + + manifest = yamldecode(local.kro_rgd_content_with_region) + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks, + kubernetes_manifest.kro_rbac + ] +} + +# ============================================================================= +# ACK Resources - DynamoDB Table and S3 Bucket (examples) +# ============================================================================= + +# DynamoDB Table via ACK +resource "kubernetes_manifest" "ack_dynamodb_table" { + count = var.enable_ack_capability ? 1 : 0 + + manifest = yamldecode(file("${path.module}/kubernetes/ack-resources/dynamodb-table.yaml")) + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks + ] +} + +# S3 Bucket via ACK (with dynamic region) +resource "kubernetes_manifest" "ack_s3_bucket" { + count = var.enable_ack_capability ? 1 : 0 + + manifest = yamldecode( + replace( + file("${path.module}/kubernetes/ack-resources/s3-bucket.yaml"), + "locationConstraint: ap-southeast-2", + "locationConstraint: ${var.aws_region}" + ) + ) + + field_manager { + force_conflicts = true + } + + depends_on = [ + module.eks + ] +} diff --git a/examples/eks-capabilities/kubernetes/ack-resources/dynamodb-table.yaml b/examples/eks-capabilities/kubernetes/ack-resources/dynamodb-table.yaml new file mode 100644 index 0000000..27c17cc --- /dev/null +++ b/examples/eks-capabilities/kubernetes/ack-resources/dynamodb-table.yaml @@ -0,0 +1,25 @@ +# Example: DynamoDB Table created via ACK +apiVersion: dynamodb.services.k8s.aws/v1alpha1 +kind: Table +metadata: + name: eks-capabilities-table + namespace: default + annotations: + # Delete AWS resource when Kubernetes resource is deleted (default behavior) + services.k8s.aws/deletion-policy: Delete +spec: + tableName: eks-capabilities-table + attributeDefinitions: + - attributeName: id + attributeType: S + keySchema: + - attributeName: id + keyType: HASH + billingMode: PAY_PER_REQUEST + # Allow deletion of table (set to false to enable deletion) + deletionProtectionEnabled: false + tags: + - key: Environment + value: example + - key: ManagedBy + value: terraform diff --git a/examples/eks-capabilities/kubernetes/ack-resources/s3-bucket.yaml b/examples/eks-capabilities/kubernetes/ack-resources/s3-bucket.yaml new file mode 100644 index 0000000..ab7cec4 --- /dev/null +++ b/examples/eks-capabilities/kubernetes/ack-resources/s3-bucket.yaml @@ -0,0 +1,16 @@ +# Example: S3 Bucket created via ACK +apiVersion: s3.services.k8s.aws/v1alpha1 +kind: Bucket +metadata: + name: eks-capabilities-bucket + namespace: default + labels: + Environment: example + ManagedBy: terraform + annotations: + # Delete AWS resource when Kubernetes resource is deleted (default behavior) + services.k8s.aws/deletion-policy: Delete +spec: + name: eks-capabilities-bucket + createBucketConfiguration: + locationConstraint: ap-southeast-2 diff --git a/examples/eks-capabilities/kubernetes/dev-team/eks-capabilities-app-instance.yaml b/examples/eks-capabilities/kubernetes/dev-team/eks-capabilities-app-instance.yaml new file mode 100644 index 0000000..e2690f2 --- /dev/null +++ b/examples/eks-capabilities/kubernetes/dev-team/eks-capabilities-app-instance.yaml @@ -0,0 +1,20 @@ +apiVersion: kro.run/v1alpha1 +kind: WebAppStack +metadata: + name: eks-capabilities-app + namespace: default +spec: + name: eks-capabilities-app + team: platform-team + image: ghcr.io/platformfuzz/awsfanboy-feijoa-app-image:latest + replicas: 2 + containerPort: 80 + ingress: + enabled: true + annotations: + alb.ingress.kubernetes.io/load-balancer-name: eks-capabilities-app-alb + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]' + bucket: + enabled: false + name: eks-capabilities-bucket + region: ap-southeast-2 diff --git a/examples/eks-capabilities/kubernetes/platform-team/eks-capabilities-appstack-rgd.yaml.tpl b/examples/eks-capabilities/kubernetes/platform-team/eks-capabilities-appstack-rgd.yaml.tpl new file mode 100644 index 0000000..b41e89e --- /dev/null +++ b/examples/eks-capabilities/kubernetes/platform-team/eks-capabilities-appstack-rgd.yaml.tpl @@ -0,0 +1,263 @@ +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: eks-capabilities-appstack.kro.run +spec: + schema: + apiVersion: v1alpha1 + kind: WebAppStack + spec: + name: string + team: string + image: string + replicas: integer + containerPort: integer + bucket: + enabled: boolean + name: string + region: string + ingress: + enabled: boolean + annotations: object + status: + deploymentStatus: ${deployment.status.conditions} + bucketStatus: ${s3Bucket.status.ackResourceMetadata.arn} + serviceStatus: ${service.status} + + resources: + # Kubernetes Deployment + - id: deployment + template: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: ${schema.spec.name} + labels: + app: ${schema.spec.name} + team: ${schema.spec.team} + spec: + replicas: ${schema.spec.replicas} + selector: + matchLabels: + app: ${schema.spec.name} + template: + metadata: + labels: + app: ${schema.spec.name} + team: ${schema.spec.team} + spec: + serviceAccountName: ${schema.spec.name} + containers: + - name: ${schema.spec.name} + image: ${schema.spec.image} + ports: + - containerPort: ${schema.spec.containerPort} + name: http + env: + - name: AWS_REGION + value: "__AWS_REGION__" + - name: DYNAMODB_TABLE_NAME + value: ${schema.spec.name} + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "256Mi" + + # Kubernetes Service + - id: service + template: + apiVersion: v1 + kind: Service + metadata: + name: ${schema.spec.name} + labels: + app: ${schema.spec.name} + team: ${schema.spec.team} + spec: + type: ClusterIP + selector: + app: ${schema.spec.name} + ports: + - port: 80 + targetPort: ${schema.spec.containerPort} + protocol: TCP + name: http + + # ServiceAccount for Pod Identity + - id: serviceaccount + template: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: ${schema.spec.name} + labels: + app: ${schema.spec.name} + team: ${schema.spec.team} + + # Pod Identity Association (ACK) + - id: podidentity + template: + apiVersion: eks.services.k8s.aws/v1alpha1 + kind: PodIdentityAssociation + metadata: + name: ${schema.spec.name} + namespace: ${schema.metadata.namespace} + annotations: + services.k8s.aws/skip-resource-tags: "true" + spec: + clusterName: "__CLUSTER_NAME__" + namespace: ${schema.metadata.namespace} + serviceAccount: ${schema.spec.name} + roleARN: ${iamRole.status.ackResourceMetadata.arn} + + # IAM Role (ACK) + - id: iamRole + template: + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Role + metadata: + name: ${schema.spec.name}-role + namespace: ${schema.metadata.namespace} + spec: + name: ${schema.spec.name}-role + assumeRolePolicyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "pods.eks.amazonaws.com" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + } + ] + } + policies: + - ${iamPolicy.status.ackResourceMetadata.arn} + tags: + - key: App + value: ${schema.spec.name} + - key: Team + value: ${schema.spec.team} + + # IAM Policy for DynamoDB (ACK) + - id: iamPolicy + template: + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Policy + metadata: + name: ${schema.spec.name}-policy + namespace: ${schema.metadata.namespace} + spec: + name: ${schema.spec.name}-policy + policyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan" + ], + "Resource": "${dynamodbTable.status.ackResourceMetadata.arn}" + } + ] + } + tags: + - key: App + value: ${schema.spec.name} + + # DynamoDB Table (ACK) + - id: dynamodbTable + template: + apiVersion: dynamodb.services.k8s.aws/v1alpha1 + kind: Table + metadata: + name: ${schema.spec.name} + namespace: ${schema.metadata.namespace} + labels: + app: ${schema.spec.name} + team: ${schema.spec.team} + spec: + tableName: ${schema.spec.name} + attributeDefinitions: + - attributeName: id + attributeType: S + keySchema: + - attributeName: id + keyType: HASH + billingMode: PAY_PER_REQUEST + tags: + - key: App + value: ${schema.spec.name} + - key: Team + value: ${schema.spec.team} + + # S3 Bucket (ACK) - Conditional + - id: s3Bucket + includeWhen: + - ${schema.spec.bucket.enabled} + template: + apiVersion: s3.services.k8s.aws/v1alpha1 + kind: Bucket + metadata: + name: ${schema.spec.bucket.name} + namespace: ${schema.metadata.namespace} + labels: + team: ${schema.spec.team} + spec: + name: ${schema.spec.bucket.name} + createBucketConfiguration: + locationConstraint: ${schema.spec.bucket.region} + + # Ingress (Conditional) + - id: ingress + includeWhen: + - ${schema.spec.ingress.enabled} + template: + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ${schema.spec.name}-ingress + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + spec: + ingressClassName: alb + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ${schema.spec.name} + port: + number: 80 + + # Pod Disruption Budget + - id: pdb + template: + apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: ${schema.spec.name} + labels: + app: ${schema.spec.name} + spec: + minAvailable: 1 + selector: + matchLabels: + app: ${schema.spec.name} diff --git a/examples/eks-capabilities/main.tf b/examples/eks-capabilities/main.tf new file mode 100644 index 0000000..6cb9241 --- /dev/null +++ b/examples/eks-capabilities/main.tf @@ -0,0 +1,138 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.4" + } + } +} + +provider "aws" { + region = var.aws_region +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = module.eks.cluster_ca_certificate + token = module.eks.cluster_auth_token + } +} + +locals { + base_name = var.cluster_name + name = var.cluster_name + tags = var.tags +} + +module "vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = local.base_name + vpc_cidr = "10.0.0.0/16" + availability_zones = ["${var.aws_region}a", "${var.aws_region}b"] + + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"] + + # Enable Internet Gateway & NAT Gateway + create_igw = true + nat_gateway_type = "single" + + enable_eks_tags = true + eks_cluster_name = local.name + + tags = local.tags +} + +# IAM policy to allow ACK to manage Pod Identity Associations +resource "aws_iam_policy" "ack_eks_pod_identity" { + name = "${var.cluster_name}-ack-eks-pod-identity" + description = "Permissions for ACK to manage EKS Pod Identity Associations" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "eks:CreatePodIdentityAssociation", + "eks:DeletePodIdentityAssociation", + "eks:DescribePodIdentityAssociation", + "eks:ListPodIdentityAssociations", + "eks:TagResource", + "eks:UntagResource", + "eks:ListTagsForResource", + "eks:DescribeCluster", + "eks:ListClusters" + ] + Resource = "*" + } + ] + }) +} + +# EKS cluster with all three capabilities enabled +module "eks" { + source = "../../" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + vpc_id = module.vpc.vpc_id + subnet_ids = concat(module.vpc.public_subnet_ids, module.vpc.private_subnet_ids) + node_subnet_ids = module.vpc.private_subnet_ids + + # Use EC2 compute mode + compute_mode = ["ec2"] + + # EC2 Node Group Configuration + node_instance_types = var.node_instance_types + node_desired_size = var.node_desired_size + node_min_size = var.node_min_size + node_max_size = var.node_max_size + node_disk_size = var.node_disk_size + + # Enable EKS Capabilities + # Note: ArgoCD requires AWS Identity Center setup, so it's disabled by default + enable_ack_capability = var.enable_ack_capability + ack_capability_iam_policy_arns = { + s3 = "arn:aws:iam::aws:policy/AmazonS3FullAccess" + dynamodb = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess" + iam = "arn:aws:iam::aws:policy/IAMFullAccess" + pod_identity = aws_iam_policy.ack_eks_pod_identity.arn + } + enable_kro_capability = var.enable_kro_capability + kro_capability_role_arn = var.kro_capability_role_arn + enable_argocd_capability = var.enable_argocd_capability # Requires AWS Identity Center configuration + + # Optional: Enable EBS CSI Driver for persistent volumes + enable_ebs_csi_driver = var.enable_ebs_csi_driver + + # Enable Pod Identity Agent for AWS SDK credentials in pods + enable_pod_identity_agent = var.enable_pod_identity_agent + + tags = var.tags +} diff --git a/examples/eks-capabilities/outputs.tf b/examples/eks-capabilities/outputs.tf new file mode 100644 index 0000000..a77354e --- /dev/null +++ b/examples/eks-capabilities/outputs.tf @@ -0,0 +1,75 @@ +# VPC Outputs +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +# EKS Cluster Outputs +output "cluster_name" { + description = "Name of the EKS cluster" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "Endpoint for EKS control plane" + value = module.eks.cluster_endpoint +} + +output "cluster_ca_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = module.eks.cluster_ca_data + sensitive = true +} + +# EKS Capabilities Outputs +output "ack_capability_arn" { + description = "ARN of the ACK capability" + value = module.eks.ack_capability_arn +} + +output "kro_capability_arn" { + description = "ARN of the KRO capability" + value = module.eks.kro_capability_arn +} + +output "argocd_capability_arn" { + description = "ARN of the ArgoCD capability" + value = module.eks.argocd_capability_arn +} + +# Additional Outputs for Kubernetes Resources +output "aws_account_id" { + description = "AWS Account ID" + value = data.aws_caller_identity.current.account_id +} + +output "oidc_provider_url" { + description = "OIDC Provider URL (without https://)" + value = replace(module.eks.oidc_provider_url, "https://", "") +} + +# Instructions +output "next_steps" { + description = "Next steps to use the capabilities" + value = <<-EOT + Capabilities are now enabled! Kubernetes resources have been automatically deployed. + + 1. Configure kubectl: + aws eks update-kubeconfig --name ${module.eks.cluster_name} --region ${var.aws_region} + + 2. Verify RBAC for KRO (automatically deployed): + kubectl get clusterrolebinding eks-capabilities-kro-cluster-admin + + 3. Verify Resource Graph Definition (automatically deployed): + kubectl get resourcegraphdefinition eks-capabilities-appstack.kro.run + + 4. Deploy application using KRO (dev team): + kubectl apply -f kubernetes/dev-team/eks-capabilities-app-instance.yaml + + 5. Verify ACK resources (automatically deployed): + kubectl get table,bucket,role + + 6. Verify capabilities: + kubectl api-resources | grep -E "(resourcegraphdefinition|table|bucket|role)" + EOT +} diff --git a/examples/eks-capabilities/terraform.tfvars.example b/examples/eks-capabilities/terraform.tfvars.example new file mode 100644 index 0000000..dd93d94 --- /dev/null +++ b/examples/eks-capabilities/terraform.tfvars.example @@ -0,0 +1,12 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and update with your values + +aws_region = "ap-southeast-2" +cluster_name = "eks-capabilities" + +# Optional: Override default node configuration +# node_instance_types = ["t3.medium"] +# node_desired_size = 2 +# node_min_size = 1 +# node_max_size = 3 +# enable_ebs_csi_driver = true diff --git a/examples/eks-capabilities/variables.tf b/examples/eks-capabilities/variables.tf new file mode 100644 index 0000000..901072c --- /dev/null +++ b/examples/eks-capabilities/variables.tf @@ -0,0 +1,92 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-southeast-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "eks-capabilities" +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS cluster" + type = string + default = "1.34" +} + +variable "node_instance_types" { + description = "List of EC2 instance types for the node group" + type = list(string) + default = ["t3.medium"] +} + +variable "node_desired_size" { + description = "Desired number of nodes in the node group" + type = number + default = 2 +} + +variable "node_min_size" { + description = "Minimum number of nodes in the node group" + type = number + default = 1 +} + +variable "node_max_size" { + description = "Maximum number of nodes in the node group" + type = number + default = 3 +} + +variable "node_disk_size" { + description = "Disk size in GiB for worker nodes" + type = number + default = 20 +} + +variable "enable_ebs_csi_driver" { + description = "Whether to enable the EBS CSI Driver addon" + type = bool + default = true +} + +variable "enable_pod_identity_agent" { + description = "Whether to enable the EKS Pod Identity Agent addon" + type = bool + default = true +} + +variable "enable_ack_capability" { + description = "Whether to enable ACK capability" + type = bool + default = true +} + +variable "enable_kro_capability" { + description = "Whether to enable KRO capability" + type = bool + default = true +} + +variable "enable_argocd_capability" { + description = "Whether to enable the ArgoCD capability" + type = bool + default = false +} + +variable "kro_capability_role_arn" { + description = "Optional IAM role ARN for the KRO capability. If not set, the module creates one." + type = string + default = null +} + +variable "tags" { + description = "Map of tags to apply to all resources" + type = map(string) + default = { + Environment = "example" + ManagedBy = "terraform" + } +} diff --git a/fargate.tf b/fargate.tf new file mode 100644 index 0000000..4ce20dd --- /dev/null +++ b/fargate.tf @@ -0,0 +1,60 @@ +# ============================================================================= +# Fargate Profile IAM Role +# ============================================================================= + +data "aws_iam_policy_document" "eks_fargate_assume_role" { + count = contains(var.compute_mode, "fargate") ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["eks-fargate-pods.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "eks_fargate" { + count = contains(var.compute_mode, "fargate") ? 1 : 0 + + name = "${var.cluster_name}-eks-fargate-role" + assume_role_policy = data.aws_iam_policy_document.eks_fargate_assume_role[0].json + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "eks_fargate_pod_execution_role" { + count = contains(var.compute_mode, "fargate") ? 1 : 0 + + role = aws_iam_role.eks_fargate[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" +} + +# ============================================================================= +# Fargate Profiles +# ============================================================================= + +resource "aws_eks_fargate_profile" "default" { + for_each = contains(var.compute_mode, "fargate") ? var.fargate_profiles : {} + + cluster_name = aws_eks_cluster.this.name + fargate_profile_name = each.key + pod_execution_role_arn = aws_iam_role.eks_fargate[0].arn + subnet_ids = each.value.subnet_ids != null ? each.value.subnet_ids : var.subnet_ids + + dynamic "selector" { + for_each = each.value.selectors != null ? each.value.selectors : [] + content { + namespace = selector.value.namespace + labels = selector.value.labels + } + } + + tags = merge(var.tags, each.value.tags != null ? each.value.tags : {}) + + depends_on = [ + aws_iam_role_policy_attachment.eks_fargate_pod_execution_role[0], + ] +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..f0aea86 --- /dev/null +++ b/main.tf @@ -0,0 +1,97 @@ +# Get current AWS region +data "aws_region" "current" {} + +# ============================================================================= +# EKS Cluster IAM Role +# ============================================================================= + +data "aws_iam_policy_document" "eks_cluster_assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["eks.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "eks_cluster" { + name = "${var.cluster_name}-eks-cluster-role" + assume_role_policy = data.aws_iam_policy_document.eks_cluster_assume_role.json + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "eks_cluster_policy" { + role = aws_iam_role.eks_cluster.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" +} + +# ============================================================================= +# OIDC Provider for IRSA +# ============================================================================= + +data "tls_certificate" "eks" { + url = aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +resource "aws_iam_openid_connect_provider" "eks" { + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint] + url = aws_eks_cluster.this.identity[0].oidc[0].issuer + + tags = var.tags + + # Explicit dependencies to ensure proper creation order + depends_on = [ + aws_eks_cluster.this, + data.tls_certificate.eks + ] +} + +# ============================================================================= +# EKS Cluster +# ============================================================================= + +resource "aws_eks_cluster" "this" { + name = var.cluster_name + version = var.cluster_version + role_arn = aws_iam_role.eks_cluster.arn + + vpc_config { + subnet_ids = var.subnet_ids + endpoint_public_access = var.endpoint_public_access + endpoint_private_access = true + public_access_cidrs = var.public_access_cidrs + } + + enabled_cluster_log_types = var.enabled_cluster_log_types + + # Access configuration - required for EKS Capabilities + # Capabilities require API or API_AND_CONFIG_MAP authentication mode + # Use dynamic block to only set when capabilities are enabled + dynamic "access_config" { + for_each = (var.enable_ack_capability || var.enable_kro_capability || var.enable_argocd_capability) || var.cluster_authentication_mode != "CONFIG_MAP" ? [1] : [] + content { + authentication_mode = var.cluster_authentication_mode != "CONFIG_MAP" ? var.cluster_authentication_mode : "API_AND_CONFIG_MAP" + } + } + + # AutoMode configuration is handled via the compute block + # Note: AutoMode support may require specific AWS provider versions + # This structure is ready for future AutoMode expansion + + tags = var.tags + + lifecycle { + # Ignore changes to access_config if it was manually set or already exists + # This prevents replacement when capabilities are enabled on existing clusters + ignore_changes = [access_config] + } + + depends_on = [ + aws_iam_role_policy_attachment.eks_cluster_policy, + ] +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..1061855 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,118 @@ +output "cluster_name" { + description = "Name of the EKS cluster" + value = aws_eks_cluster.this.name +} + +output "cluster_endpoint" { + description = "Endpoint for EKS control plane" + value = aws_eks_cluster.this.endpoint +} + +output "cluster_ca_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = aws_eks_cluster.this.certificate_authority[0].data +} + +output "cluster_ca_certificate" { + description = "Decoded certificate data required to communicate with the cluster" + value = base64decode(aws_eks_cluster.this.certificate_authority[0].data) +} + +output "cluster_auth_token" { + description = "Token to authenticate with the EKS cluster" + value = data.aws_eks_cluster_auth.this.token + sensitive = true +} + +output "cluster_version" { + description = "Kubernetes version of the EKS cluster" + value = aws_eks_cluster.this.version +} + +output "cluster_arn" { + description = "ARN of the EKS cluster" + value = aws_eks_cluster.this.arn +} + +output "oidc_provider_arn" { + description = "ARN of the EKS OIDC provider" + value = aws_iam_openid_connect_provider.eks.arn +} + +output "oidc_provider_url" { + description = "URL of the EKS OIDC provider" + value = aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +# ============================================================================= +# EC2 Outputs +# ============================================================================= + +output "node_group_id" { + description = "ID of the EKS node group (when EC2 mode is enabled)" + value = contains(var.compute_mode, "ec2") ? aws_eks_node_group.default[0].id : null +} + +output "node_group_arn" { + description = "ARN of the EKS node group (when EC2 mode is enabled)" + value = contains(var.compute_mode, "ec2") ? aws_eks_node_group.default[0].arn : null +} + +output "node_group_status" { + description = "Status of the EKS node group (when EC2 mode is enabled)" + value = contains(var.compute_mode, "ec2") ? aws_eks_node_group.default[0].status : null +} + +output "node_role_arn" { + description = "IAM role ARN for EC2 nodes (when EC2 mode is enabled)" + value = contains(var.compute_mode, "ec2") ? aws_iam_role.eks_nodes[0].arn : null +} + +# ============================================================================= +# Fargate Outputs +# ============================================================================= + +output "fargate_profile_arns" { + description = "Map of Fargate profile ARNs (when Fargate mode is enabled)" + value = contains(var.compute_mode, "fargate") ? { + for k, v in aws_eks_fargate_profile.default : k => v.arn + } : {} +} + +output "fargate_role_arn" { + description = "IAM role ARN for Fargate pods (when Fargate mode is enabled)" + value = contains(var.compute_mode, "fargate") ? aws_iam_role.eks_fargate[0].arn : null +} + +# ============================================================================= +# Addon Outputs +# ============================================================================= + +output "ebs_csi_driver_role_arn" { + description = "IAM role ARN for EBS CSI Driver (when enabled)" + value = var.enable_ebs_csi_driver ? aws_iam_role.ebs_csi_driver[0].arn : null +} + +output "aws_lb_controller_role_arn" { + description = "IAM role ARN for AWS Load Balancer Controller (when enabled)" + value = var.enable_aws_lb_controller ? aws_iam_role.aws_lb_controller[0].arn : null +} + +# ============================================================================= +# EKS Capabilities Outputs +# ============================================================================= + +output "ack_capability_arn" { + description = "ARN of the ACK capability (when enabled)" + value = var.enable_ack_capability ? aws_eks_capability.ack[0].arn : null +} + +output "kro_capability_arn" { + description = "ARN of the KRO capability (when enabled)" + value = var.enable_kro_capability ? aws_eks_capability.kro[0].arn : null +} + +output "argocd_capability_arn" { + description = "ARN of the ArgoCD capability (when enabled)" + value = var.enable_argocd_capability ? aws_eks_capability.argocd[0].arn : null +} diff --git a/tests/eks_test.tftest.hcl b/tests/eks_test.tftest.hcl new file mode 100644 index 0000000..1b1b725 --- /dev/null +++ b/tests/eks_test.tftest.hcl @@ -0,0 +1,179 @@ +run "eks_cluster_creation" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["ec2"] + } + + assert { + condition = aws_eks_cluster.this.name == "test-eks-cluster" + error_message = "EKS cluster name should be 'test-eks-cluster'" + } + + assert { + condition = aws_eks_cluster.this.version == "1.28" + error_message = "EKS cluster version should be '1.28'" + } + + assert { + condition = length(aws_eks_cluster.this.vpc_config[0].subnet_ids) == 2 + error_message = "EKS cluster should have 2 subnet IDs" + } +} + +run "eks_node_group_config" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["ec2"] + node_instance_types = ["t3.medium", "t3.large"] + node_desired_size = 3 + node_min_size = 2 + node_max_size = 5 + node_disk_size = 50 + } + + assert { + condition = length(aws_eks_node_group.default) == 1 + error_message = "Node group should be created when EC2 mode is enabled" + } + + assert { + condition = aws_eks_node_group.default[0].scaling_config[0].desired_size == 3 + error_message = "Node group desired size should be 3" + } + + assert { + condition = aws_eks_node_group.default[0].scaling_config[0].min_size == 2 + error_message = "Node group min size should be 2" + } + + assert { + condition = aws_eks_node_group.default[0].scaling_config[0].max_size == 5 + error_message = "Node group max size should be 5" + } + + assert { + condition = aws_eks_node_group.default[0].disk_size == 50 + error_message = "Node group disk size should be 50" + } +} + +run "eks_iam_roles" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["ec2"] + } + + assert { + condition = aws_iam_role.eks_cluster.name == "test-eks-cluster-eks-cluster-role" + error_message = "Cluster IAM role name should be correct" + } + + assert { + condition = length(aws_iam_role.eks_nodes) == 1 + error_message = "Node IAM role should be created when EC2 mode is enabled" + } + + assert { + condition = aws_iam_role.eks_nodes[0].name == "test-eks-cluster-eks-nodes-role" + error_message = "Node IAM role name should be correct" + } +} + +run "eks_oidc_provider" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["ec2"] + } + + assert { + condition = contains(aws_iam_openid_connect_provider.eks.client_id_list, "sts.amazonaws.com") + error_message = "OIDC provider client ID should include 'sts.amazonaws.com'" + } +} + +run "eks_addons_optional" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["ec2"] + enable_ebs_csi_driver = true + enable_aws_lb_controller = true + } + + assert { + condition = length(aws_iam_role.ebs_csi_driver) == 1 + error_message = "EBS CSI Driver IAM role should be created when enabled" + } + + assert { + condition = length(aws_eks_addon.ebs_csi_driver) == 1 + error_message = "EBS CSI Driver addon should be created when enabled" + } + + assert { + condition = length(aws_iam_role.aws_lb_controller) == 1 + error_message = "AWS Load Balancer Controller IAM role should be created when enabled" + } + + assert { + condition = length(kubernetes_service_account.aws_lb_controller) == 1 + error_message = "AWS Load Balancer Controller service account should be created when enabled" + } +} + +run "eks_fargate_mode" { + command = plan + + variables { + cluster_name = "test-eks-cluster" + cluster_version = "1.28" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] + compute_mode = ["fargate"] + fargate_profiles = { + default = { + subnet_ids = ["subnet-12345678"] + selectors = [ + { + namespace = "default" + labels = {} + } + ] + } + } + } + + assert { + condition = length(aws_iam_role.eks_fargate) == 1 + error_message = "Fargate IAM role should be created when Fargate mode is enabled" + } + + assert { + condition = length(aws_eks_fargate_profile.default) == 1 + error_message = "Fargate profile should be created when Fargate mode is enabled" + } +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..4aef633 --- /dev/null +++ b/variables.tf @@ -0,0 +1,287 @@ +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string +} + +variable "cluster_version" { + description = "Kubernetes version to use for the EKS cluster" + type = string +} + +variable "compute_mode" { + description = "List of compute modes to enable. Valid values: ec2, fargate, automode" + type = list(string) + default = ["ec2"] + + validation { + condition = alltrue([ + for mode in var.compute_mode : contains(["ec2", "fargate", "automode"], mode) + ]) + error_message = "compute_mode must contain only 'ec2', 'fargate', or 'automode'" + } +} + +variable "vpc_id" { + description = "VPC ID where the cluster is deployed" + type = string +} + +variable "subnet_ids" { + description = "Subnet IDs for EKS cluster control plane (should include both public and private)" + type = list(string) +} + +variable "node_subnet_ids" { + description = "Subnet IDs for EKS node groups (should be private subnets only for security). If null, uses subnet_ids." + type = list(string) + default = null +} + +variable "endpoint_public_access" { + description = "Whether the Amazon EKS public API server endpoint is enabled" + type = bool + default = true +} + +variable "public_access_cidrs" { + description = "List of CIDR blocks that can access the Amazon EKS public API server endpoint" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "enabled_cluster_log_types" { + description = "List of control plane logging types to enable" + type = list(string) + default = ["api", "audit", "authenticator", "controllerManager", "scheduler"] +} + +variable "cluster_authentication_mode" { + description = "Authentication mode for the EKS cluster. Valid values: CONFIG_MAP, API, API_AND_CONFIG_MAP. Defaults to API_AND_CONFIG_MAP when capabilities are enabled, otherwise CONFIG_MAP." + type = string + default = "CONFIG_MAP" + + validation { + condition = contains(["CONFIG_MAP", "API", "API_AND_CONFIG_MAP"], var.cluster_authentication_mode) + error_message = "cluster_authentication_mode must be one of: CONFIG_MAP, API, API_AND_CONFIG_MAP" + } +} + +# ============================================================================= +# EC2 Node Group Variables +# ============================================================================= + +variable "node_instance_types" { + description = "List of EC2 instance types for the node group" + type = list(string) + default = ["t3.medium"] +} + +variable "node_desired_size" { + description = "Desired number of nodes in the node group" + type = number + default = 2 +} + +variable "node_min_size" { + description = "Minimum number of nodes in the node group" + type = number + default = 1 +} + +variable "node_max_size" { + description = "Maximum number of nodes in the node group" + type = number + default = 3 +} + +variable "node_disk_size" { + description = "Disk size in GiB for worker nodes" + type = number + default = 20 +} + +variable "node_update_max_unavailable" { + description = "Maximum number of nodes unavailable during update" + type = number + default = 1 +} + +variable "node_remote_access_enabled" { + description = "Whether to enable remote access to nodes" + type = bool + default = false +} + +variable "node_remote_access_ssh_key" { + description = "EC2 SSH key name for remote access" + type = string + default = null +} + +variable "node_remote_access_security_groups" { + description = "List of security group IDs for remote access" + type = list(string) + default = [] +} + +variable "node_labels" { + description = "Key-value map of Kubernetes labels to apply to nodes" + type = map(string) + default = {} +} + +# ============================================================================= +# Fargate Variables +# ============================================================================= + +variable "fargate_profiles" { + description = "Map of Fargate profiles to create. Key is the profile name." + type = map(object({ + subnet_ids = optional(list(string)) + selectors = optional(list(object({ + namespace = string + labels = optional(map(string)) + })), []) + tags = optional(map(string)) + })) + default = {} +} + +# ============================================================================= +# AutoMode Variables +# ============================================================================= + +# AutoMode variables will be added here as the feature becomes available +# Currently, AutoMode is configured via the compute block in the cluster resource + +# ============================================================================= +# AWS Auth Variables +# ============================================================================= + +variable "aws_auth_map_users" { + description = "List of IAM users to add to aws-auth ConfigMap for Kubernetes access" + type = list(object({ + userarn = string + username = string + groups = list(string) + })) + default = [] +} + +variable "aws_auth_map_roles" { + description = "List of IAM roles to add to aws-auth ConfigMap for Kubernetes access" + type = list(object({ + rolearn = string + username = string + groups = list(string) + })) + default = [] +} + +# ============================================================================= +# Addon Variables +# ============================================================================= + +variable "enable_ebs_csi_driver" { + description = "Whether to install AWS EBS CSI Driver" + type = bool + default = false +} + +variable "enable_pod_identity_agent" { + description = "Whether to install EKS Pod Identity Agent add-on" + type = bool + default = false +} + +variable "pod_identity_agent_version" { + description = "Version of the EKS Pod Identity Agent add-on. If null, uses latest version." + type = string + default = null +} + +variable "ebs_csi_driver_version" { + description = "Version of the AWS EBS CSI Driver add-on. If null, uses latest version." + type = string + default = null +} + +variable "enable_aws_lb_controller" { + description = "Whether to install AWS Load Balancer Controller" + type = bool + default = false +} + +variable "aws_lb_controller_helm_version" { + description = "Version of the AWS Load Balancer Controller Helm chart" + type = string + default = "1.7.2" +} + +variable "aws_lb_controller_helm_values" { + description = "Additional Helm values for the AWS Load Balancer Controller" + type = map(string) + default = {} +} + +# ============================================================================= +# EKS Capabilities Variables +# ============================================================================= + +variable "enable_ack_capability" { + description = "Whether to enable AWS Controllers for Kubernetes (ACK) capability" + type = bool + default = false +} + +variable "enable_kro_capability" { + description = "Whether to enable Kube Resource Orchestrator (KRO) capability" + type = bool + default = false +} + +variable "enable_argocd_capability" { + description = "Whether to enable ArgoCD GitOps capability" + type = bool + default = false +} + +variable "ack_capability_role_arn" { + description = "IAM role ARN for ACK capability to create AWS resources. If not provided, AWS will create a default role." + type = string + default = null +} + +variable "ack_capability_iam_policy_arns" { + description = "Map of IAM policy ARNs to attach to the ACK capability role. Required for ACK to manage AWS resources (e.g., S3, DynamoDB, IAM)." + type = map(string) + default = {} +} + +variable "kro_capability_role_arn" { + description = "IAM role ARN for KRO capability. If not provided, AWS will create a default role." + type = string + default = null +} + +variable "argocd_capability_role_arn" { + description = "IAM role ARN for ArgoCD capability. If not provided, AWS will create a default role." + type = string + default = null +} + +variable "argocd_capability_configuration" { + description = "Configuration JSON for ArgoCD capability. If not provided, a minimal configuration will be used. Full ArgoCD setup requires AWS Identity Center configuration." + type = string + default = null +} + +# ============================================================================= +# Common Variables +# ============================================================================= + +variable "tags" { + description = "Map of tags to apply to all resources" + type = map(string) + default = {} +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..d2c8335 --- /dev/null +++ b/versions.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +}