dflook / terraform-github-actions

GitHub actions for terraform
781 stars 153 forks source link

Improve the cloud backend type by supporting envs vars #267

Closed DanyC97 closed 1 year ago

DanyC97 commented 1 year ago

Suggestion

๐Ÿ‘‹ @danielflooktech , first thank you for sharing the suite of actions, very useful as it covers various edge cases especially when we use the TFC.

I'd like to make a suggestion to improve the support for the backend cloud as current implementation suffers from few bits which , for the record, is mainly due to the situation created by Hashi when they introduce delta between remote and cloud. They claim is the same but is not really the same.

What am i trying to do

In nutshell i'm trying to create a reusable workflow as below with a given backend.tf as

cat backend.tf

terraform {
  cloud {
    organization = "dani-tfc-free-org"
  }
}

Note:

The TFC workspace name fo-tfc-mgmt is running in remote execution mode, however similar is when is set for local. The TF version set in the workspace is 1.4.5.

cat reusable-terraform-plan-run.yml

# This is a reusable workflow to run terraform plan and apply on
# 'pull_request'
name: Reusable Terraform Plan
on:
  workflow_call:
    inputs:
      tf_workspace:
        description: "Name of the terraform workspace"
        required: false
        type: string
      tf_path:
        description: "Path to the terraform configuration"
        required: false
        default: .
        type: string
      tf_variables:
        description: "Variable definitions"
        required: false
        type: string
      tf_var_file:
        description: "List of var file paths, one per line"
        required: false
        type: string
      tf_backend_config:
        description: "List of backend config values to set, one per line"
        required: false
        type: string
      tf_backend_config_file:
        description: "Path to a backend config file"
        required: false
        type: string
    secrets:
      tf_api_token:
        description: "Terraform Cloud API token."
        required: true
      gcp_project_id:
        description: "Google Cloud Project id"
        required: false
      gcp_project_number:
        description: "Google Cloud Project number"
        required: false
      gcp_wif_sa_id:
        description: "Google Cloud Workload Identity Federation Service Account id"
        required: false
      gcp_wif_pool_name:
        description: "Google Cloud Workload Identity Federation pool name"
        required: false

# Allow only one of this workflow to run at a time per branch.
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

# Allow read repository contents, write deployments and pull requests.
permissions:
  contents: read
  pull-requests: write

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  tf-plan-run:
    runs-on: ubuntu-latest
    steps:
      - name: โคต๏ธ Check out code from GitHub
        uses: actions/checkout@v3.5.2 # v3.5.2

      # Unfortunately Github Action interpreter currently doesn't identify
      # the secrets key word when used in an if conditional expression
      - name: Check for input secret availability
        id: secret-check
        # perform secret check & put boolean result as an output
        shell: bash
        run: |
          if [ "${{ secrets.gcp_project_id }}" != '' ] && [ "${{ secrets.gcp_project_number }}" != '' ] && [ "${{ secrets.gcp_wif_sa_id }}" != '' ] && [ "${{ secrets.gcp_wif_pool_name }}" != '' ]; then
            echo "available=true" >> $GITHUB_OUTPUT;
          else
            echo "available=false" >> $GITHUB_OUTPUT;
          fi

      - name: Setup Google Auth
        if: ${{ steps.secret-check.outputs.available == 'true' }}
        uses: google-github-actions/auth@v1.1.1 # v1.1.1
        with:
          service_account: ${{ secrets.gcp_wif_sa_id }}@${{ secrets.gcp_project_id }}.iam.gserviceaccount.com
          workload_identity_provider: "projects/${{ secrets.gcp_project_number }}/locations/global/workloadIdentityPools/${{ secrets.gcp_wif_pool_name }}/providers/github-provider"
          token_format: access_token
          access_token_lifetime: 300s

      - name: Terraform plan
        uses: dflook/terraform-plan@v1.34.0
        env:
          TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.tf_api_token }}
          TFE_TOKEN: ${{ secrets.tf_api_token }}
          TF_WORKSPACE: ${{ inputs.tf_workspace }}
        with:
          # in case of 'cloud' backend type is ignored
          workspace: ${{ inputs.tf_workspace }}
          path: ${{ inputs.tf_path }}
          backend_config_file: ${{ inputs.tf_backend_config_file }}
          backend_config: ${{ inputs.tf_backend_config }}
          variables: ${{ inputs.tf_variables }}
          var_file: ${{ inputs.tf_var_file }}

