Welcome to Chapter 13! So far, we’ve meticulously built a robust, production-ready Node.js application, complete with a well-structured codebase, comprehensive testing, secure authentication, and a Dockerized environment. In the previous chapter, we finalized our Docker setup, ensuring our application can be consistently built and run across different environments. Now, it’s time to automate the process of getting our code from development to a deployable artifact.
This chapter will guide you through setting up a Continuous Integration/Continuous Deployment (CI/CD) pipeline using GitHub Actions. Our primary goal is to automate the building of our Docker image, running tests, and pushing the resulting image to AWS Elastic Container Registry (ECR). This automation is crucial for modern development workflows, enabling faster, more reliable, and consistent deployments. By the end of this chapter, any code pushed to our main branch will automatically trigger a workflow that validates our code, builds a new Docker image, and makes it available in ECR, ready for deployment.
Planning & Design
A well-designed CI/CD pipeline is the backbone of efficient software delivery. For our Node.js application, we’ll design a pipeline that integrates seamlessly with GitHub and AWS.
Component Architecture
The following diagram illustrates the workflow we’ll establish:
Explanation:
- Developer Pushes Code: Any
pushevent to ourmainbranch (or a pull request merge) will initiate the CI/CD workflow. - GitHub Repository: Our source code repository, hosted on GitHub.
- GitHub Actions: GitHub’s native CI/CD service. It orchestrates the entire pipeline based on our defined workflow file.
- Checkout Code: The workflow starts by fetching our latest code from the repository.
- Build & Test Node.js App: This step involves installing Node.js dependencies, running linting checks, and executing our unit and integration tests. This ensures code quality and correctness before proceeding.
- Docker Build: If tests pass, a Docker image of our application is built using the
Dockerfilewe prepared in the previous chapter. - Authenticate to AWS ECR: GitHub Actions will use AWS credentials (stored securely as GitHub Secrets) to authenticate with AWS ECR.
- Push Docker Image: The newly built and tagged Docker image is pushed to our dedicated ECR repository.
- AWS Elastic Container Registry (ECR): A fully managed Docker container registry that makes it easy to store, manage, and deploy Docker container images.
- Image Ready for Deployment: Once in ECR, the Docker image is ready to be pulled by services like AWS ECS (which we’ll cover in the next chapter) for deployment.
File Structure
We will introduce a new directory and file for our GitHub Actions workflow:
.
├── .github/
│ └── workflows/
│ └── main.yml # Our CI/CD workflow definition
├── src/
├── tests/
├── Dockerfile
├── package.json
└── ...
Step-by-Step Implementation
Let’s get started with setting up our CI/CD pipeline.
1. Setup/Configuration
Before we write our GitHub Actions workflow, we need to prepare our AWS environment and secure our credentials.
a) Create an AWS ECR Repository
We need a place in AWS to store our Docker images. ECR provides this functionality.
- Log in to AWS Console: Go to https://aws.amazon.com/console/ and log in.
- Navigate to ECR: Search for “ECR” in the services search bar and select “Elastic Container Registry”.
- Create Repository:
- Click on “Create repository”.
- Visibility settings: Choose “Private”.
- Repository name: Enter a descriptive name, e.g.,
nodejs-api-repo. - Leave other settings as default for now.
- Click “Create repository”.
Keep note of your Repository URI (e.g., 123456789012.dkr.ecr.your-region.amazonaws.com/nodejs-api-repo). We’ll need this later.
b) Create an AWS IAM User for GitHub Actions
For GitHub Actions to interact with AWS ECR, it needs secure credentials. We’ll create a dedicated IAM user with minimal necessary permissions following the principle of least privilege.
- Navigate to IAM: Search for “IAM” in the AWS console and select “Identity and Access Management”.
- Create User:
- Go to “Users” in the left navigation pane and click “Create user”.
- User name: Enter
github-actions-user. - AWS credential type: Check “Access key - Programmatic access”.
- Click “Next”.
- Set Permissions:
- Select “Attach policies directly”.
- Search for and select the policy
AmazonECRContainerRegistryPowerUser. This policy grants sufficient permissions to push and pull images from ECR. For production, you might create a custom policy with even tighter restrictions, but this is a good starting point. - Click “Next”.
- Review and Create: Review the user details and click “Create user”.
- Store Credentials: Crucially, copy the
Access key IDandSecret access keyimmediately. These will only be shown once. If you lose them, you’ll have to create new credentials.
c) Configure GitHub Repository Secrets
To secure our AWS credentials, we will store them as secrets in our GitHub repository. This prevents them from being exposed in our workflow files or logs.
- Navigate to GitHub Repository Settings: Go to your project’s GitHub repository.
- Go to Secrets: Click on “Settings” -> “Secrets and variables” -> “Actions”.
- Add Repository Secrets:
- Click “New repository secret”.
- Name:
AWS_ACCESS_KEY_ID - Secret: Paste the Access key ID you copied from AWS.
- Click “Add secret”.
- Repeat for the secret key:
- Name:
AWS_SECRET_ACCESS_KEY - Secret: Paste the Secret access key you copied from AWS.
- Click “Add secret”.
- Name:
- Add one more for the AWS region:
- Name:
AWS_REGION - Secret: Enter your AWS region, e.g.,
us-east-1,eu-west-2. - Click “Add secret”.
- Name:
2. Core Implementation
Now that our environment is configured, let’s create the GitHub Actions workflow file.
a) Create GitHub Actions Workflow File
Create the directory .github/workflows at the root of your project, and then create a file named main.yml inside it.
File: .github/workflows/main.yml
name: CI/CD Pipeline to AWS ECR
on:
push:
branches:
- main # Trigger on push to the main branch
pull_request:
branches:
- main # Optionally trigger on pull requests to main for early feedback
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
ECR_REPOSITORY: nodejs-api-repo # Replace with your ECR repository name
IMAGE_TAG: ${{ github.sha }} # Use commit SHA as image tag for uniqueness
jobs:
build-and-push:
runs-on: ubuntu-latest # The type of runner that the job will run on
steps:
- name: Checkout repository
uses: actions/checkout@v4 # Action to check out your repository code
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Ensure this matches your project's Node.js version
- name: Install dependencies
run: npm ci # Use npm ci for clean installs in CI environments
- name: Run tests
run: npm test # Execute your project's test suite
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and tag Docker image
run: |
docker build -t ${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} .
docker tag ${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
# The 'latest' tag is useful for development/staging, but for production,
# using the commit SHA or a semantic version tag is highly recommended
# to ensure immutability and easy rollback.
- name: Push Docker image to ECR
run: |
docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
Explanation of the Workflow File:
name: A descriptive name for your workflow.on: Defines when the workflow should run. Here, it triggers onpushandpull_requestevents to themainbranch.env: Defines environment variables available to all jobs and steps in the workflow.AWS_REGION: Pulled from GitHub secrets.ECR_REPOSITORY: Your ECR repository name.IMAGE_TAG: We’re usinggithub.shawhich is the full SHA of the commit that triggered the workflow. This creates a unique and traceable tag for each image. We also taglatestfor convenience.
jobs: A workflow run is made up of one or more jobs.build-and-push: The name of our single job.runs-on: ubuntu-latest: Specifies the virtual environment where the job will execute.steps: A sequence of tasks that are executed as part of the job.Checkout repository: Usesactions/checkout@v4to clone your repository into the runner.Setup Node.js: Usesactions/setup-node@v4to install the specified Node.js version.Install dependencies: Runsnpm cito install project dependencies.npm ciis preferred overnpm installin CI environments for faster, more reliable, and reproducible builds as it usespackage-lock.json.Run tests: Executesnpm test. This is a critical step for CI. If tests fail, the workflow stops, preventing a broken image from being pushed.Configure AWS credentials: Usesaws-actions/configure-aws-credentials@v4to configure the AWS CLI with our secret credentials.Login to Amazon ECR: Usesaws-actions/amazon-ecr-login@v2to authenticate Docker with our ECR registry. Theid: login-ecrallows us to reference its output (like the registry URI) in subsequent steps.Build and tag Docker image:docker build -t ...: Builds the Docker image using ourDockerfilein the current directory (.). It tags the image with the repository name and the unique commit SHA.docker tag ...: Creates an additionallatesttag for convenience, pointing to the same image. In a production setup, you would typically use more specific version tags or only the SHA for immutable deployments.
Push Docker image to ECR: Pushes both the SHA-tagged andlatest-tagged images to ECR.
3. Testing This Component
To test our CI/CD pipeline, you simply need to push a change to your main branch.
Commit your changes:
git add .github/workflows/main.yml git commit -m "feat: Add GitHub Actions CI/CD for ECR"Push to GitHub:
git push origin mainMonitor GitHub Actions:
- Go to your GitHub repository.
- Click on the “Actions” tab.
- You should see a new workflow run initiated by your push. Click on it to view the progress of each step.
- All steps should pass successfully.
Verify Image in AWS ECR:
- Once the GitHub Actions workflow completes successfully, go back to your AWS Console.
- Navigate to ECR and select your
nodejs-api-reporepository. - You should see at least two new image tags: one with the full commit SHA (e.g.,
1a2b3c4d5e6f...) and one withlatest.
Production Considerations
Building a CI/CD pipeline isn’t just about automation; it’s about building a reliable and secure system.
- Security (IAM & GitHub Secrets):
- Least Privilege: We followed this by granting only
AmazonECRContainerRegistryPowerUserpermissions to ourgithub-actions-user. In a real-world scenario, you might create an even more granular custom policy that only allows pushing to specific repositories. - GitHub Secrets: Using GitHub Secrets is paramount for protecting sensitive credentials. Never hardcode AWS keys or other secrets directly in your workflow files.
- Least Privilege: We followed this by granting only
- Performance:
- Docker Layer Caching: The
docker buildcommand benefits from Docker’s layer caching. If the base image or earlier layers haven’t changed, Docker reuses cached layers, speeding up builds. Ensure yourDockerfileis optimized to take advantage of this. npm civsnpm install: As mentioned,npm ciis faster and more reliable in CI environments.
- Docker Layer Caching: The
- Error Handling and Notifications:
- GitHub Actions provides built-in notifications for workflow failures. You can configure email notifications directly in GitHub repository settings under “Notifications” or integrate with external services like Slack using GitHub Actions marketplace actions (e.g.,
rtCamp/action-slack-notify). - Ensure your
npm testcommand exits with a non-zero code on failure to stop the workflow.
- GitHub Actions provides built-in notifications for workflow failures. You can configure email notifications directly in GitHub repository settings under “Notifications” or integrate with external services like Slack using GitHub Actions marketplace actions (e.g.,
- Image Tagging Strategy:
- Using
github.shafor unique image tags is a best practice for production. It ensures immutability and makes it easy to trace which code version corresponds to which deployed image. - The
latesttag is convenient but should be used with caution in production, as it can be ambiguous. For production deployments, always reference specific, immutable tags (like the commit SHA or a semantic version).
- Using
- Environment Variables for Docker Build: If your application needs environment variables during the Docker build process (e.g.,
NODE_ENV=production), ensure they are passed correctly in yourdocker buildcommand using--build-arg. For runtime environment variables, these will be handled during deployment (e.g., via ECS task definitions).
Code Review Checkpoint
At this point, you should have:
- AWS ECR Repository: A private repository created in AWS ECR.
- AWS IAM User: A dedicated IAM user with programmatic access and
AmazonECRContainerRegistryPowerUserpolicy attached. - GitHub Secrets:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_REGIONconfigured in your GitHub repository secrets. - GitHub Actions Workflow: A
.github/workflows/main.ymlfile that:- Triggers on pushes to
main. - Checks out code, sets up Node.js, installs dependencies, and runs tests.
- Configures AWS credentials and logs into ECR.
- Builds and tags a Docker image (with commit SHA and
latest). - Pushes the Docker image to your ECR repository.
- Triggers on pushes to
This setup forms the foundation of our automated deployment process.
Common Issues & Solutions
AccessDeniedExceptionwhen pushing to ECR:- Issue: The IAM user credentials used by GitHub Actions do not have sufficient permissions to push images to ECR.
- Solution: Double-check that the
AmazonECRContainerRegistryPowerUserpolicy (or a custom policy with equivalent permissions) is attached to thegithub-actions-userin AWS IAM. Ensure theAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin GitHub Secrets are correct and correspond to this IAM user.
docker buildfails in GitHub Actions:- Issue: The Docker build process encounters an error, often due to issues in the
Dockerfileor missing files. - Solution: Review the logs in the GitHub Actions run carefully. The output from
docker buildwill indicate the specific error. Common causes include incorrect paths inCOPYcommands, missing build dependencies, or syntax errors in theDockerfile. Ensure yourDockerfileworks locally before pushing.
- Issue: The Docker build process encounters an error, often due to issues in the
Error: No credentials found.orNot authorized to perform: ecr:GetAuthorizationToken:- Issue: GitHub Actions failed to configure AWS credentials or log into ECR.
- Solution: Verify that
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_REGIONare correctly set as repository secrets in GitHub, and that theaws-actions/configure-aws-credentials@v4step andaws-actions/amazon-ecr-login@v2steps are configured correctly in your workflow file.
npm testfails unexpectedly in CI:- Issue: Tests pass locally but fail in the CI environment.
- Solution: This can happen due to environment differences. Check Node.js versions (ensure
setup-nodematches your local version), environment variables, or platform-specific issues. Make sure your tests are robust and don’t rely on local environment specifics. Review the test logs in GitHub Actions for detailed error messages.
Testing & Verification
To ensure everything is correctly set up and working:
- Make a minor code change: Introduce a small, harmless change to a non-critical file (e.g., add a comment to
src/app.ts). - Commit and push: Commit this change and push it to your
mainbranch. - Monitor GitHub Actions: Observe the workflow run on the “Actions” tab of your GitHub repository.
- Verify that all steps (Checkout, Setup Node.js, Install dependencies, Run tests, Configure AWS credentials, Login to ECR, Build & tag Docker image, Push Docker image) complete successfully with green checkmarks.
- Verify ECR Images: Once the workflow finishes, navigate to your ECR repository in the AWS Console. Confirm that a new image with the latest commit SHA tag and an updated
latesttag are present.
If all these checks pass, congratulations! You have successfully implemented a robust CI/CD pipeline for your Node.js application, automating the build, test, and container image publishing process.
Summary & Next Steps
In this chapter, we achieved a significant milestone by establishing a Continuous Integration/Continuous Deployment (CI/CD) pipeline using GitHub Actions and AWS ECR. We configured AWS credentials securely, created an ECR repository, and crafted a workflow that automatically builds, tests, and pushes our Dockerized Node.js application to ECR upon every push to the main branch. This automation dramatically improves our development efficiency, ensures code quality, and prepares our application for rapid deployment.
With our Docker images now residing in AWS ECR, ready for consumption, the next logical step is to deploy them to a scalable and managed container orchestration service. In Chapter 14: Deploying to AWS ECS Fargate, we will leverage AWS Elastic Container Service (ECS) with the Fargate launch type to deploy our application, ensuring high availability, scalability, and ease of management. We’ll define task definitions, services, and clusters to bring our production-ready Node.js application to life in the cloud.