Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform bootstrap recipe #26

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1f48bf8
wip state management terraform recipe
Feb 17, 2023
a4ec399
Escape CLI_ARGS in taskfile template
Feb 23, 2023
834d24d
Fix help text on how to provide environment name
Feb 23, 2023
c6a9b35
Fix chicken & egg problem with backend config file generation
Feb 23, 2023
911ea12
Validate Azure resource group name
Feb 24, 2023
4118d9e
Add environments var
Feb 24, 2023
ad03982
Drop env name from tfstate key
Feb 24, 2023
004adac
WIP loop over envs and create all (mostly TODOs)
Feb 24, 2023
ef5ed6b
Use hash of anchor instead of randNumeric
majori Mar 7, 2023
9a3e34d
Improve recipe process
majori Mar 13, 2023
c825dd5
Use stableRandom in tfstate storage account name
ilkka Mar 21, 2023
921987a
Remove some whitespace
ilkka Mar 22, 2023
3568b0c
Don't fail when terraform.tfstate.d doesn't exist
ilkka Mar 22, 2023
dd13599
Init task name changed
ilkka Mar 22, 2023
5621ee0
Loop over environments and create them all
ilkka Mar 24, 2023
87834f6
Remove init dep on az:login
ilkka Mar 24, 2023
491fe8b
Fix missing colon
ilkka Mar 24, 2023
3edd762
Need to do the whole tf init for each env
ilkka Mar 24, 2023
b8d2ddb
Remove backend config between envs
ilkka Mar 24, 2023
24bc791
Tabs to spaces in rendered backend.tf
ilkka Mar 24, 2023
1179ba7
Comment cleanup
ilkka Mar 24, 2023
5549053
wip: reconcile with other branch
ilkka May 17, 2023
2833c6d
Start using table variables
majori Sep 25, 2023
a6a4648
Create Github Actions pipeline conditionally
majori Sep 26, 2023
32f6698
Set CI service principal credentials with `gh`
majori Sep 27, 2023
fb551bf
Merge branch 'main' into ilkka/20-Create-Terraform-state-bootstrap-re…
Dec 1, 2023
4adfaed
Fix typo in environment variable
Dec 1, 2023
d71a80b
Don't use env vars for reusable workflows
Dec 4, 2023
0cb4201
Sub and tenant IDs are passed as secrets
Dec 4, 2023
0e0aafb
Merge remote-tracking branch 'origin/ilkka/20-Create-Terraform-state-…
Dec 4, 2023
dfd12fc
Fix generated terraform formatting
Dec 4, 2023
2d77ee5
Pass container and key to tf init in plan stage
Dec 4, 2023
469bcce
Fix configuring plan and apply env
Dec 4, 2023
e597072
Add a readme with some details on the permissions setup
Dec 7, 2023
ffedb28
Reword SP and app reg
Dec 7, 2023
fe77508
Fix state storage formatting
Dec 7, 2023
b2f0387
Making RGs is optional
Dec 7, 2023
0bb7d96
Add recommendations
majori Dec 7, 2023
160ed5d
Merge branch 'main' into ilkka/20-Create-Terraform-state-bootstrap-re…
Dec 8, 2023
a7b49eb
Merge remote-tracking branch 'origin/main' into ilkka/20-Create-Terra…
Dec 13, 2023
c606b70
Provide the shasum resource tag separately
Dec 15, 2023
ce6c3e8
Output role assignment ID for later use
Dec 15, 2023
82d24a2
Update to more recent tf version
Dec 15, 2023
b63649c
Correctly manage tfstate container permissions
Dec 15, 2023
ed4d978
Fix passthru mustache stuff
Dec 15, 2023
58218fd
Add resource tag to tf GHA workflow
Dec 15, 2023
c24e328
Clean up some whitespace
Dec 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions examples/terraform-bootstrap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Terraform Bootstrap Example recipe

This recipe demonstrates how to bootstrap Terraform state management in Azure.
The goal is to have separate state storage for a number of different
environments, all on Azure, and to manage the state storage itself using
Terraform. This is a common pattern in IaC projects.

The recipe is mainly intended to be used together with GitHub Actions, but that
functionality is optional and managed by a user setting that is set when
executing the recipe. If GitHub Actions is used, pipeline files are generated
such that IaC changes flow through a format check, validate, plan and apply
pipeline.

## Prerequisites

Pre-creating resources is optional, but permissions management is easier if you
do. The following resources are required:

