Skip to content

Commit

Permalink
feat: Module for GitHub self hosted runner on Azure Container Apps Jo…
Browse files Browse the repository at this point in the history
…bs (#171)
  • Loading branch information
Krusty93 authored Oct 18, 2023
1 parent 9f79005 commit 723f1dd
Show file tree
Hide file tree
Showing 12 changed files with 579 additions and 0 deletions.
76 changes: 76 additions & 0 deletions container_app_job_gh_runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Azure Container App Job as GitHub Runners

This module creates the infrastructure to host GitHub self hosted runners using Azure Container Apps jobs.

## Included resources

The following resources are created and managed by this module:

- resource group
- subnet
- container app environment
- container app job

## How to use it

Give a try to the example saved in `terraform-azurerm-v3/container_app_job_gh_runner/tests` to see a working demo of this module

### Prerequisites

Before running the demo, you need to manually create:

- vnet
- keyvault
- a valid GitHub PAT stored as secret

Names of these resources are required as module input

<!-- markdownlint-disable -->
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_azapi"></a> [azapi](#requirement\_azapi) | <= 1.9.0 |
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | >= 3.44.0, <= 3.76.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [azapi_resource.runner_environment](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) | resource |
| [azapi_resource.runner_job](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) | resource |
| [azurerm_key_vault_access_policy.keyvault_containerapp](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy) | resource |
| [azurerm_resource_group.runner_rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource |
| [azurerm_subnet.runner_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource |
| [azurerm_key_vault.key_vault](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source |
| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_app"></a> [app](#input\_app) | n/a | <pre>object({<br> repo_owner = optional(string, "pagopa")<br> repos = set(string)<br> image = optional(string, "ghcr.io/pagopa/github-self-hosted-runner-azure:beta-dockerfile-v2@sha256:ed51ac419d78b6410be96ecaa8aa8dbe645aa0309374132886412178e2739a47")<br> })</pre> | n/a | yes |
| <a name="input_env_short"></a> [env\_short](#input\_env\_short) | Short environment prefix | `string` | n/a | yes |
| <a name="input_environment"></a> [environment](#input\_environment) | n/a | <pre>object({<br> workspace_id = string<br> customerId = string<br> sharedKey = string<br> })</pre> | n/a | yes |
| <a name="input_key_vault"></a> [key\_vault](#input\_key\_vault) | n/a | <pre>object({<br> resource_group_name = string<br> name = string<br> secret_name = string<br> })</pre> | n/a | yes |
| <a name="input_location"></a> [location](#input\_location) | Resource group and resources location | `string` | n/a | yes |
| <a name="input_network"></a> [network](#input\_network) | n/a | <pre>object({<br> rg_vnet = string<br> vnet = string<br> cidr_subnets = list(string)<br> })</pre> | n/a | yes |
| <a name="input_prefix"></a> [prefix](#input\_prefix) | Project prefix | `string` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | Tags for new resources | `map(any)` | <pre>{<br> "CreatedBy": "Terraform"<br>}</pre> | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_ca_name"></a> [ca\_name](#output\_ca\_name) | Container App job name |
| <a name="output_cae_name"></a> [cae\_name](#output\_cae\_name) | Container App Environment name |
| <a name="output_resource_group"></a> [resource\_group](#output\_resource\_group) | Resource group name |
| <a name="output_subnet_cidr"></a> [subnet\_cidr](#output\_subnet\_cidr) | Subnet CIDR blocks |
| <a name="output_subnet_name"></a> [subnet\_name](#output\_subnet\_name) | Subnet name |
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
49 changes: 49 additions & 0 deletions container_app_job_gh_runner/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
locals {
name = "${var.prefix}-${var.env_short}"
resource_group_name = "${local.name}-github-runner-rg"

rules = [for repo in var.app.repos :
{
name = "github-runner-${repo}"
type = "github-runner"
metadata = {
owner = var.app.repo_owner
runnerScope = "repo"
repos = "${repo}"
targetWorkflowQueueLength = "1"
labels = "github-runner-${repo}"
}
auth = [
{
secretRef = "personal-access-token"
triggerParameter = "personalAccessToken"
}
]
}
]

containers = [for repo in var.app.repos :
{
env = [
{
name = "GITHUB_PAT"
secretRef = "personal-access-token"
},
{
name = "REPO_URL"
value = "https://github.com/${var.app.repo_owner}/${repo}"
},
{
name = "REGISTRATION_TOKEN_API_URL"
value = "https://api.github.com/repos/${var.app.repo_owner}/${repo}/actions/runners/registration-token"
}
]
image = var.app.image
name = "github-runner-${repo}"
resources = {
cpu = 1.0
memory = "2Gi"
}
}
]
}
97 changes: 97 additions & 0 deletions container_app_job_gh_runner/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
data "azurerm_key_vault" "key_vault" {
resource_group_name = var.key_vault.resource_group_name
name = var.key_vault.name
}

resource "azurerm_resource_group" "runner_rg" {
name = local.resource_group_name
location = var.location

tags = var.tags
}

resource "azurerm_subnet" "runner_subnet" {
name = "${local.name}-github-runner-snet"
resource_group_name = var.network.rg_vnet
virtual_network_name = var.network.vnet
address_prefixes = var.network.cidr_subnets
service_endpoints = []
}

resource "azapi_resource" "runner_environment" {
type = "Microsoft.App/managedEnvironments@2023-05-01"
name = "${local.name}-github-runner-cae"
location = azurerm_resource_group.runner_rg.location
parent_id = azurerm_resource_group.runner_rg.id

tags = var.tags

body = jsonencode({
properties = {
appLogsConfiguration = {
destination = "log-analytics"
logAnalyticsConfiguration = {
customerId = var.environment.customerId
sharedKey = var.environment.sharedKey
}
}
zoneRedundant = true
vnetConfiguration = {
infrastructureSubnetId = azurerm_subnet.runner_subnet.id
internal = true
}
}
})
}

resource "azapi_resource" "runner_job" {
type = "Microsoft.App/jobs@2023-05-01"
name = "${local.name}-github-runner-job"
location = azurerm_resource_group.runner_rg.location
parent_id = azurerm_resource_group.runner_rg.id

tags = var.tags

identity {
type = "SystemAssigned"
}

body = jsonencode({
properties = {
environmentId = azapi_resource.runner_environment.id
configuration = {
replicaRetryLimit = 1
replicaTimeout = 1800
eventTriggerConfig = {
parallelism = 1
replicaCompletionCount = 1
scale = {
maxExecutions = 10
minExecutions = 0
pollingInterval = 20
rules = local.rules
}
}
secrets = [{
keyVaultUrl = "${data.azurerm_key_vault.key_vault.vault_uri}secrets/${var.key_vault.secret_name}" # no versioning
identity = "system"
name = "personal-access-token"
}]
triggerType = "Event"
}
template = {
containers = local.containers
}
}
})
}

resource "azurerm_key_vault_access_policy" "keyvault_containerapp" {
key_vault_id = data.azurerm_key_vault.key_vault.id
tenant_id = azapi_resource.runner_job.identity[0].tenant_id
object_id = azapi_resource.runner_job.identity[0].principal_id

secret_permissions = [
"Get",
]
}
24 changes: 24 additions & 0 deletions container_app_job_gh_runner/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
output "resource_group" {
value = azurerm_resource_group.runner_rg.name
description = "Resource group name"
}

output "subnet_name" {
value = azurerm_subnet.runner_subnet.name
description = "Subnet name"
}

output "subnet_cidr" {
value = azurerm_subnet.runner_subnet.address_prefixes
description = "Subnet CIDR blocks"
}

output "cae_name" {
value = azapi_resource.runner_environment.name
description = "Container App Environment name"
}

output "ca_name" {
value = azapi_resource.runner_job.name
description = "Container App job name"
}
1 change: 1 addition & 0 deletions container_app_job_gh_runner/tests/backend.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
subscription=DevOpsLab
41 changes: 41 additions & 0 deletions container_app_job_gh_runner/tests/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
terraform {
required_version = ">= 1.3.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "<= 3.76.0"
}

azapi = {
source = "azure/azapi"
version = "<= 1.9.0"
}
}
}

provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = false
}
resource_group {
prevent_deletion_if_contains_resources = false
}
}
}

data "azurerm_client_config" "current" {
}

resource "random_id" "unique" {
byte_length = 3
}

locals {
project = "${var.prefix}${random_id.unique.hex}"
env_short = substr(random_id.unique.hex, 0, 1)
rg_name = "${local.project}-${local.env_short}-github-runner-rg"
key_vault_name = "${local.project}-${local.env_short}-kv"
vnet_name = "${local.project}-${local.env_short}-vnet"
}
28 changes: 28 additions & 0 deletions container_app_job_gh_runner/tests/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
output "random_id" {
value = random_id.unique.hex
}

output "resource_group" {
value = module.runner.resource_group
description = "Resource group name"
}

output "subnet_name" {
value = module.runner.subnet_name
description = "Subnet name"
}

output "subnet_cidr" {
value = module.runner.subnet_cidr
description = "Subnet CIDR blocks"
}

output "cae_name" {
value = module.runner.cae_name
description = "Container App Environment name"
}

output "ca_name" {
value = module.runner.ca_name
description = "Container App job name"
}
61 changes: 61 additions & 0 deletions container_app_job_gh_runner/tests/resources.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
resource "azurerm_resource_group" "rg" {
name = local.rg_name
location = var.location
}

#tfsec:ignore:azure-keyvault-specify-network-acl
#tfsec:ignore:azure-keyvault-no-purge
resource "azurerm_key_vault" "key_vault" {
resource_group_name = azurerm_resource_group.rg.name
name = local.key_vault_name
sku_name = "standard"
location = azurerm_resource_group.rg.location
tenant_id = data.azurerm_client_config.current.tenant_id
public_network_access_enabled = true
soft_delete_retention_days = 7
}

resource "azurerm_virtual_network" "vnet" {
resource_group_name = azurerm_resource_group.rg.name
name = local.vnet_name
location = azurerm_resource_group.rg.location
address_space = ["10.0.0.0/16"]
}

# module to use
module "runner" {
source = "../" # change me with module URI

location = var.location
prefix = var.prefix
env_short = local.env_short # change me with your env

# set reference to the secret which holds the GitHub PAT with access to your repos
key_vault = {
resource_group_name = azurerm_key_vault.key_vault.resource_group_name
name = azurerm_key_vault.key_vault.name
secret_name = var.key_vault.secret_name
}

# creates a subnet in the specified existing vnet. Use a /23 CIDR block
network = {
rg_vnet = azurerm_virtual_network.vnet.resource_group_name
vnet = azurerm_virtual_network.vnet.name
cidr_subnets = var.network.cidr_subnets
}

# set reference to the log analytics workspace you want to use for logging
environment = {
workspace_id = var.environment.workspace_id
customerId = var.environment.customerId
sharedKey = var.environment.sharedKey
}

# set app properties - especially the list of repos to support
app = {
repos = var.app.repos
repo_owner = var.app.repo_owner
}

tags = var.tags
}
Loading

0 comments on commit 723f1dd

Please sign in to comment.