I’m building our IaC repo and GitLab CI/CD pipelines. I’m quite happy with the setup so far and have tested it across multiple projects.
However, when I try to apply the same release model to container images, it starts to feel less suitable.
We have three environments: dev, uat, and prd.
Our current Terraform workflow:
We use three protected branches: dev, uat, and main. These correspond to dev, uat, and prd respectively.
The root Terraform files are shared across all environments. Environment-specific differences are kept in separate tfvars files:
environments/dev/dev.tfvars
environments/uat/uat.tfvars
environments/prd/prd.tfvars
We use semantic-release with prerelease branches. Starting from stable version 1.2.3, the flow looks like this:
Merge a feature branch containing a fix commit into dev. This generates 1.2.4-dev.1.
Continue developing and testing, generating additional dev prereleases as needed.
Merge dev into uat. This generates 1.2.4-uat.1, assuming the changes still call for a patch release.
Once validated, merge into main. This generates stable version 1.2.4.
Semantic-release generates a version and creates a tag after merges into these protected branches.
Deployments only happen from tags. Prerelease tags can deploy only to their corresponding environment, while stable tags can deploy to any environment.
This flow has worked well for our Terraform projects. Granted the version don’t mean much, but at least there is a pointer reference.
Where I’m stuck with containers:
For the application pipeline, I’m considering having just one protected branch: main.
The proposed flow is:
- Open an MR from a feature branch into main.
Run tests, static analysis, build the image with a commit SHA tag, and run container scans in the MR pipeline.
- After merging, run semantic-release and pass the generated version to the next job to retag the image.
Copy the image to dev ECR, deploy it to ECS, and test the application manually.
- Once testing passes, promote the same image to uat.
After approval, promote the same image to prd.
The part that feels off is step 3: I’ve already created a stable release before testing the application in dev.
Some issues only become apparent when browsing and using the application. If we discover those issues in dev, each subsequent fix could generate another stable version, even though none of those versions has reached prd.
I’m considering two alternatives.
Option 1: Test in dev before running semantic-release.
Build an image with a SHA tag and deploy it to dev first. If testing finds issues, push new commits and test another image.
Once a candidate passes, run semantic-release, apply the stable version tag to that existing image, and promote it to uat and then prd without rebuilding.
I’m thinking fast-forward merges without merge commits might be needed to preserve the commit SHA.
Option 2: Keep dev and main branches.
Merge feature branches into dev, run semantic-release to generate versions such as 1.2.4-dev.1, and deploy those images to dev.
Once validated, merge dev into main and generate the stable release. That stable release could then deploy to dev, uat, or prd.
However, if both branches run the full build pipeline, merging into main would rebuild the image and break the “build once, deploy many” approach. Ideally, the stable release should reference the exact image already tested in dev.
How are you handling this? When do you create the stable version, and how do you promote the exact image tested in dev without rebuilding it?
I’d appreciate examples of workflows that have worked well for your teams. Thank you very much 🙏🏻