OpenTofu/Terraform templates for managing a Harness account, organizations, and projects using the harness/harness provider.
New to this repo? Read
harness-platform-bootstrap/README.mdbefore anything else. It walks through the one-time bootstrap that stands up the platform org, the Platform Management project, the deploy pipelines, and the service account that everything else depends on. Don't run the deployment entrypoint until bootstrap has been applied.
| Step | Path | What it does |
|---|---|---|
| 1️⃣ Start here | harness-platform-bootstrap/ |
Run once — creates the platform org/project, deployer service account, git connector, and the tofu_deploy / tofu_deploy_iacm pipelines. |
| 2️⃣ | harness-platform-deployment/ |
Day-to-day deployment of orgs/projects/resources, driven by the scope_level variable (account | organization | project). |
| — | modules/ |
Consolidated harness-resources module (+ git/cloud connectors, iacm-workspaces). Not run directly. |
| — | workspace-creation/ |
Import-or-create the Harness IACM workspace used as the Terraform state backend (when using IACM). |
- Read the bootstrap README first, then the deployment one. Each entrypoint has its own README with the variables it needs.
- Never commit secrets or account IDs. Copy
terraform.tfvars.example→terraform.tfvars(gitignored) and fill it per environment, or use the env varsHARNESS_ACCOUNT_ID,HARNESS_PLATFORM_API_KEY,HARNESS_ENDPOINT. - Providers: copy
providers.tf.example→providers.tfin each entrypoint.providers.tfis gitignored — never commit it. - Backend-agnostic: no state backend is hardcoded. Use Harness IACM, AWS S3,
GCS, or local state — wire your choice into the entrypoint's
terraform.tf. The GCS snippets left in comments are examples only, not a requirement. - Environment-generic defaults: values like org/repo names and secret refs default to examples — override them per environment, don't assume the defaults fit your account.
- One org per change: the deploy pipeline resolves a single org per push/PR; keep each config change scoped to one organization.
- Config lives in
platform-configs/organizations/<Org Name>/…— the folder name matches the org; projects are auto-discovered fromprojects/*/config.yaml. - Always
tofu planand review beforeapply. The pipelines gate apply behind an approval stage — don't bypass it.
⚠️ Legacy section. The "Overview" and "Deployment Workflow" content below references older entrypoint names (harness-platform-setup,harness-organization,harness-project) that have been consolidated into theharness-platform-bootstrap+harness-platform-deploymententrypoints listed above. It is being revised — trust the Start Here section and the per-entrypoint READMEs over the text below.
This library provides three independent Terraform entrypoints that map to Harness's three resource scopes.
Account ──► harness-platform-setup/
Org ──► harness-organization/ (also discovers and instantiates projects)
Project ──► harness-project/ (reusable module, called directly or by org)
Each entrypoint manages:
| Resource | platform-setup | organization | project |
|---|---|---|---|
| Roles | ✓ | ✓ | ✓ |
| Resource Groups | ✓ | ✓ | ✓ |
| User Groups + Role Bindings | ✓ | ✓ | ✓ |
| Environments + Overrides | ✓ | ✓ | ✓ |
| OPA Policies + Policy Sets | ✓ | ✓ | ✓ |
| Git Connectors | ✓ | ✓ | ✓ |
| Cloud Provider Connectors (AWS) | ✓ | ✓ | ✓ |
- OpenTofu (
tofu) or Terraform (terraform) - A Harness account with an API key that has account-admin permissions
- The
harness/harnessprovider>= 0.31andhashicorp/time ~> 0.9.1(fetched automatically bytofu init)
Run this once when setting up a new Harness account. Creates account-scoped roles, resource groups, user groups, OPA policies, and environments.
terraform.tfvars minimum content:
harness_platform_account = "abc123xyz"
tags = {}Run this once per Harness organization. Creates the org, applies org-scoped resources, and automatically discovers and creates all projects under that org from platform-configs/organizations/<org-name>/projects/.
terraform.tfvars minimum content:
harness_platform_account = "abc123xyz"
organization_name = "My Org" # Must match folder name in platform-configs/organizations/Project discovery is automatic. Any folder matching
platform-configs/organizations/<org-name>/projects/*/config.yamlis instantiated as a Harness project. No variables list required.
Use this entrypoint directly when creating a single project outside of org-level discovery.
terraform.tfvars minimum content:
harness_platform_account = "abc123xyz"
organization_id = "My_Org"
project_name = "My Project"platform-configs/
└── organizations/
└── <Organization Name>/ # Folder name == organization_name variable
├── config.yaml # Org metadata (name, description, tags)
├── groups/
├── roles/
├── resource_groups/
├── environments/
├── policies/
├── policy_sets/
├── git-connectors/
├── cloud-provider-connectors/
└── projects/
└── <Project Name>/ # Folder name == project identifier
├── config.yaml # Project metadata
├── groups/
├── roles/
├── resource_groups/
├── environments/
├── policies/
├── policy_sets/
├── git-connectors/
└── cloud-provider-connectors/
For harness-organization and harness-project, every resource category is merged from two sources:
- Global defaults —
<entrypoint>/<template_folder>/<category>/where<template_folder>defaults totemplatesand is configurable via thedefault_org_template/default_project_templatevariable orconfig.yamlfield. - Org or project overrides —
platform-configs/organizations/<org>/<category>/(or.../projects/<project>/<category>/)
When a file exists in both locations with the same relative path, the org/project version wins. Files only in the global defaults are included unchanged. This means you can ship sensible defaults in templates/ and only place YAML files in platform-configs/ for what you want to change or add.
For harness-platform-setup, there is no merge — it reads exclusively from harness-platform-setup/templates/.
The Harness resource identifier is derived from the filename (without .yaml), with spaces and dashes replaced by underscores. You can override this by setting identifier: explicitly in the YAML.
name: "My Organization" # Display name
description: "..." # Optional
identifier: "my_org" # Optional — overrides filename-derived identifier
default_org_template: "templates-two" # Optional — org-level template folder to use instead of the default "templates"
default_project_template: "templates-two" # Optional — project-level template folder to use for all projects under this org,
# or for a specific project when set in that project's config.yaml.
# Priority: project config.yaml > org config.yaml > variable default
tags:
owner: platform-team
env: prodscope_level: account # Group already exst in th eacocunt level so it will be Inherited to the relevant scope
name: "Platform Admins"
tags:
purpose: admins
# SSO linking (all optional)
linked_sso_type: "SAML"
linked_sso_id: "saml-provider-id"
sso_group_id: "saml-group-id"
sso_group_name: "Platform Admins"
# Role bindings granted to this group
role_bindings:
- role: _organization_admin # Built-in or custom role identifier
resource_group: _all_resources_including_child_scopes
- role: my_custom_role
resource_group: my_resource_groupExisting groups (pre-created outside Terraform, e.g. SSO-synced): prefix the filename with
_(e.g._existing_admins.yaml). Terraform will look up the group by identifier and assign role bindings without trying to create it. or by adding the value
name: "Developer"
permissions:
- core_pipeline_view
- core_pipeline_execute
- core_environment_view
- core_service_view
- core_connector_view
- core_secret_viewValid permissions are validated at plan time against the live Harness API. Invalid permissions cause a
preconditionfailure with a clear error listing the invalid entries.
name: "Production Environments"
description: "Access to production environments only"
color: "#0063F7"
included_scopes:
- filter: INCLUDING_CHILD_SCOPES
account_id: "<account_id>"
resource_filters:
- resource_type: ENVIRONMENT
filters:
- name: type
values:
- Production
- resource_type: PIPELINE
filters: [] # Empty = all pipelinesname: "Production"
type: Production # PreProduction | Production
description: "..."
tags:
env: prodWith environment override YAML (V2 overrides):
name: "Production"
type: Production
yaml:
environmentRef: production
variables:
- name: region
value: eu-west-1
type: StringPolicy files use the .rego extension. The filename (without .rego) becomes the identifier and display name.
package pipeline
deny[msg] {
input.pipeline.stages[_].stage.spec.execution.steps[_].step.type == "ShellScript"
msg := "ShellScript steps are not allowed"
}name: "Mandatory Pipeline Policies"
action: onrun # onrun | onsave | onstep | afterTerraformPlan
type: pipeline # pipeline | template | inputset | ...
policies:
- identifier: enforce_template_version_schema
severity: error # error | warning
- identifier: my_custom_policy
severity: warningGitHub connector (GitHub App auth):
name: "Platform GitHub Connector"
type: Github # Github | Git
description: "..."
tags:
provider: github
connector_url: https://github.com/my-org
connection_type: Account # Account | Repo
validation_repo: my-validation-repo
execute_on_delegate: false
http_credentials:
github_app:
application_id: "123456"
installation_id: "789012"
private_key_ref: account.my_github_app_key
api_auth:
github_app_api: trueGeneric Git connector (username/token):
name: "Internal Git Connector"
type: Git
connector_url: https://git.internal.example.com/my-org
connection_type: Account
validation_repo: my-repo
execute_on_delegate: true
delegate_selectors:
- internal-delegate
http_credentials:
username: git-user
password_ref: account.git_tokenThe type field selects the provider. Currently only aws is supported.
OIDC (recommended for EKS/IRSA):
type: aws
name: "AWS OIDC Connector"
execute_on_delegate: false
oidc_authentication:
iam_role_arn: "arn:aws:iam::123456789012:role/HarnessOIDCRole"
region: "eu-west-1"
delegate_selectors:
- my-delegateIRSA:
type: aws
name: "AWS IRSA Connector"
execute_on_delegate: true
irsa_authentication:
iam_role_arn: "arn:aws:iam::123456789012:role/HarnessIRSARole"
delegate_selectors:
- my-delegateManual (access key/secret):
type: aws
name: "AWS Manual Connector"
execute_on_delegate: true
manual_authentication:
access_key_ref: account.aws_access_key
secret_key_ref: account.aws_secret_key
delegate_selectors:
- my-delegate
region: "eu-west-1"Inherit from delegate:
type: aws
name: "AWS Delegate Connector"
execute_on_delegate: true
inherit_from_delegate:
delegate_selectors:
- my-delegateWith optional cross-account access (any auth type):
cross_account_access:
role_arn: "arn:aws:iam::999999999999:role/HarnessCrossAccount"
external_id: "harness-external-id"Supports Github and Git connector types. The connector_type variable controls which Harness resource is created. All credential fields are optional — pass only what your YAML contains.
| Variable | Required | Description |
|---|---|---|
connector_type |
✓ | Github or Git |
connector_name |
✓ | Display name |
connector_identifier |
✓ | Harness identifier |
git_connector_url |
✓ | Repository or account URL |
connection_type |
✓ | Account or Repo |
git_connector_http_credentials |
— | HTTP auth block (username/password or GitHub App) |
git_connector_ssh_credentials |
— | SSH auth block |
git_connector_api_auth |
— | API auth (required for Git Experience) |
org_id, project_id |
— | Scope; omit for account-level |
AWS only. Exactly one authentication mode must be provided; the module enforces this with a precondition.
| Variable | Required | Description |
|---|---|---|
connector_name |
✓ | Display name |
connector_identifier |
✓ | Harness identifier |
aws_connector_oidc_authentication |
— | OIDC auth object |
aws_connector_irsa_authentication |
— | IRSA auth object |
aws_connector_manual_authentication |
— | Access key / secret key object |
aws_connector_inherit_from_delegate |
— | Delegate inherit object |
aws_connector_cross_account_access |
— | Cross-account role ARN |
org_id, project_id |
— | Scope; omit for account-level |
- Create the folder:
platform-configs/organizations/<Org Name>/ - Add
config.yaml:name: "Org Name" description: "..." tags: owner: platform-team
- Add any resource YAML files in the relevant category subdirectories (groups, roles, etc.).
- Run
harness-organization/withorganization_name = "Org Name".
- Create the folder:
platform-configs/organizations/<Org Name>/projects/<Project Name>/ - Add
config.yaml:name: "Project Name" description: "..."
- Add project-specific YAML overrides as needed. Any category you leave empty will inherit from
harness-project/templates/. - Re-run
harness-organization/— the project is discovered and created automatically.
| Scope | Principle |
|---|---|
| Account | iacm-admin role, account-wide admin groups, OPA governance |
| Organization | Org-admin groups, shared resource groups, org-scoped connectors |
| Project | Developer/release-manager groups, pipeline-scoped environments, IACM workspace permissions |
iacm_workspace_*permissions are project-scoped only (not valid at account or org scope).- Environment access is granted through Resource Groups (filter by env type), not role permissions directly.
- Harness built-in identifiers (e.g.
_organization_admin,_all_resources_including_child_scopes) are prefixed with_and referenced directly inrole_bindings.
This repo uses local state only. Each entrypoint maintains its own terraform.tfstate file within its directory.
- Do not configure a remote backend unless you intend to migrate.
- Run one entrypoint at a time — they are independent.
- Commit
terraform.tfstatefiles only if your team has agreed on a shared local-state workflow; otherwise keep them local and gitignored.
- Provider:
harness/harness >= 0.31,hashicorp/time ~> 0.9.1 harness_platform_role_assignments.identifiermust be unique per scope; the library derives it as<group_identifier>_<role_identifier>.harness_platform_usergroupSSO fields are in thelifecycle { ignore_changes }block — SSO linking managed outside Terraform will not be overwritten on subsequent runs.harness_platform_permissionsis fetched live at plan time to validate role permissions against the actual account.
MIT. See LICENSE.