Skip to content
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

Support Canary & Blue/Green for ECS by pipectl init #4801

Merged
merged 22 commits into from Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
276 changes: 243 additions & 33 deletions pkg/app/pipectl/cmd/initialize/ecs.go
Expand Up @@ -15,57 +15,57 @@
package initialize

import (
"fmt"
"time"

"github.com/pipe-cd/pipecd/pkg/app/pipectl/cmd/initialize/prompt"
"github.com/pipe-cd/pipecd/pkg/config"
"github.com/pipe-cd/pipecd/pkg/model"
)

// Use genericConfigs in order to simplify using the GenericApplicationSpec and keep the order as we want.
type genericECSApplicationSpec struct {
Name string `json:"name"`
Input config.ECSDeploymentInput `json:"input"`
Pipeline genericDeploymentPipeline `json:"pipeline,omitempty"`
Description string `json:"description,omitempty"`
}

func generateECSConfig(p prompt.Prompt) (*genericConfig, error) {
// inputs
var (
appName string
serviceDefFile string
taskDefFile string
targetGroupArn string
containerName string
containerPort int
appName string = "<YOUR_APPLICATION_NAME>"
serviceDefFile string = "<YOUR_SERVICE_DEFINITION_FILE>"
taskDefFile string = "<YOUR_TASK_DEFINITION_FILE>"

deploymentStrategy string = "0" // QuickSync by default
)

const (
deploymentStrategyQuickSync = "0"
deploymentStrategyCanary = "1"
deploymentStrategyBlueGreen = "2"
)

inputs := []prompt.Input{
{
Message: "Name of the application",
TargetPointer: &appName,
Required: true,
Required: false,
},
{
Message: "Name of the service definition file (e.g. serviceDef.yaml)",
TargetPointer: &serviceDefFile,
Required: true,
Required: false,
},
{
Message: "Name of the task definition file (e.g. taskDef.yaml)",
TargetPointer: &taskDefFile,
Required: true,
},
// target group inputs
{
Message: "ARN of the target group to the service",
TargetPointer: &targetGroupArn,
Required: false,
},
{
Message: "Name of the container of the target group",
TargetPointer: &containerName,
Required: false,
},
{
Message: "Port number of the container of the target group",
TargetPointer: &containerPort,
Message: fmt.Sprintf("Deployment strategy [%s]QuickSync [%s]Canary [%s]BlueGreen", deploymentStrategyQuickSync, deploymentStrategyCanary, deploymentStrategyBlueGreen),
TargetPointer: &deploymentStrategy,
Required: false,
},
}
Expand All @@ -75,25 +75,235 @@
return nil, err
}

var in *config.ECSDeploymentInput
var pipeline *genericDeploymentPipeline
switch deploymentStrategy {
case deploymentStrategyQuickSync:
in, err = inputECSQuickSync(&p)
case deploymentStrategyCanary:
in, pipeline, err = inputECSCanary(&p)
case deploymentStrategyBlueGreen:
in, pipeline, err = inputECSBlueGreen(&p)
default:
return nil, fmt.Errorf("invalid deployment strategy: %s", deploymentStrategy)
}
if err != nil {
return nil, err
}

Check warning on line 92 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L91-L92

Added lines #L91 - L92 were not covered by tests

in.ServiceDefinitionFile = serviceDefFile
in.TaskDefinitionFile = taskDefFile

spec := &genericECSApplicationSpec{
Name: appName,
Input: config.ECSDeploymentInput{
ServiceDefinitionFile: serviceDefFile,
TaskDefinitionFile: taskDefFile,
TargetGroups: config.ECSTargetGroups{
Primary: &config.ECSTargetGroup{
TargetGroupArn: targetGroupArn,
ContainerName: containerName,
ContainerPort: containerPort,
},
},
},
Name: appName,
Input: *in,
Description: "Generated by `pipectl init`. See https://pipecd.dev/docs/user-guide/configuration-reference/ for more.",
}
if pipeline != nil {
spec.Pipeline = *pipeline
}

return &genericConfig{
Kind: config.KindECSApp,
APIVersion: config.VersionV1Beta1,
ApplicationSpec: spec,
}, nil
}

func inputECSQuickSync(p *prompt.Prompt) (*config.ECSDeploymentInput, error) {
tg, err := inputECSTargetGroup(p, "")
if err != nil {
return nil, err
}

Check warning on line 117 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L116-L117

Added lines #L116 - L117 were not covered by tests

return &config.ECSDeploymentInput{
TargetGroups: config.ECSTargetGroups{
Primary: tg,
},
}, nil
}

