Skip to content

Commit c53a679

Browse files
authored
Merge pull request #7 from mdevilliers/retain-max-versions
Feature : Ability to auto deleted non-aliased functions
2 parents 088f7a1 + b8bb389 commit c53a679

File tree

10 files changed

+213
-41
lines changed

10 files changed

+213
-41
lines changed

aws/lambda_alias.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package aws
22

33
import (
4-
"log"
5-
64
"github.com/aws/aws-sdk-go/aws"
75
"github.com/aws/aws-sdk-go/aws/awserr"
86
"github.com/aws/aws-sdk-go/service/lambda"
@@ -32,9 +30,7 @@ func updateAlias(svc *lambda.Lambda, functionName, aliasName, functionVersion st
3230
FunctionVersion: aws.String(functionVersion),
3331
}
3432

35-
resp, err := svc.UpdateAlias(req)
36-
37-
log.Println("UpdateAlias : ", resp, err)
33+
_, err := svc.UpdateAlias(req)
3834

3935
if err != nil {
4036
return errors.WithStack(err)
@@ -52,9 +48,7 @@ func newAlias(svc *lambda.Lambda, functionName, aliasName, functionVersion strin
5248
FunctionVersion: aws.String(functionVersion),
5349
}
5450

55-
resp, err := svc.CreateAlias(req)
56-
57-
log.Println("CreateAlias : ", resp, err)
51+
_, err := svc.CreateAlias(req)
5852

5953
if err != nil {
6054
return errors.WithStack(err)
@@ -71,9 +65,7 @@ func aliasExists(svc *lambda.Lambda, functionName, aliasName string) (bool, erro
7165
Name: aws.String(aliasName),
7266
}
7367

74-
resp, err := svc.GetAlias(req)
75-
76-
log.Println("GetAlias : ", resp, err)
68+
_, err := svc.GetAlias(req)
7769

7870
if err != nil {
7971
if aerr, ok := err.(awserr.Error); ok {

aws/lambda_function.go

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package aws
33
import (
44
"fmt"
55
"log"
6+
"sort"
7+
"strconv"
68

79
"github.com/aws/aws-sdk-go/aws"
810
"github.com/aws/aws-sdk-go/aws/awserr"
@@ -35,9 +37,7 @@ func functionExists(svc *lambda.Lambda, name string) (bool, error) {
3537
FunctionName: aws.String(name),
3638
}
3739

38-
resp, err := svc.GetFunction(req)
39-
40-
log.Println("GetFunction : ", resp, err)
40+
_, err := svc.GetFunction(req)
4141

4242
if err != nil {
4343
if aerr, ok := err.(awserr.Error); ok {
@@ -62,8 +62,6 @@ func updateLambdaFunction(svc *lambda.Lambda, s3Bucket, s3Key string, metadata d
6262

6363
resp, err := svc.UpdateFunctionCode(req)
6464

65-
log.Println("UpdateFunctionCode : ", resp, err)
66-
6765
if err != nil {
6866
return nil, errors.WithStack(err)
6967
}
@@ -97,11 +95,119 @@ func newLambdaFunction(svc *lambda.Lambda, s3Bucket, s3Key, role string, metadat
9795

9896
resp, err := svc.CreateFunction(req)
9997

100-
log.Println("CreateFunction : ", resp, err)
101-
10298
if err != nil {
10399
return nil, errors.WithStack(err)
104100
}
105101

106102
return resp, nil
107103
}
104+
105+
func ReduceUnAliasedVersions(svc *lambda.Lambda, maxVersions int, metadata deployer.FunctionMetadata) error {
106+
107+
// get all aliased functions
108+
allAliasesReq := &lambda.ListAliasesInput{
109+
FunctionName: aws.String(metadata.FunctionName),
110+
}
111+
allAliasResp, err := svc.ListAliases(allAliasesReq)
112+
113+
if err != nil {
114+
errors.WithStack(err)
115+
}
116+
117+
// get all versions for a function
118+
versionReq := &lambda.ListVersionsByFunctionInput{
119+
FunctionName: aws.String(metadata.FunctionName),
120+
}
121+
122+
versionResp, err := svc.ListVersionsByFunction(versionReq)
123+
124+
if err != nil {
125+
errors.WithStack(err)
126+
}
127+
128+
// if there are less (or equal) versions than the max versions
129+
if len(versionResp.Versions) <= maxVersions {
130+
return nil
131+
}
132+
133+
// create an array to hold all versions without an active alias
134+
versionsUnAliased := []*lambda.FunctionConfiguration{}
135+
136+
// use the code hash to build up a list of versions without aliases
137+
for _, version := range versionResp.Versions {
138+
139+
drop := false
140+
141+
// $LATEST is a special poiter to the latest function
142+
// helpfully it isn't returned in the list of aliases
143+
// so we need a special case here
144+
if *(version.Version) == "$LATEST" {
145+
drop = true
146+
} else {
147+
// we need to loop though our list of aliases checking if that version
148+
// hasn't been assigned an alias
149+
for _, aliasedFunction := range allAliasResp.Aliases {
150+
151+
if *(aliasedFunction.FunctionVersion) == *(version.Version) {
152+
drop = true
153+
}
154+
}
155+
}
156+
157+
if !drop {
158+
versionsUnAliased = append(versionsUnAliased, version)
159+
}
160+
161+
}
162+
163+
// if the unaliased versions number less or equal to maxVersions to retain
164+
if len(versionsUnAliased) <= maxVersions {
165+
return nil
166+
}
167+
168+
// order by versions
169+
sort.Sort(byVersion(versionsUnAliased))
170+
171+
// delete all versions - the last n (maxVersions)
172+
toDelete := versionsUnAliased[0 : len(versionsUnAliased)-maxVersions]
173+
174+
for _, version := range toDelete {
175+
176+
log.Println("deleting unaliased function : ", *(version.Version))
177+
deleteRequest := &lambda.DeleteFunctionInput{
178+
FunctionName: version.FunctionName,
179+
Qualifier: version.Version,
180+
}
181+
_, err := svc.DeleteFunction(deleteRequest)
182+
183+
if err != nil {
184+
return errors.WithStack(err)
185+
}
186+
187+
}
188+
189+
return nil
190+
}
191+
192+
// byVersion implements sort.Interface for []*lambda.FunctionConfiguration based on
193+
// the Version.
194+
type byVersion []*lambda.FunctionConfiguration
195+
196+
func (a byVersion) Len() int { return len(a) }
197+
func (a byVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
198+
func (a byVersion) Less(i, j int) bool {
199+
200+
iVersion, err := strconv.Atoi(*(a[i].Version))
201+
202+
if err != nil {
203+
panic("version not a number")
204+
}
205+
206+
jVersion, _ := strconv.Atoi(*(a[j].Version))
207+
208+
if err != nil {
209+
panic("version not a number")
210+
}
211+
212+
return iVersion < jVersion
213+
}

cmd/deployer/main.go

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@ func main() {
2121
// DO NOTHING
2222
}
2323

24+
// Policy holds information for the deployer to implement
2425
type Policy struct {
25-
MaximumVersions int
26+
// MaximumUnAliasedVersions is the maximum unaliased versions of a lambda function
27+
// we want to keep. Versions with an alias are never deleted.
28+
MaximumUnAliasedVersions int
29+
30+
// ReduceUnAliasedVersions is true if MaxUnAliasedVersions has been specified
31+
ReduceUnAliasedVersions bool
2632
}
2733

34+
// S3Event struct captures the JSON structure of the event passed when a new
35+
// object is created in S3
2836
type S3Event struct {
2937
Records []struct {
3038
EventVersion string `json:"eventVersion"`
@@ -62,6 +70,9 @@ type S3Event struct {
6270
} `json:"Records"`
6371
}
6472

73+
// Handle is called when ever an object is written to S3 via the uploader.
74+
// We assume this is always a lambda function zip file and that AWS Lambda will error
75+
// if the file is not of a correct format.
6576
func Handle(evt json.RawMessage, ctx *runtime.Context) (string, error) {
6677

6778
log.Println("deployer : ", deployer.VersionString())
@@ -73,9 +84,15 @@ func Handle(evt json.RawMessage, ctx *runtime.Context) (string, error) {
7384
return "error", errors.New("DEPLOYER_FUNCTION_ROLE_ARN not set")
7485
}
7586

87+
policy, err := loadPolicy()
88+
89+
if err != nil {
90+
return "error", errors.Wrap(err, "error loading policy")
91+
}
92+
7693
s3Event := S3Event{}
7794

78-
err := json.Unmarshal(evt, &s3Event)
95+
err = json.Unmarshal(evt, &s3Event)
7996

8097
if err != nil {
8198
return "error", errors.Wrap(err, "error un-marshaling event json")
@@ -87,36 +104,48 @@ func Handle(evt json.RawMessage, ctx *runtime.Context) (string, error) {
87104
return "error", err
88105
}
89106

90-
svc := lambda.New(session, aws.NewConfig())
107+
lambdaSvc := lambda.New(session, aws.NewConfig())
108+
s3Svc := s3.New(session, aws.NewConfig())
91109

92110
bucket := s3Event.Records[0].S3.Bucket.Name
93111
key := s3Event.Records[0].S3.Object.Key
94112

95-
s3Svc := s3.New(session, aws.NewConfig())
96113
meta, err := getMetadata(s3Svc, bucket, key)
97114

98115
if err != nil {
99116
return "error", errors.Wrap(err, "error reading metadata from s3 object")
100117
}
101118

102119
// create or update the lambda function
103-
conf, err := aws_helper.CreateOrUpdateFunction(svc, bucket, key, role, meta)
120+
conf, err := aws_helper.CreateOrUpdateFunction(lambdaSvc, bucket, key, role, meta)
104121

105122
if err != nil {
106123
return "error", errors.Wrap(err, "error creating or updating lambda function")
107124
}
108125

109126
// update, create the alias
110-
err = aws_helper.CreateOrUpdateAlias(svc, conf, meta)
127+
err = aws_helper.CreateOrUpdateAlias(lambdaSvc, conf, meta)
111128

112129
if err != nil {
113130
return "error", errors.Wrap(err, "error creating or updating alias")
114131
}
115132

133+
// delete unused versions if required
134+
if policy.ReduceUnAliasedVersions {
135+
136+
err = aws_helper.ReduceUnAliasedVersions(lambdaSvc, policy.MaximumUnAliasedVersions, meta)
137+
138+
if err != nil {
139+
return "error", errors.Wrap(err, "error deleting UnAliased versions")
140+
}
141+
142+
}
143+
116144
return "ok", nil
117145

118146
}
119147

148+
// getMetadata parses the S3 object metadata
120149
func getMetadata(svc *s3.S3, s3Bucket, s3Key string) (deployer.FunctionMetadata, error) {
121150

122151
req := &s3.HeadObjectInput{
@@ -130,26 +159,26 @@ func getMetadata(svc *s3.S3, s3Bucket, s3Key string) (deployer.FunctionMetadata,
130159
return deployer.FunctionMetadata{}, err
131160
}
132161

133-
memorySize, err := strconv.ParseInt(*(resp.Metadata["Function-Memory-Size"]), 10, 64)
162+
memorySize, err := strconv.ParseInt(*(resp.Metadata[deployer.FunctionMemorySizeTag]), 10, 64)
134163

135164
if err != nil {
136165
return deployer.FunctionMetadata{}, errors.Wrap(err, "cannot parse function-memory-size")
137166
}
138167

139-
timeout, err := strconv.ParseInt(*(resp.Metadata["Function-Timeout"]), 10, 64)
168+
timeout, err := strconv.ParseInt(*(resp.Metadata[deployer.FunctionTimeoutTag]), 10, 64)
140169

141170
if err != nil {
142171
return deployer.FunctionMetadata{}, errors.Wrap(err, "cannot parse function-timeout")
143172
}
144173

145174
meta := deployer.FunctionMetadata{
146-
Description: *(resp.Metadata["Function-Description"]),
147-
FunctionName: *(resp.Metadata["Function-Name"]),
148-
Handler: *(resp.Metadata["Function-Handler"]),
149-
Runtime: *(resp.Metadata["Function-Runtime"]),
175+
Description: *(resp.Metadata[deployer.FunctionDescriptionTag]),
176+
FunctionName: *(resp.Metadata[deployer.FunctionNameTag]),
177+
Handler: *(resp.Metadata[deployer.FunctionHandlerTag]),
178+
Runtime: *(resp.Metadata[deployer.FunctionRuntimeTag]),
150179
MemorySize: int64(memorySize),
151180
Timeout: int64(timeout),
152-
Alias: *(resp.Metadata["Function-Alias"]),
181+
Alias: *(resp.Metadata[deployer.FunctionAliasTag]),
153182
EnvVars: map[string]interface{}{},
154183
}
155184

@@ -159,9 +188,33 @@ func getMetadata(svc *s3.S3, s3Bucket, s3Key string) (deployer.FunctionMetadata,
159188
err = json.Unmarshal([]byte(envVars), &meta.EnvVars)
160189

161190
if err != nil {
162-
return deployer.FunctionMetadata{}, errors.Wrap(err, "error un-marshaling envionmental vars")
191+
return deployer.FunctionMetadata{}, errors.Wrap(err, "error un-marshaling environmental vars")
163192
}
164193

165194
return meta, nil
166195

167196
}
197+
198+
func loadPolicy() (Policy, error) {
199+
200+
maxUnAliasedVersionsStr := os.Getenv("DEPLOYER_POLICY_MAX_UNALIASED_VERSIONS")
201+
202+
maxUnAliasedVersions := int64(0)
203+
var reduceUnAliasedVersions bool
204+
var err error
205+
206+
if maxUnAliasedVersionsStr != "" {
207+
208+
maxUnAliasedVersions, err = strconv.ParseInt(maxUnAliasedVersionsStr, 10, 64)
209+
210+
if err != nil {
211+
return Policy{}, err
212+
}
213+
reduceUnAliasedVersions = true
214+
}
215+
return Policy{
216+
MaximumUnAliasedVersions: int(maxUnAliasedVersions),
217+
ReduceUnAliasedVersions: reduceUnAliasedVersions,
218+
}, nil
219+
220+
}

cmd/uploader/cmd_upload.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/aws/aws-sdk-go/aws"
1010
"github.com/aws/aws-sdk-go/aws/session"
1111
"github.com/aws/aws-sdk-go/service/s3"
12+
deployer "github.com/mdevilliers/lambda-deployer"
1213
"github.com/spf13/cobra"
1314
)
1415

@@ -51,13 +52,13 @@ func newUploadCommand() *cobra.Command {
5152
Bucket: aws.String(_config.S3BucketName),
5253
Key: aws.String(fileName),
5354
Metadata: map[string]*string{
54-
"Function-Description": aws.String(_config.Description),
55-
"Function-Name": aws.String(_config.FunctionName),
56-
"Function-Handler": aws.String(_config.Handler),
57-
"Function-Runtime": aws.String(_config.Runtime),
58-
"Function-Memory-Size": aws.String(fmt.Sprintf("%d", _config.MemorySize)),
59-
"Function-Timeout": aws.String(fmt.Sprintf("%d", _config.Timeout)),
60-
"Function-Alias": aws.String(_config.Alias),
55+
deployer.FunctionDescriptionTag: aws.String(_config.Description),
56+
deployer.FunctionNameTag: aws.String(_config.FunctionName),
57+
deployer.FunctionHandlerTag: aws.String(_config.Handler),
58+
deployer.FunctionRuntimeTag: aws.String(_config.Runtime),
59+
deployer.FunctionMemorySizeTag: aws.String(fmt.Sprintf("%d", _config.MemorySize)),
60+
deployer.FunctionTimeoutTag: aws.String(fmt.Sprintf("%d", _config.Timeout)),
61+
deployer.FunctionAliasTag: aws.String(_config.Alias),
6162
},
6263
}
6364

terraform/example/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ module "auto_deployer" {
8585

8686
application = "example"
8787
environment = "staging"
88-
deployer_filepath = "../../cmd/deployer/deployer.zip"
88+
deployer_filepath = "../../cmd/deployer/lambda-deployer.zip"
8989

9090
function_role_arn = "${aws_iam_role.my_lambda_role.arn}"
9191
s3_bucket_arn = "${aws_s3_bucket.deployment_uploads.arn}"

0 commit comments

Comments
 (0)