This repository provisions a GitHub Enterprise Server (GHES) instance on Microsoft Azure using Terraform. The Terraform state is stored remotely in an Azure Storage Account (blob backend).
The deployment is split into two Terraform workspaces:
| Folder | Purpose |
|---|---|
azure-storage-blob-backend/ |
Bootstraps the Storage Account used to hold the remote Terraform state. Run this once, first. |
ghes/ |
Provisions the GHES VM, networking, disks, and security rules. Uses the backend created above. |
The ghes/ workspace creates:
- A resource group, virtual network, and subnet (
10.0.0.0/16/10.0.1.0/24) - A Standard SKU static public IP
- A network interface with an attached network security group (NSG)
- NSG rules for web/git traffic (
22, 25, 80, 443, 8080, 8443, 9418) and admin SSH (122) - A Linux VM running the GitHub Enterprise Server marketplace image (Gen2)
- A 200 GB premium OS disk and a separate 200 GB premium data disk
| Requirement | Version / Notes |
|---|---|
| Terraform | >= 1.9.0 |
| Azure CLI | Latest |
azurerm provider |
~> 4.0 (installed automatically by terraform init) |
| Azure subscription | Contributor rights to create resource groups, storage, networking, and VMs |
| SSH key pair | Public key supplied via the ssh_public_key variable |
GHES image / sizing: Defaults to image SKU
github-enterprise-gen2, version3.21.1, on aStandard_D8s_v4VM (8 vCPU / 32 GiB) to meet GHES 3.x minimum requirements.
Authenticate the Azure CLI and select the target subscription:
az login --tenant <TENANT_ID>
az account set --subscription <SUBSCRIPTION_ID>Export the subscription ID so the azurerm 4.x provider can pick it up (required):
# bash / zsh
export ARM_SUBSCRIPTION_ID="$(az account show --query id -o tsv)"# PowerShell
$env:ARM_SUBSCRIPTION_ID = (az account show --query id -o tsv)See the Azure CLI authentication guide for service-principal and OIDC alternatives.
This step creates the Storage Account that the GHES workspace uses for its remote state. It runs with local state.
cd azure-storage-blob-backend
terraform init
terraform plan # review the 3 resources to be created
terraform applyAfter apply, note the outputs (resource_group_name, storage_account_name, container_name). They must match the backend "azurerm" block in ghes/main.tf:
backend "azurerm" {
resource_group_name = "terraformbackend-resources"
storage_account_name = "terraformbackendstoracct"
container_name = "terraformbackend-content"
key = "terraform.tfstate"
use_azuread_auth = true
}If you change the
prefixvariable, update the backend block inghes/main.tfaccordingly.
cd ../ghes
terraform init # connects to the remote backend created in step 2
terraform validate
terraform plan
terraform applyWhen the apply completes, Terraform prints the public_ip output:
Outputs:
public_ip = "20.x.x.x"
Override any of these via -var, a *.tfvars file, or environment variables (TF_VAR_<name>):
| Variable | Default | Description |
|---|---|---|
prefix |
ghes |
Name prefix for all resources |
location |
japaneast |
Azure region |
ghes_version |
3.21.1 |
GHES marketplace image version |
image_sku |
github-enterprise-gen2 |
github-enterprise-gen2 (Gen2) or GitHub-Enterprise (Gen1) |
vm_size |
Standard_D8s_v4 |
VM size (>= 8 vCPU / 32 GiB for GHES 3.x) |
admin_username |
ghadmin |
VM admin user |
os_disk_size_gb |
200 |
Root OS disk size |
data_disk_size_gb |
200 |
GHES data disk size |
allowed_ssh_cidr |
* |
CIDR allowed to reach admin SSH (port 122) — restrict in production |
ssh_public_key |
sample key | SSH public key for VM access |
Example:
terraform apply -var="location=eastus" -var="ghes_version=3.21.1" -var="allowed_ssh_cidr=203.0.113.0/24"- Open
https://<public_ip>in a browser. - Upload your GitHub Enterprise license file and set the management console password.
- Configure the instance (hostname, TLS, authentication) and let it restart.
- Verify GitHub Actions, then register self-hosted runners against the new hostname.
To deploy via CI/CD, fork this repository and create an Azure service principal:
az ad sp create-for-rbac --name "ghes-terraform" --role Contributor \
--scopes /subscriptions/<SUBSCRIPTION_ID>Add the following repository Actions secrets (Settings -> Secrets and variables -> Actions):
| Secret name | Description |
|---|---|
ARM_CLIENT_ID |
Service principal client ID |
ARM_CLIENT_SECRET |
Service principal client secret |
ARM_SUBSCRIPTION_ID |
Subscription ID |
ARM_TENANT_ID |
Tenant ID |
Workflows:
.github/workflows/pr.yml— runsfmt,init,validate, andplanon pull requests..github/workflows/deploy.yml— runsinitandapplyon push tomain/master.
Both workflows use hashicorp/setup-terraform@v3 and actions/checkout@v4.
Tip: For improved security, replace the client-secret credentials with OIDC federated credentials.
# Detect drift
terraform plan -detailed-exitcode
# Show the current public IP
terraform output public_ip
# Tear down (destroys the GHES instance — back up first)
terraform destroy- GHES supports skipping at most ~2 feature releases per upgrade hop. To move an existing appliance to
3.21.x, upgrade sequentially (e.g.2.20 -> 2.22 -> 3.0 -> ... -> 3.21) using the appliance upgrade/hotpatch packages — you cannot jump the running appliance directly. - Re-provisioning with a new
ghes_versionon this Terraform config replaces the VM; back up first withghe-backup/ghe-restore. - Always take a backup and snapshot the data disk before any upgrade.