Skip to content

Examples

Runnable versions of the first two examples live in the examples/ directory of the repository.

Basic Role

The smallest useful configuration: a role for one repository plus a read-only policy.

module "github_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
}

resource "aws_iam_role_policy_attachment" "read_only" {
  role       = module.github_role.github_role_name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

output "github_role_arn" {
  value = module.github_role.github_role_arn
}

Least-Privilege Deployment Role

A role that may only run Terraform against one state bucket and one lock table:

module "github_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
}

data "aws_iam_policy_document" "terraform_state" {
  statement {
    sid       = "ListStateBucket"
    actions   = ["s3:ListBucket"]
    resources = ["arn:aws:s3:::my-terraform-state"]
  }

  statement {
    sid       = "ReadWriteState"
    actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
    resources = ["arn:aws:s3:::my-terraform-state/aws-control/*"]
  }

  statement {
    sid       = "LockTable"
    actions   = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
    resources = ["arn:aws:dynamodb:us-west-2:123456789012:table/terraform-locks"]
  }
}

resource "aws_iam_role_policy" "terraform_state" {
  name   = "terraform-state-access"
  role   = module.github_role.github_role_name
  policy = data.aws_iam_policy_document.terraform_state.json
}

Production Role Limited to main

A role that pushes release images should not be available to every branch. Allow only workflows running on main:

module "github_role_production" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name    = "infrahouse"
  repo_name      = "my-service"
  subject_claims = ["ref:refs/heads/main"]
}

If the deployment job sets environment: production, its token carries environment:production instead; allow that claim and protect the environment. See Configuration.

Several Repositories

The trust policy covers a single repository, so create one role per repository:

locals {
  repositories = ["aws-control", "website", "data-pipeline"]
}

module "github_role" {
  source   = "registry.infrahouse.com/infrahouse/github-role/aws"
  version  = "1.4.0"
  for_each = toset(local.repositories)

  gh_org_name = "infrahouse"
  repo_name   = each.value
}

output "github_role_arns" {
  value = { for repo, mod in module.github_role : repo => mod.github_role_arn }
}

Custom Name and Longer Sessions

A migration job that runs for three hours needs both a longer role session and a matching request from the workflow:

module "github_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name          = "infrahouse"
  repo_name            = "data-pipeline"
  role_name            = "data-pipeline-migrations"
  max_session_duration = 10800 # 3 hours
}
- uses: aws-actions/configure-aws-credentials@v5
  with:
    role-to-assume: arn:aws:iam::123456789012:role/data-pipeline-migrations
    aws-region: us-west-2
    role-duration-seconds: 10800

Separate Plan and Apply Roles

Give the same repository two roles with different permissions and pick one per job:

module "plan_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
  role_name   = "aws-control-plan"
}

module "apply_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
  role_name   = "aws-control-apply"
}

resource "aws_iam_role_policy_attachment" "plan_read_only" {
  role       = module.plan_role.github_role_name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

Both roles trust every workflow in the repository, so restrict who may run the apply job with a GitHub environment and required reviewers.

Roles in Several Accounts

Provider aliases put a role for the same repository in each account:

provider "aws" {
  alias  = "staging"
  region = "us-west-2"
  assume_role {
    role_arn = "arn:aws:iam::111111111111:role/OrganizationAccountAccessRole"
  }
}

provider "aws" {
  alias  = "production"
  region = "us-west-2"
  assume_role {
    role_arn = "arn:aws:iam::222222222222:role/OrganizationAccountAccessRole"
  }
}

module "staging_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"
  providers = {
    aws = aws.staging
  }

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
}

module "production_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"
  providers = {
    aws = aws.production
  }

  gh_org_name = "infrahouse"
  repo_name   = "aws-control"
}

Each account needs its own GitHub OIDC identity provider.

Pushing Images to ECR

module "github_role" {
  source  = "registry.infrahouse.com/infrahouse/github-role/aws"
  version = "1.6.0"

  gh_org_name = "infrahouse"
  repo_name   = "website"
}

data "aws_iam_policy_document" "ecr_push" {
  statement {
    sid       = "GetAuthorizationToken"
    actions   = ["ecr:GetAuthorizationToken"]
    resources = ["*"]
  }

  statement {
    sid = "PushImages"
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:CompleteLayerUpload",
      "ecr:InitiateLayerUpload",
      "ecr:PutImage",
      "ecr:UploadLayerPart",
    ]
    resources = ["arn:aws:ecr:us-west-2:123456789012:repository/website"]
  }
}

resource "aws_iam_role_policy" "ecr_push" {
  name   = "ecr-push"
  role   = module.github_role.github_role_name
  policy = data.aws_iam_policy_document.ecr_push.json
}