From 0dd1e1086359bfbbe3256b63a9ce204a71d06fba Mon Sep 17 00:00:00 2001 From: Viktor Kleen Date: Mon, 4 Sep 2023 15:41:23 +0000 Subject: [PATCH] Add infrastructure for on-demand ARM64 runners on AWS (#1569) * Add infrastructure for on-demand ARM64 runners on AWS With this change, ARM64 release artifacts will be built automatically by a GitHub workflow. Since GitHub doesn't offer hosted runners running on ARM64, we're spinning up an EC2 spot instance on demand and run the jobs building ARM64 artifacts there. As a fun side note, the Terraform infrastructure code is written entirely in Nickel. * Remove unused `update-github` script * Address comments from code review * Address comments from code review --- .github/workflows/release-artifacts.yaml | 119 ++++++++++++-- flake.nix | 36 ++++- infra/.gitignore | 3 + infra/README.md | 56 +++++++ infra/github-oidc.ncl | 49 ++++++ infra/github-runner.nix | 62 ++++++++ infra/github-variables.ncl | 37 +++++ infra/lambdas.ncl | 188 +++++++++++++++++++++++ infra/main.ncl | 40 +++++ infra/runner.ncl | 104 +++++++++++++ infra/spot_lambdas/start.py | 52 +++++++ infra/spot_lambdas/stop.py | 53 +++++++ infra/state.ncl | 41 +++++ infra/vpc.ncl | 49 ++++++ 14 files changed, 877 insertions(+), 12 deletions(-) create mode 100644 infra/.gitignore create mode 100644 infra/README.md create mode 100644 infra/github-oidc.ncl create mode 100644 infra/github-runner.nix create mode 100644 infra/github-variables.ncl create mode 100644 infra/lambdas.ncl create mode 100644 infra/main.ncl create mode 100644 infra/runner.ncl create mode 100644 infra/spot_lambdas/start.py create mode 100644 infra/spot_lambdas/stop.py create mode 100644 infra/state.ncl create mode 100644 infra/vpc.ncl diff --git a/.github/workflows/release-artifacts.yaml b/.github/workflows/release-artifacts.yaml index a45dd44813..5b9335715c 100644 --- a/.github/workflows/release-artifacts.yaml +++ b/.github/workflows/release-artifacts.yaml @@ -8,13 +8,85 @@ on: description: "The release tag to target" permissions: + id-token: write contents: write packages: write jobs: + start-runner: + name: Start EC2 runner + runs-on: ubuntu-latest + outputs: + instance_id: ${{ steps.invoke-start.outputs.INSTANCE_ID }} + steps: + - uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ secrets.EC2_ROLE }} + aws-region: ${{ vars.EC2_REGION }} + - name: Start EC2 instance + id: invoke-start + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }} + EC2_START: ${{ secrets.EC2_START }} + run: | + RUNNER_TOKEN=$(gh api -X POST -q '.token' /repos/${{ github.repository }}/actions/runners/registration-token) + aws lambda invoke \ + --cli-binary-format raw-in-base64-out \ + --function-name "$EC2_START" \ + --payload '{"ref_name":"${{ github.ref_name }}","runner_token":"'"${RUNNER_TOKEN}"'"}' \ + response.json + INSTANCE_ID=$(jq -r '.body.instance_id' < response.json) + echo "INSTANCE_ID=${INSTANCE_ID}" >>"$GITHUB_OUTPUT" + echo "Got EC2 instance ${INSTANCE_ID}" + echo 'Waiting for GitHub runner to start' + while [[ -z "$(gh api /repos/${{ github.repository }}/actions/runners | jq '.runners[] | select(.name == "ec2-spot")')" ]]; do + sleep 60 + done + echo 'Done 🎉' + + stop-runner: + name: Stop EC2 runner + runs-on: ubuntu-latest + # Ensure that `stop-runner` will always stop the EC2 instance, even if other jobs failed or were canceled + if: ${{ always() }} + needs: + - start-runner + - docker-multiplatform-image + - static-binary + steps: + - uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ secrets.EC2_ROLE }} + aws-region: ${{ vars.EC2_REGION }} + - name: Delete GitHub Runner + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }} + run: | + RUNNER_ID=$(gh api /repos/${{ github.repository }}/actions/runners | jq '.runners[] | select(.name == "ec2-spot") | .id') + if [[ -n "${RUNNER_ID}" ]]; then + gh api -X DELETE /repos/${{ github.repository }}/actions/runners/${RUNNER_ID} + fi + - name: Lambda Invoke Stop + env: + EC2_STOP: ${{ secrets.EC2_STOP }} + run: | + aws lambda invoke \ + --cli-binary-format raw-in-base64-out \ + --function-name "$EC2_STOP" \ + --payload '{"instance_id":"${{ needs.start-runner.outputs.instance_id }}"}' \ + response.json + cat response.json + docker-image: name: "Build docker image" - runs-on: "ubuntu-latest" + strategy: + matrix: + os: + - runs-on: ubuntu-latest + architecture: x86_64 + - runs-on: [EC2, ARM64, Linux] + architecture: arm64 + runs-on: ${{ matrix.os.runs-on }} steps: - uses: actions/checkout@v3 with: @@ -30,7 +102,7 @@ jobs: name: "Build docker image" run: | nix build --print-build-logs .#dockerImage - cp ./result nickel-docker-image.tar.gz + cp ./result nickel-${{ matrix.os.architecture }}-docker-image.tar.gz echo "imageName=$(nix eval --raw .#dockerImage.imageName)" >> "$GITHUB_OUTPUT" echo "imageTag=$(nix eval --raw .#dockerImage.imageTag)" >> "$GITHUB_OUTPUT" - name: "Upload docker image as release asset" @@ -38,7 +110,7 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }} run: | - gh release upload --clobber $RELEASE_TAG nickel-docker-image.tar.gz + gh release upload --clobber $RELEASE_TAG nickel-${{ matrix.os.architecture }}-docker-image.tar.gz - name: Log in to registry # This is where you will update the personal access token to GITHUB_TOKEN run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin @@ -47,13 +119,38 @@ jobs: RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }} TARBALL_TAG: ${{ steps.build-image.outputs.imageName }}:${{ steps.build-image.outputs.imageTag }} run: | - docker load -i nickel-docker-image.tar.gz - docker tag "$TARBALL_TAG" ghcr.io/tweag/nickel:$RELEASE_TAG - docker push ghcr.io/tweag/nickel:$RELEASE_TAG + docker load -i nickel-${{ matrix.os.architecture }}-docker-image.tar.gz + docker tag "$TARBALL_TAG" ghcr.io/tweag/nickel:$RELEASE_TAG-${{ matrix.os.architecture}} + docker push ghcr.io/tweag/nickel:$RELEASE_TAG-${{ matrix.os.architecture}} + + docker-multiplatform-image: + name: "Assemble multi-platform Docker image" + runs-on: ubuntu-latest + needs: docker-image + steps: + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + - name: Assemble and push image + env: + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }} + run: | + docker manifest create \ + ghcr.io/tweag/nickel:$RELEASE_TAG \ + --amend ghcr.io/tweag/nickel:$RELEASE_TAG-x86_64 \ + --amend ghcr.io/tweag/nickel:$RELEASE_TAG-arm64 \ + docker manifest push ghcr.io/tweag/nickel:$RELEASE_TAG + static-binary: name: "Build Nickel release binary" - runs-on: "ubuntu-latest" + strategy: + matrix: + os: + - runs-on: ubuntu-latest + architecture: x86_64 + - runs-on: [EC2, ARM64, Linux] + architecture: arm64 + runs-on: ${{ matrix.os.runs-on }} steps: - uses: actions/checkout@v3 with: @@ -65,13 +162,13 @@ jobs: experimental-features = nix-command flakes accept-flake-config = true nix_path: "nixpkgs=channel:nixos-unstable" - - name: "Build x86_64 static binary" + - name: "Build static binary" run: | nix build --print-build-logs .#nickel-static - cp ./result/bin/nickel nickel-x86_64-linux - - name: "Upload x86_64 static binary as release asset" + cp ./result/bin/nickel nickel-${{ os.matrix.architecture }}-linux + - name: "Upload static binary as release asset" env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag }} run: | - gh release upload --clobber $RELEASE_TAG nickel-x86_64-linux + gh release upload --clobber $RELEASE_TAG nickel-${{ os.matrix.architecture }}-linux diff --git a/flake.nix b/flake.nix index fd2aec59b6..6f0dec4c2e 100644 --- a/flake.nix +++ b/flake.nix @@ -172,6 +172,8 @@ snapFilter = mkFilter ".*snap$"; scmFilter = mkFilter ".*scm$"; importsFilter = mkFilter ".*/core/tests/integration/imports/imported/.*$"; # include all files that are imported in tests + + infraFilter = mkFilter ".*/infra/.*$"; in pkgs.lib.cleanSourceWith { src = pkgs.lib.cleanSource ./.; @@ -187,7 +189,9 @@ scmFilter filterCargoSources importsFilter - ]; + ] && !(builtins.any (filter: filter path type) [ + infraFilter + ]); }; # Given a rust toolchain, provide Nickel's Rust dependencies, Nickel, as @@ -466,6 +470,35 @@ ''; }; + infraShell = nickel: + let + terraform = pkgs.terraform.withPlugins (p: with p; [ + archive + aws + github + ]); + ec2-region = "eu-north-1"; + ec2-ami = (import "${nixpkgs}/nixos/modules/virtualisation/amazon-ec2-amis.nix").latest.${ec2-region}.aarch64-linux.hvm-ebs; + run-terraform = pkgs.writeShellScriptBin "run-terraform" '' + set -e + ${nickel}/bin/nickel export > main.tf.json < ]; + ec2.hvm = true; + + services.github-runner = { + enable = true; + replace = true; + name = runner-name; + package = pkgs.github-runner.override { + # nodejs v16 was marked as EOL ahead of its EOL in https://github.com/NixOS/nixpkgs/pull/229910 + # It will reach EOL only on 2023-09-11 + nodejs_16 = pkgs.nodejs_16.overrideAttrs (orig: { + meta = + orig.meta + // { + knownVulnerabilities = + lib.filter + (x: x != "This NodeJS release has reached its end of life. See https://nodejs.org/en/about/releases/.") + orig.meta.knownVulnerabilities; + }; + }); + }; + extraPackages = with pkgs; [ + gawk + nix + ]; + url = "${url}"; + tokenFile = "/run/secrets/github-runner.token"; + extraLabels = [ + "EC2" + ]; + }; + + systemd.services.github-runner-init = { + description = "Setup tasks before trying to start the GitHub runner"; + + before = [ "github-runner-$${runner-name}.service" ]; + requiredBy = [ "github-runner-$${runner-name}.service" ]; + + script = '' + #!$${pkgs.runtimeShell} -eu + umask 077 + mkdir -p /run/secrets + while ! $${pkgs.awscli2}/bin/aws sts get-caller-identity; do + sleep 10 + done + $${pkgs.awscli2}/bin/aws ssm get-parameter --name '${token}' --with-decryption --output json --query 'Parameter.Value' \ + | $${pkgs.jq}/bin/jq -rj > /run/secrets/github-runner.token + $${pkgs.awscli2}/bin/aws ssm delete-parameter --name '${token}' + ''; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; +} diff --git a/infra/github-variables.ncl b/infra/github-variables.ncl new file mode 100644 index 0000000000..937404c700 --- /dev/null +++ b/infra/github-variables.ncl @@ -0,0 +1,37 @@ +{ + region | String, + github + | { + owner | String, + repo | String, + ec2_role | String, + ec2_start | String, + ec2_stop | String, + .. + }, + config = { + resource.github_actions_variable."ec2_region" = { + repository = "%{github.repo}", + variable_name = "EC2_REGION", + value = region, + }, + + resource.github_actions_secret."ec2_role" = { + repository = "%{github.repo}", + secret_name = "EC2_ROLE", + plaintext_value = github.ec2_role, + }, + + resource.github_actions_secret."ec2_start" = { + repository = "%{github.repo}", + secret_name = "EC2_START", + plaintext_value = github.ec2_start, + }, + + resource.github_actions_secret."ec2_stop" = { + repository = "%{github.repo}", + secret_name = "EC2_STOP", + plaintext_value = github.ec2_stop, + }, + } +} diff --git a/infra/lambdas.ncl b/infra/lambdas.ncl new file mode 100644 index 0000000000..a0a22e5179 --- /dev/null +++ b/infra/lambdas.ncl @@ -0,0 +1,188 @@ +{ + naming_prefix | String, + region | String, + account-id | String, + vpc.subnet_id | String, + runner + | { + launch_template | String, + launch_template_arn | String, + role_arn | String, + instance_type | String, + instance_tag = naming_prefix, + .. + }, + lambda.invoke_policy = "${resource.aws_iam_policy.lambda_invoke.arn}", + ssm + | { + parameter-path | String, + parameter-arn | String, + }, + github = { + ec2_start = "${resource.aws_lambda_function.spot_start.function_name}", + ec2_stop = "${resource.aws_lambda_function.spot_stop.function_name}", + }, + config = { + data.archive_file.lambda = { + type = "zip", + source_dir = "${path.module}/spot_lambdas/", + output_path = "${path.module}/build/lambda.zip", + }, + + resource.aws_lambda_function.spot_start = { + function_name = "%{naming_prefix}-start", + filename = config.data.archive_file.lambda.output_path, + source_code_hash = "${data.archive_file.lambda.output_base64sha256}", + role = "${resource.aws_iam_role.lambda_execution_role.arn}", + runtime = "python3.11", + handler = "start.lambda_handler", + timeout = 120, + + environment.variables = { + LAUNCH_TEMPLATE = runner.launch_template, + SSM_PARAMETER = ssm.parameter-path, + TAG_KEY = runner.instance_tag, + } + }, + + resource.aws_lambda_function.spot_stop = { + function_name = "%{naming_prefix}-stop", + filename = config.data.archive_file.lambda.output_path, + source_code_hash = "${data.archive_file.lambda.output_base64sha256}", + role = "${resource.aws_iam_role.lambda_execution_role.arn}", + runtime = "python3.11", + handler = "stop.lambda_handler", + timeout = 120, + environment.variables = { + TAG_KEY = runner.instance_tag, + } + }, + + resource.aws_iam_role.lambda_execution_role = { + name = "%{naming_prefix}-lambda-execution-role", + assume_role_policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Principal.Service = "lambda.amazonaws.com", + Effect = "Allow", + } + ] + }, + }, + + resource.aws_iam_role_policy.lambda_ssm_parameters = { + name = "%{naming_prefix}-lambda-ssm-parameter-policy", + role = "${resource.aws_iam_role.lambda_execution_role.name}", + policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = [ + "ssm:PutParameter" + ], + Resource = ssm.parameter-arn, + Effect = "Allow", + } + ], + } + }, + + resource.aws_iam_role_policy.lambda_ec2_policy = { + name = "%{naming_prefix}-lambda-ec2-policy", + role = "${resource.aws_iam_role.lambda_execution_role.name}", + policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = [ + "ec2:DescribeInstances", + ], + Resource = "*", + Effect = "Allow", + }, + { + Action = [ + "ec2:RunInstances" + ], + Condition = { + StringEquals."ec2:LaunchTemplate" = runner.launch_template_arn, + StringEqualsIfExists."ec2:InstanceType" = runner.instance_type, + "Bool"."ec2:IsLaunchTemplateResource" = "true", + }, + Resource = "*", + Effect = "Allow", + }, + { + Action = [ + "ec2:RunInstances" + ], + Condition = { + "ForAllValues:StringNotEquals"."aws:TagKeys" = runner.instance_tag, + }, + Resource = "arn:aws:ec2:*:*:instance/*", + Effect = "Deny", + }, + { + Action = [ + "ec2:TerminateInstances" + ], + Condition = { + StringLike."aws:ResourceTag/%{runner.instance_tag}" = "*" + }, + Resource = ["arn:aws:ec2:%{region}:%{account-id}:instance/*"], + Effect = "Allow", + }, + { + Action = [ + "iam:PassRole" + ], + Resource = [runner.role_arn], + Effect = "Allow", + }, + { + Action = [ + "ec2:CreateTags" + ], + Condition = { + StringEquals."ec2:CreateAction" = "RunInstances", + StringLike."aws:RequestTag/%{runner.instance_tag}" = "*", + }, + Resource = ["arn:*:ec2:%{region}:%{account-id}:*/*"], + Effect = "Allow", + } + ], + } + }, + + resource.aws_iam_policy.lambda_invoke = { + name = "%{naming_prefix}-lambda-invoke-policy", + policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = "lambda:InvokeFunction", + Resource = [ + "${resource.aws_lambda_function.spot_start.arn}", + "${resource.aws_lambda_function.spot_stop.arn}", + ], + Effect = "Allow", + } + ], + } + }, + } +} diff --git a/infra/main.ncl b/infra/main.ncl new file mode 100644 index 0000000000..d0744f9454 --- /dev/null +++ b/infra/main.ncl @@ -0,0 +1,40 @@ +{ + naming_prefix = "tweag-nickel-release-infra", + github = { + owner = "tweag", + repo = "nickel", + }, + repo_url = "https://github.com/%{github.owner}/%{github.repo}", + runner.nix_config = "github-runner.nix", + region, + account-id = "${data.aws_caller_identity.current.account_id}", + config = { + terraform = { + required_providers = { + archive.source = "registry.terraform.io/hashicorp/archive", + aws.source = "registry.terraform.io/hashicorp/aws", + github.source = "registry.terraform.io/integrations/github", + }, + }, + provider.aws = + let region' = region in + { + region = region', + default_tags.tags = { + owner = naming_prefix, + } + }, + + provider.github = { + owner = github.owner, + }, + + data.aws_caller_identity.current = {}, + } +} +& (import "state.ncl") +& (import "lambdas.ncl") +& (import "vpc.ncl") +& (import "runner.ncl") +& (import "github-oidc.ncl") +& (import "github-variables.ncl") diff --git a/infra/runner.ncl b/infra/runner.ncl new file mode 100644 index 0000000000..2775b86d0f --- /dev/null +++ b/infra/runner.ncl @@ -0,0 +1,104 @@ +{ + naming_prefix | String, + nixos-ami | String, + vpc.subnet_id, + region, + account-id, + runner = { + role = "${resource.aws_iam_role.runner.name}", + role_arn = "${resource.aws_iam_role.runner.arn}", + instance_type = "c6g.8xlarge", + nix_config | String, + launch_template = "${resource.aws_launch_template.nixos-runner.id}", + launch_template_arn = "${resource.aws_launch_template.nixos-runner.arn}", + }, + repo_url | String, + ssm = { + parameter-path = "/%{naming_prefix}/runner-registration-token", + parameter-arn = "arn:aws:ssm:%{region}:%{account-id}:parameter%{parameter-path}" + }, + config = { + resource.aws_iam_role.runner = { + name = "%{naming_prefix}-runner-role", + assume_role_policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Principal.Service = "ec2.amazonaws.com", + Effect = "Allow", + } + ] + }, + }, + + resource.aws_iam_instance_profile.runner = { + name = "%{naming_prefix}-runner-profile", + role = runner.role, + }, + + resource.aws_iam_role_policy.runner_ssm_parameters = { + name = "%{naming_prefix}-ssm_paramters-policy", + role = runner.role, + policy = + std.serialize + 'Json + { + Version = "2012-10-17", + Statement = [ + { + Action = [ + "ssm:GetParameter", + "ssm:DeleteParameter", + ], + Resource = ssm.parameter-arn, + Effect = "Allow", + } + ], + } + }, + + resource.aws_security_group.allow_egress = { + name = "%{naming_prefix}-allow-egress", + vpc_id = "${resource.aws_vpc.runner_vpc.id}", + }, + + resource.aws_security_group_rule.allow_egress = { + type = "egress", + from_port = 0, + to_port = 0, + protocol = "-1", + cidr_blocks = ["0.0.0.0/0"], + ipv6_cidr_blocks = ["::/0"], + security_group_id = "${resource.aws_security_group.allow_egress.id}", + }, + + resource.aws_launch_template.nixos-runner = { + name = "%{naming_prefix}-nixos-runner", + block_device_mappings = [ + { + device_name = "/dev/xvda", + ebs = { + delete_on_termination = true, + volume_size = 100, + }, + } + ], + image_id = nixos-ami, + instance_initiated_shutdown_behavior = "terminate", + instance_type = runner.instance_type, + network_interfaces = [ + { + subnet_id = vpc.subnet_id, + security_groups = ["${resource.aws_security_group.allow_egress.id}"], + associate_public_ip_address = true, + } + ], + iam_instance_profile.arn = "${resource.aws_iam_instance_profile.runner.arn}", + user_data = m%"${base64encode(templatefile(%{"\""}%{runner.nix_config}", { url = %{"\""}%{repo_url}", token = %{"\""}%{ssm.parameter-path}" }))}"%, + }, + } +} diff --git a/infra/spot_lambdas/start.py b/infra/spot_lambdas/start.py new file mode 100644 index 0000000000..be29d66751 --- /dev/null +++ b/infra/spot_lambdas/start.py @@ -0,0 +1,52 @@ +import os +import boto3 + +ssm = boto3.client('ssm') +ec2 = boto3.client('ec2') + +def lambda_handler(event, context): + tag_key = os.environ.get('TAG_KEY') + launch_template_id = os.environ.get('LAUNCH_TEMPLATE') + ssm_parameter = os.environ.get('SSM_PARAMETER') + + ssm.put_parameter( + Name=ssm_parameter, + Value=event['runner_token'], + Type="SecureString", + Overwrite=True, + ) + response = ec2.run_instances( + MinCount=1, + MaxCount=1, + LaunchTemplate={ + "LaunchTemplateId": launch_template_id, + "Version": "$Latest" + }, + InstanceMarketOptions={ + "MarketType": "spot", + "SpotOptions": { + "SpotInstanceType": "one-time", + "InstanceInterruptionBehavior": "terminate", + }, + }, + TagSpecifications=[ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": tag_key, + "Value": event['ref_name'] + } + ] + } + ] + ) + + instance_id = response['Instances'][0]['InstanceId'] + + return { + 'statusCode': 200, + 'body': { + "instance_id": instance_id, + } + } \ No newline at end of file diff --git a/infra/spot_lambdas/stop.py b/infra/spot_lambdas/stop.py new file mode 100644 index 0000000000..d01b911286 --- /dev/null +++ b/infra/spot_lambdas/stop.py @@ -0,0 +1,53 @@ +import os +import boto3 + +ec2 = boto3.client('ec2') + +def has_runner_tag(instance, tag_key): + return any(tag['Key'] == tag_key for tag in instance['Tags']) + +def bad_request(reason): + return { + 'statusCode': 400, + 'body': { + 'reason': reason + } + } + +def lambda_handler(event, context): + tag_key = os.environ.get('TAG_KEY') + + if 'instance_id' not in event: + return bad_request("No instance_id provided") + + instances = ec2.describe_instances( + InstanceIds=[event['instance_id']], + Filters=[ + { + 'Name': 'instance-state-name', + 'Values': ['running'], + } + ], + ) + + if not len(instances['Reservations']) == 1: + return bad_request("Too many or no reservations") + + if not len(instances['Reservations'][0]['Instances']) == 1: + return bad_request("Too many or no instances") + + instance = instances['Reservations'][0]['Instances'][0] + + if not has_runner_tag(instance, tag_key): + return bad_request("Instance is not tagged") + + ec2.terminate_instances( + InstanceIds=[instance['InstanceId']] + ) + + return { + 'statusCode': 200, + 'body': { + "instance_id": instance['InstanceId'] + } + } \ No newline at end of file diff --git a/infra/state.ncl b/infra/state.ncl new file mode 100644 index 0000000000..8eeb80ce76 --- /dev/null +++ b/infra/state.ncl @@ -0,0 +1,41 @@ +{ + naming_prefix | String, + region | String, + state = { + bucket | String = "%{naming_prefix}-tfstate", + key | String = "%{naming_prefix}.tfstate", + dynamodb | String = "%{naming_prefix}-tfstate-lock", + }, + config = { + terraform.backend.s3 = + let region' = region in + { + bucket = state.bucket, + key = state.key, + region = region', + dynamodb_table = state.dynamodb, + }, + + resource.aws_s3_bucket.tfstate = { + bucket = state.bucket, + }, + + resource.aws_s3_bucket_versioning.tfstate-versioning = { + bucket = "${aws_s3_bucket.tfstate.id}", + versioning_configuration.status = "Enabled", + }, + + resource.aws_dynamodb_table.tfstate-lock = { + name = state.dynamodb, + read_capacity = 5, + write_capacity = 5, + hash_key = "LockID", + attribute = [ + { + name = "LockID", + type = "S" + } + ] + }, + } +} diff --git a/infra/vpc.ncl b/infra/vpc.ncl new file mode 100644 index 0000000000..baf58aedcf --- /dev/null +++ b/infra/vpc.ncl @@ -0,0 +1,49 @@ +{ + naming_prefix | String, + vpc = { + id = "${resource.aws_vpc.runner_vpc.id}", + subnet_id = "${resource.aws_subnet.runner_subnet.id}", + }, + config = { + resource.aws_vpc.runner_vpc = { + cidr_block = "10.0.0.0/16", + assign_generated_ipv6_cidr_block = true, + }, + + resource.aws_subnet.runner_subnet = { + vpc_id = vpc.id, + cidr_block = "${cidrsubnet(resource.aws_vpc.runner_vpc.cidr_block, 8, 0)}", + ipv6_cidr_block = "${cidrsubnet(resource.aws_vpc.runner_vpc.ipv6_cidr_block, 8, 0)}", + assign_ipv6_address_on_creation = true, + }, + + resource.aws_internet_gateway.runner_gw = { + vpc_id = vpc.id, + }, + + resource.aws_egress_only_internet_gateway.runner_gw6 = { + vpc_id = vpc.id, + }, + + resource.aws_route_table.runner_vpc_internet_route_table = { + vpc_id = vpc.id, + }, + + resource.aws_route.runner_vpc_default_route = { + route_table_id = "${resource.aws_route_table.runner_vpc_internet_route_table.id}", + destination_cidr_block = "0.0.0.0/0", + gateway_id = "${resource.aws_internet_gateway.runner_gw.id}", + }, + + resource.aws_route.runner_vpc_default6_route = { + route_table_id = "${resource.aws_route_table.runner_vpc_internet_route_table.id}", + destination_ipv6_cidr_block = "::/0", + egress_only_gateway_id = "${resource.aws_egress_only_internet_gateway.runner_gw6.id}", + }, + + resource.aws_route_table_association.runner_vpc_internet_route_table_association = { + subnet_id = vpc.subnet_id, + route_table_id = "${resource.aws_route_table.runner_vpc_internet_route_table.id}" + }, + } +}