1. A subscription
2. (OPTIONAL) As many resource groups as you want to have environments (e.g. dev,
qa, prod). These resource groups should be empty, but they don't have to be.
3. A service principal with contributor access to the resource groups, and a
client secret for the SP.

### Generating a service principal

1. Go to Azure Portal and the Entra ID blade and add an Enterprise Application.
It does not matter what the redirect URL is. Everything can be left at default.
An App Registration should also be generated in your chosen tenant.
2. For each resource group, go to the IAM blade and add the application as a
contributor.
3. Go to the Certificates & secrets blade for the Service Principal and add a
client secret. Copy the secret value.

## Usage

Authenticate to Azure:

```shell
az login
```

You can use either your own account (if you have the necessary permissions on
the target subscription) of the Service Principal generated earlier.

Run the following commands:

```shell
cd terraform && task init
```

If you chose to generate a CI/CD pipeline, the init task will prompt for the
subscription ID, tenant ID, client ID and client secret for each of the
environments. Use the values for the Service Principal generated earlier.
These values will be stored as secrets in GitHub.

If you used the Service Principal credentials when running `task init`, you are
done. If not, you need to also assign the "Storage Blob Data Contributor" role
to the Service Principal on the storage accounts created by the recipe. You can
do this in the Azure Portal in IAM blades of the resource groups.
25 changes: 25 additions & 0 deletions examples/terraform-bootstrap/recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: v1
name: terraform-bootstrap
version: v0.0.1
description: Set up Terraform basics like state file bootstrapping
initHelp: Install Task from https://taskfile.dev and run `task init` in the 'terraform' subdirectory of the project directory to set up terraform.
vars:
- name: ENVIRONMENTS
description: Comma-separated list of environments to create, e.g. "dev, qa, prod"
columns: [NAME, RESOURCE_GROUP_NAME]

- name: SERVICE_NAME
description: Service name

- name: CREATE_RESOURCE_GROUPS
confirm: true

- name: RESOURCE_GROUP_LOCATION
if: CREATE_RESOURCE_GROUPS == true
options:
- "North Europe"
- "West Europe"
# ...

- name: CREATE_GITHUB_ACTIONS_PIPELINE
confirm: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{{- if .Variables.CREATE_GITHUB_ACTIONS_PIPELINE }}
name: Terraform Apply

on:
workflow_call:
inputs:
ENVIRONMENT:
required: true
type: string
TERRAFORM_VERSION:
required: true
type: string
secrets:
ARM_SUBSCRIPTION_ID:
required: true
ARM_TENANT_ID:
required: true
ARM_CLIENT_ID:
required: true
ARM_CLIENT_SECRET:
required: true

env:
TF_IN_AUTOMATION: "true"

jobs:
plan:
runs-on: ubuntu-latest
environment: {{ "${{ inputs.ENVIRONMENT }}" }}
env:
ARM_CLIENT_ID: {{ "${{ secrets.ARM_CLIENT_ID }}" }}
ARM_CLIENT_SECRET: {{ "${{ secrets.ARM_CLIENT_SECRET }}" }}
ARM_SUBSCRIPTION_ID: {{ "${{ secrets.ARM_SUBSCRIPTION_ID }}" }}
ARM_TENANT_ID: {{ "${{ secrets.ARM_TENANT_ID }}" }}
steps:
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: {{ "${{ inputs.TERRAFORM_VERSION }}" }}

- name: Download the plan
uses: actions/download-artifact@v2
with:
name: terraform-plan-{{ "${{ inputs.ENVIRONMENT }}" }}

- name: Restore run permissions
run: chmod -R +x .terraform

- name: Terraform Apply
id: apply
run: terraform apply -input=false -no-color terraform.tfplan
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{{- if .Variables.CREATE_GITHUB_ACTIONS_PIPELINE }}
name: Terraform Plan

on:
workflow_call:
inputs:
ENVIRONMENT:
required: true
type: string
TERRAFORM_VERSION:
required: true
type: string
TERRAFORM_BACKEND_STORAGE_NAME:
required: true
type: string
RESOURCE_GROUP_NAME:
required: true
type: string
secrets:
ARM_SUBSCRIPTION_ID:
required: true
ARM_TENANT_ID:
required: true
ARM_CLIENT_ID:
required: true
ARM_CLIENT_SECRET:
required: true

env:
TF_IN_AUTOMATION: "true"

