Case Study - Nginx Service

Overview

The nginx-demo service provides good example of the how to specify and share a service via a Servicefile.

Service Goals

A static, HA, intranet site accessible 'behind the firewall' to corporate users, with high concurrent usage.

Design

To achieve the service goals, demo service runs the Nginx container in an auto-scaling group behind a load balancer.

The load balancer and scaling group are to be placed in a private subnet, in multiple availability zones.

Container orchestration via ECS, with container images hosted in the public Docker hub repository.

A Backups container will be used to provision intranet site contents via an S3 bucket. 

Implementation

Folder Layout

The service is encoded in Servicefile.json which references supporting files in the config, data, and lib folders...

Servicefile.json

SectionCode
service-name
"service-name": {
    "comment": "use yac prefix for all yac demo services",
    "value": "yac/nginx-demo:1.0"
  }
service-description
"service-description": {
    "summary": "A static, HA, intranet site accessible 'behind the firewall' to corporate users, with high concurrent usage.",
    "details":  ["*HA though ASG",
                 "No public access",
                 "Static site content files staged via S3",
                 "Access logs saved to S3"],
    "maintainer" : {
      "name":     "Thomas Jackson",
      "email":    "thomas.b.jackson@gmail.com"
    },
    "tags": ["nginx", "intranet"]
  }
stock-resources
"stock-resources": {
    "comment": ["leverage the 'stock' i-elb and asg resource templates included with yac"],
    "value": ["e-elb", "asg"]
  }
service-params
"service-params": {
    "comment": "constants for this service. any can be rendered into stack templates via yac-ref",
    "boot-file": {
      "comment": "the file that the ec2 instance should use to boot from",
      "value":   "config/boot.sh"
    },
    "s3-service-path": {
      "comments": "the root path for placing stuff related to this service",
      "value":  "nginx"  
    }     
  }
service-inputs
"service-inputs": {
    "comments": "script that will be called to create dynamic params needed by this service",
    "value":  "lib/inputs.py"  
  }
stack-template

Prefix of this section looks like:

"stack-template": {      
    "Parameters" : {
      "ImageId" : {
        "Description" : "CoreOS-stable-835.9.0-hvm, December 6, 2015",
        "Type" :        "String",
        "Default" :     "ami-16cfd277"
      },
      ...

The full template is pretty small, thanks to the stock resources.

The container definition format should look familiar (uses ECS Cloud Formation syntax!)

 Click here to expand...
"stack-template": {      
    "Parameters" : {
      "ImageId" : {
        "Description" : "CoreOS-stable-835.9.0-hvm, December 6, 2015",
        "Type" :        "String",
        "Default" :     "ami-16cfd277"
      },    
      "InstanceType" : {
        "Description" : "Type of EC2 instance to launch. Don't need much for this simple server.",
        "Type" :        "String",
        "Default" :    "m3.medium"
      },
      "DockerVolumeSize" : {
        "Description" : "Only have a few containers so only a few container images to store",
        "Type" : "String",
        "Default" : "10"
      }, 
      "HomeVolumeSize" : {
        "Description" : "Size to support the intended size of the service DB. Don't need much here either",
        "Type" : "String",
        "Default" : "10"
      }      
    },
    "Resources": {
      "ECS": {
        "Type": "AWS::ECS::Cluster"
      },
      "NginxService": {
        "Type" : "AWS::ECS::Service",
        "Properties" : {
          "Cluster" : { "Ref": "ECS" },
          "DesiredCount" : 1,
          "TaskDefinition" : { "Ref" : "NginxTD" }
        }
      },
      "NginxTD": {
        "Type" : "AWS::ECS::TaskDefinition",
        "Properties" : {
          "ContainerDefinitions": [
            {
              "Name": "nginx", 
              "Image": "nginx:latest",
              "Cpu": 0,
              "Essential": true,
              "VolumesFrom": [],
              "Memory": 512,               
              "PortMappings": [
                {
                  "HostPort": 80,
                  "ContainerPort": 80
                }
              ],
              "MountPoints": [
                {
                  "ContainerPath": "/etc/nginx/conf.d/default.conf",
                  "SourceVolume": "nginx-conf",
                  "ReadOnly": false
                },
                {
                  "ContainerPath": "/var/local/nginx",
                  "SourceVolume": "site-data",
                  "ReadOnly": false
                },
                {
                  "ContainerPath": "/etc/localtime",
                  "SourceVolume": "localtime",
                  "ReadOnly": true
                }
              ]
            }            
          ],
          "Volumes": [
            {
              "Host": {
                "SourcePath": "/var/local/nginx/conf/nginx.conf"
              },
              "Name": "nginx-conf"
            },
            {
              "Host": {
                "SourcePath": "/var/local/nginx"
              },
              "Name": "site-data"
            },
            {
              "Host": {
                "SourcePath": "/etc/localtime"
              },
              "Name": "localtime"
            }
          ]
        }
      }
    },
    "ResourceTweaks": {
      "AppLaunchConfig" : {
        "Type" : "AWS::AutoScaling::LaunchConfiguration",
        "Properties" : {
          "AssociatePublicIpAddress": true
        }
      },
      "AppSG" : {
        "Type" : "AWS::EC2::SecurityGroup",
        "Properties" : {
          "SecurityGroupIngress" : [
            {
              "IpProtocol" : "tcp",
              "FromPort" : "22",
              "ToPort" : "22",
              "CidrIp" : "0.0.0.0/0"
            }
          ]
        }
      }
    }
deploy-for-boot
"deploy-for-boot": {
    "comment": "files and/or directories to deploy to s3 before starting this service",
    "files": [
      {
        "comment": "for configuring nginx",
        "src": "config/nginx.conf",
        "dest": {"yac-join" : [ "/", [
                   "s3:/", 
                  {"yac-ref": "s3-bucket"},
                  {"yac-ref": "s3-service-path"},
                  {"yac-ref": "env"},
                   "nginx.conf" ]]}
      },
      {
		"comment": "for configuring the backups container, which will restore files from s3 onto the ec2 instance",
        "src": "config/backups.json",
        "dest": {"yac-join" : [ "/", [
                   "s3:/", 
                  {"yac-ref": "s3-bucket"},
                  {"yac-ref": "s3-service-path"},
                  {"yac-ref": "env"},
                   "backups.json" ]]}
      }
    ],
    "directories": [
      {
		"comment": "the static contents of the intranet site itself",
        "src": "data",
        "dest": {"yac-join" : [ "/", [
                   "s3:/", 
                  {"yac-ref": "s3-bucket"},
                  {"yac-ref": "s3-service-path"},
                  {"yac-ref": "env"},
                   "data" ]]}
      }
    ]
  }