Back to Blog
Lesson 57 of the CI/CD: Continuous Integration from Scratch course
DevOpsSeptember 17, 20264 min read

Cleaning Up Environments: Automated Staging Resource Management

Learn how to automate staging cleanup in your CI/CD pipeline. Master resource management and cleanup jobs to prevent orphaned cloud infrastructure.

ci/cdgithub actionsautomationdevopscleanupresource management
A woman kneeling while managing a robotic vacuum cleaner in a modern kitchen space.

Previously in this course, we covered how to test and validate our infrastructure code in testing-infrastructure-iac-validation-in-ci-cd-pipelines. Now, we need to address what happens after our deployment or pull request preview session concludes. Unmonitored staging infrastructure accumulates invisible cloud debt, driving up bills and leaving security blind spots.

This lesson teaches you how to implement cleanup, master resource management, and establish reliable automation for temporary environments using GitHub Actions.

The Problem of Orphaned Staging Resources

When developers spin up dynamic staging environments or pull request preview containers, resources are created on demand. Without a dedicated lifecycle policy, these preview deployments linger indefinitely. Much like managing physical infrastructure, ignoring digital hygiene leads to clutter—a challenge mirrored in broader workspace upkeep, as discussed when cleaning up resources: Kubernetes deletion & hygiene.

To prevent cloud waste, your CI/CD pipeline must treat resource destruction as a first-class citizen. Every create operation needs a corresponding, guaranteed teardown path.

Designing a Cleanup Job in GitHub Actions

A garbage collector works on a city street, managing waste collection at dusk.

To manage resource lifecycles effectively, we use GitHub Actions jobs configured with conditional execution or specialized event triggers (such as pull_request: [closed]).

Let's look at how to structure a workflow that provisions a temporary staging container or stack on pull request open, and tears it down when the pull request is merged or closed.

YAML
name: Staging Lifecycle

on:
  pull_request:
    types: [opened, synchronize, closed]

jobs:
  provision-or-cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Cloud CLI
        uses: azure/setup-kubectl@v3 # Or your cloud provider CLI

      - name: Deploy Staging Stack
        if: github.event.action != 'closed'
        run: |
          echo "Deploying preview environment for PR #${{ github.event.pull_request.number }}"
          # Add your provisioning commands here (e.g., terraform apply or docker compose up)

      - name: Teardown Staging Stack
        if: github.event.action == 'closed'
        run: |
          echo "Destroying preview environment for PR #${{ github.event.pull_request.number }}"
          # Add your teardown commands here (e.g., terraform destroy or docker compose down)

Ensuring Guaranteed Teardown with Always Steps

Relying solely on conditional event checks can fail if a provisioning step errors out midway, leaving orphaned assets behind. To achieve robust automation, you should separate your pipeline into distinct jobs or use the always() status check function in GitHub Actions.

Here is how you structure a cleanup step that runs regardless of whether preceding deployment steps succeeded or failed:

YAML
jobs:
  deploy-and-cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Provision Temporary Resources
        id: provision
        run: |
          # Generate a unique resource identifier based on the run ID
          echo "RESOURCE_ID=staging-${{ github.run_id }}" >> $GITHUB_ENV
          echo "Provisioning resource ${{ env.RESOURCE_ID }}..."
          # Mock creation command
          touch resource_lock.txt

      - name: Run Integration Tests
        run: |
          echo "Running tests against ${{ env.RESOURCE_ID }}..."

      - name: Cleanup Staging Resources
        if: always()
        run: |
          echo "Executing mandatory teardown for ${{ env.RESOURCE_ID }}..."
          if [ -f resource_lock.txt ]; then
            rm resource_lock.txt
            echo "Successfully destroyed resource."
          else
            echo "No resource found to clean up."
          fi

Hands-on Exercise: Build Your Cleanup Step

Time to apply this to our running course project. You will add a simulated cleanup script to your workflow.

  1. Open your existing GitHub Actions workflow YAML file in .github/workflows/.
  2. Add a new job or append a step to your existing deployment job that creates a temporary temporary text file or mock resource named temp_env_${{ github.run_id }}.tmp.
  3. Add a subsequent cleanup step utilizing if: always() that targets this file, printing a success message once removed.
  4. Commit your changes, push to GitHub, and verify in the Actions tab that the cleanup step executed successfully even after testing steps finished.

Common Pitfalls in Automated Cleanup

When implementing automated cleanup and resource management, watch out for these frequent traps:

  • Failing on Missing Resources: If a teardown script fails when an asset doesn't exist, your cleanup job will crash and block your pipeline status. Always write idempotent deletion commands (e.g., rm -f or checking existence before deleting).
  • Hardcoded Identifiers: Using static names for staging resources causes race conditions when multiple pull requests trigger workflows simultaneously. Always scope resources using unique run IDs or pull request numbers.
  • Ignoring Permissions: Cleanup jobs require appropriate cloud credentials or repository tokens. Ensure your cleanup steps inherit valid secrets and tokens even during failure states.

Frequently Asked Questions

What happens if the runner crashes before reaching the cleanup step?

If the runner itself is terminated abruptly (e.g., hardware failure or timeout), standard workflow steps won't run. For critical cloud infrastructure, always pair pipeline cleanup with cloud-native TTL (Time-To-Live) tags or serverless lifecycle policies.

Can I run a cleanup job on a schedule?

Yes. You can combine pull-request triggers with a schedule trigger (cron syntax) to sweep for abandoned resources daily or weekly.

How do I pass variables between deployment and cleanup jobs?

You can use workflow artifacts, environment files, or cloud-tagging strategies where the resource name is deterministically generated from the pull request number or commit SHA.

Recap

In this lesson, you learned how to maintain cost-effective, secure environments through disciplined resource management. By implementing conditional checks, always() execution guards, and automated teardown scripts, you ensure that temporary infrastructure never outlives its usefulness.

Up next, we will explore advanced debugging techniques to troubleshoot stubborn pipeline issues using interactive shells in advanced-debugging-techniques.

Similar Posts