jobs:
plan:
runs-on: ubuntu-latest
env:
ARM_CLIENT_ID: {{ "${{ secrets.ARM_CLIENT_ID }}" }}
ARM_CLIENT_SECRET: {{ "${{ secrets.ARM_CLIENT_SECRET }}" }}
ARM_SUBSCRIPTION_ID: {{ "${{ secrets.ARM_SUBSCRIPTION_ID }}" }}
ARM_TENANT_ID: {{ "${{ secrets.ARM_TENANT_ID }}" }}
steps:
- name: Check out repository code
uses: actions/checkout@v2

- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: {{ "${{ inputs.TERRAFORM_VERSION }}" }}

- name: Terraform Format
id: fmt
run: terraform fmt -check
working-directory: ./terraform

- name: Terraform Init
id: init
run: >-
terraform init
-backend-config="resource_group_name={{ "${{ inputs.RESOURCE_GROUP_NAME }}" }}"
-backend-config="storage_account_name={{ "${{ inputs.TERRAFORM_BACKEND_STORAGE_NAME }}" }}"
-backend-config="container_name=tfstate"
-backend-config="key=tfstate_"

working-directory: ./terraform

- name: Terraform Workspace
id: workspace
run: terraform workspace select {{ "${{ inputs.ENVIRONMENT }}" }}
working-directory: ./terraform

- name: Terraform Validate
id: validate
run: terraform validate -no-color
working-directory: ./terraform

- name: Terraform Plan
id: plan
run: terraform plan -out=terraform.tfplan -no-color
working-directory: ./terraform
continue-on-error: true
env:
TF_VAR_resource_group_name: {{ "${{ inputs.RESOURCE_GROUP_NAME }}" }}

- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1

- name: Archive Terraform plan
uses: actions/upload-artifact@v2
with:
name: terraform-plan-{{ "${{ inputs.ENVIRONMENT }}" }}
path: ./terraform
retention-days: 7
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{{- if .Variables.CREATE_GITHUB_ACTIONS_PIPELINE -}}
name: Terraform CI/CD
on:
push:
branches:
- main
paths:
- "terraform/**"
- ".github/workflows/terraform*.yml"

jobs:
{{- range $i, $env := .Variables.ENVIRONMENTS }}
{{- if (gt $i 0) }}
{{/* Add empty line if not the first job */ -}}
{{ end }}
build-{{ $env.NAME }}:
name: Build for {{ $env.NAME | upper }}
uses: ./.github/workflows/terraform-plan.yml
with:
ENVIRONMENT: {{ $env.NAME }}
TERRAFORM_VERSION: {{ "${{ vars.TERRAFORM_VERSION }}" }}
TERRAFORM_BACKEND_STORAGE_NAME: {{ template "storage_account_name_prefix" $ }}{{ template "resource_tag" $ }}{{ $env.NAME }}
RESOURCE_GROUP_NAME: {{ $env.RESOURCE_GROUP_NAME }}
secrets:
ARM_CLIENT_ID: {{ printf "${{ secrets.ARM_CLIENT_ID_%s }}" ($env.NAME | upper) }}
ARM_CLIENT_SECRET: {{ printf "${{ secrets.ARM_CLIENT_SECRET_%s }}" ($env.NAME | upper) }}
ARM_SUBSCRIPTION_ID: {{ printf "${{ secrets.ARM_SUBSCRIPTION_ID_%s }}" ($env.NAME | upper) }}
ARM_TENANT_ID: {{ printf "${{ secrets.ARM_TENANT_ID_%s }}" ($env.NAME | upper) }}

deploy-{{ $env.NAME }}:
name: Deploy to {{ $env.NAME | upper }}
needs: build-{{ $env.NAME }}
uses: ./.github/workflows/terraform-apply.yml
with:
ENVIRONMENT: {{ $env.NAME }}
TERRAFORM_VERSION: {{ "${{ vars.TERRAFORM_VERSION }}"}}
secrets:
ARM_CLIENT_ID: {{ printf "${{ secrets.ARM_CLIENT_ID_%s }}" ($env.NAME | upper) }}
ARM_CLIENT_SECRET: {{ printf "${{ secrets.ARM_CLIENT_SECRET_%s }}" ($env.NAME | upper) }}
ARM_SUBSCRIPTION_ID: {{ printf "${{ secrets.ARM_SUBSCRIPTION_ID_%s }}" ($env.NAME | upper) }}
ARM_TENANT_ID: {{ printf "${{ secrets.ARM_TENANT_ID_%s }}" ($env.NAME | upper) }}
{{- end }}
{{- end -}}
2 changes: 2 additions & 0 deletions examples/terraform-bootstrap/templates/terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.terraform
terraform.tfstate.d
Loading
Loading