func inputECSCanary(p *prompt.Prompt) (*config.ECSDeploymentInput, *genericDeploymentPipeline, error) {
// target groups configs
primaryTarget, err := inputECSTargetGroup(p, "(primary TaskSet)")
if err != nil {
return nil, nil, err
}

Check warning on line 131 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L130-L131

Added lines #L130 - L131 were not covered by tests

canaryTarget, err := inputECSTargetGroup(p, "(canary TaskSet)")
if err != nil {
return nil, nil, err
}

Check warning on line 136 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L135-L136

Added lines #L135 - L136 were not covered by tests

deploymentInput := config.ECSDeploymentInput{
TargetGroups: config.ECSTargetGroups{
Primary: primaryTarget,
Canary: canaryTarget,
},
}

// pipeline configs
var (
canaryTrafficPercent int = 10
)
inputs := []prompt.Input{
{
Message: fmt.Sprintf("Percentage of traffic to canary (default:%d)", canaryTrafficPercent),
TargetPointer: &canaryTrafficPercent,
Required: false,
},
}

err = p.RunSlice(inputs)
if err != nil {
return nil, nil, err
}

Check warning on line 160 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L159-L160

Added lines #L159 - L160 were not covered by tests

pipeline := &genericDeploymentPipeline{
Stages: []genericPipelineStage{
{
Name: model.StageECSCanaryRollout,
With: &config.ECSCanaryRolloutStageOptions{
Scale: config.Percentage{
// scale=trafficPercentage at first for the simpler configuration.
Number: canaryTrafficPercent,
},
},
},
{
Name: model.StageECSTrafficRouting,
With: &config.ECSTrafficRoutingStageOptions{
Canary: config.Percentage{
Number: canaryTrafficPercent,
},
},
},
{
// The simplest analysis stage, just waiting for 30 seconds.
// The purpose is to let users know AnalysisStage and adopt Progressive Delivery without human operations.
Name: model.StageAnalysis,
With: &config.AnalysisStageOptions{
Duration: config.Duration(60 * time.Second),
},
},
{
Name: model.StageECSPrimaryRollout,
},
{
Name: model.StageECSTrafficRouting,
With: &config.ECSTrafficRoutingStageOptions{
Primary: config.Percentage{
Number: 100,
},
},
},
{
Name: model.StageECSCanaryClean,
},
},
}

return &deploymentInput, pipeline, nil
}

func inputECSBlueGreen(p *prompt.Prompt) (*config.ECSDeploymentInput, *genericDeploymentPipeline, error) {
// target groups configs
primaryTarget, err := inputECSTargetGroup(p, "(primary TaskSet)")
if err != nil {
return nil, nil, err
}

Check warning on line 214 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L213-L214

Added lines #L213 - L214 were not covered by tests

canaryTarget, err := inputECSTargetGroup(p, "(canary TaskSet)")
if err != nil {
return nil, nil, err
}

Check warning on line 219 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L218-L219

Added lines #L218 - L219 were not covered by tests

deploymentInput := config.ECSDeploymentInput{
TargetGroups: config.ECSTargetGroups{
Primary: primaryTarget,
Canary: canaryTarget,
},
}

pipeline := &genericDeploymentPipeline{
Stages: []genericPipelineStage{
{
Name: model.StageECSCanaryRollout,
With: &config.ECSCanaryRolloutStageOptions{
Scale: config.Percentage{
Number: 100,
},
},
},
{
Name: model.StageECSTrafficRouting,
With: &config.ECSTrafficRoutingStageOptions{
Canary: config.Percentage{
Number: 100,
},
},
},
{
// The simplest analysis stage, just waiting for 30 seconds.
// The purpose is to let users know AnalysisStage and adopt Progressive Delivery without human operations.
Name: model.StageAnalysis,
With: &config.AnalysisStageOptions{
Duration: config.Duration(60 * time.Second),
},
},
{
Name: model.StageECSPrimaryRollout,
},
{
Name: model.StageECSTrafficRouting,
With: &config.ECSTrafficRoutingStageOptions{
Primary: config.Percentage{
Number: 100,
},
},
},
{
Name: model.StageECSCanaryClean,
},
},
}

return &deploymentInput, pipeline, nil
}

func inputECSTargetGroup(p *prompt.Prompt, annotation string) (*config.ECSTargetGroup, error) {
var (
targetGroupArn string = "<YOUR_TARGET_GROUP_ARN>"
containerName string = "<YOUR_CONTAINER_NAME>"
containerPort int = 80
)

inputs := []prompt.Input{
{
Message: fmt.Sprintf("ARN of the target group to the service %s", annotation),
TargetPointer: &targetGroupArn,
Required: false,
},
{
Message: fmt.Sprintf("Name of the container of the target group %s", annotation),
TargetPointer: &containerName,
Required: false,
},
{
Message: fmt.Sprintf("Port number of the container of the target group %s", annotation),
TargetPointer: &containerPort,
Required: false,
},
}

err := p.RunSlice(inputs)
if err != nil {
return nil, err
}

Check warning on line 302 in pkg/app/pipectl/cmd/initialize/ecs.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipectl/cmd/initialize/ecs.go#L301-L302

Added lines #L301 - L302 were not covered by tests

return &config.ECSTargetGroup{
TargetGroupArn: targetGroupArn,
ContainerName: containerName,
ContainerPort: containerPort,
}, nil
}