diff --git a/.github/gitcliff.toml b/.github/gitcliff.toml new file mode 100644 index 0000000..902a6c4 --- /dev/null +++ b/.github/gitcliff.toml @@ -0,0 +1,25 @@ +# git-cliff configuration for release changelog +# Docs: https://github.com/orhun/git-cliff + +[changelog] +header = "" +body = "{{#each groups}}{{#if commits}}\n### {{title}}\n{{#each commits}}- {{this.message | trim}} ({{this.sha}})\n{{/each}}{{/if}}{{/each}}\n" +footer = "" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true + +commit_parsers = [ + { message = "feat\(.*\):", group = "Features" }, + { message = "fix\(.*\):", group = "Bug Fixes" }, + { message = "perf\(.*\):", group = "Performance" }, + { message = "refactor\(.*\):", group = "Refactors" }, + { message = "docs\(.*\):", group = "Documentation" }, + { message = "chore\(.*\):", group = "Chores" }, + { message = "test\(.*\):", group = "Tests" }, + { message = "ci\(.*\):", group = "CI" } +] + +protect_breaking_changes = true diff --git a/.github/workflows/terraform-module.yml b/.github/workflows/terraform-module.yml new file mode 100644 index 0000000..cda0f38 --- /dev/null +++ b/.github/workflows/terraform-module.yml @@ -0,0 +1,85 @@ +name: quix-aks module CI/CD + +on: + push: + branches: [ main, dev ] + paths: + - 'modules/quix-aks/**' + - '.github/workflows/terraform-module.yml' + workflow_dispatch: + inputs: + bump: + description: 'Version bump (patch, minor, major)' + required: true + default: 'minor' + type: choice + options: [patch, minor, major] + +permissions: + contents: write + +jobs: + validate: + name: Validate Terraform + runs-on: ubuntu-latest + defaults: + run: + working-directory: modules/quix-aks + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.6.6 + + - name: Terraform fmt + run: terraform fmt -check -recursive + + - name: Terraform init (no backend) + run: terraform init -backend=false + + - name: Terraform validate + run: terraform validate + + - name: Generate terraform-docs (inject & commit) + uses: terraform-docs/gh-actions@v1.3.0 + with: + working-dir: modules/quix-aks + output-file: README.md + output-method: inject + config-file: '' + git-push: true + + release: + name: Tag release + runs-on: ubuntu-latest + needs: validate + # Only release on manual dispatch with bump==minor + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.bump == 'minor' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create tag (SemVer bump) + id: tag + uses: anothrNick/github-tag-action@1.67.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEFAULT_BUMP: ${{ github.event.inputs.bump }} + TAG_PREFIX: 'v' + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + if: steps.tag.outputs.new_tag != '' + with: + tag_name: ${{ steps.tag.outputs.new_tag }} + name: ${{ steps.tag.outputs.new_tag }} + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index fc41f6e..d6c5065 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Terraform Modules (Azure) -Repository of production-ready Terraform modules. The primary module is `azure/modules/quix-aks` (full AKS with networking, NAT, RBAC, and optional Bastion/jumpbox). +Repository of production-ready Terraform modules for installing quix-platform. ## Structure @@ -58,3 +58,53 @@ Access a private AKS: see `BASTION_ACCESS.md`. ## Module versioning Publish SemVer tags and reference the module with `?ref=vX.Y.Z` when consuming from git. + +### Using this module from another repo with a Git tag + +HTTPS example: + +```hcl +module "quix_aks" { + source = "git::https://github.com/quixio/terraform-quixplatform-azure.git//modules/quix-aks?ref=0.0.2" + + name = "my-aks" + location = "westeurope" + resource_group_name = "rg-my-aks" + create_resource_group = true + + vnet_name = "vnet-my-aks" + vnet_address_space = ["10.240.0.0/16"] + nodes_subnet_name = "Subnet-Nodes" + nodes_subnet_cidr = "10.240.0.0/22" + + nat_identity_name = "my-nat-id" + public_ip_name = "my-nat-ip" + nat_gateway_name = "my-nat" + availability_zone = "1" + + kubernetes_version = "1.32.4" + network_profile = { + network_plugin_mode = "vnet" + service_cidr = "172.22.0.0/16" + dns_service_ip = "172.22.0.10" + } + + node_pools = { + default = { + name = "default" + type = "system" + node_count = 1 + vm_size = "Standard_D4ds_v5" + } + } +} +``` + +SSH example: + +```hcl +module "quix_aks" { + source = "git::ssh://git@github.com/quixio/terraform-quixplatform-azure.git//modules/quix-aks?ref=0.0.2" + # ...same inputs as above +} +``` diff --git a/examples/private-quix-infr/main.tf b/examples/private-quix-infr/main.tf index f309eea..ce15f0d 100644 --- a/examples/private-quix-infr/main.tf +++ b/examples/private-quix-infr/main.tf @@ -20,12 +20,12 @@ resource "azurerm_resource_group" "this" { module "aks" { source = "../../modules/quix-aks" - name = "quix-aks-private" - location = "westeurope" - resource_group_name = "rg-quix-private" - create_resource_group = false - kubernetes_version = "1.32.4" - sku_tier = "Standard" + name = "quix-aks-private" + location = "westeurope" + resource_group_name = "rg-quix-private" + create_resource_group = false + kubernetes_version = "1.32.4" + sku_tier = "Standard" private_cluster_enabled = true vnet_name = "vnet-quix-private" @@ -40,21 +40,21 @@ module "aks" { enable_credentials_fetch = true node_pools = { - default = { - name = "default" - type = "system" - node_count = 2 - vm_size = "Standard_D4ds_v5" - } - quix_controller = { - name = "quixcontroller" - type = "user" - node_count = 1 - vm_size = "Standard_D4ds_v5" - taints = ["dedicated=controller:NoSchedule"] - labels = { role = "controller" } - } + default = { + name = "default" + type = "system" + node_count = 2 + vm_size = "Standard_D4ds_v5" } + quix_controller = { + name = "quixcontroller" + type = "user" + node_count = 1 + vm_size = "Standard_D4ds_v5" + taints = ["dedicated=controller:NoSchedule"] + labels = { role = "controller" } + } + } network_profile = { network_plugin_mode = "overlay" diff --git a/examples/public-quix-infr/main.tf b/examples/public-quix-infr/main.tf index d3ddff6..f044ec7 100644 --- a/examples/public-quix-infr/main.tf +++ b/examples/public-quix-infr/main.tf @@ -14,12 +14,12 @@ provider "azurerm" { module "aks" { source = "../../modules/quix-aks" - name = "quix-aks-public" - location = "westeurope" - resource_group_name = "rg-quix-public" - create_resource_group = true - kubernetes_version = "1.32.4" - sku_tier = "Standard" + name = "quix-aks-public" + location = "westeurope" + resource_group_name = "rg-quix-public" + create_resource_group = true + kubernetes_version = "1.32.4" + sku_tier = "Standard" private_cluster_enabled = false vnet_name = "vnet-quix-public" @@ -35,28 +35,28 @@ module "aks" { enable_credentials_fetch = true node_pools = { - default = { - name = "default" - type = "system" - node_count = 1 - vm_size = "Standard_D4ds_v5" - }, - quix_controller = { - name = "quixcontroller" - type = "user" - node_count = 1 - vm_size = "Standard_D4ds_v5" - taints = ["dedicated=controller:NoSchedule"] - labels = { role = "controller" } - } - quix_deployments = { - name = "quixdeployment" - type = "user" - node_count = 1 - vm_size = "Standard_D4ds_v5" - taints = ["dedicated=controller:NoSchedule"] - labels = { role = "controller" } - } + default = { + name = "default" + type = "system" + node_count = 1 + vm_size = "Standard_D4ds_v5" + }, + quix_controller = { + name = "quixcontroller" + type = "user" + node_count = 1 + vm_size = "Standard_D4ds_v5" + taints = ["dedicated=controller:NoSchedule"] + labels = { role = "controller" } + } + quix_deployments = { + name = "quixdeployment" + type = "user" + node_count = 1 + vm_size = "Standard_D4ds_v5" + taints = ["dedicated=controller:NoSchedule"] + labels = { role = "controller" } + } } network_profile = { diff --git a/modules/quix-aks/README.md b/modules/quix-aks/README.md index b0ea410..b0bdfcc 100644 --- a/modules/quix-aks/README.md +++ b/modules/quix-aks/README.md @@ -10,8 +10,8 @@ | Name | Version | |------|---------| -| [azurerm](#provider\_azurerm) | >= 3.112.0, < 4.0.0 | -| [null](#provider\_null) | n/a | +| [azurerm](#provider\_azurerm) | 3.117.1 | +| [null](#provider\_null) | 3.2.4 | ## Modules @@ -65,8 +65,8 @@ No modules. | [name](#input\_name) | Name of the AKS cluster | `string` | n/a | yes | | [nat\_gateway\_name](#input\_nat\_gateway\_name) | Name of the NAT Gateway | `string` | n/a | yes | | [nat\_identity\_name](#input\_nat\_identity\_name) | Name of the managed identity for NAT | `string` | n/a | yes | -| [network\_profile](#input\_network\_profile) | AKS network profile |
object({
network_plugin_mode = string # "overlay" or "vnet"
service_cidr = string
dns_service_ip = string
pod_cidr = optional(string)
network_policy = optional(string, "calico")
outbound_type = optional(string, "userAssignedNATGateway")
})
| n/a | yes | -| [node\_pools](#input\_node\_pools) | Map of additional node pools (include a 'system' pool to override default) |
map(object({
name = string
type = string # system | user
node_count = number
vm_size = string
max_pods = optional(number)
taints = optional(list(string))
labels = optional(map(string))
mode = optional(string) # system | user (overrides type)
}))
| `{}` | no | +| [network\_profile](#input\_network\_profile) | AKS network profile |
object({
network_plugin_mode = string # "overlay" or "vnet"
service_cidr = string
dns_service_ip = string
pod_cidr = optional(string)
network_policy = optional(string, "calico")
outbound_type = optional(string, "userAssignedNATGateway")
})
| n/a | yes | +| [node\_pools](#input\_node\_pools) | Map of additional node pools (include a 'system' pool to override default) |
map(object({
name = string
type = string # system | user
node_count = number
vm_size = string
max_pods = optional(number)
taints = optional(list(string))
labels = optional(map(string))
mode = optional(string) # system | user (overrides type)
}))
| `{}` | no | | [nodes\_subnet\_cidr](#input\_nodes\_subnet\_cidr) | CIDR for the AKS nodes subnet | `string` | n/a | yes | | [nodes\_subnet\_name](#input\_nodes\_subnet\_name) | Name of the AKS nodes subnet | `string` | n/a | yes | | [oidc\_issuer\_enabled](#input\_oidc\_issuer\_enabled) | Enable OIDC issuer | `bool` | `true` | no | diff --git a/modules/quix-aks/aks.tf b/modules/quix-aks/aks.tf index 01bdd65..e9fbc78 100644 --- a/modules/quix-aks/aks.tf +++ b/modules/quix-aks/aks.tf @@ -3,12 +3,12 @@ ################################################################################ resource "azurerm_kubernetes_cluster" "this" { - name = var.name - location = local.rg_location - resource_group_name = local.rg_name_effective - dns_prefix = "${var.name}-dns" - kubernetes_version = var.kubernetes_version - sku_tier = var.sku_tier + name = var.name + location = local.rg_location + resource_group_name = local.rg_name_effective + dns_prefix = "${var.name}-dns" + kubernetes_version = var.kubernetes_version + sku_tier = var.sku_tier private_cluster_enabled = var.private_cluster_enabled oidc_issuer_enabled = var.oidc_issuer_enabled @@ -53,9 +53,9 @@ resource "azurerm_kubernetes_cluster" "this" { } resource "azurerm_kubernetes_cluster_node_pool" "additional" { - for_each = { for k, p in var.node_pools : k => p if k != (length(local.system_pool_keys) > 0 ? local.system_pool_keys[0] : "__none__") } + for_each = { for k, p in var.node_pools : k => p if k != (length(local.system_pool_keys) > 0 ? local.system_pool_keys[0] : "__none__") } # Sanitize name: lowercase, alphanum only, start with letter, max 12 - name = substr( + name = substr( ( can(regex("^[a-z]", (replace(lower(each.value.name), "[^a-z0-9]", "") != "" ? replace(lower(each.value.name), "[^a-z0-9]", "") : "pool"))) ? (replace(lower(each.value.name), "[^a-z0-9]", "") != "" ? replace(lower(each.value.name), "[^a-z0-9]", "") : "pool") @@ -77,7 +77,7 @@ resource "azurerm_kubernetes_cluster_node_pool" "additional" { drain_timeout_in_minutes = 0 node_soak_duration_in_minutes = 0 } - tags = var.tags + tags = var.tags } ################################################################################ diff --git a/modules/quix-aks/main.tf b/modules/quix-aks/main.tf index d641579..9bc31c7 100644 --- a/modules/quix-aks/main.tf +++ b/modules/quix-aks/main.tf @@ -3,17 +3,17 @@ ################################################################################ locals { - is_overlay = var.network_profile.network_plugin_mode == "overlay" + is_overlay = var.network_profile.network_plugin_mode == "overlay" system_pool_keys = [for k, p in var.node_pools : k if lower(coalesce(p.mode, p.type)) == "system"] system_pool = length(local.system_pool_keys) > 0 ? var.node_pools[local.system_pool_keys[0]] : null # Sanitize system pool name: lowercase, alphanum only, start with letter, max 12 - system_pool_name_base = replace(lower(local.system_pool.name), "[^a-z0-9]", "") - system_pool_name_nonempty = local.system_pool_name_base != "" ? local.system_pool_name_base : "system" - system_pool_name_prefixed = can(regex("^[a-z]", local.system_pool_name_nonempty)) ? local.system_pool_name_nonempty : "p${local.system_pool_name_nonempty}" - system_pool_name = substr(local.system_pool_name_prefixed, 0, 12) - rg_id = var.create_resource_group ? azurerm_resource_group.this[0].id : data.azurerm_resource_group.existing[0].id - rg_name_effective = var.resource_group_name - rg_location = var.create_resource_group ? azurerm_resource_group.this[0].location : data.azurerm_resource_group.existing[0].location + system_pool_name_base = replace(lower(local.system_pool.name), "[^a-z0-9]", "") + system_pool_name_nonempty = local.system_pool_name_base != "" ? local.system_pool_name_base : "system" + system_pool_name_prefixed = can(regex("^[a-z]", local.system_pool_name_nonempty)) ? local.system_pool_name_nonempty : "p${local.system_pool_name_nonempty}" + system_pool_name = substr(local.system_pool_name_prefixed, 0, 12) + rg_id = var.create_resource_group ? azurerm_resource_group.this[0].id : data.azurerm_resource_group.existing[0].id + rg_name_effective = var.resource_group_name + rg_location = var.create_resource_group ? azurerm_resource_group.this[0].location : data.azurerm_resource_group.existing[0].location } ################################################################################ diff --git a/modules/quix-aks/variables.tf b/modules/quix-aks/variables.tf index 21dd654..8d355a1 100644 --- a/modules/quix-aks/variables.tf +++ b/modules/quix-aks/variables.tf @@ -80,14 +80,14 @@ variable "nodes_subnet_cidr" { variable "node_pools" { description = "Map of additional node pools (include a 'system' pool to override default)" type = map(object({ - name = string - type = string # system | user - node_count = number - vm_size = string - max_pods = optional(number) - taints = optional(list(string)) - labels = optional(map(string)) - mode = optional(string) # system | user (overrides type) + name = string + type = string # system | user + node_count = number + vm_size = string + max_pods = optional(number) + taints = optional(list(string)) + labels = optional(map(string)) + mode = optional(string) # system | user (overrides type) })) default = {} validation { @@ -103,7 +103,7 @@ variable "node_pools" { variable "network_profile" { description = "AKS network profile" type = object({ - network_plugin_mode = string # "overlay" or "vnet" + network_plugin_mode = string # "overlay" or "vnet" service_cidr = string dns_service_ip = string pod_cidr = optional(string) @@ -111,9 +111,9 @@ variable "network_profile" { outbound_type = optional(string, "userAssignedNATGateway") }) validation { - condition = contains(["overlay", "vnet"], var.network_profile.network_plugin_mode) - error_message = "network_profile.network_plugin_mode must be 'overlay' or 'vnet'." -} + condition = contains(["overlay", "vnet"], var.network_profile.network_plugin_mode) + error_message = "network_profile.network_plugin_mode must be 'overlay' or 'vnet'." + } }