From 7d13a1f8b6e772e8eb34991c8770fdaf59488b09 Mon Sep 17 00:00:00 2001 From: BadgerHobbs Date: Mon, 11 Sep 2023 02:59:02 +0100 Subject: [PATCH] Added Terraform files and GitHub Actions. --- .github/workflows/artifact.yml | 44 ++++++ .github/workflows/artifact_encrypted.yml | 46 +++++++ .github/workflows/repository_file.yml | 42 ++++++ .../workflows/repository_file_encrypted.yml | 51 +++++++ README.md | 128 +++++++++++++++++- action.yml | 120 ++++++++++++++++ main.tf | 19 +++ 7 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/artifact.yml create mode 100644 .github/workflows/artifact_encrypted.yml create mode 100644 .github/workflows/repository_file.yml create mode 100644 .github/workflows/repository_file_encrypted.yml create mode 100644 action.yml create mode 100644 main.tf diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml new file mode 100644 index 0000000..4c9f4e1 --- /dev/null +++ b/.github/workflows/artifact.yml @@ -0,0 +1,44 @@ +name: Artifact +on: + workflow_dispatch: + push: + paths-ignore: + - "**.tfstate" + - "**.tfstate.encrypted" + +jobs: + artifact: + runs-on: ubuntu-latest + name: Artifact + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Download Artifact + uses: badgerhobbs/terraform-state@v1 + with: + operation: download + location: artifact + github_token: ${{ secrets.gh_access_token }} + continue-on-error: true + + - name: Configure Terraform + uses: hashicorp/setup-terraform@v2 + + - name: Initialize Terraform + run: terraform init + + - name: Run Terraform Plan + run: | + terraform plan -var="run_id=${{ github.run_id }}" + + - name: Run Terraform Apply + run: | + terraform apply -auto-approve -var="run_id=${{ github.run_id }}" + + - name: Upload Artifact + uses: badgerhobbs/terraform-state@v1 + with: + operation: upload + location: artifact + github_token: ${{ secrets.gh_access_token }} diff --git a/.github/workflows/artifact_encrypted.yml b/.github/workflows/artifact_encrypted.yml new file mode 100644 index 0000000..541adc5 --- /dev/null +++ b/.github/workflows/artifact_encrypted.yml @@ -0,0 +1,46 @@ +name: Artifact Encrypted +on: + workflow_dispatch: + workflow_run: + workflows: ["Artifact"] + types: + - completed + +jobs: + artifact_encrypted: + runs-on: ubuntu-latest + name: Artifact Encrypted + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Download Encrypted Artifact & Decrypt Artifact + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: artifact + github_token: ${{ secrets.gh_access_token }} + continue-on-error: true + + - name: Configure Terraform + uses: hashicorp/setup-terraform@v2 + + - name: Initialize Terraform + run: terraform init + + - name: Run Terraform Plan + run: | + terraform plan -var="run_id=${{ github.run_id }}" + + - name: Run Terraform Apply + run: | + terraform apply -auto-approve -var="run_id=${{ github.run_id }}" + + - name: Encrypt Artifact & Upload Encrypted Artifact + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: artifact + github_token: ${{ secrets.gh_access_token }} diff --git a/.github/workflows/repository_file.yml b/.github/workflows/repository_file.yml new file mode 100644 index 0000000..c014e18 --- /dev/null +++ b/.github/workflows/repository_file.yml @@ -0,0 +1,42 @@ +name: Repository File +on: + workflow_dispatch: + workflow_run: + workflows: ["Artifact Encrypted"] + types: + - completed + +jobs: + repository_file: + runs-on: ubuntu-latest + name: Repository File + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + token: ${{ secrets.gh_access_token }} + + - name: Configure Terraform + uses: hashicorp/setup-terraform@v2 + + - name: Initialize Terraform + run: terraform init + + - name: Run Terraform Plan + run: | + terraform plan -var="run_id=${{ github.run_id }}" + + - name: Run Terraform Apply + run: | + terraform apply -auto-approve -var="run_id=${{ github.run_id }}" + + - name: List all files and directories + run: | + echo "List all directories and files in the GitHub workspace" + ls -R + + - name: Commit Repository File + uses: badgerhobbs/terraform-state@v1 + with: + operation: upload + location: repository diff --git a/.github/workflows/repository_file_encrypted.yml b/.github/workflows/repository_file_encrypted.yml new file mode 100644 index 0000000..7d4bc18 --- /dev/null +++ b/.github/workflows/repository_file_encrypted.yml @@ -0,0 +1,51 @@ +name: Repository File Encrypted +on: + workflow_dispatch: + workflow_run: + workflows: ["Repository File"] + types: + - completed + +jobs: + repository_file_encrypted: + runs-on: ubuntu-latest + name: Repository File Encrypted + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + token: ${{ secrets.gh_access_token }} + + - name: Decrypt Repository File + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: repository + continue-on-error: true + + - name: Configure Terraform + uses: hashicorp/setup-terraform@v2 + + - name: Initialize Terraform + run: terraform init + + - name: Run Terraform Plan + run: | + terraform plan -var="run_id=${{ github.run_id }}" + + - name: Run Terraform Apply + run: | + terraform apply -auto-approve -var="run_id=${{ github.run_id }}" + + - name: List all files and directories + run: | + echo "List all directories and files in the GitHub workspace" + ls -R + + - name: Encrypt and Commit Repository File + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: repository diff --git a/README.md b/README.md index e0a1952..0622787 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,126 @@ -# terraform-state -GitHub Action that stores Terraform state file as encrypted artifact or repository file. +# Terraform State + +[![Artifact](https://github.com/BadgerHobbs/terraform-state/actions/workflows/artifact.yml/badge.svg)](https://github.com/BadgerHobbs/terraform-state/actions/workflows/artifact.yml) [![Artifact Encrypted](https://github.com/BadgerHobbs/terraform-state/actions/workflows/artifact_encrypted.yml/badge.svg)](https://github.com/BadgerHobbs/terraform-state/actions/workflows/artifact_encrypted.yml) [![Repository File](https://github.com/BadgerHobbs/terraform-state/actions/workflows/repository_file.yml/badge.svg)](https://github.com/BadgerHobbs/terraform-state/actions/workflows/repository_file.yml) [![Repository File Encrypted](https://github.com/BadgerHobbs/terraform-state/actions/workflows/repository_file_encrypted.yml/badge.svg)](https://github.com/BadgerHobbs/terraform-state/actions/workflows/repository_file_encrypted.yml) + +Terraform State is a GitHub Action that manages the storage of your Terraform state file as an (optionally) encrypted artifact or repository file. This makes it easier for you to handle your state file securely and efficiently within GitHub, not requiring a 3rd party service. + +## Getting Started + +Below you can find documentation on how to setup and use the `terraform-state` GitHub Action. + +### Setup + +The following inputs are used by the GitHub Action. + +| Variable | Description | Required | Default | +| -------------- | ----------------------------------------------------------------------------------------------------------- | -------- | ------- | +| encryption_key | AES-256 Encryption key used to encrypt/decrypt the Terraform state file. | False | N/A | +| operation | Specifies if the operation is to download or upload the Terraform state file. Options: `download`, `upload` | True | N/A | +| location | Specifies the storage location of the Terraform state file. Options: `repository`, `artifact`. | True | N/A | +| directory | Directory of the Terraform state file. | False | "." | +| github_token | GitHub Access Token used to retrieve latest artifact. | False | N/A | + +It is recommended to use GitHub secrets to store the `encryption_key` and `github_token`. + +### Usage + +The following examples illustrates the best practices to use `terraform-state` to handle various scenarios of uploading and downloading a Terraform state file. + +In addition, please note that while storing encrypted state within the repository ensures reasonable security, it is not recommended specifically for public repositories. Preferably, you should use artifacts. However, keep in mind that artifacts by default only last 90 days (can be changed in the repository settings). + +When using storing the Terraform state within the repository, changes are commited to the current branch. To prevent endless loops when the GitHub Action is triggered to run on push, configure the following. + +```yml +push: + paths-ignore: + - "**.tfstate" + - "**.tfstate.encrypted" +``` + +#### Artifact + +Please see thie [Example Workflow](.github/workflows/artifact.yml). + +```yml +- name: Download Artifact + uses: badgerhobbs/terraform-state@v1 + with: + operation: download + location: artifact + github_token: ${{ secrets.gh_access_token }} + continue-on-error: true + +- name: Upload Artifact + uses: badgerhobbs/terraform-state@v1 + with: + operation: upload + location: artifact + github_token: ${{ secrets.gh_access_token }} +``` + +#### Artifact Encrypted + +Please see thie [Example Workflow](.github/workflows/artifact_encrypted.yml). + +```yml +- name: Download Encrypted Artifact & Decrypt Artifact + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: artifact + github_token: ${{ secrets.gh_access_token }} + continue-on-error: true + +- name: Encrypt Artifact & Upload Encrypted Artifact + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: artifact + github_token: ${{ secrets.gh_access_token }} +``` + +#### Repository File + +Please see thie [Example Workflow](.github/workflows/repository_file.yml). + +```yml +- name: Commit Repository File + uses: badgerhobbs/terraform-state@v1 + with: + operation: upload + location: repository +``` + +#### Repository File Encrypted + +Please see thie [Example Workflow](.github/workflows/repository_file_encrypted.yml). + +```yml +- name: Decrypt Repository File + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: repository + continue-on-error: true + +- name: Encrypt and Commit Repository File + uses: badgerhobbs/terraform-state@v1 + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: repository +``` + +## Acknowledgments + +Despite different approaches, the development of this GitHub Action was influenced by the previous work of: + +- [sturlabragason/terraform_state_artifact](https://github.com/sturlabragason/terraform_state_artifact) +- [devgioele/terraform-state-artifact](https://github.com/devgioele/terraform-state-artifact) + +## License + +The scripts and documentation in this project are released under the [MIT License](LICENSE). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..575a958 --- /dev/null +++ b/action.yml @@ -0,0 +1,120 @@ +name: "terraform-state" +description: "Stores 2file as encrypted artifact or repository file." +author: "Andrew Riggs" + +inputs: + encryption_key: + description: "AES-256 Encryption key used to encrypt/decrypt the Terraform state file." + required: false + + operation: + description: "Specifies if the operation is to download or upload the Terraform state file. [Options: download/upload]" + required: true + + location: + description: "Specifies the storage location of the Terraform state file. [Options: repository/artifact]" + required: true + + directory: + description: "Directory of the Terraform state file." + required: false + default: "." + + github_token: + description: "GitHub Access Token used to retrieve latest artifact." + required: false + +runs: + using: "composite" + steps: + + - name: Configure Git User + shell: bash + run: | + git config --global user.name "terraform-state" + git config --global user.email "github-actions@users.noreply.github.com" + + # Artifact + - name: Download Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'download' && isNull(inputs.encryption_key) }}" + shell: bash + run: | + REPO="${{ github.repository }}" + ARTIFACT_URI="https://api.github.com/repos/$REPO/actions/artifacts" + TOKEN="${{ inputs.github_token }}" + RESPONSE=$(curl -H "Authorization: token $TOKEN" -s $ARTIFACT_URI | jq -r '.artifacts[]') + if [ "$RESPONSE" ] ; then + LATEST_ARTIFACT_URI=$(echo $RESPONSE | jq -r 'select(.name=="Terraform State") | .url' | sort -r | head -n 1) + if [ "$LATEST_ARTIFACT_URI" ] ; then + curl -L -H "Authorization: token $TOKEN" -o ${{ inputs.directory }}/terraform.tfstate.zip $LATEST_ARTIFACT_URI + unzip ${{ inputs.directory }}/terraform.tfstate.zip + fi + fi + + - name: Upload Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'upload' && isNull(inputs.encryption_key) }}" + uses: actions/upload-artifact@v3 + with: + name: Terraform State + path: "${{ inputs.directory }}/terraform.tfstate" + + # Encrypted Artifact + - name: Download Encrypted Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'download' && !isNull(inputs.encryption_key) }}" + shell: bash + run: | + REPO="${{ github.repository }}" + ARTIFACT_URI="https://api.github.com/repos/$REPO/actions/artifacts" + TOKEN="${{ inputs.github_token }}" + RESPONSE=$(curl -H "Authorization: token $TOKEN" -s $ARTIFACT_URI | jq -r '.artifacts[]') + if [ "$RESPONSE" ] ; then + LATEST_ARTIFACT_URI=$(echo $RESPONSE | jq -r 'select(.name=="Encrypted Terraform State") | .url' | sort -r | head -n 1) + if [ "$LATEST_ARTIFACT_URI" ] ; then + curl -L -H "Authorization: token $TOKEN" -o ${{ inputs.directory }}/terraform.tfstate.encrypted.zip $LATEST_ARTIFACT_URI + unzip ${{ inputs.directory }}/terraform.tfstate.encrypted.zip + fi + fi + + - name: Decrypt Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'download' && !isNull(inputs.encryption_key) }}" + shell: bash + run: | + openssl enc -d -aes256 -in ${{ inputs.directory }}/terraform.tfstate.encrypted -out ${{ inputs.directory }}/terraform.tfstate -k ${{ inputs.encryption_key }} + + - name: Encrypt Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'upload' && !isNull(inputs.encryption_key) }}" + shell: bash + run: | + openssl enc -e -aes256 -in ${{ inputs.directory }}/terraform.tfstate -out ${{ inputs.directory }}/terraform.tfstate.encrypted -k ${{ inputs.encryption_key }} + + - name: Upload Encrypted Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'upload' && !isNull(inputs.encryption_key) }}" + uses: actions/upload-artifact@v3 + with: + name: Encrypted Terraform State + path: "${{ inputs.directory }}/terraform.tfstate.encrypted" + + # Repository File + - name: Commit Repository File + if: "${{ inputs.location == 'repository' && inputs.operation == 'upload' && isNull(inputs.encryption_key) }}" + shell: bash + run: | + git add ${{ inputs.directory }}/terraform.tfstate + git commit -m "🏗️ Automatically Updated Terraform State." + git push + + # Encrypted Repository File + - name: Decrypt Repository File + if: "${{ inputs.location == 'repository' && inputs.operation == 'download' && !isNull(inputs.encryption_key) }}" + shell: bash + run: | + openssl enc -d -aes256 -in ${{ inputs.directory }}/terraform.tfstate.encrypted -out ${{ inputs.directory }}/terraform.tfstate -k ${{ inputs.encryption_key }} + + - name: Encrypt and Commit Repository File + if: "${{ inputs.location == 'repository' && inputs.operation == 'upload' && !isNull(inputs.encryption_key) }}" + shell: bash + run: | + openssl enc -e -aes256 -in ${{ inputs.directory }}/terraform.tfstate -out ${{ inputs.directory }}/terraform.tfstate.encrypted -k ${{ inputs.encryption_key }} + git add ${{ inputs.directory }}/terraform.tfstate.encrypted + git commit -m "🏗️ Automatically Updated Encrypted Terraform State." + git push diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..bca7886 --- /dev/null +++ b/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + version = "3.1.0" + } + } +} + +resource "random_id" "random" { + keepers = { + random_id = "${var.run_id}" + } + byte_length = 8 +} + +variable "run_id" { + type = string +}