Common
Building a small backend service with AWS Fargate with Route53 Service Discovery
7. September 2018
0

I wanted to run a small backend API service used by another frontend facing service in a simple and stress free manner. This felt like the perfect opportunity to give Fargate a try. It is a fully managed docker container runtime provided by AWS and nicely integrated into the AWS ecosystem. Well it is early after it’s release so probably there will be some limitations and pitfalls. The same fact applies to the relatively new Service Discovery service which is meant as a lean alternative to a load balancer for internal service discovery. It is basically Route 53 DNS with some API’s and tooling around to register and deregister nodes dynamically based on provisioning.

The following CloudFormation template contains everything needed to make up the service including a small database.

This template uses cfn-sphere macros for simplification.

AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Stack
Parameters:
  subnetIds:
    Description: Subnet IDs
    Type: List<AWS::EC2::Subnet::Id>
  vpcId:
    Description: VPC ID
    Type: AWS::EC2::VPC::Id
  allowedIP:
    Type: String
    Description: IP address in CIDR notation to grant access
  databaseUser:
    Type: String
    Description: database user name
  databasePassword:
    Type: String
    Description: database password
    NoEcho: true


Resources:
  dbSg:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "database"
      VpcId: "|Ref|vpcId"

  dbSgIngressFromApp:
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties:
      GroupId: "|Ref|dbSg"
      SourceSecurityGroupId: "|Ref|taskSg"
      FromPort: 3306
      ToPort: 3306
      IpProtocol: tcp

  taskSg:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "public access"
      VpcId: "|Ref|vpcId"

  serviceConsumerSg:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "sg attached to the instances consuming this service"
      VpcId: "|Ref|vpcId"

  taskSgIngressFromServiceConsumerSg:
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties:
      GroupId: "|Ref|taskSg"
      SourceSecurityGroupId: "|Ref| serviceConsumerSg"
      FromPort: 80
      ToPort: 80
      IpProtocol: tcp

  taskSgEgressToDb:
    Type: "AWS::EC2::SecurityGroupEgress"
    Properties:
      GroupId: "|Ref|taskSg"
      DestinationSecurityGroupId: "|Ref|dbSg"
      FromPort: 3000
      ToPort: 3000
      IpProtocol: tcp

  taskSgEgressToWorld:
    Type: "AWS::EC2::SecurityGroupEgress"
    Properties:
      GroupId: "|Ref|taskSg"
      CidrIp: "0.0.0.0/0"
      FromPort: 0
      ToPort: 65535
      IpProtocol: tcp

  logGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      RetentionInDays: 7

  cluster:
    Type: "AWS::ECS::Cluster"

  executionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: "Allow"
          Principal:
            Service: "ecs-tasks.amazonaws.com"
          Action: "sts:AssumeRole"
      Path: "/"
      Policies:
        - PolicyName: "cloudwatch-logs-access"
          PolicyDocument:
            Statement:
            - Action:
                - "logs:PutLogEvents"
                - "logs:CreateLogStream"
              Effect: "Allow"
              Resource: "|GetAtt|logGroup|Arn"

  taskRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "ecs-tasks.amazonaws.com"
            Action: "sts:AssumeRole"
      Path: "/"
      Policies:
        - PolicyName: "cloudwatch-full-access"
          PolicyDocument:
            Statement:
              - Action: "cloudwatch:*"
                Effect: "Allow"
                Resource: "*"

  service:
    Type: "AWS::ECS::Service"

    Properties:
      Cluster: "|Ref|cluster"

      LaunchType: "FARGATE"
      DeploymentConfiguration:
        MinimumHealthyPercent: 100

        MaximumPercent: 200
      DesiredCount: 2
      TaskDefinition: "|Ref|task"
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: "ENABLED"
          SecurityGroups:
            - "|Ref|taskSg"
          Subnets: "|Ref|subnetIds"
      ServiceRegistries:
        - RegistryArn: "|GetAtt|serviceDiscoveryService|Arn"

  task:
    Type: "AWS::ECS::TaskDefinition"

    Properties:
      RequiresCompatibilities:
        - "FARGATE"
      Family: "backendService"
      Memory: 512
      Cpu: 256
      NetworkMode: "awsvpc"
      TaskRoleArn: "|Ref|taskRole"
      ExecutionRoleArn: "|Ref|executionRole"
      ContainerDefinitions:
        - Name: "webApp"
          Image: "myBackendService:1.0.0"
          Memory: 512
          Cpu: 256
          PortMappings:
            - ContainerPort: 3000
              HostPort: 3000
          LogConfiguration:
            LogDriver: "awslogs"
            Options:
              awslogs-group: "|Ref|logGroup"
              awslogs-region: "|Ref|AWS::Region"
              awslogs-stream-prefix: "|Ref|AWS::StackName"
          Environment:
            - Name: "DATABASE_HOST"
              Value: "|GetAtt|database|Endpoint.Address"
            - Name: "DATABASE_USER"
              Value: "|Ref|databaseUser"
            - Name: "DATABASE_PASSWORD"
              Value: "|Ref|databasePassword"

  serviceDiscoveryNamespace:
    Type: "AWS::ServiceDiscovery::PublicDnsNamespace"
    Properties:
      Description: "some namespace for service discovery"
      Name: "my.internal.domain"

  serviceDiscoveryService:
    Type: "AWS::ServiceDiscovery::Service"
    Properties:
      Description: "myBackendService"
      Name: "mybackendservice"
      DnsConfig:
        NamespaceId: "|Ref|serviceDiscoveryNamespace"
        RoutingPolicy: "WEIGHTED"
        DnsRecords:
          - Type: "A"
            TTL: "60"
      HealthCheckCustomConfig:
        FailureThreshold: 1

  databaseSubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupDescription: "|Ref|AWS::StackName"
      SubnetIds: "|Ref|subnetIds"

  database:
    Type: "AWS::RDS::DBInstance"
    Properties:
      AllocatedStorage: "20"
      DBInstanceClass: "db.t2.small"
      Engine: "MySQL"
      EngineVersion: "5.6"
      MasterUsername: "|Ref|databaseUser"
      MasterUserPassword: "|Ref|databasePassword"
      DBSubnetGroupName: "|Ref|databaseSubnetGroup"
      VPCSecurityGroups:
        - "|Ref|dbSg"
      PubliclyAccessible: true
      StorageType: "gp2"