Skip to content

Commit 1907eda

Browse files
authored
[eks/actions-runner-controller] Auth via GitHub App, prefer webhook auto-scaling (cloudposse/terraform-aws-components#519)
1 parent dc49bf0 commit 1907eda

File tree

6 files changed

+198
-112
lines changed

6 files changed

+198
-112
lines changed

src/README.md

Lines changed: 78 additions & 35 deletions
Large diffs are not rendered by default.

src/main.tf

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
locals {
2-
enabled = module.this.enabled
3-
identity_account_name = module.account_map.outputs.identity_account_account_name
4-
identity_account_id = module.account_map.outputs.full_account_map[local.identity_account_name]
5-
stack_name = local.enabled ? format("${module.this.tenant != null ? "%[1]s-" : ""}%[2]s-%[3]s", module.this.tenant, module.this.environment, module.this.stage) : ""
6-
webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false
7-
webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com"
8-
9-
default_secrets = local.enabled ? [
2+
enabled = module.this.enabled
3+
4+
webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false
5+
webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com"
6+
7+
github_app_enabled = length(var.github_app_id) > 0 && length(var.github_app_installation_id) > 0
8+
create_secret = local.enabled && length(var.existing_kubernetes_secret_name) == 0
9+
10+
busy_metrics_filtered = { for runner, runner_config in var.runners : runner => try(runner_config.busy_metrics, null) == null ? null : {
11+
for k, v in runner_config.busy_metrics : k => v if v != null
12+
} }
13+
14+
default_secrets = local.create_secret ? [
1015
{
11-
name = "authSecret.github_token"
12-
value = join("", data.aws_ssm_parameter.github_token[0].*.value)
16+
name = local.github_app_enabled ? "authSecret.github_app_private_key" : "authSecret.github_token"
17+
value = one(data.aws_ssm_parameter.github_token[*].value)
1318
type = "string"
1419
}
1520
] : []
1621

17-
webhook_secrets = local.webhook_enabled ? [
22+
webhook_secrets = local.create_secret && local.webhook_enabled ? [
1823
{
1924
name = "githubWebhookServer.secret.github_webhook_secret_token"
20-
value = join("", data.aws_ssm_parameter.github_webhook_secret_token[0].*.value)
25+
value = one(data.aws_ssm_parameter.github_webhook_secret_token[*].value)
2126
type = "string"
2227
}
2328
] : []
@@ -80,39 +85,36 @@ locals {
8085
iam_policy_statements = concat(local.default_iam_policy_statements, local.s3_iam_policy_statements)
8186
}
8287

83-
data "aws_partition" "current" {
84-
count = local.enabled ? 1 : 0
85-
}
86-
8788
data "aws_ssm_parameter" "github_token" {
88-
count = local.enabled ? 1 : 0
89+
count = local.create_secret ? 1 : 0
8990

90-
name = var.ssm_github_token_path
91+
name = var.ssm_github_secret_path
9192
with_decryption = true
9293
}
9394

9495
data "aws_ssm_parameter" "github_webhook_secret_token" {
95-
count = local.webhook_enabled ? 1 : 0
96+
count = local.create_secret && local.webhook_enabled ? 1 : 0
9697

9798
name = var.ssm_github_webhook_secret_token_path
9899
with_decryption = true
99100
}
100101

101102
module "actions_runner_controller" {
102103
source = "cloudposse/helm-release/aws"
103-
version = "0.6.0"
104+
version = "0.7.0"
104105

105-
name = "" # avoids hitting length restrictions on IAM Role names
106-
chart = var.chart
107-
repository = var.chart_repository
108-
description = var.chart_description
109-
chart_version = var.chart_version
110-
kubernetes_namespace = var.kubernetes_namespace
111-
create_namespace = var.create_namespace
112-
wait = var.wait
113-
atomic = var.atomic
114-
cleanup_on_fail = var.cleanup_on_fail
115-
timeout = var.timeout
106+
name = "" # avoids hitting length restrictions on IAM Role names
107+
chart = var.chart
108+
repository = var.chart_repository
109+
description = var.chart_description
110+
chart_version = var.chart_version
111+
wait = var.wait
112+
atomic = var.atomic
113+
cleanup_on_fail = var.cleanup_on_fail
114+
timeout = var.timeout
115+
116+
kubernetes_namespace = var.kubernetes_namespace
117+
create_namespace_with_kubernetes = var.create_namespace
116118

117119
eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer
118120

@@ -155,7 +157,23 @@ module "actions_runner_controller" {
155157
},
156158
authSecret = {
157159
enabled = true
158-
create = true
160+
create = local.create_secret
161+
}
162+
}),
163+
local.github_app_enabled ? yamlencode({
164+
authSecret = {
165+
github_app_id = var.github_app_id
166+
github_app_installation_id = var.github_app_installation_id
167+
}
168+
}) : "",
169+
local.create_secret ? "" : yamlencode({
170+
authSecret = {
171+
name = var.existing_kubernetes_secret_name
172+
},
173+
githubWebhookServer = {
174+
secret = {
175+
name = var.existing_kubernetes_secret_name
176+
}
159177
}
160178
}),
161179
# additional values
@@ -171,18 +189,18 @@ module "actions_runner" {
171189
for_each = local.enabled ? var.runners : {}
172190

173191
source = "cloudposse/helm-release/aws"
174-
version = "0.6.0"
192+
version = "0.7.0"
175193

176194
name = each.key
177195
chart = "${path.module}/charts/actions-runner"
178196

179197
kubernetes_namespace = var.kubernetes_namespace
180-
create_namespace = var.create_namespace
198+
create_namespace = false # will be created by controller above
181199
atomic = var.atomic
182200

183201
eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer
184202

185-
values = [
203+
values = compact([
186204
yamlencode({
187205
release_name = each.key
188206
service_account_name = module.actions_runner_controller.service_account_name
@@ -197,16 +215,11 @@ module "actions_runner" {
197215
scale_down_delay_seconds = each.value.scale_down_delay_seconds
198216
min_replicas = each.value.min_replicas
199217
max_replicas = each.value.max_replicas
200-
scale_up_threshold = try(each.value.busy_metrics.scale_up_threshold, null)
201-
scale_down_threshold = try(each.value.busy_metrics.scale_down_threshold, null)
202-
scale_up_adjustment = try(each.value.busy_metrics.scale_up_adjustment, null)
203-
scale_down_adjustment = try(each.value.busy_metrics.scale_down_adjustment, null)
204-
scale_up_factor = try(each.value.busy_metrics.scale_up_factor, null)
205-
scale_down_factor = try(each.value.busy_metrics.scale_down_factor, null)
206218
webhook_driven_scaling_enabled = each.value.webhook_driven_scaling_enabled
207219
pull_driven_scaling_enabled = each.value.pull_driven_scaling_enabled
208-
})
209-
]
220+
}),
221+
local.busy_metrics_filtered[each.key] == null ? "" : yamlencode(local.busy_metrics_filtered[each.key]),
222+
])
210223

211224
depends_on = [module.actions_runner_controller]
212225
}

src/provider-helm.tf

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
#
33
# This file is a drop-in to provide a helm provider.
44
#
5+
# It depends on 2 standard Cloud Posse data source modules to be already
6+
# defined in the same component:
7+
#
8+
# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster
9+
# 2. module.eks to provide the EKS cluster information
10+
#
511
# All the following variables are just about configuring the Kubernetes provider
612
# to be able to modify EKS cluster. The reason there are so many options is
713
# because at various times, each one of them has had problems, so we give you a choice.
@@ -100,9 +106,11 @@ locals {
100106
"--role-arn", local.kube_exec_auth_role_arn
101107
] : []
102108

103-
certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data
104-
eks_cluster_id = module.eks.outputs.eks_cluster_id
105-
eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint
109+
# Provide dummy configuration for the case where the EKS cluster is not available.
110+
certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "")
111+
# Use coalesce+try to handle both the case where the output is missing and the case where it is empty.
112+
eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing")
113+
eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "")
106114
}
107115

