github.com/webonyx/up@v0.7.4-0.20180808230834-91b94e551323/platform/lambda/stack/resources/resources.go (about) 1 package resources 2 3 import ( 4 "fmt" 5 "strconv" 6 7 "github.com/apex/up" 8 "github.com/apex/up/config" 9 "github.com/apex/up/internal/util" 10 "github.com/aws/aws-sdk-go/service/route53" 11 ) 12 13 // Map type. 14 type Map map[string]interface{} 15 16 // Versions is a map of stage to lambda function version. 17 type Versions map[string]string 18 19 // Config for the resource template. 20 type Config struct { 21 // Zones already present in route53. This is used to 22 // ensure that existing zones previously set up, or 23 // automatically configured when purchasing a domain 24 // are not duplicated. 25 Zones []*route53.HostedZone 26 27 // Versions map used to maintain the correct lambda 28 // function aliases when updating a stack. 29 Versions Versions 30 31 *up.Config 32 } 33 34 // New template. 35 func New(c *Config) map[string]interface{} { 36 return Map{ 37 "AWSTemplateFormatVersion": "2010-09-09", 38 "Parameters": parameters(c), 39 "Outputs": outputs(c), 40 "Resources": resources(c), 41 } 42 } 43 44 // ref of id. 45 func ref(id string) Map { 46 return Map{ 47 "Ref": id, 48 } 49 } 50 51 // get value from named ref. 52 func get(name, value string) Map { 53 return Map{ 54 "Fn::GetAtt": []string{ 55 name, 56 value, 57 }, 58 } 59 } 60 61 // join strings with delim. 62 func join(delim string, s ...interface{}) Map { 63 return Map{ 64 "Fn::Join": []interface{}{ 65 delim, 66 s, 67 }, 68 } 69 } 70 71 // stageVariable by name. 72 func stageVariable(name string) string { 73 return fmt.Sprintf("${stageVariables.%s}", name) 74 } 75 76 // lambda ARN for function name. 77 func lambdaArn(name string) Map { 78 return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", ref(name)) 79 } 80 81 // lambda ARN for function name with qualifier. 82 func lambdaArnQualifier(name, qualifier string) Map { 83 return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", join(":", ref(name), qualifier)) 84 } 85 86 // getZone returns a zone by domain or nil. 87 func getZone(c *Config, domain string) *route53.HostedZone { 88 for _, z := range c.Zones { 89 if *z.Name == domain+"." { 90 return z 91 } 92 } 93 return nil 94 } 95 96 // dnsZone returns the ref to a new zone, or id to an existing zone. 97 func dnsZone(c *Config, m Map, domain string) interface{} { 98 // already exists 99 if z := getZone(c, domain); z != nil { 100 return *z.Id 101 } 102 103 id := util.Camelcase("dns_zone_%s", domain) 104 105 // already registered for creation 106 if m[id] != nil { 107 return ref(id) 108 } 109 110 // new zone 111 m[id] = Map{ 112 "Type": "AWS::Route53::HostedZone", 113 "Properties": Map{ 114 "Name": domain, 115 }, 116 } 117 118 return ref(id) 119 } 120 121 // api sets up the app resources. 122 func api(c *Config, m Map) { 123 m["Api"] = Map{ 124 "Type": "AWS::ApiGateway::RestApi", 125 "Properties": Map{ 126 "Name": ref("Name"), 127 "Description": util.ManagedByUp(c.Description), 128 "BinaryMediaTypes": []string{ 129 "*/*", 130 }, 131 }, 132 } 133 134 integration := Map{ 135 "Type": "AWS_PROXY", 136 "IntegrationHttpMethod": "POST", 137 "Uri": join("", 138 "arn:aws:apigateway:", 139 ref("AWS::Region"), 140 ":lambda:path/2015-03-31/functions/", 141 lambdaArnQualifier("FunctionName", stageVariable("qualifier")), 142 "/invocations"), 143 } 144 145 m["ApiRootMethod"] = Map{ 146 "Type": "AWS::ApiGateway::Method", 147 "Properties": Map{ 148 "RestApiId": ref("Api"), 149 "ResourceId": get("Api", "RootResourceId"), 150 "HttpMethod": "ANY", 151 "AuthorizationType": "NONE", 152 "Integration": integration, 153 }, 154 } 155 156 m["ApiProxyResource"] = Map{ 157 "Type": "AWS::ApiGateway::Resource", 158 "Properties": Map{ 159 "RestApiId": ref("Api"), 160 "ParentId": get("Api", "RootResourceId"), 161 "PathPart": "{proxy+}", 162 }, 163 } 164 165 m["ApiProxyMethod"] = Map{ 166 "Type": "AWS::ApiGateway::Method", 167 "Properties": Map{ 168 "RestApiId": ref("Api"), 169 "ResourceId": ref("ApiProxyResource"), 170 "HttpMethod": "ANY", 171 "AuthorizationType": "NONE", 172 "Integration": integration, 173 }, 174 } 175 176 stages(c, m) 177 } 178 179 // stages sets up the stage specific resources. 180 func stages(c *Config, m Map) { 181 for _, s := range c.Stages.List() { 182 if s.IsRemote() { 183 stage(c, s, m) 184 } 185 } 186 } 187 188 // stage sets up the stage specific resources. 189 func stage(c *Config, s *config.Stage, m Map) { 190 aliasID := stageAlias(c, s, m) 191 deploymentID := stageDeployment(c, s, m, aliasID) 192 stagePermissions(c, s, m, aliasID) 193 stageDomain(c, s, m, deploymentID) 194 } 195 196 // stageAlias sets up the lambda alias and deployment and returns the alias id. 197 func stageAlias(c *Config, s *config.Stage, m Map) string { 198 id := util.Camelcase("api_function_alias_%s", s.Name) 199 version, ok := c.Versions[s.Name] 200 201 if !ok { 202 panic(fmt.Sprintf("stage %q is missing a function version mapping", s.Name)) 203 } 204 205 m[id] = Map{ 206 "Type": "AWS::Lambda::Alias", 207 "Properties": Map{ 208 "Name": s.Name, 209 "Description": util.ManagedByUp(""), 210 "FunctionName": ref("FunctionName"), 211 "FunctionVersion": version, 212 }, 213 } 214 215 return id 216 } 217 218 // stagePermissions sets up the lambda:invokeFunction permissions for API Gateway. 219 func stagePermissions(c *Config, s *config.Stage, m Map, aliasID string) { 220 id := util.Camelcase("api_lambda_permission_%s", s.Name) 221 222 m[id] = Map{ 223 "Type": "AWS::Lambda::Permission", 224 "DependsOn": aliasID, 225 "Properties": Map{ 226 "Action": "lambda:invokeFunction", 227 "FunctionName": lambdaArnQualifier("FunctionName", s.Name), 228 "Principal": "apigateway.amazonaws.com", 229 "SourceArn": join("", 230 "arn:aws:execute-api", 231 ":", 232 ref("AWS::Region"), 233 ":", 234 ref("AWS::AccountId"), 235 ":", 236 ref("Api"), 237 "/*"), 238 }, 239 } 240 } 241 242 // stageDeployment sets up the API Gateway deployment. 243 func stageDeployment(c *Config, s *config.Stage, m Map, aliasID string) string { 244 id := util.Camelcase("api_deployment_%s", s.Name) 245 246 m[id] = Map{ 247 "Type": "AWS::ApiGateway::Deployment", 248 "DependsOn": []string{"ApiRootMethod", "ApiProxyMethod", aliasID}, 249 "Properties": Map{ 250 "RestApiId": ref("Api"), 251 "StageName": s.Name, 252 "StageDescription": Map{ 253 "Variables": Map{ 254 "qualifier": s.Name, 255 }, 256 }, 257 }, 258 } 259 260 return id 261 } 262 263 // stageDomain sets up a custom domain, dns record and path mapping. 264 func stageDomain(c *Config, s *config.Stage, m Map, deploymentID string) { 265 if s.Domain == "" { 266 return 267 } 268 269 id := util.Camelcase("api_domain_%s", s.Name) 270 271 m[id] = Map{ 272 "Type": "AWS::ApiGateway::DomainName", 273 "Properties": Map{ 274 "CertificateArn": s.Cert, 275 "DomainName": s.Domain, 276 }, 277 } 278 279 stagePathMapping(c, s, m, deploymentID, id) 280 281 if s.Zone != false { 282 stageDNSRecord(c, s, m, id) 283 } 284 } 285 286 // stagePathMapping sets up the stage deployment mapping. 287 func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) { 288 id := util.Camelcase("api_domain_%s_path_mapping", s.Name) 289 290 m[id] = Map{ 291 "Type": "AWS::ApiGateway::BasePathMapping", 292 "DependsOn": []string{deploymentID, domainID}, 293 "Properties": Map{ 294 "DomainName": s.Domain, 295 "BasePath": util.BasePath(s.Path), 296 "RestApiId": ref("Api"), 297 "Stage": s.Name, 298 }, 299 } 300 } 301 302 // stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain. 303 func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) { 304 id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain) 305 zoneName := util.Domain(s.Domain) 306 307 // explicit .zone was specified 308 if s, ok := s.Zone.(string); ok { 309 zoneName = s 310 } 311 312 zone := dnsZone(c, m, zoneName) 313 314 m[id] = Map{ 315 "Type": "AWS::Route53::RecordSet", 316 "Properties": Map{ 317 "Name": s.Domain, 318 "Type": "A", 319 "Comment": util.ManagedByUp(""), 320 "HostedZoneId": zone, 321 "AliasTarget": Map{ 322 "DNSName": get(domainID, "DistributionDomainName"), 323 "HostedZoneId": "Z2FDTNDATAQYW2", 324 }, 325 }, 326 } 327 } 328 329 // dns setups the the user-defined DNS zones and records. 330 func dns(c *Config, m Map) { 331 for _, z := range c.DNS.Zones { 332 zone := dnsZone(c, m, z.Name) 333 334 for _, r := range z.Records { 335 id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type) 336 337 m[id] = Map{ 338 "Type": "AWS::Route53::RecordSet", 339 "Properties": Map{ 340 "Name": r.Name, 341 "Type": r.Type, 342 "TTL": strconv.Itoa(r.TTL), 343 "ResourceRecords": r.Value, 344 "HostedZoneId": zone, 345 "Comment": util.ManagedByUp(""), 346 }, 347 } 348 } 349 } 350 } 351 352 // resources of the stack. 353 func resources(c *Config) Map { 354 m := Map{} 355 api(c, m) 356 dns(c, m) 357 alerting(c, m) 358 warming(c, m) 359 return m 360 } 361 362 // parameters of the stack. 363 func parameters(c *Config) Map { 364 return Map{ 365 "Name": Map{ 366 "Description": "Name of application", 367 "Type": "String", 368 }, 369 "FunctionName": Map{ 370 "Description": "Name of application function", 371 "Type": "String", 372 }, 373 } 374 } 375 376 // outputs of the stack. 377 func outputs(c *Config) Map { 378 return Map{ 379 "ApiName": Map{ 380 "Description": "API name", 381 "Value": ref("Name"), 382 }, 383 "ApiFunctionName": Map{ 384 "Description": "API Lambda function name", 385 "Value": ref("FunctionName"), 386 }, 387 "ApiFunctionArn": Map{ 388 "Description": "API Lambda function ARN", 389 "Value": lambdaArn("FunctionName"), 390 }, 391 } 392 }