-
Notifications
You must be signed in to change notification settings - Fork 332
Reimplement Lambda deployment QUICK_SYNC strategy #1443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
6a1da4a
40ee772
f3e6fe8
8bb1f52
91ade99
cdefecb
8bff2b1
9d52465
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ package lambda | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/aws/aws-sdk-go/aws" | ||
|
|
@@ -28,6 +29,11 @@ import ( | |
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| const defaultAliasName = "Service" | ||
|
|
||
| // ErrNotFound lambda resource occurred. | ||
| var ErrNotFound = errors.New("lambda resource not found") | ||
|
|
||
| type client struct { | ||
| region string | ||
| client *lambda.Lambda | ||
|
|
@@ -66,7 +72,31 @@ func newClient(region, profile, credentialsFile string, logger *zap.Logger) (*cl | |
| return c, nil | ||
| } | ||
|
|
||
| func (c *client) Apply(ctx context.Context, fm FunctionManifest) error { | ||
| func (c *client) AvailableFunctionName(ctx context.Context, name string) (bool, error) { | ||
| input := &lambda.GetFunctionInput{ | ||
| FunctionName: aws.String(name), | ||
| } | ||
| _, err := c.client.GetFunctionWithContext(ctx, input) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| return false, fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| return false, fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| return false, fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| // Only in case ResourceNotFound error occurred, the FunctionName is available. | ||
| case lambda.ErrCodeResourceNotFoundException: | ||
| return true, nil | ||
| } | ||
| } | ||
| return false, fmt.Errorf("unknown error given: %w", err) | ||
| } | ||
| return false, nil | ||
| } | ||
|
|
||
| func (c *client) CreateFunction(ctx context.Context, fm FunctionManifest) error { | ||
| input := &lambda.CreateFunctionInput{ | ||
| Code: &lambda.FunctionCode{ | ||
| ImageUri: aws.String(fm.Spec.ImageURI), | ||
|
|
@@ -87,6 +117,174 @@ func (c *client) Apply(ctx context.Context, fm FunctionManifest) error { | |
| return fmt.Errorf("total code size per account exceeded: %w", err) | ||
| case lambda.ErrCodeResourceNotFoundException, lambda.ErrCodeResourceNotReadyException: | ||
| return fmt.Errorf("resource error occurred: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| return fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| } | ||
| } | ||
| return fmt.Errorf("unknown error given: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (c *client) UpdateFunction(ctx context.Context, fm FunctionManifest) error { | ||
| codeInput := &lambda.UpdateFunctionCodeInput{ | ||
| FunctionName: aws.String(fm.Spec.Name), | ||
| ImageUri: aws.String(fm.Spec.ImageURI), | ||
| } | ||
| _, err := c.client.UpdateFunctionCodeWithContext(ctx, codeInput) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| return fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| return fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeCodeStorageExceededException: | ||
| return fmt.Errorf("total code size per account exceeded: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| return fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| case lambda.ErrCodeResourceConflictException: | ||
| return fmt.Errorf("resource already existed or in progress: %w", err) | ||
| } | ||
| } | ||
| return fmt.Errorf("unknown error given: %w", err) | ||
| } | ||
|
|
||
| // TODO more configurable fields from here: | ||
| // https://docs.aws.amazon.com/sdk-for-go/api/service/lambda/#UpdateFunctionConfigurationInput | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *client) PublishFunction(ctx context.Context, fm FunctionManifest) (version string, err error) { | ||
| input := &lambda.PublishVersionInput{ | ||
| FunctionName: aws.String(fm.Spec.Name), | ||
| } | ||
| cfg, err := c.client.PublishVersionWithContext(ctx, input) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| err = fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| err = fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| err = fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| case lambda.ErrCodeCodeStorageExceededException: | ||
| err = fmt.Errorf("total code size per account exceeded: %w", err) | ||
| case lambda.ErrCodeResourceNotFoundException: | ||
| err = fmt.Errorf("resource not found: %w", err) | ||
| case lambda.ErrCodeResourceConflictException: | ||
| err = fmt.Errorf("resource already existed or in progress: %w", err) | ||
| } | ||
| } | ||
| err = fmt.Errorf("unknown error given: %w", err) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overrides all previous errors.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Return fast to avoid deep nesting. aerr, ok := err.(awserr.Error)
if !ok {
return ...
}
switch aeer.Code() {
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, let's me fix this part 🙏 |
||
| return | ||
| } | ||
| version = *cfg.Version | ||
| return | ||
| } | ||
|
|
||
| // VersionTraffic presents the version, and the percent of traffic that's routed to it. | ||
| type VersionTraffic struct { | ||
| Version string | ||
| Percent float64 | ||
| } | ||
|
|
||
| func (c *client) GetTrafficConfig(ctx context.Context, fm FunctionManifest) (routingTrafficCfg []VersionTraffic, err error) { | ||
| input := &lambda.GetAliasInput{ | ||
| FunctionName: aws.String(fm.Spec.Name), | ||
| Name: aws.String(defaultAliasName), | ||
| } | ||
|
|
||
| cfg, err := c.client.GetAliasWithContext(ctx, input) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| err = fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| err = fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| err = fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| case lambda.ErrCodeResourceNotFoundException: | ||
| err = ErrNotFound | ||
| } | ||
| } | ||
| err = fmt.Errorf("unknown error given: %w", err) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here too. |
||
| return | ||
| } | ||
|
|
||
| for version := range cfg.RoutingConfig.AdditionalVersionWeights { | ||
| routingTrafficCfg = append(routingTrafficCfg, VersionTraffic{ | ||
| Version: version, | ||
| Percent: *cfg.RoutingConfig.AdditionalVersionWeights[version], | ||
| }) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| func (c *client) CreateRoutingTraffic(ctx context.Context, fm FunctionManifest, version string) error { | ||
| input := &lambda.CreateAliasInput{ | ||
| FunctionName: aws.String(fm.Spec.Name), | ||
| FunctionVersion: aws.String(version), | ||
| Name: aws.String(defaultAliasName), | ||
| } | ||
| _, err := c.client.CreateAliasWithContext(ctx, input) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| return fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| return fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| return fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| case lambda.ErrCodeResourceNotFoundException: | ||
| return fmt.Errorf("resource not found: %w", err) | ||
| case lambda.ErrCodeResourceConflictException: | ||
| return fmt.Errorf("resource already existed or in progress: %w", err) | ||
| } | ||
| } | ||
| return fmt.Errorf("unknown error given: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (c *client) UpdateRoutingTraffic(ctx context.Context, fm FunctionManifest, routingTraffic []VersionTraffic) error { | ||
| routingTrafficMap := make(map[string]*float64) | ||
| switch len(routingTraffic) { | ||
| case 2: | ||
| routingTrafficMap[routingTraffic[0].Version] = aws.Float64(routingTraffic[0].Percent) | ||
| routingTrafficMap[routingTraffic[1].Version] = aws.Float64(routingTraffic[1].Percent) | ||
| case 1: | ||
| routingTrafficMap[routingTraffic[0].Version] = aws.Float64(routingTraffic[0].Percent) | ||
| default: | ||
| return fmt.Errorf("invalid routing traffic configuration given") | ||
| } | ||
|
|
||
| input := &lambda.UpdateAliasInput{ | ||
| FunctionName: aws.String(fm.Spec.Name), | ||
| Name: aws.String(defaultAliasName), | ||
| RoutingConfig: &lambda.AliasRoutingConfiguration{ | ||
| AdditionalVersionWeights: routingTrafficMap, | ||
| }, | ||
| } | ||
|
|
||
| _, err := c.client.UpdateAliasWithContext(ctx, input) | ||
| if err != nil { | ||
| if aerr, ok := err.(awserr.Error); ok { | ||
| switch aerr.Code() { | ||
| case lambda.ErrCodeInvalidParameterValueException: | ||
| return fmt.Errorf("invalid parameter given: %w", err) | ||
| case lambda.ErrCodeServiceException: | ||
| return fmt.Errorf("aws lambda service encountered an internal error: %w", err) | ||
| case lambda.ErrCodeTooManyRequestsException: | ||
| return fmt.Errorf("request throughput limit was exceeded: %w", err) | ||
| case lambda.ErrCodeResourceNotFoundException: | ||
| return fmt.Errorf("resource not found: %w", err) | ||
| case lambda.ErrCodeResourceConflictException: | ||
| return fmt.Errorf("resource already existed or in progress: %w", err) | ||
| } | ||
| } | ||
| return fmt.Errorf("unknown error given: %w", err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,19 +25,28 @@ import ( | |
| "github.com/pipe-cd/pipe/pkg/config" | ||
| ) | ||
|
|
||
| const DefaultFunctionManifestFilename = "function.yaml" | ||
| const defaultFunctionManifestFilename = "function.yaml" | ||
|
|
||
| // Client is wrapper of AWS client. | ||
| type Client interface { | ||
| Apply(ctx context.Context, fm FunctionManifest) error | ||
| AvailableFunctionName(ctx context.Context, name string) (bool, error) | ||
| CreateFunction(ctx context.Context, fm FunctionManifest) error | ||
| UpdateFunction(ctx context.Context, fm FunctionManifest) error | ||
| PublishFunction(ctx context.Context, fm FunctionManifest) (version string, err error) | ||
| GetTrafficConfig(ctx context.Context, fm FunctionManifest) (routingTrafficCfg []VersionTraffic, err error) | ||
| CreateRoutingTraffic(ctx context.Context, fm FunctionManifest, version string) error | ||
| UpdateRoutingTraffic(ctx context.Context, fm FunctionManifest, routingTraffic []VersionTraffic) error | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: How about |
||
| } | ||
|
|
||
| // Registry holds a pool of aws client wrappers. | ||
| type Registry interface { | ||
| Client(name string, cfg *config.CloudProviderLambdaConfig, logger *zap.Logger) (Client, error) | ||
| } | ||
|
|
||
| // LoadFunctionManifest returns FunctionManifest object from a given Function config manifest file. | ||
| func LoadFunctionManifest(appDir, functionManifestFilename string) (FunctionManifest, error) { | ||
| if functionManifestFilename == "" { | ||
| functionManifestFilename = DefaultFunctionManifestFilename | ||
| functionManifestFilename = defaultFunctionManifestFilename | ||
| } | ||
| path := filepath.Join(appDir, functionManifestFilename) | ||
| return loadFunctionManifest(path) | ||
|
|
@@ -77,6 +86,7 @@ var defaultRegistry = ®istry{ | |
| newGroup: &singleflight.Group{}, | ||
| } | ||
|
|
||
| // DefaultRegistry returns a pool of aws clients and a mutex associated with it. | ||
| func DefaultRegistry() Registry { | ||
| return defaultRegistry | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ package lambda | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "time" | ||
|
|
||
| provider "github.com/pipe-cd/pipe/pkg/app/piped/cloudprovider/lambda" | ||
| "github.com/pipe-cd/pipe/pkg/app/piped/deploysource" | ||
|
|
@@ -82,16 +84,65 @@ func decideRevisionName(in *executor.Input, fm provider.FunctionManifest, commit | |
| return | ||
| } | ||
|
|
||
| func apply(ctx context.Context, in *executor.Input, cloudProviderName string, cloudProviderCfg *config.CloudProviderLambdaConfig, fm provider.FunctionManifest) bool { | ||
| func sync(ctx context.Context, in *executor.Input, cloudProviderName string, cloudProviderCfg *config.CloudProviderLambdaConfig, fm provider.FunctionManifest) bool { | ||
| in.LogPersister.Infof("Start applying the lambda function manifest") | ||
| client, err := provider.DefaultRegistry().Client(cloudProviderName, cloudProviderCfg, in.Logger) | ||
| if err != nil { | ||
| in.LogPersister.Errorf("Unable to create Lambda client for the provider %s: %v", cloudProviderName, err) | ||
| return false | ||
| } | ||
|
|
||
| if err := client.Apply(ctx, fm); err != nil { | ||
| in.LogPersister.Errorf("Failed to apply the lambda function manifest (%v)", err) | ||
| ok, err := client.AvailableFunctionName(ctx, fm.Spec.Name) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should rename this function
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, I will go for |
||
| if err != nil { | ||
| in.LogPersister.Errorf("Unable to validate function name %s: %v", fm.Spec.Name, err) | ||
| return false | ||
| } | ||
| if ok { | ||
| if err := client.CreateFunction(ctx, fm); err != nil { | ||
| in.LogPersister.Errorf("Failed to create lambda function %s: %v", fm.Spec.Name, err) | ||
| return false | ||
| } | ||
| } else { | ||
| if err := client.UpdateFunction(ctx, fm); err != nil { | ||
| in.LogPersister.Errorf("Failed to update lambda function %s: %v", fm.Spec.Name, err) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // Wait before ready to commit change. | ||
| in.LogPersister.Info("Waiting to update lambda function in progress...") | ||
| time.Sleep(3 * time.Minute) | ||
|
Comment on lines
+113
to
+115
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fyi, the Lambda's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really? 😢 I think fixed 3 minutes is not safe for all cases, but we don't know how much is good.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /ping
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm working on it, but shall we make another PR to fix this separately from this PR 👀
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that is ok. Let's add a TODO and do it in another PR. |
||
|
|
||
| // Commit version for applied Lambda function. | ||
| version, err := client.PublishFunction(ctx, fm) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned that when users clicked the SYNC button to sync the application without any changes.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, you guess right, due to the docs of PublishVersion, we have to make some changes on source/configuration of the function to be able to publish a new version of it. So the error returned here for that case is predictable. 👍
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a note about this lack on the AWS documentation. |
||
| if err != nil { | ||
| in.LogPersister.Errorf("Failed to commit new version for Lambda function %s: %v", fm.Spec.Name, err) | ||
| return false | ||
| } | ||
|
|
||
| _, err = client.GetTrafficConfig(ctx, fm) | ||
| // Create Alias on not yet existed. | ||
| if errors.Is(err, provider.ErrNotFound) { | ||
| if err := client.CreateRoutingTraffic(ctx, fm, version); err != nil { | ||
| in.LogPersister.Errorf("Failed to create traffic routing for Lambda function %s (version: %s): %v", fm.Spec.Name, version, err) | ||
| return false | ||
| } | ||
| in.LogPersister.Infof("Successfully applied the lambda function manifest") | ||
| return true | ||
| } | ||
| if err != nil { | ||
| in.LogPersister.Errorf("Failed to prapare traffic routing for Lambda function %s: %v", fm.Spec.Name, err) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: "prepare" |
||
| return false | ||
| } | ||
| // Update 100% traffic to the new lambda version. | ||
| routingCfg := []provider.VersionTraffic{ | ||
| { | ||
| Version: version, | ||
| Percent: 100, | ||
| }, | ||
| } | ||
| if err = client.UpdateRoutingTraffic(ctx, fm, routingCfg); err != nil { | ||
| in.LogPersister.Errorf("Failed to update traffic routing for Lambda function %s (version: %s): %v", fm.Spec.Name, version, err) | ||
| return false | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you know what, actually Kapetanios's TODO plugin uses only
TODO:orFIXME:as a keyword by default. So you'd better useTODO:if you need to open a new issue.https://kapetanios.dev/docs/plugins/todo/