diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml new file mode 100644 index 0000000..e781825 --- /dev/null +++ b/.github/workflows/artifact.yml @@ -0,0 +1,42 @@ +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@main + with: + operation: upload + location: artifact + 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@main + with: + operation: upload + location: artifact diff --git a/.github/workflows/artifact_encrypted.yml b/.github/workflows/artifact_encrypted.yml new file mode 100644 index 0000000..47afae2 --- /dev/null +++ b/.github/workflows/artifact_encrypted.yml @@ -0,0 +1,44 @@ +name: Artifact Encrypted +on: + workflow_dispatch: + push: + paths-ignore: + - "**.tfstate" + - "**.tfstate.encrypted" + +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@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: artifact + 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@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: artifact diff --git a/.github/workflows/repository_file.yml b/.github/workflows/repository_file.yml new file mode 100644 index 0000000..313440c --- /dev/null +++ b/.github/workflows/repository_file.yml @@ -0,0 +1,42 @@ +name: Repository File +on: + workflow_dispatch: + push: + paths-ignore: + - "**.tfstate" + - "**.tfstate.encrypted" + +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@main + 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..1c94416 --- /dev/null +++ b/.github/workflows/repository_file_encrypted.yml @@ -0,0 +1,51 @@ +name: Repository File Encrypted +on: + workflow_dispatch: + push: + paths-ignore: + - "**.tfstate" + - "**.tfstate.encrypted" + +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@main + 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@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: repository diff --git a/README.md b/README.md index e0a1952..14817b1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,122 @@ -# 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 files as (optionally) encrypted artifacts or repository files. This tool makes it easier for you to handle your state files securely and efficiently within GitHub. + +## 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. | 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 Terraform state files. + +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@main + with: + operation: upload + location: artifact + continue-on-error: true + +- name: Upload Artifact + uses: badgerhobbs/terraform-state@main + with: + operation: upload + location: artifact +``` + +#### Artifact Encrypted + +Please see thie [Example Workflow](.github/workflows/artifact_encrypted.yml). + +```yml +- name: Download Encrypted Artifact & Decrypt Artifact + uses: badgerhobbs/terraform-state@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: artifact + continue-on-error: true + +- name: Encrypt Artifact & Upload Encrypted Artifact + uses: badgerhobbs/terraform-state@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: upload + location: artifact +``` + +#### Repository File + +Please see thie [Example Workflow](.github/workflows/repository_file.yml). + +```yml +- name: Commit Repository File + uses: badgerhobbs/terraform-state@main + 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@main + with: + encryption_key: ${{ secrets.encryption_key }} + operation: download + location: repository + continue-on-error: true + +- name: Encrypt and Commit Repository File + uses: badgerhobbs/terraform-state@main + 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..27c9eff --- /dev/null +++ b/action.yml @@ -0,0 +1,124 @@ +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." + required: false + +runs: + using: "composite" + steps: + + - name: Configure Git User + shell: bash + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@users.noreply.github.com" + + # Artifact + - name: Download Artifact + if: "${{ inputs.location == 'artifact' && inputs.operation == 'download' && 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) + echo "Most recent artifact URI = $LATEST_ARTIFACT_URI" + 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' && 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' && 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) + echo "Most recent artifact URI = $LATEST_ARTIFACT_URI" + 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' && 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' && 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' && 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' && inputs.encryption_key == '' }}" + shell: bash + run: | + git add ${{ inputs.directory }}/terraform.tfstate + git commit -m "🏗️ Automatically Updated Terraform State." + git pull --rebase + git push + + # Encrypted Repository File + - name: Decrypt Repository File + if: "${{ inputs.location == 'repository' && inputs.operation == 'download' && 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' && 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 pull --rebase + 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 +}