Using Amazon ECS Fargate with AWS SAM CLI

Jessica Deen profile picture
Jessica Deen
Principal Developer Advocate at AWS

Dependencies

This pattern uses the AWS SAM CLI for deploying CloudFormation stacks on your AWS account. You should follow the appropriate steps for installing SAM CLI.

About

AWS SAM CLI streamlines the deployment of your containerized applications and necessary infrastructure resources efficiently. An extension of AWS CloudFormation, SAM CLI simplifies serverless application deployment and management by allowing you to define infrastructure as code. This facilitates version control and reproducibility, simplifying the packaging and deployment of your application code, dependencies, and configurations.

This pattern will show how to deploy a simple nodeJS application to AWS Fargate using AWS SAM CLI. The following resources will be created as part of the provided templates:

  • Fargate Cluster
  • Amazon Elastic Container Registry
  • Fargate Service
  • Fargate Task Definition
  • Amazon VPC
  • Internet Gateway
  • 2 Public Subnets
  • 2 Private Subnets
  • Application Load Balancer

Architecture

The following diagram shows the architecture that will be deployed:

Virtual private cloud (VPC)Internet gatewayPublic subnetAWS FargateContainerAmazon Elastic Container Service (Amazon ECS)TrafficApplication Load BalancerAmazon Elastic Container Registry (Amazon ECR)RegistryImage

Define your Infrastructure

The following AWS SAM CLI template.yml creates a simple Amazon ECS cluster using AWS Fargate.

As part of the template.yml, the following resources will be created:

  • Amazon ECS Cluster
  • ECR Repo
  • Log Group
  • All IAM related roles/policies

In addition to the template.yml, you will also need a vpc.yml, where the necessary network resources are defined.

Finally, AWS SAM CLI will also look for a samconfig file, which contains default parameters for your Infrastructure as Code.

  • template.yml
  • vpc.yml
  • samconfig.toml
File: template.ymlLanguage: yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AWS Fargate with VPC, ALB, and ECR

Parameters:
  ImageTag:
    Description: tag name for image
    Type: String
    Default: latest

Globals:
  Function:
    Timeout: 3
    MemorySize: 128

Resources:  
  VPC:
    Type: AWS::Serverless::Application
    Properties:
      Location: ./vpc.yml

  ECRRepo:
    Type: AWS::ECR::Repository
    Properties:
      EmptyOnDelete: true

  Cluster:
    Type: AWS::ECS::Cluster
    Properties: 
      CapacityProviders: 
        - FARGATE
  
  Service:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: "hello-world"
      Cluster: !Ref Cluster
      LaunchType: FARGATE
      EnableExecuteCommand: true
      HealthCheckGracePeriodSeconds: 5
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets: [!GetAtt "VPC.Outputs.PublicSubnet1", !GetAtt VPC.Outputs.PublicSubnet2]
          SecurityGroups: [!GetAtt VPC.Outputs.SG]
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 50
      DesiredCount: 1
      TaskDefinition: !Ref "TaskDefinition"
      LoadBalancers:
        - ContainerName: "hello-world"
          ContainerPort: 3000
          TargetGroupArn: !GetAtt VPC.Outputs.LB

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: HelloWorld
      Cpu: 1024
      Memory: 8192
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      TaskRoleArn: !Ref ECSTaskRole
      RuntimePlatform:
        CpuArchitecture: X86_64
      ContainerDefinitions:
        - Name: hello-world
          Cpu: 1024
          Memory: 8192
          Image: !Sub
            - ${RepoUrl}:${ImageTag}
            - RepoUrl: !GetAtt ECRRepo.RepositoryUri
          PortMappings:
            - ContainerPort: 3000
          LogConfiguration:
            LogDriver: awslogs
            Options:
              mode: non-blocking
              max-buffer-size: 25m
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: containerlog
  
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /fargatelogs

  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: [ecs-tasks.amazonaws.com]
          Action: ['sts:AssumeRole']
          Condition:
            ArnLike:
              aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
            StringEquals:
              aws:SourceAccount: !Ref AWS::AccountId
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  ECSTaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: [ecs-tasks.amazonaws.com]
          Action: ['sts:AssumeRole']
          Condition:
            ArnLike:
              aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
            StringEquals:
              aws:SourceAccount: !Ref AWS::AccountId
      Path: /

Outputs:
  ClusterName:
    Description: Amazon ECS Cluster Name
    Value: !Ref Cluster
  ServiceName:
    Description: Amazon ECS Service Name
    Value: !GetAtt Service.Name
  FQDN:
    Description: URL for your application
    Value: !GetAtt VPC.Outputs.PublicLBFQDN
  RepositoryUrl:
    Description: URL of the repo
    Value: !GetAtt ECRRepo.RepositoryUri

You will also need an application you can deploy to your infrastructure. For the purpose of this demo, a simple Hello World nodeJS application is provided for you.

  • index.js
  • package.json
  • Dockerfile
File: index.jsLanguage: js
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
  res.status(200).json
})

app.listen(port, () => {
  console.log(`Access your application at`, `http://localhost:${port}`)
})

module.exports = app

Once you have the files downloaded, you can deploy your infrastructure using the following commands:

Language: sh
sam build && sam deploy

Test it Out

After the provided templates are deployed, you can find the the following AWS CloudFormation outputs using the following commands:

Language: sh
RepositoryUrl=$(aws cloudformation --region us-east-1 describe-stacks --stack-name nodejs-sam --query "Stacks[0].Outputs[?OutputKey=='RepositoryUrl'].OutputValue" --output text) && echo $RepositoryUrl

FQDN=$(aws cloudformation --region us-east-1 describe-stacks --stack-name nodejs-sam --query "Stacks[0].Outputs[?OutputKey=='FQDN'].OutputValue" --output text) && echo $FQDN

WARNING

Note: You will want to build and push your container image prior to deploying this application to Fargate.

The default template will look for the image in your provisioned Amazon Elastic Container Registry with the tag "latest". Once the image exists, you will be able to test your application using the output retrieved from the FQDN command.

TIP

In Production, it's not best practice to use the latest tag for your containerized application images. Instead, you'll want to tag your images per release as part of your CI/CD workflow or pipeline.