Deploy a NGINX docker container on ECS Fargate using Cloudformation

Posted on: 28-2-2024

To deploy a NGINX docker container on ECS Fargate using Cloudformation we need to follow the following steps:

  1. Create a Dockerfile
  2. Push Docker image to ECR
  3. Create a template file
  4. Create a Cluster Resource
  5. Create a TaskDefinition
  6. Create a Cluster Service
  7. Deploy

This tutorial assumes you have a working nginx config file and some files to serve. For example the output of a ng build command.

Create a Dockerfile.

Create a Dockerfile in the root of your project.

# Use nginx as base image
FROM nginx:latest

# Copy the nginx config file to the container
COPY nginx.conf /etc/nginx/nginx.conf

# Copy files you want to serve to the container
COPY /dist/<your-app> /www/data

# Metadata that tells the user the container listens on port 80
EXPOSE 80

Push Docker image to ECR

We need to push our docker image to ECR because we want to use this image in our Cloudformation template.

ECR stands for Elastic Container Registry and is a place on AWS to store docker images.

Create a repository in AWS using for example the console. Follow the push commands to push your image to your created ECR repository.

Create a template file

We want to use Cloudformation to use Infrastructure as Code to deploy services. For this, we need to add a yml file which is our configuration file containing all services we want to use.

Create a file named cloudformation-template.yml in the root of your project.

Create a Cluster Resource

To deploy a docker container on ECS Fargate it is useful to understand how ECS is build up. You have got a cluster which is made up of services, which contains tasks which are created based on task-definitions.

Add the following resource to your template:

  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: 'cf-nginx-angular-cluster'
      CapacityProviders:
        - 'FARGATE'

This creates a ECS Fargate cluster with the name 'cf-nginx-angular-cluster'

Create a TaskDefinition

Add the following resource to your template:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: 'cf-nginx-angular-task-definition'
          Image: '<your-image-uri>'
          PortMappings:
            - AppProtocol: http
              ContainerPort: 80
              HostPort: 80
              Protocol: tcp
      Cpu: 256
      Memory: 512
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      Family: 'cf-nginx-angular-task-definition-family'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - 'FARGATE'

Our task-definition needs an execution-role so it can make AWS API calls on our behalf.

That's why we also need to add the following resource to our template:

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'cf-nginx-angular-ecs-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          Effect: 'Allow'
          Principal:
            Service:
              - ecs-tasks.amazonaws.com
          Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'

This creates an IAM Role with the managed policy 'AmazonECSTaskExecutionRolePolicy' which is a policy that provides access to other AWS service resources that are required to run Amazon ECS tasks.

Create a Cluster Service

Add the following the resource to your template:
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: 'cf-nginx-angular-service'
      Cluster: !GetAtt ECSCluster.Arn
      DesiredCount: 1
      LaunchType: 'FARGATE'
      TaskDefinition: !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: 'ENABLED'
          SecurityGroups:
            - !GetAtt SecurityGroup.GroupId
          Subnets:
            - '<your-subnet>'

Because we want our deployed nginx container to be accessible from the internet we need to create a security group wich allows such access and reference this security group from our service resource.

That's why we also need to add the following resource:

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: 'cf-nginx-angular-security-group'
      GroupDescription: 'Allows http requests from everywhere'
      SecurityGroupIngress:
        CidrIp: '0.0.0.0/0'
        IpProtocol: tcp
        FromPort: 80
        ToPort: 80

This security group contains an inbound rule that states that all http trafic on port 80 is allowed.

Deploy

This is how your cloudformation-template.yml should look like:
AWSTemplateFormatVersion: 2010-09-09
Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: 'cf-nginx-angular-cluster'
      CapacityProviders:
        - 'FARGATE'

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: 'cf-nginx-angular-task-definition'
          Image: '357839314461.dkr.ecr.eu-central-1.amazonaws.com/test:latest'
          PortMappings:
            - AppProtocol: http
              ContainerPort: 80
              HostPort: 80
              Protocol: tcp
      Cpu: 256
      Memory: 512
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      Family: 'cf-nginx-angular-task-definition-family'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - 'FARGATE'

  ECSService:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: 'cf-nginx-angular-service'
      Cluster: !GetAtt ECSCluster.Arn
      DesiredCount: 1
      LaunchType: 'FARGATE'
      TaskDefinition: !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: 'ENABLED'
          SecurityGroups:
            - !GetAtt SecurityGroup.GroupId
          Subnets:
            - '<your-subnet>'

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: 'cf-nginx-angular-security-group'
      GroupDescription: 'Allows http requests from everywhere'
      SecurityGroupIngress:
        CidrIp: '0.0.0.0/0'
        IpProtocol: tcp
        FromPort: 80
        ToPort: 80

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'cf-nginx-angular-ecs-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          Effect: 'Allow'
          Principal:
            Service:
              - ecs-tasks.amazonaws.com
          Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'

Validate the file using the following command:

aws cloudformation validate-template --template-body file://cloudformation-template.yml

Run the follow command to deploy your nginx container using cloudformation.

aws cloudformation create-stack --stack-name cf-nginx-angular-stack --template-body file://./cloudformation-template.yml --capabilities CAPABILITY_NAMED_IAM

Because we create a resource of type AWS::IAM::Role with a custom name we need to use the flag --capabilities CAPABILITY_NAMED_IAM.

Navigate to your deployed task and go to its public ip.

Now view your deployed app!

Links