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  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