platform/lambda/stack/resources/resources.go
package resources
import (
"fmt"
"strconv"
"github.com/apex/up"
"github.com/apex/up/config"
"github.com/apex/up/internal/util"
"github.com/aws/aws-sdk-go/service/route53"
)
// Map type.
type Map map[string]interface{}
// Versions is a map of stage to lambda function version.
type Versions map[string]string
// Config for the resource template.
type Config struct {
// Zones already present in route53. This is used to
// ensure that existing zones previously set up, or
// automatically configured when purchasing a domain
// are not duplicated.
Zones []*route53.HostedZone
// Versions map used to maintain the correct lambda
// function aliases when updating a stack.
Versions Versions
*up.Config
}
// New template.
func New(c *Config) map[string]interface{} {
return Map{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": parameters(c),
"Outputs": outputs(c),
"Resources": resources(c),
}
}
// ref of id.
func ref(id string) Map {
return Map{
"Ref": id,
}
}
// get value from named ref.
func get(name, value string) Map {
return Map{
"Fn::GetAtt": []string{
name,
value,
},
}
}
// join strings with delim.
func join(delim string, s ...interface{}) Map {
return Map{
"Fn::Join": []interface{}{
delim,
s,
},
}
}
// stageVariable by name.
func stageVariable(name string) string {
return fmt.Sprintf("${stageVariables.%s}", name)
}
// lambda ARN for function name.
func lambdaArn(name string) Map {
return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", ref(name))
}
// lambda ARN for function name with qualifier.
func lambdaArnQualifier(name, qualifier string) Map {
return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", join(":", ref(name), qualifier))
}
// getZone returns a zone by domain or nil.
func getZone(c *Config, domain string) *route53.HostedZone {
for _, z := range c.Zones {
if *z.Name == domain+"." {
return z
}
}
return nil
}
// dnsZone returns the ref to a new zone, or id to an existing zone.
func dnsZone(c *Config, m Map, domain string) interface{} {
// already exists
if z := getZone(c, domain); z != nil {
return *z.Id
}
id := util.Camelcase("dns_zone_%s", domain)
// already registered for creation
if m[id] != nil {
return ref(id)
}
// new zone
m[id] = Map{
"Type": "AWS::Route53::HostedZone",
"DeletionPolicy": "Retain",
"UpdateReplacePolicy": "Retain",
"Properties": Map{
"Name": domain,
},
}
return ref(id)
}
// api sets up the app resources.
func api(c *Config, m Map) {
m["Api"] = Map{
"Type": "AWS::ApiGateway::RestApi",
"Properties": Map{
"Name": ref("Name"),
"Description": util.ManagedByUp(c.Description),
"BinaryMediaTypes": []string{
"*/*",
},
},
}
integration := Map{
"Type": "AWS_PROXY",
"IntegrationHttpMethod": "POST",
"Uri": join("",
"arn:aws:apigateway:",
ref("AWS::Region"),
":lambda:path/2015-03-31/functions/",
lambdaArnQualifier("FunctionName", stageVariable("qualifier")),
"/invocations"),
}
m["ApiRootMethod"] = Map{
"Type": "AWS::ApiGateway::Method",
"Properties": Map{
"RestApiId": ref("Api"),
"ResourceId": get("Api", "RootResourceId"),
"HttpMethod": "ANY",
"AuthorizationType": "NONE",
"Integration": integration,
},
}
m["ApiProxyResource"] = Map{
"Type": "AWS::ApiGateway::Resource",
"Properties": Map{
"RestApiId": ref("Api"),
"ParentId": get("Api", "RootResourceId"),
"PathPart": "{proxy+}",
},
}
m["ApiProxyMethod"] = Map{
"Type": "AWS::ApiGateway::Method",
"Properties": Map{
"RestApiId": ref("Api"),
"ResourceId": ref("ApiProxyResource"),
"HttpMethod": "ANY",
"AuthorizationType": "NONE",
"Integration": integration,
},
}
stages(c, m)
}
// stages sets up the stage specific resources.
func stages(c *Config, m Map) {
for _, s := range c.Stages.List() {
if s.IsRemote() {
stage(c, s, m)
}
}
}
// stage sets up the stage specific resources.
func stage(c *Config, s *config.Stage, m Map) {
aliasID := stageAlias(c, s, m)
deploymentID := stageDeployment(c, s, m, aliasID)
stagePermissions(c, s, m, aliasID)
stageDomain(c, s, m, deploymentID)
}
// stageAlias sets up the lambda alias and deployment and returns the alias id.
func stageAlias(c *Config, s *config.Stage, m Map) string {
id := util.Camelcase("api_function_alias_%s", s.Name)
version, ok := c.Versions[s.Name]
if !ok {
panic(fmt.Sprintf("stage %q is missing a function version mapping", s.Name))
}
m[id] = Map{
"Type": "AWS::Lambda::Alias",
"Properties": Map{
"Name": s.Name,
"Description": util.ManagedByUp(""),
"FunctionName": ref("FunctionName"),
"FunctionVersion": version,
},
}
return id
}
// stagePermissions sets up the lambda:invokeFunction permissions for API Gateway.
func stagePermissions(c *Config, s *config.Stage, m Map, aliasID string) {
id := util.Camelcase("api_lambda_permission_%s", s.Name)
m[id] = Map{
"Type": "AWS::Lambda::Permission",
"DependsOn": aliasID,
"Properties": Map{
"Action": "lambda:invokeFunction",
"FunctionName": lambdaArnQualifier("FunctionName", s.Name),
"Principal": "apigateway.amazonaws.com",
"SourceArn": join("",
"arn:aws:execute-api",
":",
ref("AWS::Region"),
":",
ref("AWS::AccountId"),
":",
ref("Api"),
"/*"),
},
}
}
// stageDeployment sets up the API Gateway deployment.
func stageDeployment(c *Config, s *config.Stage, m Map, aliasID string) string {
id := util.Camelcase("api_deployment_%s", s.Name)
m[id] = Map{
"Type": "AWS::ApiGateway::Deployment",
"DependsOn": []string{"ApiRootMethod", "ApiProxyMethod", aliasID},
"Properties": Map{
"RestApiId": ref("Api"),
"StageName": s.Name,
"StageDescription": Map{
"Variables": Map{
"qualifier": s.Name,
},
},
},
}
return id
}
// stageDomain sets up a custom domain, dns record and path mapping.
func stageDomain(c *Config, s *config.Stage, m Map, deploymentID string) {
if s.Domain == "" {
return
}
id := util.Camelcase("api_domain_%s", s.Name)
m[id] = Map{
"Type": "AWS::ApiGateway::DomainName",
"Properties": Map{
"CertificateArn": s.Cert,
"DomainName": s.Domain,
},
}
stagePathMapping(c, s, m, deploymentID, id)
if s.Zone != false {
stageDNSRecord(c, s, m, id)
}
}
// stagePathMapping sets up the stage deployment mapping.
func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) {
id := util.Camelcase("api_domain_%s_path_mapping", s.Name)
m[id] = Map{
"Type": "AWS::ApiGateway::BasePathMapping",
"DependsOn": []string{deploymentID, domainID},
"Properties": Map{
"DomainName": s.Domain,
"BasePath": util.BasePath(s.Path),
"RestApiId": ref("Api"),
"Stage": s.Name,
},
}
}
// stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain.
func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) {
id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain)
zoneName := util.Domain(s.Domain)
// explicit .zone was specified
if s, ok := s.Zone.(string); ok {
zoneName = s
}
zone := dnsZone(c, m, zoneName)
m[id] = Map{
"Type": "AWS::Route53::RecordSet",
"Properties": Map{
"Name": s.Domain,
"Type": "A",
"Comment": util.ManagedByUp(""),
"HostedZoneId": zone,
"AliasTarget": Map{
"DNSName": get(domainID, "DistributionDomainName"),
"HostedZoneId": "Z2FDTNDATAQYW2",
},
},
}
}
// dns setups the the user-defined DNS zones and records.
func dns(c *Config, m Map) {
for _, z := range c.DNS.Zones {
zone := dnsZone(c, m, z.Name)
for _, r := range z.Records {
id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type)
m[id] = Map{
"Type": "AWS::Route53::RecordSet",
"Properties": Map{
"Name": r.Name,
"Type": r.Type,
"TTL": strconv.Itoa(r.TTL),
"ResourceRecords": r.Value,
"HostedZoneId": zone,
"Comment": util.ManagedByUp(""),
},
}
}
}
}
// resources of the stack.
func resources(c *Config) Map {
m := Map{}
api(c, m)
dns(c, m)
return m
}
// parameters of the stack.
func parameters(c *Config) Map {
return Map{
"Name": Map{
"Description": "Name of application",
"Type": "String",
},
"FunctionName": Map{
"Description": "Name of application function",
"Type": "String",
},
}
}
// outputs of the stack.
func outputs(c *Config) Map {
return Map{
"ApiName": Map{
"Description": "API name",
"Value": ref("Name"),
},
"ApiFunctionName": Map{
"Description": "API Lambda function name",
"Value": ref("FunctionName"),
},
"ApiFunctionArn": Map{
"Description": "API Lambda function ARN",
"Value": lambdaArn("FunctionName"),
},
}
}