Workflow
June 3, 2026 · View on GitHub
After the above preparation, we can begin setting up the infrastructure.
It is recommended to create the infrastructure in stages:
- ECR
- StateManager
- GitHub OIDC
- ECS Cluster
- ALB
- ECS Service
- Task Definition
Amazon ECR
ECR will be used as the central registry for container images built by the CI/CD pipeline.
All application images will be pushed to this registry and later pulled by the runtime environment (for example ECS, EKS, or other compute services).
You can initialize ECR first:
terraform apply -target='module.ecr.aws_ecr_repository.prod_forge_repo'
The "Chicken-and-Egg" Dilemma
In many organizations, the DevOps and backend teams work independently and often in parallel. This can create a common problem during the early stages of a project:
How can infrastructure be configured if the backend service is not yet ready for deployment?
To reduce the dependency between these teams, we introduce a simple solution: a temporary bootstrap application.
Initial Application Image
Inside the repository there is a folder called:
initial-image
This directory contains a minimal backend application used solely for bootstrapping the infrastructure.
The application should:
- use the same runtime as the real service
- be simple enough to build immediately
- be deployable to the infrastructure
For example, since the production backend uses Node.js, the bootstrap service uses Express as a minimal framework.
The goal of this service is not to implement business logic, but simply to provide a deployable container image that allows infrastructure components to be tested.
We can build an initial image locally for initial work and upload it to ECR:
aws ecr get-login-password --region eu-central-1 \
| docker login \
--username AWS \
--password-stdin <USER_ID>.dkr.ecr.eu-central-1.amazonaws.com
docker buildx build \
--platform linux/amd64 \
-t <ECR_URL>:<version> \
--push .
Required Endpoints
The bootstrap service exposes a small set of endpoints.
- /health - This endpoint is required. When deploying services to AWS ECS, a health check must be configured. ECS periodically calls this endpoint to verify that the container is running correctly. The endpoint simply returns:
200 OK
This indicates that the service is alive and healthy.
-
/version - This endpoint is optional but highly recommended. It returns the version of the currently running container. This makes it easy to:
- verify which version is deployed
- confirm that a deployment succeeded
- detect situations where old containers are still running during rolling updates
AWS Secrets Manager
Sensitive configuration values such as:
- API keys
- database credentials
- third-party service tokens
- application secrets
should never be stored in the repository or Terraform variables.
Instead, they should be stored in AWS Secrets Manager, which provides secure storage, access control, and auditing.
If any secrets are already known at this stage of the project, they can be added to Secrets Manager immediately so that they are available for the infrastructure and application during deployment.
terraform apply -target='module.secrets_manager.aws_secretsmanager_secret.app_secret'
Store secrets using the following command:
aws secretsmanager put-secret-value \
--secret-id prod/app/config \
--secret-string '{"SOME_SECRET":"secret"}'
Retrieve a secret value:
aws secretsmanager get-secret-value \
--secret-id prod/app/config \
--query SecretString \
--output text
aws secretsmanager get-secret-value \
--secret-id prod/app/config \
--query SecretString \
--output text | jq -r '.SOME_SECRET'
GitHub Integration for CI/CD
Once Secrets Manager and ECR are configured, the next step is to connect the GitHub repository to the CI/CD pipeline.
This allows the pipeline to:
- build Docker images
- push images to ECR
- trigger deployment processes automatically
Approaches
There are two main ways to grant the CI/CD pipeline access to AWS:
1. Dedicated AWS User
Create a dedicated IAM user specifically for CI/CD tasks.
Then configure:
- ACCESS_KEY_ID
- SECRET_ACCESS_KEY
as GitHub Secrets in the repository settings.
All actions in the CI/CD pipeline will run under this user’s permissions.
This approach works but requires careful management of IAM keys. Rotating secrets and enforcing least-privilege policies can be cumbersome.
2. OpenID Connect (OIDC) - Recommended
A more secure and modern approach is to configure OIDC between GitHub and AWS.
- The repository itself is trusted to assume roles in AWS
- No IAM user or long-lived keys are needed
- Permissions are granted per repository, making the workflow cleaner and safer
With this setup, the GitHub Actions workflow can assume a role that has privileges such as pushing images to ECR and deploying infrastructure.
Terraform Setup
To create the OIDC provider via Terraform, run:
terraform apply -target=module.github_oidc
This step ensures that:
- GitHub Actions can authenticate to AWS securely
- Only authorized workflows can perform deployments
- The CI/CD workflow is fully automated and auditable
Why OIDC is Recommended
- No hard-coded credentials in GitHub secrets
- Least privilege - only the repository can assume the role
- Improved security - reduces risk of compromised keys
- Cleaner workflow - easier to manage and audit access
ECS Cluster
After applying the Terraform configuration, a new ECS Cluster should appear in the AWS Console.
You can verify this by navigating to:
AWS Console → ECS → Clusters
The cluster will be used to run our backend services using AWS Fargate.
Application Load Balancer (ALB)
Once the Application Load Balancer is created, you can verify it using the AWS CLI:
aws elbv2 describe-load-balancers --region eu-central-1
The output should contain a DNS name for the load balancer.
Example:
DNSName: xyz123.eu-central-1.elb.amazonaws.com
At this stage, if you open the DNS address in your browser, the response will likely be:
503 Service Unavailable
This is expected because the ECS service may not yet have a healthy running task attached to the load balancer.
ECS Service
The ECS service is responsible for running and maintaining the desired number of application tasks.
You can inspect the service using the following commands.
List services in the cluster:
aws ecs list-services \
--cluster <CLUSTER_NAME> \
--region eu-central-1
Describe the service in detail:
aws ecs describe-services \
--cluster <CLUSTER_NAME> \
--services <SERVICE_NAME> \
--region eu-central-1
This will show information such as:
- running tasks
- desired task count
- deployment status
- load balancer configuration
Task Definition
A Task Definition describes how our backend application should run inside ECS.
When using AWS Fargate, we define the compute resources allocated to the container.
Example configuration:
cpu = "256"
memory = "512"
This defines the virtual machine resources that will be allocated to the containerized service.
The task definition also specifies the Docker image that will be deployed.
You can see logs from the container:
aws logs describe-log-groups
Find your log group (for example, /ecs/prod-forge-todolist):
aws logs tail <LOG_GROUP> --since 10h
One-Run ECS Tasks
Certain operations should not run as permanent services.
Examples include:
- database migrations
- seed scripts
- maintenance jobs
For these cases we use one-run ECS tasks.
Inside the backend repository there is a directory:
ecs/
This directory contains a task template used to run migration jobs.
These tasks are not created at the infrastructure level because they are ephemeral by nature.
Instead they are:
- created during the release pipeline
- executed once
- removed after completion
This approach keeps infrastructure clean while still allowing safe execution of operational tasks.
Typical workflow:
CI/CD → run migration task → apply schema changes → deploy new application version
Because database migrations follow a forward-only strategy, running them before application deployment is safe.