Post

CI/CD Pipelines for Harbor to ECR Migration and Terraform Plan

Practical GitHub Actions examples for Harbor to ECR image migration and Terraform plan checks on pull requests.

CI/CD Pipelines for Harbor to ECR Migration and Terraform Plan

CI/CD Pipelines for Harbor to ECR Migration and Terraform Plan

I recently worked on two pipelines that solved real delivery problems:

  • Migrating container images from Harbor to AWS ECR
  • Running Terraform plan automatically on pull requests

Both pipelines were designed to be repeatable, auditable, and safe to run in shared environments.

Prerequisites

Before using these snippets, I set up the following:

  • IAM role for Harbor to ECR migration workflow: arn:aws:iam::<ACCOUNT_ID>:role/github-ecr-migration-role
  • IAM role for Terraform PR workflow: arn:aws:iam::<ACCOUNT_ID>:role/github-terraform-plan-role
  • GitHub Actions secrets: HARBOR_USERNAME, HARBOR_PASSWORD
  • images.txt at repo root with one repository path per line:
1
2
3
platform/api
platform/worker
shared/frontend
  • Terraform backend/state access already configured for the target environment

Pipeline 1: Harbor to ECR Migration

The objective was to move existing images from Harbor into ECR without rebuilding images or changing tags.

Approach

  1. Store source image list in a file (images.txt)
  2. Authenticate to Harbor and AWS using GitHub Actions secrets + OIDC
  3. Create ECR repositories if they do not exist
  4. Copy images directly registry-to-registry
  5. Keep logs for each migrated image

I used skopeo so the pipeline copies manifests/layers directly and keeps tags intact.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
name: harbor-to-ecr-migration

on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: "Tag to migrate"
        required: true
        default: "latest"

permissions:
  id-token: write
  contents: read

jobs:
  migrate:
    runs-on: ubuntu-latest
    env:
      AWS_REGION: us-east-1
      AWS_ACCOUNT_ID: "123456789012"
      HARBOR_REGISTRY: harbor.example.com
      ECR_REGISTRY: 123456789012.dkr.ecr.us-east-1.amazonaws.com
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-ecr-migration-role
          aws-region: $

      - name: Install skopeo
        run: sudo apt-get update && sudo apt-get install -y skopeo

      - name: Login to Harbor
        run: echo "$" | skopeo login $HARBOR_REGISTRY -u "$" --password-stdin

      - name: Login to ECR
        run: aws ecr get-login-password --region $AWS_REGION | skopeo login $ECR_REGISTRY -u AWS --password-stdin

      - name: Copy images Harbor -> ECR
        run: |
          TAG="$"
          while read -r IMAGE; do
            [ -z "$IMAGE" ] && continue
            aws ecr describe-repositories --repository-names "$IMAGE" >/dev/null 2>&1 || \
              aws ecr create-repository --repository-name "$IMAGE" >/dev/null

            skopeo copy --all \
              docker://$HARBOR_REGISTRY/$IMAGE:$TAG \
              docker://$ECR_REGISTRY/$IMAGE:$TAG
            echo "Migrated $IMAGE:$TAG"
          done < images.txt

Pipeline 2: Terraform Plan on Pull Requests

This pipeline gives quick feedback before merge and keeps infrastructure review in the PR workflow.

What it does

  1. Runs on pull requests
  2. Executes fmt, init, validate, and plan
  3. Uploads tfplan output as an artifact
  4. Fails the PR if validation or plan fails
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: terraform-plan

on:
  pull_request:
    paths:
      - "terraform/**"

permissions:
  id-token: write
  contents: read

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform/envs/dev
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-terraform-plan-role
          aws-region: us-east-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.5

      - name: Terraform fmt
        run: terraform fmt -check -recursive

      - name: Terraform init
        run: terraform init -input=false

      - name: Terraform validate
        run: terraform validate

      - name: Terraform plan
        run: terraform plan -input=false -no-color -out=tfplan

      - name: Save plan output
        run: terraform show -no-color tfplan > tfplan.txt

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: terraform-plan-dev
          path: terraform/envs/dev/tfplan.txt

What I Learned

  • OIDC is critical for both pipelines so we avoid long-lived credentials
  • Migration jobs should be manually triggered and logged carefully
  • Terraform plan in PRs improves quality before apply
  • Keep production apply as a separate workflow with approvals

These two pipelines made migrations safer and IaC reviews faster for the team.

This post is licensed under CC BY 4.0 by the author.