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.