Example Dockerized application deployed on ECS.
- Docker for developing, shipping, and running,
- ECR to store the Docker images
- ECS as the container orchestration platform
A comprehensive ECS deployment system using GitHub Actions, Ecspresso, and Terraform for automated application deployment across multiple environments.
This system provides a complete CI/CD pipeline for deploying containerized applications to Amazon ECS using:
- GitHub Actions: Automated workflows for building, testing, and deploying applications
- Ecspresso: ECS deployment tool
- Terraform/Atmos: Infrastructure as Code for ECS services and supporting resources
Please note the examples here can be named anything, they just would need to be configured differently.
βββ app-on-ecs/ # Sample application
β βββ .github/workflows/ # GitHub Actions workflows
β βββ deploy/taskdef.json # Partial task definition template
βββ infra-config/ # Infrastructure configuration
β βββ .github/environments/ # Environment-specific configurations
βββ infrastructure/ # Live infrastructure definitions
βββ stacks/ # Terraform/Atmos stack configurations
We've consolidated all the workflows, including the shared/reusable GitHub reusable workflows. We've done this to make it easier for Developers to understand how the example leverages all the workflows. In practice, we recommend moving the reusable workflows into a centralized repository, where they can be shared by other application repositories. For example, from this example, we would recommend moving all the ecspresso-* and all workflow-* workflow files to a centralized repository (e.g. a repository named github-action-workflows). The best solution will depend on your GitHub organization structure and team size. Pick what works for you and your team.
When your workflows are consolidated, you will need only 3 inside this repository:
- feature-branch.yml
- main-branch.yml
- release.yml
The remaining workflows are the reusable/shared implementation. This approach makes it easier to define a standardized CI/CD interface for all of your services.
.github
βββ configs/
β βββ draft-release.yml
β βββ environment.yaml
βββ workflows/
βββ ecspresso-feature-branch.yml
βββ ecspresso-hotfix-branch.yml
βββ ecspresso-hotfix-mixin.yml
βββ ecspresso-hotfix-release.yml
βββ ecspresso-main-branch.yml
βββ ecspresso-release.yml
βββ feature-branch.yml
βββ main-branch.yaml
βββ release.yaml
βββ workflow-cd-ecspresso.yml
βββ workflow-cd-preview-ecspresso.yml
βββ workflow-ci-dockerized-app-build.yml
βββ workflow-ci-dockerized-app-promote.yml
βββ workflow-controller-draft-release.yml
βββ workflow-controller-hotfix-reintegrate.yml
βββ workflow-controller-hotfix-release-branch.yml
βββ workflow-controller-hotfix-release.ymlThe system includes three main deployment workflows:
- Trigger: Push to
mainbranch - Environment:
dev - Actions:
- Build and test Docker image
- Deploy to
devenvironment - Create draft release
- Trigger: Published GitHub release
- Environment:
staging&production - Actions:
- Promote (tag) release commit sha Docker tag to release version
- Deploy tagged version to
staging&production - Use release tag as image version
- Trigger: Pull requests to
release/**branches - Environment:
qa1orqa2(based on labels) - Actions:
- Deploy to QA environments for testing
- Support rollback on failure
All workflows use the use-partial-taskdefinition: true parameter, enabling the partial task definition deployment approach.
Partial task definitions are minimal JSON templates that contain only the essential container configuration needed for deployment. Instead of maintaining complete ECS task definitions, you define only the core container properties. This allows the app developers to configure environment variables, task settings (such as memory and CPU usage), and other configuration within the app repository.
The minimum required setup is to have the image defined by the environment variable IMAGE, which is set by GitHub Actions.
deploy/taskdef.json:
{
"containerDefinitions": [
{
"name": "app",
"image": "{{ must_env `IMAGE` }}"
}
]
}- Template Processing: Ecspresso processes the partial task definition template
- Environment Variable Substitution: Variables like
{{ must_env `IMAGE` }}are replaced with actual values - Infrastructure Integration: The ECS service configuration from Terraform provides:
- Task CPU and memory settings
- Network configuration
- IAM roles
- Load balancer settings
- Environment variables
- Simplified Maintenance: The app developers can configure environment variables, and other settings, while things like volume mounts, security groups, and other infrastructure settings are managed by Terraform.
- Infrastructure Separation: Infrastructure settings managed via Terraform
- Consistency: Standardized deployment patterns across applications
In your application repository, you define the action location for environments. In this example application .github/configs/environment.yaml is used.
# assumes the same organization
environment-info-org: cloudposse
environment-info-repo: infra-config
implementation_path: .github/environments
implementation_file: ecspresso.yml
implementation_ref: mainThis tells GitHub Actions where to find the environment configuration action.
Then the infra-config/.github/environments/ecspresso.yml file defines environment-specific settings as a composite action.
Show file contents
name: "Environments"
description: "Get information about cluster"
inputs:
environment:
description: "Environment name"
required: true
namespace:
description: "Namespace name"
required: true
repository:
description: "Repository name"
required: false
application:
description: "Application name"
required: false
attributes:
description: "Comma separated attributes"
required: false
outputs:
name:
description: "Environment name"
value: ${{ steps.result.outputs.name }}
region:
description: "AWS Region"
value: ${{ steps.result.outputs.region }}
role:
description: "IAM Role"
value: ${{ steps.result.outputs.role }}
cluster:
description: "Cluster"
value: ${{ steps.result.outputs.cluster }}
namespace:
description: "Namespace"
value: ${{ steps.result.outputs.namespace }}
ssm-path:
description: "SSM path"
value: ${{ steps.result.outputs.ssm-path }}
s3-bucket:
description: "S3 Bucket name"
value: ${{ steps.result.outputs.s3-bucket }}
account-id:
description: "AWS account id"
value: ${{ steps.result.outputs.aws-account-id }}
stage:
description: "Stage name"
value: ${{ steps.result.outputs.stage }}
runs:
using: "composite"
steps:
- uses: cloudposse/[email protected]
id: region
with:
region: us-east-2
format: "fixed"
- uses: cloudposse/[email protected]
id: name
with:
query: .${{ inputs.application == '' }}
config: |
true:
name: ${{ inputs.repository }}
false:
name: ${{ inputs.application }}
- uses: cloudposse/[email protected]
id: result
with:
query: .${{ inputs.environment }}
config: |
qa1:
cluster: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}-qa1
role: arn:aws:iam::QA_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}-qa1
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2
qa2:
cluster: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}-qa2
role: arn:aws:iam::QA_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}-qa2
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2
dev:
cluster: acme-plat-${{ steps.region.outputs.result }}-dev-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-dev-${{ steps.name.outputs.name }}
role: arn:aws:iam::DEV_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-dev-${{ steps.name.outputs.name }}
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2
production:
cluster: acme-plat-${{ steps.region.outputs.result }}-prod-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-prod-${{ steps.name.outputs.name }}
role: arn:aws:iam::PROD_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-prod-${{ steps.name.outputs.name }}
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2
sandbox:
cluster: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}
role: arn:aws:iam::SANDBOX_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-sandbox-${{ steps.name.outputs.name }}
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2
staging:
cluster: acme-plat-${{ steps.region.outputs.result }}-staging-ecs-platform
name: acme-plat-${{ steps.region.outputs.result }}-staging-${{ steps.name.outputs.name }}
role: arn:aws:iam::STAGING_ACCOUNT_ID:role/acme-plat-${{ steps.region.outputs.result }}-staging-${{ steps.name.outputs.name }}
ssm-path: /ecs-service/${{ steps.name.outputs.name }}/url/0
s3-bucket: acme-plat-${{ steps.region.outputs.result }}-sandbox-ecs-tasks-mirror
region: us-east-2dev: Development environmentqa1/qa2: "Preview" environmentsstaging: Staging environmentproduction: Production environmentsandbox: Sandbox environment
components:
terraform:
ecs-service/example-app-on-ecs:
metadata:
component: ecs-service
inherits:
- ecs-services/defaults
vars:
name: example-app-on-ecs
github_actions_ecspresso_enabled: true
github_actions_allowed_repos:
- cloudposse/example-app-on-ecs
containers:
service:
name: app
image: MY_ECR_ACCOUNT.dkr.ecr.MY_ECR_REGION.amazonaws.com/MY_ECR_REPOSITORY:latest
port_mappings:
- containerPort: 8080
hostPort: 8080
protocol: tcp
task:
desired_count: 1
task_memory: 512
task_cpu: 256Provides base configuration inherited by all ECS services:
components:
terraform:
ecs-services/defaults:
vars:
enabled: true
use_lb: true
task:
launch_type: FARGATE
deployment_controller_type: ECS
network_mode: awsvpc
desired_count: 1
task_memory: 2048
task_cpu: 1024Defines preview environment specific service instances:
components:
terraform:
ecs-service/example-app-on-ecs/qa1:
metadata:
component: ecs-service
inherits:
- ecs-service/example-app-on-ecs
vars:
enabled: true
attributes: [qa1]
ecs-service/example-app-on-ecs/qa2:
metadata:
component: ecs-service
inherits:
- ecs-service/example-app-on-ecs
vars:
enabled: true
attributes: [qa2]- AWS Account with ECS cluster deployed
- ECR repository for container images
- GitHub repository with required secrets configured
secrets:
github-private-actions-pat: "${{ secrets.PUBLIC_AND_PRIVATE_REPO_ACCESS_TOKEN_REPO }}" # GitHub PAT for private repos
registry: "${{ secrets.ECR_REGISTRY }}" # Your ECR registry URL
secret-outputs-passphrase: "${{ secrets.GHA_SECRET_OUTPUT_PASSPHRASE }}" # Passphrase for secret outputs
ecr-region: "${{ secrets.ECR_REGION }}" # AWS region (e.g., us-east-2)
ecr-iam-role: "${{ secrets.ECR_IAM_ROLE }}" # IAM role ARN for ECR access- Configure your ECS service in
infra-live/stacks/catalog/ecs-service/:
components:
terraform:
ecs-service/your-app:
vars:
name: your-app
github_actions_ecspresso_enabled: true
github_actions_allowed_repos:
- your-org/your-repo- Deploy the infrastructure using Terraform/Atmos:
atmos terraform apply ecs-service/your-app -s your-environment- Create partial task definition at
deploy/taskdef.json:
{
"containerDefinitions": [
{
"name": "app",
"image": "{{ must_env `IMAGE` }}"
}
]
}-
Add GitHub workflows (copy from
app-on-ecs/.github/workflows/) -
Update workflow configuration to match your application name and requirements
- Label PR with
deploy/qa1ordeploy/qa2: Triggers deployment to QA environments - Push to main branch: Triggers build and deployment to
devenvironment - Create release: Triggers deployment to
productionenvironment - Create hotfix PR: Triggers deployment to QA environments
graph LR
A[Push to main] --> B[Build Image]
B --> C[Deploy to dev]
C --> D[Create Draft Release]
E[Publish Release] --> F[Deploy to production]
G[Hotfix PR] --> H[Deploy to qa1/qa2]
- GitHub Actions build and push Docker images to ECR
- Environment Configuration provides deployment targets and credentials
- Ecspresso deploys using partial task definitions combined with Terraform-managed infrastructure
- Terraform/Atmos manages ECS services, load balancers, and supporting AWS resources
- CI Phase: Build and test Docker image
- Environment Resolution: Determine target environment and configuration
- AWS Authentication: Assume deployment role for target environment
- Task Definition Processing: Merge partial definition with infrastructure settings
- ECS Deployment: Deploy using Ecspresso with rollback support
- Health Checks: Verify deployment success and service health
- Deployment Failures: Check IAM roles and permissions for the target environment
- Ensure the roles and cluster names are correct
- Image Pull Errors: Verify ECR repository permissions and image tags
Check out these related projects.
- github-actions-workflows - Reusable workflows for different types of projects
This project is under active development, and we encourage contributions from our community.
Many thanks to our outstanding contributors:
For π bug reports & feature requests, please use the issue tracker.
In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow.
- Review our Code of Conduct and Contributor Guidelines.
- Fork the repo on GitHub
- Clone the project to your own machine
- Commit changes to your own branch
- Push your work back up to your fork
- Submit a Pull Request so that we can review your changes
NOTE: Be sure to merge the latest changes from "upstream" before making a pull request!
Join our Open Source Community on Slack. It's FREE for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally sweet infrastructure.
Sign up for our newsletter and join 3,000+ DevOps engineers, CTOs, and founders who get insider access to the latest DevOps trends, so you can always stay in the know. Dropped straight into your Inbox every week β and usually a 5-minute read.
Join us every Wednesday via Zoom for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a live Q&A that you canβt find anywhere else. It's FREE for everyone!
Preamble to the Apache License, Version 2.0
Complete license is available in the LICENSE file.
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
All other trademarks referenced herein are the property of their respective owners.
Copyright Β© 2017-2025 Cloud Posse, LLC