108116
data "aws_eks_cluster_auth" "eks" {
@@ -114,14 +122,14 @@ provider "helm" {
114122
kubernetes {
115123
host = local.eks_cluster_endpoint
116124
cluster_ca_certificate = base64decode(local.certificate_authority_data)
117-
token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null
125+
token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null
118126
# The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster
119127
# in KUBECONFIG is some other cluster, this will cause problems, so we override it always.
120128
config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : ""
121129
config_context = var.kubeconfig_context
122130

123131
dynamic "exec" {
124-
for_each = local.kube_exec_auth_enabled ? ["exec"] : []
132+
for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : []
125133
content {
126134
api_version = local.kubeconfig_exec_auth_api_version
127135
command = "aws"
@@ -132,21 +140,21 @@ provider "helm" {
132140
}
133141
}
134142
experiments {
135-
manifest = var.helm_manifest_experiment_enabled
143+
manifest = var.helm_manifest_experiment_enabled && module.this.enabled
136144
}
137145
}
138146

139147
provider "kubernetes" {
140148
host = local.eks_cluster_endpoint
141149
cluster_ca_certificate = base64decode(local.certificate_authority_data)
142-
token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null
150+
token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null
143151
# The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster
144152
# in KUBECONFIG is some other cluster, this will cause problems, so we override it always.
145153
config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : ""
146154
config_context = var.kubeconfig_context
147155

148156
dynamic "exec" {
149-
for_each = local.kube_exec_auth_enabled ? ["exec"] : []
157+
for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : []
150158
content {
151159
api_version = local.kubeconfig_exec_auth_api_version
152160
command = "aws"

src/remote-state.tf

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
11
module "eks" {
22
source = "cloudposse/stack-config/yaml//modules/remote-state"
3-
version = "0.22.4"
3+
version = "1.3.1"
44

55
component = var.eks_component_name
66

77
context = module.this.context
88
}
9-
10-
module "account_map" {
11-
source = "cloudposse/stack-config/yaml//modules/remote-state"
12-
version = "0.22.4"
13-
14-
component = "account-map"
15-
environment = var.account_map_environment_name
16-
stage = var.account_map_stage_name
17-
tenant = var.account_map_tenant_name
18-
19-
context = module.this.context
20-
}

src/variables.tf

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ variable "rbac_enabled" {
8888

8989
# Runner-specific settings
9090

91+
/*
9192
variable "account_map_environment_name" {
9293
type = string
9394
description = "The name of the environment where `account_map` is provisioned"
@@ -110,6 +111,20 @@ variable "account_map_tenant_name" {
110111
default = "core"
111112
}
112113
114+
*/
115+
116+
variable "existing_kubernetes_secret_name" {
117+
type = string
118+
description = <<-EOT
119+
If you are going to create the Kubernetes Secret the runner-controller will use
120+
by some means (such as SOPS) outside of this component, set the name of the secret
121+
here and it will be used. In this case, this component will not create a secret
122+
and you can leave the secret-related inputs with their default (empty) values.
123+
The same secret will be used by both the runner-controller and the webhook-server.
124+
EOT
125+
default = ""
126+
}
127+
113128
variable "s3_bucket_arns" {
114129
type = list(string)
115130
description = "List of ARNs of S3 Buckets to which the runners will have read-write access to."
@@ -147,23 +162,30 @@ variable "runners" {
147162
EOT
148163

149164
type = map(object({
150-
type = string
151-
scope = string
152-
image = string
153-
dind_enabled = bool
154-
scale_down_delay_seconds = number
155-
min_replicas = number
156-
max_replicas = number
157-
busy_metrics = map(string)
165+
type = string
166+
scope = string
167+
image = optional(string, "")
168+
dind_enabled = bool
169+
scale_down_delay_seconds = number
170+
min_replicas = number
171+
max_replicas = number
172+
busy_metrics = optional(object({
173+
scale_up_threshold = string
174+
scale_down_threshold = string
175+
scale_up_adjustment = optional(string)
176+
scale_down_adjustment = optional(string)
177+
scale_up_factor = optional(string)
178+
scale_down_factor = optional(string)
179+
}))
158180
webhook_driven_scaling_enabled = bool
159181
pull_driven_scaling_enabled = bool
160182
labels = list(string)
161-
storage = optional(string, false)
183+
storage = optional(string, "")
162184
resources = object({
163185
limits = object({
164186
cpu = string
165187
memory = string
166-
ephemeral_storage = optional(string, false)
188+
ephemeral_storage = optional(string, "")
167189
})
168190
requests = object({
169191
cpu = string
@@ -195,9 +217,21 @@ variable "eks_component_name" {
195217
default = "eks/cluster"
196218
}
197219

198-
variable "ssm_github_token_path" {
220+
variable "github_app_id" {
221+
type = string
222+
description = "The ID of the GitHub App to use for the runner controller."
223+
default = ""
224+
}
225+
226+
variable "github_app_installation_id" {
227+
type = string
228+
description = "The \"Installation ID\" of the GitHub App to use for the runner controller."
229+
default = ""
230+
}
231+
232+
variable "ssm_github_secret_path" {
199233
type = string
200-
description = "The path in SSM to the GitHub token."
234+
description = "The path in SSM to the GitHub app private key file contents or GitHub PAT token."
201235
default = ""
202236
}
203237

src/versions.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ terraform {
44
required_providers {
55
aws = {
66
source = "hashicorp/aws"
7-
version = "~> 4.0"
7+
version = ">= 4.9.0"
88
}
99
helm = {
1010
source = "hashicorp/helm"

0 commit comments

Comments
 (0)