diff --git a/.ci/build.ps1 b/.ci/build.ps1 new file mode 100644 index 0000000000..d92a1b62e6 --- /dev/null +++ b/.ci/build.ps1 @@ -0,0 +1,8 @@ +$TOP_DIR=$(git rev-parse --show-toplevel) +$OUTPUT_DIR="$TOP_DIR/lambda_output" + +New-Item "$OUTPUT_DIR" -ItemType Directory -ErrorAction SilentlyContinue + +$env:DOCKER_BUILDKIT=1 +docker build --no-cache --target=final --output=type=local,dest="$OUTPUT_DIR" -f "$TOP_DIR/.ci/Dockerfile" "$TOP_DIR" + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..270106b1fa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*] +end_of_line = lf diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml new file mode 100644 index 0000000000..96f3de47a3 --- /dev/null +++ b/.github/workflows/packer-build.yml @@ -0,0 +1,35 @@ +name: "Packer checks" +on: + push: + branches: + - master + - develop + pull_request: + paths: + - "images/**" + - ".github/workflows/packer-build.yml" + +env: + AWS_REGION: eu-west-1 + +jobs: + verify_packer: + name: Verify packer + runs-on: ubuntu-latest + container: + image: hashicorp/packer:1.7.8 + defaults: + run: + working-directory: images/linux-amzn2 + steps: + - name: "Checkout" + uses: actions/checkout@v2 + + - name: packer init + run: packer init . + + - name: check terraform formatting + run: packer fmt -recursive -check=true . + + - name: packer validate + run: packer validate . diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 450976e2e1..273280b545 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -43,7 +43,7 @@ jobs: fail-fast: false matrix: terraform: [0.14.3, 0.15.5, 1.0.8] - example: ["default", "ubuntu"] + example: ["default", "ubuntu", "prebuilt"] defaults: run: working-directory: examples/${{ matrix.example }} diff --git a/README.md b/README.md index 58aef2a7cb..02a7f79fb5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This [Terraform](https://www.terraform.io/) module creates the required infrastr - [Install app](#install-app) - [Encryption](#encryption) - [Idle runners](#idle-runners) + - [Prebuilt Images](#prebuilt-images) - [Examples](#examples) - [Sub modules](#sub-modules) - [ARM64 configuration for submodules](#arm64-configuration-for-submodules) @@ -265,6 +266,10 @@ idle_config = [{ }] ``` +### Prebuilt Images + +This module also allows you to run agents from a prebuilt AMI to gain faster startup times. You can find more information in [the image README.md](/images/README.md) + #### Supported config Cron expressions are parsed by [cron-parser](https://github.com/harrisiirak/cron-parser#readme). The supported syntax. @@ -289,6 +294,7 @@ Examples are located in the [examples](./examples) directory. The following exam - _[Default](examples/default/README.md)_: The default example of the module - _[Permissions boundary](examples/permissions-boundary/README.md)_: Example usages of permissions boundaries. +- _[Prebuilt Images](examples/prebuilt/README.md)_: Example usages of deploying runners with a custom prebuilt image. ## Sub modules @@ -346,10 +352,10 @@ In case the setup does not work as intended follow the trace of events: | Name | Source | Version | |------|--------|---------| -| [runner\_binaries](#module\_runner\_binaries) | ./modules/runner-binaries-syncer | n/a | -| [runners](#module\_runners) | ./modules/runners | n/a | -| [ssm](#module\_ssm) | ./modules/ssm | n/a | -| [webhook](#module\_webhook) | ./modules/webhook | n/a | +| [runner\_binaries](#module\_runner\_binaries) | ./modules/runner-binaries-syncer | | +| [runners](#module\_runners) | ./modules/runners | | +| [ssm](#module\_ssm) | ./modules/ssm | | +| [webhook](#module\_webhook) | ./modules/webhook | | ## Resources @@ -422,6 +428,7 @@ In case the setup does not work as intended follow the trace of events: | [syncer\_lambda\_s3\_key](#input\_syncer\_lambda\_s3\_key) | S3 key for syncer lambda function. Required if using S3 bucket to specify lambdas. | `any` | `null` | no | | [syncer\_lambda\_s3\_object\_version](#input\_syncer\_lambda\_s3\_object\_version) | S3 object version for syncer lambda function. Useful if S3 versioning is enabled on source bucket. | `any` | `null` | no | | [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 | +| [enabled_userdata](#input\_enabled_userdata) | Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI | `bool` | `true` | no | | [userdata\_post\_install](#input\_userdata\_post\_install) | Script to be ran after the GitHub Actions runner is installed on the EC2 instances | `string` | `""` | no | | [userdata\_pre\_install](#input\_userdata\_pre\_install) | Script to be ran before the GitHub Actions runner is installed on the EC2 instances | `string` | `""` | no | | [userdata\_template](#input\_userdata\_template) | Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored. | `string` | `null` | no | diff --git a/examples/prebuilt/.terraform.lock.hcl b/examples/prebuilt/.terraform.lock.hcl new file mode 100644 index 0000000000..ba82aa4f3a --- /dev/null +++ b/examples/prebuilt/.terraform.lock.hcl @@ -0,0 +1,60 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.61.0" + constraints = ">= 3.27.0, >= 3.38.0" + hashes = [ + "h1:YZaozdn4J1Ax01NkubRAdv809vlpJOXRBC4KsqBzpvA=", + "h1:fpZ14qQnn+uEOO2ZOlBFHgty48Ol8IOwd+ewxZ4z3zc=", + "zh:0483ca802ddb0ae4f73144b4357ba72242c6e2641aeb460b1aa9a6f6965464b0", + "zh:274712214ebeb0c1269cbc468e5705bb5741dc45b05c05e9793ca97f22a1baa1", + "zh:3c6bd97a2ca809469ae38f6893348386c476cb3065b120b785353c1507401adf", + "zh:53dd41a9aed9860adbbeeb71a23e4f8195c656fd15a02c90fa2d302a5f577d8c", + "zh:65c639c547b97bc880fd83e65511c0f4bbfc91b63cada3b8c0d5776444221700", + "zh:a2769e19137ff480c1dd3e4f248e832df90fb6930a22c66264d9793895161714", + "zh:a5897a99332cc0071e46a71359b86a8e53ab09c1453e94cd7cf45a0b577ff590", + "zh:bdc2353642d16d8e2437a9015cd4216a1772be9736645cc17d1a197480e2b5b7", + "zh:cbeace1deae938f6c0aca3734e6088f3633ca09611aff701c15cb6d42f2b918a", + "zh:d33ca19012aabd98cc03fdeccd0bd5ce56e28f61a1dfbb2eea88e89487de7fb3", + "zh:d548b29a864b0687e85e8a993f208e25e3ecc40fcc5b671e1985754b32fdd658", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.1.0" + hashes = [ + "h1:/OpJKWupvFd8WJX1mTt8vi01pP7dkA6e//4l4C3TExE=", + "h1:KfieWtVyGWwplSoLIB5usKAUnrIkDQBkWaR5TI+4WYg=", + "zh:0f1ec65101fa35050978d483d6e8916664b7556800348456ff3d09454ac1eae2", + "zh:36e42ac19f5d68467aacf07e6adcf83c7486f2e5b5f4339e9671f68525fc87ab", + "zh:6db9db2a1819e77b1642ec3b5e95042b202aee8151a0256d289f2e141bf3ceb3", + "zh:719dfd97bb9ddce99f7d741260b8ece2682b363735c764cac83303f02386075a", + "zh:7598bb86e0378fd97eaa04638c1a4c75f960f62f69d3662e6d80ffa5a89847fe", + "zh:ad0a188b52517fec9eca393f1e2c9daea362b33ae2eb38a857b6b09949a727c1", + "zh:c46846c8df66a13fee6eff7dc5d528a7f868ae0dcf92d79deaac73cc297ed20c", + "zh:dc1a20a2eec12095d04bf6da5321f535351a594a636912361db20eb2a707ccc4", + "zh:e57ab4771a9d999401f6badd8b018558357d3cbdf3d33cc0c4f83e818ca8e94b", + "zh:ebdcde208072b4b0f8d305ebf2bfdc62c926e0717599dcf8ec2fd8c5845031c3", + "zh:ef34c52b68933bedd0868a13ccfd59ff1c820f299760b3c02e008dc95e2ece91", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.1.0" + hashes = [ + "h1:EPIax4Ftp2SNdB9pUfoSjxoueDoLc/Ck3EUoeX0Dvsg=", + "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", + "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", + "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", + "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", + "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", + "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", + "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", + "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", + "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", + "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", + "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", + "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + ] +} diff --git a/examples/prebuilt/README.md b/examples/prebuilt/README.md new file mode 100644 index 0000000000..3a560fb80d --- /dev/null +++ b/examples/prebuilt/README.md @@ -0,0 +1,83 @@ +# Action runners deployment with prebuilt image + +This module shows how to create GitHub action runners using a prebuilt AMI for the runners + +## Usages + +Steps for the full setup, such as creating a GitHub app can be found in the root module's [README](../../README.md). + +### Lambdas + +You can either download the released lambda code or build them locally yourself. + +First download the Lambda releases from GitHub. 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 + +```bash +cd lambdas-download +terraform init +terraform apply +cd .. +``` + +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 need to specify the build location for all of the zip files. + +```hcl + webhook_lambda_zip = "../../lambda_output/webhook.zip" + runner_binaries_syncer_lambda_zip = "../../lambda_output/runner-binaries-syncer.zip" + runners_lambda_zip = "../../lambda_output/runners.zip" +``` + +### GitHub App Configuration + +Before running Terraform, ensure the GitHub app is configured. See the [configuration details](../../README.md#usages) for more details. + +### Packer Image + +You will need to build your image. This example deployment uses the image example in `/images/linux-amz2`. You must build this image with packer in your AWS account first. Once you have built this you need to provider your owner ID as a variable + +## Deploy + +To use your image in the terraform modules you will need to set some values on the module. + +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. + +```hcl + +module "runners" { + ... + # set the name of the ami to use + ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] } + # provide the owner id of + ami_owners = [""] + + enabled_userdata = false + ... +} +``` + +If your owner is the same as the account you are logging into then you can use `aws_caller_identity` to retrieve it dynamically. + +```hcl +data "aws_caller_identity" "current" {} + +module "runners" { + ... + ami_owners = [data.aws_caller_identity.current.account_id] + ... +} +``` + +You can then deploy the terraform + +```bash +terraform init +terraform apply +``` + +You can receive the webhook details by running: + +```bash +terraform output -raw webhook_secret +``` + +Be-aware some shells will print some end of line character `%`. diff --git a/examples/prebuilt/lambdas-download/main.tf b/examples/prebuilt/lambdas-download/main.tf new file mode 100644 index 0000000000..87f31bd8a9 --- /dev/null +++ b/examples/prebuilt/lambdas-download/main.tf @@ -0,0 +1,25 @@ +locals { + version = "" +} + +module "lambdas" { + source = "../../../modules/download-lambda" + lambdas = [ + { + name = "webhook" + tag = local.version + }, + { + name = "runners" + tag = local.version + }, + { + name = "runner-binaries-syncer" + tag = local.version + } + ] +} + +output "files" { + value = module.lambdas.files +} diff --git a/examples/prebuilt/main.tf b/examples/prebuilt/main.tf new file mode 100644 index 0000000000..f67c5d984d --- /dev/null +++ b/examples/prebuilt/main.tf @@ -0,0 +1,46 @@ +locals { + environment = "prebuilt" + aws_region = "eu-west-1" +} + +resource "random_password" "random" { + length = 28 +} + +data "aws_caller_identity" "current" {} + +module "runners" { + source = "../../" + create_service_linked_role_spot = true + aws_region = local.aws_region + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + environment = local.environment + + github_app = { + key_base64 = var.github_app_key_base64 + id = var.github_app_id + webhook_secret = random_password.random.result + } + + webhook_lambda_zip = "../../lambda_output/webhook.zip" + runner_binaries_syncer_lambda_zip = "../../lambda_output/runner-binaries-syncer.zip" + runners_lambda_zip = "../../lambda_output/runners.zip" + + runner_extra_labels = "default,example" + + # configure your pre-built AMI + enabled_userdata = false + ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] } + ami_owners = [data.aws_caller_identity.current.account_id] + + # enable access to the runners via SSM + enable_ssm_on_runners = true + + # override delay of events in seconds + delay_webhook_event = 5 + + # override scaling down + scale_down_schedule_expression = "cron(* * * * ? *)" +} diff --git a/examples/prebuilt/outputs.tf b/examples/prebuilt/outputs.tf new file mode 100644 index 0000000000..d6886efe36 --- /dev/null +++ b/examples/prebuilt/outputs.tf @@ -0,0 +1,15 @@ +output "runners" { + value = { + lambda_syncer_name = module.runners.binaries_syncer.lambda.function_name + } +} + +output "webhook_endpoint" { + value = module.runners.webhook.endpoint +} + +output "webhook_secret" { + sensitive = true + value = random_password.random.result +} + diff --git a/examples/prebuilt/providers.tf b/examples/prebuilt/providers.tf new file mode 100644 index 0000000000..b6c81d5415 --- /dev/null +++ b/examples/prebuilt/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = local.aws_region +} diff --git a/examples/prebuilt/variables.tf b/examples/prebuilt/variables.tf new file mode 100644 index 0000000000..69dcd0c61c --- /dev/null +++ b/examples/prebuilt/variables.tf @@ -0,0 +1,4 @@ + +variable "github_app_key_base64" {} + +variable "github_app_id" {} diff --git a/examples/prebuilt/versions.tf b/examples/prebuilt/versions.tf new file mode 100644 index 0000000000..c96d0eee84 --- /dev/null +++ b/examples/prebuilt/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.27" + } + local = { + source = "hashicorp/local" + } + random = { + source = "hashicorp/random" + } + } + required_version = ">= 0.14" +} diff --git a/examples/prebuilt/vpc.tf b/examples/prebuilt/vpc.tf new file mode 100644 index 0000000000..a7d21422f1 --- /dev/null +++ b/examples/prebuilt/vpc.tf @@ -0,0 +1,7 @@ +module "vpc" { + source = "git::https://github.com/philips-software/terraform-aws-vpc.git?ref=2.2.0" + + environment = local.environment + aws_region = local.aws_region + create_private_hosted_zone = false +} diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000000..172f97d0a1 --- /dev/null +++ b/images/README.md @@ -0,0 +1,37 @@ +# Prebuilt Images + +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. + +These images share the same scripting as used in the user-data mechanism in `/modules/runners/templates/`. We use a `tempaltefile` mechanism to insert the relevant script fragments into the scripts used for provisioning the images. + +The example in `linux-amzn2` also uploads a `start-runner.sh` 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. + +## Building your own + +To build these images you first need to install packer. +You will also need an amazon account and to have provisioned your credentials for packer to consume. + +Assuming you are building the `linux-amzn2` image. Then run the following from within the `linux-amzn2` folder + +```bash +packer init . +packer validate . +packer build github_agent.linux.pkr.hcl +``` + +Your image will then begin to build inside AWS and when finished you will be provided with complete AMI. + +## Using your image + +To use your image in the terraform modules you will need to set some values on the module. + +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. + +```hcl +# set the name of the ami to use +ami_filter = { name = ["github-runner-amzn2-x86_64-2021*"] } +# provide the owner id of +ami_owners = [""] + +enabled_userdata = false +``` diff --git a/images/install-runner.sh b/images/install-runner.sh new file mode 100644 index 0000000000..e042333f00 --- /dev/null +++ b/images/install-runner.sh @@ -0,0 +1,8 @@ +#!/bin/bash -e + +user_name=ec2-user + +## This wrapper file re-uses scripts in the /modules/runners/templates directory +## of this repo. These are the same that are used by the user_data functionality +## to bootstrap the instance if it is started from an existing AMI. +${install_runner} \ No newline at end of file diff --git a/images/linux-amzn2/github_agent.linux.pkr.hcl b/images/linux-amzn2/github_agent.linux.pkr.hcl new file mode 100644 index 0000000000..566423d03f --- /dev/null +++ b/images/linux-amzn2/github_agent.linux.pkr.hcl @@ -0,0 +1,87 @@ +packer { + required_plugins { + amazon = { + version = ">= 0.0.2" + source = "github.com/hashicorp/amazon" + } + } +} + +variable "action_runner_url" { + description = "The URL to the tarball of the action runner" + type = string + default = "https://github.com/actions/runner/releases/download/v2.284.0/actions-runner-linux-x64-2.284.0.tar.gz" +} + +variable "region" { + description = "The region to build the image in" + type = string + default = "eu-west-1" +} + +source "amazon-ebs" "githubrunner" { + ami_name = "github-runner-amzn2-x86_64-${formatdate("YYYYMMDDhhmm", timestamp())}" + instance_type = "m3.medium" + region = var.region + source_ami_filter { + filters = { + name = "amzn2-ami-hvm-2.*-x86_64-ebs" + root-device-type = "ebs" + virtualization-type = "hvm" + } + most_recent = true + owners = ["137112412989"] + } + ssh_username = "ec2-user" + tags = { + OS_Version = "amzn2" + Release = "Latest" + Base_AMI_Name = "{{ .SourceAMIName }}" + } +} + +build { + name = "githubactions-runner" + sources = [ + "source.amazon-ebs.githubrunner" + ] + provisioner "shell" { + environment_vars = [] + inline = [ + "sudo yum update -y", + "sudo yum install -y amazon-cloudwatch-agent curl jq git", + "sudo amazon-linux-extras install docker", + "sudo systemctl enable docker.service", + "sudo systemctl enable containerd.service", + "sudo service docker start", + "sudo usermod -a -G docker ec2-user", + ] + } + + provisioner "shell" { + environment_vars = [ + "RUNNER_TARBALL_URL=${var.action_runner_url}" + ] + inline = [templatefile("../install-runner.sh", { + install_runner = templatefile("../../modules/runners/templates/install-runner.sh", { + ARM_PATCH = "" + S3_LOCATION_RUNNER_DISTRIBUTION = "" + }) + })] + } + + provisioner "file" { + content = templatefile("../start-runner.sh", { + start_runner = templatefile("../../modules/runners/templates/start-runner.sh", {}) + }) + destination = "/tmp/start-runner.sh" + } + + provisioner "shell" { + inline = [ + "sudo mv /tmp/start-runner.sh /var/lib/cloud/scripts/per-boot/start-runner.sh", + "sudo chmod +x /var/lib/cloud/scripts/per-boot/start-runner.sh", + ] + } + +} \ No newline at end of file diff --git a/images/start-runner.sh b/images/start-runner.sh new file mode 100644 index 0000000000..7555e44225 --- /dev/null +++ b/images/start-runner.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e +exec > >(tee /var/log/runner-startup.log | logger -t user-data -s 2>/dev/console) 2>&1 + +cd /home/ec2-user/actions-runner + +## This wrapper file re-uses scripts in the /modules/runners/templates directory +## of this repo. These are the same that are used by the user_data functionality +## to bootstrap the instance if it is started from an existing AMI. +${start_runner} \ No newline at end of file diff --git a/main.tf b/main.tf index 4c9b3858ec..6708b4cd06 100644 --- a/main.tf +++ b/main.tf @@ -1,6 +1,7 @@ locals { tags = merge(var.tags, { - Environment = var.environment + Environment = var.environment, + "ghr:environment" = format("%s", var.environment) }) s3_action_runner_url = "s3://${module.runner_binaries.bucket.id}/${module.runner_binaries.runner_distribution_object_key}" @@ -125,6 +126,7 @@ module "runners" { role_path = var.role_path role_permissions_boundary = var.role_permissions_boundary + enabled_userdata = var.enabled_userdata userdata_template = var.userdata_template userdata_pre_install = var.userdata_pre_install userdata_post_install = var.userdata_post_install diff --git a/modules/runners/README.md b/modules/runners/README.md index fcc9d6b85b..05cb8b2c42 100644 --- a/modules/runners/README.md +++ b/modules/runners/README.md @@ -79,6 +79,7 @@ No modules. | [aws_iam_role.scale_down](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.scale_up](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.describe_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.dist_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.runner_session_manager_aws_managed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.scale_down](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | @@ -98,6 +99,9 @@ No modules. | [aws_launch_template.runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template) | resource | | [aws_security_group.runner_sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_ssm_parameter.cloudwatch_agent_config_runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.runner_agent_mode](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.runner_config_run_as](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.runner_enable_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_ami.runner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.lambda_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -149,7 +153,7 @@ No modules. | [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra labels for the runners (GitHub). Separate each label by a comma | `string` | `""` | no | | [runner\_group\_name](#input\_runner\_group\_name) | Name of the runner group. | `string` | `"Default"` | no | | [runner\_iam\_role\_managed\_policy\_arns](#input\_runner\_iam\_role\_managed\_policy\_arns) | Attach AWS or customer-managed IAM policies (by ARN) to the runner IAM role | `list(string)` | `[]` | no | -| [runner\_log\_files](#input\_runner\_log\_files) | (optional) List of logfiles to send to CloudWatch, will only be used if `enable_cloudwatch_agent` is set to true. Object description: `log_group_name`: Name of the log group, `prefix_log_group`: If true, the log group name will be prefixed with `/github-self-hosted-runners/`, `file_path`: path to the log file, `log_stream_name`: name of the log stream. |
list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
}))
|
[
{
"file_path": "/var/log/messages",
"log_group_name": "messages",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
},
{
"file_path": "/var/log/user-data.log",
"log_group_name": "user_data",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
},
{
"file_path": "/home/ec2-user/actions-runner/_diag/Runner_**.log",
"log_group_name": "runner",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
}
]
| no | +| [runner\_log\_files](#input\_runner\_log\_files) | (optional) List of logfiles to send to CloudWatch, will only be used if `enable_cloudwatch_agent` is set to true. Object description: `log_group_name`: Name of the log group, `prefix_log_group`: If true, the log group name will be prefixed with `/github-self-hosted-runners/`, `file_path`: path to the log file, `log_stream_name`: name of the log stream. |
list(object({
log_group_name = string
prefix_log_group = bool
file_path = string
log_stream_name = string
}))
|
[
{
"file_path": "/var/log/messages",
"log_group_name": "messages",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
},
{
"file_path": "/var/log/user-data.log",
"log_group_name": "user_data",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
},
{
"file_path": "/var/log/runner-startup.log",
"log_group_name": "runner-startup",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
},
{
"file_path": "/home/ec2-user/actions-runner/_diag/Runner_**.log",
"log_group_name": "runner",
"log_stream_name": "{instance_id}",
"prefix_log_group": true
}
]
| no | | [runners\_lambda\_s3\_key](#input\_runners\_lambda\_s3\_key) | S3 key for runners lambda function. Required if using S3 bucket to specify lambdas. | `any` | `null` | no | | [runners\_lambda\_s3\_object\_version](#input\_runners\_lambda\_s3\_object\_version) | S3 object version for runners lambda function. Useful if S3 versioning is enabled on source bucket. | `any` | `null` | no | | [runners\_maximum\_count](#input\_runners\_maximum\_count) | The maximum number of runners that will be created. | `number` | `3` | no | @@ -160,6 +164,7 @@ No modules. | [sqs\_build\_queue](#input\_sqs\_build\_queue) | SQS queue to consume accepted build events. |
object({
arn = string
})
| n/a | yes | | [subnet\_ids](#input\_subnet\_ids) | List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | n/a | yes | | [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 | +| [enabled\_userdata](#input\_enabled_userdata) | Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI | `bool` | `true` | no | | [userdata\_post\_install](#input\_userdata\_post\_install) | User-data script snippet to insert after GitHub action runner install | `string` | `""` | no | | [userdata\_pre\_install](#input\_userdata\_pre\_install) | User-data script snippet to insert before GitHub action runner install | `string` | `""` | no | | [userdata\_template](#input\_userdata\_template) | Alternative user-data template, replacing the default template. By providing your own user\_data you have to take care of installing all required software, including the action runner. Variables userdata\_pre/post\_install are ignored. | `string` | `null` | no | diff --git a/modules/runners/logging.tf b/modules/runners/logging.tf index b66fe29c33..e0b19d59a8 100644 --- a/modules/runners/logging.tf +++ b/modules/runners/logging.tf @@ -28,7 +28,7 @@ resource "aws_cloudwatch_log_group" "gh_runners" { } resource "aws_iam_role_policy" "cloudwatch" { - count = var.enable_ssm_on_runners ? 1 : 0 + count = var.enable_cloudwatch_agent ? 1 : 0 name = "CloudWatchLogginAndMetrics" role = aws_iam_role.runner.name policy = templatefile("${path.module}/policies/instance-cloudwatch-policy.json", diff --git a/modules/runners/main.tf b/modules/runners/main.tf index de788f1b2f..8a48fece0e 100644 --- a/modules/runners/main.tf +++ b/modules/runners/main.tf @@ -3,20 +3,18 @@ locals { { "Name" = format("%s-action-runner", var.environment) }, - { - "Environment" = format("%s", var.environment) - }, var.tags, ) - name_sg = var.overrides["name_sg"] == "" ? local.tags["Name"] : var.overrides["name_sg"] - name_runner = var.overrides["name_runner"] == "" ? local.tags["Name"] : var.overrides["name_runner"] - role_path = var.role_path == null ? "/${var.environment}/" : var.role_path - instance_profile_path = var.instance_profile_path == null ? "/${var.environment}/" : var.instance_profile_path - lambda_zip = var.lambda_zip == null ? "${path.module}/lambdas/runners/runners.zip" : var.lambda_zip - userdata_template = var.userdata_template == null ? "${path.module}/templates/user-data.sh" : var.userdata_template - userdata_arm_patch = "${path.module}/templates/arm-runner-patch.tpl" - userdata_install_config_runner = "${path.module}/templates/install-config-runner.sh" + name_sg = var.overrides["name_sg"] == "" ? local.tags["Name"] : var.overrides["name_sg"] + name_runner = var.overrides["name_runner"] == "" ? local.tags["Name"] : var.overrides["name_runner"] + role_path = var.role_path == null ? "/${var.environment}/" : var.role_path + instance_profile_path = var.instance_profile_path == null ? "/${var.environment}/" : var.instance_profile_path + lambda_zip = var.lambda_zip == null ? "${path.module}/lambdas/runners/runners.zip" : var.lambda_zip + userdata_template = var.userdata_template == null ? "${path.module}/templates/user-data.sh" : var.userdata_template + userdata_arm_patch = "${path.module}/templates/arm-runner-patch.tpl" + userdata_install_runner = "${path.module}/templates/install-runner.sh" + userdata_start_runner = "${path.module}/templates/start-runner.sh" instance_types = distinct(var.instance_types == null ? [var.instance_type] : var.instance_types) @@ -112,32 +110,27 @@ resource "aws_launch_template" "runner" { } - user_data = base64encode(templatefile(local.userdata_template, { + user_data = var.enabled_userdata ? base64encode(templatefile(local.userdata_template, { + pre_install = var.userdata_pre_install + install_runner = templatefile(local.userdata_install_runner, { + S3_LOCATION_RUNNER_DISTRIBUTION = var.s3_location_runner_binaries + ARM_PATCH = var.runner_architecture == "arm64" ? templatefile(local.userdata_arm_patch, {}) : "" + }) + post_install = var.userdata_post_install + start_runner = templatefile(local.userdata_start_runner, {}) + ghes_url = var.ghes_url + ghes_ssl_verify = var.ghes_ssl_verify + ## retain these for backwards compatibility environment = var.environment - pre_install = var.userdata_pre_install - post_install = var.userdata_post_install enable_cloudwatch_agent = var.enable_cloudwatch_agent ssm_key_cloudwatch_agent_config = var.enable_cloudwatch_agent ? aws_ssm_parameter.cloudwatch_agent_config_runner[0].name : "" - ghes_url = var.ghes_url - ghes_ssl_verify = var.ghes_ssl_verify - install_config_runner = local.install_config_runner - })) + })) : "" tags = local.tags update_default_version = true } -locals { - arm_patch = var.runner_architecture == "arm64" ? templatefile(local.userdata_arm_patch, {}) : "" - install_config_runner = templatefile(local.userdata_install_config_runner, { - environment = var.environment - s3_location_runner_distribution = var.s3_location_runner_binaries - run_as_root_user = var.runner_as_root ? "root" : "" - arm_patch = local.arm_patch - }) -} - resource "aws_security_group" "runner_sg" { name_prefix = "${var.environment}-github-actions-runner-sg" description = "Github Actions Runner security group" diff --git a/modules/runners/policies-runner.tf b/modules/runners/policies-runner.tf index 2aa62da8dd..212f9a4b24 100644 --- a/modules/runners/policies-runner.tf +++ b/modules/runners/policies-runner.tf @@ -26,7 +26,8 @@ resource "aws_iam_role_policy" "ssm_parameters" { role = aws_iam_role.runner.name policy = templatefile("${path.module}/policies/instance-ssm-parameters-policy.json", { - arn_ssm_parameters = "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${var.environment}-*" + arn_ssm_parameters_prefix= "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${var.environment}-*" + arn_ssm_parameters_path ="arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${var.environment}/*" } ) } @@ -41,6 +42,12 @@ resource "aws_iam_role_policy" "dist_bucket" { ) } +resource "aws_iam_role_policy" "describe_tags" { + name = "runner-describe-tags" + role = aws_iam_role.runner.name + policy = file("${path.module}/policies/instance-describe-tags-policy.json") +} + resource "aws_iam_role_policy_attachment" "managed_policies" { count = length(var.runner_iam_role_managed_policy_arns) role = aws_iam_role.runner.name diff --git a/modules/runners/policies/instance-describe-tags-policy.json b/modules/runners/policies/instance-describe-tags-policy.json new file mode 100644 index 0000000000..c66074fdaf --- /dev/null +++ b/modules/runners/policies/instance-describe-tags-policy.json @@ -0,0 +1,10 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:DescribeTags", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/modules/runners/policies/instance-ssm-parameters-policy.json b/modules/runners/policies/instance-ssm-parameters-policy.json index 96e8b02c95..5a7aa9e356 100644 --- a/modules/runners/policies/instance-ssm-parameters-policy.json +++ b/modules/runners/policies/instance-ssm-parameters-policy.json @@ -6,15 +6,19 @@ "Action": [ "ssm:DeleteParameter" ], - "Resource": "${arn_ssm_parameters}" + "Resource": "${arn_ssm_parameters_prefix}" }, { "Effect": "Allow", "Action": [ + "ssm:GetParameter", "ssm:GetParameters", - "ssm:GetParameter" + "ssm:GetParametersByPath" ], - "Resource": "${arn_ssm_parameters}" + "Resource": [ + "${arn_ssm_parameters_prefix}", + "${arn_ssm_parameters_path}" + ] } ] } diff --git a/modules/runners/runner-config.tf b/modules/runners/runner-config.tf new file mode 100644 index 0000000000..eb6370e58f --- /dev/null +++ b/modules/runners/runner-config.tf @@ -0,0 +1,21 @@ +resource "aws_ssm_parameter" "runner_config_run_as" { + name = "/${var.environment}/runner/run-as" + type = "String" + value = var.runner_as_root ? "root" : "ec2-user" + tags = local.tags +} + +resource "aws_ssm_parameter" "runner_agent_mode" { + name = "/${var.environment}/runner/agent-mode" + type = "String" + # TODO: Update this to allow for ephemeral runners + value = "persistent" + tags = local.tags +} + +resource "aws_ssm_parameter" "runner_enable_cloudwatch" { + name = "/${var.environment}/runner/enable-cloudwatch" + type = "String" + value = var.enable_cloudwatch_agent + tags = local.tags +} diff --git a/modules/runners/templates/install-config-runner.sh b/modules/runners/templates/install-config-runner.sh deleted file mode 100644 index a1147303b1..0000000000 --- a/modules/runners/templates/install-config-runner.sh +++ /dev/null @@ -1,35 +0,0 @@ -cd /home/$USER_NAME -mkdir actions-runner && cd actions-runner - -TOKEN=$(curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 180") -REGION=$(curl -f -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) - -aws s3 cp ${s3_location_runner_distribution} actions-runner.tar.gz --region $REGION -tar xzf ./actions-runner.tar.gz -rm -rf actions-runner.tar.gz - -${arm_patch} - -INSTANCE_ID=$(curl -f -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-id) - -echo wait for configuration -while [[ $(aws ssm get-parameters --names ${environment}-$INSTANCE_ID --with-decryption --region $REGION | jq -r ".Parameters | .[0] | .Value") == null ]]; do - echo Waiting for configuration ... - sleep 1 -done -CONFIG=$(aws ssm get-parameters --names ${environment}-$INSTANCE_ID --with-decryption --region $REGION | jq -r ".Parameters | .[0] | .Value") -aws ssm delete-parameter --name ${environment}-$INSTANCE_ID --region $REGION - -export RUNNER_ALLOW_RUNASROOT=1 -os_id=$(awk -F= '/^ID/{print $2}' /etc/os-release) -if [[ "$os_id" =~ ^ubuntu.* ]]; then - ./bin/installdependencies.sh -fi - -./config.sh --unattended --name $INSTANCE_ID --work "_work" $CONFIG - -chown -R $USER_NAME:$USER_NAME . -OVERWRITE_SERVICE_USER=${run_as_root_user} -SERVICE_USER=$${OVERWRITE_SERVICE_USER:-$USER_NAME} - -./svc.sh install $SERVICE_USER diff --git a/modules/runners/templates/install-runner.sh b/modules/runners/templates/install-runner.sh new file mode 100644 index 0000000000..94e440e265 --- /dev/null +++ b/modules/runners/templates/install-runner.sh @@ -0,0 +1,50 @@ +# shellcheck shell=bash + +## install the runner + +s3_location=${S3_LOCATION_RUNNER_DISTRIBUTION} + +if [ -z "$RUNNER_TARBALL_URL" ] && [ -z "$s3_location" ]; then + echo "Neither RUNNER_TARBALL_URL or s3_location are set" + exit 1 +fi + +file_name="actions-runner.tar.gz" + +echo "Creating actions-runner directory for the GH Action installtion" +cd /home/"$user_name" +mkdir actions-runner && cd actions-runner + + +if [[ -n "$RUNNER_TARBALL_URL" ]]; then + echo "Downloading the GH Action runner from $RUNNER_TARBALL_URL to $file_name" + curl -o $file_name -L "$RUNNER_TARBALL_URL" +else + echo "Retrieving TOKEN from AWS API" + token=$(curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 180") + + region=$(curl -f -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) + echo "Reteieved REGION from AWS API ($region)" + + echo "Downloading the GH Action runner from s3 bucket $s3_location" + aws s3 cp "$s3_location" "$file_name" --region "$region" +fi + +echo "Un-tar action runner" +tar xzf ./$file_name +echo "Delete tar file" +rm -rf $file_name + +${ARM_PATCH} + +echo "export RUNNER_ALLOW_RUNASROOT=1" +export RUNNER_ALLOW_RUNASROOT=1 + +os_id=$(awk -F= '/^ID/{print $2}' /etc/os-release) +if [[ "$os_id" =~ ^ubuntu.* ]]; then + echo "Installing dependencies" + ./bin/installdependencies.sh +fi + +echo "Set file ownership of action runner" +chown -R "$user_name":"$user_name" . \ No newline at end of file diff --git a/modules/runners/templates/start-runner.sh b/modules/runners/templates/start-runner.sh new file mode 100644 index 0000000000..92e1e34526 --- /dev/null +++ b/modules/runners/templates/start-runner.sh @@ -0,0 +1,77 @@ +# shellcheck shell=bash + +## Retrieve instance metadata + +echo "Retrieving TOKEN from AWS API" +token=$(curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 180") + +region=$(curl -f -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) +echo "Reteieved REGION from AWS API ($region)" + +instance_id=$(curl -f -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/meta-data/instance-id) +echo "Reteieved INSTANCE_ID from AWS API ($instance_id)" + +tags=$(aws ec2 describe-tags --region "$region" --filters "Name=resource-id,Values=$instance_id") +echo "Retrieved tags from AWS API ($tags)" + +environment=$(echo "$tags" | jq -r '.Tags[] | select(.Key == "ghr:environment") | .Value') +echo "Reteieved ghr:environment tag - ($environment)" + +parameters=$(aws ssm get-parameters-by-path --path "/$environment/runner" --region "$region" --query "Parameters[*].{Name:Name,Value:Value}") +echo "Retrieved parameters from AWS SSM ($parameters)" + +run_as=$(echo "$parameters" | jq --arg environment "$environment" -r '.[] | select(.Name == "/\($environment)/runner/run-as") | .Value') +echo "Retrieved /$environment/runner/run-as parameter - ($run_as)" + +enable_cloudwatch_agent=$(echo "$parameters" | jq --arg environment "$environment" -r '.[] | select(.Name == "/\($environment)/runner/enable-cloudwatch") | .Value') +echo "Retrieved /$environment/runner/enable-cloudwatch parameter - ($enable_cloudwatch_agent)" + +agent_mode=$(echo "$parameters" | jq --arg environment "$environment" -r '.[] | select(.Name == "/\($environment)/runner/agent-mode") | .Value') +echo "Retrieved /$environment/runner/agent-mode parameter - ($agent_mode)" + +if [[ -n "$enable_cloudwatch_agent" ]]; then + echo "Cloudwatch is enabled" + amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c "ssm:$environment-cloudwatch_agent_config_runner" +fi + + +## Configure the runner + +echo "Get GH Runner token from AWS SSM" +config=$(aws ssm get-parameters --names "$environment"-"$instance_id" --with-decryption --region "$region" | jq -r ".Parameters | .[0] | .Value") + +while [[ -z "$config" ]]; do + echo "Waiting for GH Runner token to become available in AWS SSM" + sleep 1 + config=$(aws ssm get-parameters --names "$environment"-"$instance_id" --with-decryption --region "$region" | jq -r ".Parameters | .[0] | .Value") +done + +echo "Delete GH Runner token from AWS SSM" +aws ssm delete-parameter --name "$environment"-"$instance_id" --region "$region" + +if [ -z "$run_as" ]; then + run_as="ec2-user" +fi + +echo "Configure GH Runner as user $run_as" +sudo -u "$run_as" -- ./config.sh --unattended --name "$instance_id" --work "_work" $${config} + +## Start the runner +echo "Starting runner after $(awk '{print int($1/3600)":"int(($1%3600)/60)":"int($1%60)}' /proc/uptime)" +echo "Starting the runner as user $run_as" + +if [[ $agent_mode = "ephemeral" ]]; then + echo "Starting the runner in ephemeral mode" + sudo -u "$run_as" -- ./run.sh + echo "Runner has finished" + + echo "Stopping cloudwatch service" + service awslogsd stop + echo "Terminating instance" + aws ec2 terminate-instances --instance-ids "$instance_id" --region "$region" +else + echo "Installing the runner as a service" + ./svc.sh install "$run_as" + echo "Starting the runner in persistent mode" + ./svc.sh start +fi \ No newline at end of file diff --git a/modules/runners/templates/user-data.sh b/modules/runners/templates/user-data.sh index 0fdf4faa7b..568e48c8c6 100644 --- a/modules/runners/templates/user-data.sh +++ b/modules/runners/templates/user-data.sh @@ -5,21 +5,17 @@ ${pre_install} yum update -y -%{ if enable_cloudwatch_agent ~} -yum install amazon-cloudwatch-agent -y -amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c ssm:${ssm_key_cloudwatch_agent_config} -%{ endif ~} - # Install docker amazon-linux-extras install docker service docker start usermod -a -G docker ec2-user -yum install -y curl jq git +yum install -y amazon-cloudwatch-agent curl jq git + +user_name=ec2-user -USER_NAME=ec2-user -${install_config_runner} +${install_runner} ${post_install} -./svc.sh start +${start_runner} diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index fd72a55710..c349557fde 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -84,6 +84,12 @@ variable "ami_owners" { default = ["amazon"] } +variable "enabled_userdata" { + description = "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI" + type = bool + default = true +} + variable "userdata_template" { description = "Alternative user-data template, replacing the default template. By providing your own user_data you have to take care of installing all required software, including the action runner. Variables userdata_pre/post_install are ignored." type = string @@ -297,7 +303,13 @@ variable "runner_log_files" { "prefix_log_group" : true, "file_path" : "/home/ec2-user/actions-runner/_diag/Runner_**.log", "log_stream_name" : "{instance_id}" - } + }, + { + "log_group_name" : "runner-startup", + "prefix_log_group" : true, + "file_path" : "/var/log/runner-startup.log", + "log_stream_name" : "{instance_id}" + }, ] } diff --git a/variables.tf b/variables.tf index 21b8ed3dea..a627a067f2 100644 --- a/variables.tf +++ b/variables.tf @@ -165,6 +165,12 @@ variable "kms_key_arn" { default = null } +variable "enabled_userdata" { + description = "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI" + type = bool + default = true +} + variable "userdata_template" { description = "Alternative user-data template, replacing the default template. By providing your own user_data you have to take care of installing all required software, including the action runner. Variables userdata_pre/post_install are ignored." type = string @@ -313,7 +319,13 @@ variable "runner_log_files" { "prefix_log_group" : true, "file_path" : "/home/ec2-user/actions-runner/_diag/Runner_**.log", "log_stream_name" : "{instance_id}" - } + }, + { + "log_group_name" : "runner-startup", + "prefix_log_group" : true, + "file_path" : "/var/log/runner-startup.log", + "log_stream_name" : "{instance_id}" + }, ] }