github.com/mweagle/Sparta@v1.15.0/decorator/log_aggregator.go (about)

     1  package decorator
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/aws/aws-sdk-go/aws/session"
     7  	sparta "github.com/mweagle/Sparta"
     8  	spartaIAM "github.com/mweagle/Sparta/aws/iam"
     9  	spartaIAMBuilder "github.com/mweagle/Sparta/aws/iam/builder"
    10  	gocf "github.com/mweagle/go-cloudformation"
    11  	"github.com/sirupsen/logrus"
    12  )
    13  
    14  // LogAggregatorAssumePolicyDocument is the document for LogSubscription filters
    15  var LogAggregatorAssumePolicyDocument = sparta.ArbitraryJSONObject{
    16  	"Version": "2012-10-17",
    17  	"Statement": []sparta.ArbitraryJSONObject{
    18  		{
    19  			"Action": []string{"sts:AssumeRole"},
    20  			"Effect": "Allow",
    21  			"Principal": sparta.ArbitraryJSONObject{
    22  				"Service": []string{
    23  					"logs.us-west-2.amazonaws.com",
    24  				},
    25  			},
    26  		},
    27  	},
    28  }
    29  
    30  /*
    31  Inspired by
    32  
    33  https://theburningmonk.com/2018/07/centralised-logging-for-aws-lambda-revised-2018/
    34  
    35  Create a new LogAggregatorDecorator and then hook up the decorator to the
    36  desired lambda functions as in:
    37  
    38  decorator := spartaDecorators.NewLogAggregatorDecorator(kinesisResource, kinesisMapping, loggingRelay)
    39  
    40  // Add the decorator to each function
    41  for _, eachLambda := range lambdaFunctions {
    42  	if eachLambda.Decorators == nil {
    43  		eachLambda.Decorators = make([]sparta.TemplateDecoratorHandler, 0)
    44  	}
    45  	eachLambda.Decorators = append(eachLambda.Decorators, decorator)
    46  }
    47  
    48  // Add the decorator to the service
    49  workflowHooks.ServiceDecorators = []sparta.ServiceDecoratorHookHandler{decorator}
    50  */
    51  
    52  func logAggregatorResName(baseName string) string {
    53  	return sparta.CloudFormationResourceName(fmt.Sprintf("LogAggregator%s", baseName),
    54  		baseName)
    55  }
    56  
    57  // LogAggregatorDecorator is the decorator that
    58  // satisfies both the ServiceDecoratorHandler and TemplateDecoratorHandler
    59  // interfaces. It ensures that each lambda function has a CloudWatch logs
    60  // subscription that forwards to a Kinesis stream. That stream is then
    61  // subscribed to by the relay lambda function. Only log statements
    62  // of level info or higher are published to Kinesis.
    63  type LogAggregatorDecorator struct {
    64  	kinesisStreamResourceName string
    65  	iamRoleNameResourceName   string
    66  	kinesisResource           *gocf.KinesisStream
    67  	kinesisMapping            *sparta.EventSourceMapping
    68  	logRelay                  *sparta.LambdaAWSInfo
    69  }
    70  
    71  // Ensure compliance
    72  var _ sparta.ServiceDecoratorHookHandler = (*LogAggregatorDecorator)(nil)
    73  var _ sparta.TemplateDecoratorHandler = (*LogAggregatorDecorator)(nil)
    74  
    75  // KinesisLogicalResourceName returns the name of the Kinesis stream that will be provisioned
    76  // by this Decorator
    77  func (lad *LogAggregatorDecorator) KinesisLogicalResourceName() string {
    78  	return lad.kinesisStreamResourceName
    79  }
    80  
    81  // DecorateService annotates the service with the Kinesis hook
    82  func (lad *LogAggregatorDecorator) DecorateService(context map[string]interface{},
    83  	serviceName string,
    84  	template *gocf.Template,
    85  	S3Bucket string,
    86  	S3Key string,
    87  	buildID string,
    88  	awsSession *session.Session,
    89  	noop bool,
    90  	logger *logrus.Logger) error {
    91  
    92  	// Create the Kinesis Stream
    93  	template.AddResource(lad.kinesisStreamResourceName, lad.kinesisResource)
    94  
    95  	// Create the IAM role
    96  	putRecordPriv := spartaIAMBuilder.Allow("kinesis:PutRecord").
    97  		ForResource().
    98  		Attr(lad.kinesisStreamResourceName, "Arn").
    99  		ToPolicyStatement()
   100  	passRolePriv := spartaIAMBuilder.Allow("iam:PassRole").
   101  		ForResource().
   102  		Literal("arn:aws:iam::").
   103  		AccountID(":").
   104  		Literal("role/").
   105  		Literal(lad.iamRoleNameResourceName).
   106  		ToPolicyStatement()
   107  
   108  	statements := make([]spartaIAM.PolicyStatement, 0)
   109  	statements = append(statements,
   110  		putRecordPriv,
   111  		passRolePriv,
   112  	)
   113  	iamPolicyList := gocf.IAMRolePolicyList{}
   114  	iamPolicyList = append(iamPolicyList,
   115  		gocf.IAMRolePolicy{
   116  			PolicyDocument: sparta.ArbitraryJSONObject{
   117  				"Version":   "2012-10-17",
   118  				"Statement": statements,
   119  			},
   120  			PolicyName: gocf.String("LogAggregatorPolicy"),
   121  		},
   122  	)
   123  	iamLogAggregatorRole := &gocf.IAMRole{
   124  		RoleName:                 gocf.String(lad.iamRoleNameResourceName),
   125  		AssumeRolePolicyDocument: LogAggregatorAssumePolicyDocument,
   126  		Policies:                 &iamPolicyList,
   127  	}
   128  	template.AddResource(lad.iamRoleNameResourceName, iamLogAggregatorRole)
   129  
   130  	return nil
   131  }
   132  
   133  // DecorateTemplate annotates the lambda with the log forwarding sink info
   134  func (lad *LogAggregatorDecorator) DecorateTemplate(serviceName string,
   135  	lambdaResourceName string,
   136  	lambdaResource gocf.LambdaFunction,
   137  	resourceMetadata map[string]interface{},
   138  	S3Bucket string,
   139  	S3Key string,
   140  	buildID string,
   141  	template *gocf.Template,
   142  	context map[string]interface{},
   143  	logger *logrus.Logger) error {
   144  
   145  	// The relay function should consume the stream
   146  	if lad.logRelay.LogicalResourceName() == lambdaResourceName {
   147  		// Need to add a Lambda EventSourceMapping
   148  		eventSourceMappingResourceName := sparta.CloudFormationResourceName("LogAggregator",
   149  			"EventSourceMapping",
   150  			lambdaResourceName)
   151  
   152  		template.AddResource(eventSourceMappingResourceName, &gocf.LambdaEventSourceMapping{
   153  			StartingPosition: gocf.String(lad.kinesisMapping.StartingPosition),
   154  			BatchSize:        gocf.Integer(lad.kinesisMapping.BatchSize),
   155  			EventSourceArn:   gocf.GetAtt(lad.kinesisStreamResourceName, "Arn"),
   156  			FunctionName:     gocf.GetAtt(lambdaResourceName, "Arn"),
   157  		})
   158  	} else {
   159  		// The other functions should publish their logs to the stream
   160  		subscriptionName := logAggregatorResName(fmt.Sprintf("Lambda%s", lambdaResourceName))
   161  		subscriptionFilterRes := &gocf.LogsSubscriptionFilter{
   162  			DestinationArn: gocf.GetAtt(lad.kinesisStreamResourceName, "Arn"),
   163  			RoleArn:        gocf.GetAtt(lad.iamRoleNameResourceName, "Arn"),
   164  			LogGroupName: gocf.Join("",
   165  				gocf.String("/aws/lambda/"),
   166  				gocf.Ref(lambdaResourceName)),
   167  			FilterPattern: gocf.String("{$.level = info || $.level = warning || $.level = error }"),
   168  		}
   169  		template.AddResource(subscriptionName, subscriptionFilterRes)
   170  	}
   171  	return nil
   172  }
   173  
   174  // NewLogAggregatorDecorator returns a ServiceDecoratorHook that registers a Kinesis
   175  // stream lambda log aggregator
   176  func NewLogAggregatorDecorator(
   177  	kinesisResource *gocf.KinesisStream,
   178  	kinesisMapping *sparta.EventSourceMapping,
   179  	relay *sparta.LambdaAWSInfo) *LogAggregatorDecorator {
   180  
   181  	return &LogAggregatorDecorator{
   182  		kinesisStreamResourceName: logAggregatorResName("Kinesis"),
   183  		kinesisResource:           kinesisResource,
   184  		kinesisMapping:            kinesisMapping,
   185  		iamRoleNameResourceName:   logAggregatorResName("IAMRole"),
   186  		logRelay:                  relay,
   187  	}
   188  }