Configure AWS ECR with Terraform | GitHub Actions

Configuring AWS ECR and deploying it in an EC2 Instance using GitHub Actions

Our Goal

In this Blog, I will try to explain how to build a docker image and push it to AWS ECR and deploy it to an AWS EC2 instance using GitHub Actions. We will be provisioning resources using Terraform. It is a lovely automation tool I like to use. Also, I am a Certified Hashicorp Terraform Associated. I have already made a Project for your reference feel free to check it out here here

What is AWS ECR

AWS Elastic Container Registry (ECR) is a fully-managed, private Docker container registry that makes it easy to store, manage, and deploy Docker container images. With ECR, you can easily store and manage container images in your own, secure, and scalable registry.

ECR integrates with other AWS services, such as Amazon ECS and AWS Fargate, making it easy to deploy your containerized applications. You can also use ECR with other container orchestration tools, such as Kubernetes.

Some of the features of ECR include:

  • Image management: ECR provides a simple and user-friendly interface to store, manage, and deploy Docker container images.

  • Security: ECR is designed to be highly secure, with built-in support for authentication and authorization using IAM.

  • Scalability: ECR is designed to scale automatically, handling the storage and transfer of large image files.

  • Integration with other AWS services: ECR integrates with other AWS services such as Amazon ECS and AWS Fargate, making it easy to deploy your containerized applications.

  • Easy to use: ECR is easy to use, with a simple API and command-line interface that makes it easy to store, manage, and deploy container images.

  • Cost-effective: ECR is cost-effective, with pay-per-use pricing that only charges for the amount of data stored and data transferred.

How are we provisioning it using Terraform?

Below is a simple Terraform Code I wrote for provisioning this resource:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.37.0"
    }
  }
  backend "s3" {
    bucket = "abcd"
    key    = "aws/ec2-deploy/terraform.tfstate"
    region = "us-east-1"
  }
}

provider "aws" {
  region = "us-east-1"
}
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  instance_tenancy     = "default"
  tags = {
    Name = "Project VPC"
  }
}
resource "aws_subnet" "public_subnets" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = element(var.public_subnet_cidrs, count.index)
  availability_zone = element(var.azs, count.index)

  tags = {
    Name = "Public Subnet ${count.index + 1}"
  }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "Project VPC IG"
  }
}

resource "aws_route_table" "second_rt" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "2nd Route Table"
  }
}

resource "aws_route_table_association" "public_subnet_asso" {
  count          = length(var.public_subnet_cidrs)
  subnet_id      = element(aws_subnet.public_subnets[*].id, count.index)
  route_table_id = aws_route_table.second_rt.id
}
resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  description = "Allow TLS inbound traffic"
  vpc_id      = aws_vpc.main.id
  ingress = [{
    description      = "egress"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = []
    prefix_list_ids  = []
    security_groups  = []
    self             = false
  }]
  egress = [{
    description      = "egress"
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = []
    prefix_list_ids  = []
    security_groups  = []
    self             = false
  }]
  tags = {
    Name = "allow_tls"
  }
}
resource "aws_key_pair" "deploy" {
  key_name   = "deployer"
  public_key = "ssh-rsa"
}

resource "aws_iam_instance_profile" "example" {
  name = ""
  role = ""
}
resource "aws_ecr_repository" "example" {
  name = "example-repository"
}
resource "aws_instance" "name" {
  ami                    = "ami-08c40ec9ead489470"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.allow_tls.id]
  key_name               = aws_key_pair.deploy.key_name
  iam_instance_profile   = aws_iam_instance_profile.example.name
  subnet_id              = aws_subnet.public_subnets[0].id
  associate_public_ip_address = true
  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ubuntu"
    private_key = var.private_key
    # private_key = file("~/.ssh/devenv")
  }
  tags = {
    Name = "terraform-ec2"
  }
}

output "public_ip" {
  value     = aws_instance.name.public_ip
  sensitive = true
}

It consists of an EC2 Instance, security groups, a key pair, Route Table, VPC, and ECR configuration. In the next part, we will see the GitHub Actions to build and push the image, and finally SSH into the EC2 instance and deploy the app.

GitHub Actions for Provisioning and Deploying

