Skip to content

Commit

Permalink
Support Canary & Blue/Green for ECS by pipectl init (#4801)
Browse files Browse the repository at this point in the history
* Add expected YAML for kustomize

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add Kustomize pattern for pipectl init

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add Helm pattern for pipectl init

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add options for Helm

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* fix typo

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add ECS canary for pipectl init

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add pipectl init status

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* add init cmd to pipectl doc

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* add pipectl init explanation

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* fi pipectl init status

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Embed the simple AnalysisStage in pipeline by default

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Merge fix origin/master into pipectl-init-ecs-canary

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* fix prompt message and default value

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Fix output YAML structure by generic structs

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Support Blue/Green for ECS by pipectl init

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* omitempty Percentage

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Changed default values for simpler config

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Rename funcs to avoid name conflicts with other platforms

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

---------

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Co-authored-by: Khanh Tran <32532742+khanhtc1202@users.noreply.github.com>
  • Loading branch information
t-kikuc and khanhtc1202 committed Apr 22, 2024
1 parent 8f18b06 commit 16035d1
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 45 deletions.
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 @@ func generateECSConfig(p prompt.Prompt) (*genericConfig, error) {
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
}

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
}

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
}

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

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
}

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
}

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

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
}

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

0 comments on commit 16035d1

Please sign in to comment.