Skip to content

Commit

Permalink
Merge pull request #1 from fujiwara/tag-management
Browse files Browse the repository at this point in the history
mangae Tags in function.json
  • Loading branch information
fujiwara authored Oct 31, 2019
2 parents f2acc22 + 3274617 commit d99acf9
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 63 deletions.
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ lambroll does,

- Create a function.
- Create a Zip archive from local directory.
- Update function code / configuration.
- Update function code / configuration / tags / aliases.

That's all.

lambroll does not,

- Manage resources related to the Lambda function.
- e.g. IAM Role, function triggers, API Gateway, etc.
- Build native extensions for Linux (AWS Lambda running environment).
- Build native binaries or extensions for Linux (AWS Lambda running environment).

When you hope to manage these resources, we recommend other deployment tools ([AWS SAM](https://aws.amazon.com/serverless/sam/), [Serverless Framework](https://serverless.com/), etc.).

Expand Down Expand Up @@ -212,9 +212,9 @@ REPORT RequestId: dcc584f5-ceaf-4109-b405-8e59ca7ae92f Duration: 597.87 ms Bille
2019/10/28 23:16:43 [info] completed
```

#### function.json
### function.json

function.json is a definition for Lambda function. JSON structure is same as `CreateFunction` for Lambda API.
function.json is a definition for Lambda function. JSON structure is based from [`CreateFunction` for Lambda API](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html).

```json
{
Expand All @@ -230,34 +230,56 @@ function.json is a definition for Lambda function. JSON structure is same as `Cr
"MemorySize": 128,
"Role": "arn:aws:iam::123456789012:role/hello_lambda_function",
"Runtime": "nodejs10.x",
"Tags": {
"Env": "dev"
},
"Timeout": 5,
"TracingConfig": {
"Mode": "PassThrough"
}
}
```

At reading the file, expand `{{ env }}` and `{{ must_env }}` syntax in JSON.
#### Tags

When "Tags" key exists in function.json, lambroll set / remove tags to the lambda function at deploy.

```json5
{
// ...
"Tags": {
"Env": "dev",
"Foo": "Bar"
}
}
```

When "Tags" key does not exist, lambroll doesn't manage tags.
If you hope to remove all tags, set `"Tags": {}` expressly.

#### Expand enviroment variables

At reading the file, lambrol evaluates `{{ env }}` and `{{ must_env }}` syntax in JSON.

For example,

```
{{ env `FOO` `default for FOO` }}
```

Environment variable `FOO` is expanded. When `FOO` is not defined, use default value.
Environment variable `FOO` is expanded here. When `FOO` is not defined, use default value.

```
{{ must_env `FOO` }}
```

Environment variable `FOO` is expanded. When `FOO` is not defined, lambroll will panic and abort.

#### .lambdaignore
### .lambdaignore

lambroll will ignore files defined in `.lambdaignore` file at creating a zip archive.

For example,
For example,

```
# comment
Expand Down
47 changes: 27 additions & 20 deletions create.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,41 @@ func (app *App) prepareFunctionCodeForDeploy(opt DeployOption, def *Function) er
return nil
}

func (app *App) create(opt DeployOption, def *lambda.CreateFunctionInput) error {
err := app.prepareFunctionCodeForDeploy(opt, def)
func (app *App) create(opt DeployOption, fn *Function) error {
err := app.prepareFunctionCodeForDeploy(opt, fn)
if err != nil {
return errors.Wrap(err, "failed to prepare function code")
}
log.Println("[info] creating function", opt.label())
log.Println("[debug]\n", def.String())
if *opt.DryRun {
return nil
log.Println("[debug]\n", fn.String())

version := "(created)"
if !*opt.DryRun {
fn.Publish = aws.Bool(true)
res, err := app.lambda.CreateFunction(fn.CreateFunctionInput)
if err != nil {
return errors.Wrap(err, "failed to create function")
}
version = *res.Version
log.Printf("[info] deployed function version %s", version)
}

def.Publish = aws.Bool(true)
res, err := app.lambda.CreateFunction(def)
if err != nil {
return errors.Wrap(err, "failed to create function")
if err := app.updateTags(fn, opt); err != nil {
return err
}
log.Printf("[info] deployed function version %s", *res.Version)

log.Printf("[info] creating alias set %s to version %s", CurrentAliasName, *res.Version)
alias, err := app.lambda.CreateAlias(&lambda.CreateAliasInput{
FunctionName: def.FunctionName,
FunctionVersion: res.Version,
Name: aws.String(CurrentAliasName),
})
if err != nil {
return errors.Wrap(err, "failed to create alias")
log.Printf("[info] creating alias set %s to version %s %s", CurrentAliasName, version, opt.label())
if !*opt.DryRun {
alias, err := app.lambda.CreateAlias(&lambda.CreateAliasInput{
FunctionName: fn.FunctionName,
FunctionVersion: aws.String(version),
Name: aws.String(CurrentAliasName),
})
if err != nil {
return errors.Wrap(err, "failed to create alias")
}
log.Println("[info] alias created")
log.Printf("[debug]\n%s", alias.String())
}
log.Println("[info] alias created")
log.Printf("[debug]\n%s", alias.String())
return nil
}
56 changes: 29 additions & 27 deletions deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,63 +70,65 @@ func (app *App) Deploy(opt DeployOption) error {
}
log.Printf("[debug] %s", opt.String())

def, err := app.loadFunction(*opt.FunctionFilePath)
fn, err := app.loadFunction(*opt.FunctionFilePath)
if err != nil {
return errors.Wrap(err, "failed to load function")
}

log.Printf("[info] starting deploy function %s", *def.FunctionName)
log.Printf("[info] starting deploy function %s", *fn.FunctionName)
_, err = app.lambda.GetFunction(&lambda.GetFunctionInput{
FunctionName: def.FunctionName,
FunctionName: fn.FunctionName,
})
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case lambda.ErrCodeResourceNotFoundException:
return app.create(opt, def)
return app.create(opt, fn)
}
}
return err
}

err = app.prepareFunctionCodeForDeploy(opt, def)
err = app.prepareFunctionCodeForDeploy(opt, fn)
if err != nil {
return errors.Wrap(err, "failed to prepare function code for deploy")
}

log.Println("[info] updating function configuration", opt.label())
confIn := &lambda.UpdateFunctionConfigurationInput{
DeadLetterConfig: def.DeadLetterConfig,
Description: def.Description,
Environment: def.Environment,
FunctionName: def.FunctionName,
Handler: def.Handler,
KMSKeyArn: def.KMSKeyArn,
Layers: def.Layers,
MemorySize: def.MemorySize,
Role: def.Role,
Runtime: def.Runtime,
Timeout: def.Timeout,
TracingConfig: def.TracingConfig,
VpcConfig: def.VpcConfig,
DeadLetterConfig: fn.DeadLetterConfig,
Description: fn.Description,
Environment: fn.Environment,
FunctionName: fn.FunctionName,
Handler: fn.Handler,
KMSKeyArn: fn.KMSKeyArn,
Layers: fn.Layers,
MemorySize: fn.MemorySize,
Role: fn.Role,
Runtime: fn.Runtime,
Timeout: fn.Timeout,
TracingConfig: fn.TracingConfig,
VpcConfig: fn.VpcConfig,
}
log.Printf("[debug]\n%s", confIn.String())

var newerVersion string
if !*opt.DryRun {
_, err := app.lambda.UpdateFunctionConfiguration(confIn)
if err != nil {
if _, err := app.lambda.UpdateFunctionConfiguration(confIn); err != nil {
return errors.Wrap(err, "failed to update function confugration")
}
}
if err := app.updateTags(fn, opt); err != nil {
return err
}

log.Println("[info] updating function code", opt.label())
codeIn := &lambda.UpdateFunctionCodeInput{
FunctionName: def.FunctionName,
ZipFile: def.Code.ZipFile,
S3Bucket: def.Code.S3Bucket,
S3Key: def.Code.S3Key,
S3ObjectVersion: def.Code.S3ObjectVersion,
FunctionName: fn.FunctionName,
ZipFile: fn.Code.ZipFile,
S3Bucket: fn.Code.S3Bucket,
S3Key: fn.Code.S3Key,
S3ObjectVersion: fn.Code.S3ObjectVersion,
}
if *opt.DryRun {
codeIn.DryRun = aws.Bool(true)
Expand All @@ -141,13 +143,13 @@ func (app *App) Deploy(opt DeployOption) error {
}
if res.Version != nil {
newerVersion = *res.Version
log.Printf("[info] deployed version %s", *res.Version)
log.Printf("[info] deployed version %s %s", *res.Version, opt.label())
}
if *opt.DryRun {
return nil
}

return app.updateAliases(*def.FunctionName, versionAlias{newerVersion, CurrentAliasName})
return app.updateAliases(*fn.FunctionName, versionAlias{newerVersion, CurrentAliasName})
}

func (app *App) updateAliases(functionName string, vs ...versionAlias) error {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/aws/aws-sdk-go v1.25.19
github.com/go-test/deep v1.0.4
github.com/hashicorp/logutils v1.0.0
github.com/kayac/go-config v0.1.0
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/pkg/errors v0.8.1
golang.org/x/net v0.0.0-20191021144547-ec77196f6094 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/aws/aws-sdk-go v1.25.19 h1:sp3xP91qIAVhWufyn9qM6Zhhn6kX06WJQcmhRj7QTX
github.com/aws/aws-sdk-go v1.25.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
Expand All @@ -20,6 +22,8 @@ github.com/kayac/go-config v0.1.0 h1:Yu087bRps9BcmWK9HcuIBGI/YLpmLXYASzPZVZKc8UY
github.com/kayac/go-config v0.1.0/go.mod h1:m8920IaLog2vC6iFDMSROoD3n98ThCBW+XzroliX3Bc=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
25 changes: 20 additions & 5 deletions init.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (app *App) Init(opt InitOption) error {
FunctionName: opt.FunctionName,
})
var c *lambda.FunctionConfiguration
exists := true
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
Expand All @@ -41,6 +42,7 @@ func (app *App) Init(opt InitOption) error {
fmt.Sprintf("arn:aws:iam:%s:role/YOUR_LAMBDA_ROLE_NAME", app.AWSAccountID()),
),
}
exists = false
default:
}
}
Expand All @@ -51,7 +53,7 @@ func (app *App) Init(opt InitOption) error {
log.Printf("[info] function %s found", *opt.FunctionName)
c = res.Configuration
}
fn := &lambda.CreateFunctionInput{
cfi := &lambda.CreateFunctionInput{
Description: c.Description,
FunctionName: c.FunctionName,
Handler: c.Handler,
Expand All @@ -61,20 +63,20 @@ func (app *App) Init(opt InitOption) error {
Timeout: c.Timeout,
}
if e := c.Environment; e != nil {
fn.Environment = &lambda.Environment{
cfi.Environment = &lambda.Environment{
Variables: e.Variables,
}
}
for _, layer := range c.Layers {
fn.Layers = append(fn.Layers, layer.Arn)
cfi.Layers = append(cfi.Layers, layer.Arn)
}
if t := c.TracingConfig; t != nil {
fn.TracingConfig = &lambda.TracingConfig{
cfi.TracingConfig = &lambda.TracingConfig{
Mode: t.Mode,
}
}
if v := c.VpcConfig; v != nil && *v.VpcId != "" {
fn.VpcConfig = &lambda.VpcConfig{
cfi.VpcConfig = &lambda.VpcConfig{
SubnetIds: v.SubnetIds,
SecurityGroupIds: v.SecurityGroupIds,
}
Expand All @@ -87,6 +89,19 @@ func (app *App) Init(opt InitOption) error {
}
}

fn := &Function{CreateFunctionInput: cfi}
if exists {
arn := app.functionArn(fn)
log.Printf("[debug] listing tags of %s", arn)
tags, err := app.lambda.ListTags(&lambda.ListTagsInput{
Resource: aws.String(arn),
})
if err != nil {
return errors.Wrap(err, "faled to list tags")
}
fn.Tags = tags.Tags
}

log.Printf("[info] creating %s", IgnoreFilename)
err = app.saveFile(
IgnoreFilename,
Expand Down
15 changes: 14 additions & 1 deletion lambroll.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lambroll

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
Expand All @@ -12,7 +13,19 @@ import (
)

// Function is alias for lambda.CreateFunctionInput
type Function = lambda.CreateFunctionInput
type Function struct {
*lambda.CreateFunctionInput
Tags map[string]*string `json:"Tags,omitempty"`
}

func (app *App) functionArn(fn *Function) string {
return fmt.Sprintf(
"arn:aws:lambda:%s:%s:function:%s",
*app.sess.Config.Region,
app.AWSAccountID(),
*fn.FunctionName,
)
}

var (
// IgnoreFilename defines file name includes ingore patterns at creating zip archive.
Expand Down
Loading

0 comments on commit d99acf9

Please sign in to comment.