Below is one of the ways u can write the actions to deploy the Infra and build the Docker Image and Push into AWS ECR.

name: CI/CD
on:
  push:
    branches:
      - master
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  TF_STATE_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }}
  PRIVATE_SSH_KEY: ${{ secrets.AWS_SSH_KEY_PRIVATE }}
  PUBLIC_SSH_KEY: ${{ secrets.AWS_SSH_KEY_PUBLIC }}
  AWS_REGION: us-east-1

jobs:
  deploy-infra:
    runs-on: ubuntu-latest
    outputs:
      SERVER_PUBLIC_IP: ${{ steps.set-output.outputs.instance_public_ip }}
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: setup terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_wrapper: false
      - name: Terraform Init
        id: init
        run: terraform init
        working-directory: ./Terraform
      - name: Terraform Plan
        id: plan
        run: |-
          terraform plan \
          -var="public_key=${{ env.PUBLIC_SSH_KEY }}" \
          -var="private_key=${{ env.PRIVATE_SSH_KEY }}" \
          -out=PLAN
        working-directory: ./Terraform
      - name: Terraform Apply
        id: apply
        run: terraform apply -auto-approve -var="public_key=${{ env.PUBLIC_SSH_KEY }}" -var="private_key=${{ env.PRIVATE_SSH_KEY }}"
        working-directory: ./Terraform
      - name: Set Output
        id: set-output
        run: echo "::set-output name=instance_public_ip::$(terraform output -raw public_ip)"
        working-directory: ./Terraform
  deploy-app:
    runs-on: ubuntu-latest
    needs: deploy-infra
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set Instance Public IP
        run: echo SERVER_PUBLIC_IP=${{ needs.deploy-infra.outputs.SERVER_PUBLIC_IP }} >> $GITHUB_ENV
      - name: Login to AWS ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
        with:
          aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Build, tag, and push image to Amazon ECR
        env:
          # ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ aws_ecr_repository.example.registry_id }}.dkr.ecr.us-west-2.amazonaws.com
          IMAGE_TAG: ${{ github.sha }}
        run: |-
          docker build -t itisaby/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push itisaby/$ECR_REPOSITORY:$IMAGE_TAG
        working-directory: ./NodeApp
      - name: Deploy Docker Image to EC2
        env:
          SERVER_PUBLIC_IP: ${{ env.SERVER_PUBLIC_IP }}
          # ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ aws_ecr_repository.example.registry_id }}.dkr.ecr.us-west-2.amazonaws.com
          IMAGE_TAG: ${{ github.sha }} 
          AWS_REGION: ${{ env.AWS_REGION }}
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.SERVER_PUBLIC_IP }}
          username: ubuntu
          key: ${{ env.PRIVATE_SSH_KEY }}
          envs: ECR_REPOSITORY,IMAGE_TAG,AWS_REGION,SERVER_PUBLIC_IP,PRIVATE_SSH_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
          script: |-
            sudo apt update
            sudo apt install docker.io -y
            sudo apt install awscli -y 
            sudo $(aws ecr get-login --no-include-email --region us-east-1)
            sudo docker stop example-node || true
            sudo docker rm example-node || true
            sudo docker pull itisaby/$ECR_REPOSITORY:$IMAGE_TAG
            sudo docker run -d --name example-node -p 80:3000 itisaby/$ECR_REPOSITORY:$IMAGE_TAG 
Footer

Here it builds and pushes the Docker image to the ECR repository. The image is tagged with the git sha.

It's important to note that you will need to set the AWS_ACCESS_KEY and AWS_SECRET_KEY secrets in GitHub before running this workflow, also you will need to have the Terraform code to create the ECR repository and the Dockerfile in your repository.

It's also important to test this workflow in a development environment before deploying to production and make sure you understand the permissions required to interact with ECR and the resources it might create.

Conclusion

Finally, this is one of the ways u can use AWS ECR and Terraform to build and deploy images. But there can be multiple ways as well. In the next blog on AWS ECR, I will explain it using AWS ECS (might use AWS Fargate) Service instead of deploying it using AWS EC2 by using ssh.

Did you find this article valuable?

Support Arnab's Blog by becoming a sponsor. Any amount is appreciated!