diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 100% rename from .github/CONTRIBUTING.md rename to CONTRIBUTING.md diff --git a/README.md b/README.md index bbd9205..82e3fd8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Deploy GitHub Action runners in your AWS Account. Uses AWS CodeBuild to manage ephemeral runners, so you don't have to. + [![GitHub repo link](https://github.com/cloudandthings/terraform-aws-github-runners/blob/main/docs/images/icon.gif )](https://github.com/cloudandthings/terraform-aws-github-runners) --- @@ -11,29 +12,29 @@ Deploy GitHub Action runners in your AWS Account. Uses AWS CodeBuild to manage e ![Terraform Version](https://img.shields.io/badge/tf-%3E%3D0.13.0-blue) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) -# Previous version notice +## Why? -Previously, this module used EC2 spot instances with configurable AutoScaling. +Deploying self-hosted GitHub runner should be simple. +It shouldn't need a long setup process or a lot of infrastructure. -Should you wish to continue to use this older approach, the code has been moved to (terraform-aws-github-runners-ec2)[https://github.com/cloudandthings/terraform-aws-github-runners-ec2]. +This module additionally does not require public inbound traffic, and can be easily customised if needed. -# Features +## Features - Simple! See the provided examples for a quick-start. - Serverless. No EC2 instances that need to be maintained - Cost-effective. Only billed for when CodeBuild project is running as projects are billed per build minute. - Scalable. By default one runner process and 20GB storage is provided per vCPU per EC2 instance. -A full list of created resources is shown below. +For many projects, CI/CD is run infrequently and there are long periods of time when no CI/CD runs occur. Using CodeBuild means that GitHub runners are always available, and cost nothing when idle. -# Why? +## Previous version notice -Deploying self-hosted GitHub runner should be simple. -It shouldn't need a long setup process or a lot of infrastructure. +Previously, this module used EC2 spot instances with configurable AutoScaling. -This module additionally does not require public inbound traffic, and can be easily customised if needed. +Should you wish to continue to use this older approach, the code has been moved to (terraform-aws-github-runners-ec2)[https://github.com/cloudandthings/terraform-aws-github-runners-ec2]. -# Known limitations +## Known limitations 1. Additional config needed if using custom ECR image @@ -57,59 +58,22 @@ For example: # How to use it -## 1. Decide on how to authenticate to GitHub - -Here we focus on setting up a personal access token to authenticate to GitHub. -OAuth is also supported but not implemented / documented here. - - - -### GitHub access token - -Note that CodeBuild only supports 1 GitHub token to be configured for all CodeBuild projects in the same AWS Account and Region. - -Therefore, when using multiple CodeBuild projects, you can configure the token once per region (not once per project). - -There are a few approaches that you could take, choose one from the below. - -#### 2. Create your GitHub token - -Create a [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). -Make sure that the fine grained token has [these](https://docs.aws.amazon.com/codebuild/latest/userguide/access-tokens-github.html#access-tokens-github-prereqs) permissions. - - -#### 2a. Add the token to CodeBuild separately. - -You would add the token as [documented here](https://docs.aws.amazon.com/codebuild/latest/userguide/access-tokens-github.html) -This is recommended if you do not want to maintain the token in Terraform. - -#### 2b. Provide the token as an input Terraform variable - -Note that although the variable is sensitive, the value will still be stored in Terraform state. - -#### 2c. Use AWS Parameter Store - - - -Add the token to AWS Systems Manager Parameter Store, and configure this module to read it. -The module will add the token to Codebuild for you. - -This is recommended if you have only a single project. - -### Adding your token to Parameter Store (Optional) - -Add it to AWS Systems Manager Parameter Store with the `SecureString` type. - -[![Parameter Store configuration](https://github.com/cloudandthings/terraform-aws-github-runners/blob/main/docs/images/ssm.png)](https://github.com/cloudandthings/terraform-aws-github-runners/blob/main/docs/images/ssm.png ) +## 1. Setup authentication to GitHub +There are several ways to set up authentication to GitHub. +Follow the guide [here](/docs/GITHUB-AUTH-SETUP.md). ## 2. Configure this module Configure and deploy this module using Terraform. See examples below. +## 3. (Optional) create a custom Docker image + +If you want the runner to execute in a pre-configured environment you can build and push your own Docker image for it to use. + # More info -- Found an issue? Want to help? [Contribute](https://github.com/cloudandthings/terraform-aws-github-runners/.github/contribute.md). +- Found an issue? Want to help? See the [contribution guide](/CONTRIBUTING.md). + +### GitHub access token + +Note that CodeBuild only supports 1 GitHub token to be configured for all CodeBuild projects in the same AWS Account and Region. + +Therefore, when using multiple CodeBuild projects, you can configure the token once per region (not once per project). + +There are a few approaches that you could take, choose one from the below. + +#### 2. Create your GitHub token + +Create a [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). +Make sure that the fine grained token has [these](https://docs.aws.amazon.com/codebuild/latest/userguide/access-tokens-github.html#access-tokens-github-prereqs) permissions. + + +#### 2a. Add the token to CodeBuild separately. + +You would add the token as [documented here](https://docs.aws.amazon.com/codebuild/latest/userguide/access-tokens-github.html) +This is recommended if you do not want to maintain the token in Terraform. + +#### 2b. Provide the token as an input Terraform variable + +Note that although the variable is sensitive, the value will still be stored in Terraform state. + +#### 2c. Use AWS Parameter Store + + + +Add the token to AWS Systems Manager Parameter Store, and configure this module to read it. +The module will add the token to Codebuild for you. + +This is recommended if you have only a single project. + +### Adding your token to Parameter Store (Optional) + +Add it to AWS Systems Manager Parameter Store with the `SecureString` type. + +[![Parameter Store configuration](https://github.com/cloudandthings/terraform-aws-github-runners/blob/main/docs/images/ssm.png)](https://github.com/cloudandthings/terraform-aws-github-runners/blob/main/docs/images/ssm.png ) diff --git a/examples/advanced/README.md b/examples/advanced/README.md index b717acc..c2b4127 100644 --- a/examples/advanced/README.md +++ b/examples/advanced/README.md @@ -57,7 +57,7 @@ module "github_runner" { ################################ security_group_ids = [aws_security_group.this.id] - use_ecr_image = true + create_ecr_repository = true cloudwatch_logs_group_name = "/some/log/group" } ``` diff --git a/examples/advanced/main.tf b/examples/advanced/main.tf index 27512a7..8aafcd0 100644 --- a/examples/advanced/main.tf +++ b/examples/advanced/main.tf @@ -53,6 +53,6 @@ module "github_runner" { ################################ security_group_ids = [aws_security_group.this.id] - use_ecr_image = true + create_ecr_repository = true cloudwatch_logs_group_name = "/some/log/group" } diff --git a/iam.tf b/iam.tf index 4450751..0c1ee04 100644 --- a/iam.tf +++ b/iam.tf @@ -1,5 +1,7 @@ +################################################################################ +# Cloudwatch permissions +################################################################################ data "aws_iam_policy_document" "cloudwatch_required" { - # Cloudwatch permissions statement { sid = "AllowCreateLogGroup" effect = "Allow" @@ -26,9 +28,11 @@ resource "aws_iam_role_policy" "cloudwatch_required" { policy = data.aws_iam_policy_document.cloudwatch_required.json } +################################################################################ +# VPC permissions +################################################################################ data "aws_iam_policy_document" "networking_required" { count = local.has_vpc_config ? 1 : 0 - # VPC permissions statement { sid = "AllowNetworkingDescribe" effect = "Allow" @@ -74,8 +78,10 @@ resource "aws_iam_role_policy" "networking_required" { policy = data.aws_iam_policy_document.networking_required[0].json } +################################################################################ +# S3 permissions +################################################################################ data "aws_iam_policy_document" "s3_required" { - # S3 permissions count = local.has_s3_log_bucket ? 1 : 0 statement { effect = "Allow" @@ -94,9 +100,11 @@ resource "aws_iam_role_policy" "s3_required" { policy = data.aws_iam_policy_document.s3_required[0].json } +################################################################################ +# ECR permissions +################################################################################ data "aws_iam_policy_document" "ecr_required" { - count = var.use_ecr_image ? 1 : 0 - # ECR permissions + count = local.use_ecr_repository ? 1 : 0 statement { effect = "Allow" actions = [ @@ -120,7 +128,7 @@ data "aws_iam_policy_document" "ecr_required" { } resource "aws_iam_role_policy" "ecr_required" { - count = var.use_ecr_image ? 1 : 0 + count = local.use_ecr_repository ? 1 : 0 name = "${var.name}-ecr" role = local.create_iam_role ? aws_iam_role.this[0].name : var.iam_role_name policy = data.aws_iam_policy_document.ecr_required[count.index].json @@ -140,6 +148,9 @@ data "aws_iam_policy_document" "assume_role" { } } +################################################################################ +# Create role +################################################################################ resource "aws_iam_role" "this" { count = local.create_iam_role ? 1 : 0 name = var.name @@ -147,6 +158,9 @@ resource "aws_iam_role" "this" { permissions_boundary = var.iam_role_permissions_boundary == null ? null : var.iam_role_permissions_boundary } +################################################################################ +# Custom permissions +################################################################################ resource "aws_iam_role_policy_attachment" "additional" { for_each = var.iam_role_policies diff --git a/locals.tf b/locals.tf index 107408e..e3dc2c1 100644 --- a/locals.tf +++ b/locals.tf @@ -13,12 +13,6 @@ locals { subnet_arns = [for subnet_id in var.subnet_ids : "arn:aws:ec2:${local.aws_region}:${local.aws_account_id}:subnet/${subnet_id}"] - security_group_ids = ( - length(var.security_group_ids) == 0 - ? try([aws_security_group.codebuild[0].id], []) - : concat(try([aws_security_group.codebuild[0].id], []), var.security_group_ids) - ) - create_iam_role = var.iam_role_name == null cloudwatch_logs_group_arn = ( diff --git a/main.tf b/main.tf index 859989a..1a8fee6 100644 --- a/main.tf +++ b/main.tf @@ -1,18 +1,30 @@ +################################################################################ +# CodeBuild +################################################################################ + resource "aws_codebuild_project" "this" { name = var.name description = var.description build_timeout = var.build_timeout - service_role = local.create_iam_role ? aws_iam_role.this[0].arn : "arn:aws:iam::${local.aws_account_id}:role/${var.iam_role_name}" + service_role = ( + local.create_iam_role + ? aws_iam_role.this[0].arn + : "arn:aws:iam::${local.aws_account_id}:role/${var.iam_role_name}" + ) artifacts { type = "NO_ARTIFACTS" } environment { - type = var.environment_type - compute_type = var.environment_compute_type - image = var.use_ecr_image ? "${aws_ecr_repository.this[0].repository_url}:latest" : var.environment_image - image_pull_credentials_type = var.use_ecr_image ? "SERVICE_ROLE" : "CODEBUILD" + type = var.environment_type + compute_type = var.environment_compute_type + image = ( + local.use_ecr_repository + ? data.aws_ecr_image.latest[0].image_uri + : var.environment_image + ) + image_pull_credentials_type = local.use_ecr_repository ? "SERVICE_ROLE" : "CODEBUILD" # privileged_mode = true } @@ -81,19 +93,35 @@ resource "aws_codebuild_webhook" "this" { } } +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = local.has_vpc_config && length(var.security_group_ids) == 0 + security_group_name = coalesce(var.security_group_name, var.name) + + security_group_ids = concat( + local.create_security_group + ? [aws_security_group.codebuild[0].id] + : [], + var.security_group_ids + ) +} + resource "aws_security_group" "codebuild" { #checkov:skip=CKV2_AWS_5:access logging not required - count = local.has_vpc_config ? 1 : 0 + count = local.create_security_group ? 1 : 0 vpc_id = var.vpc_id - name = var.name + name = local.security_group_name description = "Security group for CodeBuild project ${var.name}" tags = { - Name = var.name + Name = local.security_group_name } } resource "aws_vpc_security_group_egress_rule" "codebuild" { - count = local.has_vpc_config ? 1 : 0 + count = local.create_security_group ? 1 : 0 security_group_id = aws_security_group.codebuild[count.index].id cidr_ipv4 = "0.0.0.0/0" @@ -102,7 +130,7 @@ resource "aws_vpc_security_group_egress_rule" "codebuild" { } resource "aws_vpc_security_group_ingress_rule" "codebuild" { - count = local.has_vpc_config ? 1 : 0 + count = local.create_security_group ? 1 : 0 security_group_id = aws_security_group.codebuild[count.index].id cidr_ipv4 = "0.0.0.0/0" @@ -112,12 +140,20 @@ resource "aws_vpc_security_group_ingress_rule" "codebuild" { description = "Allow HTTPS traffic from ALL" } -# TODO +################################################################################ +# ECS Repository +################################################################################ + +locals { + use_ecr_repository = var.create_ecr_repository || var.ecr_repository_name + ecr_repository_name = coalesce(var.ecr_repository_name, var.name) +} + resource "aws_ecr_repository" "this" { #checkov:skip=CKV_AWS_136:encryption not required #checkov:skip=CKV_AWS_51:latest tag used by codebuild so tag needs to be overwritten - count = var.use_ecr_image ? 1 : 0 - name = var.name + count = var.create_ecr_repository ? 1 : 0 + name = local.ecr_repository_name image_tag_mutability = "IMMUTABLE" @@ -132,8 +168,15 @@ resource "aws_ecr_repository" "this" { } } +data "aws_ecr_image" "latest" { + count = local.use_ecr_repository ? 1 : 0 + + repository_name = local.ecr_repository_name + most_recent = true +} + resource "aws_ecr_lifecycle_policy" "policy" { - count = var.use_ecr_image ? 1 : 0 + count = var.create_ecr_repository ? 1 : 0 repository = aws_ecr_repository.this[0].name policy = <<-EOF diff --git a/variables.tf b/variables.tf index 59633e5..430d291 100644 --- a/variables.tf +++ b/variables.tf @@ -37,19 +37,19 @@ variable "description" { variable "environment_type" { type = string default = "LINUX_CONTAINER" - description = "Type of build environment to use for related builds. Valid values: LINUX_CONTAINER, LINUX_GPU_CONTAINER, WINDOWS_CONTAINER (deprecated), WINDOWS_SERVER_2019_CONTAINER, ARM_CONTAINER, LINUX_LAMBDA_CONTAINER, ARM_LAMBDA_CONTAINER" + description = "Type of build environment to use for related builds. Valid values: `LINUX_CONTAINER`, `LINUX_GPU_CONTAINER`, `WINDOWS_CONTAINER` (deprecated), `WINDOWS_SERVER_2019_CONTAINER`, `ARM_CONTAINER`, `LINUX_LAMBDA_CONTAINER`, `ARM_LAMBDA_CONTAINER`" } variable "environment_compute_type" { type = string default = "BUILD_GENERAL1_SMALL" - description = " Information about the compute resources the build project will use. Valid values: BUILD_GENERAL1_SMALL, BUILD_GENERAL1_MEDIUM, BUILD_GENERAL1_LARGE, BUILD_GENERAL1_2XLARGE, BUILD_LAMBDA_1GB, BUILD_LAMBDA_2GB, BUILD_LAMBDA_4GB, BUILD_LAMBDA_8GB, BUILD_LAMBDA_10GB. BUILD_GENERAL1_SMALL is only valid if type is set to LINUX_CONTAINER. When type is set to LINUX_GPU_CONTAINER, compute_type must be BUILD_GENERAL1_LARGE. When type is set to LINUX_LAMBDA_CONTAINER or ARM_LAMBDA_CONTAINER, compute_type must be BUILD_LAMBDA_XGB" + description = " Information about the compute resources the build project will use. Valid values: `BUILD_GENERAL1_SMALL`, `BUILD_GENERAL1_MEDIUM`, `BUILD_GENERAL1_LARGE`, `BUILD_GENERAL1_2XLARGE`, `BUILD_LAMBDA_1GB`, `BUILD_LAMBDA_2GB`, `BUILD_LAMBDA_4GB`, `BUILD_LAMBDA_8GB`, `BUILD_LAMBDA_10GB`. `BUILD_GENERAL1_SMALL` is only valid if type is set to `LINUX_CONTAINER`. When type is set to `LINUX_GPU_CONTAINER`, compute_type must be `BUILD_GENERAL1_LARGE`. When type is set to `LINUX_LAMBDA_CONTAINER` or `ARM_LAMBDA_CONTAINER`, compute_type must be `BUILD_LAMBDA_XGB`" } variable "environment_image" { type = string default = "aws/codebuild/amazonlinux2-x86_64-standard:5.0" - description = "Docker image to use for this build project. Valid values include Docker images provided by CodeBuild (e.g aws/codebuild/amazonlinux2-x86_64-standard:4.0), Docker Hub images (e.g., hashicorp/terraform:latest). If use_ecr_image is set to true, this value will be ignored and the ECR image location will be used." + description = "Docker image to use for this build project. Valid values include Docker images provided by CodeBuild (e.g `aws/codebuild/amazonlinux2-x86_64-standard:4.0`), Docker Hub images (e.g., `hashicorp/terraform:latest`). If `use_ecr_image` is set to true, this value will be ignored and the ECR image location will be used." } # logs @@ -60,13 +60,13 @@ variable "create_cloudwatch_log_group" { } variable "cloudwatch_logs_group_name" { - description = "Name of the log group used by the codebuild project. If blank then a default is used." + description = "Name of the log group used by the CodeBuild project. If not specified then a default is used." type = string default = null } variable "cloudwatch_logs_stream_name" { - description = "Name of the log stream used by the codebuild project. If blank then a default is used." + description = "Name of the log stream used by the CodeBuild project. If not specified then a default is used." type = string default = null } @@ -78,7 +78,7 @@ variable "cloudwatch_log_group_retention_in_days" { } variable "s3_logs_bucket_name" { - description = "Name of the S3 bucket to store logs in. If null then logging to S3 will be disabled." + description = "Name of the S3 bucket to store logs in. If not specified then logging to S3 will be disabled." type = string default = null } @@ -91,25 +91,31 @@ variable "s3_logs_bucket_prefix" { # vpc variable "vpc_id" { type = string - description = "The VPC ID for AWS Codebuild to launch ephemeral instances in." + description = "The VPC ID for AWS CodeBuild to launch ephemeral instances in." default = null } variable "subnet_ids" { type = list(string) - description = "The list of Subnet IDs for AWS Codebuild to launch ephemeral EC2 instances in." + description = "The list of Subnet IDs for AWS CodeBuild to launch ephemeral EC2 instances in." default = [] } +variable "security_group_name" { + description = "Name to use on created Security Group. Defaults to `name`" + type = string + default = null +} + variable "security_group_ids" { type = list(string) - description = "The list of Security Group IDs for AWS Codebuild to launch ephemeral EC2 instances in." + description = "The list of Security Group IDs for AWS CodeBuild to launch ephemeral EC2 instances in." default = [] } # IAM variable "iam_role_name" { - description = "Name of the IAM role to be used, if one is not given a role will be created" + description = "Name of the IAM role to be used. If not specified then a role will be created" type = string default = null } @@ -128,13 +134,13 @@ variable "iam_role_permissions_boundary" { # GitHub variable "github_personal_access_token" { - description = "The GitHub personal access token to use for accessing the repository" + description = "The GitHub personal access token to use for accessing the repository. If not specified then GitHub auth must be configured separately." type = string default = null } variable "github_personal_access_token_ssm_parameter" { - description = "The GitHub personal access token to use for accessing the repository" + description = "The GitHub personal access token to use for accessing the repository. If not specified then GitHub auth must be configured separately." type = string default = null } @@ -147,8 +153,14 @@ variable "kms_key_id" { } # Custom image -variable "use_ecr_image" { - description = "Determines whether the build image will be pulled from ECR, if set to true an ECR repository will be created and an image needs to be pushed to it before running the build project" +variable "create_ecr_repository" { + description = "If set to true then an ECR repository will be created, and an image needs to be pushed to it before running the build project" type = string default = false } + +variable "ecr_repository_name" { + description = "Name of the ECR repository to create or use. If not specified and `create_ecr_repository` is true, then a default is used." + type = string + default = null +}