From b87d6cb71cdcd879367e5fd6966ea0ecf7f208d4 Mon Sep 17 00:00:00 2001 From: Carlos Bustillo <20931458+carlosbustillordguez@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:28:40 +0200 Subject: [PATCH] feat: initial commit --- .commitlintrc.js | 1 + .editorconfig | 18 + .github/CONTRIBUTING.md | 14 + .../bug_report_local_install.md | 97 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 29 ++ .github/PULL_REQUEST_TEMPLATE.md | 32 ++ .github/workflows/pr-title-check.yml | 80 ++++ .github/workflows/pre-commit.yml | 50 +++ .github/workflows/release.yml | 40 ++ .github/workflows/terraform-docs.yml | 22 ++ .gitignore | 60 +++ .markdownlint.json | 11 + .pre-commit-config.yaml | 63 ++++ .terraform-docs.yml | 47 +++ .tflint.hcl | 33 ++ LICENSE | 21 ++ README.md | 192 ++++++++++ ...ub-self-hosted-runner-aws-codebuild.drawio | 40 ++ ...ithub-self-hosted-runner-aws-codebuild.png | Bin 0 -> 61434 bytes main.tf | 357 ++++++++++++++++++ outputs.tf | 9 + variables.tf | 99 +++++ versions.tf | 10 + 23 files changed, 1325 insertions(+) create mode 100644 .commitlintrc.js create mode 100644 .editorconfig create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report_local_install.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/pr-title-check.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/terraform-docs.yml create mode 100644 .gitignore create mode 100644 .markdownlint.json create mode 100644 .pre-commit-config.yaml create mode 100644 .terraform-docs.yml create mode 100644 .tflint.hcl create mode 100644 LICENSE create mode 100644 README.md create mode 100644 images/github-self-hosted-runner-aws-codebuild.drawio create mode 100644 images/github-self-hosted-runner-aws-codebuild.png create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..28fe5c5 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..242b943 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Dockerfile] +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..46c8ba7 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Notes for Contributors + +Please follow [Github Flow](https://guides.github.com/introduction/flow/index.html). + +This is a rough outline of the workflow: + +- Create a topic branch from where you want to base your work. This is usually master/main. +- Make commits of logical units (and add tests!). +- Make sure your commit messages follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). +- Push your changes to a topic branch in your fork of the repository. +- Submit a pull request. The pull request's title must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). +- Rebase and force-push to your fork's branch as necessary. + +Thanks for you contributions! diff --git a/.github/ISSUE_TEMPLATE/bug_report_local_install.md b/.github/ISSUE_TEMPLATE/bug_report_local_install.md new file mode 100644 index 0000000..02d24ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_local_install.md @@ -0,0 +1,97 @@ +--- +name: Local installation bug report +about: Create a bug report +labels: +- bug +- area/local_installation +--- + + + + +### Describe the bug + + + +### How can we reproduce it? + + + +### Environment information + +* OS: + + +* `uname -a` and/or `systeminfo | Select-String "^OS"` output: + +```bash +INSERT_OUTPUT_HERE +``` + + + +* Tools availability and versions: + + + +```bash +INSERT_TOOLS_VERSIONS_HERE +``` + +* `.tf` files: + + +
file content + +```bash +INSERT_FILE_CONTENT_HERE +``` + +
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2c897c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea for this project +labels: +- feature +--- + + + + +### What problem are you facing? + + + +### How could this Terraform module help solve your problem? + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..574d996 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ + + + +Put an `x` into the box if that apply: + +- [ ] This PR introduces breaking change. +- [ ] This PR fixes a bug. +- [ ] This PR adds new functionality. +- [ ] This PR enhances existing functionality. + +### Description of your changes + + + + + +### How can we test changes + + diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000..a703af5 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,80 @@ +name: Validate PR title + +on: + pull_request: + types: + - opened + - edited + - synchronize + branches: + - main + - master + +permissions: + pull-requests: write + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + # Please look up the latest version from + # https://github.com/amannn/action-semantic-pull-request/releases + - uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Configure which types are allowed. + # Default: https://github.com/commitizen/conventional-commit-types + types: | + fix + feat + chore + refactor + docs + style + test + perf + ci + build + revert + # These are regex patterns auto-wrapped in `^ $`. + # scopes: | + # no-ref + # Configure that a scope must always be provided. + requireScope: false + # Configure additional validation for the subject based on a regex. + # This example ensures the subject doesn't start with an uppercase character. + subjectPattern: ^(?![A-Z]).+$ + # If `subjectPattern` is configured, you can use this property to override + # the default error message that is shown when the pattern doesn't match. + # The variables `subject` and `title` can be used within the message. + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + doesn't start with an uppercase character. + + - uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! 👋🏼 + + We require pull request titles to follow the approved [PR title specification](https://gist.github.com/carlosbustillordguez/c7c45a1cfbb50c30a488ceebda6ef18f) and it looks like your proposed title needs to be adjusted. + + Details: + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + # Delete a previous comment when the issue has been resolved + - uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ steps.lint_pr_title.outputs.error_message == null }} + with: + header: pr-title-lint-error + delete: true diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..164e7b2 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,50 @@ +name: Common Issues Check + +on: + pull_request: + branches: + - main + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + + - name: Get changed files + id: file_changes + run: | + export DIFF=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }}) + echo "Diff between ${{ github.base_ref }} and ${{ github.sha }}" + echo "files=$( echo "$DIFF" | xargs echo )" >> $GITHUB_OUTPUT + + # Need to success pre-commit fix push + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Execute pre-commit + uses: cloudposse/github-action-pre-commit@v4.0.0 + env: + # We don't want to enforce conventional commits at the commit message level, + # it's enforced at the PR title level with pr-title-check.yml + SKIP: commitlint,terraform_docs_without_aggregate_type_defaults + with: + # Push back fixes to the pull request branch due to the 'token' argument + token: ${{ secrets.GITHUB_TOKEN }} + extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} + git_user_name: pre-commit + git_user_email: 20931458+carlosbustillordguez@users.noreply.github.com + git_commit_message: "pre-commit fixes" + - name: Execute pre-commit check that have no auto-fixes + if: always() + uses: cloudposse/github-action-pre-commit@v4.0.0 + env: + SKIP: check-added-large-files,check-merge-conflict,check-vcs-permalinks,end-of-file-fixer,trailing-whitespace,check-yaml,check-merge-conflict,check-executables-have-shebangs,check-case-conflict,mixed-line-ending,detect-aws-credentials,detect-private-key,commitlint,terraform_docs_without_aggregate_type_defaults + with: + extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f560e3c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - '**/*.tf' + # Ignore paths + - '!tests/**' +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Release + uses: cycjimmy/semantic-release-action@v4 + id: semantic + with: + semantic_version: 18.0.0 + extra_plugins: | + @semantic-release/changelog@6.0.0 + @semantic-release/git@10.0.0 + env: + GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} + + - name: Release Summary + if: steps.semantic.outputs.new_release_published == 'true' + shell: bash + run: | + echo '### Published release ${{ steps.semantic.outputs.new_release_version }} :rocket:' >> $GITHUB_STEP_SUMMARY + echo 'Release Notes:' >> $GITHUB_STEP_SUMMARY + echo '${{ steps.semantic.outputs.new_release_notes }}' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/terraform-docs.yml b/.github/workflows/terraform-docs.yml new file mode 100644 index 0000000..4fc2396 --- /dev/null +++ b/.github/workflows/terraform-docs.yml @@ -0,0 +1,22 @@ +name: terraform-docs + +on: + pull_request: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Render terraform docs and push changes back to PR + uses: terraform-docs/gh-actions@v1.0.0 + with: + working-dir: '.' + config-file: .terraform-docs.yml + # Commit and push the changes + git-push: "true" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ade917c --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# +*.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Ignore Dependency Lock File because this is a distributable module +**/.terraform.lock.hcl + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +!.vscode/*.code-snippets + +# Other ignores directories and files +**/.ssh/* +module-test/ diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..375ee10 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,11 @@ +{ + "line-length": false, + "no-inline-html": { + "allowed_elements": [ + "a", + "br", + "pre", + "img" + ] + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8a2fa43 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +# Uncomment if commitlint is used +default_stages: [commit] + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + # Git style + - id: check-added-large-files + - id: check-merge-conflict + - id: check-vcs-permalinks + - id: forbid-new-submodules + # - id: no-commit-to-branch + + # Common errors + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: check-yaml + - id: check-json + - id: check-executables-have-shebangs + + # Cross platform + - id: check-case-conflict + - id: mixed-line-ending + args: [--fix=lf] + + # Security + - id: detect-private-key + +- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.10.0 + hooks: + # Checks if your commit messages meet the conventional commit format + - id: commitlint + stages: [commit-msg] + additional_dependencies: ['@commitlint/config-conventional'] + +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.92.1 + hooks: + - id: terraform_validate + - id: terraform_fmt + args: + - --args=-no-color + - --args=-diff + - --args=-write=false + # Security scanner for your Terraform code. + - id: terraform_trivy + args: + - --args=--skip-dirs="**/.terraform" + # Uncomment to exclude download modules from the scan + # - --args=--tf-exclude-downloaded-modules + # A Pluggable Terraform Linter + - id: terraform_tflint + args: + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + - --args=--module + # Inserts input and output documentation into README.md (using terraform-docs). + - id: terraform_docs_without_aggregate_type_defaults + args: + - --hook-config=--path-to-file=README.md + - --args=--config=.terraform-docs.yml diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..1231b65 --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,47 @@ +formatter: "markdown table" + +version: "" + +header-from: main.tf +footer-from: "" + +recursive: + enabled: false + path: modules + +sections: + hide: [] + show: [] + +content: "" + +output: + file: "README.md" + mode: inject + template: |- + + {{ .Content }} + + +output-values: + enabled: false + from: "" + +sort: + enabled: true + by: name + +settings: + anchor: true + color: true + default: true + description: false + escape: true + hide-empty: false + html: true + indent: 2 + lockfile: true + read-comments: true + required: true + sensitive: true + type: true diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..f2032b2 --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,33 @@ +# Load the aws plugin, many rules are loaded by default: +# https://github.com/terraform-linters/tflint-ruleset-aws +plugin "aws" { + enabled = true + version = "0.21.1" + source = "github.com/terraform-linters/tflint-ruleset-aws" +} + +# Default rules: +# https://github.com/terraform-linters/tflint/tree/master/docs/rules +rule "terraform_comment_syntax" { + enabled = true +} + +rule "terraform_deprecated_index" { + enabled = true +} + +rule "terraform_documented_outputs" { + enabled = true +} + +rule "terraform_documented_variables" { + enabled = true +} + +rule "terraform_naming_convention" { + enabled = true +} + +rule "terraform_unused_required_providers" { + enabled = true +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd88a82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2023 Carlos Bustillo + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b987bb7 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# Self-hosted GitHub Actions runners in AWS CodeBuild + +Terraform module to set up [self-hosted GitHub Actions runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners) in [AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html) containers to process [GitHub Actions](https://docs.github.com/en/actions/writing-workflows/quickstart) workflow jobs. + +This module is mainly based in [Use self-hosted GitHub Actions runners in AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner.html) and has been configured to support a [webhook at the organization level](https://docs.aws.amazon.com/codebuild/latest/userguide/github-global-organization-webhook-setup.html). + +To use GitHub Actions self-hosted runners in CodeBuild, [update your GitHub Actions workflow YAML file in GitHub](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners.html#sample-github-action-runners-update-yaml). + +**Table of Contents:** + +- [Self-hosted GitHub Actions runners in AWS CodeBuild](#self-hosted-github-actions-runners-in-aws-codebuild) + - [Solution Overview](#solution-overview) + - [Important Notes](#important-notes) + - [GitHub Integration](#github-integration) + - [Fined-grained Personal Access Token Permissions](#fined-grained-personal-access-token-permissions) + - [Configuring the CodeBuild Environment](#configuring-the-codebuild-environment) + - [VPC Configuration](#vpc-configuration) + - [Usage](#usage) + - [Requirements](#requirements) + - [Providers](#providers) + - [Modules](#modules) + - [Resources](#resources) + - [Inputs](#inputs) + - [Outputs](#outputs) + +## Solution Overview + +A CodeBuild project is configured to set up self-hosted GitHub Actions runners in CodeBuild containers to process GitHub Actions workflow jobs. This is done by setting up a webhook using a CodeBuild project, and updating the GitHub Actions workflow YAML to use self-hosted runners hosted on CodeBuild machines. + +Self-hosted GitHub Actions runners in AWS CodeBuild + +The GitHub self-hosted runners will be created as an ephemeral runner in the repository that made the request event `WORKFLOW_JOB_QUEUED` and the `runs-on` setting in your GitHub workflow must set [according the expected format by AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners.html#sample-github-action-runners-update-yaml), otherwise the request won't be processed. + +The high-level steps to configure a CodeBuild project to run GitHub Actions jobs are as follows: + +1. Create a personal access token to connect the CodeBuild project to GitHub. +2. Create a CodeBuild project with a webhook and set up the webhook with the `WORKFLOW_JOB_QUEUED` event filter. +3. [Update your GitHub Actions workflow YAML in GitHub to configure your build environment](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners.html#sample-github-action-runners-update-yaml). + +## Important Notes + +- By default CodeBuild logs are stored in AWS CloudWatch in the `/aws/codebuild/` log group. To change this, set the `input_codebuild_logs_config` input variable. +- All resources with tags support, will be tagged with the following tags as default: + - `Terraform`: indicates the resources is managed by Terraform. Value `true`. + - `TerraformWorkspace`: indicates the current [Terraform's workspace](https://www.terraform.io/cli/workspaces). If no workspace is used, the value is `default`. + + Additional tags can be defined by setting the `tags` input variable, e.g.: + + ```hcl + tags = { + Project = "MyProject" + TerraformModule = "MyModule" + } + ``` + +### GitHub Integration + +When working with GitHub source CodeBuild webhooks, the CodeBuild service will automatically create (on `aws_codebuild_webhook` resource creation) and delete (on `aws_codebuild_webhook` resource deletion) the GitHub repository webhook using its granted OAuth permissions. + +The AWS account that Terraform uses to create CodeBuild webhook must have authorized CodeBuild to access GitHub's OAuth API in each applicable region. ***This is a manual step that must be done before executing this Terraform module***. If OAuth is not configured, AWS will return an error similar to `ResourceNotFoundException: Could not find access token for server type github`. + +OAuth can be configured by setting the default source credential for CodeBuild in the working region, e.g.: `https://eu-west-1.console.aws.amazon.com/codesuite/codebuild/sourceCredentials/default?provider=github®ion=eu-west-1`. Three options are available: + +- **GitHub App**: Connect project to GitHub using an AWS managed GitHub App. +- **Personal access token**: Connect project to GitHub using a personal access token. +- **OAuth App**: Connect project to GitHub using an OAuth App. + +#### Fined-grained Personal Access Token Permissions + +Create a fine-grained personal access token with the following permissions: + +- **Repository permissions**: + - `Contents`: Read-only. Grants access to private repositories. + - `Commit statuses`: Read and write. Grants permission to create commit statuses. + - `Webhooks`: Read and write. Grants permission to manage webhooks. + - `Administration`: Read and write. Grants administration permissions on repositories. + +- **Organization permission**: + - `Webhooks`: Read and write. Grants permission to manage webhooks for an organization. + +Then, save the token value in [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). The new secret must have the following tags: + +| Key | Value | +|------|---------| +| `codebuild:source` | | +| `codebuild:source:provider`| `github` | +| `codebuild:source:type` | `personal_access_token` | + +Finally, set the name of the secret as value for the `pat_aws_secret_name` input variable. + +### Configuring the CodeBuild Environment + +The CodeBuild environment is defined by the `codebuild_project_environment` input variable. Default values are suitable for most of the cases. + +- Valid values for `codebuild_project_environment.compute_type` can be found in the [official documentation for `aws_codebuild_project` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project#compute_type). +- Valid values for `codebuild_project_environment.image` can be found in [Compute images supported with the CodeBuild-hosted GitHub Actions runner](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners-update-yaml.images.html). For the source code see [AWS CodeBuild curated Docker images](https://github.com/aws/aws-codebuild-docker-images/). +- Valid values for `codebuild_project_environment.type` can be found in the [official documentation for `aws_codebuild_project` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project#type) + +### VPC Configuration + +This module is designed to create a CodeBuild project inside a VPC and provides access to private resources. The specified VPC must meet the following requirements: + +- The subnet used by the CodeBuild project must provide outbound access. Private subnets with a NAT Gateway are highly recommended. +- The attached Security Group to the CodeBuild project must provide only outbound access to Internet. + +For a full checklist see [best practices for VPCs](https://docs.aws.amazon.com/codebuild/latest/userguide/vpc-support.html#best-practices-for-vpcs). + +## Usage + +```hcl +module "codebuild_github_runners" { + source = "../" + + codebuild_project_name = "ghSelfhostedRunners" + codebuild_project_description = "Uses CodeBuild to run GitHub Self-hosted Runners." + github_organization_name = "myorg" + pat_aws_secret_name = "aws-codebuild-github-runners" + aws_region = "eu-west-1" + + codebuild_vpc_config = { + security_group_ids = ["sg-abc"] + subnets = ["subnet-abc", "subnet-def", "subnet-ghi"] + vpc_id = "vpc-abc" + } + + tags = { + Project = "CodeBuild GitHub Runners" + TerraformModule = "codebuild_github_runners" + Environment = "Integrations" + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >=1.5 | +| [aws](#requirement\_aws) | >=5.63.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 5.63.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_codebuild_project.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | +| [aws_codebuild_webhook.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_webhook) | resource | +| [aws_iam_policy.base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.codebuild_cw_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.codebuild_s3_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.codebuild_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.secret_manager](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.codebuild](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.codebuild_cw_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.codebuild_s3_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.codebuild_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.secret_manager](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_secretsmanager_secret.pat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_region](#input\_aws\_region) | The AWS region to provision the resources. | `string` | n/a | yes | +| [codebuild\_logs\_config](#input\_codebuild\_logs\_config) | CodeBuild logs configuration.
`cloudwatch_logs`: (Optional) Configuration to store logs on AWS CloudWatch.
- `status`: Current status of logs in CloudWatch Logs for a build project. Valid values: `ENABLED`, `DISABLED`.
- `group_name`: Group name of the logs in CloudWatch Logs.
- `stream_name`: Prefix of the log stream name of the logs in CloudWatch Logs.
`s3_logs`: (Optional) Configuration to store logs on AWS S3.
- `status`: Current status of logs in S3 for a build project. Valid values: `ENABLED`, `DISABLED`.
- `location`: Name of the S3 bucket and the path prefix for S3 logs. Must be set if status is ENABLED, otherwise it must be empty.
- `encryption_disabled`: Whether to disable encrypting S3 logs. |
object({
cloudwatch_logs = optional(object({
status = string
group_name = string
stream_name = string
}))
s3_logs = optional(object({
status = string
location = string
encryption_disabled = bool
}))
})
| `{}` | no | +| [codebuild\_project\_description](#input\_codebuild\_project\_description) | The description of the CodeBuild build project. | `string` | n/a | yes | +| [codebuild\_project\_environment](#input\_codebuild\_project\_environment) | The CodeBuild environment configuration.
`compute_type`: (Required) Information about the compute resources the build project will use.
`image`: (Required) Docker image to use for this build project.
Valid values include Docker images provided by CodeBuild, DockerHub images and full
Docker repository URIs such as those for ECR.
`type`: (Required) Type of build environment to use for related builds.
`image_pull_credentials_type`: (Optional) Type of credentials AWS CodeBuild uses to pull images in your build.
`privileged_mode`: (Optional) Whether to enable running the Docker daemon inside a Docker container. |
object({
compute_type = string
image = string
type = string
image_pull_credentials_type = optional(string, "CODEBUILD")
privileged_mode = optional(bool, true)
})
|
{
"compute_type": "BUILD_GENERAL1_SMALL",
"image": "aws/codebuild/standard:7.0",
"image_pull_credentials_type": "CODEBUILD",
"privileged_mode": true,
"type": "LINUX_CONTAINER"
}
| no | +| [codebuild\_project\_name](#input\_codebuild\_project\_name) | The name of the CodeBuild build project. | `string` | n/a | yes | +| [codebuild\_vpc\_config](#input\_codebuild\_vpc\_config) | The VPC configuration to provision the CodeBuild Runners.
`security_group_ids`: (Required) Security group IDs to assign to running builds.
`subnets`: (Required) Subnet IDs within which to run builds.
`vpc_id`: (Required) ID of the VPC within which to run builds. |
object({
security_group_ids = list(string)
subnets = list(string)
vpc_id = string
})
| n/a | yes | +| [github\_organization\_name](#input\_github\_organization\_name) | The name of the GitHub organization to add the webhook for AWS CodeBuild Runners. | `string` | n/a | yes | +| [pat\_aws\_secret\_name](#input\_pat\_aws\_secret\_name) | The name of the AWS Secret Manager secret with the personal access token with access to GitHub. | `string` | `""` | no | +| [tags](#input\_tags) | Tags added to all supported resources. | `map(any)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [codebuild\_webhook\_payload\_url](#output\_codebuild\_webhook\_payload\_url) | The CodeBuild endpoint where webhook events are sent. | +| [codebuild\_webhook\_url](#output\_codebuild\_webhook\_url) | The URL to the webhook. | + diff --git a/images/github-self-hosted-runner-aws-codebuild.drawio b/images/github-self-hosted-runner-aws-codebuild.drawio new file mode 100644 index 0000000..51f1d11 --- /dev/null +++ b/images/github-self-hosted-runner-aws-codebuild.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/github-self-hosted-runner-aws-codebuild.png b/images/github-self-hosted-runner-aws-codebuild.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8b5ad81fc1364742311ecda4cdfbf26329dc80 GIT binary patch literal 61434 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,FE-IGQ=drw%LyXGeRAV*W1g27UD8!34)<|{!! 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�%F+w$ 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 Y5#N6gFZVu@fPjB$%34Zg3f7PQ5A;boq5uE@ literal 0 HcmV?d00001 diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..b321692 --- /dev/null +++ b/main.tf @@ -0,0 +1,357 @@ +data "aws_caller_identity" "current" {} + +data "aws_secretsmanager_secret" "pat" { + count = var.pat_aws_secret_name != "" ? 1 : 0 + + name = var.pat_aws_secret_name +} + +################################################################################ +# Local Values +################################################################################ + +locals { + # A list of strings with the subnets ARNs + codebuild_subnets_arn = formatlist( + "arn:aws:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/%s", + var.codebuild_vpc_config.subnets) + + # Fixed tags + fixed_tags = { + Terraform = true + TerraformWorkspace = terraform.workspace + } + + common_tags = merge(local.fixed_tags, var.tags) +} + + +################################################################################ +# IAM Resources +################################################################################ + +## CodeBuild Base Policy + +resource "aws_iam_policy" "base" { + name = "CodeBuildBasePolicy-${var.codebuild_project_name}-${var.aws_region}" + description = "CodeBuild Base Policy" + path = "/" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Resource" : [ + "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${var.codebuild_project_name}", + "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${var.codebuild_project_name}:*" + ], + "Action" : [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + }, + { + "Effect" : "Allow", + "Resource" : [ + "arn:aws:s3:::codepipeline-${var.aws_region}-*", + ], + "Action" : [ + "s3:PutObject", + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketAcl", + "s3:GetBucketLocation" + ] + }, + { + "Effect" : "Allow", + "Resource" : [ + "arn:aws:codebuild:${var.aws_region}:${data.aws_caller_identity.current.account_id}:report-group/${var.codebuild_project_name}-*" + ], + "Action" : [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ] + } + ] + }) + + tags = local.common_tags +} + +## CodeBuild CloudWatch Logs Policy +resource "aws_iam_policy" "codebuild_cw_logs" { + count = var.codebuild_logs_config.cloudwatch_logs != null ? 1 : 0 + + name = "CodeBuildCloudWatchLogsPolicy-${var.codebuild_project_name}-${var.aws_region}" + description = "CodeBuild CloudWatch Logs Policy" + path = "/" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Resource" : [ + "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:${var.codebuild_logs_config.cloudwatch_logs.group_name}", + "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:${var.codebuild_logs_config.cloudwatch_logs.group_name}:*" + ], + "Action" : [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + } + ] + }) + + tags = local.common_tags +} + +## CodeBuild S3 Logs Policy +resource "aws_iam_policy" "codebuild_s3_logs" { + count = var.codebuild_logs_config.s3_logs != null ? 1 : 0 + + name = "CodeBuildS3LogsPolicy-${var.codebuild_project_name}-${var.aws_region}" + description = "CodeBuild S3 Logs Policy" + path = "/" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Resource" : [ + "arn:aws:s3:::${var.codebuild_logs_config.s3_logs.location}", + "arn:aws:s3:::${var.codebuild_logs_config.s3_logs.location}/*" + ], + "Action" : [ + "s3:PutObject", + "s3:GetBucketAcl", + "s3:GetBucketLocation" + ] + } + ] + }) + + tags = local.common_tags +} + +## CodeBuild Secrets Manager Source Credentials Policy +resource "aws_iam_policy" "secret_manager" { + count = var.pat_aws_secret_name != "" ? 1 : 0 + + name = "CodeBuildSecretsManagerSrcCredsPolicy-${var.codebuild_project_name}-${var.aws_region}" + description = "CodeBuild Secrets Manager Source Credentials Policy" + path = "/" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Resource" : [ + data.aws_secretsmanager_secret.pat[0].arn + ], + "Action" : [ + "secretsmanager:GetSecretValue" + ] + } + ] + }) + + tags = local.common_tags +} + +## CodeBuild VPC Policy +resource "aws_iam_policy" "codebuild_vpc" { + name = "CodeBuildVpcPolicy-${var.codebuild_project_name}-${var.aws_region}" + description = "CodeBuild VPC Policy" + path = "/" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Action" : [ + "ec2:CreateNetworkInterface", + "ec2:DescribeDhcpOptions", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeVpcs" + ], + "Resource" : "*" + }, + { + "Effect" : "Allow", + "Action" : [ + "ec2:CreateNetworkInterfacePermission" + ], + "Resource" : "arn:aws:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:network-interface/*", + "Condition" : { + "StringEquals" : { + "ec2:Subnet" : local.codebuild_subnets_arn, + "ec2:AuthorizedService" : "codebuild.amazonaws.com" + } + } + } + ] + }) + + tags = local.common_tags +} + +## IAM role to be assumed by CodeBuild +resource "aws_iam_role" "codebuild" { + name = "github-runners-codebuild-${var.codebuild_project_name}" + description = "Service role used by CodeBuild Project ${var.codebuild_project_name}" + path = "/service-role/" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "codebuild.amazonaws.com" + } + Action = "sts:AssumeRole" + }, + ] + }) + + tags = local.common_tags +} + +## IAM Policies assigment to CodeBuild Service Role +resource "aws_iam_role_policy_attachment" "base" { + role = aws_iam_role.codebuild.name + policy_arn = aws_iam_policy.base.arn +} + +resource "aws_iam_role_policy_attachment" "codebuild_cw_logs" { + count = var.codebuild_logs_config.cloudwatch_logs != null ? 1 : 0 + + role = aws_iam_role.codebuild.name + policy_arn = aws_iam_policy.codebuild_cw_logs[0].arn +} + +resource "aws_iam_role_policy_attachment" "codebuild_s3_logs" { + count = var.codebuild_logs_config.s3_logs != null ? 1 : 0 + + role = aws_iam_role.codebuild.name + policy_arn = aws_iam_policy.codebuild_s3_logs[0].arn +} + +resource "aws_iam_role_policy_attachment" "secret_manager" { + count = var.pat_aws_secret_name != "" ? 1 : 0 + + role = aws_iam_role.codebuild.name + policy_arn = aws_iam_policy.secret_manager[0].arn +} + +resource "aws_iam_role_policy_attachment" "codebuild_vpc" { + role = aws_iam_role.codebuild.name + policy_arn = aws_iam_policy.codebuild_vpc.arn +} + + +################################################################################ +## CodeBuild Project Resources +################################################################################ + +## Manages a CodeBuild webhook, which is an endpoint accepted by the CodeBuild service +## to trigger builds from source code repositories. +## The CodeBuild service will automatically create and delete GitHub organization webhook +## using its granted OAuth permissions. +resource "aws_codebuild_webhook" "this" { + project_name = aws_codebuild_project.this.name + + build_type = "BUILD" + + filter_group { + filter { + type = "EVENT" + pattern = "WORKFLOW_JOB_QUEUED" + } + } + + scope_configuration { + name = var.github_organization_name + scope = "GITHUB_ORGANIZATION" + } +} + + +## CodeBuild Project +resource "aws_codebuild_project" "this" { + name = var.codebuild_project_name + description = var.codebuild_project_description + build_timeout = 60 + queued_timeout = 480 + service_role = aws_iam_role.codebuild.arn + + source { + type = "GITHUB" + location = "CODEBUILD_DEFAULT_WEBHOOK_SOURCE_LOCATION" + git_clone_depth = 1 + + git_submodules_config { + fetch_submodules = false + } + } + + artifacts { + type = "NO_ARTIFACTS" + } + + environment { + compute_type = var.codebuild_project_environment.compute_type + image = var.codebuild_project_environment.image + type = var.codebuild_project_environment.type + image_pull_credentials_type = var.codebuild_project_environment.image_pull_credentials_type + privileged_mode = var.codebuild_project_environment.privileged_mode + } + + vpc_config { + vpc_id = var.codebuild_vpc_config.vpc_id + subnets = var.codebuild_vpc_config.subnets + security_group_ids = var.codebuild_vpc_config.security_group_ids + } + + dynamic "logs_config" { + for_each = length(var.codebuild_logs_config) > 0 ? [1] : [] + + content { + dynamic "cloudwatch_logs" { + for_each = var.codebuild_logs_config.cloudwatch_logs != null ? { key = var.codebuild_logs_config["cloudwatch_logs"] } : {} + + content { + status = lookup(cloudwatch_logs.value, "status", null) + group_name = lookup(cloudwatch_logs.value, "group_name", null) + stream_name = lookup(cloudwatch_logs.value, "stream_name", null) + } + } + + dynamic "s3_logs" { + for_each = var.codebuild_logs_config.s3_logs != null ? { key = var.codebuild_logs_config["s3_logs"] } : {} + + content { + status = lookup(s3_logs.value, "status", null) + location = lookup(s3_logs.value, "location", null) + encryption_disabled = lookup(s3_logs.value, "encryption_disabled", null) + } + } + } + } + + tags = local.common_tags +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..90d0db6 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,9 @@ +output "codebuild_webhook_url" { + description = "The URL to the webhook." + value = aws_codebuild_webhook.this.url +} + +output "codebuild_webhook_payload_url" { + description = "The CodeBuild endpoint where webhook events are sent." + value = aws_codebuild_webhook.this.payload_url +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..9f97353 --- /dev/null +++ b/variables.tf @@ -0,0 +1,99 @@ +variable "tags" { + description = "Tags added to all supported resources." + type = map(any) + default = {} +} + +variable "aws_region" { + description = "The AWS region to provision the resources." + type = string +} + +variable "codebuild_project_name" { + description = "The name of the CodeBuild build project." + type = string +} + +variable "codebuild_project_description" { + description = "The description of the CodeBuild build project." + type = string +} + +variable "codebuild_project_environment" { + description = <