cat ci-terraform-plan-run.yml (caller workflow)

name: Terraform Plan
on:
  # For terraform plan
  pull_request:
    types: [opened, synchronize]

jobs:
  ci-terraform:
    uses: ./.github/workflows/reusable-terraform-plan-run.yml
    with:
      tf_workspace: fo-tfc-mgmt
      # tf_backend_config: workspace=fo-tfc-mgmt
    secrets:
      tf_api_token: ${{ secrets.TF_TOKEN_APP_TERRAFORM_IO }}

Issues bumped into

Current pattern is to discover the backend type and then build the config, unless provided using backend_config_file. Now there is also s'thing which i'm not sure i understand why we assume cloud = remote but that is a different question to answer.

Now as per Hashi docs, they suggest to

Workarounds used but no luck

I have tried to pass in the TF_WORKSPACE

      - name: Terraform plan
        uses: dflook/terraform-plan@v1.34.0
        env:
          TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.tf_api_token }}
          TFE_TOKEN: ${{ secrets.tf_api_token }}
          TF_WORKSPACE: ${{ inputs.tf_workspace }}
        with:
          # in case of 'cloud' backend type is ignored
          workspace: ${{ inputs.tf_workspace }}

but that is being unset in the code here i hope i'm not wrong) and also the workspace is not being used as https://github.com/dflook/terraform-github-actions/blob/main/image/actions.sh#L101 is run and discover in an early phase

What i would like to see

I'd love to whitelist and allow at least the TF_WORKSPACE to be passed in as env unless you can expand and integrate easily the workspace and the backend_type such that is being used (in my tests it didn't work at all)

dflook commented 1 year ago

Hi, thanks for creating an issue.

To check I understand this correctly, there are two main issues here:

  1. The workspace input is not working with a cloud backend.
  2. The backend_config/backend_config_file input is not working with a cloud backend.

You are correct TF_WORKSPACE doesn't do anything when passed to the action, the idea is you use the workspace input instead.

DanyC97 commented 1 year ago

To check I understand this correctly, there are two main issues here:

you are correct .

The backend_config/backend_config_file input is not working with a cloud backend.

right and by looking at Hashi docs, it was never meant to work with cloud backend but remote only. And even when using remote it looks like you can't have inside the file

workspaces = foo OR workspaces = {name = "foo" }

My feeling is that remote was designed for the world before TFC and then they tried to "consolidate" the 2 worlds: oss & tfc which resulted in ...

the idea is you use the workspace input instead.

right, thank you for the context ๐Ÿ‘ , let me know if there is anything i can help you with

danicomnea-wpp commented 1 year ago

hey @dflook , fyi been running a bit more tests but this time enabled ACTIONS_STEP_DEBUG: true at the runner level (in my repo settings) and what i see is this.

Given my workflow setup as below - pasting only the snippets to save space

caller wk


jobs:
ci-terraform:
uses: ./.github/workflows/reusable-terraform-plan-run.yml
with:
tf_workspace: fo-tfc-mgmt
secrets:
tf_api_token: ${{ secrets.TF_TOKEN_APP_TERRAFORM_IO }}

