├── modules ├── runners │ ├── .gitkeep │ ├── lambdas │ │ ├── runners │ │ │ ├── .nvmrc │ │ │ ├── src │ │ │ │ ├── local-pool.ts │ │ │ │ ├── local-down.ts │ │ │ │ ├── scale-runners │ │ │ │ │ ├── ScaleError.ts │ │ │ │ │ ├── cache.ts │ │ │ │ │ ├── scale-down-config.ts │ │ │ │ │ └── scale-down-config.test.ts │ │ │ │ ├── aws │ │ │ │ │ ├── ssm.ts │ │ │ │ │ └── ssm.test.ts │ │ │ │ ├── modules.d.ts │ │ │ │ ├── logger │ │ │ │ │ ├── logger.test.ts │ │ │ │ │ ├── logger.child.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── lambda.ts │ │ │ │ ├── local.ts │ │ │ │ └── gh-auth │ │ │ │ │ └── gh-auth.ts │ │ │ ├── .prettierrc │ │ │ ├── jest.config.js │ │ │ ├── .eslintrc.yaml │ │ │ ├── test │ │ │ │ └── resources │ │ │ │ │ └── sqs_receive_event.json │ │ │ └── package.json │ │ └── .gitignore │ ├── versions.tf │ ├── pool │ │ ├── versions.tf │ │ ├── outputs.tf │ │ ├── policies │ │ │ └── lambda-pool.json │ │ └── variables.tf │ ├── policies │ │ ├── instance-describe-tags-policy.json │ │ ├── lambda-cloudwatch.json │ │ ├── instance-s3-policy.json │ │ ├── service-linked-role-create-policy.json │ │ ├── instance-role-trust-policy.json │ │ ├── lambda-vpc.json │ │ ├── instance-ec2.json │ │ ├── instance-ssm-parameters-policy.json │ │ ├── instance-cloudwatch-policy.json │ │ ├── lambda-scale-down.json │ │ ├── instance-ssm-policy.json │ │ └── lambda-scale-up.json │ ├── templates │ │ ├── cloudwatch_config.json │ │ ├── install-runner.ps1 │ │ ├── user-data.sh │ │ ├── user-data.ps1 │ │ └── install-runner.sh │ ├── runner-config.tf │ ├── policies-lambda-common.tf │ ├── outputs.tf │ ├── policies-runner.tf │ ├── logging.tf │ └── pool.tf ├── webhook │ ├── policies.tf │ ├── lambdas │ │ └── webhook │ │ │ ├── .nvmrc │ │ │ ├── .gitignore │ │ │ ├── src │ │ │ ├── webhook │ │ │ │ └── modules.d.ts │ │ │ ├── ssm │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── local.ts │ │ │ ├── lambda.ts │ │ │ ├── logger │ │ │ │ ├── logger.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── logger.child.test.ts │ │ │ └── sqs │ │ │ │ └── index.ts │ │ │ ├── .prettierrc │ │ │ ├── jest.config.js │ │ │ ├── .eslintrc.yaml │ │ │ ├── test │ │ │ └── resources │ │ │ │ └── multi_runner_configurations.json │ │ │ └── package.json │ ├── terraform.tfvars │ ├── versions.tf │ ├── policies │ │ ├── lambda-cloudwatch.json │ │ ├── lambda-publish-sqs-policy.json │ │ ├── lambda-orchestrator.json │ │ └── lambda-ssm.json │ ├── outputs.tf │ ├── yarn.lock │ └── main.tf ├── runner-binaries-syncer │ ├── lambdas │ │ └── runner-binaries-syncer │ │ │ ├── .nvmrc │ │ │ ├── response.json │ │ │ ├── README.md │ │ │ ├── .gitignore │ │ │ ├── .prettierrc │ │ │ ├── src │ │ │ ├── local.ts │ │ │ ├── lambda.ts │ │ │ ├── logger │ │ │ │ ├── logger.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── logger.child.test.ts │ │ │ └── lambda.test.ts │ │ │ ├── jest.config.js │ │ │ ├── .eslintrc.yaml │ │ │ ├── template.yaml │ │ │ └── package.json │ ├── trigger.json │ ├── terraform.tfvars │ ├── versions.tf │ ├── policies │ │ ├── lambda-kms.json │ │ ├── lambda-cloudwatch.json │ │ ├── lambda-vpc.json │ │ └── lambda-syncer.json │ └── outputs.tf ├── download-lambda │ ├── outputs.tf │ ├── variables.tf │ ├── versions.tf │ ├── main.tf │ ├── .terraform.lock.hcl │ └── README.md ├── ssm │ ├── local.tf │ ├── versions.tf │ ├── outputs.tf │ ├── ssm.tf │ ├── variables.tf │ └── README.md ├── setup-iam-permissions │ ├── outputs.tf │ ├── versions.tf │ ├── policies │ │ ├── assume-role-for-account.json │ │ ├── deploy-policy.json │ │ ├── boundary.json │ │ └── deploy-boundary.json │ ├── variables.tf │ └── main.tf └── multi-runner │ ├── ssm.tf │ ├── versions.tf │ ├── main.tf │ ├── webhook.tf │ ├── runner-binaries.tf │ └── outputs.tf ├── .ci ├── .dockerignore ├── build.ps1 ├── build-yarn.sh ├── build.sh └── Dockerfile ├── examples ├── base │ ├── outputs.tf │ ├── versions.tf │ ├── templates │ │ └── resource-group.json │ ├── main.tf │ ├── variables.tf │ ├── vpc.tf │ └── README.md ├── arm64 │ ├── providers.tf │ ├── lambdas-download │ │ ├── versions.tf │ │ ├── main.tf │ │ └── README.md │ ├── variables.tf │ ├── vpc.tf │ ├── outputs.tf │ ├── versions.tf │ └── main.tf ├── lambdas-download │ ├── versions.tf │ ├── variables.tf │ ├── main.tf │ ├── README.md │ └── .terraform.lock.hcl ├── multi-runner │ ├── providers.tf │ ├── outputs.tf │ ├── variables.tf │ ├── versions.tf │ ├── vpc.tf │ └── templates │ │ └── user-data.sh ├── permissions-boundary │ ├── setup │ │ ├── providers.tf │ │ ├── outputs.tf │ │ ├── versions.tf │ │ ├── main.tf │ │ ├── README.md │ │ └── .terraform.lock.hcl │ ├── variables.tf │ ├── providers.tf │ ├── outputs.tf │ ├── versions.tf │ ├── vpc.tf │ └── main.tf ├── windows │ ├── lambdas-download │ │ ├── versions.tf │ │ ├── main.tf │ │ └── README.md │ ├── providers.tf │ ├── variables.tf │ ├── outputs.tf │ ├── versions.tf │ ├── main.tf │ └── README.md ├── prebuilt │ ├── providers.tf │ ├── outputs.tf │ ├── variables.tf │ ├── versions.tf │ └── main.tf ├── ephemeral │ ├── providers.tf │ ├── variables.tf │ ├── outputs.tf │ ├── versions.tf │ └── main.tf ├── default │ ├── providers.tf │ ├── variables.tf │ ├── outputs.tf │ ├── versions.tf │ ├── main.tf │ └── README.md └── ubuntu │ ├── variables.tf │ ├── outputs.tf │ ├── providers.tf │ ├── versions.tf │ ├── vpc.tf │ ├── templates │ └── user-data.sh │ └── README.md ├── images ├── start-runner.ps1 ├── install-runner.ps1 ├── install-runner.sh ├── start-runner.sh ├── README.md ├── windows-core-2019 │ ├── bootstrap_win.ps1 │ └── windows-provisioner.ps1 └── windows-core-2022 │ ├── bootstrap_win.ps1 │ └── windows-provisioner.ps1 ├── .tflint.hcl ├── MAINTAINERS.md ├── .vscode ├── settings.json └── extensions.json ├── .github ├── lint │ └── tflint.tfvars ├── workflows │ ├── lambda-runners.yml │ ├── lambda-webhook.yml │ ├── lambda-runner-binaries-syncer.yml │ ├── update-docs.yml │ ├── semantic-check.yml │ ├── lambda-template.yml │ ├── auto-approve-dependabot.yml │ ├── stale.yml │ └── packer-build.yml └── dependabot.yml ├── policies └── lambda-publish-sqs-policy.json ├── versions.tf ├── .editorconfig ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── variables.deprecated.tf ├── outputs.tf └── docs └── architecture.drawio /modules/runners/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/webhook/policies.tf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ci/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /examples/base/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc" { 2 | value = module.vpc 3 | } 4 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/response.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/trigger.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value" 3 | } 4 | -------------------------------------------------------------------------------- /examples/arm64/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | } 4 | -------------------------------------------------------------------------------- /examples/lambdas-download/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1" 3 | } 4 | -------------------------------------------------------------------------------- /examples/multi-runner/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | } 4 | -------------------------------------------------------------------------------- /examples/arm64/lambdas-download/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1" 3 | } 4 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "eu-west-1" 3 | } 4 | -------------------------------------------------------------------------------- /examples/windows/lambdas-download/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1" 3 | } 4 | -------------------------------------------------------------------------------- /modules/download-lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "files" { 2 | value = null_resource.download[*].triggers.file 3 | } 4 | -------------------------------------------------------------------------------- /images/start-runner.ps1: -------------------------------------------------------------------------------- 1 | Start-Transcript -Path "C:\runner-startup.log" -Append 2 | ${start_runner} 3 | Stop-Transcript 4 | -------------------------------------------------------------------------------- /modules/ssm/local.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | kms_key_arn = var.kms_key_arn == null ? "alias/aws/ssm" : var.kms_key_arn 3 | } 4 | -------------------------------------------------------------------------------- /.tflint.hcl: -------------------------------------------------------------------------------- 1 | config { 2 | format = "compact" 3 | module = true 4 | 5 | varfile = [".github/lint/tflint.tfvars"] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Navdeep Gupta 2 | Niek Palm 3 | Scott Guymer 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sonarlint.rules": { 3 | "javascript:S4123": { 4 | "level": "off" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /examples/lambdas-download/variables.tf: -------------------------------------------------------------------------------- 1 | variable "module_version" { 2 | description = "Module release version." 3 | type = string 4 | } 5 | -------------------------------------------------------------------------------- /modules/webhook/terraform.tfvars: -------------------------------------------------------------------------------- 1 | environment = "test" 2 | github_app_webhook_secret = "niek" 3 | aws_region = "eu-west-1" 4 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/outputs.tf: -------------------------------------------------------------------------------- 1 | output "role" { 2 | value = module.iam.role 3 | } 4 | 5 | output "boundary" { 6 | value = module.iam.boundary 7 | } 8 | -------------------------------------------------------------------------------- /examples/prebuilt/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | default_tags { 4 | tags = { 5 | Example = local.environment 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/windows/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | default_tags { 4 | tags = { 5 | Example = local.environment 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/ephemeral/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | default_tags { 4 | tags = { 5 | Example = local.environment 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/terraform.tfvars: -------------------------------------------------------------------------------- 1 | environment = "test" 2 | distribution_bucket_name = "alkj4klrj32trogjreoigfvj" 3 | aws_region = "eu-west-1" 4 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | output "role" { 3 | value = aws_iam_role.deploy.arn 4 | } 5 | 6 | output "boundary" { 7 | value = aws_iam_policy.boundary.arn 8 | } 9 | -------------------------------------------------------------------------------- /.github/lint/tflint.tfvars: -------------------------------------------------------------------------------- 1 | aws_region = null 2 | github_app = { 3 | id = "0" 4 | key_base64 = "0" 5 | webhook_secret = "0" 6 | } 7 | subnet_ids = [] 8 | vpc_id = null 9 | -------------------------------------------------------------------------------- /examples/arm64/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /examples/default/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | 4 | default_tags { 5 | tags = { 6 | Example = local.environment 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/ephemeral/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /examples/ubuntu/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /examples/windows/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/README.md: -------------------------------------------------------------------------------- 1 | # Lambda runner binary syncer 2 | 3 | For testing the lambda locally check out [this guide](../../../../docs/test-lambda-local.md). -------------------------------------------------------------------------------- /modules/download-lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "lambdas" { 2 | description = "Name and tag for lambdas to download." 3 | type = list(object({ 4 | name = string 5 | tag = string 6 | })) 7 | } 8 | -------------------------------------------------------------------------------- /examples/permissions-boundary/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /examples/base/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | } 8 | required_version = ">= 1" 9 | } 10 | -------------------------------------------------------------------------------- /modules/ssm/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/multi-runner/outputs.tf: -------------------------------------------------------------------------------- 1 | output "webhook_endpoint" { 2 | value = module.multi-runner.webhook.endpoint 3 | } 4 | 5 | output "webhook_secret" { 6 | sensitive = true 7 | value = random_id.random.hex 8 | } 9 | -------------------------------------------------------------------------------- /examples/permissions-boundary/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | alias = "terraform_role" 3 | region = local.aws_region 4 | assume_role { 5 | role_arn = data.terraform_remote_state.iam.outputs.role 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modules/runners/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/webhook/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/base/templates/resource-group.json: -------------------------------------------------------------------------------- 1 | { 2 | "ResourceTypeFilters": ["AWS::AllSupported"], 3 | "TagFilters": [ 4 | { 5 | "Key": "Example", 6 | "Values": ["${example}"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/prebuilt/outputs.tf: -------------------------------------------------------------------------------- 1 | output "webhook_endpoint" { 2 | value = module.runners.webhook.endpoint 3 | } 4 | 5 | output "webhook_secret" { 6 | sensitive = true 7 | value = random_id.random.hex 8 | } 9 | 10 | -------------------------------------------------------------------------------- /modules/runners/pool/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.14.1" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | } 8 | required_version = ">= 1.3.0" 9 | } 10 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/runners/lambdas/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # production 5 | dist/ 6 | build/ 7 | 8 | # misc 9 | .DS_Store 10 | .env* 11 | *.zip 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | -------------------------------------------------------------------------------- /modules/multi-runner/ssm.tf: -------------------------------------------------------------------------------- 1 | module "ssm" { 2 | source = "../ssm" 3 | 4 | kms_key_arn = var.kms_key_arn 5 | path_prefix = "${local.ssm_root_path}/${var.ssm_paths.app}" 6 | github_app = var.github_app 7 | tags = local.tags 8 | } 9 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # production 5 | dist/ 6 | build/ 7 | 8 | # misc 9 | .DS_Store 10 | .env* 11 | *.zip 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | -------------------------------------------------------------------------------- /modules/runners/pool/outputs.tf: -------------------------------------------------------------------------------- 1 | output "role_pool" { 2 | value = aws_iam_role.pool 3 | } 4 | 5 | output "lambda" { 6 | value = aws_lambda_function.pool 7 | } 8 | 9 | output "lambda_log_group" { 10 | value = aws_cloudwatch_log_group.pool 11 | } 12 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/local-pool.ts: -------------------------------------------------------------------------------- 1 | import { adjust } from './pool/pool'; 2 | 3 | export function run(): void { 4 | adjust({ poolSize: 1 }) 5 | .then() 6 | .catch((e) => { 7 | console.log(e); 8 | }); 9 | } 10 | 11 | run(); 12 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/local-down.ts: -------------------------------------------------------------------------------- 1 | import { scaleDown } from './scale-runners/scale-down'; 2 | 3 | export function run(): void { 4 | scaleDown() 5 | .then() 6 | .catch((e) => { 7 | console.log(e); 8 | }); 9 | } 10 | 11 | run(); 12 | -------------------------------------------------------------------------------- /examples/base/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_resourcegroups_group" "resourcegroups_group" { 2 | name = "${var.prefix}-group" 3 | resource_query { 4 | query = templatefile("${path.module}/templates/resource-group.json", { 5 | example = var.prefix 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # production 5 | dist/ 6 | build/ 7 | 8 | # misc 9 | .DS_Store 10 | .env* 11 | *.zip 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | -------------------------------------------------------------------------------- /policies/lambda-publish-sqs-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["sqs:SendMessage", "sqs:GetQueueAttributes"], 7 | "Resource": ${sqs_resource_arn} 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/arm64/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "git::https://github.com/philips-software/terraform-aws-vpc.git?ref=2.2.0" 3 | 4 | environment = local.environment 5 | aws_region = local.aws_region 6 | create_private_hosted_zone = false 7 | } 8 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-describe-tags-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "ec2:DescribeTags", 7 | "Resource": "*" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /modules/runners/policies/lambda-cloudwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["logs:CreateLogStream", "logs:PutLogEvents"], 7 | "Resource": "${log_group_arn}*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /modules/webhook/policies/lambda-cloudwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["logs:CreateLogStream", "logs:PutLogEvents"], 7 | "Resource": "${log_group_arn}*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/webhook/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | ENVIRONMENT: string; 4 | PARAMETER_GITHUB_APP_WEBHOOK_SECRET: string; 5 | REPOSITORY_WHITE_LIST: string; 6 | RUNNER_LABELS: string; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/default/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | 10 | variable "environment" { 11 | type = string 12 | default = null 13 | } 14 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/policies/lambda-kms.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["kms:GenerateDataKey", "kms:Decrypt"], 7 | "Resource": "${kms_key_arn}" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /modules/webhook/policies/lambda-publish-sqs-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["sqs:SendMessage", "sqs:GetQueueAttributes"], 7 | "Resource": ${sqs_resource_arns} 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/base/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "Prefix used for resource naming." 3 | type = string 4 | } 5 | 6 | variable "aws_region" { 7 | description = "AWS region to create the VPC, assuming zones `a` and `b` exists." 8 | type = string 9 | } 10 | -------------------------------------------------------------------------------- /examples/multi-runner/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | 10 | variable "environment" { 11 | type = string 12 | default = null 13 | } 14 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/policies/lambda-cloudwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["logs:CreateLogStream", "logs:PutLogEvents"], 7 | "Resource": "${log_group_arn}*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/scale-runners/ScaleError.ts: -------------------------------------------------------------------------------- 1 | class ScaleError extends Error { 2 | constructor(public message: string) { 3 | super(message); 4 | this.name = 'ScaleError'; 5 | this.stack = new Error().stack; 6 | } 7 | } 8 | 9 | export default ScaleError; 10 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-s3-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "githubActionDist", 6 | "Effect": "Allow", 7 | "Action": ["s3:GetObject", "s3:GetObjectAcl"], 8 | "Resource": ["${s3_arn}"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /modules/runners/templates/cloudwatch_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "agent": { 3 | "metrics_collection_interval": 5 4 | }, 5 | "logs": { 6 | "logs_collected": { 7 | "files": { 8 | "collect_list": ${logfiles} 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.41" 8 | } 9 | random = { 10 | source = "hashicorp/random" 11 | version = "~> 3.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | tab_width = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "semi": true, 6 | "importOrderSeparation": true, 7 | "importOrderSortSpecifiers": true, 8 | "importOrder": [ 9 | "", 10 | "^[./]" 11 | ] 12 | } -------------------------------------------------------------------------------- /modules/runners/policies/service-linked-role-create-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "iam:CreateServiceLinkedRole", 7 | "Resource": "arn:${aws_partition}:iam::*:role/aws-service-role/*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "semi": true, 6 | "importOrderSeparation": true, 7 | "importOrderSortSpecifiers": true, 8 | "importOrder": [ 9 | "", 10 | "^[./]" 11 | ] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # state 2 | *.tfstate* 3 | 4 | # Module directory 5 | .terraform/ 6 | 7 | # keys 8 | *id_rsa* 9 | 10 | # other 11 | node_modules/ 12 | .idea 13 | .DS_Store 14 | *.out 15 | secrets.auto.tfvars 16 | .envrc 17 | *.zip 18 | *.gz 19 | *.tgz 20 | *.env* 21 | .vscode 22 | 23 | **/coverage/* 24 | 25 | node_modules/ -------------------------------------------------------------------------------- /modules/download-lambda/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | null = { 10 | source = "hashicorp/null" 11 | version = "~> 3.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/multi-runner/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | random = { 10 | source = "hashicorp/random" 11 | version = "~> 3.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-role-trust-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "Service": "ec2.amazonaws.com" 9 | }, 10 | "Action": "sts:AssumeRole" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/permissions-boundary/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook" { 8 | value = { 9 | secret = random_id.random.hex 10 | endpoint = module.runners.webhook.endpoint 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /images/install-runner.ps1: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | user_name=ec2-user 4 | 5 | ## This wrapper file re-uses scripts in the /modules/runners/templates directory 6 | ## of this repo. These are the same that are used by the user_data functionality 7 | ## to bootstrap the instance if it is started from an existing AMI. 8 | ${install_runner} -------------------------------------------------------------------------------- /.ci/build.ps1: -------------------------------------------------------------------------------- 1 | $TOP_DIR=$(git rev-parse --show-toplevel) 2 | $OUTPUT_DIR="$TOP_DIR/lambda_output" 3 | 4 | New-Item "$OUTPUT_DIR" -ItemType Directory -ErrorAction SilentlyContinue 5 | 6 | $env:DOCKER_BUILDKIT=1 7 | docker build --no-cache --target=final --output=type=local,dest="$OUTPUT_DIR" -f "$TOP_DIR/.ci/Dockerfile" "$TOP_DIR" 8 | 9 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "semi": true, 6 | "importOrderSeparation": true, 7 | "importOrderSortSpecifiers": true, 8 | "importOrder": [ 9 | "", 10 | "^[./]" 11 | ] 12 | } -------------------------------------------------------------------------------- /images/install-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | user_name=$(cat /tmp/install-user.txt) 4 | 5 | ## This wrapper file re-uses scripts in the /modules/runners/templates directory 6 | ## of this repo. These are the same that are used by the user_data functionality 7 | ## to bootstrap the instance if it is started from an existing AMI. 8 | ${install_runner} -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/local.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './logger'; 2 | import { sync } from './syncer/syncer'; 3 | 4 | sync() 5 | .then() 6 | .catch((e) => { 7 | if (e instanceof Error) { 8 | logger.error(e.message); 9 | } 10 | logger.debug('Ignoring error', { error: e }); 11 | }); 12 | -------------------------------------------------------------------------------- /modules/runners/policies/lambda-vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:CreateNetworkInterface", 8 | "ec2:DescribeNetworkInterfaces", 9 | "ec2:DeleteNetworkInterface" 10 | ], 11 | "Resource": "*" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/arm64/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook_endpoint" { 8 | value = module.runners.webhook.endpoint 9 | } 10 | 11 | output "webhook_secret" { 12 | sensitive = true 13 | value = random_id.random.hex 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/ubuntu/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook_endpoint" { 8 | value = module.runners.webhook.endpoint 9 | } 10 | 11 | output "webhook_secret" { 12 | sensitive = true 13 | value = random_id.random.hex 14 | } 15 | 16 | -------------------------------------------------------------------------------- /modules/webhook/policies/lambda-orchestrator.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "sqs:ReceiveMessage", 8 | "sqs:DeleteMessage", 9 | "sqs:GetQueueAttributes" 10 | ], 11 | "Resource": "${sqs_webhook_event_arn}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/antonbabenko/pre-commit-terraform 3 | rev: v1.76.0 4 | hooks: 5 | - id: terraform_fmt 6 | - id: terraform_tflint 7 | - id: terraform_docs 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v4.3.0 10 | hooks: 11 | - id: check-merge-conflict 12 | -------------------------------------------------------------------------------- /examples/default/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook_endpoint" { 8 | value = module.runners.webhook.endpoint 9 | } 10 | 11 | output "webhook_secret" { 12 | sensitive = true 13 | value = random_id.random.hex 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/ephemeral/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook_endpoint" { 8 | value = module.runners.webhook.endpoint 9 | } 10 | 11 | output "webhook_secret" { 12 | sensitive = true 13 | value = random_id.random.hex 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/windows/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name 4 | } 5 | } 6 | 7 | output "webhook_endpoint" { 8 | value = module.runners.webhook.endpoint 9 | } 10 | 11 | output "webhook_secret" { 12 | sensitive = true 13 | value = random_id.random.hex 14 | } 15 | 16 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/aws/ssm.ts: -------------------------------------------------------------------------------- 1 | import { SSM } from '@aws-sdk/client-ssm'; 2 | 3 | export async function getParameterValue(parameter_name: string): Promise { 4 | const client = new SSM({ region: process.env.AWS_REGION }); 5 | return (await client.getParameter({ Name: parameter_name, WithDecryption: true })).Parameter?.Value as string; 6 | } 7 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/ssm/index.ts: -------------------------------------------------------------------------------- 1 | import { SSM } from '@aws-sdk/client-ssm'; 2 | 3 | export async function getParameterValue(parameter_name: string): Promise { 4 | const client = new SSM({ region: process.env.AWS_REGION }); 5 | return (await client.getParameter({ Name: parameter_name, WithDecryption: true })).Parameter?.Value as string; 6 | } 7 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/policies/lambda-vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:CreateNetworkInterface", 8 | "ec2:DescribeNetworkInterfaces", 9 | "ec2:DeleteNetworkInterface" 10 | ], 11 | "Resource": "*" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "editorconfig.editorconfig", 7 | "yzhang.markdown-all-in-one", 8 | "hashicorp.terraform" 9 | ] 10 | } -------------------------------------------------------------------------------- /modules/runners/policies/instance-ec2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "ec2:TerminateInstances", 7 | "Resource": "*", 8 | "Condition": { 9 | "StringEquals": { 10 | "aws:ARN": "$${ec2:SourceInstanceARN}" 11 | } 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/ubuntu/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | default_tags { 4 | tags = { 5 | Example = local.environment 6 | } 7 | } 8 | // If you use roles with specific permissions please add your role 9 | // assume_role { 10 | // role_arn = "arn:aws:iam::123456789012:role/MyAdminRole" 11 | // } 12 | } 13 | 14 | provider "random" { 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/lambda-runners.yml: -------------------------------------------------------------------------------- 1 | name: Lambda Runners 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - .github/workflows/lambda-runners.yml 9 | - "modules/runners/lambdas/runners/**" 10 | jobs: 11 | build: 12 | uses: ./.github/workflows/lambda-template.yml 13 | with: 14 | working-directory: modules/runners/lambdas/runners 15 | -------------------------------------------------------------------------------- /.github/workflows/lambda-webhook.yml: -------------------------------------------------------------------------------- 1 | name: Lambda Webhook 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - .github/workflows/lambda-webhook.yml 9 | - "modules/webhook/lambdas/webhook/**" 10 | jobs: 11 | build: 12 | uses: ./.github/workflows/lambda-template.yml 13 | with: 14 | working-directory: modules/webhook/lambdas/webhook 15 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/policies/lambda-syncer.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "s3:GetObjectTagging", 8 | "s3:GetObjectVersionTagging", 9 | "s3:PutObject", 10 | "s3:PutObjectTagging" 11 | ], 12 | "Resource": ["${s3_resource_arn}"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/arm64/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/default/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/prebuilt/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub for API usages." 3 | 4 | type = object({ 5 | id = string 6 | key_base64 = string 7 | }) 8 | } 9 | 10 | variable "runner_os" { 11 | type = string 12 | default = "linux" 13 | } 14 | 15 | variable "ami_name_filter" { 16 | type = string 17 | default = "github-runner-amzn2-x86_64-*" 18 | } 19 | -------------------------------------------------------------------------------- /examples/prebuilt/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/ubuntu/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/windows/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/ephemeral/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/multi-runner/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts', '!src/**/*.d.ts'], 6 | coverageThreshold: { 7 | global: { 8 | branches: 87, 9 | functions: 99, 10 | lines: 99, 11 | statements: 99 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts', 'src/**/*.d.ts'], 6 | coverageThreshold: { 7 | global: { 8 | branches: 92, 9 | functions: 92, 10 | lines: 92, 11 | statements: 92, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/permissions-boundary/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.0" 10 | } 11 | random = { 12 | source = "hashicorp/random" 13 | version = "~> 3.0" 14 | } 15 | } 16 | required_version = ">= 1.3.0" 17 | } 18 | -------------------------------------------------------------------------------- /images/start-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | exec > >(tee /var/log/runner-startup.log | logger -t user-data -s 2>/dev/console) 2>&1 3 | 4 | cd /opt/actions-runner 5 | 6 | ## This wrapper file re-uses scripts in the /modules/runners/templates directory 7 | ## of this repo. These are the same that are used by the user_data functionality 8 | ## to bootstrap the instance if it is started from an existing AMI. 9 | ${start_runner} 10 | -------------------------------------------------------------------------------- /modules/webhook/outputs.tf: -------------------------------------------------------------------------------- 1 | output "gateway" { 2 | value = aws_apigatewayv2_api.webhook 3 | } 4 | 5 | output "lambda" { 6 | value = aws_lambda_function.webhook 7 | } 8 | 9 | output "lambda_log_group" { 10 | value = aws_cloudwatch_log_group.webhook 11 | } 12 | 13 | output "role" { 14 | value = aws_iam_role.webhook_lambda 15 | } 16 | 17 | output "endpoint_relative_path" { 18 | value = local.webhook_endpoint 19 | } 20 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts', '!src/**/*.d.ts'], 6 | coverageThreshold: { 7 | global: { 8 | branches: 87, 9 | functions: 92, 10 | lines: 98, 11 | statements: 98 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/policies/assume-role-for-account.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { "AWS": "arn:${aws_partition}:iam::${account_id}:root" }, 7 | "Effect": "Allow", 8 | "Sid": "", 9 | "Condition": { 10 | "Bool": { 11 | "aws:MultiFactorAuthPresent": "true" 12 | } 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/outputs.tf: -------------------------------------------------------------------------------- 1 | output "bucket" { 2 | value = aws_s3_bucket.action_dist 3 | } 4 | 5 | output "runner_distribution_object_key" { 6 | value = local.action_runner_distribution_object_key 7 | } 8 | 9 | output "lambda" { 10 | value = aws_lambda_function.syncer 11 | } 12 | 13 | output "lambda_log_group" { 14 | value = aws_cloudwatch_log_group.syncer 15 | } 16 | 17 | output "lambda_role" { 18 | value = aws_iam_role.syncer_lambda 19 | } 20 | -------------------------------------------------------------------------------- /examples/lambdas-download/main.tf: -------------------------------------------------------------------------------- 1 | module "lambdas" { 2 | source = "../../modules/download-lambda" 3 | lambdas = [ 4 | { 5 | name = "webhook" 6 | tag = var.module_version 7 | }, 8 | { 9 | name = "runners" 10 | tag = var.module_version 11 | }, 12 | { 13 | name = "runner-binaries-syncer" 14 | tag = var.module_version 15 | } 16 | ] 17 | } 18 | 19 | output "files" { 20 | value = module.lambdas.files 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/lambda-runner-binaries-syncer.yml: -------------------------------------------------------------------------------- 1 | name: Lambda Syncer 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - .github/workflows/lambda-runner-binaries-syncer.yml 9 | - "modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/**" 10 | jobs: 11 | build: 12 | uses: ./.github/workflows/lambda-template.yml 13 | with: 14 | working-directory: modules/runner-binaries-syncer/lambdas/runner-binaries-syncer 15 | -------------------------------------------------------------------------------- /.ci/build-yarn.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -e 2 | 3 | # Build all the lambda's, output on the default place (inside the lambda module) 4 | 5 | lambdaSrcDirs=("modules/runner-binaries-syncer/lambdas/runner-binaries-syncer" "modules/runners/lambdas/runners" "modules/webhook/lambdas/webhook") 6 | repoRoot=$(dirname $(dirname $(realpath ${BASH_SOURCE[0]}))) 7 | 8 | for lambdaDir in ${lambdaSrcDirs[@]}; do 9 | cd "$repoRoot/${lambdaDir}" 10 | yarn && yarn run all && yarn run dist 11 | done 12 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | module "iam" { 4 | source = "../../../modules/setup-iam-permissions" 5 | 6 | environment = "boundaries" 7 | account_id = data.aws_caller_identity.current.account_id 8 | 9 | namespaces = { 10 | boundary_namespace = "boundaries" 11 | role_namespace = "runners" 12 | policy_namespace = "runners" 13 | instance_profile_namespace = "runners" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | #- google 6 | - eslint:recommended 7 | - plugin:@typescript-eslint/recommended 8 | parser: "@typescript-eslint/parser" 9 | parserOptions: 10 | ecmaVersion: 12 11 | sourceType: module 12 | plugins: 13 | - "@typescript-eslint" 14 | rules: 15 | semi: error 16 | max-len: 17 | - error 18 | - 120 19 | 20 | overrides: 21 | - files: 22 | - "*.ts" 23 | - "*.tsx" 24 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | #- google 6 | - eslint:recommended 7 | - plugin:@typescript-eslint/recommended 8 | parser: "@typescript-eslint/parser" 9 | parserOptions: 10 | ecmaVersion: 12 11 | sourceType: module 12 | plugins: 13 | - "@typescript-eslint" 14 | rules: 15 | semi: error 16 | max-len: 17 | - error 18 | - 120 19 | 20 | overrides: 21 | - files: 22 | - "*.ts" 23 | - "*.tsx" 24 | -------------------------------------------------------------------------------- /modules/download-lambda/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "download" { 2 | count = length(var.lambdas) 3 | 4 | triggers = { 5 | name = var.lambdas[count.index].name 6 | file = "${var.lambdas[count.index].name}.zip" 7 | tag = var.lambdas[count.index].tag 8 | } 9 | 10 | provisioner "local-exec" { 11 | command = "curl -o ${self.triggers.file} -fL https://github.com/philips-labs/terraform-aws-github-runner/releases/download/${self.triggers.tag}/${self.triggers.name}.zip" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/base/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "3.16.0" 4 | 5 | name = "${var.prefix}-vpc" 6 | cidr = "10.0.0.0/16" 7 | 8 | azs = ["${var.aws_region}a", "${var.aws_region}b"] 9 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] 10 | public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] 11 | 12 | enable_dns_hostnames = true 13 | enable_nat_gateway = true 14 | map_public_ip_on_launch = false 15 | single_nat_gateway = true 16 | } 17 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | #- google 6 | - eslint:recommended 7 | - plugin:@typescript-eslint/recommended 8 | parser: "@typescript-eslint/parser" 9 | parserOptions: 10 | ecmaVersion: 12 11 | sourceType: module 12 | plugins: 13 | - "@typescript-eslint" 14 | rules: 15 | semi: error 16 | max-len: 17 | - error 18 | - 120 19 | 20 | overrides: 21 | - files: 22 | - "*.ts" 23 | - "*.tsx" 24 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/policies/deploy-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "Services", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "iam:*", 9 | "s3:*", 10 | "ec2:*", 11 | "events:*", 12 | "lambda:*", 13 | "sqs:*", 14 | "ssm:*", 15 | "logs:*", 16 | "apigateway:*", 17 | "resource-groups:*", 18 | "kms:*" 19 | ], 20 | "Resource": "*" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/arm64/lambdas-download/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | version = "" 3 | } 4 | 5 | module "lambdas" { 6 | source = "../../../modules/download-lambda" 7 | lambdas = [ 8 | { 9 | name = "webhook" 10 | tag = local.version 11 | }, 12 | { 13 | name = "runners" 14 | tag = local.version 15 | }, 16 | { 17 | name = "runner-binaries-syncer" 18 | tag = local.version 19 | } 20 | ] 21 | } 22 | 23 | output "files" { 24 | value = module.lambdas.files 25 | } 26 | -------------------------------------------------------------------------------- /examples/windows/lambdas-download/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | version = "" 3 | } 4 | 5 | module "lambdas" { 6 | source = "../../../modules/download-lambda" 7 | lambdas = [ 8 | { 9 | name = "webhook" 10 | tag = local.version 11 | }, 12 | { 13 | name = "runners" 14 | tag = local.version 15 | }, 16 | { 17 | name = "runner-binaries-syncer" 18 | tag = local.version 19 | } 20 | ] 21 | } 22 | 23 | output "files" { 24 | value = module.lambdas.files 25 | } 26 | -------------------------------------------------------------------------------- /modules/webhook/policies/lambda-ssm.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ssm:GetParameter" 8 | ], 9 | "Resource": [ 10 | "${github_app_webhook_secret_arn}" 11 | ] 12 | %{ if kms_key_arn != "" ~} 13 | }, 14 | { 15 | "Effect": "Allow", 16 | "Action": [ 17 | "kms:Decrypt" 18 | ], 19 | "Resource": "${kms_key_arn}" 20 | %{ endif ~} 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /modules/ssm/outputs.tf: -------------------------------------------------------------------------------- 1 | output "parameters" { 2 | value = { 3 | github_app_id = { 4 | name = aws_ssm_parameter.github_app_id.name 5 | arn = aws_ssm_parameter.github_app_id.arn 6 | } 7 | github_app_key_base64 = { 8 | name = aws_ssm_parameter.github_app_key_base64.name 9 | arn = aws_ssm_parameter.github_app_key_base64.arn 10 | } 11 | github_app_webhook_secret = { 12 | name = aws_ssm_parameter.github_app_webhook_secret.name 13 | arn = aws_ssm_parameter.github_app_webhook_secret.arn 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/template.yaml: -------------------------------------------------------------------------------- 1 | # AWS SAM template for testing Lambda locally, we use Terraform as deployment framework 2 | Resources: 3 | Syncer: 4 | Type: AWS::Serverless::Function 5 | Properties: 6 | Runtime: nodejs18.x 7 | Handler: dist/index.handler 8 | MemorySize: 256 9 | Timeout: 300 10 | Environment: 11 | Variables: 12 | GITHUB_RUNNER_ARCHITECTURE: 13 | GITHUB_RUNNER_OS: 14 | LOG_LEVEL: 15 | S3_BUCKET_NAME: 16 | S3_OBJECT_KEY: 17 | -------------------------------------------------------------------------------- /modules/runners/templates/install-runner.ps1: -------------------------------------------------------------------------------- 1 | ## install the runner 2 | 3 | Write-Host "Creating actions-runner directory for the GH Action installtion" 4 | New-Item -ItemType Directory -Path C:\actions-runner ; Set-Location C:\actions-runner 5 | 6 | Write-Host "Downloading the GH Action runner from s3 bucket $s3_location" 7 | aws s3 cp ${S3_LOCATION_RUNNER_DISTRIBUTION} actions-runner.zip 8 | 9 | Write-Host "Un-zip action runner" 10 | Expand-Archive -Path actions-runner.zip -DestinationPath . 11 | 12 | Write-Host "Delete zip file" 13 | Remove-Item actions-runner.zip 14 | 15 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/scale-runners/cache.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | 3 | export type UnboxPromise = T extends Promise ? U : T; 4 | 5 | export type GhRunners = UnboxPromise>['data']['runners']; 6 | 7 | export class githubCache { 8 | static clients: Map = new Map(); 9 | static runners: Map = new Map(); 10 | 11 | public static reset(): void { 12 | githubCache.clients.clear(); 13 | githubCache.runners.clear(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/local.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import express from 'express'; 3 | 4 | import { handle } from './webhook/handler'; 5 | 6 | const app = express(); 7 | 8 | app.use(bodyParser.json()); 9 | 10 | app.post('/event_handler', (req, res) => { 11 | handle(req.headers, JSON.stringify(req.body)) 12 | .then((c) => res.status(c.statusCode).end()) 13 | .catch((e) => { 14 | console.log(e); 15 | res.status(404); 16 | }); 17 | }); 18 | 19 | app.listen(3000, (): void => { 20 | console.log('webhook app listening on port 3000!'); 21 | }); 22 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { logger, setContext } from './logger'; 4 | import { sync } from './syncer/syncer'; 5 | 6 | // eslint-disable-next-line 7 | export async function handler(event: any, context: Context): Promise { 8 | setContext(context, 'lambda.ts'); 9 | logger.logEventIfEnabled(event); 10 | 11 | try { 12 | await sync(); 13 | } catch (e) { 14 | if (e instanceof Error) { 15 | logger.warn(`Ignoring error: ${e.message}`); 16 | } 17 | logger.debug('Ignoring error', { error: e }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # NOTE: This build requires docker buildkit integration which was introduced 5 | # in Docker v19.03+ and at least 4GB of memory available to the 6 | # docker daemon 7 | 8 | set -eou pipefail 9 | 10 | TOP_DIR=$(git rev-parse --show-toplevel) 11 | OUTPUT_DIR=${OUTPUT_DIR:-${TOP_DIR}/lambda_output} 12 | 13 | mkdir -p "${OUTPUT_DIR}" 14 | 15 | ( 16 | set -x 17 | DOCKER_BUILDKIT=1 docker build \ 18 | --no-cache \ 19 | --target=final \ 20 | --output=type=local,dest="${OUTPUT_DIR}" \ 21 | -f "${TOP_DIR}/.ci/Dockerfile" \ 22 | "${TOP_DIR}" 23 | ) 24 | -------------------------------------------------------------------------------- /examples/ubuntu/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "3.11.2" 4 | 5 | name = "vpc-${local.environment}" 6 | cidr = "10.0.0.0/16" 7 | 8 | azs = ["${local.aws_region}a", "${local.aws_region}b", "${local.aws_region}c"] 9 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 10 | public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 11 | 12 | enable_dns_hostnames = true 13 | enable_nat_gateway = true 14 | map_public_ip_on_launch = false 15 | single_nat_gateway = true 16 | 17 | tags = { 18 | Environment = local.environment 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/multi-runner/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "3.11.2" 4 | 5 | name = "vpc-${local.environment}" 6 | cidr = "10.0.0.0/16" 7 | 8 | azs = ["${local.aws_region}a", "${local.aws_region}b", "${local.aws_region}c"] 9 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 10 | public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 11 | 12 | enable_dns_hostnames = true 13 | enable_nat_gateway = true 14 | map_public_ip_on_launch = false 15 | single_nat_gateway = true 16 | 17 | tags = { 18 | Environment = local.environment 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/permissions-boundary/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "3.11.2" 4 | 5 | name = "vpc-${local.environment}" 6 | cidr = "10.0.0.0/16" 7 | 8 | azs = ["${local.aws_region}a", "${local.aws_region}b", "${local.aws_region}c"] 9 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 10 | public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 11 | 12 | enable_dns_hostnames = true 13 | enable_nat_gateway = true 14 | map_public_ip_on_launch = false 15 | single_nat_gateway = true 16 | 17 | tags = { 18 | Environment = local.environment 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/test/resources/multi_runner_configurations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "ubuntu-queue-id", 4 | "arn": "queueARN", 5 | "fifo": false, 6 | "matcherConfig": { 7 | "labelMatchers": [[ 8 | "self-hosted", 9 | "linux", 10 | "x64", 11 | "ubuntu" 12 | ]], 13 | "exactMatch": true 14 | } 15 | }, 16 | { 17 | "id": "latest-queue-id", 18 | "arn": "queueARN", 19 | "fifo": false, 20 | "matcherConfig": { 21 | "labelMatchers": [[ 22 | "self-hosted", 23 | "linux", 24 | "x64", 25 | "latest" 26 | ]], 27 | "exactMatch": false 28 | } 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-ssm-parameters-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ssm:DeleteParameter", 8 | "ssm:GetParameters", 9 | "ssm:GetParameter" 10 | ], 11 | "Resource": "${arn_ssm_parameters_path_tokens}*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "ssm:GetParameter", 17 | "ssm:GetParameters", 18 | "ssm:GetParametersByPath" 19 | ], 20 | "Resource": [ 21 | "${arn_ssm_parameters_path_config}", 22 | "${arn_ssm_parameters_path_config}/*" 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/arm64/lambdas-download/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1 | 7 | 8 | ## Providers 9 | 10 | No providers. 11 | 12 | ## Modules 13 | 14 | | Name | Source | Version | 15 | |------|--------|---------| 16 | | [lambdas](#module\_lambdas) | ../../../modules/download-lambda | n/a | 17 | 18 | ## Resources 19 | 20 | No resources. 21 | 22 | ## Inputs 23 | 24 | No inputs. 25 | 26 | ## Outputs 27 | 28 | | Name | Description | 29 | |------|-------------| 30 | | [files](#output\_files) | n/a | 31 | -------------------------------------------------------------------------------- /examples/windows/lambdas-download/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1 | 7 | 8 | ## Providers 9 | 10 | No providers. 11 | 12 | ## Modules 13 | 14 | | Name | Source | Version | 15 | |------|--------|---------| 16 | | [lambdas](#module\_lambdas) | ../../../modules/download-lambda | n/a | 17 | 18 | ## Resources 19 | 20 | No resources. 21 | 22 | ## Inputs 23 | 24 | No inputs. 25 | 26 | ## Outputs 27 | 28 | | Name | Description | 29 | |------|-------------| 30 | | [files](#output\_files) | n/a | 31 | -------------------------------------------------------------------------------- /modules/ssm/ssm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "github_app_id" { 2 | name = "${var.path_prefix}/github_app_id" 3 | type = "SecureString" 4 | value = var.github_app.id 5 | key_id = local.kms_key_arn 6 | tags = var.tags 7 | } 8 | 9 | resource "aws_ssm_parameter" "github_app_key_base64" { 10 | name = "${var.path_prefix}/github_app_key_base64" 11 | type = "SecureString" 12 | value = var.github_app.key_base64 13 | key_id = local.kms_key_arn 14 | tags = var.tags 15 | } 16 | 17 | resource "aws_ssm_parameter" "github_app_webhook_secret" { 18 | name = "${var.path_prefix}/github_app_webhook_secret" 19 | type = "SecureString" 20 | value = var.github_app.webhook_secret 21 | key_id = local.kms_key_arn 22 | tags = var.tags 23 | } 24 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-cloudwatch-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "cloudwatch:PutMetricData", 8 | "ec2:DescribeVolumes", 9 | "ec2:DescribeTags", 10 | "logs:PutLogEvents", 11 | "logs:DescribeLogStreams", 12 | "logs:DescribeLogGroups", 13 | "logs:CreateLogStream" 14 | ], 15 | "Resource": "*" 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Action": [ 20 | "ssm:GetParameter" 21 | ], 22 | "Resource": "${ssm_parameter_arn}/*" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/workflows/update-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update docs 2 | on: 3 | push: 4 | branches: 5 | - release-please--branches--main 6 | permissions: read-all 7 | jobs: 8 | docs: 9 | # update docs after merge back to develop 10 | name: Auto update terraform docs 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout branch 16 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # ratchet:actions/checkout@v3 17 | - name: Generate TF docs 18 | uses: terraform-docs/gh-actions@f6d59f89a280fa0a3febf55ef68f146784b20ba0 # ratchet:terraform-docs/gh-actions@v1.0.0 19 | with: 20 | find-dir: . 21 | git-commit-message: "docs: auto update terraform docs" 22 | git-push: true 23 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, Context } from 'aws-lambda'; 2 | 3 | import { logger, setContext } from './logger'; 4 | import { handle } from './webhook/handler'; 5 | 6 | export interface Response { 7 | statusCode: number; 8 | body?: string; 9 | } 10 | export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise { 11 | setContext(context, 'lambda.ts'); 12 | logger.logEventIfEnabled(event); 13 | 14 | let result: Response; 15 | try { 16 | result = await handle(event.headers, event.body as string); 17 | } catch (e) { 18 | logger.error(`Failed to handle webhook event`, { error: e }); 19 | result = { 20 | statusCode: 500, 21 | body: 'Check the Lambda logs for the error details.', 22 | }; 23 | } 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/semantic-check.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic Check" 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | jobs: 12 | main: 13 | name: Semantic Commit Message Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # ratchet:actions/checkout@v3 17 | - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # ratchet:amannn/action-semantic-pull-request@v5 18 | name: Check PR for Semantic Commit Message 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | requireScope: false 23 | validateSingleCommit: true 24 | ignoreLabels: release merge 25 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/policies/boundary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "ServiceBoundaries", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "s3:*", 9 | "ec2:*", 10 | "lambda:*", 11 | "logs:*", 12 | "sqs:*", 13 | "resource-groups:*", 14 | "ssm:*", 15 | "ssmmessages:*", 16 | "ec2messages:*", 17 | "cloudwatch:*" 18 | ], 19 | "Resource": "*" 20 | }, 21 | { 22 | "Sid": "RoleInNamespace", 23 | "Effect": "Allow", 24 | "Action": ["iam:PassRole"], 25 | "Resource": "arn:${aws_partition}:iam::${account_id}:role/${role_namespace}/*" 26 | }, 27 | { 28 | "Sid": "Decrypt", 29 | "Effect": "Allow", 30 | "Action": ["kms:Decrypt"], 31 | "Resource": "*" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/lambda-template.yml: -------------------------------------------------------------------------------- 1 | name: Lambda Syncer 2 | on: 3 | workflow_call: 4 | inputs: 5 | working-directory: 6 | required: true 7 | type: string 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: [18] 15 | container: 16 | image: node:${{ matrix.node }} 17 | defaults: 18 | run: 19 | working-directory: ${{ inputs.working-directory }}/${{ inputs.image }} 20 | 21 | steps: 22 | - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0 23 | - name: Install dependencies 24 | run: yarn install 25 | - name: Run prettier 26 | run: yarn format-check 27 | - name: Run linter 28 | run: yarn lint 29 | - name: Run tests 30 | run: yarn test 31 | - name: Build distribution 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /modules/runners/templates/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1 4 | 5 | # AWS suggest to create a log for debug purpose based on https://aws.amazon.com/premiumsupport/knowledge-center/ec2-linux-log-user-data/ 6 | # As side effect all command, set +x disable debugging explicitly. 7 | # 8 | # An alternative for masking tokens could be: exec > >(sed 's/--token\ [^ ]* /--token\ *** /g' > /var/log/user-data.log) 2>&1 9 | 10 | set +x 11 | 12 | %{ if enable_debug_logging } 13 | set -x 14 | %{ endif } 15 | 16 | ${pre_install} 17 | 18 | yum update -y 19 | 20 | # Install docker 21 | amazon-linux-extras install docker 22 | service docker start 23 | usermod -a -G docker ec2-user 24 | 25 | yum install -y amazon-cloudwatch-agent curl jq git 26 | 27 | user_name=ec2-user 28 | 29 | ${install_runner} 30 | 31 | ${post_install} 32 | 33 | ${start_runner} 34 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/scale-runners/scale-down-config.ts: -------------------------------------------------------------------------------- 1 | import parser from 'cron-parser'; 2 | import moment from 'moment'; 3 | 4 | export type ScalingDownConfigList = ScalingDownConfig[]; 5 | export interface ScalingDownConfig { 6 | cron: string; 7 | idleCount: number; 8 | timeZone: string; 9 | } 10 | 11 | function inPeriod(period: ScalingDownConfig): boolean { 12 | const now = moment(new Date()); 13 | const expr = parser.parseExpression(period.cron, { 14 | tz: period.timeZone, 15 | }); 16 | const next = moment(expr.next().toDate()); 17 | return Math.abs(next.diff(now, 'seconds')) < 5; // we keep a range of 5 seconds 18 | } 19 | 20 | export function getIdleRunnerCount(scalingDownConfigs: ScalingDownConfigList): number { 21 | for (const scalingDownConfig of scalingDownConfigs) { 22 | if (inPeriod(scalingDownConfig)) { 23 | return scalingDownConfig.idleCount; 24 | } 25 | } 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /modules/runners/runner-config.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "runner_config_run_as" { 2 | name = "${var.ssm_paths.root}/${var.ssm_paths.config}/run_as" 3 | type = "String" 4 | value = var.runner_as_root ? "root" : var.runner_run_as 5 | tags = local.tags 6 | } 7 | 8 | resource "aws_ssm_parameter" "runner_agent_mode" { 9 | name = "${var.ssm_paths.root}/${var.ssm_paths.config}/agent_mode" 10 | type = "String" 11 | value = var.enable_ephemeral_runners ? "ephemeral" : "persistent" 12 | tags = local.tags 13 | } 14 | 15 | resource "aws_ssm_parameter" "runner_enable_cloudwatch" { 16 | name = "${var.ssm_paths.root}/${var.ssm_paths.config}/enable_cloudwatch" 17 | type = "String" 18 | value = var.enable_cloudwatch_agent 19 | tags = local.tags 20 | } 21 | 22 | resource "aws_ssm_parameter" "token_path" { 23 | name = "${var.ssm_paths.root}/${var.ssm_paths.config}/token_path" 24 | type = "String" 25 | value = "${var.ssm_paths.root}/${var.ssm_paths.tokens}" 26 | tags = local.tags 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve dependabot 2 | # Warning: The pull_request_target event is granted a read/write repository 3 | # token and can access secrets, even when it is triggered from a fork. Although 4 | # the workflow runs in the context of the base of the pull request, you should 5 | # make sure that you do not check out, build, or run untrusted code from the 6 | # pull request with this event. Additionally, any caches share the same scope as 7 | # the base branch, and to help prevent cache poisoning, you should not save the 8 | # cache if there is a possibility that the cache contents were altered. 9 | on: pull_request_target 10 | jobs: 11 | approve: 12 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: hmarr/auto-approve-action@44888193675f29a83e04faf4002fa8c0b537b1e4 # ratchet:hmarr/auto-approve-action@v3.2.1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | -------------------------------------------------------------------------------- /modules/multi-runner/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | tags = merge(var.tags, { 3 | "ghr:environment" = var.prefix 4 | }) 5 | 6 | github_app_parameters = { 7 | id = module.ssm.parameters.github_app_id 8 | key_base64 = module.ssm.parameters.github_app_key_base64 9 | } 10 | 11 | runner_config = { for k, v in var.multi_runner_config : k => merge({ id = aws_sqs_queue.queued_builds[k].id, arn = aws_sqs_queue.queued_builds[k].arn }, v) } 12 | 13 | tmp_distinct_list_unique_os_and_arch = distinct([for i, config in local.runner_config : { "os_type" : config.runner_config.runner_os, "architecture" : config.runner_config.runner_architecture } if config.runner_config.enable_runner_binaries_syncer]) 14 | unique_os_and_arch = { for i, v in local.tmp_distinct_list_unique_os_and_arch : "${v.os_type}_${v.architecture}" => v } 15 | 16 | ssm_root_path = "/${var.ssm_paths.root}/${var.prefix}" 17 | } 18 | 19 | resource "random_string" "random" { 20 | length = 24 21 | special = false 22 | upper = false 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale issue and PR workflow" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | workflow_dispatch: 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # ratchet:actions/stale@v7 14 | with: 15 | stale-issue-message: > 16 | This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed if no further activity occurs. Thank you for your contributions. 17 | 18 | stale-pr-message: > 19 | This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed if no further activity occurs. Thank you for your contributions. 20 | 21 | days-before-stale: 30 22 | days-before-close: 10 23 | close-issue-label: "abandoned" 24 | exempt-issue-labels: "stale:exempt" 25 | -------------------------------------------------------------------------------- /modules/download-lambda/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/null" { 5 | version = "3.0.0" 6 | hashes = [ 7 | "h1:ysHGBhBNkIiJLEpthB/IVCLpA1Qoncp3KbCTFGFZTO0=", 8 | "zh:05fb7eab469324c97e9b73a61d2ece6f91de4e9b493e573bfeda0f2077bc3a4c", 9 | "zh:1688aa91885a395c4ae67636d411475d0b831e422e005dcf02eedacaafac3bb4", 10 | "zh:24a0b1292e3a474f57c483a7a4512d797e041bc9c2fbaac42fe12e86a7fb5a3c", 11 | "zh:2fc951bd0d1b9b23427acc93be09b6909d72871e464088171da60fbee4fdde03", 12 | "zh:6db825759425599a326385a68acc6be2d9ba0d7d6ef587191d0cdc6daef9ac63", 13 | "zh:85985763d02618993c32c294072cc6ec51f1692b803cb506fcfedca9d40eaec9", 14 | "zh:a53186599c57058be1509f904da512342cfdc5d808efdaf02dec15f0f3cb039a", 15 | "zh:c2e07b49b6efa676bdc7b00c06333ea1792a983a5720f9e2233db27323d2707c", 16 | "zh:cdc8fe1096103cf5374751e2e8408ec4abd2eb67d5a1c5151fe2c7ecfd525bef", 17 | "zh:dbdef21df0c012b0d08776f3d4f34eb0f2f229adfde07ff252a119e52c0f65b7", 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright © 2020 Koninklijke Philips N.V, https://www.philips.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /modules/runners/policies-lambda-common.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "lambda_assume_role_policy" { 2 | statement { 3 | actions = ["sts:AssumeRole"] 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = ["lambda.amazonaws.com"] 8 | } 9 | } 10 | } 11 | 12 | resource "aws_iam_policy" "ami_id_ssm_parameter_read" { 13 | count = var.ami_id_ssm_parameter_name != null ? 1 : 0 14 | name = "${var.prefix}-ami-id-ssm-parameter-read" 15 | path = local.role_path 16 | description = "Allows for reading ${var.prefix} GitHub runner AMI ID from an SSM parameter" 17 | policy = <<-JSON 18 | { 19 | "Version": "2012-10-17", 20 | "Statement": [ 21 | { 22 | "Effect": "Allow", 23 | "Action": [ 24 | "ssm:GetParameter" 25 | ], 26 | "Resource": [ 27 | "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${trimprefix(var.ami_id_ssm_parameter_name, "/")}" 28 | ] 29 | } 30 | ] 31 | } 32 | JSON 33 | } 34 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | AWS_REGION: string; 4 | ENVIRONMENT: string; 5 | GHES_URL: string; 6 | LAUNCH_TEMPLATE_NAME: string; 7 | LOG_LEVEL: 'silly' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; 8 | LOG_TYPE: 'json' | 'pretty' | 'hidden'; 9 | MINIMUM_RUNNING_TIME_IN_MINUTES: string; 10 | PARAMETER_GITHUB_APP_CLIENT_ID_NAME: string; 11 | PARAMETER_GITHUB_APP_CLIENT_SECRET_NAME: string; 12 | PARAMETER_GITHUB_APP_ID_NAME: string; 13 | PARAMETER_GITHUB_APP_KEY_BASE64_NAME: string; 14 | RUNNER_OWNER: string; 15 | SCALE_DOWN_CONFIG: string; 16 | SSM_TOKEN_PATH: string; 17 | SUBNET_IDS: string; 18 | INSTANCE_TYPES: string; 19 | INSTANCE_TARGET_CAPACITY_TYPE: 'on-demand' | 'spot'; 20 | INSTANCE_MAX_SPOT_PRICE: string | undefined; 21 | INSTANCE_ALLOCATION_STRATEGY: 22 | | 'lowest-price' 23 | | 'price-capacity-optimized' 24 | | 'diversified' 25 | | 'capacity-optimized' 26 | | 'capacity-optimized-prioritized'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.ci/Dockerfile: -------------------------------------------------------------------------------- 1 | #syntax=docker/dockerfile:1.2 2 | FROM node:16 as build 3 | WORKDIR /lambda 4 | RUN apt-get update \ 5 | && apt-get install -y zip \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | FROM build as runner-binaries-syncer 9 | COPY modules/runner-binaries-syncer/lambdas/runner-binaries-syncer /lambda 10 | RUN --mount=type=cache,target=/lambda/node_modules,id=runner-binaries-syncer \ 11 | yarn install && yarn dist 12 | 13 | FROM build as runners 14 | COPY modules/runners/lambdas/runners /lambda 15 | RUN --mount=type=cache,target=/lambda/node_modules,id=runners \ 16 | yarn install && yarn dist 17 | 18 | FROM build as webhook 19 | COPY modules/webhook/lambdas/webhook /lambda 20 | RUN --mount=type=cache,target=/lambda/node_modules,id=webhook \ 21 | yarn install && yarn dist 22 | 23 | FROM scratch as final 24 | COPY --from=runner-binaries-syncer /lambda/runner-binaries-syncer.zip /runner-binaries-syncer.zip 25 | COPY --from=runners /lambda/runners.zip /runners.zip 26 | COPY --from=webhook /lambda/webhook.zip /webhook.zip 27 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/logger/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { logger, setContext } from '.'; 4 | 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | const context: Context = { 11 | awsRequestId: '1', 12 | callbackWaitsForEmptyEventLoop: false, 13 | functionName: 'unit-test', 14 | functionVersion: '', 15 | getRemainingTimeInMillis: () => 0, 16 | invokedFunctionArn: '', 17 | logGroupName: '', 18 | logStreamName: '', 19 | memoryLimitInMB: '', 20 | done: () => { 21 | return; 22 | }, 23 | fail: () => { 24 | return; 25 | }, 26 | succeed: () => { 27 | return; 28 | }, 29 | }; 30 | 31 | describe('A root logger.', () => { 32 | test('Should log set context.', async () => { 33 | setContext(context, 'unit-test'); 34 | 35 | expect(logger.getPersistentLogAttributes()).toEqual( 36 | expect.objectContaining({ 37 | 'aws-request-id': context.awsRequestId, 38 | 'function-name': context.functionName, 39 | module: 'unit-test', 40 | }), 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/logger/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { logger, setContext } from '.'; 4 | 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | const context: Context = { 11 | awsRequestId: '1', 12 | callbackWaitsForEmptyEventLoop: false, 13 | functionName: 'unit-test', 14 | functionVersion: '', 15 | getRemainingTimeInMillis: () => 0, 16 | invokedFunctionArn: '', 17 | logGroupName: '', 18 | logStreamName: '', 19 | memoryLimitInMB: '', 20 | done: () => { 21 | return; 22 | }, 23 | fail: () => { 24 | return; 25 | }, 26 | succeed: () => { 27 | return; 28 | }, 29 | }; 30 | 31 | describe('A root logger.', () => { 32 | test('Should log set context.', async () => { 33 | setContext(context, 'unit-test'); 34 | 35 | expect(logger.getPersistentLogAttributes()).toEqual( 36 | expect.objectContaining({ 37 | 'aws-request-id': context.awsRequestId, 38 | 'function-name': context.functionName, 39 | module: 'unit-test', 40 | }), 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 7 | | [aws](#requirement\_aws) | ~> 4.0 | 8 | 9 | ## Providers 10 | 11 | | Name | Version | 12 | |------|---------| 13 | | [aws](#provider\_aws) | 4.12.1 | 14 | 15 | ## Modules 16 | 17 | | Name | Source | Version | 18 | |------|--------|---------| 19 | | [iam](#module\_iam) | ../../../modules/setup-iam-permissions | n/a | 20 | 21 | ## Resources 22 | 23 | | Name | Type | 24 | |------|------| 25 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 26 | 27 | ## Inputs 28 | 29 | No inputs. 30 | 31 | ## Outputs 32 | 33 | | Name | Description | 34 | |------|-------------| 35 | | [boundary](#output\_boundary) | n/a | 36 | | [role](#output\_role) | n/a | 37 | -------------------------------------------------------------------------------- /modules/webhook/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | buffer-from@^1.0.0: 6 | version "1.1.2" 7 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 8 | integrity sha1-KxRqb9cugLT1XSVfNe1Zo6mkG9U= 9 | 10 | source-map-support@^0.5.19: 11 | version "0.5.20" 12 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" 13 | integrity sha1-EhZgifj15ejFaSazd2Mzkt0stsk= 14 | dependencies: 15 | buffer-from "^1.0.0" 16 | source-map "^0.6.0" 17 | 18 | source-map@^0.6.0: 19 | version "0.6.1" 20 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 21 | integrity sha1-dHIq8y6WFOnCh6jQu95IteLxomM= 22 | 23 | tslog@^3.2.2: 24 | version "3.2.2" 25 | resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.2.tgz#5bbaa1fab685c4273e59b38064227321a69a0694" 26 | integrity sha1-W7qh+raFxCc+WbOAZCJzIaaaBpQ= 27 | dependencies: 28 | source-map-support "^0.5.19" 29 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/logger/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { logger, setContext } from '.'; 4 | 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | const context: Context = { 11 | awsRequestId: '1', 12 | callbackWaitsForEmptyEventLoop: false, 13 | functionName: 'unit-test', 14 | functionVersion: '', 15 | getRemainingTimeInMillis: () => 0, 16 | invokedFunctionArn: '', 17 | logGroupName: '', 18 | logStreamName: '', 19 | memoryLimitInMB: '', 20 | done: () => { 21 | return; 22 | }, 23 | fail: () => { 24 | return; 25 | }, 26 | succeed: () => { 27 | return; 28 | }, 29 | }; 30 | 31 | describe('A root logger.', () => { 32 | test('Should log set context.', async () => { 33 | setContext(context, 'unit-test'); 34 | 35 | expect(logger.getPersistentLogAttributes()).toEqual( 36 | expect.objectContaining({ 37 | 'aws-request-id': context.awsRequestId, 38 | 'function-name': context.functionName, 39 | module: 'unit-test', 40 | }), 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /.github/workflows/packer-build.yml: -------------------------------------------------------------------------------- 1 | name: "Packer checks" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - "images/**" 9 | - ".github/workflows/packer-build.yml" 10 | - "module/runners/templates/**" 11 | env: 12 | AWS_REGION: eu-west-1 13 | jobs: 14 | verify_packer: 15 | name: Verify packer 16 | runs-on: ubuntu-latest 17 | container: 18 | image: index.docker.io/hashicorp/packer@sha256:f795aace438ef92e738228c21d5ceb7d5dd73ceb7e0b1efab5b0e90cbc4d4dcd # ratchet:hashicorp/packer:1.7.8 19 | strategy: 20 | matrix: 21 | image: ["linux-amzn2", "windows-core-2019", "windows-core-2022", "ubuntu-focal", "ubuntu-jammy", "ubuntu-jammy-arm64"] 22 | defaults: 23 | run: 24 | working-directory: images/${{ matrix.image }} 25 | steps: 26 | - name: "Checkout" 27 | uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # ratchet:actions/checkout@v3 28 | - name: packer init 29 | run: packer init . 30 | - name: check packer formatting 31 | run: packer fmt -recursive -check=true . 32 | - name: packer validate 33 | run: packer validate . 34 | -------------------------------------------------------------------------------- /examples/permissions-boundary/setup/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.12.1" 6 | constraints = "~> 4.0" 7 | hashes = [ 8 | "h1:YvwxXRDVzn9j6Gt7Vg8tCcyF/niapue5sxSUw1TH+9U=", 9 | "zh:2b432dc3bf7e0987bf9dcad5d397c384890d12fcd95827bc4581ca2955fc623a", 10 | "zh:2f79a448a4e5ad24a706ab634078d0ef159be3278eb24988b7d2185173f5dd8f", 11 | "zh:5d70074c10cefb30d4104af54f912e58ffa1b6871277b0a5324c8f13000f5009", 12 | "zh:63623743fb15d54787a96c9761b97a935ff396672e625730cb7a5c1971acf4b6", 13 | "zh:8263f376e6db684667c10e28df8d8d188e02fd09ad58e1ad7075e363c389e24c", 14 | "zh:8b5aa9fd1ddf1de0ab7d462891123405e5af04d7e4d1e4b03381634b3cae4884", 15 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 16 | "zh:d00b2d0b374ab92e934eb597668c5f3e415c4cf8335e6a52ab99949b8fcf57dd", 17 | "zh:d0e037725aced6cacc2e0a1903b31083c64f8765fb1263e4f8f891745266b7fb", 18 | "zh:e6e244123bc1df109db90bef0af2a875a0b3afb268f21c3e5bc34753657102ad", 19 | "zh:ec6901ab8b99ae3df50340e9aa86ed3bac1369f5e1403c0362edd9944640fa22", 20 | "zh:f6a4d0ce3bd3d4b81163c4ae75b66e50c10b935c60a63d7fb96df285c0eeca40", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/lambda.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | import { mocked } from 'jest-mock'; 3 | 4 | import { handler } from './lambda'; 5 | import { sync } from './syncer/syncer'; 6 | 7 | jest.mock('./syncer/syncer'); 8 | 9 | const context: Context = { 10 | awsRequestId: '1', 11 | callbackWaitsForEmptyEventLoop: false, 12 | functionName: 'unit-test', 13 | functionVersion: '', 14 | getRemainingTimeInMillis: () => 0, 15 | invokedFunctionArn: '', 16 | logGroupName: '', 17 | logStreamName: '', 18 | memoryLimitInMB: '', 19 | done: () => { 20 | return; 21 | }, 22 | fail: () => { 23 | return; 24 | }, 25 | succeed: () => { 26 | return; 27 | }, 28 | }; 29 | 30 | describe('Test download sync wrapper.', () => { 31 | it('Test successful download.', async () => { 32 | const mock = mocked(sync); 33 | mock.mockImplementation(() => { 34 | return new Promise((resolve) => { 35 | resolve(); 36 | }); 37 | }); 38 | await expect(handler({}, context)).resolves; 39 | }); 40 | 41 | it('Test wrapper with returning an error. ', async () => { 42 | const mock = mocked(sync); 43 | mock.mockRejectedValue(new Error('')); 44 | 45 | await expect(handler({}, context)).resolves; 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger'; 2 | import { Context } from 'aws-lambda'; 3 | 4 | const childLoggers: Logger[] = []; 5 | 6 | const defaultValues = { 7 | region: process.env.AWS_REGION || 'N/A', 8 | environment: process.env.ENVIRONMENT || 'N/A', 9 | }; 10 | 11 | function setContext(context: Context, module?: string) { 12 | logger.addPersistentLogAttributes({ 13 | 'aws-request-id': context.awsRequestId, 14 | 'function-name': context.functionName, 15 | module: module, 16 | }); 17 | 18 | // Add the context to all child loggers 19 | childLoggers.forEach((childLogger) => { 20 | childLogger.addPersistentLogAttributes({ 21 | 'aws-request-id': context.awsRequestId, 22 | 'function-name': context.functionName, 23 | }); 24 | }); 25 | } 26 | 27 | const logger = new Logger({ 28 | serviceName: process.env.SERVICE_NAME || 'webhook', 29 | persistentLogAttributes: { 30 | ...defaultValues, 31 | }, 32 | }); 33 | 34 | function createChildLogger(module: string): Logger { 35 | const childLogger = logger.createChild({ 36 | persistentLogAttributes: { 37 | module: module, 38 | }, 39 | }); 40 | 41 | childLoggers.push(childLogger); 42 | return childLogger; 43 | } 44 | 45 | export { createChildLogger, logger, setContext }; 46 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger'; 2 | import { Context } from 'aws-lambda'; 3 | 4 | const childLoggers: Logger[] = []; 5 | 6 | const defaultValues = { 7 | region: process.env.AWS_REGION || 'N/A', 8 | environment: process.env.ENVIRONMENT || 'N/A', 9 | }; 10 | 11 | function setContext(context: Context, module?: string) { 12 | logger.addPersistentLogAttributes({ 13 | 'aws-request-id': context.awsRequestId, 14 | 'function-name': context.functionName, 15 | module: module, 16 | }); 17 | 18 | // Add the context to all child loggers 19 | childLoggers.forEach((childLogger) => { 20 | childLogger.addPersistentLogAttributes({ 21 | 'aws-request-id': context.awsRequestId, 22 | 'function-name': context.functionName, 23 | }); 24 | }); 25 | } 26 | 27 | const logger = new Logger({ 28 | serviceName: process.env.SERVICE_NAME || 'syncer', 29 | persistentLogAttributes: { 30 | ...defaultValues, 31 | }, 32 | }); 33 | 34 | function createChildLogger(module: string): Logger { 35 | const childLogger = logger.createChild({ 36 | persistentLogAttributes: { 37 | module: module, 38 | }, 39 | }); 40 | 41 | childLoggers.push(childLogger); 42 | return childLogger; 43 | } 44 | 45 | export { createChildLogger, logger, setContext }; 46 | -------------------------------------------------------------------------------- /modules/ssm/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_app" { 2 | description = "GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)." 3 | type = object({ 4 | key_base64 = string 5 | id = string 6 | webhook_secret = string 7 | }) 8 | } 9 | 10 | variable "environment" { 11 | description = "A name that identifies the environment, used as prefix and for tagging." 12 | type = string 13 | default = null 14 | 15 | validation { 16 | condition = var.environment == null 17 | error_message = "The \"environment\" variable is no longer used. To migrate, set the \"prefix\" variable to the original value of \"environment\" and optionally, add \"Environment\" to the \"tags\" variable map with the same value." 18 | } 19 | } 20 | 21 | variable "path_prefix" { 22 | description = "The path prefix used for naming resources" 23 | type = string 24 | } 25 | 26 | variable "kms_key_arn" { 27 | description = "Optional CMK Key ARN to be used for Parameter Store." 28 | type = string 29 | default = null 30 | } 31 | 32 | variable "tags" { 33 | description = "Map of tags that will be added to created resources. By default resources will be tagged with name and environment." 34 | type = map(string) 35 | default = {} 36 | } 37 | -------------------------------------------------------------------------------- /examples/base/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1 | 7 | | [aws](#requirement\_aws) | ~> 4.0 | 8 | 9 | ## Providers 10 | 11 | | Name | Version | 12 | |------|---------| 13 | | [aws](#provider\_aws) | ~> 4.0 | 14 | 15 | ## Modules 16 | 17 | | Name | Source | Version | 18 | |------|--------|---------| 19 | | [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | 3.16.0 | 20 | 21 | ## Resources 22 | 23 | | Name | Type | 24 | |------|------| 25 | | [aws_resourcegroups_group.resourcegroups_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/resourcegroups_group) | resource | 26 | 27 | ## Inputs 28 | 29 | | Name | Description | Type | Default | Required | 30 | |------|-------------|------|---------|:--------:| 31 | | [aws\_region](#input\_aws\_region) | AWS region to create the VPC, assuming zones `a` and `b` exists. | `string` | n/a | yes | 32 | | [prefix](#input\_prefix) | Prefix used for resource naming. | `string` | n/a | yes | 33 | 34 | ## Outputs 35 | 36 | | Name | Description | 37 | |------|-------------| 38 | | [vpc](#output\_vpc) | n/a | 39 | -------------------------------------------------------------------------------- /modules/runners/policies/lambda-scale-down.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:DescribeInstances", 8 | "ec2:DescribeTags" 9 | ], 10 | "Resource": [ 11 | "*" 12 | ] 13 | }, 14 | { 15 | "Effect": "Allow", 16 | "Action": [ 17 | "ec2:TerminateInstances" 18 | ], 19 | "Resource": [ 20 | "*" 21 | ], 22 | "Condition": { 23 | "StringEquals": { 24 | "ec2:ResourceTag/ghr:Application": "github-action-runner" 25 | } 26 | } 27 | }, 28 | { 29 | "Effect": "Allow", 30 | "Action": [ 31 | "ec2:TerminateInstances" 32 | ], 33 | "Resource": [ 34 | "*" 35 | ], 36 | "Condition": { 37 | "StringEquals": { 38 | "ec2:ResourceTag/Application": "github-action-runner" 39 | } 40 | } 41 | }, 42 | { 43 | "Effect": "Allow", 44 | "Action": [ 45 | "ssm:GetParameter" 46 | ], 47 | "Resource": [ 48 | "${github_app_key_base64_arn}", 49 | "${github_app_id_arn}" 50 | ] 51 | %{ if kms_key_arn != "" ~} 52 | }, 53 | { 54 | "Effect": "Allow", 55 | "Action": [ 56 | "kms:Decrypt" 57 | ], 58 | "Resource": "${kms_key_arn}" 59 | %{ endif ~} 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/test/resources/sqs_receive_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "f7f4e155-2079-4255-b7a0-1b7b4be45ff9", 5 | "receiptHandle": "AQEBpE+kwApifOOwbeTp0xFbeOOjnPTHvMCPFIbft3ah3C50GAUD2RKz3ZzVKFxFRdD50uHrKt7rKpDHCuavO5TBj9Gql7YH6G4iR9Vqz9XFFAQQGlcHf+EfVsDAewPr0FLiW40ZC+mNNGwYh9Bqbo5MAmpNWxYWImI4VIEGknW0oFLMSSVd6js7eSkRaJoL5belvjl06b48b/PUvyk0Su367xTTRsf6esih3ALb9RBI0ylV78kmDEQLcNi/7X1pA3UChQcvEn5+bp5JKlhalQRFDyRqMmZr7KeUDI/vG2gbMHOWuLkwzTl5jsKGc/pPVi86", 6 | "body": "{\"id\":128620228,\"repositoryName\":\"Hello-World\",\"repositoryOwner\":\"Codertocat\",\"eventType\":\"check_run\",\"installationId\":12345}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1588152306469", 10 | "SequenceNumber": "18853311064165616128", 11 | "MessageGroupId": "128620228", 12 | "SenderId": "AROAVQMGTCYMGIEWL5JV5:default-action-runners-webhook", 13 | "MessageDeduplicationId": "bdc9a81e515df0131ddc015b1182b57ffcd79b0321bfe32bb40572b23ee68c50", 14 | "ApproximateFirstReceiveTimestamp": "1588152306469" 15 | }, 16 | "messageAttributes": {}, 17 | "md5OfBody": "f30235cb7733c3ac59a14d99c59a6dbf", 18 | "eventSource": "aws:sqs", 19 | "eventSourceARN": "arn:aws:sqs:eu-west-1:378776262168:default-action-runners-webhook-events.fifo", 20 | "awsRegion": "eu-west-1" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/scale-runners/scale-down-config.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment-timezone'; 2 | 3 | import { ScalingDownConfigList, getIdleRunnerCount } from './scale-down-config'; 4 | 5 | const DEFAULT_TIMEZONE = 'America/Los_Angeles'; 6 | const DEFAULT_IDLE_COUNT = 1; 7 | const now = moment.tz(new Date(), 'America/Los_Angeles'); 8 | 9 | function getConfig(cronTabs: string[]): ScalingDownConfigList { 10 | return cronTabs.map((cron) => ({ 11 | cron: cron, 12 | idleCount: DEFAULT_IDLE_COUNT, 13 | timeZone: DEFAULT_TIMEZONE, 14 | })); 15 | } 16 | 17 | describe('scaleDownConfig', () => { 18 | describe('Check runners that should be kept idle based on config.', () => { 19 | it('One active cron configuration', async () => { 20 | const scaleDownConfig = getConfig(['* * * * * *']); 21 | expect(getIdleRunnerCount(scaleDownConfig)).toEqual(DEFAULT_IDLE_COUNT); 22 | }); 23 | 24 | it('No active cron configuration', async () => { 25 | const scaleDownConfig = getConfig(['* * * * * ' + ((now.day() + 1) % 7)]); 26 | expect(getIdleRunnerCount(scaleDownConfig)).toEqual(0); 27 | }); 28 | 29 | it('1 of 2 cron configurations be active', async () => { 30 | const scaleDownConfig = getConfig(['* * * * * ' + ((now.day() + 1) % 7), '* * * * * ' + (now.day() % 7)]); 31 | expect(getIdleRunnerCount(scaleDownConfig)).toEqual(DEFAULT_IDLE_COUNT); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/logger/logger.child.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { createChildLogger, logger, setContext } from '.'; 4 | 5 | const childLogger = createChildLogger('child'); 6 | 7 | const context: Context = { 8 | awsRequestId: '1', 9 | callbackWaitsForEmptyEventLoop: false, 10 | functionName: 'unit-test', 11 | functionVersion: '', 12 | getRemainingTimeInMillis: () => 0, 13 | invokedFunctionArn: '', 14 | logGroupName: '', 15 | logStreamName: '', 16 | memoryLimitInMB: '', 17 | done: () => { 18 | return; 19 | }, 20 | fail: () => { 21 | return; 22 | }, 23 | succeed: () => { 24 | return; 25 | }, 26 | }; 27 | 28 | describe('A child logger.', () => { 29 | test('should log inherit context from root and combined with own context.', () => { 30 | expect(childLogger).not.toBe(logger); 31 | setContext(context, 'unit-test'); 32 | 33 | expect(logger.getPersistentLogAttributes()).toEqual( 34 | expect.objectContaining({ 35 | 'aws-request-id': context.awsRequestId, 36 | 'function-name': context.functionName, 37 | module: 'unit-test', 38 | }), 39 | ); 40 | 41 | expect(childLogger.getPersistentLogAttributes()).toEqual( 42 | expect.objectContaining({ 43 | module: 'child', 44 | 'aws-request-id': context.awsRequestId, 45 | 'function-name': context.functionName, 46 | }), 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /modules/runners/outputs.tf: -------------------------------------------------------------------------------- 1 | output "launch_template" { 2 | value = aws_launch_template.runner 3 | } 4 | 5 | output "role_runner" { 6 | value = aws_iam_role.runner 7 | } 8 | 9 | output "lambda_scale_up" { 10 | value = aws_lambda_function.scale_up 11 | } 12 | 13 | output "lambda_scale_up_log_group" { 14 | value = aws_cloudwatch_log_group.scale_up 15 | } 16 | 17 | output "role_scale_up" { 18 | value = aws_iam_role.scale_up 19 | } 20 | 21 | output "lambda_scale_down" { 22 | value = aws_lambda_function.scale_down 23 | } 24 | 25 | output "lambda_scale_down_log_group" { 26 | value = aws_cloudwatch_log_group.scale_down 27 | } 28 | 29 | output "role_scale_down" { 30 | value = aws_iam_role.scale_down 31 | } 32 | 33 | output "lambda_pool" { 34 | value = try(module.pool[0].lambda, null) 35 | } 36 | 37 | output "lambda_pool_log_group" { 38 | value = try(module.pool[0].lambda_log_group, null) 39 | } 40 | 41 | output "role_pool" { 42 | value = try(module.pool[0].role_pool, null) 43 | } 44 | 45 | output "runners_log_groups" { 46 | description = "List of log groups from different log files of runner machine." 47 | value = try(aws_cloudwatch_log_group.gh_runners, []) 48 | } 49 | 50 | output "logfiles" { 51 | value = local.logfiles 52 | description = "List of logfiles to send to CloudWatch. Object description: `log_group_name`: Name of the log group, `file_path`: path to the log file, `log_stream_name`: name of the log stream." 53 | } 54 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/logger/logger.child.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { createChildLogger, logger, setContext } from '.'; 4 | 5 | const childLogger = createChildLogger('child'); 6 | 7 | const context: Context = { 8 | awsRequestId: '1', 9 | callbackWaitsForEmptyEventLoop: false, 10 | functionName: 'unit-test', 11 | functionVersion: '', 12 | getRemainingTimeInMillis: () => 0, 13 | invokedFunctionArn: '', 14 | logGroupName: '', 15 | logStreamName: '', 16 | memoryLimitInMB: '', 17 | done: () => { 18 | return; 19 | }, 20 | fail: () => { 21 | return; 22 | }, 23 | succeed: () => { 24 | return; 25 | }, 26 | }; 27 | 28 | describe('A child logger.', () => { 29 | test('should log inherit context from root and combined with own context.', () => { 30 | expect(childLogger).not.toBe(logger); 31 | setContext(context, 'unit-test'); 32 | 33 | expect(logger.getPersistentLogAttributes()).toEqual( 34 | expect.objectContaining({ 35 | 'aws-request-id': context.awsRequestId, 36 | 'function-name': context.functionName, 37 | module: 'unit-test', 38 | }), 39 | ); 40 | 41 | expect(childLogger.getPersistentLogAttributes()).toEqual( 42 | expect.objectContaining({ 43 | module: 'child', 44 | 'aws-request-id': context.awsRequestId, 45 | 'function-name': context.functionName, 46 | }), 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment" { 2 | description = "A name that identifies the environment, used as prefix and for tagging." 3 | type = string 4 | default = null 5 | 6 | validation { 7 | condition = var.environment == null 8 | error_message = "The \"environment\" variable is no longer used. To migrate, set the \"prefix\" variable to the original value of \"environment\" and optionally, add \"Environment\" to the \"tags\" variable map with the same value." 9 | } 10 | } 11 | 12 | variable "prefix" { 13 | description = "The prefix used for naming resources" 14 | type = string 15 | default = "github-actions" 16 | } 17 | 18 | variable "namespaces" { 19 | description = "The role will be only allowed to create roles, policies and instance profiles in the given namespace / path. All policies in the boundaries namespace cannot be modified by this role." 20 | type = object({ 21 | boundary_namespace = string 22 | role_namespace = string 23 | policy_namespace = string 24 | instance_profile_namespace = string 25 | }) 26 | } 27 | 28 | variable "account_id" { 29 | description = "The module allows to switch to the created role from the provided account id." 30 | type = string 31 | 32 | } 33 | 34 | variable "aws_partition" { 35 | description = "(optional) partition in the arn namespace if not aws" 36 | type = string 37 | default = "aws" 38 | } 39 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/logger/logger.child.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | 3 | import { createChildLogger, logger, setContext } from '.'; 4 | 5 | const childLogger = createChildLogger('child'); 6 | 7 | const context: Context = { 8 | awsRequestId: '1', 9 | callbackWaitsForEmptyEventLoop: false, 10 | functionName: 'unit-test', 11 | functionVersion: '', 12 | getRemainingTimeInMillis: () => 0, 13 | invokedFunctionArn: '', 14 | logGroupName: '', 15 | logStreamName: '', 16 | memoryLimitInMB: '', 17 | done: () => { 18 | return; 19 | }, 20 | fail: () => { 21 | return; 22 | }, 23 | succeed: () => { 24 | return; 25 | }, 26 | }; 27 | 28 | describe('A child logger.', () => { 29 | test('should log inherit context from root and combined with own context.', () => { 30 | expect(childLogger).not.toBe(logger); 31 | setContext(context, 'unit-test'); 32 | 33 | expect(logger.getPersistentLogAttributes()).toEqual( 34 | expect.objectContaining({ 35 | 'aws-request-id': context.awsRequestId, 36 | 'function-name': context.functionName, 37 | module: 'unit-test', 38 | }), 39 | ); 40 | 41 | expect(childLogger.getPersistentLogAttributes()).toEqual( 42 | expect.objectContaining({ 43 | module: 'child', 44 | 'aws-request-id': context.awsRequestId, 45 | 'function-name': context.functionName, 46 | }), 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /variables.deprecated.tf: -------------------------------------------------------------------------------- 1 | variable "enabled_userdata" { 2 | description = "DEPCRECATED: Replaced by `enable_userdata`." 3 | type = string 4 | default = null 5 | 6 | validation { 7 | condition = anytrue([var.enabled_userdata == null]) 8 | error_message = "DEPCRECATED, replaced by `enable_userdata`." 9 | } 10 | } 11 | 12 | variable "runner_enable_workflow_job_labels_check_all" { 13 | description = "DEPCRECATED: Replaced by `enable_runner_workflow_job_labels_check_all`." 14 | type = string 15 | default = null 16 | 17 | validation { 18 | condition = anytrue([var.runner_enable_workflow_job_labels_check_all == null]) 19 | error_message = "DEPCRECATED, replaced by `enable_runner_workflow_job_labels_check_all`." 20 | } 21 | } 22 | 23 | variable "fifo_build_queue" { 24 | description = "DEPCRECATED: Replaced by `enable_fifo_build_queue`." 25 | type = string 26 | default = null 27 | 28 | validation { 29 | condition = anytrue([var.fifo_build_queue == null]) 30 | error_message = "DEPCRECATED, replaced by `enable_fifo_build_queue`." 31 | } 32 | } 33 | 34 | variable "enable_enable_fifo_build_queue" { 35 | description = "DEPCRECATED: Replaced by `enable_fifo_build_queue` / `fifo_build_queue`." 36 | type = string 37 | default = null 38 | 39 | validation { 40 | condition = anytrue([var.enable_enable_fifo_build_queue == null]) 41 | error_message = "DEPCRECATED, replaced by `enable_fifo_build_queue`." 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | # NPM production dependencies are part of the generated Lambda JavaScript. 7 | # Therefore updates on production are prefixed with fix(component) to trigger releases. 8 | # Development updates are prefixed with chore, and not triggering a release. 9 | 10 | version: 2 11 | updates: 12 | - package-ecosystem: "github-actions" 13 | # Workflow files stored in the 14 | # default location of `.github/workflows` 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | 19 | - package-ecosystem: "npm" 20 | directory: "/modules/runner-binaries-syncer/lambdas/runner-binaries-syncer" 21 | schedule: 22 | interval: "weekly" 23 | commit-message: 24 | prefix: "fix(syncer)" 25 | prefix-development: "chore(syncer)" 26 | 27 | - package-ecosystem: "npm" 28 | directory: "/modules/webhook/lambdas/webhook" 29 | schedule: 30 | interval: "weekly" 31 | commit-message: 32 | prefix: "fix(webhook)" 33 | prefix-development: "chore(webhook)" 34 | 35 | - package-ecosystem: "npm" 36 | directory: "/modules/runners/lambdas/runners" 37 | schedule: 38 | interval: "weekly" 39 | commit-message: 40 | prefix: "fix(runners)" 41 | prefix-development: "chore(runners)" 42 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger'; 2 | import { Context } from 'aws-lambda'; 3 | 4 | const childLoggers: Logger[] = []; 5 | 6 | const defaultValues = { 7 | region: process.env.AWS_REGION || 'N/A', 8 | environment: process.env.ENVIRONMENT || 'N/A', 9 | }; 10 | 11 | function setContext(context: Context, module?: string) { 12 | logger.addPersistentLogAttributes({ 13 | 'aws-request-id': context.awsRequestId, 14 | 'function-name': context.functionName, 15 | module: module, 16 | }); 17 | 18 | // Add the context to all child loggers 19 | childLoggers.forEach((childLogger) => { 20 | childLogger.addPersistentLogAttributes({ 21 | 'aws-request-id': context.awsRequestId, 22 | 'function-name': context.functionName, 23 | }); 24 | }); 25 | } 26 | 27 | const logger = new Logger({ 28 | serviceName: process.env.SERVICE_NAME || 'runners', 29 | persistentLogAttributes: { 30 | ...defaultValues, 31 | }, 32 | }); 33 | 34 | function createChildLogger(module: string): Logger { 35 | const childLogger = logger.createChild({ 36 | persistentLogAttributes: { 37 | module: module, 38 | }, 39 | }); 40 | 41 | childLoggers.push(childLogger); 42 | return childLogger; 43 | } 44 | 45 | type LogAttributes = { 46 | [key: string]: unknown; 47 | }; 48 | 49 | function addPersistentContextToChildLogger(attributes: LogAttributes) { 50 | childLoggers.forEach((childLogger) => { 51 | childLogger.addPersistentLogAttributes(attributes); 52 | }); 53 | } 54 | 55 | export { addPersistentContextToChildLogger, createChildLogger, logger, setContext }; 56 | -------------------------------------------------------------------------------- /modules/runners/templates/user-data.ps1: -------------------------------------------------------------------------------- 1 | 2 | $ErrorActionPreference = "Continue" 3 | $VerbosePreference = "Continue" 4 | Start-Transcript -Path "C:\UserData.log" -Append 5 | 6 | ${pre_install} 7 | 8 | # Install Chocolatey 9 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 10 | $env:chocolateyUseWindowsCompression = 'true' 11 | Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression 12 | 13 | # Add Chocolatey to powershell profile 14 | $ChocoProfileValue = @' 15 | $ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" 16 | if (Test-Path($ChocolateyProfile)) { 17 | Import-Module "$ChocolateyProfile" 18 | } 19 | 20 | refreshenv 21 | '@ 22 | # Write it to the $profile location 23 | Set-Content -Path "$PsHome\Microsoft.PowerShell_profile.ps1" -Value $ChocoProfileValue -Force 24 | # Source it 25 | . "$PsHome\Microsoft.PowerShell_profile.ps1" 26 | 27 | 28 | refreshenv 29 | 30 | Write-Host "Installing cloudwatch agent..." 31 | Invoke-WebRequest -Uri https://s3.amazonaws.com/amazoncloudwatch-agent/windows/amd64/latest/amazon-cloudwatch-agent.msi -OutFile C:\amazon-cloudwatch-agent.msi 32 | $cloudwatchParams = '/i', 'C:\amazon-cloudwatch-agent.msi', '/qn', '/L*v', 'C:\CloudwatchInstall.log' 33 | Start-Process "msiexec.exe" $cloudwatchParams -Wait -NoNewWindow 34 | Remove-Item C:\amazon-cloudwatch-agent.msi 35 | 36 | 37 | # Install dependent tools 38 | Write-Host "Installing additional development tools" 39 | choco install git awscli -y 40 | refreshenv 41 | 42 | ${install_runner} 43 | ${post_install} 44 | ${start_runner} 45 | 46 | Stop-Transcript 47 | -------------------------------------------------------------------------------- /modules/runners/policies/instance-ssm-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ssm:DescribeAssociation", 8 | "ssm:GetDeployablePatchSnapshotForInstance", 9 | "ssm:GetDocument", 10 | "ssm:DescribeDocument", 11 | "ssm:GetManifest", 12 | "ssm:ListAssociations", 13 | "ssm:ListInstanceAssociations", 14 | "ssm:PutInventory", 15 | "ssm:PutComplianceItems", 16 | "ssm:PutConfigurePackageResult", 17 | "ssm:UpdateAssociationStatus", 18 | "ssm:UpdateInstanceAssociationStatus", 19 | "ssm:UpdateInstanceInformation" 20 | ], 21 | "Resource": "*" 22 | }, 23 | { 24 | "Effect": "Allow", 25 | "Action": [ 26 | "ssmmessages:CreateControlChannel", 27 | "ssmmessages:CreateDataChannel", 28 | "ssmmessages:OpenControlChannel", 29 | "ssmmessages:OpenDataChannel" 30 | ], 31 | "Resource": "*" 32 | }, 33 | { 34 | "Effect": "Allow", 35 | "Action": [ 36 | "ec2messages:AcknowledgeMessage", 37 | "ec2messages:DeleteMessage", 38 | "ec2messages:FailMessage", 39 | "ec2messages:GetEndpoint", 40 | "ec2messages:GetMessages", 41 | "ec2messages:SendReply" 42 | ], 43 | "Resource": "*" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /examples/permissions-boundary/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = "boundaries" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | data "terraform_remote_state" "iam" { 11 | backend = "local" 12 | 13 | config = { 14 | path = "${path.module}/setup/terraform.tfstate" 15 | } 16 | } 17 | 18 | resource "aws_kms_key" "github" { 19 | is_enabled = true 20 | } 21 | 22 | resource "aws_kms_alias" "github" { 23 | name = "alias/github/action-runners" 24 | target_key_id = aws_kms_key.github.key_id 25 | } 26 | 27 | module "runners" { 28 | source = "../../" 29 | providers = { 30 | aws = aws.terraform_role 31 | } 32 | 33 | aws_region = local.aws_region 34 | vpc_id = module.vpc.vpc_id 35 | subnet_ids = module.vpc.private_subnets 36 | kms_key_arn = aws_kms_key.github.key_id 37 | 38 | prefix = local.environment 39 | tags = { 40 | Project = "ProjectX" 41 | } 42 | 43 | github_app = { 44 | key_base64 = var.github_app.key_base64 45 | id = var.github_app.id 46 | webhook_secret = random_id.random.hex 47 | } 48 | 49 | webhook_lambda_zip = "../lambdas-download/webhook.zip" 50 | runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip" 51 | runners_lambda_zip = "../lambdas-download/runners.zip" 52 | enable_organization_runners = false 53 | runner_extra_labels = "default,example" 54 | 55 | instance_profile_path = "/runners/" 56 | role_path = "/runners/" 57 | role_permissions_boundary = data.terraform_remote_state.iam.outputs.boundary 58 | } 59 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/ssm/index.test.ts: -------------------------------------------------------------------------------- 1 | import { GetParameterCommandOutput, SSM } from '@aws-sdk/client-ssm'; 2 | import nock from 'nock'; 3 | 4 | import { getParameterValue } from '.'; 5 | 6 | jest.mock('@aws-sdk/client-ssm'); 7 | 8 | const cleanEnv = process.env; 9 | 10 | beforeEach(() => { 11 | jest.resetModules(); 12 | jest.clearAllMocks(); 13 | process.env = { ...cleanEnv }; 14 | nock.disableNetConnect(); 15 | }); 16 | 17 | describe('Test getParameterValue', () => { 18 | test('Gets parameters and returns string', async () => { 19 | // Arrange 20 | const parameterValue = 'test'; 21 | const parameterName = 'testParam'; 22 | const output: GetParameterCommandOutput = { 23 | Parameter: { 24 | Name: parameterName, 25 | Type: 'SecureString', 26 | Value: parameterValue, 27 | }, 28 | $metadata: { 29 | httpStatusCode: 200, 30 | }, 31 | }; 32 | 33 | SSM.prototype.getParameter = jest.fn().mockResolvedValue(output); 34 | 35 | // Act 36 | const result = await getParameterValue(parameterName); 37 | 38 | // Assert 39 | expect(result).toBe(parameterValue); 40 | }); 41 | 42 | test('Gets invalid parameters and returns string', async () => { 43 | // Arrange 44 | const parameterName = 'invalid'; 45 | const output: GetParameterCommandOutput = { 46 | $metadata: { 47 | httpStatusCode: 200, 48 | }, 49 | }; 50 | 51 | SSM.prototype.getParameter = jest.fn().mockResolvedValue(output); 52 | 53 | // Act 54 | const result = await getParameterValue(parameterName); 55 | 56 | // Assert 57 | expect(result).toBe(undefined); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/aws/ssm.test.ts: -------------------------------------------------------------------------------- 1 | import { GetParameterCommandOutput, SSM } from '@aws-sdk/client-ssm'; 2 | import nock from 'nock'; 3 | 4 | import { getParameterValue } from './ssm'; 5 | 6 | jest.mock('@aws-sdk/client-ssm'); 7 | 8 | const cleanEnv = process.env; 9 | 10 | beforeEach(() => { 11 | jest.resetModules(); 12 | jest.clearAllMocks(); 13 | process.env = { ...cleanEnv }; 14 | nock.disableNetConnect(); 15 | }); 16 | 17 | describe('Test getParameterValue', () => { 18 | test('Gets parameters and returns string', async () => { 19 | // Arrange 20 | const parameterValue = 'test'; 21 | const parameterName = 'testParam'; 22 | const output: GetParameterCommandOutput = { 23 | Parameter: { 24 | Name: parameterName, 25 | Type: 'SecureString', 26 | Value: parameterValue, 27 | }, 28 | $metadata: { 29 | httpStatusCode: 200, 30 | }, 31 | }; 32 | 33 | SSM.prototype.getParameter = jest.fn().mockResolvedValue(output); 34 | 35 | // Act 36 | const result = await getParameterValue(parameterName); 37 | 38 | // Assert 39 | expect(result).toBe(parameterValue); 40 | }); 41 | 42 | test('Gets invalid parameters and returns string', async () => { 43 | // Arrange 44 | const parameterName = 'invalid'; 45 | const output: GetParameterCommandOutput = { 46 | $metadata: { 47 | httpStatusCode: 200, 48 | }, 49 | }; 50 | 51 | SSM.prototype.getParameter = jest.fn().mockResolvedValue(output); 52 | 53 | // Act 54 | const result = await getParameterValue(parameterName); 55 | 56 | // Assert 57 | expect(result).toBe(undefined); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /modules/multi-runner/webhook.tf: -------------------------------------------------------------------------------- 1 | module "webhook" { 2 | source = "../webhook" 3 | prefix = var.prefix 4 | tags = local.tags 5 | kms_key_arn = var.kms_key_arn 6 | 7 | runner_config = local.runner_config 8 | sqs_workflow_job_queue = length(aws_sqs_queue.webhook_events_workflow_job_queue) > 0 ? aws_sqs_queue.webhook_events_workflow_job_queue[0] : null 9 | 10 | github_app_parameters = { 11 | webhook_secret = module.ssm.parameters.github_app_webhook_secret 12 | } 13 | 14 | lambda_s3_bucket = var.lambda_s3_bucket 15 | webhook_lambda_s3_key = var.webhook_lambda_s3_key 16 | webhook_lambda_s3_object_version = var.webhook_lambda_s3_object_version 17 | webhook_lambda_apigateway_access_log_settings = var.webhook_lambda_apigateway_access_log_settings 18 | lambda_runtime = var.lambda_runtime 19 | lambda_architecture = var.lambda_architecture 20 | lambda_zip = var.webhook_lambda_zip 21 | lambda_timeout = var.webhook_lambda_timeout 22 | logging_retention_in_days = var.logging_retention_in_days 23 | logging_kms_key_id = var.logging_kms_key_id 24 | 25 | role_path = var.role_path 26 | role_permissions_boundary = var.role_permissions_boundary 27 | repository_white_list = var.repository_white_list 28 | 29 | lambda_subnet_ids = var.lambda_subnet_ids 30 | lambda_security_group_ids = var.lambda_security_group_ids 31 | aws_partition = var.aws_partition 32 | 33 | log_level = var.log_level 34 | } 35 | -------------------------------------------------------------------------------- /examples/windows/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = "windows" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | module "base" { 11 | source = "../base" 12 | 13 | prefix = local.environment 14 | aws_region = local.aws_region 15 | } 16 | 17 | module "runners" { 18 | source = "../../" 19 | 20 | aws_region = local.aws_region 21 | vpc_id = module.base.vpc.vpc_id 22 | subnet_ids = module.base.vpc.private_subnets 23 | prefix = local.environment 24 | 25 | github_app = { 26 | key_base64 = var.github_app.key_base64 27 | id = var.github_app.id 28 | webhook_secret = random_id.random.hex 29 | } 30 | 31 | # Grab the lambda packages from local directory. Must run /.ci/build.sh first 32 | webhook_lambda_zip = "../../lambda_output/webhook.zip" 33 | runner_binaries_syncer_lambda_zip = "../../lambda_output/runner-binaries-syncer.zip" 34 | runners_lambda_zip = "../../lambda_output/runners.zip" 35 | 36 | enable_organization_runners = false 37 | # no need to add extra windows tag here as it is automatically added by GitHub 38 | runner_extra_labels = "default,example" 39 | 40 | # Set the OS to Windows 41 | runner_os = "windows" 42 | # we need to give the runner time to start because this is windows. 43 | runner_boot_time_in_minutes = 20 44 | 45 | # enable access to the runners via SSM 46 | enable_ssm_on_runners = true 47 | 48 | instance_types = ["m5.large", "c5.large"] 49 | 50 | # override delay of events in seconds for testing 51 | delay_webhook_event = 5 52 | 53 | # override scaling down for testing 54 | scale_down_schedule_expression = "cron(* * * * ? *)" 55 | } 56 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Context, SQSEvent } from 'aws-lambda'; 2 | import 'source-map-support/register'; 3 | 4 | import { logger, setContext } from './logger'; 5 | import { PoolEvent, adjust } from './pool/pool'; 6 | import ScaleError from './scale-runners/ScaleError'; 7 | import { scaleDown } from './scale-runners/scale-down'; 8 | import { scaleUp } from './scale-runners/scale-up'; 9 | 10 | export async function scaleUpHandler(event: SQSEvent, context: Context): Promise { 11 | setContext(context, 'lambda.ts'); 12 | logger.logEventIfEnabled(event); 13 | 14 | if (event.Records.length !== 1) { 15 | logger.warn('Event ignored, only one record at the time can be handled, ensure the lambda batch size is set to 1.'); 16 | return new Promise((resolve) => resolve()); 17 | } 18 | 19 | try { 20 | await scaleUp(event.Records[0].eventSource, JSON.parse(event.Records[0].body)); 21 | } catch (e) { 22 | if (e instanceof ScaleError) { 23 | throw e; 24 | } else { 25 | logger.warn(`Ignoring error: ${(e as Error).message}`); 26 | } 27 | } 28 | } 29 | 30 | export async function scaleDownHandler(event: unknown, context: Context): Promise { 31 | setContext(context, 'lambda.ts'); 32 | logger.logEventIfEnabled(event); 33 | 34 | try { 35 | await scaleDown(); 36 | } catch (e) { 37 | logger.error(`${(e as Error).message}`, { error: e as Error }); 38 | } 39 | } 40 | 41 | export async function adjustPool(event: PoolEvent, context: Context): Promise { 42 | setContext(context, 'lambda.ts'); 43 | logger.logEventIfEnabled(event); 44 | 45 | try { 46 | await adjust(event); 47 | } catch (e) { 48 | logger.error(`${(e as Error).message}`, { error: e as Error }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-runner-lambda-syncer", 3 | "version": "1.0.0", 4 | "main": "lambda.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node-dev src/local.ts", 8 | "test": "NODE_ENV=test jest", 9 | "test:watch": "NODE_ENV=test jest --watch", 10 | "lint": "yarn eslint src", 11 | "watch": "ts-node-dev --respawn --exit-child src/local.ts", 12 | "build": "ncc build src/lambda.ts -o dist", 13 | "dist": "yarn build && cd dist && zip ../runner-binaries-syncer.zip index.js", 14 | "format": "prettier --write \"**/*.ts\"", 15 | "format-check": "prettier --check \"**/*.ts\"", 16 | "all": "yarn build && yarn format && yarn lint && yarn test" 17 | }, 18 | "devDependencies": { 19 | "@babel/helper-get-function-arity": "^7.16.7", 20 | "@octokit/rest": "^19.0.7", 21 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 22 | "@types/aws-lambda": "^8.10.114", 23 | "@types/jest": "^29.5.0", 24 | "@types/node": "^18.15.5", 25 | "@types/request": "^2.48.8", 26 | "@typescript-eslint/eslint-plugin": "^5.54.1", 27 | "@typescript-eslint/parser": "^5.54.1", 28 | "@vercel/ncc": "^0.36.1", 29 | "aws-sdk-client-mock": "^2.1.1", 30 | "aws-sdk-client-mock-jest": "^2.1.1", 31 | "eslint": "^8.35.0", 32 | "eslint-plugin-prettier": "4.2.1", 33 | "jest": "^29.5", 34 | "jest-mock": "^29.5.0", 35 | "prettier": "2.8.4", 36 | "ts-jest": "^29.0.5", 37 | "ts-node-dev": "^2.0.0", 38 | "typescript": "^5.0.2" 39 | }, 40 | "dependencies": { 41 | "@aws-lambda-powertools/logger": "^1.6.0", 42 | "@aws-sdk/client-s3": "^3.294.0", 43 | "@aws-sdk/lib-storage": "^3.294.0", 44 | "axios": "^1.3.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | # Prebuilt Images 2 | 3 | The images inside this folder are pre-built images designed to shorten the boot time of your runners and make using ephemeral runners a faster experience. 4 | 5 | These images share the same scripting as used in the user-data mechanism in `/modules/runners/templates/`. We use a `templatefile` mechanism to insert the relevant script fragments into the scripts used for provisioning the images. 6 | 7 | The examples in `linux-amzn2` and `windows-core-2019` also upload a `start-runner` script that uses the exact same startup process as used in the user-data mechanism. This means that the image created here does not need any extra scripts injected or changes to boot up and connect to GH. 8 | 9 | ## Building your own 10 | 11 | To build these images you first need to install packer. 12 | You will also need an amazon account and to have provisioned your credentials for packer to consume. 13 | 14 | Assuming you are building the `linux-amzn2` image. Then run the following from within the `linux-amzn2` folder 15 | 16 | ```bash 17 | packer init . 18 | packer validate . 19 | packer build github_agent.linux.pkr.hcl 20 | ``` 21 | 22 | Your image will then begin to build inside AWS and when finished you will be provided with complete AMI. 23 | 24 | ## Using your image 25 | 26 | To use your image in the terraform modules you will need to set some values on the module. 27 | 28 | Assuming you have built the `linux-amzn2` image which has a pre-defined AMI name in the following format `github-runner-amzn2-x86_64-YYYYMMDDhhmm` you can use the following values. 29 | 30 | ```hcl 31 | # set the name of the ami to use 32 | ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] } 33 | # provide the owner id of 34 | ami_owners = [""] 35 | 36 | enable_userdata = false 37 | ``` 38 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/local.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './logger'; 2 | import { ActionRequestMessage, scaleUp } from './scale-runners/scale-up'; 3 | 4 | const sqsEvent = { 5 | Records: [ 6 | { 7 | messageId: 'e8d74d08-644e-42ca-bf82-a67daa6c4dad', 8 | receiptHandle: 9 | // eslint-disable-next-line max-len 10 | 'AQEBCpLYzDEKq4aKSJyFQCkJduSKZef8SJVOperbYyNhXqqnpFG5k74WygVAJ4O0+9nybRyeOFThvITOaS21/jeHiI5fgaM9YKuI0oGYeWCIzPQsluW5CMDmtvqv1aA8sXQ5n2x0L9MJkzgdIHTC3YWBFLQ2AxSveOyIHwW+cHLIFCAcZlOaaf0YtaLfGHGkAC4IfycmaijV8NSlzYgDuxrC9sIsWJ0bSvk5iT4ru/R4+0cjm7qZtGlc04k9xk5Fu6A+wRxMaIyiFRY+Ya19ykcevQldidmEjEWvN6CRToLgclk=', 11 | body: { 12 | repositoryName: 'self-hosted', 13 | repositoryOwner: 'test-runners', 14 | eventType: 'workflow_job', 15 | id: 987654, 16 | installationId: 123456789, 17 | }, 18 | attributes: { 19 | ApproximateReceiveCount: '1', 20 | SentTimestamp: '1626450047230', 21 | SequenceNumber: '18863115285800432640', 22 | MessageGroupId: '19072', 23 | SenderId: 'AROA5KW7SQ6TTB3PW6WPH:cicddev-webhook', 24 | MessageDeduplicationId: '0c458eeb87b7f6d2607301268fd3bf33dd898a49ebd888754ff7db510c4bff1e', 25 | ApproximateFirstReceiveTimestamp: '1626450077251', 26 | }, 27 | messageAttributes: {}, 28 | md5OfBody: '4aef3bd70526e152e86426a0938cbec6', 29 | eventSource: 'aws:sqs', 30 | eventSourceARN: 'arn:aws:sqs:us-west-2:916370655143:cicddev-queued-builds.fifo', 31 | awsRegion: 'us-west-2', 32 | }, 33 | ], 34 | }; 35 | 36 | export function run(): void { 37 | scaleUp(sqsEvent.Records[0].eventSource, sqsEvent.Records[0].body as ActionRequestMessage) 38 | .then() 39 | .catch((e) => { 40 | logger.error(e); 41 | }); 42 | } 43 | 44 | run(); 45 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-runner-lambda-agent-webhook", 3 | "version": "1.0.0", 4 | "main": "lambda.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node-dev src/local.ts", 8 | "test": "NODE_ENV=test jest", 9 | "test:watch": "NODE_ENV=test jest --watch", 10 | "lint": "yarn eslint src", 11 | "watch": "ts-node-dev --respawn --exit-child src/local.ts", 12 | "build": "ncc build src/lambda.ts -o dist", 13 | "dist": "yarn build && cd dist && zip ../webhook.zip index.js", 14 | "format": "prettier --write \"**/*.ts\"", 15 | "format-check": "prettier --check \"**/*.ts\"", 16 | "all": "yarn build && yarn format && yarn lint && yarn test" 17 | }, 18 | "devDependencies": { 19 | "@babel/helper-get-function-arity": "^7.16.7", 20 | "@octokit/webhooks-definitions": "^3.67.3", 21 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 22 | "@types/aws-lambda": "^8.10.111", 23 | "@types/express": "^4.17.17", 24 | "@types/jest": "^29.4.0", 25 | "@types/node": "^18.15.3", 26 | "@typescript-eslint/eslint-plugin": "^5.54.0", 27 | "@typescript-eslint/parser": "^5.56.0", 28 | "@vercel/ncc": "0.36.1", 29 | "body-parser": "^1.20.2", 30 | "eslint": "^8.36.0", 31 | "eslint-plugin-prettier": "4.2.1", 32 | "express": "^4.18.2", 33 | "jest": "^29.4", 34 | "jest-mock": "^29.3.1", 35 | "nock": "^13.3.0", 36 | "prettier": "2.8.4", 37 | "ts-jest": "^29.0.5", 38 | "ts-node-dev": "^2.0.0", 39 | "typescript": "^5.0.2" 40 | }, 41 | "dependencies": { 42 | "@aws-lambda-powertools/logger": "^1.6.0", 43 | "@aws-sdk/client-sqs": "^3.296.0", 44 | "@aws-sdk/client-ssm": "^3.282.0", 45 | "@octokit/rest": "^19.0.7", 46 | "@octokit/webhooks": "^10.7.0", 47 | "aws-lambda": "^1.0.7" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-runner-lambda-scale-runners", 3 | "version": "1.0.0", 4 | "main": "lambda.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node-dev src/local.ts", 8 | "test": "NODE_ENV=test jest", 9 | "test:watch": "NODE_ENV=test jest --watch", 10 | "lint": "yarn eslint src", 11 | "watch": "ts-node-dev --respawn --exit-child src/local.ts", 12 | "build": "ncc build src/lambda.ts -o dist", 13 | "dist": "yarn build && cd dist && zip ../runners.zip index.js", 14 | "format": "prettier --write \"**/*.ts\"", 15 | "format-check": "prettier --check \"**/*.ts\"", 16 | "all": "yarn build && yarn format && yarn lint && yarn test" 17 | }, 18 | "devDependencies": { 19 | "@babel/helper-get-function-arity": "^7.16.7", 20 | "@trivago/prettier-plugin-sort-imports": "^4.0.0", 21 | "@types/aws-lambda": "^8.10.114", 22 | "@types/express": "^4.17.17", 23 | "@types/jest": "^29.4.1", 24 | "@types/node": "^18.11.18", 25 | "@typescript-eslint/eslint-plugin": "^5.54.0", 26 | "@typescript-eslint/parser": "^5.54.0", 27 | "@vercel/ncc": "^0.36.1", 28 | "eslint": "^8.33.0", 29 | "eslint-plugin-prettier": "4.2.1", 30 | "jest": "^29.5", 31 | "jest-mock": "^29.5.0", 32 | "jest-mock-extended": "^3.0.1", 33 | "moment-timezone": "^0.5.41", 34 | "nock": "^13.3.0", 35 | "prettier": "2.8.4", 36 | "ts-jest": "^29.0.5", 37 | "ts-node": "^10.9.1", 38 | "ts-node-dev": "^2.0.0" 39 | }, 40 | "dependencies": { 41 | "@aws-lambda-powertools/logger": "^1.6.0", 42 | "@aws-sdk/client-ssm": "^3.296.0", 43 | "@octokit/auth-app": "4.0.9", 44 | "@octokit/rest": "^19.0.7", 45 | "@octokit/types": "^9.0.0", 46 | "aws-sdk": "^2.1340.0", 47 | "cron-parser": "^4.7.1", 48 | "typescript": "^4.9.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/multi-runner/runner-binaries.tf: -------------------------------------------------------------------------------- 1 | module "runner_binaries" { 2 | source = "../runner-binaries-syncer" 3 | for_each = local.unique_os_and_arch 4 | prefix = "${var.prefix}-${each.value.os_type}-${each.value.architecture}" 5 | tags = local.tags 6 | 7 | distribution_bucket_name = "${var.prefix}-${each.value.os_type}-${each.value.architecture}-dist-${random_string.random.result}" 8 | 9 | runner_os = each.value.os_type 10 | runner_architecture = each.value.architecture 11 | 12 | lambda_s3_bucket = var.lambda_s3_bucket 13 | syncer_lambda_s3_key = var.syncer_lambda_s3_key 14 | syncer_lambda_s3_object_version = var.syncer_lambda_s3_object_version 15 | lambda_runtime = var.lambda_runtime 16 | lambda_architecture = var.lambda_architecture 17 | lambda_zip = var.runner_binaries_syncer_lambda_zip 18 | lambda_timeout = var.runner_binaries_syncer_lambda_timeout 19 | logging_retention_in_days = var.logging_retention_in_days 20 | logging_kms_key_id = var.logging_kms_key_id 21 | enable_event_rule_binaries_syncer = var.enable_event_rule_binaries_syncer 22 | 23 | server_side_encryption_configuration = var.runner_binaries_s3_sse_configuration 24 | 25 | role_path = var.role_path 26 | role_permissions_boundary = var.role_permissions_boundary 27 | 28 | log_level = var.log_level 29 | 30 | lambda_subnet_ids = var.lambda_subnet_ids 31 | lambda_security_group_ids = var.lambda_security_group_ids 32 | aws_partition = var.aws_partition 33 | 34 | lambda_principals = var.lambda_principals 35 | } 36 | locals { 37 | runner_binaries_by_os_and_arch_map = { 38 | for k, v in module.runner_binaries : k => { arn = v.bucket.arn, id = v.bucket.id, key = v.runner_distribution_object_key } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /images/windows-core-2019/bootstrap_win.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | Write-Output "Running User Data Script" 4 | Write-Host "(host) Running User Data Script" 5 | 6 | Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force -ErrorAction Ignore 7 | 8 | # Don't set this before Set-ExecutionPolicy as it throws an error 9 | $ErrorActionPreference = "stop" 10 | 11 | # Remove HTTP listener 12 | Remove-Item -Path WSMan:\Localhost\listener\listener* -Recurse 13 | 14 | # Create a self-signed certificate to let ssl work 15 | $Cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName "packer" 16 | New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $Cert.Thumbprint -Force 17 | 18 | # WinRM 19 | Write-Output "Setting up WinRM" 20 | Write-Host "(host) setting up WinRM" 21 | 22 | # I'm not really sure why we need the cmd.exe wrapper, but it works with it and doesn't work without it 23 | cmd.exe /c winrm quickconfig -q 24 | cmd.exe /c winrm set "winrm/config" '@{MaxTimeoutms="1800000"}' 25 | cmd.exe /c winrm set "winrm/config/winrs" '@{MaxMemoryPerShellMB="1024"}' 26 | cmd.exe /c winrm set "winrm/config/service" '@{AllowUnencrypted="true"}' 27 | cmd.exe /c winrm set "winrm/config/client" '@{AllowUnencrypted="true"}' 28 | cmd.exe /c winrm set "winrm/config/service/auth" '@{Basic="true"}' 29 | cmd.exe /c winrm set "winrm/config/client/auth" '@{Basic="true"}' 30 | cmd.exe /c winrm set "winrm/config/service/auth" '@{CredSSP="true"}' 31 | cmd.exe /c winrm set "winrm/config/listener?Address=*+Transport=HTTPS" "@{Port=`"5986`";Hostname=`"packer`";CertificateThumbprint=`"$($Cert.Thumbprint)`"}" 32 | cmd.exe /c netsh advfirewall firewall set rule group="remote administration" new enable=yes 33 | cmd.exe /c netsh firewall add portopening TCP 5986 "Port 5986" 34 | cmd.exe /c net stop winrm 35 | cmd.exe /c sc config winrm start= auto 36 | cmd.exe /c net start winrm 37 | 38 | -------------------------------------------------------------------------------- /images/windows-core-2022/bootstrap_win.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | Write-Output "Running User Data Script" 4 | Write-Host "(host) Running User Data Script" 5 | 6 | Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force -ErrorAction Ignore 7 | 8 | # Don't set this before Set-ExecutionPolicy as it throws an error 9 | $ErrorActionPreference = "stop" 10 | 11 | # Remove HTTP listener 12 | Remove-Item -Path WSMan:\Localhost\listener\listener* -Recurse 13 | 14 | # Create a self-signed certificate to let ssl work 15 | $Cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName "packer" 16 | New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $Cert.Thumbprint -Force 17 | 18 | # WinRM 19 | Write-Output "Setting up WinRM" 20 | Write-Host "(host) setting up WinRM" 21 | 22 | # I'm not really sure why we need the cmd.exe wrapper, but it works with it and doesn't work without it 23 | cmd.exe /c winrm quickconfig -q 24 | cmd.exe /c winrm set "winrm/config" '@{MaxTimeoutms="1800000"}' 25 | cmd.exe /c winrm set "winrm/config/winrs" '@{MaxMemoryPerShellMB="1024"}' 26 | cmd.exe /c winrm set "winrm/config/service" '@{AllowUnencrypted="true"}' 27 | cmd.exe /c winrm set "winrm/config/client" '@{AllowUnencrypted="true"}' 28 | cmd.exe /c winrm set "winrm/config/service/auth" '@{Basic="true"}' 29 | cmd.exe /c winrm set "winrm/config/client/auth" '@{Basic="true"}' 30 | cmd.exe /c winrm set "winrm/config/service/auth" '@{CredSSP="true"}' 31 | cmd.exe /c winrm set "winrm/config/listener?Address=*+Transport=HTTPS" "@{Port=`"5986`";Hostname=`"packer`";CertificateThumbprint=`"$($Cert.Thumbprint)`"}" 32 | cmd.exe /c netsh advfirewall firewall set rule group="remote administration" new enable=yes 33 | cmd.exe /c netsh firewall add portopening TCP 5986 "Port 5986" 34 | cmd.exe /c net stop winrm 35 | cmd.exe /c sc config winrm start= auto 36 | cmd.exe /c net start winrm 37 | 38 | -------------------------------------------------------------------------------- /modules/multi-runner/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = [for runner in module.runners : { 3 | launch_template_name = runner.launch_template.name 4 | launch_template_id = runner.launch_template.id 5 | launch_template_version = runner.launch_template.latest_version 6 | launch_template_ami_id = runner.launch_template.image_id 7 | lambda_up = runner.lambda_scale_up 8 | lambda_up_log_group = runner.lambda_scale_up_log_group 9 | lambda_down = runner.lambda_scale_down 10 | lambda_down_log_group = runner.lambda_scale_down_log_group 11 | lambda_pool = runner.lambda_pool 12 | lambda_pool_log_group = runner.lambda_pool_log_group 13 | role_runner = runner.role_runner 14 | role_scale_up = runner.role_scale_up 15 | role_scale_down = runner.role_scale_down 16 | role_pool = runner.role_pool 17 | runners_log_groups = runner.runners_log_groups 18 | logfiles = runner.logfiles 19 | }] 20 | } 21 | 22 | output "binaries_syncer" { 23 | value = [for runner_binary in module.runner_binaries : { 24 | lambda = runner_binary.lambda 25 | lambda_log_group = runner_binary.lambda_log_group 26 | lambda_role = runner_binary.lambda_role 27 | location = "s3://runner_binary.bucket.id}/runner_binary.bucket.key" 28 | bucket = runner_binary.bucket 29 | }] 30 | } 31 | 32 | output "webhook" { 33 | value = { 34 | gateway = module.webhook.gateway 35 | lambda = module.webhook.lambda 36 | lambda_log_group = module.webhook.lambda_log_group 37 | lambda_role = module.webhook.role 38 | endpoint = "${module.webhook.gateway.api_endpoint}/${module.webhook.endpoint_relative_path}" 39 | } 40 | } 41 | 42 | output "ssm_parameters" { 43 | value = module.ssm.parameters 44 | } 45 | -------------------------------------------------------------------------------- /modules/webhook/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | webhook_endpoint = "webhook" 3 | role_path = var.role_path == null ? "/${var.prefix}/" : var.role_path 4 | lambda_zip = var.lambda_zip == null ? "${path.module}/lambdas/webhook/webhook.zip" : var.lambda_zip 5 | } 6 | 7 | resource "aws_apigatewayv2_api" "webhook" { 8 | name = "${var.prefix}-github-action-webhook" 9 | protocol_type = "HTTP" 10 | tags = var.tags 11 | } 12 | 13 | resource "aws_apigatewayv2_route" "webhook" { 14 | api_id = aws_apigatewayv2_api.webhook.id 15 | route_key = "POST /${local.webhook_endpoint}" 16 | target = "integrations/${aws_apigatewayv2_integration.webhook.id}" 17 | } 18 | 19 | resource "aws_apigatewayv2_stage" "webhook" { 20 | lifecycle { 21 | ignore_changes = [ 22 | // see bug https://github.com/terraform-providers/terraform-provider-aws/issues/12893 23 | default_route_settings, 24 | // not terraform managed 25 | deployment_id 26 | ] 27 | } 28 | 29 | api_id = aws_apigatewayv2_api.webhook.id 30 | name = "$default" 31 | auto_deploy = true 32 | dynamic "access_log_settings" { 33 | for_each = var.webhook_lambda_apigateway_access_log_settings[*] 34 | content { 35 | destination_arn = access_log_settings.value.destination_arn 36 | format = access_log_settings.value.format 37 | } 38 | } 39 | tags = var.tags 40 | } 41 | 42 | resource "aws_apigatewayv2_integration" "webhook" { 43 | lifecycle { 44 | ignore_changes = [ 45 | // not terraform managed 46 | passthrough_behavior 47 | ] 48 | } 49 | 50 | api_id = aws_apigatewayv2_api.webhook.id 51 | integration_type = "AWS_PROXY" 52 | 53 | connection_type = "INTERNET" 54 | description = "GitHub App webhook for receiving build events." 55 | integration_method = "POST" 56 | integration_uri = aws_lambda_function.webhook.invoke_arn 57 | } 58 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | resource "aws_iam_role" "deploy" { 4 | name = "${var.prefix}-terraform" 5 | 6 | permissions_boundary = aws_iam_policy.deploy_boundary.arn 7 | assume_role_policy = templatefile("${path.module}/policies/assume-role-for-account.json", { 8 | account_id = var.account_id 9 | aws_partition = var.aws_partition 10 | }) 11 | } 12 | 13 | resource "aws_iam_policy" "boundary" { 14 | name = "${var.prefix}-boundary" 15 | path = "/${var.namespaces.boundary_namespace}/" 16 | 17 | policy = templatefile("${path.module}/policies/boundary.json", { 18 | role_namespace = var.namespaces.role_namespace 19 | account_id = data.aws_caller_identity.current.account_id 20 | aws_partition = var.aws_partition 21 | }) 22 | } 23 | 24 | resource "aws_iam_policy" "deploy" { 25 | name = "${var.prefix}-terraform" 26 | path = "/" 27 | 28 | policy = templatefile("${path.module}/policies/deploy-policy.json", { 29 | account_id = data.aws_caller_identity.current.account_id 30 | }) 31 | } 32 | 33 | resource "aws_iam_role_policy_attachment" "deploy" { 34 | role = aws_iam_role.deploy.name 35 | policy_arn = aws_iam_policy.deploy.arn 36 | } 37 | 38 | resource "aws_iam_policy" "deploy_boundary" { 39 | name = "${var.prefix}-terraform-boundary" 40 | path = "/${var.namespaces.boundary_namespace}/" 41 | 42 | policy = templatefile("${path.module}/policies/deploy-boundary.json", { 43 | account_id = data.aws_caller_identity.current.account_id 44 | role_namespace = var.namespaces.role_namespace 45 | policy_namespace = var.namespaces.policy_namespace 46 | instance_profile_namespace = var.namespaces.instance_profile_namespace 47 | boundary_namespace = var.namespaces.boundary_namespace 48 | permission_boundary = aws_iam_policy.boundary.arn 49 | aws_partition = var.aws_partition 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /modules/runners/pool/policies/lambda-pool.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:DescribeInstances", 8 | "ec2:DescribeTags", 9 | "ec2:RunInstances", 10 | "ec2:CreateFleet", 11 | "ec2:CreateTags" 12 | ], 13 | "Resource": [ 14 | "*" 15 | ] 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Action": "iam:PassRole", 20 | "Resource": "${arn_runner_instance_role}" 21 | }, 22 | { 23 | "Effect": "Allow", 24 | "Action": [ 25 | "ssm:PutParameter" 26 | ], 27 | "Resource": "*" 28 | }, 29 | { 30 | "Effect": "Allow", 31 | "Action": [ 32 | "ssm:GetParameter" 33 | ], 34 | "Resource": [ 35 | "${github_app_key_base64_arn}", 36 | "${github_app_id_arn}" 37 | ] 38 | %{ if kms_key_arn != "" ~} 39 | }, 40 | { 41 | "Effect": "Allow", 42 | "Action": [ 43 | "kms:Decrypt" 44 | ], 45 | "Resource": "${kms_key_arn}" 46 | %{ endif ~} 47 | %{ if ami_kms_key_arn != "" ~} 48 | }, 49 | { 50 | "Effect": "Allow", 51 | "Action": [ 52 | "kms:DescribeKey", 53 | "kms:ReEncrypt*", 54 | "kms:Decrypt" 55 | ], 56 | "Resource": "${ami_kms_key_arn}" 57 | }, 58 | { 59 | "Effect": "Allow", 60 | "Action": [ 61 | "kms:CreateGrant" 62 | ], 63 | "Resource": "${ami_kms_key_arn}", 64 | "Condition": { 65 | "Bool": { 66 | "aws:ViaAWSService": "true" 67 | } 68 | } 69 | %{ endif ~} 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /examples/prebuilt/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = "prebuilt" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | data "aws_caller_identity" "current" {} 11 | 12 | module "base" { 13 | source = "../base" 14 | 15 | prefix = local.environment 16 | aws_region = local.aws_region 17 | } 18 | 19 | module "runners" { 20 | source = "../../" 21 | create_service_linked_role_spot = true 22 | aws_region = local.aws_region 23 | vpc_id = module.base.vpc.vpc_id 24 | subnet_ids = module.base.vpc.private_subnets 25 | 26 | prefix = local.environment 27 | enable_organization_runners = false 28 | 29 | github_app = { 30 | key_base64 = var.github_app.key_base64 31 | id = var.github_app.id 32 | webhook_secret = random_id.random.hex 33 | } 34 | 35 | webhook_lambda_zip = "../lambdas-download/webhook.zip" 36 | runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip" 37 | runners_lambda_zip = "../lambdas-download/runners.zip" 38 | 39 | runner_extra_labels = "default,example" 40 | 41 | runner_os = var.runner_os 42 | 43 | # configure your pre-built AMI 44 | enable_userdata = false 45 | ami_filter = { name = [var.ami_name_filter] } 46 | ami_owners = [data.aws_caller_identity.current.account_id] 47 | 48 | # Look up runner AMI ID from an AWS SSM parameter (overrides ami_filter at instance launch time) 49 | # NOTE: the parameter must be managed outside of this module (e.g. in a runner AMI build workflow) 50 | # ami_id_ssm_parameter_name = "my-runner-ami-id" 51 | 52 | # disable binary syncer since github agent is already installed in the AMI. 53 | enable_runner_binaries_syncer = false 54 | 55 | # enable access to the runners via SSM 56 | enable_ssm_on_runners = true 57 | 58 | # override delay of events in seconds 59 | delay_webhook_event = 5 60 | 61 | # override scaling down 62 | scale_down_schedule_expression = "cron(* * * * ? *)" 63 | } 64 | -------------------------------------------------------------------------------- /examples/lambdas-download/README.md: -------------------------------------------------------------------------------- 1 | # Wrapper module to download lambda's for running the examples 2 | 3 | Module is used by examples to download Lambda distribution from the GitHub release. 4 | 5 | ```bash 6 | terraform init 7 | terraform apply -var=module_version= 8 | ``` 9 | 10 | 11 | ## Requirements 12 | 13 | | Name | Version | 14 | |------|---------| 15 | | [terraform](#requirement\_terraform) | >= 1 | 16 | 17 | ## Providers 18 | 19 | No providers. 20 | 21 | ## Modules 22 | 23 | | Name | Source | Version | 24 | |------|--------|---------| 25 | | [lambdas](#module\_lambdas) | ../../modules/download-lambda | n/a | 26 | 27 | ## Resources 28 | 29 | No resources. 30 | 31 | ## Inputs 32 | 33 | | Name | Description | Type | Default | Required | 34 | |------|-------------|------|---------|:--------:| 35 | | [module\_version](#input\_module\_version) | Module release version. | `string` | n/a | yes | 36 | 37 | ## Outputs 38 | 39 | | Name | Description | 40 | |------|-------------| 41 | | [files](#output\_files) | n/a | 42 | 43 | 44 | 45 | ## Requirements 46 | 47 | | Name | Version | 48 | |------|---------| 49 | | [terraform](#requirement\_terraform) | >= 1 | 50 | 51 | ## Providers 52 | 53 | No providers. 54 | 55 | ## Modules 56 | 57 | | Name | Source | Version | 58 | |------|--------|---------| 59 | | [lambdas](#module\_lambdas) | ../../modules/download-lambda | n/a | 60 | 61 | ## Resources 62 | 63 | No resources. 64 | 65 | ## Inputs 66 | 67 | | Name | Description | Type | Default | Required | 68 | |------|-------------|------|---------|:--------:| 69 | | [module\_version](#input\_module\_version) | Module release version. | `string` | n/a | yes | 70 | 71 | ## Outputs 72 | 73 | | Name | Description | 74 | |------|-------------| 75 | | [files](#output\_files) | n/a | 76 | -------------------------------------------------------------------------------- /modules/webhook/lambdas/webhook/src/sqs/index.ts: -------------------------------------------------------------------------------- 1 | import { SQS, SendMessageCommandInput } from '@aws-sdk/client-sqs'; 2 | import { WorkflowJobEvent } from '@octokit/webhooks-types'; 3 | 4 | import { createChildLogger } from '../logger'; 5 | 6 | const logger = createChildLogger('sqs'); 7 | 8 | export interface ActionRequestMessage { 9 | id: number; 10 | eventType: string; 11 | repositoryName: string; 12 | repositoryOwner: string; 13 | installationId: number; 14 | queueId: string; 15 | queueFifo: boolean; 16 | } 17 | 18 | export interface MatcherConfig { 19 | labelMatchers: string[][]; 20 | exactMatch: boolean; 21 | } 22 | 23 | export interface QueueConfig { 24 | matcherConfig: MatcherConfig; 25 | id: string; 26 | arn: string; 27 | fifo: boolean; 28 | } 29 | export interface GithubWorkflowEvent { 30 | workflowJobEvent: WorkflowJobEvent; 31 | } 32 | 33 | export const sendActionRequest = async (message: ActionRequestMessage): Promise => { 34 | const sqs = new SQS({ region: process.env.AWS_REGION }); 35 | 36 | const sqsMessage: SendMessageCommandInput = { 37 | QueueUrl: message.queueId, 38 | MessageBody: JSON.stringify(message), 39 | }; 40 | 41 | logger.debug(`sending message to SQS: ${JSON.stringify(sqsMessage)}`); 42 | if (message.queueFifo) { 43 | sqsMessage.MessageGroupId = String(message.id); 44 | } 45 | 46 | await sqs.sendMessage(sqsMessage); 47 | }; 48 | 49 | export const sendWebhookEventToWorkflowJobQueue = async (message: GithubWorkflowEvent): Promise => { 50 | const webhook_events_workflow_job_queue = process.env.SQS_WORKFLOW_JOB_QUEUE || undefined; 51 | 52 | if (webhook_events_workflow_job_queue != undefined) { 53 | const sqs = new SQS({ region: process.env.AWS_REGION }); 54 | const sqsMessage: SendMessageCommandInput = { 55 | QueueUrl: String(process.env.SQS_WORKFLOW_JOB_QUEUE), 56 | MessageBody: JSON.stringify(message), 57 | }; 58 | logger.debug(`Sending Webhook events to the workflow job queue: ${webhook_events_workflow_job_queue}`); 59 | try { 60 | await sqs.sendMessage(sqsMessage); 61 | } catch (e) { 62 | logger.warn(`Error in sending webhook events to workflow job queue: ${(e as Error).message}`); 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /modules/runners/templates/install-runner.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | ## install the runner 4 | 5 | s3_location=${S3_LOCATION_RUNNER_DISTRIBUTION} 6 | architecture=${RUNNER_ARCHITECTURE} 7 | 8 | if [ -z "$RUNNER_TARBALL_URL" ] && [ -z "$s3_location" ]; then 9 | echo "Neither RUNNER_TARBALL_URL or s3_location are set" 10 | exit 1 11 | fi 12 | 13 | file_name="actions-runner.tar.gz" 14 | 15 | echo "Setting up GH Actions runner tool cache" 16 | # Required for various */setup-* actions to work, location is also know by various environment 17 | # variable names in the actions/runner software : RUNNER_TOOL_CACHE / RUNNER_TOOLSDIRECTORY / AGENT_TOOLSDIRECTORY 18 | # Warning, not all setup actions support the env vars and so this specific path must be created regardless 19 | mkdir -p /opt/hostedtoolcache 20 | 21 | echo "Creating actions-runner directory for the GH Action installation" 22 | cd /opt/ 23 | mkdir -p actions-runner && cd actions-runner 24 | 25 | 26 | if [[ -n "$RUNNER_TARBALL_URL" ]]; then 27 | echo "Downloading the GH Action runner from $RUNNER_TARBALL_URL to $file_name" 28 | curl -o $file_name -L "$RUNNER_TARBALL_URL" 29 | else 30 | echo "Retrieving TOKEN from AWS API" 31 | token=$(curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 180") 32 | 33 | region=$(curl -f -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) 34 | echo "Retrieved REGION from AWS API ($region)" 35 | 36 | echo "Downloading the GH Action runner from s3 bucket $s3_location" 37 | aws s3 cp "$s3_location" "$file_name" --region "$region" 38 | fi 39 | 40 | echo "Un-tar action runner" 41 | tar xzf ./$file_name 42 | echo "Delete tar file" 43 | rm -rf $file_name 44 | 45 | os_id=$(awk -F= '/^ID/{print $2}' /etc/os-release) 46 | echo OS: $os_id 47 | 48 | # Install libicu60 for arm64 on non-ubuntu 49 | if [[ "$architecture" == "arm64" ]] && [[ ! "$os_id" =~ ^ubuntu.* ]]; then 50 | yum install -y libicu60 51 | fi 52 | 53 | # Install dependencies for ubuntu 54 | if [[ "$os_id" =~ ^ubuntu.* ]]; then 55 | echo "Installing dependencies" 56 | ./bin/installdependencies.sh 57 | fi 58 | 59 | echo "Set file ownership of action runner" 60 | chown -R "$user_name":"$user_name" . 61 | chown -R "$user_name":"$user_name" /opt/hostedtoolcache 62 | -------------------------------------------------------------------------------- /modules/ssm/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 7 | | [aws](#requirement\_aws) | ~> 4.0 | 8 | 9 | ## Providers 10 | 11 | | Name | Version | 12 | |------|---------| 13 | | [aws](#provider\_aws) | ~> 4.0 | 14 | 15 | ## Modules 16 | 17 | No modules. 18 | 19 | ## Resources 20 | 21 | | Name | Type | 22 | |------|------| 23 | | [aws_ssm_parameter.github_app_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 24 | | [aws_ssm_parameter.github_app_key_base64](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 25 | | [aws_ssm_parameter.github_app_webhook_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 26 | 27 | ## Inputs 28 | 29 | | Name | Description | Type | Default | Required | 30 | |------|-------------|------|---------|:--------:| 31 | | [environment](#input\_environment) | A name that identifies the environment, used as prefix and for tagging. | `string` | `null` | no | 32 | | [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
object({
key_base64 = string
id = string
webhook_secret = string
})
| n/a | yes | 33 | | [kms\_key\_arn](#input\_kms\_key\_arn) | Optional CMK Key ARN to be used for Parameter Store. | `string` | `null` | no | 34 | | [path\_prefix](#input\_path\_prefix) | The path prefix used for naming resources | `string` | n/a | yes | 35 | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no | 36 | 37 | ## Outputs 38 | 39 | | Name | Description | 40 | |------|-------------| 41 | | [parameters](#output\_parameters) | n/a | 42 | -------------------------------------------------------------------------------- /modules/runners/policies/lambda-scale-up.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ec2:DescribeInstances", 8 | "ec2:DescribeTags", 9 | "ec2:RunInstances", 10 | "ec2:CreateFleet", 11 | "ec2:CreateTags" 12 | ], 13 | "Resource": [ 14 | "*" 15 | ] 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Action": "iam:PassRole", 20 | "Resource": "${arn_runner_instance_role}" 21 | }, 22 | { 23 | "Effect": "Allow", 24 | "Action": [ 25 | "ssm:PutParameter" 26 | ], 27 | "Resource": "*" 28 | }, 29 | { 30 | "Effect": "Allow", 31 | "Action": [ 32 | "ssm:GetParameter" 33 | ], 34 | "Resource": [ 35 | "${github_app_key_base64_arn}", 36 | "${github_app_id_arn}" 37 | ] 38 | }, 39 | { 40 | "Effect": "Allow", 41 | "Action": [ 42 | "sqs:ReceiveMessage", 43 | "sqs:GetQueueAttributes", 44 | "sqs:DeleteMessage" 45 | ], 46 | "Resource": "${sqs_arn}" 47 | %{ if kms_key_arn != "" ~} 48 | }, 49 | { 50 | "Effect": "Allow", 51 | "Action": [ 52 | "kms:Decrypt" 53 | ], 54 | "Resource": "${kms_key_arn}" 55 | %{ endif ~} 56 | %{ if ami_kms_key_arn != "" ~} 57 | }, 58 | { 59 | "Effect": "Allow", 60 | "Action": [ 61 | "kms:DescribeKey", 62 | "kms:ReEncrypt*", 63 | "kms:Decrypt" 64 | ], 65 | "Resource": "${ami_kms_key_arn}" 66 | }, 67 | { 68 | "Effect": "Allow", 69 | "Action": [ 70 | "kms:CreateGrant" 71 | ], 72 | "Resource": "${ami_kms_key_arn}", 73 | "Condition": { 74 | "Bool": { 75 | "aws:ViaAWSService": "true" 76 | } 77 | } 78 | %{ endif ~} 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /images/windows-core-2019/windows-provisioner.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Continue" 2 | $VerbosePreference = "Continue" 3 | 4 | # Install Chocolatey 5 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 6 | $env:chocolateyUseWindowsCompression = 'true' 7 | Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression 8 | 9 | # Add Chocolatey to powershell profile 10 | $ChocoProfileValue = @' 11 | $ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" 12 | if (Test-Path($ChocolateyProfile)) { 13 | Import-Module "$ChocolateyProfile" 14 | } 15 | 16 | refreshenv 17 | '@ 18 | # Write it to the $profile location 19 | Set-Content -Path "$PsHome\Microsoft.PowerShell_profile.ps1" -Value $ChocoProfileValue -Force 20 | # Source it 21 | . "$PsHome\Microsoft.PowerShell_profile.ps1" 22 | 23 | refreshenv 24 | 25 | Write-Host "Installing cloudwatch agent..." 26 | Invoke-WebRequest -Uri https://s3.amazonaws.com/amazoncloudwatch-agent/windows/amd64/latest/amazon-cloudwatch-agent.msi -OutFile C:\amazon-cloudwatch-agent.msi 27 | $cloudwatchParams = '/i', 'C:\amazon-cloudwatch-agent.msi', '/qn', '/L*v', 'C:\CloudwatchInstall.log' 28 | Start-Process "msiexec.exe" $cloudwatchParams -Wait -NoNewWindow 29 | Remove-Item C:\amazon-cloudwatch-agent.msi 30 | 31 | # Install dependent tools 32 | Write-Host "Installing additional development tools" 33 | choco install git awscli -y 34 | refreshenv 35 | 36 | Write-Host "Creating actions-runner directory for the GH Action installtion" 37 | New-Item -ItemType Directory -Path C:\actions-runner ; Set-Location C:\actions-runner 38 | 39 | Write-Host "Downloading the GH Action runner from ${action_runner_url}" 40 | Invoke-WebRequest -Uri ${action_runner_url} -OutFile actions-runner.zip 41 | 42 | Write-Host "Un-zip action runner" 43 | Expand-Archive -Path actions-runner.zip -DestinationPath . 44 | 45 | Write-Host "Delete zip file" 46 | Remove-Item actions-runner.zip 47 | 48 | $action = New-ScheduledTaskAction -WorkingDirectory "C:\actions-runner" -Execute "PowerShell.exe" -Argument "-File C:\start-runner.ps1" 49 | $trigger = New-ScheduledTaskTrigger -AtStartup 50 | Register-ScheduledTask -TaskName "runnerinit" -Action $action -Trigger $trigger -User System -RunLevel Highest -Force 51 | 52 | C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule -------------------------------------------------------------------------------- /images/windows-core-2022/windows-provisioner.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Continue" 2 | $VerbosePreference = "Continue" 3 | 4 | # Install Chocolatey 5 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 6 | $env:chocolateyUseWindowsCompression = 'true' 7 | Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression 8 | 9 | # Add Chocolatey to powershell profile 10 | $ChocoProfileValue = @' 11 | $ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" 12 | if (Test-Path($ChocolateyProfile)) { 13 | Import-Module "$ChocolateyProfile" 14 | } 15 | 16 | refreshenv 17 | '@ 18 | # Write it to the $profile location 19 | Set-Content -Path "$PsHome\Microsoft.PowerShell_profile.ps1" -Value $ChocoProfileValue -Force 20 | # Source it 21 | . "$PsHome\Microsoft.PowerShell_profile.ps1" 22 | 23 | refreshenv 24 | 25 | Write-Host "Installing cloudwatch agent..." 26 | Invoke-WebRequest -Uri https://s3.amazonaws.com/amazoncloudwatch-agent/windows/amd64/latest/amazon-cloudwatch-agent.msi -OutFile C:\amazon-cloudwatch-agent.msi 27 | $cloudwatchParams = '/i', 'C:\amazon-cloudwatch-agent.msi', '/qn', '/L*v', 'C:\CloudwatchInstall.log' 28 | Start-Process "msiexec.exe" $cloudwatchParams -Wait -NoNewWindow 29 | Remove-Item C:\amazon-cloudwatch-agent.msi 30 | 31 | # Install dependent tools 32 | Write-Host "Installing additional development tools" 33 | choco install git awscli -y 34 | refreshenv 35 | 36 | Write-Host "Creating actions-runner directory for the GH Action installtion" 37 | New-Item -ItemType Directory -Path C:\actions-runner ; Set-Location C:\actions-runner 38 | 39 | Write-Host "Downloading the GH Action runner from ${action_runner_url}" 40 | Invoke-WebRequest -Uri ${action_runner_url} -OutFile actions-runner.zip 41 | 42 | Write-Host "Un-zip action runner" 43 | Expand-Archive -Path actions-runner.zip -DestinationPath . 44 | 45 | Write-Host "Delete zip file" 46 | Remove-Item actions-runner.zip 47 | 48 | $action = New-ScheduledTaskAction -WorkingDirectory "C:\actions-runner" -Execute "PowerShell.exe" -Argument "-File C:\start-runner.ps1" 49 | $trigger = New-ScheduledTaskTrigger -AtStartup 50 | Register-ScheduledTask -TaskName "runnerinit" -Action $action -Trigger $trigger -User System -RunLevel Highest -Force 51 | 52 | C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule -------------------------------------------------------------------------------- /modules/runners/pool/variables.tf: -------------------------------------------------------------------------------- 1 | variable "config" { 2 | type = object({ 3 | lambda = object({ 4 | log_level = string 5 | logging_retention_in_days = number 6 | logging_kms_key_id = string 7 | reserved_concurrent_executions = number 8 | s3_bucket = string 9 | s3_key = string 10 | s3_object_version = string 11 | security_group_ids = list(string) 12 | runtime = string 13 | architecture = string 14 | timeout = number 15 | zip = string 16 | subnet_ids = list(string) 17 | }) 18 | tags = map(string) 19 | ghes = object({ 20 | url = string 21 | ssl_verify = string 22 | }) 23 | github_app_parameters = object({ 24 | key_base64 = map(string) 25 | id = map(string) 26 | }) 27 | subnet_ids = list(string) 28 | runner = object({ 29 | disable_runner_autoupdate = bool 30 | ephemeral = bool 31 | boot_time_in_minutes = number 32 | extra_labels = string 33 | launch_template = object({ 34 | name = string 35 | }) 36 | group_name = string 37 | name_prefix = string 38 | pool_owner = string 39 | role = object({ 40 | arn = string 41 | }) 42 | }) 43 | instance_types = list(string) 44 | instance_target_capacity_type = string 45 | instance_allocation_strategy = string 46 | instance_max_spot_price = string 47 | prefix = string 48 | pool = list(object({ 49 | schedule_expression = string 50 | size = number 51 | })) 52 | role_permissions_boundary = string 53 | kms_key_arn = string 54 | ami_kms_key_arn = string 55 | role_path = string 56 | ssm_token_path = string 57 | ami_id_ssm_parameter_name = string 58 | ami_id_ssm_parameter_read_policy_arn = string 59 | }) 60 | } 61 | 62 | variable "aws_partition" { 63 | description = "(optional) partition for the arn if not 'aws'" 64 | type = string 65 | default = "aws" 66 | } 67 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "runners" { 2 | value = { 3 | launch_template_name = module.runners.launch_template.name 4 | launch_template_id = module.runners.launch_template.id 5 | launch_template_version = module.runners.launch_template.latest_version 6 | launch_template_ami_id = module.runners.launch_template.image_id 7 | lambda_up = module.runners.lambda_scale_up 8 | lambda_up_log_group = module.runners.lambda_scale_up_log_group 9 | lambda_down = module.runners.lambda_scale_down 10 | lambda_down_log_group = module.runners.lambda_scale_down_log_group 11 | lambda_pool = module.runners.lambda_pool 12 | lambda_pool_log_group = module.runners.lambda_pool_log_group 13 | role_runner = module.runners.role_runner 14 | role_scale_up = module.runners.role_scale_up 15 | role_scale_down = module.runners.role_scale_down 16 | role_pool = module.runners.role_pool 17 | runners_log_groups = module.runners.runners_log_groups 18 | labels = sort(split(",", local.runner_labels)) 19 | logfiles = module.runners.logfiles 20 | } 21 | } 22 | 23 | output "binaries_syncer" { 24 | value = var.enable_runner_binaries_syncer ? { 25 | lambda = module.runner_binaries[0].lambda 26 | lambda_log_group = module.runner_binaries[0].lambda_log_group 27 | lambda_role = module.runner_binaries[0].lambda_role 28 | location = "s3://${module.runner_binaries[0].bucket.id}/module.runner_binaries[0].bucket.key" 29 | bucket = module.runner_binaries[0].bucket 30 | } : null 31 | } 32 | 33 | output "webhook" { 34 | value = { 35 | gateway = module.webhook.gateway 36 | lambda = module.webhook.lambda 37 | lambda_log_group = module.webhook.lambda_log_group 38 | lambda_role = module.webhook.role 39 | endpoint = "${module.webhook.gateway.api_endpoint}/${module.webhook.endpoint_relative_path}" 40 | } 41 | } 42 | 43 | output "ssm_parameters" { 44 | value = module.ssm.parameters 45 | } 46 | 47 | 48 | output "queues" { 49 | description = "SQS queues." 50 | value = { 51 | build_queue_arn = aws_sqs_queue.queued_builds.arn 52 | build_queue_dlq_arn = var.redrive_build_queue.enabled ? aws_sqs_queue.queued_builds_dlq[0].arn : null 53 | webhook_workflow_job_queue = try(aws_sqs_queue.webhook_events_workflow_job_queue[0], null) != null ? aws_sqs_queue.webhook_events_workflow_job_queue[0].arn : "" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/arm64/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = "default" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | 11 | ################################################################################ 12 | ### Hybrid account 13 | ################################################################################ 14 | 15 | module "runners" { 16 | source = "../../" 17 | create_service_linked_role_spot = true 18 | aws_region = local.aws_region 19 | vpc_id = module.vpc.vpc_id 20 | subnet_ids = module.vpc.private_subnets 21 | 22 | prefix = local.environment 23 | tags = { 24 | Project = "ProjectX" 25 | } 26 | 27 | github_app = { 28 | key_base64 = var.github_app.key_base64 29 | id = var.github_app.id 30 | webhook_secret = random_id.random.hex 31 | } 32 | 33 | # Grab zip files via lambda_download, will automatically get the ARM64 build 34 | webhook_lambda_zip = "../lambdas-download/webhook.zip" 35 | runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip" 36 | runners_lambda_zip = "../lambdas-download/runners.zip" 37 | 38 | enable_organization_runners = false 39 | # Runners will automatically get the "arm64" label 40 | runner_extra_labels = "default,example" 41 | 42 | # enable access to the runners via SSM 43 | enable_ssm_on_runners = true 44 | 45 | # use S3 or KMS SSE to runners S3 bucket 46 | # runner_binaries_s3_sse_configuration = { 47 | # rule = { 48 | # apply_server_side_encryption_by_default = { 49 | # sse_algorithm = "AES256" 50 | # } 51 | # } 52 | # } 53 | 54 | # Uncommet idle config to have idle runners from 9 to 5 in time zone Amsterdam 55 | # idle_config = [{ 56 | # cron = "* * 9-17 * * *" 57 | # timeZone = "Europe/Amsterdam" 58 | # idleCount = 1 59 | # }] 60 | 61 | # Let the module manage the service linked role 62 | # create_service_linked_role_spot = true 63 | 64 | runner_architecture = "arm64" 65 | # Ensure all instance types have ARM64 architecture (ie. AWS Graviton processors) 66 | instance_types = ["t4g.large", "c6g.large"] 67 | 68 | # override delay of events in seconds 69 | delay_webhook_event = 5 70 | runners_maximum_count = 1 71 | 72 | # set up a fifo queue to remain order 73 | enable_fifo_build_queue = true 74 | 75 | # override scaling down 76 | scale_down_schedule_expression = "cron(* * * * ? *)" 77 | } 78 | -------------------------------------------------------------------------------- /examples/ubuntu/templates/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1 3 | 4 | 5 | # AWS suggest to create a log for debug purpose based on https://aws.amazon.com/premiumsupport/knowledge-center/ec2-linux-log-user-data/ 6 | # As side effect all command, set +x disable debugging explicitly. 7 | # 8 | # An alternative for masking tokens could be: exec > >(sed 's/--token\ [^ ]* /--token\ *** /g' > /var/log/user-data.log) 2>&1 9 | set +x 10 | 11 | %{ if enable_debug_logging } 12 | set -x 13 | %{ endif } 14 | 15 | ${pre_install} 16 | 17 | # Install AWS CLI 18 | apt-get update 19 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 20 | awscli \ 21 | build-essential \ 22 | curl \ 23 | git \ 24 | iptables \ 25 | jq \ 26 | uidmap \ 27 | unzip \ 28 | wget 29 | 30 | user_name=ubuntu 31 | user_id=$(id -ru $user_name) 32 | 33 | # install and configure cloudwatch logging agent 34 | wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb 35 | dpkg -i -E ./amazon-cloudwatch-agent.deb 36 | amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c ssm:${ssm_key_cloudwatch_agent_config} 37 | 38 | # configure systemd for running service in users accounts 39 | cat >/etc/systemd/user@UID.service <<-EOF 40 | 41 | [Unit] 42 | Description=User Manager for UID %i 43 | After=user-runtime-dir@%i.service 44 | Wants=user-runtime-dir@%i.service 45 | 46 | [Service] 47 | LimitNOFILE=infinity 48 | LimitNPROC=infinity 49 | User=%i 50 | PAMName=systemd-user 51 | Type=notify 52 | 53 | [Install] 54 | WantedBy=default.target 55 | 56 | EOF 57 | 58 | echo export XDG_RUNTIME_DIR=/run/user/$user_id >>/home/$user_name/.bashrc 59 | 60 | systemctl daemon-reload 61 | systemctl enable user@UID.service 62 | systemctl start user@UID.service 63 | 64 | curl -fsSL https://get.docker.com/rootless >>/opt/rootless.sh && chmod 755 /opt/rootless.sh 65 | su -l $user_name -c /opt/rootless.sh 66 | echo export DOCKER_HOST=unix:///run/user/$user_id/docker.sock >>/home/$user_name/.bashrc 67 | echo export PATH=/home/$user_name/bin:$PATH >>/home/$user_name/.bashrc 68 | 69 | # Run docker service by default 70 | loginctl enable-linger $user_name 71 | su -l $user_name -c "systemctl --user enable docker" 72 | 73 | ${install_runner} 74 | 75 | # config runner for rootless docker 76 | cd /opt/actions-runner/ 77 | echo DOCKER_HOST=unix:///run/user/$user_id/docker.sock >>.env 78 | echo PATH=/home/$user_name/bin:$PATH >>.env 79 | 80 | ${post_install} 81 | 82 | cd /opt/actions-runner 83 | 84 | ${start_runner} 85 | -------------------------------------------------------------------------------- /examples/multi-runner/templates/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1 3 | 4 | 5 | # AWS suggest to create a log for debug purpose based on https://aws.amazon.com/premiumsupport/knowledge-center/ec2-linux-log-user-data/ 6 | # As side effect all command, set +x disable debugging explicitly. 7 | # 8 | # An alternative for masking tokens could be: exec > >(sed 's/--token\ [^ ]* /--token\ *** /g' > /var/log/user-data.log) 2>&1 9 | set +x 10 | 11 | %{ if enable_debug_logging } 12 | set -x 13 | %{ endif } 14 | 15 | ${pre_install} 16 | 17 | # Install AWS CLI 18 | apt-get update 19 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 20 | awscli \ 21 | build-essential \ 22 | curl \ 23 | git \ 24 | iptables \ 25 | jq \ 26 | uidmap \ 27 | unzip \ 28 | wget 29 | 30 | user_name=ubuntu 31 | user_id=$(id -ru $user_name) 32 | 33 | # install and configure cloudwatch logging agent 34 | wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb 35 | dpkg -i -E ./amazon-cloudwatch-agent.deb 36 | amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c ssm:${ssm_key_cloudwatch_agent_config} 37 | 38 | # configure systemd for running service in users accounts 39 | cat >/etc/systemd/user@UID.service <<-EOF 40 | 41 | [Unit] 42 | Description=User Manager for UID %i 43 | After=user-runtime-dir@%i.service 44 | Wants=user-runtime-dir@%i.service 45 | 46 | [Service] 47 | LimitNOFILE=infinity 48 | LimitNPROC=infinity 49 | User=%i 50 | PAMName=systemd-user 51 | Type=notify 52 | 53 | [Install] 54 | WantedBy=default.target 55 | 56 | EOF 57 | 58 | echo export XDG_RUNTIME_DIR=/run/user/$user_id >>/home/$user_name/.bashrc 59 | 60 | systemctl daemon-reload 61 | systemctl enable user@UID.service 62 | systemctl start user@UID.service 63 | 64 | curl -fsSL https://get.docker.com/rootless >>/opt/rootless.sh && chmod 755 /opt/rootless.sh 65 | su -l $user_name -c /opt/rootless.sh 66 | echo export DOCKER_HOST=unix:///run/user/$user_id/docker.sock >>/home/$user_name/.bashrc 67 | echo export PATH=/home/$user_name/bin:$PATH >>/home/$user_name/.bashrc 68 | 69 | # Run docker service by default 70 | loginctl enable-linger $user_name 71 | su -l $user_name -c "systemctl --user enable docker" 72 | 73 | ${install_runner} 74 | 75 | # config runner for rootless docker 76 | cd /opt/actions-runner/ 77 | echo DOCKER_HOST=unix:///run/user/$user_id/docker.sock >>.env 78 | echo PATH=/home/$user_name/bin:$PATH >>.env 79 | 80 | ${post_install} 81 | 82 | cd /opt/actions-runner 83 | 84 | ${start_runner} 85 | -------------------------------------------------------------------------------- /examples/lambdas-download/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.46.0" 6 | constraints = "~> 4.0" 7 | hashes = [ 8 | "h1:m7RCtncaQbSD9VhNTX2xbuZY3TlYnUrluvmYZeYHb1s=", 9 | "zh:1678e6a4bdb3d81a6713adc62ca0fdb8250c584e10c10d1daca72316e9db8df2", 10 | "zh:329903acf86ef6072502736dff4c43c2b50f762a958f76aa924e2d74c7fca1e3", 11 | "zh:33db8131fe0ec7e1d9f30bc9f65c2440e9c1f708d681b6062757a351f1df7ce6", 12 | "zh:3a3b010bc393784c16f4b6cdce7f76db93d5efa323fce4920bfea9e9ba6abe44", 13 | "zh:979e2713a5759a7483a065e149e3cb69db9225326fc0457fa3fc3a48aed0c63f", 14 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 15 | "zh:9efcf0067e16ad53da7504178a05eb2118770b4ae00c193c10ecad4cbfce308e", 16 | "zh:a10655bf1b6376ab7f3e55efadf54dc70f7bd07ca11369557c312095076f9d62", 17 | "zh:b0394dd42cbd2a718a7dd7ae0283f04769aaf8b3d52664e141da59c0171a11ab", 18 | "zh:b958e614c2cf6d9c05a6ad5e94dc5c04b97ebfb84415da068be5a081b5ebbe24", 19 | "zh:ba5069e624210c63ad9e633a8eb0108b21f2322bc4967ba2b82d09168c466888", 20 | "zh:d7dfa597a17186e7f4d741dd7111849f1c0dd6f7ebc983043d8262d2fb37b408", 21 | "zh:e8a641ca2c99f96d64fa2725875e797273984981d3e54772a2823541c44e3cd3", 22 | "zh:f89898b7067c4246293a8007f59f5cfcac7b8dd251d39886c7a53ba596251466", 23 | "zh:fb1e1df1d5cc208e08a850f8e84423bce080f01f5e901791c79df369d3ed52f2", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/null" { 28 | version = "3.2.1" 29 | constraints = "~> 3.0" 30 | hashes = [ 31 | "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", 32 | "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", 33 | "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", 34 | "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", 35 | "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", 36 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 37 | "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", 38 | "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", 39 | "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", 40 | "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", 41 | "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", 42 | "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", 43 | "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /modules/runners/policies-runner.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | resource "aws_iam_role" "runner" { 4 | name = "${var.prefix}-runner-role" 5 | assume_role_policy = templatefile("${path.module}/policies/instance-role-trust-policy.json", {}) 6 | path = local.role_path 7 | permissions_boundary = var.role_permissions_boundary 8 | tags = local.tags 9 | } 10 | 11 | resource "aws_iam_instance_profile" "runner" { 12 | name = "${var.prefix}-runner-profile" 13 | role = aws_iam_role.runner.name 14 | path = local.instance_profile_path 15 | } 16 | 17 | resource "aws_iam_role_policy" "runner_session_manager_aws_managed" { 18 | name = "runner-ssm-session" 19 | count = var.enable_ssm_on_runners ? 1 : 0 20 | role = aws_iam_role.runner.name 21 | policy = templatefile("${path.module}/policies/instance-ssm-policy.json", {}) 22 | } 23 | 24 | resource "aws_iam_role_policy" "ssm_parameters" { 25 | name = "runner-ssm-parameters" 26 | role = aws_iam_role.runner.name 27 | policy = templatefile("${path.module}/policies/instance-ssm-parameters-policy.json", 28 | { 29 | arn_ssm_parameters_path_tokens = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.ssm_paths.root}/${var.ssm_paths.tokens}" 30 | arn_ssm_parameters_path_config = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.ssm_paths.root}/${var.ssm_paths.config}" 31 | } 32 | ) 33 | } 34 | 35 | resource "aws_iam_role_policy" "dist_bucket" { 36 | count = var.enable_runner_binaries_syncer ? 1 : 0 37 | 38 | name = "distribution-bucket" 39 | role = aws_iam_role.runner.name 40 | policy = templatefile("${path.module}/policies/instance-s3-policy.json", 41 | { 42 | s3_arn = "${var.s3_runner_binaries.arn}/${var.s3_runner_binaries.key}" 43 | } 44 | ) 45 | } 46 | 47 | resource "aws_iam_role_policy" "describe_tags" { 48 | name = "runner-describe-tags" 49 | role = aws_iam_role.runner.name 50 | policy = file("${path.module}/policies/instance-describe-tags-policy.json") 51 | } 52 | 53 | resource "aws_iam_role_policy_attachment" "managed_policies" { 54 | count = length(var.runner_iam_role_managed_policy_arns) 55 | role = aws_iam_role.runner.name 56 | policy_arn = element(var.runner_iam_role_managed_policy_arns, count.index) 57 | } 58 | 59 | 60 | resource "aws_iam_role_policy" "ec2" { 61 | name = "ec2" 62 | role = aws_iam_role.runner.name 63 | policy = templatefile("${path.module}/policies/instance-ec2.json", {}) 64 | } 65 | 66 | // see also logging.tf for logging and metrics policies 67 | -------------------------------------------------------------------------------- /modules/runners/logging.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | runner_log_files = ( 3 | var.runner_log_files != null 4 | ? var.runner_log_files 5 | : [ 6 | { 7 | "prefix_log_group" : true, 8 | "file_path" : "/var/log/messages", 9 | "log_group_name" : "messages", 10 | "log_stream_name" : "{instance_id}" 11 | }, 12 | { 13 | "log_group_name" : "user_data", 14 | "prefix_log_group" : true, 15 | "file_path" : var.runner_os == "windows" ? "C:/UserData.log" : "/var/log/user-data.log", 16 | "log_stream_name" : "{instance_id}" 17 | }, 18 | { 19 | "log_group_name" : "runner", 20 | "prefix_log_group" : true, 21 | "file_path" : var.runner_os == "windows" ? "C:/actions-runner/_diag/Runner_*.log" : "/opt/actions-runner/_diag/Runner_**.log", 22 | "log_stream_name" : "{instance_id}" 23 | }, 24 | { 25 | "log_group_name" : "runner-startup", 26 | "prefix_log_group" : true, 27 | "file_path" : var.runner_os == "windows" ? "C:/runner-startup.log" : "/var/log/runner-startup.log", 28 | "log_stream_name" : "{instance_id}" 29 | } 30 | ] 31 | ) 32 | logfiles = var.enable_cloudwatch_agent ? [for l in local.runner_log_files : { 33 | "log_group_name" : l.prefix_log_group ? "/github-self-hosted-runners/${var.prefix}/${l.log_group_name}" : "/${l.log_group_name}" 34 | "log_stream_name" : l.log_stream_name 35 | "file_path" : l.file_path 36 | }] : [] 37 | 38 | loggroups_names = distinct([for l in local.logfiles : l.log_group_name]) 39 | 40 | } 41 | 42 | 43 | resource "aws_ssm_parameter" "cloudwatch_agent_config_runner" { 44 | count = var.enable_cloudwatch_agent ? 1 : 0 45 | name = "${var.ssm_paths.root}/${var.ssm_paths.config}/cloudwatch_agent_config_runner" 46 | type = "String" 47 | value = var.cloudwatch_config != null ? var.cloudwatch_config : templatefile("${path.module}/templates/cloudwatch_config.json", { 48 | logfiles = jsonencode(local.logfiles) 49 | }) 50 | tags = local.tags 51 | } 52 | 53 | resource "aws_cloudwatch_log_group" "gh_runners" { 54 | count = length(local.loggroups_names) 55 | name = local.loggroups_names[count.index] 56 | retention_in_days = var.logging_retention_in_days 57 | kms_key_id = var.logging_kms_key_id 58 | tags = local.tags 59 | } 60 | 61 | resource "aws_iam_role_policy" "cloudwatch" { 62 | count = var.enable_cloudwatch_agent ? 1 : 0 63 | name = "CloudWatchLogginAndMetrics" 64 | role = aws_iam_role.runner.name 65 | policy = templatefile("${path.module}/policies/instance-cloudwatch-policy.json", 66 | { 67 | ssm_parameter_arn = aws_ssm_parameter.cloudwatch_agent_config_runner[0].arn 68 | } 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /modules/runners/lambdas/runners/src/gh-auth/gh-auth.ts: -------------------------------------------------------------------------------- 1 | import { createAppAuth } from '@octokit/auth-app'; 2 | import { 3 | AppAuthOptions, 4 | AppAuthentication, 5 | AuthInterface, 6 | InstallationAccessTokenAuthentication, 7 | InstallationAuthOptions, 8 | StrategyOptions, 9 | } from '@octokit/auth-app/dist-types/types'; 10 | import { OctokitOptions } from '@octokit/core/dist-types/types'; 11 | import { request } from '@octokit/request'; 12 | import { Octokit } from '@octokit/rest'; 13 | 14 | import { getParameterValue } from '../aws/ssm'; 15 | import { createChildLogger } from '../logger'; 16 | 17 | const logger = createChildLogger('gh-auth'); 18 | 19 | export async function createOctoClient(token: string, ghesApiUrl = ''): Promise { 20 | const ocktokitOptions: OctokitOptions = { 21 | auth: token, 22 | }; 23 | if (ghesApiUrl) { 24 | ocktokitOptions.baseUrl = ghesApiUrl; 25 | ocktokitOptions.previews = ['antiope']; 26 | } 27 | return new Octokit(ocktokitOptions); 28 | } 29 | 30 | export async function createGithubAppAuth( 31 | installationId: number | undefined, 32 | ghesApiUrl = '', 33 | ): Promise { 34 | const auth = await createAuth(installationId, ghesApiUrl); 35 | const appAuthOptions: AppAuthOptions = { type: 'app' }; 36 | return auth(appAuthOptions); 37 | } 38 | 39 | export async function createGithubInstallationAuth( 40 | installationId: number | undefined, 41 | ghesApiUrl = '', 42 | ): Promise { 43 | const auth = await createAuth(installationId, ghesApiUrl); 44 | const installationAuthOptions: InstallationAuthOptions = { type: 'installation', installationId }; 45 | return auth(installationAuthOptions); 46 | } 47 | 48 | async function createAuth(installationId: number | undefined, ghesApiUrl: string): Promise { 49 | const appId = parseInt(await getParameterValue(process.env.PARAMETER_GITHUB_APP_ID_NAME)); 50 | let authOptions: StrategyOptions = { 51 | appId, 52 | privateKey: Buffer.from( 53 | await getParameterValue(process.env.PARAMETER_GITHUB_APP_KEY_BASE64_NAME), 54 | 'base64', 55 | // replace literal \n characters with new lines to allow the key to be stored as a 56 | // single line variable. This logic should match how the GitHub Terraform provider 57 | // processes private keys to retain compatibility between the projects 58 | ) 59 | .toString() 60 | .replace('/[\\n]/g', String.fromCharCode(10)), 61 | }; 62 | if (installationId) authOptions = { ...authOptions, installationId }; 63 | 64 | logger.debug(`GHES API URL: ${ghesApiUrl}`); 65 | if (ghesApiUrl) { 66 | authOptions.request = request.defaults({ 67 | baseUrl: ghesApiUrl, 68 | }); 69 | } 70 | return createAppAuth(authOptions); 71 | } 72 | -------------------------------------------------------------------------------- /docs/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7Vxbc5s4FP41ntl9SAYQNz/6kmS7TbdJ026m++KRjYxpMHJBjp38+hUgMLrYITa2k9ZpZooOQhI63/nOOZJIC/Smy6sYziafsIfClqF5yxbotwxDb+tt+l8qecolrmnnAj8OPFZpJbgLnhETakw6DzyUcBUJxiEJZrxwhKMIjQgng3GMF3y1MQ75XmfQR5LgbgRDWXofeGTC3sJwVvK/UOBPip51m73wFBaV2ZskE+jhRUUELlqgF2NM8qvpsofCdPKKecmfu1xztxxYjCJS5wEtxN/CpX3739fn0Rfju/sd3v44Sx9Im3mE4Zy9cef+jgp6IZ57bODkqZiNGQ4iks2o1aW/tMOe1rLonV5aOjcsQSCWHV6gy6W0DV4glh1eoIvN60L/ujjAikAqcc1rQv9aZYD0F3TxnIRBhHol9jQq9GPoBVQnPRzimMoiHNHZ607INKQlnV4uJgFBdzM4Smd1Qe2GysY4Igz9ulGU2cSnrVL0zNLr6dJPDe0cLhLz3I/xfJZ1+YHiX3l3QC8Ho0yZtBES4wdUDKxlAPrvMkVLdxyEoTDgRxSTgBpCJwz8tG2C064gK4VoTNIW6VsEkX+dlfpAYyNXdeHBZII89joyeBme017RsiJiYL5CeIpI/ESrFHdtZliMWUxWXKzM1DatXDapmKhbUAtk1OCXTa+sh14wA1Ib0+eu24eTeDyIoD74eNN5SD78e9aWbKmPF1GIITUkbRhEkPVRtSjkUbphRRyTCfZxBMOLlbRLtRh55cSt6lzjVCEZoH4gQp4YeuCcYB5uIRyisAtHD37WlKDmtZpI8DweoU3cARgfw9hHZFNFNjHpu27UbIxCSIJHnnpVamKP3qR0tEKEbVgcIuy2zTeRj5Q9tVJ2J47hU6UaY7n1/bTbfD9AYN7X1acX+QhWyCvnpBYYN2qngsYvc8pTcSJhUGH8ElNYHbvn2lUz1tdyhMhdAiOUTW0JzPoUYek8RQAFRxhMGRxHGI6AnG04QqkWW9LKzee7r6lxPCKGymPQww1OAhJkPmREx4HiinKvhQq8I5CqF1gYYkLwdI/8U4SDL9GP2TT77AQAXQJA5+YDFVxBghZQdhDKGGOdjxVjD3qvbVr9S6Nyrx/EtKFclVGKF8H9ZxYKtK6looFx9iN683VIKSGwOZYoIVRB5UvxEExm+XSMgyXy1gVIMcpRlIdHXVpUBUpwFgx8Nv0HYCWgAY6VdENmJceVSamQNQ5JOQe4R8MJxg9b0FFFhSjyOmkGlgIhxKOHVBQOs3KBhwxiMCZFPTbJ9MnLICzakX2RaXVtU+K+tQS0PcXUjXD0mhRT0bCuMa3vGPQYDu/jygS0aCJ/SSnokRuyxYbcc6tW/NRUyCJHLFcBmcyHVNaZzVqGHaYpxzCmVz4p9VqBJ0dKSuKswLNgnzi3MMkPsgxnHbdNA88LVXlSeUNkpAWiZuDj5Nxnb7Uzk9gikQCZSBQZkGOtB9lOCZDhyjEn+jlHSQqzeB69pTDnBfXyHqrAyMFjHLMmAVlNxzhbJUYW4BMdg3HRXhMds7b32jWUunTcC818XSjV16ye7vw2oVQIp0MPHiS30wR3dcAoSk1+8vLPMXhue66x6gY72psgG9MQkntt8yqMWN/UDrAKY60jp9s5ohJDu7tNV9vl2OaPPp28J+R9QkkCffRn82QGTNN9ZV7Y7enAsn8bMkt+JsfIB8sVqWMxma4K4/wgSZWRxXFRenH8EO79rFRpMrX+82xf28Rd2IOHf8jfD/cfR+asdh65Y86omwIVik00tFAuppSmDjZS9Av1eYpePV0MB4/HCdpLZloosGIR+R6xoZWL6tp8doo4f+WIEwgRJ3CNg/G0et1O5umvKJ4GESRI3uU5EfQG+667Q8mgcKada8Bl078jGVuaw8HKcpxaZLwn/lNHBIrNRDTFj+gUD+wCt8bigRUsLUc3doPlAbypnCnL3jQ9OnHyp0f0p3vwmBZwj+wxZeR1MvUnHPa66VmdQOFDd8WfrXUAcF6HP8NxdP03SrrBIWI5W8g07PaRc+4i538vi4Wg9tZo44e/dptn/RedZ9D4FtBOTAvkiLFZNmVcUG+FklV+czyZLaCg+CLdFE1YI0ruzLZNBySYosEQJtmze/DPh2RBNWqcd2addfdMmj8Cu9sKr3wK7dssP6ismP7MQlrKwxNrc6jyFAR188EzHJY2wH9C0N/C2W9EjgT+8tMQNoZW9euLNUkUsIHNGcaOKVVpblyjZ0Ke30jCtSmNVB2s+Rz7MKIKyshPVP77Ok9ziLCR36kBxUmaAxy42cg/nB17kKCu+muDU/78C61Hm9Yby2GMLUO+fatfoU1wnswwGQRRQmA0UgSSl5ZrAVNhBQ2eYLFdTn+m4usEV/FxQiFrXH/ySapejE57CQ1s9m78Qqfu6i6NS9rCAbty2WDXyMQRoCg2UfcQsaPxA3RNAawN7SybjnoHe+3hn8311+0sL4VpFqd9/zsvsoM/ceqmAE3IaE1bjtAOyqny914n/W0MsAX9KWKahvRHi6uP73NzXf0JA3DxPw== -------------------------------------------------------------------------------- /examples/ephemeral/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = "ephemeral" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | module "base" { 11 | source = "../base" 12 | 13 | prefix = local.environment 14 | aws_region = local.aws_region 15 | } 16 | 17 | module "runners" { 18 | source = "../../" 19 | create_service_linked_role_spot = true 20 | aws_region = local.aws_region 21 | vpc_id = module.base.vpc.vpc_id 22 | subnet_ids = module.base.vpc.private_subnets 23 | 24 | prefix = local.environment 25 | tags = { 26 | Project = "ProjectX" 27 | } 28 | 29 | github_app = { 30 | key_base64 = var.github_app.key_base64 31 | id = var.github_app.id 32 | webhook_secret = random_id.random.hex 33 | } 34 | 35 | # Grab the lambda packages from local directory. Must run /.ci/build.sh first 36 | webhook_lambda_zip = "../../lambda_output/webhook.zip" 37 | runner_binaries_syncer_lambda_zip = "../../lambda_output/runner-binaries-syncer.zip" 38 | runners_lambda_zip = "../../lambda_output/runners.zip" 39 | 40 | enable_organization_runners = true 41 | runner_extra_labels = "default,example" 42 | 43 | # enable access to the runners via SSM 44 | enable_ssm_on_runners = true 45 | 46 | # Let the module manage the service linked role 47 | # create_service_linked_role_spot = true 48 | 49 | instance_types = ["m5.large", "c5.large"] 50 | 51 | # override delay of events in seconds 52 | delay_webhook_event = 0 53 | 54 | # Ensure you set the number not too low, each build require a new instance 55 | runners_maximum_count = 20 56 | 57 | # override scaling down 58 | scale_down_schedule_expression = "cron(* * * * ? *)" 59 | 60 | enable_ephemeral_runners = true 61 | 62 | # # Example of simple pool usages 63 | # pool_runner_owner = "my-org" 64 | # pool_config = [{ 65 | # size = 20 66 | # schedule_expression = "cron(* * * * ? *)" 67 | # }] 68 | # 69 | # 70 | enable_job_queued_check = true 71 | 72 | # configure your pre-built AMI 73 | # enable_userdata = false 74 | # ami_filter = { name = ["github-runner-amzn2-x86_64-*"] } 75 | # data "aws_caller_identity" "current" {} 76 | # ami_owners = [data.aws_caller_identity.current.account_id] 77 | 78 | # Enable debug logging for the lambda functions 79 | # log_level = "debug" 80 | 81 | # Setup a dead letter queue, by default scale up lambda will kepp retrying to process event in case of scaling error. 82 | # redrive_policy_build_queue = { 83 | # enabled = true 84 | # maxReceiveCount = 50 # 50 retries every 30 seconds => 25 minutes 85 | # deadLetterTargetArn = null 86 | # } 87 | } 88 | -------------------------------------------------------------------------------- /modules/runners/pool.tf: -------------------------------------------------------------------------------- 1 | module "pool" { 2 | count = length(var.pool_config) == 0 ? 0 : 1 3 | 4 | source = "./pool" 5 | 6 | config = { 7 | prefix = var.prefix 8 | ghes = { 9 | ssl_verify = var.ghes_ssl_verify 10 | url = var.ghes_url 11 | } 12 | github_app_parameters = var.github_app_parameters 13 | instance_allocation_strategy = var.instance_allocation_strategy 14 | instance_max_spot_price = var.instance_max_spot_price 15 | instance_target_capacity_type = var.instance_target_capacity_type 16 | instance_types = var.instance_types 17 | kms_key_arn = local.kms_key_arn 18 | ami_kms_key_arn = local.ami_kms_key_arn 19 | lambda = { 20 | log_level = var.log_level 21 | logging_retention_in_days = var.logging_retention_in_days 22 | logging_kms_key_id = var.logging_kms_key_id 23 | reserved_concurrent_executions = var.pool_lambda_reserved_concurrent_executions 24 | s3_bucket = var.lambda_s3_bucket 25 | s3_key = var.runners_lambda_s3_key 26 | s3_object_version = var.runners_lambda_s3_object_version 27 | security_group_ids = var.lambda_security_group_ids 28 | subnet_ids = var.lambda_subnet_ids 29 | architecture = var.lambda_architecture 30 | runtime = var.lambda_runtime 31 | timeout = var.pool_lambda_timeout 32 | zip = local.lambda_zip 33 | } 34 | pool = var.pool_config 35 | role_path = local.role_path 36 | role_permissions_boundary = var.role_permissions_boundary 37 | runner = { 38 | disable_runner_autoupdate = var.disable_runner_autoupdate 39 | ephemeral = var.enable_ephemeral_runners 40 | boot_time_in_minutes = var.runner_boot_time_in_minutes 41 | extra_labels = var.runner_extra_labels 42 | launch_template = aws_launch_template.runner 43 | group_name = var.runner_group_name 44 | name_prefix = var.runner_name_prefix 45 | pool_owner = var.pool_runner_owner 46 | role = aws_iam_role.runner 47 | } 48 | subnet_ids = var.subnet_ids 49 | ssm_token_path = "${var.ssm_paths.root}/${var.ssm_paths.tokens}" 50 | ami_id_ssm_parameter_name = var.ami_id_ssm_parameter_name 51 | ami_id_ssm_parameter_read_policy_arn = var.ami_id_ssm_parameter_name != null ? aws_iam_policy.ami_id_ssm_parameter_read[0].arn : null 52 | tags = local.tags 53 | } 54 | 55 | aws_partition = var.aws_partition 56 | 57 | } 58 | -------------------------------------------------------------------------------- /examples/ubuntu/README.md: -------------------------------------------------------------------------------- 1 | # Action runners deployment ubuntu example 2 | 3 | This module shows how to create GitHub action runners using an Ubuntu AMI. Lambda release will be downloaded from GitHub. 4 | 5 | ## Usages 6 | 7 | Steps for the full setup, such as creating a GitHub app can be found in the root module's [README](../../README.md). First download the Lambda releases from GitHub. Alternatively you can build the lambdas locally with Node or Docker, there is a simple build script in `/.ci/build.sh`. In the `main.tf` you can simply remove the location of the lambda zip files, the default location will work in this case. 8 | 9 | > Ensure you have set the version in `lambdas-download/main.tf` for running the example. The version needs to be set to a GitHub release version, see https://github.com/philips-labs/terraform-aws-github-runner/releases 10 | 11 | 12 | ```bash 13 | cd ../lambdas-download 14 | terraform init 15 | terraform apply -var=module_version= 16 | cd - 17 | ``` 18 | 19 | Before running Terraform, ensure the GitHub app is configured. 20 | 21 | ```bash 22 | terraform init 23 | terraform apply 24 | ``` 25 | 26 | 27 | ## Requirements 28 | 29 | | Name | Version | 30 | |------|---------| 31 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 32 | | [aws](#requirement\_aws) | ~> 4.0 | 33 | | [local](#requirement\_local) | ~> 2.0 | 34 | | [random](#requirement\_random) | ~> 3.0 | 35 | 36 | ## Providers 37 | 38 | | Name | Version | 39 | |------|---------| 40 | | [random](#provider\_random) | 3.4.3 | 41 | 42 | ## Modules 43 | 44 | | Name | Source | Version | 45 | |------|--------|---------| 46 | | [base](#module\_base) | ../base | n/a | 47 | | [runners](#module\_runners) | ../../ | n/a | 48 | | [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | 3.11.2 | 49 | 50 | ## Resources 51 | 52 | | Name | Type | 53 | |------|------| 54 | | [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | 55 | 56 | ## Inputs 57 | 58 | | Name | Description | Type | Default | Required | 59 | |------|-------------|------|---------|:--------:| 60 | | [github\_app](#input\_github\_app) | GitHub for API usages. |
object({
id = string
key_base64 = string
})
| n/a | yes | 61 | 62 | ## Outputs 63 | 64 | | Name | Description | 65 | |------|-------------| 66 | | [runners](#output\_runners) | n/a | 67 | | [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | 68 | | [webhook\_secret](#output\_webhook\_secret) | n/a | 69 | -------------------------------------------------------------------------------- /examples/windows/README.md: -------------------------------------------------------------------------------- 1 | # Action runners deployment windows example 2 | 3 | This module shows how to create GitHub action runners using an Windows Runners. Lambda release will be downloaded from GitHub. 4 | 5 | ## Usages 6 | 7 | Steps for the full setup, such as creating a GitHub app can be found in the root module's [README](../../README.md). First, download the Lambda releases from GitHub. Alternatively you can build the lambdas locally with Node or Docker, for which there is a build script available at `/.ci/build.sh`. In the `main.tf` you can remove the location of the lambda zip files, the default location will work in this case. 8 | 9 | > Ensure you have set the version in `lambdas-download/main.tf` for running the example. The version needs to be set to a GitHub release version, see 10 | 11 | 12 | ```pwsh 13 | cd lambdas-download 14 | terraform init 15 | terraform apply 16 | cd .. 17 | ``` 18 | 19 | Before running Terraform, ensure the GitHub app is configured. 20 | 21 | ```bash 22 | terraform init 23 | terraform apply 24 | ``` 25 | 26 | _**Note**_: It can take upwards of ten minutes for a runner to start processing jobs, and about as long for logs to start showing up. It's recommend that scale the runners via a warm-up job and then keep them idled. 27 | 28 | 29 | ## Requirements 30 | 31 | | Name | Version | 32 | |------|---------| 33 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 34 | | [aws](#requirement\_aws) | ~> 4.0 | 35 | | [local](#requirement\_local) | ~> 2.0 | 36 | | [random](#requirement\_random) | ~> 3.0 | 37 | 38 | ## Providers 39 | 40 | | Name | Version | 41 | |------|---------| 42 | | [random](#provider\_random) | 3.4.3 | 43 | 44 | ## Modules 45 | 46 | | Name | Source | Version | 47 | |------|--------|---------| 48 | | [base](#module\_base) | ../base | n/a | 49 | | [runners](#module\_runners) | ../../ | n/a | 50 | 51 | ## Resources 52 | 53 | | Name | Type | 54 | |------|------| 55 | | [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | 56 | 57 | ## Inputs 58 | 59 | | Name | Description | Type | Default | Required | 60 | |------|-------------|------|---------|:--------:| 61 | | [github\_app](#input\_github\_app) | GitHub for API usages. |
object({
id = string
key_base64 = string
})
| n/a | yes | 62 | 63 | ## Outputs 64 | 65 | | Name | Description | 66 | |------|-------------| 67 | | [runners](#output\_runners) | n/a | 68 | | [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | 69 | | [webhook\_secret](#output\_webhook\_secret) | n/a | 70 | -------------------------------------------------------------------------------- /examples/default/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | environment = var.environment != null ? var.environment : "default" 3 | aws_region = "eu-west-1" 4 | } 5 | 6 | resource "random_id" "random" { 7 | byte_length = 20 8 | } 9 | 10 | module "base" { 11 | source = "../base" 12 | 13 | prefix = local.environment 14 | aws_region = local.aws_region 15 | } 16 | 17 | module "runners" { 18 | source = "../../" 19 | create_service_linked_role_spot = true 20 | aws_region = local.aws_region 21 | vpc_id = module.base.vpc.vpc_id 22 | subnet_ids = module.base.vpc.private_subnets 23 | 24 | prefix = local.environment 25 | tags = { 26 | Project = "ProjectX" 27 | } 28 | 29 | github_app = { 30 | key_base64 = var.github_app.key_base64 31 | id = var.github_app.id 32 | webhook_secret = random_id.random.hex 33 | } 34 | 35 | # configure the block device mappings, default for Amazon Linux2 36 | # block_device_mappings = [{ 37 | # device_name = "/dev/xvda" 38 | # delete_on_termination = true 39 | # volume_type = "gp3" 40 | # volume_size = 10 41 | # encrypted = true 42 | # iops = null 43 | # }] 44 | 45 | # Grab zip files via lambda_download 46 | # webhook_lambda_zip = "../lambdas-download/webhook.zip" 47 | # runner_binaries_syncer_lambda_zip = "../lambdas-download/runner-binaries-syncer.zip" 48 | # runners_lambda_zip = "../lambdas-download/runners.zip" 49 | 50 | enable_organization_runners = true 51 | runner_extra_labels = "default,example" 52 | 53 | # enable access to the runners via SSM 54 | enable_ssm_on_runners = true 55 | 56 | # use S3 or KMS SSE to runners S3 bucket 57 | # runner_binaries_s3_sse_configuration = { 58 | # rule = { 59 | # apply_server_side_encryption_by_default = { 60 | # sse_algorithm = "AES256" 61 | # } 62 | # } 63 | # } 64 | 65 | # Uncommet idle config to have idle runners from 9 to 5 in time zone Amsterdam 66 | # idle_config = [{ 67 | # cron = "* * 9-17 * * *" 68 | # timeZone = "Europe/Amsterdam" 69 | # idleCount = 1 70 | # }] 71 | 72 | # Let the module manage the service linked role 73 | # create_service_linked_role_spot = true 74 | 75 | instance_types = ["m5.large", "c5.large"] 76 | 77 | # override delay of events in seconds 78 | delay_webhook_event = 5 79 | runners_maximum_count = 1 80 | 81 | # set up a fifo queue to remain order 82 | enable_fifo_build_queue = true 83 | 84 | # override scaling down 85 | scale_down_schedule_expression = "cron(* * * * ? *)" 86 | # enable this flag to publish webhook events to workflow job queue 87 | # enable_workflow_job_events_queue = true 88 | 89 | enable_user_data_debug_logging_runner = true 90 | 91 | # prefix GitHub runners with the environment name 92 | runner_name_prefix = "${local.environment}_" 93 | 94 | # Enable debug logging for the lambda functions 95 | # log_level = "debug" 96 | } 97 | -------------------------------------------------------------------------------- /modules/download-lambda/README.md: -------------------------------------------------------------------------------- 1 | # Module - Download lambda artifacts 2 | 3 | This module is optional and provides an option to download via Terraform the Lambda artifacts from GitHub. 4 | 5 | ## Usages 6 | 7 | ```hcl 8 | module "lambdas" { 9 | source = "" 10 | lambdas = [ 11 | { 12 | name = "webhook" 13 | tag = "v0.15.0" 14 | }, 15 | { 16 | name = "runners" 17 | tag = "v0.15.0" 18 | }, 19 | { 20 | name = "runner-binaries-syncer" 21 | tag = "v0.15.0" 22 | } 23 | ] 24 | } 25 | ``` 26 | 27 | 28 | ## Requirements 29 | 30 | | Name | Version | 31 | |------|---------| 32 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 33 | | [aws](#requirement\_aws) | ~> 4.0 | 34 | | [null](#requirement\_null) | ~> 3.0 | 35 | 36 | ## Providers 37 | 38 | | Name | Version | 39 | |------|---------| 40 | | [null](#provider\_null) | 3.0.0 | 41 | 42 | ## Modules 43 | 44 | No modules. 45 | 46 | ## Resources 47 | 48 | | Name | Type | 49 | |------|------| 50 | | [null_resource.download](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 51 | 52 | ## Inputs 53 | 54 | | Name | Description | Type | Default | Required | 55 | |------|-------------|------|---------|:--------:| 56 | | [lambdas](#input\_lambdas) | Name and tag for lambdas to download. |
list(object({
name = string
tag = string
}))
| n/a | yes | 57 | 58 | ## Outputs 59 | 60 | | Name | Description | 61 | |------|-------------| 62 | | [files](#output\_files) | n/a | 63 | 64 | 65 | 66 | ## Requirements 67 | 68 | | Name | Version | 69 | |------|---------| 70 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 71 | | [aws](#requirement\_aws) | ~> 4.0 | 72 | | [null](#requirement\_null) | ~> 3.0 | 73 | 74 | ## Providers 75 | 76 | | Name | Version | 77 | |------|---------| 78 | | [null](#provider\_null) | 3.0.0 | 79 | 80 | ## Modules 81 | 82 | No modules. 83 | 84 | ## Resources 85 | 86 | | Name | Type | 87 | |------|------| 88 | | [null_resource.download](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 89 | 90 | ## Inputs 91 | 92 | | Name | Description | Type | Default | Required | 93 | |------|-------------|------|---------|:--------:| 94 | | [lambdas](#input\_lambdas) | Name and tag for lambdas to download. |
list(object({
name = string
tag = string
}))
| n/a | yes | 95 | 96 | ## Outputs 97 | 98 | | Name | Description | 99 | |------|-------------| 100 | | [files](#output\_files) | n/a | 101 | -------------------------------------------------------------------------------- /examples/default/README.md: -------------------------------------------------------------------------------- 1 | # Action runners deployment default example 2 | 3 | This module shows how to create GitHub action runners. Lambda release will be downloaded from GitHub. 4 | 5 | ## Usages 6 | 7 | Steps for the full setup, such as creating a GitHub app can be found in the root module's [README](../../README.md). First download the Lambda releases from GitHub. Alternatively you can build the lambdas locally with Node or Docker, there is a simple build script in `/.ci/build.sh`. In the `main.tf` you can simply remove the location of the lambda zip files, the default location will work in this case. 8 | 9 | > Ensure you have set the version in `lambdas-download/main.tf` for running the example. The version needs to be set to a GitHub release version, see https://github.com/philips-labs/terraform-aws-github-runner/releases 10 | 11 | ```bash 12 | cd ../lambdas-download 13 | terraform init 14 | terraform apply -var=module_version= 15 | cd - 16 | ``` 17 | 18 | Before running Terraform, ensure the GitHub app is configured. See the [configuration details](../../README.md#usages) for more details. 19 | 20 | ```bash 21 | terraform init 22 | terraform apply 23 | ``` 24 | 25 | You can receive the webhook details by running: 26 | 27 | ```bash 28 | terraform output -raw webhook_secret 29 | ``` 30 | 31 | Be-aware some shells will print some end of line character `%`. 32 | 33 | 34 | ## Requirements 35 | 36 | | Name | Version | 37 | |------|---------| 38 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 39 | | [aws](#requirement\_aws) | ~> 4.0 | 40 | | [local](#requirement\_local) | ~> 2.0 | 41 | | [random](#requirement\_random) | ~> 3.0 | 42 | 43 | ## Providers 44 | 45 | | Name | Version | 46 | |------|---------| 47 | | [random](#provider\_random) | 3.4.3 | 48 | 49 | ## Modules 50 | 51 | | Name | Source | Version | 52 | |------|--------|---------| 53 | | [base](#module\_base) | ../base | n/a | 54 | | [runners](#module\_runners) | ../../ | n/a | 55 | 56 | ## Resources 57 | 58 | | Name | Type | 59 | |------|------| 60 | | [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | 61 | 62 | ## Inputs 63 | 64 | | Name | Description | Type | Default | Required | 65 | |------|-------------|------|---------|:--------:| 66 | | [environment](#input\_environment) | n/a | `string` | `null` | no | 67 | | [github\_app](#input\_github\_app) | GitHub for API usages. |
object({
id = string
key_base64 = string
})
| n/a | yes | 68 | 69 | ## Outputs 70 | 71 | | Name | Description | 72 | |------|-------------| 73 | | [runners](#output\_runners) | n/a | 74 | | [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | 75 | | [webhook\_secret](#output\_webhook\_secret) | n/a | 76 | -------------------------------------------------------------------------------- /modules/setup-iam-permissions/policies/deploy-boundary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "CreateOrChangeOnlyWithBoundary", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "iam:CreateRole", 9 | "iam:AttachRolePolicy", 10 | "iam:PutRolePermissionsBoundary", 11 | "iam:PutRolePolicy" 12 | ], 13 | "Resource": "arn:${aws_partition}:iam::${account_id}:role/${role_namespace}/*", 14 | "Condition": { 15 | "StringEquals": { 16 | "iam:PermissionsBoundary": "${permission_boundary}" 17 | } 18 | } 19 | }, 20 | { 21 | "Sid": "RoleInNamespace", 22 | "Effect": "Allow", 23 | "Action": [ 24 | "iam:TagRole", 25 | "iam:GetRolePolicy", 26 | "iam:GetRole", 27 | "iam:DeleteRole", 28 | "iam:PassRole", 29 | "iam:DetachRolePolicy", 30 | "iam:DeleteRolePolicy" 31 | ], 32 | "Resource": "arn:${aws_partition}:iam::${account_id}:role/${role_namespace}/*" 33 | }, 34 | { 35 | "Sid": "PolicyInNamespace", 36 | "Effect": "Allow", 37 | "Action": [ 38 | "iam:CreatePolicy", 39 | "iam:DeletePolicy", 40 | "iam:DeletePolicyVersion", 41 | "iam:GetPolicy", 42 | "iam:GetPolicyVersion", 43 | "iam:SetDefaultPolicyVersion" 44 | ], 45 | "Resource": "arn:${aws_partition}:iam::${account_id}:policy/${policy_namespace}/*" 46 | }, 47 | { 48 | "Sid": "InstanceProfileInNamespace", 49 | "Effect": "Allow", 50 | "Action": [ 51 | "iam:CreateInstanceProfile", 52 | "iam:RemoveRoleFromInstanceProfile", 53 | "iam:DeleteInstanceProfile", 54 | "iam:AddRoleToInstanceProfile", 55 | "iam:GetInstanceProfile" 56 | ], 57 | "Resource": "arn:${aws_partition}:iam::${account_id}:instance-profile/${instance_profile_namespace}/*" 58 | }, 59 | { 60 | "Sid": "IamListActions", 61 | "Effect": "Allow", 62 | "Action": [ 63 | "iam:ListInstanceProfilesForRole", 64 | "iam:ListPolicies", 65 | "iam:ListPolicyVersions", 66 | "iam:ListEntitiesForPolicy", 67 | "iam:ListRolePolicies", 68 | "iam:ListAttachedRolePolicies" 69 | ], 70 | "Resource": "*" 71 | }, 72 | { 73 | "Sid": "NoBoundaryPolicyEdit", 74 | "Effect": "Deny", 75 | "Action": [ 76 | "iam:CreatePolicyVersion", 77 | "iam:DeletePolicy", 78 | "iam:DeletePolicyVersion", 79 | "iam:SetDefaultPolicyVersion" 80 | ], 81 | "Resource": "arn:${aws_partition}:iam::${account_id}:policy/${boundary_namespace}/*" 82 | }, 83 | { 84 | "Sid": "Services", 85 | "Effect": "Allow", 86 | "Action": [ 87 | "s3:*", 88 | "ec2:*", 89 | "events:*", 90 | "logs:*", 91 | "lambda:*", 92 | "sqs:*", 93 | "ssm:*", 94 | "apigateway:*", 95 | "resource-groups:*", 96 | "kms:*" 97 | ], 98 | "Resource": "*" 99 | } 100 | ] 101 | } 102 | --------------------------------------------------------------------------------