Building a Google Cloud Platform CI/CD/DevOps solution with Pulumi and GitLab - Part 2 Setting up the GCP organisational structure

February 16, 2020

In Part 1 we bootstrapped our GitLab configuration using Pulumi, then used a GitOps style build pipeline in GitLab CI to further configure our GitLab project.

Next we’ll run through setting up an organisation structure in Google Cloud with Pulumi, creating a project for shared resources, and creating a service account that is an admin for a folder in the org structure.

The Organisation and folder structure of resources (and a number of other GCP features) are only available with a GSuite account.
For a solo hobby project its not required however as soon as you’re in a team environment its recommended for:

  • Applying security policies at the organisation and folder level to make it easier to apply the principle of least privilege, and
  • Having a service accounts that can create and destroy projects (for automated integration testing)

The Pulumi GCP getting started docs are at https://www.pulumi.com/docs/get-started/gcp/create-project/

Following on from our our infrastructure project in part 1 we’ll create a new folder named gcp-org and then in it run pulumi new gcp-typescript

Enter org for the project name and enter any value for the GCP project for now, we’ll update it after creating the shared project. This will create the default Pulumi project we can get started with.

We will want to constants to share between Pulumi projects so create a filed named globals.ts in the project root directory. As mentioned in Part 1 you’ll need a GSuite account to create an organisation to populate the orgId const.

// This will be prefixed to global resources as required to ensure ids are unique
export const orgResourcePrefix = '' || process.env.ORG_PREFIX

export const orgId = '' || process.env.GCP_ORG_ID
export const orgIdentifier = 'organizations/' + orgId

export const billingAccount = '' || process.env.GCP_BILLING_ACCOUNT

if(!orgResourcePrefix) throw 'ORG_PREFIX is required'
if(!orgId) throw 'GCP_ORG_ID is required'
if(!billingAccount) throw 'GCP_BILLING_ACCOUNT is required'

Open up gcp-org/index.ts and import the globals.ts file and define the folder structure for our organisation in

import * as globals from '../globals'
import { Folder } from "@pulumi/gcp/organizations/folder";
import {Project} from "@pulumi/gcp/organizations";

// Setup the folder structure  ======

const prodFolder = new Folder('Prod', { displayName: 'Prod', parent: globals.orgIdentifier})

const nonProdFolder = new Folder('Non-Prod', { displayName: 'Non-Prod', parent: globals.orgIdentifier})
const e2eFolder = new Folder('CI-E2E', { displayName: 'CI-E2E', parent: nonProdFolder.id})

Now lets create a project at the organisation level that we can use for global/shared resources, and non project specific services accounts. We can skip creating the auto-generated network subnets as we won’t need them.

const orgLevelProjectId = `${globals.orgResourcePrefix}-org`
const orgLevelProject = new Project(orgLevelProjectId,
    {projectId: orgLevelProjectId, orgId: globals.orgId, billingAccount: globals.billingAccount, autoCreateNetwork: false})

Here we create the service account that we will use for integration tests, and then at the CI-E2E folder we apply an IAM policy with the Folder Admin role. This gives the service account full access to resources under that folder, so it can create and provision a project to run an integration test in, and then delete it afterwards.

const e2eServiceAccount = new gcp.serviceAccount.Account('e2e-admin',
    { project: orgLevelProject.id, accountId: 'e2e-admin', displayName: 'Service account to administer temporary projects for end-to-end integration tests'})

const e2eAdminIAM = new gcp.folder.IAMBinding('e2e-admin',
    { folder: e2eFolder.name, members: [pulumi.interpolate `serviceAccount:${e2eServiceAccount.email}`], role: 'roles/resourcemanager.folderAdmin'});

To manage the org structure with GitOps lets add to the GitLab CI configuration. Create the file gcp-org/.gcp-org-ci.yml containing:

gcp-org-preview:
  stage: infra-preview
  only:
    - production/gcp-org
  script:
    - cd gcp-org
    - gcloud auth activate-service-account --key-file=$SERVICE_ACCOUNT_KEY
    - pulumi stack select prod
    - npm install
    - pulumi preview --stack prod --suppress-outputs --show-replacement-steps

gcp-org-deploy:
  stage: infra-deploy
  only:
    - production/gcp-org
  when: manual
  script:
    - cd gcp-org
    - gcloud auth activate-service-account --key-file=$SERVICE_ACCOUNT_KEY
    - pulumi stack select prod
    - npm install
    - pulumi up --stack prod --skip-preview --suppress-outputs

And update /.gitlab-ci.yml to also include this file

Before you run this you’ll need to configure a file variable in your GitLab CI settings named SERVICE_ACCOUNT_KEY with a service account key that has the suitable permissions to perform the querying and updating of resources required.

The final stage in the CI build is a manual step, with the intention that you should review the output of the preview stage logs before starting the final deploy stage.

git url

When you have verified the preview plan then start the final manual build stage. After that it has completed in the GCP console go to IAM -> Manage Resources and it should now look similar to this:

git url