├── .github └── workflows │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── ghrdocker ├── Dockerfile └── entrypoint.sh ├── ghrhelm ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── config.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml └── values.yaml ├── setup.sh └── terraform └── main.tf /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow creates a self-hosted Github Runner on AKS 2 | name: AKS Self Hosted Runner 3 | 4 | on: workflow_dispatch 5 | 6 | env: 7 | RESOURCE_GROUP_NAME: ${{ secrets.RESOURCE_GROUP_NAME }} 8 | REGION: ${{ secrets.REGION }} 9 | CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} 10 | REPO_OWNER: ${{ secrets.REPO_OWNER }} 11 | REPO_NAME: ${{ secrets.REPO_NAME }} 12 | REPO_URL: ${{ secrets.REPO_URL }} 13 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 14 | HELM_EXPERIMENTAL_OCI: 1 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - uses: azure/login@v1 24 | with: 25 | creds: ${{ secrets.AZURE_CREDENTIALS }} 26 | 27 | - name: Create storage account and container 28 | id: setup_storage 29 | run: | 30 | STORAGE_ACCOUNT_NAME=$(echo "$RESOURCE_GROUP_NAME" | tr '[:upper:]' '[:lower:]')$RANDOM 31 | CONTAINER_NAME=$(echo "${CLUSTER_NAME}" | tr '[:upper:]' '[:lower:]')tfstate 32 | az storage account create --resource-group $RESOURCE_GROUP_NAME --name $STORAGE_ACCOUNT_NAME --sku Standard_LRS --encryption-services blob 33 | ACCOUNT_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP_NAME --account-name $STORAGE_ACCOUNT_NAME --query '[0].value' -o tsv) 34 | az storage container create --name $CONTAINER_NAME --account-name $STORAGE_ACCOUNT_NAME --account-key $ACCOUNT_KEY 35 | echo "::set-output name=storage_account_name::$STORAGE_ACCOUNT_NAME" 36 | echo "::set-output name=container_name::$CONTAINER_NAME" 37 | echo "::set-output name=account_key::$ACCOUNT_KEY" 38 | 39 | # Create AKS Cluster 40 | - uses: gambtho/aks_create_action@main 41 | with: 42 | CLUSTER_NAME: ${{ env.CLUSTER_NAME }} 43 | RESOURCE_GROUP_NAME: ${{ secrets.RESOURCE_GROUP_NAME }} 44 | STORAGE_ACCOUNT_NAME: ${{ steps.setup_storage.outputs.storage_account_name }} 45 | STORAGE_CONTAINER_NAME: ${{ steps.setup_storage.outputs.container_name }} 46 | STORAGE_ACCESS_KEY: ${{ steps.setup_storage.outputs.account_key }} 47 | ARM_CLIENT_ID: ${{ secrets.CLIENT_ID }} 48 | ARM_CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} 49 | ARM_SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }} 50 | ARM_TENANT_ID: ${{ secrets.TENANT_ID }} 51 | ACTION_TYPE: create 52 | CREATE_ACR: true 53 | 54 | - name: Build and push Github Runner image 55 | run: | 56 | az configure --defaults acr=${CLUSTER_NAME} 57 | az acr build -t ghrunner:${GITHUB_SHA} ./ghrdocker 58 | 59 | - name: Create Helm chart 60 | run: | 61 | # az configure --defaults acr=${CLUSTER_NAME} 62 | # az acr login -n ${CLUSTER_NAME} 63 | cd ./ghrhelm 64 | helm package . 65 | # helm push ghr-0.0.1.tgz oci://${CLUSTER_NAME}.azurecr.io/ghrunner 66 | 67 | - name: AKS set context 68 | uses: azure/aks-set-context@v1 69 | with: 70 | creds: ${{ secrets.AZURE_CREDENTIALS }} 71 | resource-group: ${{ secrets.RESOURCE_GROUP_NAME }} 72 | cluster-name: ${{ env.CLUSTER_NAME }} 73 | 74 | - name: Deploy via Helm 75 | run: | 76 | cd ./ghrhelm 77 | # az configure --defaults acr=${CLUSTER_NAME} 78 | # helm pull oci://${CLUSTER_NAME}.azurecr.io/ghrunner/ghr --version 0.0.1 79 | helm install ghrunner ghr-0.0.1.tgz \ 80 | --set image.repository=${CLUSTER_NAME}.azurecr.io/ghrunner \ 81 | --set image.tag=${GITHUB_SHA} \ 82 | --set ghr.github_token=${GH_TOKEN} \ 83 | --set ghr.repo_name=${REPO_NAME} \ 84 | --set ghr.repo_url=${REPO_URL} \ 85 | --set ghr.repo_owner=${REPO_OWNER} \ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | 31 | **/.terraform/* 32 | *.plan 33 | notes.tf 34 | # .tfstate files 35 | *.tfstate 36 | *.tfstate.* 37 | *.zip 38 | certs 39 | # .tfvars files 40 | *.tfvars 41 | variables/ 42 | .vscode% 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | VERSION := 0.0.1 4 | 5 | all_terraform: init plan apply 6 | 7 | all_ghr: clean_helm image helm deploy 8 | 9 | .PHONY: init 10 | init: 11 | @ARM_SUBSCRIPTION_ID=$(shell az account show --query id --out tsv) 12 | @ARM_TENANT_ID=$(shell az account show --query tenantId --out tsv) 13 | @cd ./terraform && terraform init --upgrade -input=false -migrate-state \ 14 | -backend-config="resource_group_name=${RESOURCE_GROUP_NAME}" \ 15 | -backend-config="storage_account_name=${STORAGE_ACCOUNT_NAME}" \ 16 | -backend-config="key=${RESOURCE_GROUP_NAME}.tfstate" \ 17 | -backend-config="container_name=${STORAGE_CONTAINER_NAME}" 18 | 19 | .PHONY: plan 20 | plan: 21 | cd ./terraform && terraform plan \ 22 | -var="resource_group_name=$(RESOURCE_GROUP_NAME)" \ 23 | -var="cluster_name=${CLUSTER_NAME}" \ 24 | --out=tf.plan 25 | 26 | .PHONY: apply 27 | apply: 28 | cd ./terraform && \ 29 | terraform apply -refresh-only tf.plan && \ 30 | rm tf.plan && \ 31 | az aks update -n ${CLUSTER_NAME} -g ${RESOURCE_GROUP_NAME} --attach-acr ${CLUSTER_NAME} 32 | 33 | .PHONY: image 34 | image: 35 | az configure --defaults acr=${RESOURCE_GROUP_NAME} \ 36 | && az acr build -t ghrunner:${VERSION} ./ghrdocker 37 | 38 | .PHONY: helm 39 | helm: 40 | export HELM_EXPERIMENTAL_OCI=1 && \ 41 | az configure --defaults acr=${RESOURCE_GROUP_NAME} && \ 42 | az acr login -n ${RESOURCE_GROUP_NAME} && \ 43 | cd ./ghrhelm && \ 44 | helm package . && \ 45 | helm push ghr-${VERSION}.tgz oci://${RESOURCE_GROUP_NAME}.azurecr.io/ghrunner && \ 46 | az acr repository show --name ${RESOURCE_GROUP_NAME} --repository ghrunner && \ 47 | rm ghr-${VERSION}.tgz 48 | 49 | .PHONY: deploy 50 | deploy: 51 | export HELM_EXPERIMENTAL_OCI=1 && \ 52 | az configure --defaults acr=${RESOURCE_GROUP_NAME} && \ 53 | az acr login -n ${RESOURCE_GROUP_NAME} && \ 54 | az aks get-credentials --name ${CLUSTER_NAME} --resource-group ${RESOURCE_GROUP_NAME} && \ 55 | az configure --defaults acr=${RESOURCE_GROUP_NAME} && \ 56 | helm pull oci://${RESOURCE_GROUP_NAME}.azurecr.io/ghrunner/ghr --version ${VERSION} && \ 57 | helm install ghrunner ghr-${VERSION}.tgz \ 58 | --set image.repository=${RESOURCE_GROUP_NAME}.azurecr.io/ghrunner \ 59 | --set ghr.github_token=${GH_TOKEN} && \ 60 | --set ghr.repo_name=${GITHUB_REPO_NAME} && \ 61 | --set ghr.repo_url=${GITHUB_REPO_URL} && \ 62 | --set ghr.repo_owner=${GITHUB_REPO_OWNER} && \ 63 | rm ghr-${VERSION}.tgz 64 | 65 | .PHONY: clean_helm 66 | clean_helm: 67 | -helm uninstall ghrunner 68 | 69 | setup_command: 70 | echo ". ./setup.sh -c clusterName -g resourceGroupName -s subscriptionID -r region 2>&1" 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Actions - Self Hosted Agents on AKS 2 | 3 | This repo provides instructions and configuration to setup Self Hosted Agents for Github running on an AKS cluster. It was derived from this [article](https://github.blog/2020-08-04-github-actions-self-hosted-runners-on-google-cloud/) by [John Bohannon](https://github.com/imjohnbo). This project utilizes terraform and helm to provide support for a repeatable infrastructure as code approach. The process can also be orchestrated through an **Github workflow**. 4 | 5 | ## Setup 6 | 7 | Fork this repo and pull your fork to your computer 8 | 9 | Cd into the repo 10 | 11 | Ensure you have the following dependencies: 12 | - [jq](https://stedolan.github.io/jq/download/) 13 | - [azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) (logged in to a subscription where you have contributor rights) 14 | - [github-cli](https://cli.github.com/) (logged in) 15 | - [Create a github personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) -- and export the value -- export GH_TOKEN=paste_your_token_here 16 | 17 | Run the setup.sh script 18 | - Syntax: **. ./setup.sh** [-c CLUSTER_NAME] [-g RESOURCE_GROUP_NAME] [-s SUBSCRIPTION_ID] [-r REGION] (the extra dot is important) 19 | - make setup_cmd provides an example version 20 | 21 | This script does the following: 22 | - Create a service principal for use by terraform 23 | - Create a storage account to keep the terraform state 24 | - Create a resource group where your AKS cluster will be deployed 25 | - Save service principal and other provided variables in github secrets 26 | 27 | If running locally: 28 | 29 | - export the following variables with the appropriate values 30 | - GITHUB_REPO_OWNER: "your github id" 31 | - GITHUB_REPO_NAME: "your github repo name" 32 | - GITHUB_REPO_URL: "https://github.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_URL}" 33 | - make all_terraform 34 | - make all_ghr 35 | 36 | This uses the repo makefile to create your AKS cluster, create an ACR, and deploy the runner to the cluster 37 | 38 | ## Next steps 39 | 40 | - dynamically set repo owner/repo name 41 | - check for GH_TOKEN before deploying 42 | - remove helm install note 43 | - check for all other variables in makefile (gh secret get?) 44 | - add workflow / instructions 45 | - Validate setup for an organization 46 | - Multiple node pool 47 | - Cluster autoscaling 48 | - Virtual nodes? 49 | 50 | ## Contributing 51 | 52 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 53 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 54 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 55 | 56 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 57 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 58 | provided by the bot. You will only need to do this once across all repos using our CLA. 59 | 60 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 61 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 62 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 63 | 64 | ## Trademarks 65 | 66 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 67 | trademarks or logos is subject to and must follow 68 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 69 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 70 | Any use of third-party trademarks or logos are subject to those third-party's policies. 71 | 72 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 10 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 11 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 12 | 13 | ## Microsoft Support Policy 14 | 15 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 16 | -------------------------------------------------------------------------------- /ghrdocker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | RUN apt-get update && apt-get -y install curl \ 4 | iputils-ping \ 5 | apt-transport-https \ 6 | tar \ 7 | jq \ 8 | python && \ 9 | apt-get clean && apt-get autoremove 10 | 11 | ARG GH_RUNNER_VERSION="2.283.3" 12 | WORKDIR /actions-runner 13 | RUN curl -o actions.tar.gz --location "https://github.com/actions/runner/releases/download/v${GH_RUNNER_VERSION}/actions-runner-linux-x64-${GH_RUNNER_VERSION}.tar.gz" && \ 14 | tar -zxf actions.tar.gz && \ 15 | rm -f actions.tar.gz && \ 16 | ./bin/installdependencies.sh 17 | 18 | COPY entrypoint.sh . 19 | RUN chmod +x entrypoint.sh 20 | ENTRYPOINT ["/actions-runner/entrypoint.sh"] -------------------------------------------------------------------------------- /ghrdocker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eEuo pipefail 3 | 4 | ACTIONS_RUNNER_INPUT_NAME=$HOSTNAME 5 | export RUNNER_ALLOW_RUNASROOT=1 6 | 7 | TOKEN="$(curl -sS --request POST --url "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/actions/runners/registration-token" --header "authorization: Bearer ${GH_TOKEN}" --header 'content-type: application/json' | jq -r .token)" 8 | 9 | /actions-runner/config.sh --unattended --replace --work "/tmp" --url "$ACTIONS_RUNNER_INPUT_URL" --token "$TOKEN" --labels aks-runner 10 | /actions-runner/bin/runsvc.sh -------------------------------------------------------------------------------- /ghrhelm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /ghrhelm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: ghr 3 | description: A Github runner for kubernetes 4 | 5 | type: application 6 | 7 | version: 0.0.1 8 | 9 | appVersion: "0.0.1" 10 | -------------------------------------------------------------------------------- /ghrhelm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ghr.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ghr.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ghr.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ghr.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /ghrhelm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "ghr.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "ghr.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "ghr.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "ghr.labels" -}} 37 | helm.sh/chart: {{ include "ghr.chart" . }} 38 | {{ include "ghr.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "ghr.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "ghr.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "ghr.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "ghr.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /ghrhelm/templates/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: ghr-config 5 | labels: 6 | {{- include "ghr.labels" . | nindent 4 }} 7 | data: 8 | REPO_OWNER: {{ .Values.ghr.repo_owner }} 9 | REPO_NAME: {{ .Values.ghr.repo_name }} 10 | REPO_URL: {{ .Values.ghr.repo_url }} 11 | ACTIONS_RUNNER_INPUT_URL: {{ .Values.ghr.repo_url }} 12 | -------------------------------------------------------------------------------- /ghrhelm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "ghr.fullname" . }} 5 | labels: 6 | {{- include "ghr.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "ghr.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "ghr.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "ghr.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | envFrom: 37 | - configMapRef: 38 | name: ghr-config 39 | - secretRef: 40 | name: ghr-secret 41 | lifecycle: 42 | preStop: 43 | exec: 44 | command: 45 | [ 46 | "/bin/bash", 47 | "-c", 48 | 'RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token $(curl -sS --request POST --url "https://api.github.com/repos/{{ .Values.ghr.repo_owner }}/{{ .Values.repo_name }}/actions/runners/remove-token" --header "authorization: Bearer ${GH_TOKEN}" --header "content-type: application/json" | jq -r .token)', 49 | ] 50 | ports: 51 | - name: https 52 | containerPort: 443 53 | protocol: TCP 54 | - name: http 55 | containerPort: 80 56 | protocol: TCP 57 | resources: 58 | {{- toYaml .Values.resources | nindent 12 }} 59 | {{- with .Values.nodeSelector }} 60 | nodeSelector: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.affinity }} 64 | affinity: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | {{- with .Values.tolerations }} 68 | tolerations: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} -------------------------------------------------------------------------------- /ghrhelm/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "ghr.fullname" . }} 6 | labels: 7 | {{- include "ghr.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "ghr.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /ghrhelm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "ghr.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "ghr.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /ghrhelm/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: ghr-secret 5 | labels: 6 | {{- include "ghr.labels" . | nindent 4 }} 7 | type: Opaque 8 | data: 9 | GH_TOKEN: {{ default "" .Values.ghr.github_token | b64enc | quote }} 10 | -------------------------------------------------------------------------------- /ghrhelm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "ghr.fullname" . }} 5 | labels: 6 | {{- include "ghr.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: 80 11 | name: web 12 | protocol: TCP 13 | targetPort: 80 14 | - port: 8080 15 | name: web2 16 | protocol: TCP 17 | targetPort: 8080 18 | - port: 443 19 | name: secureweb 20 | protocol: TCP 21 | targetPort: 443 22 | selector: 23 | {{- include "ghr.selectorLabels" . | nindent 4 }} 24 | -------------------------------------------------------------------------------- /ghrhelm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "ghr.serviceAccountName" . }} 6 | labels: 7 | {{- include "ghr.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /ghrhelm/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "ghr.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "ghr.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "ghr.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /ghrhelm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for ghr. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: "" 9 | pullPolicy: IfNotPresent 10 | 11 | ingress: 12 | enabled: false 13 | 14 | serviceAccount: 15 | create: true 16 | annotations: {} 17 | name: "" 18 | 19 | podAnnotations: {} 20 | 21 | podSecurityContext: {} 22 | 23 | securityContext: 24 | privileged: true 25 | 26 | service: 27 | type: ClusterIP 28 | port: 80 29 | 30 | resources: 31 | requests: 32 | memory: "256Mi" 33 | cpu: "500m" 34 | limits: 35 | memory: "512Mi" 36 | cpu: "1" 37 | 38 | ghr: 39 | repo_owner: "" 40 | repo_name: "" 41 | repo_url: "" 42 | github_token: "" 43 | 44 | autoscaling: 45 | enabled: true 46 | minReplicas: 1 47 | maxReplicas: 100 48 | targetCPUUtilizationPercentage: 80 49 | targetMemoryUtilizationPercentage: 80 50 | 51 | nodeSelector: {} 52 | 53 | tolerations: [] 54 | 55 | affinity: {} 56 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage="$(basename "$0") [-h] [-c CLUSTER_NAME] [-g RESOURCE_GROUP_NAME] [-s SUBSCRIPTION_ID] [-r REGION] 4 | Creates a service principal and storage account for using terraform on Azure 5 | where: 6 | -h show this help text 7 | -c desired cluster name 8 | -g desired resource group name 9 | -s subscription id for cluster 10 | -r region for cluster" 11 | 12 | while getopts h:c:g:s:r: flag 13 | do 14 | case "${flag}" in 15 | h) echo "$usage"; exit;; 16 | c) cluster_name=${OPTARG};; 17 | g) resource_group_name=${OPTARG};; 18 | s) subscription=${OPTARG};; 19 | r) region=${OPTARG};; 20 | :) printf "missing argument for -%s\n" "$OPTARG" >&2; echo "$usage" >&2; exit 1;; 21 | \?) printf "illegal option: -%s\n" "$OPTARG" >&2; echo "$usage" >&2; exit 1;; 22 | esac 23 | done 24 | 25 | # mandatory arguments 26 | if [ ! "$cluster_name" ] || [ ! "$resource_group_name" ] || [ ! "$subscription" ] || [ ! "$region" ]; then 27 | echo "all arguments must be provided" 28 | echo "$usage" >&2; exit 1 29 | fi 30 | 31 | if ! command -v jq &> /dev/null 32 | then 33 | echo "jq could not be found, please make sure jq is installed and in your path " 34 | exit 35 | fi 36 | 37 | if ! command -v az &> /dev/null 38 | then 39 | echo "az could not be found, please make sure azure-cli is installed and in your path" 40 | exit 41 | fi 42 | 43 | if ! command -v gh &> /dev/null 44 | then 45 | echo "gh could not be found, please make sure github cli is installed and in your path" 46 | exit 47 | fi 48 | 49 | 50 | echo "Arguments provided:" 51 | echo "Cluster Name: $cluster_name"; 52 | echo "Resource Group Name: $resource_group_name"; 53 | echo "Subscription: $subscription"; 54 | echo "Region: $region"; 55 | 56 | STORAGE_ACCOUNT_NAME=$(echo "${resource_group_name}" | tr '[:upper:]' '[:lower:]')$RANDOM 57 | CONTAINER_NAME=$(echo "${cluster_name}" | tr '[:upper:]' '[:lower:]')tfstate 58 | 59 | az account set --subscription $subscription &> /dev/null 60 | # Create resource group 61 | az group create --location $region --resource-group $resource_group_name &> /dev/null 62 | 63 | #Create service principal and give it access to group 64 | SP_OUTPUT=$(az ad sp create-for-rbac --name $resource_group_name --role contributor --scopes /subscriptions/$subscription) 65 | # echo $SP_OUTPUT 66 | ARM_CLIENT_ID=$(echo $SP_OUTPUT | jq -r .appId) 67 | ARM_CLIENT_SECRET=$(echo $SP_OUTPUT | jq -r .password) 68 | ARM_TENANT_ID=$(echo $SP_OUTPUT | jq -r .tenant) 69 | 70 | # Create storage account 71 | az storage account create --resource-group $resource_group_name --name $STORAGE_ACCOUNT_NAME --sku Standard_LRS --encryption-services blob &> /dev/null 72 | 73 | # Get storage account key 74 | ACCOUNT_KEY=$(az storage account keys list --resource-group $resource_group_name --account-name $STORAGE_ACCOUNT_NAME --query '[0].value' -o tsv) 75 | 76 | # Create blob container 77 | az storage container create --name $CONTAINER_NAME --account-name $STORAGE_ACCOUNT_NAME --account-key $ACCOUNT_KEY &> /dev/null 78 | 79 | # gh secret set 80 | echo "Saving secrets to github" 81 | gh secret set "CLUSTER_NAME" -b $cluster_name 82 | gh secret set "RESOURCE_GROUP_NAME" -b $resource_group_name 83 | gh secret set "STORAGE_ACCOUNT_NAME" -b $STORAGE_ACCOUNT_NAME 84 | gh secret set "STORAGE_CONTAINER_NAME" -b $CONTAINER_NAME 85 | gh secret set "STORAGE_ACCESS_KEY" -b $ACCOUNT_KEY 86 | gh secret set "ARM_CLIENT_ID" -b $ARM_CLIENT_ID 87 | gh secret set "ARM_CLIENT_SECRET" -b $ARM_CLIENT_SECRET 88 | gh secret set "ARM_SUBSCRIPTION_ID" -b $subscription 89 | gh secret set "ARM_TENANT_ID" -b $ARM_TENANT_ID 90 | 91 | # echo "____________________________________________________________" 92 | # echo "____________________________________________________________" 93 | # echo "CLUSTER_NAME: $cluster_name"; 94 | # echo "RESOURCE_GROUP_NAME: $resource_group_name"; 95 | # echo "STORAGE_ACCOUNT_NAME: $STORAGE_ACCOUNT_NAME" 96 | # echo "STORAGE_CONTAINER_NAME: $CONTAINER_NAME" 97 | # echo "STORAGE_ACCESS_KEY: $ACCOUNT_KEY" 98 | # echo "ARM_CLIENT_ID: $ARM_CLIENT_ID" 99 | # echo "ARM_CLIENT_SECRET: $ARM_CLIENT_SECRET" 100 | # echo "ARM_SUBSCRIPTION_ID: $subscription" 101 | # echo "ARM_TENANT_ID: $ARM_TENANT_ID" 102 | 103 | # below is only needed if running locally, and not as part of a workflow 104 | export RESOURCE_GROUP_NAME=$resource_group_name 105 | export ARM_CLIENT_ID=$ARM_CLIENT_ID 106 | export ARM_CLIENT_SECRET=$ARM_CLIENT_SECRET 107 | export STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME 108 | export STORAGE_CONTAINER_NAME=$CONTAINER_NAME 109 | export ARM_ACCESS_KEY=$ACCOUNT_KEY 110 | export ARM_SUBSCRIPTION_ID=$subscription 111 | export ARM_TENANT_ID=$ARM_TENANT_ID 112 | export CLUSTER_NAME=$cluster_name -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "=2.66.0" 6 | } 7 | } 8 | backend "azurerm" {} 9 | } 10 | 11 | # Configure the Microsoft Azure Provider 12 | provider "azurerm" { 13 | features {} 14 | skip_provider_registration = true 15 | } 16 | 17 | data "azurerm_resource_group" "group" { 18 | name = var.resource_group_name 19 | } 20 | 21 | resource "azurerm_kubernetes_cluster" "cluster" { 22 | name = var.cluster_name 23 | location = data.azurerm_resource_group.group.location 24 | resource_group_name = data.azurerm_resource_group.group.name 25 | dns_prefix = var.cluster_name 26 | kubernetes_version = var.kubernetes_version 27 | 28 | default_node_pool { 29 | name = "default" 30 | node_count = var.agents_count 31 | vm_size = var.agents_size 32 | } 33 | 34 | identity { 35 | type = "SystemAssigned" 36 | } 37 | 38 | tags = { 39 | Environment = "Dev" 40 | } 41 | } 42 | 43 | resource "azurerm_container_registry" "acr" { 44 | name = var.cluster_name 45 | location = data.azurerm_resource_group.group.location 46 | resource_group_name = data.azurerm_resource_group.group.name 47 | sku = "Basic" 48 | admin_enabled = false 49 | } 50 | 51 | variable "resource_group_name" { 52 | type = string 53 | } 54 | 55 | variable "cluster_name" { 56 | type = string 57 | } 58 | 59 | variable "agents_size" { 60 | type = string 61 | default = "Standard_D2_v2" 62 | } 63 | 64 | variable "agents_count" { 65 | type = string 66 | default = "2" 67 | } 68 | 69 | variable "kubernetes_version" { 70 | type = string 71 | default = "1.21.2" 72 | } 73 | --------------------------------------------------------------------------------