> reusable wk
  - name: Terraform plan
    uses: dflook/terraform-plan@v1.34.0
    env:
      TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.tf_api_token }}
      TFE_TOKEN: ${{ secrets.tf_api_token }}
    with:
      # in case of 'cloud' backend type is ignored
      workspace: ${{ inputs.tf_workspace }}
      path: ${{ inputs.tf_path }}
      backend_config_file: ${{ inputs.tf_backend_config_file }}
      backend_config: ${{ inputs.tf_backend_config }}
      variables: ${{ inputs.tf_variables }}
      var_file: ${{ inputs.tf_var_file }}

the debug output shows a lead ?

Initializing Terraform

[debug] TF_WORKSPACE=fo-tfc-mgmt terraform init -input=false $INIT_ARGS

Initializing Terraform Cloud...

Initializing provider plugins...


and this works as it should ๐Ÿ‘ 

[debug] terraform workspace select fo-tfc-mgmt

::group::Selecting workspace Selecting workspace โ•ท โ”‚ Error: Invalid workspaces configuration โ”‚ โ”‚ Missing workspace mapping strategy. Either workspace "tags" or "name" is โ”‚ required. โ”‚ โ”‚ The 'workspaces' block configures how Terraform CLI maps its workspaces for โ”‚ this single โ”‚ configuration to workspaces within a Terraform Cloud organization. Two โ”‚ strategies are available: โ”‚ โ”‚ tags - A set of tags used to select remote Terraform Cloud โ”‚ workspaces to be used for this single โ”‚ configuration. New workspaces will automatically be tagged with these tag โ”‚ values. Generally, this โ”‚ is the primary and recommended strategy to use. This option conflicts with โ”‚ "name". โ”‚ โ”‚ name - The name of a single Terraform Cloud workspace to be โ”‚ used with this configuration. โ”‚ When configured, only the specified workspace can be used. This option โ”‚ conflicts with "tags". โ•ต


this fails .

In case it helps, when i run it locally w/o the action i do

export TF_WORKSPACE


and then init/ plan/ apply w/o selecting any workspace; the workspace defined in TFC won't be known to my local TF cli 

See more info [here](https://discuss.hashicorp.com/t/help-using-terraform-workspaces-in-an-automation-pipeline-with-tf-workspace-currently-selected-workspace-x-does-not-exist/40676/5) 
DanyC97 commented 1 year ago

hi @dflook , please let me know if you have any thoughts on where you think will be great to dig to get to the a solution ?

If you willing to help closing this one am here as this is a blocker for me.

cheers!

DanyC97 commented 1 year ago

In case it helps others who may bump into similar issue. A potential workaround is to templatize the stanza as below

cat backend.tf.tpl

terraform {
  cloud {
    organization = "foo"

    workspaces {
      name = "${TF_WORKSPACE}"
    }
  }
}

and the action

      - name: Terraform plan
        uses: dflook/terraform-plan@v1.34.0
        env:
          TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.tf_api_token }}
          TFE_TOKEN: ${{ secrets.tf_api_token }}
          TERRAFORM_PRE_RUN: |
            # Install gettext for envsubst
            apt-get update
            apt-get install -y --no-install-recommends gettext-base

            # Render the backend.tf file
            export TF_WORKSPACE=${{ inputs.tf_workspace }}
            envsubst <examples/backend.tf.tpl >backend.tf

        with:
          # in case of 'cloud' backend type is ignored
          workspace: ${{ inputs.tf_workspace }}
          path: ${{ inputs.tf_path }}
          backend_config_file: ${{ inputs.tf_backend_config_file }}
          backend_config: ${{ inputs.tf_backend_config }}
          variables: ${{ inputs.tf_variables }}
          var_file: ${{ inputs.tf_var_file }}
dflook commented 1 year ago

v1.35.0 has been released which supports using TF_CLOUD_ORGANIZATION and TF_CLOUD_HOSTNAME environment variables to complete the cloud block. The workspace should be specified with the workspace input.

DanyC97 commented 1 year ago

@dflook thank you v much for quick turnaround ! I shall go and test it.

will close the current issue, thank you again for your help!