github.com/mweagle/Sparta@v1.15.0/docs_source/content/reference/hybrid_topologies.md (about)

     1  ---
     2  date: 2016-03-09T19:56:50+01:00
     3  title: Hybrid Topologies
     4  weight: 150
     5  ---
     6  
     7  At a broad level, AWS Lambda represents a new level of compute abstraction for services. Developers don't immediately concern themselves with HA topologies, configuration management, capacity planning, or many of the other areas traditionally handled by operations. These are handled by the vendor supplied execution environment.
     8  
     9  However, Lambda is a relatively new technology and is not ideally suited to certain types of tasks.  For example, given the current [Lambda limits](http://docs.aws.amazon.com/lambda/latest/dg/limits.html), the following task types might better be handled by "legacy" AWS services:
    10  
    11    * Long running tasks
    12    * Tasks with significant disk space requirements
    13    * Large HTTP(S) I/O tasks
    14  
    15  It may also make sense to integrate EC2 when:
    16  
    17    * Applications are being gradually decomposed into Lambda functions
    18    * Latency-sensitive request paths can't afford [cold container](https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/) startup times
    19    * Price/performance justifies using EC2
    20    * Using EC2 as a failover for system-wide Lambda outages
    21  
    22  For such cases, Sparta supports running the exact same binary on EC2.  This section describes how to create a single Sparta service that publishes a function via AWS Lambda _and_ EC2 as part of the same application codebase. It's based on the [SpartaOmega](https://github.com/mweagle/SpartaOmega) project.
    23  
    24  # Mixed Topology
    25  
    26  Deploying your application to a mixed topology is accomplished by combining existing Sparta features. There is no "make mixed" command line option.
    27  
    28  ## Add Custom Command Line Option
    29  
    30  The first step is to add a [custom command line option](/reference/application/custom_commands). This command option will be used when your binary is running in "mixed topology" mode.  The SpartaOmega project starts up a localhost HTTP server, so we'll add a `httpServer` command line option with:
    31  
    32  ```go
    33  // Custom command to startup a simple HelloWorld HTTP server
    34  httpServerCommand := &cobra.Command{
    35    Use:   "httpServer",
    36    Short: "Sample HelloWorld HTTP server",
    37    Long:  `Sample HelloWorld HTTP server that binds to port: ` + HTTPServerPort,
    38    RunE: func(cmd *cobra.Command, args []string) error {
    39      http.HandleFunc("/", helloWorldResource)
    40      return http.ListenAndServe(fmt.Sprintf(":%d", HTTPServerPort), nil)
    41    },
    42  }
    43  sparta.CommandLineOptions.Root.AddCommand(httpServerCommand)
    44  ```
    45  
    46  Our command doesn't accept any additional flags. If your command needs additional user flags, consider adding a [ParseOptions](https://godoc.org/github.com/mweagle/Sparta#ParseOptions) call to validate they are properly set.
    47  
    48  ## Create CloudInit Userdata
    49  
    50  The next step is to write a [user-data](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) script that will be used to configure your EC2 instance(s) at startup. Your script is likely to differ from the one below, but at a minimum it will include code to download and unzip the archive containing your Sparta binary.
    51  
    52  ```bash
    53  #!/bin/bash -xe
    54  #!/bin/bash -xe
    55  SPARTA_OMEGA_BINARY_PATH=/home/ubuntu/{{.SpartaBinaryName}}
    56  
    57  ################################################################################
    58  #
    59  # Tested on Ubuntu 16.04
    60  #
    61  # AMI: ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-20160516.1 (ami-06b94666)
    62  if [ ! -f "/home/ubuntu/userdata.sh" ]
    63  then
    64    curl -vs http://169.254.169.254/latest/user-data -o /home/ubuntu/userdata.sh
    65    chmod +x /home/ubuntu/userdata.sh
    66  fi
    67  
    68  # Install everything
    69  service supervisor stop || true
    70  apt-get update -y
    71  apt-get upgrade -y
    72  apt-get install supervisor awscli unzip git -y
    73  
    74  ################################################################################
    75  # Our own binary
    76  aws s3 cp s3://{{ .S3Bucket }}/{{ .S3Key }} /home/ubuntu/application.zip
    77  unzip -o /home/ubuntu/application.zip -d /home/ubuntu
    78  chmod +x $SPARTA_OMEGA_BINARY_PATH
    79  
    80  ################################################################################
    81  # SUPERVISOR
    82  # REF: http://supervisord.org/
    83  # Cleanout secondary directory
    84  mkdir -pv /etc/supervisor/conf.d
    85  
    86  SPARTA_OMEGA_SUPERVISOR_CONF="[program:spartaomega]
    87  command=$SPARTA_OMEGA_BINARY_PATH httpServer
    88  numprocs=1
    89  directory=/tmp
    90  priority=999
    91  autostart=true
    92  autorestart=unexpected
    93  startsecs=10
    94  startretries=3
    95  exitcodes=0,2
    96  stopsignal=TERM
    97  stopwaitsecs=10
    98  stopasgroup=false
    99  killasgroup=false
   100  user=ubuntu
   101  stdout_logfile=/var/log/spartaomega.log
   102  stdout_logfile_maxbytes=1MB
   103  stdout_logfile_backups=10
   104  stdout_capture_maxbytes=1MB
   105  stdout_events_enabled=false
   106  redirect_stderr=false
   107  stderr_logfile=spartaomega.err.log
   108  stderr_logfile_maxbytes=1MB
   109  stderr_logfile_backups=10
   110  stderr_capture_maxbytes=1MB
   111  stderr_events_enabled=false
   112  "
   113  echo "$SPARTA_OMEGA_SUPERVISOR_CONF" > /etc/supervisor/conf.d/spartaomega.conf
   114  
   115  # Patch up the directory
   116  chown -R ubuntu:ubuntu /home/ubuntu
   117  
   118  # Startup Supervisor
   119  service supervisor restart || service supervisor start
   120  ```
   121  
   122  The script uses the command line option (`command=$SPARTA_OMEGA_BINARY_PATH httpServer`) that was defined in the first step.
   123  
   124  It also uses the `S3Bucket` and `S3Key` properties that Sparta creates during the build and provides to your decorator function (next section).
   125  
   126  ### Notes
   127  
   128  The script is using [text/template](https://golang.org/pkg/text/template/) markup to expand properties known at build time.  Because this content will be parsed by [ConvertToTemplateExpression](https://godoc.org/github.com/mweagle/Sparta/aws/cloudformation#ConvertToTemplateExpression) (next section), it's also possible to use [Fn::Join](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html) compatible JSON serializations (single line only) to reference properties that are known only during CloudFormation provision time.
   129  
   130  For example, if we were also provisioning a PostgreSQL instance and needed to dynamically discover the endpoint address, a shell script variable could be assigned via:
   131  
   132  ```bash
   133  POSTGRES_ADDRESS={ "Fn::GetAtt" : [ "{{ .DBInstanceResourceName }}" , "Endpoint.Address" ] }
   134  ```
   135  
   136  This expression combines both a build-time variable (`DBInstanceResourceName`: the CloudFormation resource name) and a provision time one (`Endpoint.Address`: dynamically assigned by the CloudFormation [RDS Resource](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html)).
   137  
   138  ## Decorate Toplogy
   139  
   140  The final step is to use a [TemplateDecorator](https://godoc.org/github.com/mweagle/Sparta#TemplateDecorator) to tie everything together. A decorator can annotate the CloudFormation template with any supported [go-cloudformation](https://github.com/crewjam/go-cloudformation) resource.  For this example, we'll create a single AutoScalingGroup and EC2 instance that's bootstrapped with our custom _userdata.sh_ script.
   141  
   142  ```go
   143  
   144  // The CloudFormation template decorator that inserts all the other
   145  // AWS components we need to support this deployment...
   146  func lambdaDecorator(customResourceAMILookupName string) sparta.TemplateDecorator {
   147  
   148    return func(serviceName string,
   149  	lambdaResourceName string,
   150  	lambdaResource gocf.LambdaFunction,
   151  	resourceMetadata map[string]interface{},
   152  	S3Bucket string,
   153  	S3Key string,
   154  	buildID string,
   155  	cfTemplate *gocf.Template,
   156  	context map[string]interface{},
   157  	logger *logrus.Logger) error {
   158  
   159      // Create the launch configuration with Metadata to download the ZIP file, unzip it & launch the
   160      // golang binary...
   161      ec2SecurityGroupResourceName := sparta.CloudFormationResourceName("SpartaOmegaSecurityGroup",
   162        "SpartaOmegaSecurityGroup")
   163      asgLaunchConfigurationName := sparta.CloudFormationResourceName("SpartaOmegaASGLaunchConfig",
   164        "SpartaOmegaASGLaunchConfig")
   165      asgResourceName := sparta.CloudFormationResourceName("SpartaOmegaASG",
   166        "SpartaOmegaASG")
   167      ec2InstanceRoleName := sparta.CloudFormationResourceName("SpartaOmegaEC2InstanceRole",
   168        "SpartaOmegaEC2InstanceRole")
   169      ec2InstanceProfileName := sparta.CloudFormationResourceName("SpartaOmegaEC2InstanceProfile",
   170        "SpartaOmegaEC2InstanceProfile")
   171  
   172      //////////////////////////////////////////////////////////////////////////////
   173      // 1 - Create the security group for the SpartaOmega EC2 instance
   174      ec2SecurityGroup := &gocf.EC2SecurityGroup{
   175        GroupDescription: gocf.String("SpartaOmega Security Group"),
   176        SecurityGroupIngress: &gocf.EC2SecurityGroupRuleList{
   177          gocf.EC2SecurityGroupRule{
   178            CidrIp:     gocf.String("0.0.0.0/0"),
   179            IpProtocol: gocf.String("tcp"),
   180            FromPort:   gocf.Integer(HTTPServerPort),
   181            ToPort:     gocf.Integer(HTTPServerPort),
   182          },
   183          gocf.EC2SecurityGroupRule{
   184            CidrIp:     gocf.String("0.0.0.0/0"),
   185            IpProtocol: gocf.String("tcp"),
   186            FromPort:   gocf.Integer(22),
   187            ToPort:     gocf.Integer(22),
   188          },
   189        },
   190      }
   191      template.AddResource(ec2SecurityGroupResourceName, ec2SecurityGroup)
   192      //////////////////////////////////////////////////////////////////////////////
   193      // 2 - Create the ASG and associate the userdata with the EC2 init
   194      // EC2 Instance Role...
   195      statements := sparta.CommonIAMStatements.Core
   196  
   197      // Add the statement that allows us to fetch the S3 object with this compiled
   198      // binary
   199      statements = append(statements, spartaIAM.PolicyStatement{
   200        Effect:   "Allow",
   201        Action:   []string{"s3:GetObject"},
   202        Resource: gocf.String(fmt.Sprintf("arn:aws:s3:::%s/%s", S3Bucket, S3Key)),
   203      })
   204      iamPolicyList := gocf.IAMPoliciesList{}
   205      iamPolicyList = append(iamPolicyList,
   206        gocf.IAMPolicies{
   207          PolicyDocument: sparta.ArbitraryJSONObject{
   208            "Version":   "2012-10-17",
   209            "Statement": statements,
   210          },
   211          PolicyName: gocf.String("EC2Policy"),
   212        },
   213      )
   214      ec2InstanceRole := &gocf.IAMRole{
   215        AssumeRolePolicyDocument: sparta.AssumePolicyDocument,
   216        Policies:                 &iamPolicyList,
   217      }
   218      template.AddResource(ec2InstanceRoleName, ec2InstanceRole)
   219  
   220      // Create the instance profile
   221      ec2InstanceProfile := &gocf.IAMInstanceProfile{
   222        Path:  gocf.String("/"),
   223        Roles: []gocf.Stringable{gocf.Ref(ec2InstanceRoleName).String()},
   224      }
   225      template.AddResource(ec2InstanceProfileName, ec2InstanceProfile)
   226  
   227      //Now setup the properties map, expand the userdata, and attach it...
   228      userDataProps := map[string]interface{}{
   229        "S3Bucket":    S3Bucket,
   230        "S3Key":       S3Key,
   231        "ServiceName": serviceName,
   232      }
   233  
   234      userDataTemplateInput, userDataTemplateInputErr := resources.FSString(false, "/resources/source/userdata.sh")
   235      if nil != userDataTemplateInputErr {
   236        return userDataTemplateInputErr
   237      }
   238      templateReader := strings.NewReader(userDataTemplateInput)
   239      userDataExpression, userDataExpressionErr := spartaCF.ConvertToTemplateExpression(templateReader,
   240                                                                                        userDataProps)
   241      if nil != userDataExpressionErr {
   242        return userDataExpressionErr
   243      }
   244  
   245      logger.WithFields(logrus.Fields{
   246        "Parameters": userDataProps,
   247        "Expanded":   userDataExpression,
   248      }).Debug("Expanded userdata")
   249  
   250      asgLaunchConfigurationResource := &gocf.AutoScalingLaunchConfiguration{
   251        ImageId:            gocf.GetAtt(customResourceAMILookupName, "HVM"),
   252        InstanceType:       gocf.String("t2.micro"),
   253        KeyName:            gocf.String(SSHKeyName),
   254        IamInstanceProfile: gocf.Ref(ec2InstanceProfileName).String(),
   255        UserData:           gocf.Base64(userDataExpression),
   256        SecurityGroups:     gocf.StringList(gocf.GetAtt(ec2SecurityGroupResourceName, "GroupId")),
   257      }
   258      launchConfigResource := template.AddResource(asgLaunchConfigurationName,
   259        asgLaunchConfigurationResource)
   260      launchConfigResource.DependsOn = append(launchConfigResource.DependsOn,
   261        customResourceAMILookupName)
   262  
   263      // Create the ASG
   264      asgResource := &gocf.AutoScalingAutoScalingGroup{
   265        // Empty Region is equivalent to all region AZs
   266        // Ref: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html
   267        AvailabilityZones:       gocf.GetAZs(gocf.String("")),
   268        LaunchConfigurationName: gocf.Ref(asgLaunchConfigurationName).String(),
   269        MaxSize:                 gocf.String("1"),
   270        MinSize:                 gocf.String("1"),
   271      }
   272      template.AddResource(asgResourceName, asgResource)
   273      return nil
   274    }
   275  }
   276  ```
   277  
   278  There are a few things to point out in this function:
   279  
   280    * **Security Groups** - The decorator adds an ingress rule so that the endpoint is publicly accessible:
   281  ```go
   282      gocf.EC2SecurityGroupRule{
   283            CidrIp:     gocf.String("0.0.0.0/0"),
   284            IpProtocol: gocf.String("tcp"),
   285            FromPort:   gocf.Integer(HTTPServerPort),
   286            ToPort:     gocf.Integer(HTTPServerPort),
   287          }
   288  ```
   289    * **IAM Role** - In order to download the S3 archive, the EC2 IAM Policy includes a custom privilege:
   290  ```go
   291        statements = append(statements, spartaIAM.PolicyStatement{
   292        Effect:   "Allow",
   293        Action:   []string{"s3:GetObject"},
   294        Resource: gocf.String(fmt.Sprintf("arn:aws:s3:::%s/%s", S3Bucket, S3Key)),
   295      })
   296  ```
   297    * **UserData Marshaling** - Marshaling the _userdata.sh_ script is handled by `ConvertToTemplateExpression`:
   298  ```go
   299      // Now setup the properties map, expand the userdata, and attach it...
   300      userDataProps := map[string]interface{}{
   301        "S3Bucket":    S3Bucket,
   302        "S3Key":       S3Key,
   303        "ServiceName": serviceName,
   304      }
   305      // ...
   306      templateReader := strings.NewReader(userDataTemplateInput)
   307      userDataExpression, userDataExpressionErr := spartaCF.ConvertToTemplateExpression(templateReader,
   308                                                                                        userDataProps)
   309      // ...
   310      asgLaunchConfigurationResource := &gocf.AutoScalingLaunchConfiguration{
   311        // ...
   312        UserData:           gocf.Base64(userDataExpression),
   313        // ...
   314      }
   315  ```
   316    * **Custom Command Line Flags** - To externalize the SSH Key Name, the binary expects a [custom flag](/reference/application/custom_flags) (not shown above):
   317  ```go
   318    // And add the SSHKeyName option to the provision step
   319    sparta.CommandLineOptions.Provision.Flags().StringVarP(&SSHKeyName,
   320      "key",
   321      "k",
   322      "",
   323      "SSH Key Name to use for EC2 instances")
   324  ```
   325    This value is used as an input to the AutoScalingLaunchConfiguration value:
   326  ```go
   327      asgLaunchConfigurationResource := &gocf.AutoScalingLaunchConfiguration{
   328        // ...
   329        KeyName:            gocf.String(SSHKeyName),
   330        // ...
   331      }
   332  ```
   333  
   334  # Result
   335  
   336  Deploying your Go application using a mixed topology enables your "Lambda" endpoint to be addressable via AWS Lambda and standard HTTP.
   337  
   338  ## HTTP Access
   339  
   340  ```bash
   341  
   342  $ curl -vs http://ec2-52-26-146-138.us-west-2.compute.amazonaws.com:9999/
   343  *   Trying 52.26.146.138...
   344  * Connected to ec2-52-26-146-138.us-west-2.compute.amazonaws.com (52.26.146.138) port 9999 (#0)
   345  > GET / HTTP/1.1
   346  > Host: ec2-52-26-146-138.us-west-2.compute.amazonaws.com:9999
   347  > User-Agent: curl/7.43.0
   348  > Accept: */*
   349  >
   350  < HTTP/1.1 200 OK
   351  < Date: Fri, 10 Jun 2016 14:58:15 GMT
   352  < Content-Length: 29
   353  < Content-Type: text/plain; charset=utf-8
   354  <
   355  * Connection #0 to host ec2-52-26-146-138.us-west-2.compute.amazonaws.com left intact
   356  Hello world from SpartaOmega!
   357  ```
   358  
   359  ## Lambda Access
   360  
   361  ![Lambda](/images/alternative_topology/lambda.jpg)
   362  
   363  # Conclusion
   364  
   365  Mixed topology deployment is a powerful feature that enables your application to choose the right set of resources.  It provides a way for services to non-destructively migrate to AWS Lambda or shift existing Lambda workloads to alternative compute resources.
   366  
   367  # Notes
   368    - The [SpartaOmega](https://github.com/mweagle/SpartaOmega) sample application uses [supervisord](http://supervisord.org/) for process monitoring.
   369    - The current _userdata.sh_ script isn't sufficient to reconfigure in response to CloudFormation update events.  Production systems should also include [cfn-hup](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-hup.html) listeners.
   370    - Production deployments may consider [CodeDeploy](https://aws.amazon.com/codedeploy/) to assist in HA binary rollover.
   371    - Forwarding [CloudWatch Logs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html) is not handled by this sample.
   372    - Consider using HTTPS & [Let's Encrypt](https://ivopetkov.com/b/let-s-encrypt-on-ec2/) on your EC2 instances.
   373