diff --git a/.changeset/sparkly-bananas-arrive.md b/.changeset/sparkly-bananas-arrive.md new file mode 100644 index 0000000000..58bdb6a036 --- /dev/null +++ b/.changeset/sparkly-bananas-arrive.md @@ -0,0 +1,5 @@ +--- +"github_selfhosted_runner_on_container_app_jobs": minor +--- + +Switch to GitHub App-based authentication replacing PAT-based. This approach is generally more secure and scalable. diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/README.md b/infra/modules/github_selfhosted_runner_on_container_app_jobs/README.md index 7d2c4db207..9a307a289b 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/README.md +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/README.md @@ -8,12 +8,20 @@ This module creates a Container App Job to be used as a GitHub self-hosted runne - **Container App Job**: Deploys a Container App Job in the specified Azure Container App Environment. - **Managed Identity**: Provides System Assigned Managed Identity for secure authentication with Azure resources. -- **Key Vault Access**: Grant access to the KeyVault instance with GitHub credentials (PAT token) +- **Key Vault Access**: Grant access to the KeyVault instance with GitHub credentials. - **Auto GitHub Registration**: Automatically scale and register as self-hosted runner in the target repository. +## Authentication Methods + +This module supports two authentication methods: + +1. **GitHub App Authentication (Recommended)**: Use `app_key_secret_name`, `app_id_secret_name`, and `installation_id_secret_name` in the `key_vault` variable. +2. **PAT-based Authentication (Legacy)**: Use `secret_name` in the `key_vault` variable for backward compatibility. + ## Usage Example A usage example can be found in the [examples](https://github.com/pagopa-dx/terraform-azurerm-azure-container-app/tree/main/examples/basic) directory. + ## Requirements @@ -42,7 +50,7 @@ No modules. |------|-------------|------|---------|:--------:| | [container\_app\_environment](#input\_container\_app\_environment) | Configuration for the Container App Environment. |
object({
id = string
location = string
replica_timeout_in_seconds = optional(number, 1800)
polling_interval_in_seconds = optional(number, 30)
min_instances = optional(number, 0)
max_instances = optional(number, 30)
use_labels = optional(bool, false)
override_labels = optional(list(string), [])
cpu = optional(number, 1.5)
memory = optional(string, "3Gi")
image = optional(string, "ghcr.io/pagopa/github-self-hosted-runner-azure:latest")
env_vars = optional(map(string), {})
secrets = optional(map(string), {})
})
| n/a | yes | | [environment](#input\_environment) | Values which are used to generate resource names and location short names. They are all mandatory except for domain, which should not be used only in the case of a resource used by multiple domains. |
object({
prefix = string
env_short = string
location = string
instance_number = string
})
| n/a | yes | -| [key\_vault](#input\_key\_vault) | Details of the Key Vault used to store secrets for the Container App Job. |
object({
name = string
resource_group_name = string
use_rbac = optional(bool, false)
secret_name = optional(string, "github-runner-pat")
})
| n/a | yes | +| [key\_vault](#input\_key\_vault) | "Details of the Key Vault used to store GitHub credentials. Use either:
- 'app\_key\_secret\_name', 'app\_id\_secret\_name', and 'installation\_id\_secret\_name' for GitHub App authentication"
- (Legacy) 'secret\_name' for PAT-based authentication |
object({
name = string
resource_group_name = string
use_rbac = optional(bool, false)
secret_name = optional(string, "github-runner-pat")
app_key_secret_name = optional(string, null)
app_id_secret_name = optional(string, null)
installation_id_secret_name = optional(string, null)
})
| n/a | yes | | [repository](#input\_repository) | Details of the GitHub repository, including the owner and repository name. |
object({
owner = optional(string, "pagopa")
name = string
})
| n/a | yes | | [resource\_group\_name](#input\_resource\_group\_name) | The name of the resource group where the Container App Job will be deployed. Defaults to null. | `string` | `null` | no | | [tags](#input\_tags) | A map of tags to assign to the resources. | `map(any)` | n/a | yes | diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/container_app_job.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/container_app_job.tf index c238ef535d..7b3381fc33 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/container_app_job.tf +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/container_app_job.tf @@ -24,29 +24,64 @@ resource "azurerm_container_app_job" "github_runner" { name = "github-runner-rule" custom_rule_type = "github-runner" - # https://keda.sh/docs/2.17/scalers/github-runner/ + # https://keda.sh/docs/2.19/scalers/github-runner/ metadata = merge({ + githubApiURL = "https://api.github.com" owner = var.repository.owner runnerScope = "repo" repos = var.repository.name - targetWorkflowQueueLength = "1" - github-runner = "https://api.github.com" enableEtags = "true" - }, var.container_app_environment.use_labels ? { labels = local.labels } : {}) + targetWorkflowQueueLength = "1" + }, + local.use_github_app ? { + applicationIDFromEnv = "GITHUB_APP_ID" + installationIDFromEnv = "GITHUB_APP_INSTALLATION_ID" + } : {}, + var.container_app_environment.use_labels ? { labels = local.labels } : {} + ) authentication { - secret_name = var.key_vault.secret_name - trigger_parameter = "personalAccessToken" + secret_name = local.use_github_app ? var.key_vault.app_key_secret_name : var.key_vault.secret_name + trigger_parameter = local.use_github_app ? "appKey" : "personalAccessToken" } } } } - secret { - key_vault_secret_id = "${local.key_vault_uri}secrets/${var.key_vault.secret_name}" # no versioning + dynamic "secret" { + for_each = local.use_github_app ? [1] : [] + content { + key_vault_secret_id = "${local.key_vault_uri}secrets/${var.key_vault.app_key_secret_name}" + identity = "System" + name = var.key_vault.app_key_secret_name + } + } - identity = "System" - name = var.key_vault.secret_name + dynamic "secret" { + for_each = local.use_github_app ? [1] : [] + content { + key_vault_secret_id = "${local.key_vault_uri}secrets/${var.key_vault.app_id_secret_name}" + identity = "System" + name = var.key_vault.app_id_secret_name + } + } + + dynamic "secret" { + for_each = local.use_github_app ? [1] : [] + content { + key_vault_secret_id = "${local.key_vault_uri}secrets/${var.key_vault.installation_id_secret_name}" + identity = "System" + name = var.key_vault.installation_id_secret_name + } + } + + dynamic "secret" { + for_each = local.use_github_app ? [] : [1] + content { + key_vault_secret_id = "${local.key_vault_uri}secrets/${var.key_vault.secret_name}" + identity = "System" + name = var.key_vault.secret_name + } } template { diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/README.md b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/README.md index 3b170e1687..4df77b723c 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/README.md +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/README.md @@ -4,7 +4,7 @@ This directory contains examples of how to use the GitHub SelfHosted Runner on A ## Basic Example -The [basic example](./basic) demonstrates a simple implementation of the GitHub SelfHosted Runner on Azure Container App Job module. +The [app-based example](./app-based) demonstrates a simple implementation of the GitHub SelfHosted Runner on Azure Container App Job module using GitHub App for authentication. ### Usage diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/README.md b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/README.md new file mode 100644 index 0000000000..bfb44683c6 --- /dev/null +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/README.md @@ -0,0 +1,28 @@ +# app_based + + +## Requirements + +No requirements. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [container\_app\_job\_selfhosted\_runner](#module\_container\_app\_job\_selfhosted\_runner) | pagopa-dx/github-selfhosted-runner-on-container-app-jobs/azurerm | ~> 1.4 | + +## Resources + +| Name | Type | +|------|------| +| [azurerm_container_app_environment.gh_runner](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/container_app_environment) | data source | +| [azurerm_resource_group.gh_runner](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group) | data source | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/data.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/data.tf similarity index 100% rename from infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/data.tf rename to infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/data.tf diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/locals.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/locals.tf similarity index 90% rename from infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/locals.tf rename to infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/locals.tf index 3525c1c3e6..0f6bd4d209 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/locals.tf +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/locals.tf @@ -19,6 +19,6 @@ locals { Environment = "Dev" BusinessUnit = "DevEx" ManagementTeam = "Developer Experience" - Source = "https://github.com/pagopa/dx/modules/github_selfhosted_runner_container_app_job/examples/basic" + Source = "https://github.com/pagopa/dx/modules/github_selfhosted_runner_container_app_job/examples/app_based" } } diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/main.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/main.tf new file mode 100644 index 0000000000..f4683005c8 --- /dev/null +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/app_based/main.tf @@ -0,0 +1,28 @@ +module "container_app_job_selfhosted_runner" { + source = "pagopa-dx/github-selfhosted-runner-on-container-app-jobs/azurerm" + version = "~> 1.4" + + environment = local.environment + + resource_group_name = data.azurerm_resource_group.gh_runner.name + + repository = { + name = local.repo_name + } + + container_app_environment = { + id = data.azurerm_container_app_environment.gh_runner.id + location = local.environment.location + } + + key_vault = { + name = local.key_vault.name + resource_group_name = local.key_vault.resource_group_name + use_rbac = true + app_key_secret_name = "github-runner-app-key" + app_id_secret_name = "github-runner-app-id" + installation_id_secret_name = "github-runner-app-installation-id" + } + + tags = local.tags +} diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/README.md b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/README.md similarity index 95% rename from infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/README.md rename to infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/README.md index 9c939c49b3..784a68fe21 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/README.md +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/README.md @@ -11,7 +11,7 @@ | Name | Source | Version | |------|--------|---------| -| [container\_app\_job\_selfhosted\_runner](#module\_container\_app\_job\_selfhosted\_runner) | pagopa-dx/github-selfhosted-runner-on-container-app-jobs/azurerm | ~> 1.2 | +| [container\_app\_job\_selfhosted\_runner](#module\_container\_app\_job\_selfhosted\_runner) | pagopa-dx/github-selfhosted-runner-on-container-app-jobs/azurerm | ~> 1.3 | ## Resources diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/data.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/data.tf new file mode 100644 index 0000000000..afc59b3ba0 --- /dev/null +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/data.tf @@ -0,0 +1,8 @@ +data "azurerm_resource_group" "gh_runner" { + name = "dx-d-itn-github-runner-rg-01" +} + +data "azurerm_container_app_environment" "gh_runner" { + name = "dx-d-itn-github-runner-cae-01" + resource_group_name = data.azurerm_resource_group.gh_runner.name +} diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/locals.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/locals.tf new file mode 100644 index 0000000000..5e15b34b76 --- /dev/null +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/locals.tf @@ -0,0 +1,24 @@ +locals { + environment = { + prefix = "dx" + env_short = "d" + location = "italynorth" + instance_number = "01" + } + + repo_name = "dx" + + key_vault = { + name = "dx-d-itn-common-kv-01" + resource_group_name = "dx-d-itn-common-rg-01" + } + + tags = { + CostCenter = "TS000 - Tecnologia e Servizi" + CreatedBy = "Terraform" + Environment = "Dev" + BusinessUnit = "DevEx" + ManagementTeam = "Developer Experience" + Source = "https://github.com/pagopa/dx/modules/github_selfhosted_runner_container_app_job/examples/pat_based" + } +} diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/main.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/main.tf similarity index 96% rename from infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/main.tf rename to infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/main.tf index 15cbacefc0..c48f4394e1 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/main.tf +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/main.tf @@ -1,6 +1,6 @@ module "container_app_job_selfhosted_runner" { source = "pagopa-dx/github-selfhosted-runner-on-container-app-jobs/azurerm" - version = "~> 1.2" + version = "~> 1.3" environment = local.environment diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/versions.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/versions.tf similarity index 100% rename from infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/basic/versions.tf rename to infra/modules/github_selfhosted_runner_on_container_app_jobs/examples/pat_based/versions.tf diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/locals.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/locals.tf index 463c957761..dadfcc86be 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/locals.tf +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/locals.tf @@ -21,17 +21,24 @@ locals { })) : var.resource_group_name } + use_github_app = var.key_vault.app_key_secret_name != null + runner_env_vars = merge(var.container_app_environment.env_vars, { REPO_URL = "https://github.com/${var.repository.owner}/${var.repository.name}" REGISTRATION_TOKEN_API_URL = "https://api.github.com/repos/${var.repository.owner}/${var.repository.name}/actions/runners/registration-token" }) - runner_secrets = merge(var.container_app_environment.secrets, { - GITHUB_PAT = var.key_vault.secret_name + runner_secrets = merge(var.container_app_environment.secrets, + local.use_github_app ? { + GITHUB_APP_KEY = var.key_vault.app_key_secret_name + GITHUB_APP_ID = var.key_vault.app_id_secret_name + GITHUB_APP_INSTALLATION_ID = var.key_vault.installation_id_secret_name + } : { + GITHUB_PAT = var.key_vault.secret_name }) labels = join(",", coalescelist(var.container_app_environment.override_labels, [local.env[var.environment.env_short]])) - key_vault_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/${var.key_vault.resource_group_name}/providers/Microsoft.KeyVault/vaults/${var.key_vault.name}" + key_vault_id = provider::azurerm::normalise_resource_id("/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/${var.key_vault.resource_group_name}/providers/Microsoft.KeyVault/vaults/${var.key_vault.name}") key_vault_uri = "https://${var.key_vault.name}.vault.azure.net/" } diff --git a/infra/modules/github_selfhosted_runner_on_container_app_jobs/variables.tf b/infra/modules/github_selfhosted_runner_on_container_app_jobs/variables.tf index a33fe95673..df1c2654b2 100644 --- a/infra/modules/github_selfhosted_runner_on_container_app_jobs/variables.tf +++ b/infra/modules/github_selfhosted_runner_on_container_app_jobs/variables.tf @@ -51,11 +51,26 @@ variable "container_app_environment" { variable "key_vault" { type = object({ - name = string - resource_group_name = string - use_rbac = optional(bool, false) - secret_name = optional(string, "github-runner-pat") + name = string + resource_group_name = string + use_rbac = optional(bool, false) + secret_name = optional(string, "github-runner-pat") + app_key_secret_name = optional(string, null) + app_id_secret_name = optional(string, null) + installation_id_secret_name = optional(string, null) }) - description = "Details of the Key Vault used to store secrets for the Container App Job." + description = <<-EOT + "Details of the Key Vault used to store GitHub credentials. Use either: + - 'app_key_secret_name', 'app_id_secret_name', and 'installation_id_secret_name' for GitHub App authentication" + - (Legacy) 'secret_name' for PAT-based authentication + EOT + + validation { + condition = ( + (var.key_vault.secret_name != null && var.key_vault.app_key_secret_name == null && var.key_vault.app_id_secret_name == null && var.key_vault.installation_id_secret_name == null) || + (var.key_vault.secret_name == null && var.key_vault.app_key_secret_name != null && var.key_vault.app_id_secret_name != null && var.key_vault.installation_id_secret_name != null) + ) + error_message = "Either provide 'secret_name' for PAT-based authentication OR all three GitHub App credentials ('app_key_secret_name', 'app_id_secret_name', 'installation_id_secret_name'), but not both." + } }