Repository: aws/amazon-ecs-cli-v2
Pull request #299 (2019-11-07 22:59:49)

"project init" cmd creates the stack set
Running `project init` will now create the stack set.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/Makefile b/Makefile
index 8d8585bf..37fa0218 100644
--- a/Makefile
+++ b/Makefile
@@ -98,5 +98,5 @@ gen-mocks: tools
 	${GOBIN}/mockgen -source=./internal/pkg/cli/cli.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_cli.go
 	${GOBIN}/mockgen -source=./internal/pkg/cli/completion.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_completion.go
 	${GOBIN}/mockgen -source=./internal/pkg/cli/identity.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_identity.go
-	${GOBIN}/mockgen -source=./internal/pkg/cli/env.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_env.go
 	${GOBIN}/mockgen -source=./internal/pkg/cli/app_deploy.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_projectservice.go
+	${GOBIN}/mockgen -source=./internal/pkg/cli/deploy.go -package=mocks -destination=./internal/pkg/cli/mocks/mock_deploy.go
diff --git a/go.sum b/go.sum
index 378207b8..b04a65e0 100644
--- a/go.sum
+++ b/go.sum
@@ -48,6 +48,7 @@ github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuT
 github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
 github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
 github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
+github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
 github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
 github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
diff --git a/internal/pkg/archer/project.go b/internal/pkg/archer/project.go
index 39320927..b711551f 100644
--- a/internal/pkg/archer/project.go
+++ b/internal/pkg/archer/project.go
@@ -42,8 +42,8 @@ type ProjectGetter interface {
 
 // ProjectResourceStore fetches resources related to the project.
 type ProjectResourceStore interface {
-	// Fetch all regional resources in a project.
+	// GetRegionalProjectResources fetches all regional resources in a project.
 	GetRegionalProjectResources(project *Project) ([]*ProjectRegionalResources, error)
-	// Fetch the project regional resources for a particular region.
+	// GetProjectResourcesByRegion fetches the project regional resources for a particular region.
 	GetProjectResourcesByRegion(project *Project, region string) (*ProjectRegionalResources, error)
 }
diff --git a/internal/pkg/cli/deploy.go b/internal/pkg/cli/deploy.go
new file mode 100644
index 00000000..33375acc
--- /dev/null
+++ b/internal/pkg/cli/deploy.go
@@ -0,0 +1,17 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package cli
+
+import (
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+)
+
+type environmentDeployer interface {
+	DeployEnvironment(env *deploy.CreateEnvironmentInput) error
+	StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse)
+}
+
+type projectDeployer interface {
+	DeployProject(in *deploy.CreateProjectInput) error
+}
diff --git a/internal/pkg/cli/env.go b/internal/pkg/cli/env.go
index e2159d4a..23dd6fe3 100644
--- a/internal/pkg/cli/env.go
+++ b/internal/pkg/cli/env.go
@@ -4,7 +4,6 @@
 package cli
 
 import (
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
@@ -12,12 +11,6 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/group"
 )
 
-// environmentDeployer can deploy an environment.
-type environmentDeployer interface {
-	DeployEnvironment(env *deploy.CreateEnvironmentInput) error
-	StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse)
-}
-
 // BuildEnvCmd is the top level command for environments
 func BuildEnvCmd() *cobra.Command {
 	cmd := &cobra.Command{
diff --git a/internal/pkg/cli/env_init.go b/internal/pkg/cli/env_init.go
index 6b363021..239f5799 100644
--- a/internal/pkg/cli/env_init.go
+++ b/internal/pkg/cli/env_init.go
@@ -21,6 +21,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
+const (
+	fmtDeployEnvStart    = "Proposing infrastructure changes for the %s environment."
+	fmtDeployEnvFailed   = "Failed to accept changes for the %s environment."
+	fmtStreamEnvStart    = "Creating the infrastructure for the %s environment."
+	fmtStreamEnvFailed   = "Failed to create the infrastructure for the %s environment."
+	fmtStreamEnvComplete = "Created the infrastructure for the %s environment."
+)
+
 // InitEnvOpts contains the fields to collect for adding an environment.
 type InitEnvOpts struct {
 	// Flags set by the user.
@@ -89,7 +97,7 @@ func (opts *InitEnvOpts) Execute() error {
 		ToolsAccountPrincipalARN: caller.ARN,
 	}
 
-	opts.prog.Start(fmt.Sprintf("Proposing infrastructure changes for the %s environment", color.HighlightUserInput(opts.EnvName)))
+	opts.prog.Start(fmt.Sprintf(fmtDeployEnvStart, color.HighlightUserInput(opts.EnvName)))
 	if err := opts.envDeployer.DeployEnvironment(deployEnvInput); err != nil {
 		var existsErr *cloudformation.ErrStackAlreadyExists
 		if errors.As(err, &existsErr) {
@@ -99,20 +107,20 @@ func (opts *InitEnvOpts) Execute() error {
 				color.HighlightUserInput(opts.EnvName), color.HighlightResource(opts.projectName))
 			return nil
 		}
-		opts.prog.Stop(fmt.Sprintf("%s Failed to accept changes for the %s environment", color.ErrorMarker, color.HighlightUserInput(opts.EnvName)))
+		opts.prog.Stop(log.Serrorf(fmtDeployEnvFailed, color.HighlightUserInput(opts.EnvName)))
 		return err
 	}
-	opts.prog.Start(fmt.Sprintf("Creating the infrastructure for the %s environment", color.HighlightUserInput(opts.EnvName)))
+	opts.prog.Start(fmt.Sprintf(fmtStreamEnvStart, color.HighlightUserInput(opts.EnvName)))
 	stackEvents, responses := opts.envDeployer.StreamEnvironmentCreation(deployEnvInput)
 	for stackEvent := range stackEvents {
 		opts.prog.Events(opts.humanizeEnvironmentEvents(stackEvent))
 	}
 	resp := <-responses
 	if resp.Err != nil {
-		opts.prog.Stop(fmt.Sprintf("%s Failed to create the infrastructure for the %s environment", color.ErrorMarker, color.HighlightUserInput(opts.EnvName)))
+		opts.prog.Stop(log.Serrorf(fmtStreamEnvFailed, color.HighlightUserInput(opts.EnvName)))
 		return resp.Err
 	}
-	opts.prog.Stop(fmt.Sprintf("%s Created the infrastructure for the %s environment", color.SuccessMarker, color.HighlightUserInput(opts.EnvName)))
+	opts.prog.Stop(log.Ssuccessf(fmtStreamEnvComplete, color.HighlightUserInput(opts.EnvName)))
 	if err := opts.envCreator.CreateEnvironment(resp.Env); err != nil {
 		return fmt.Errorf("store environment: %w", err)
 	}
diff --git a/internal/pkg/cli/init.go b/internal/pkg/cli/init.go
index 50bbe479..11795be8 100644
--- a/internal/pkg/cli/init.go
+++ b/internal/pkg/cli/init.go
@@ -63,12 +63,15 @@ func NewInitOpts() (*InitOpts, error) {
 	prompt := prompt.New()
 	spin := termprogress.NewSpinner()
 	id := identity.New(sess)
+	deployer := cloudformation.New(sess)
 
 	initProject := &InitProjectOpts{
 		projectStore: ssm,
 		ws:           ws,
 		prompt:       prompt,
 		identity:     id,
+		deployer:     deployer,
+		prog:         spin,
 	}
 	initApp := &InitAppOpts{
 		fs:             &afero.Afero{Fs: afero.NewOsFs()},
@@ -82,7 +85,7 @@ func NewInitOpts() (*InitOpts, error) {
 		IsProduction:  false,
 		envCreator:    ssm,
 		projectGetter: ssm,
-		envDeployer:   cloudformation.New(sess),
+		envDeployer:   deployer,
 		prog:          spin,
 		prompt:        prompt,
 		identity:      id,
diff --git a/internal/pkg/cli/mocks/mock_env.go b/internal/pkg/cli/mocks/mock_deploy.go
similarity index 64%
rename from internal/pkg/cli/mocks/mock_env.go
rename to internal/pkg/cli/mocks/mock_deploy.go
index 2e31de1f..9481b0f8 100644
--- a/internal/pkg/cli/mocks/mock_env.go
+++ b/internal/pkg/cli/mocks/mock_deploy.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: ./internal/pkg/cli/env.go
+// Source: ./internal/pkg/cli/deploy.go
 
 // Package mocks is a generated GoMock package.
 package mocks
@@ -61,3 +61,40 @@ func (mr *MockenvironmentDeployerMockRecorder) StreamEnvironmentCreation(env int
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamEnvironmentCreation", reflect.TypeOf((*MockenvironmentDeployer)(nil).StreamEnvironmentCreation), env)
 }
+
+// MockprojectDeployer is a mock of projectDeployer interface
+type MockprojectDeployer struct {
+	ctrl     *gomock.Controller
+	recorder *MockprojectDeployerMockRecorder
+}
+
+// MockprojectDeployerMockRecorder is the mock recorder for MockprojectDeployer
+type MockprojectDeployerMockRecorder struct {
+	mock *MockprojectDeployer
+}
+
+// NewMockprojectDeployer creates a new mock instance
+func NewMockprojectDeployer(ctrl *gomock.Controller) *MockprojectDeployer {
+	mock := &MockprojectDeployer{ctrl: ctrl}
+	mock.recorder = &MockprojectDeployerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockprojectDeployer) EXPECT() *MockprojectDeployerMockRecorder {
+	return m.recorder
+}
+
+// DeployProject mocks base method
+func (m *MockprojectDeployer) DeployProject(in *deploy.CreateProjectInput) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeployProject", in)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeployProject indicates an expected call of DeployProject
+func (mr *MockprojectDeployerMockRecorder) DeployProject(in interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployProject", reflect.TypeOf((*MockprojectDeployer)(nil).DeployProject), in)
+}
diff --git a/internal/pkg/cli/mocks/mock_progress.go b/internal/pkg/cli/mocks/mock_progress.go
index 70d25b0f..8a6374e1 100644
--- a/internal/pkg/cli/mocks/mock_progress.go
+++ b/internal/pkg/cli/mocks/mock_progress.go
@@ -5,10 +5,9 @@
 package mocks
 
 import (
-	reflect "reflect"
-
 	progress "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/progress"
 	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
 )
 
 // Mockprogress is a mock of progress interface
diff --git a/internal/pkg/cli/mocks/mock_prompter.go b/internal/pkg/cli/mocks/mock_prompter.go
index b4711b3a..98646bb5 100644
--- a/internal/pkg/cli/mocks/mock_prompter.go
+++ b/internal/pkg/cli/mocks/mock_prompter.go
@@ -35,6 +35,7 @@ func (m *Mockprompter) EXPECT() *MockprompterMockRecorder {
 
 // Get mocks base method
 func (m *Mockprompter) Get(message, help string, validator prompt.ValidatorFunc) (string, error) {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Get", message, help, validator)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
@@ -43,11 +44,13 @@ func (m *Mockprompter) Get(message, help string, validator prompt.ValidatorFunc)
 
 // Get indicates an expected call of Get
 func (mr *MockprompterMockRecorder) Get(message, help, validator interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*Mockprompter)(nil).Get), message, help, validator)
 }
 
 // GetSecret mocks base method
 func (m *Mockprompter) GetSecret(message, help string) (string, error) {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "GetSecret", message, help)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
@@ -56,11 +59,13 @@ func (m *Mockprompter) GetSecret(message, help string) (string, error) {
 
 // GetSecret indicates an expected call of GetSecret
 func (mr *MockprompterMockRecorder) GetSecret(message, help interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecret", reflect.TypeOf((*Mockprompter)(nil).GetSecret), message, help)
 }
 
 // SelectOne mocks base method
 func (m *Mockprompter) SelectOne(message, help string, options []string) (string, error) {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "SelectOne", message, help, options)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
@@ -69,11 +74,13 @@ func (m *Mockprompter) SelectOne(message, help string, options []string) (string
 
 // SelectOne indicates an expected call of SelectOne
 func (mr *MockprompterMockRecorder) SelectOne(message, help, options interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectOne", reflect.TypeOf((*Mockprompter)(nil).SelectOne), message, help, options)
 }
 
 // Confirm mocks base method
 func (m *Mockprompter) Confirm(message, help string, options ...prompt.ConfirmOption) (bool, error) {
+	m.ctrl.T.Helper()
 	varargs := []interface{}{message, help}
 	for _, a := range options {
 		varargs = append(varargs, a)
@@ -86,6 +93,7 @@ func (m *Mockprompter) Confirm(message, help string, options ...prompt.ConfirmOp
 
 // Confirm indicates an expected call of Confirm
 func (mr *MockprompterMockRecorder) Confirm(message, help interface{}, options ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	varargs := append([]interface{}{message, help}, options...)
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Confirm", reflect.TypeOf((*Mockprompter)(nil).Confirm), varargs...)
 }
diff --git a/internal/pkg/cli/project_init.go b/internal/pkg/cli/project_init.go
index c4c2d227..0eecec8b 100644
--- a/internal/pkg/cli/project_init.go
+++ b/internal/pkg/cli/project_init.go
@@ -10,15 +10,24 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/identity"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/session"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store/ssm"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
+	termprogress "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/progress"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/prompt"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
 	"github.com/spf13/cobra"
 )
 
+const (
+	fmtDeployProjectStart    = "Creating the infrastructure to manage container repositories under project %s."
+	fmtDeployProjectComplete = "Created the infrastructure to manage container repositories under project %s."
+	fmtDeployProjectFailed   = "Failed to create the infrastructure to manage container repositories under project %s."
+)
+
 // InitProjectOpts contains the fields to collect for creating a project.
 type InitProjectOpts struct {
 	ProjectName string
@@ -26,7 +35,9 @@ type InitProjectOpts struct {
 	identity     identityService
 	projectStore archer.ProjectStore
 	ws           archer.Workspace
+	deployer     projectDeployer
 	prompt       prompter
+	prog         progress
 }
 
 // NewInitProjectOpts returns a new InitProjectOpts.
@@ -37,7 +48,6 @@ func NewInitProjectOpts() (*InitProjectOpts, error) {
 	}
 
 	ssmStore, err := ssm.NewStore()
-
 	if err != nil {
 		return nil, err
 	}
@@ -50,7 +60,9 @@ func NewInitProjectOpts() (*InitProjectOpts, error) {
 		identity:     identity.New(defaultSession),
 		projectStore: ssmStore,
 		ws:           ws,
+		deployer:     cloudformation.New(defaultSession),
 		prompt:       prompt.New(),
+		prog:         termprogress.NewSpinner(),
 	}, nil
 }
 
@@ -105,7 +117,6 @@ func (opts *InitProjectOpts) Validate() error {
 // Execute creates a new managed empty project.
 func (opts *InitProjectOpts) Execute() error {
 	caller, err := opts.identity.Get()
-
 	if err != nil {
 		return err
 	}
@@ -114,7 +125,6 @@ func (opts *InitProjectOpts) Execute() error {
 		AccountID: caller.Account,
 		Name:      opts.ProjectName,
 	})
-
 	if err != nil {
 		// If the project already exists, move on - otherwise return the error.
 		var projectAlreadyExistsError *store.ErrProjectAlreadyExists
@@ -122,9 +132,24 @@ func (opts *InitProjectOpts) Execute() error {
 			return err
 		}
 	}
-	return opts.ws.Create(opts.ProjectName)
+	err = opts.ws.Create(opts.ProjectName)
+	if err != nil {
+		return err
+	}
+	opts.prog.Start(fmt.Sprintf(fmtDeployProjectStart, color.HighlightUserInput(opts.ProjectName)))
+	err = opts.deployer.DeployProject(&deploy.CreateProjectInput{
+		Project:   opts.ProjectName,
+		AccountID: caller.Account,
+	})
+	if err != nil {
+		opts.prog.Stop(log.Serrorf(fmtDeployProjectFailed, color.HighlightUserInput(opts.ProjectName)))
+		return err
+	}
+	opts.prog.Stop(log.Ssuccessf(fmtDeployProjectComplete, color.HighlightUserInput(opts.ProjectName)))
+	return nil
 }
 
+// RecommendedActions returns a list of suggested additional commands users can run after successfully executing this command.
 func (opts *InitProjectOpts) RecommendedActions() []string {
 	return []string{
 		fmt.Sprintf("Run %s to add a new application to your project.", color.HighlightCode("archer init")),
diff --git a/internal/pkg/deploy/cloudformation/project.go b/internal/pkg/deploy/cloudformation/project.go
index 0817012c..de1629f2 100644
--- a/internal/pkg/deploy/cloudformation/project.go
+++ b/internal/pkg/deploy/cloudformation/project.go
@@ -9,19 +9,20 @@ import (
 	"time"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/cloudformation"
 )
 
-// CreateProjectResources sets up everything required for our project-wide resources.
+// DeployProject sets up everything required for our project-wide resources.
 // These resources include things that are regional, rather than scoped to a particular
 // environment, such as ECR Repos, CodePipeline KMS keys & S3 buckets.
 // We deploy project resources through StackSets - that way we can have one
 // template that we update and all regional stacks are updated.
-func (cf CloudFormation) CreateProjectResources(project *archer.Project) error {
-	projectConfig := stack.NewProjectStackConfig(project, cf.box)
+func (cf CloudFormation) DeployProject(in *deploy.CreateProjectInput) error {
+	projectConfig := stack.NewProjectStackConfig(in, cf.box)
 
 	// First deploy the project roles needed by StackSets. These roles
 	// allow the stack set to set up our regional stacks.
@@ -40,7 +41,7 @@ func (cf CloudFormation) CreateProjectResources(project *archer.Project) error {
 	}
 
 	blankProjectTemplate, err := projectConfig.ResourceTemplate(&stack.ProjectResourcesConfig{
-		Project: projectConfig.Project.Name,
+		Project: projectConfig.Project,
 	})
 
 	if err != nil {
@@ -67,7 +68,10 @@ func (cf CloudFormation) CreateProjectResources(project *archer.Project) error {
 // Currently, this means that we'll set up an ECR repo with a policy for all envs to be able
 // to pull from it.
 func (cf CloudFormation) AddAppToProject(project *archer.Project, newApp *archer.Application) error {
-	projectConfig := stack.NewProjectStackConfig(project, cf.box)
+	projectConfig := stack.NewProjectStackConfig(&deploy.CreateProjectInput{
+		Project:   project.Name,
+		AccountID: project.AccountID,
+	}, cf.box)
 	previouslyDeployedConfig, err := cf.getLastDeployedProjectConfig(projectConfig)
 	if err != nil {
 		return fmt.Errorf("adding %s app resources to project %s: %w", newApp.Name, project.Name, err)
@@ -95,7 +99,7 @@ func (cf CloudFormation) AddAppToProject(project *archer.Project, newApp *archer
 		Version:  previouslyDeployedConfig.Version + 1,
 		Apps:     appList,
 		Accounts: previouslyDeployedConfig.Accounts,
-		Project:  projectConfig.Project.Name,
+		Project:  projectConfig.Project,
 	}
 	if err := cf.deployProjectConfig(projectConfig, &newDeploymentConfig); err != nil {
 		return fmt.Errorf("adding %s app resources to project: %w", newApp.Name, err)
@@ -108,7 +112,10 @@ func (cf CloudFormation) AddAppToProject(project *archer.Project, newApp *archer
 // with new Account IDs in resource policies (KMS Keys and ECR Repos) - and
 // sets up a new stack instance if the environment is in a new region.
 func (cf CloudFormation) AddEnvToProject(project *archer.Project, env *archer.Environment) error {
-	projectConfig := stack.NewProjectStackConfig(project, cf.box)
+	projectConfig := stack.NewProjectStackConfig(&deploy.CreateProjectInput{
+		Project:   project.Name,
+		AccountID: project.AccountID,
+	}, cf.box)
 	previouslyDeployedConfig, err := cf.getLastDeployedProjectConfig(projectConfig)
 	if err != nil {
 		return fmt.Errorf("getting previous deployed stackset %w", err)
@@ -134,7 +141,7 @@ func (cf CloudFormation) AddEnvToProject(project *archer.Project, env *archer.En
 		Version:  previouslyDeployedConfig.Version + 1,
 		Apps:     previouslyDeployedConfig.Apps,
 		Accounts: accountList,
-		Project:  projectConfig.Project.Name,
+		Project:  projectConfig.Project,
 	}
 
 	if err := cf.deployProjectConfig(projectConfig, &newDeploymentConfig); err != nil {
@@ -178,7 +185,7 @@ func (cf CloudFormation) deployProjectConfig(projectConfig *stack.ProjectStackCo
 		if aerr, ok := err.(awserr.Error); ok {
 			switch aerr.Code() {
 			case cloudformation.ErrCodeOperationIdAlreadyExistsException, cloudformation.ErrCodeOperationInProgressException, cloudformation.ErrCodeStaleRequestException:
-				return &ErrStackSetOutOfDate{projectName: projectConfig.Project.Name, parentErr: err}
+				return &ErrStackSetOutOfDate{projectName: projectConfig.Project, parentErr: err}
 			}
 		}
 		return fmt.Errorf("updating project resources: %w", err)
@@ -215,7 +222,7 @@ func (cf CloudFormation) addNewProjectStackInstances(projectConfig *stack.Projec
 	// Set up a new Stack Instance for the new region. The Stack Instance will inherit
 	// the latest StackSet template.
 	createStacksOutput, err := cf.client.CreateStackInstances(&cloudformation.CreateStackInstancesInput{
-		Accounts:     []*string{aws.String(projectConfig.Project.AccountID)},
+		Accounts:     []*string{aws.String(projectConfig.AccountID)},
 		Regions:      []*string{aws.String(env.Region)},
 		StackSetName: aws.String(projectConfig.StackSetName()),
 	})
diff --git a/internal/pkg/deploy/cloudformation/stack/project.go b/internal/pkg/deploy/cloudformation/stack/project.go
index b2df8bf1..0e2a6018 100644
--- a/internal/pkg/deploy/cloudformation/stack/project.go
+++ b/internal/pkg/deploy/cloudformation/stack/project.go
@@ -10,7 +10,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/cloudformation"
 	"gopkg.in/yaml.v3"
@@ -36,8 +36,8 @@ type ProjectResourcesConfig struct {
 // ProjectStackConfig is for providing all the values to set up an
 // environment stack and to interpret the outputs from it.
 type ProjectStackConfig struct {
-	Project *archer.Project
-	box     packd.Box
+	*deploy.CreateProjectInput
+	box packd.Box
 }
 
 const (
@@ -57,16 +57,16 @@ func ProjectConfigFrom(template *string) (*ProjectResourcesConfig, error) {
 
 // NewProjectStackConfig sets up a struct which can provide values to CloudFormation for
 // spinning up an environment.
-func NewProjectStackConfig(input *archer.Project, box packd.Box) *ProjectStackConfig {
+func NewProjectStackConfig(in *deploy.CreateProjectInput, box packd.Box) *ProjectStackConfig {
 	return &ProjectStackConfig{
-		Project: input,
-		box:     box,
+		CreateProjectInput: in,
+		box:                box,
 	}
 }
 
 // Template returns the environment CloudFormation template.
-func (e *ProjectStackConfig) Template() (string, error) {
-	template, err := e.box.FindString(projectTemplatePath)
+func (c *ProjectStackConfig) Template() (string, error) {
+	template, err := c.box.FindString(projectTemplatePath)
 	if err != nil {
 		return "", &ErrTemplateNotFound{templateLocation: projectTemplatePath, parentErr: err}
 	}
@@ -74,8 +74,8 @@ func (e *ProjectStackConfig) Template() (string, error) {
 }
 
 // ResourceTemplate generates a StackSet template with all the Project-wide resources (ECR Repos, KMS keys, S3 buckets)
-func (e *ProjectStackConfig) ResourceTemplate(config *ProjectResourcesConfig) (string, error) {
-	stackSetTemplate, err := e.box.FindString(projectResourcesTemplatePath)
+func (c *ProjectStackConfig) ResourceTemplate(config *ProjectResourcesConfig) (string, error) {
+	stackSetTemplate, err := c.box.FindString(projectResourcesTemplatePath)
 	if err != nil {
 		return "", &ErrTemplateNotFound{templateLocation: projectResourcesTemplatePath, parentErr: err}
 	}
@@ -103,57 +103,57 @@ func (e *ProjectStackConfig) ResourceTemplate(config *ProjectResourcesConfig) (s
 }
 
 // Parameters returns the parameters to be passed into a environment CloudFormation template.
-func (e *ProjectStackConfig) Parameters() []*cloudformation.Parameter {
+func (c *ProjectStackConfig) Parameters() []*cloudformation.Parameter {
 	return []*cloudformation.Parameter{
 		{
 			ParameterKey:   aws.String(projectAdminRoleParamName),
-			ParameterValue: aws.String(e.stackSetAdminRoleName()),
+			ParameterValue: aws.String(c.stackSetAdminRoleName()),
 		},
 		{
 			ParameterKey:   aws.String(projectExecutionRoleParamName),
-			ParameterValue: aws.String(e.StackSetExecutionRoleName()),
+			ParameterValue: aws.String(c.StackSetExecutionRoleName()),
 		},
 	}
 }
 
 // Tags returns the tags that should be applied to the project CloudFormation stack.
-func (e *ProjectStackConfig) Tags() []*cloudformation.Tag {
+func (c *ProjectStackConfig) Tags() []*cloudformation.Tag {
 	return []*cloudformation.Tag{
 		{
 			Key:   aws.String(projectTagKey),
-			Value: aws.String(e.Project.Name),
+			Value: aws.String(c.Project),
 		},
 	}
 }
 
 // StackName returns the name of the CloudFormation stack (based on the project name).
-func (e *ProjectStackConfig) StackName() string {
-	return fmt.Sprintf("%s-infrastructure-roles", e.Project.Name)
+func (c *ProjectStackConfig) StackName() string {
+	return fmt.Sprintf("%s-infrastructure-roles", c.Project)
 }
 
 // StackSetName returns the name of the CloudFormation StackSet (based on the project name).
-func (e *ProjectStackConfig) StackSetName() string {
-	return fmt.Sprintf("%s-infrastructure", e.Project.Name)
+func (c *ProjectStackConfig) StackSetName() string {
+	return fmt.Sprintf("%s-infrastructure", c.Project)
 }
 
 // StackSetDescription returns the description of the StackSet for project resources.
-func (e *ProjectStackConfig) StackSetDescription() string {
+func (c *ProjectStackConfig) StackSetDescription() string {
 	return "ECS CLI Project Resources (ECR repos, KMS keys, S3 buckets)"
 }
 
-func (e *ProjectStackConfig) stackSetAdminRoleName() string {
-	return fmt.Sprintf("%s-adminrole", e.Project.Name)
+func (c *ProjectStackConfig) stackSetAdminRoleName() string {
+	return fmt.Sprintf("%s-adminrole", c.Project)
 }
 
 // StackSetAdminRoleARN returns the role ARN of the role used to administer the Project
 // StackSet.
-func (e *ProjectStackConfig) StackSetAdminRoleARN() string {
+func (c *ProjectStackConfig) StackSetAdminRoleARN() string {
 	//TODO find a partition-neutral way to construct this ARN
-	return fmt.Sprintf("arn:aws:iam::%s:role/%s", e.Project.AccountID, e.stackSetAdminRoleName())
+	return fmt.Sprintf("arn:aws:iam::%s:role/%s", c.AccountID, c.stackSetAdminRoleName())
 }
 
 // StackSetExecutionRoleName returns the role name of the role used to actually create
 // Project resources.
-func (e *ProjectStackConfig) StackSetExecutionRoleName() string {
-	return fmt.Sprintf("%s-executionrole", e.Project.Name)
+func (c *ProjectStackConfig) StackSetExecutionRoleName() string {
+	return fmt.Sprintf("%s-executionrole", c.Project)
 }
diff --git a/internal/pkg/deploy/deploy.go b/internal/pkg/deploy/deploy.go
index 8ac5b635..ae6bbb2a 100644
--- a/internal/pkg/deploy/deploy.go
+++ b/internal/pkg/deploy/deploy.go
@@ -15,7 +15,7 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 )
 
-// CreateEnvironmentInput represents the fields required to deploy an environment.
+// CreateEnvironmentInput holds the fields required to deploy an environment.
 type CreateEnvironmentInput struct {
 	Project                  string // Name of the project this environment belongs to.
 	Name                     string // Name of the environment, must be unique within a project.
@@ -36,6 +36,12 @@ type CreateEnvironmentResponse struct {
 	Err error
 }
 
+// CreateProjectInput holds the fields required to create a project stack set.
+type CreateProjectInput struct {
+	Project   string // Name of the project that needs to be created.
+	AccountID string // AWS account ID to administrate the project.
+}
+
 // CreatePipelineInput represents the fields required to deploy a pipeline.
 type CreatePipelineInput struct {
 	// Name of the project this pipeline belongs to
diff --git a/internal/pkg/term/color/marker.go b/internal/pkg/term/color/marker.go
deleted file mode 100644
index b185cc55..00000000
--- a/internal/pkg/term/color/marker.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// +build !windows
-
-// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package color
-
-import "github.com/fatih/color"
-
-// String markers to denote the status of an operation.
-var (
-	SuccessMarker = color.HiGreenString("✔")
-	ErrorMarker   = color.HiRedString("✘")
-)
diff --git a/internal/pkg/term/color/marker_windows.go b/internal/pkg/term/color/marker_windows.go
deleted file mode 100644
index 5aa82ae2..00000000
--- a/internal/pkg/term/color/marker_windows.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package color
-
-import "github.com/fatih/color"
-
-// String markers to denote the status of an operation.
-var (
-	SuccessMarker = color.HiGreenString("√")
-	ErrorMarker   = color.HiRedString("X")
-)
diff --git a/internal/pkg/term/log/log.go b/internal/pkg/term/log/log.go
index e7bbcba5..be5a86d5 100644
--- a/internal/pkg/term/log/log.go
+++ b/internal/pkg/term/log/log.go
@@ -47,6 +47,23 @@ func Successf(format string, args ...interface{}) {
 	fmt.Fprintf(DiagnosticWriter, wrappedFormat, args...)
 }
 
+// Ssuccess prefixes the message with a green "✔ Success!", and returns it.
+func Ssuccess(args ...interface{}) string {
+	return fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...))
+}
+
+// Ssuccessln prefixes the message with a green "✔ Success!", appends a new line, and returns it.
+func Ssuccessln(args ...interface{}) string {
+	msg := fmt.Sprintf("%s %s", successSprintf(successPrefix), fmt.Sprint(args...))
+	return fmt.Sprintln(msg)
+}
+
+// Ssuccessf formats according to the specifier, prefixes the message with a green "✔ Success!", and returns it.
+func Ssuccessf(format string, args ...interface{}) string {
+	wrappedFormat := fmt.Sprintf("%s %s", successSprintf(successPrefix), format)
+	return fmt.Sprintf(wrappedFormat, args...)
+}
+
 // Error prefixes the message with a red "✘ Error!", and writes to standard error.
 func Error(args ...interface{}) {
 	msg := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...))
@@ -65,6 +82,23 @@ func Errorf(format string, args ...interface{}) {
 	fmt.Fprintf(DiagnosticWriter, wrappedFormat, args...)
 }
 
+// Serror prefixes the message with a red "✘ Error!", and returns it.
+func Serror(args ...interface{}) string {
+	return fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...))
+}
+
+// Serrorln prefixes the message with a red "✘ Error!", appends a new line, and returns it.
+func Serrorln(args ...interface{}) string {
+	msg := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), fmt.Sprint(args...))
+	return fmt.Sprintln(msg)
+}
+
+// Serrorf formats according to the specifier, prefixes the message with a red "✘ Error!", and returns it.
+func Serrorf(format string, args ...interface{}) string {
+	wrappedFormat := fmt.Sprintf("%s %s", errorSprintf(errorPrefix), format)
+	return fmt.Sprintf(wrappedFormat, args...)
+}
+
 // Warning prefixes the message with a "Note:", colors the *entire* message in yellow, writes to standard error.
 func Warning(args ...interface{}) {
 	msg := fmt.Sprint(args...)
diff --git a/mocks/mock_project.go b/mocks/mock_project.go
index b4f410fe..88c93fee 100644
--- a/mocks/mock_project.go
+++ b/mocks/mock_project.go
@@ -189,3 +189,56 @@ func (mr *MockProjectGetterMockRecorder) GetProject(projectName interface{}) *go
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProject", reflect.TypeOf((*MockProjectGetter)(nil).GetProject), projectName)
 }
+
+// MockProjectResourceStore is a mock of ProjectResourceStore interface
+type MockProjectResourceStore struct {
+	ctrl     *gomock.Controller
+	recorder *MockProjectResourceStoreMockRecorder
+}
+
+// MockProjectResourceStoreMockRecorder is the mock recorder for MockProjectResourceStore
+type MockProjectResourceStoreMockRecorder struct {
+	mock *MockProjectResourceStore
+}
+
+// NewMockProjectResourceStore creates a new mock instance
+func NewMockProjectResourceStore(ctrl *gomock.Controller) *MockProjectResourceStore {
+	mock := &MockProjectResourceStore{ctrl: ctrl}
+	mock.recorder = &MockProjectResourceStoreMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockProjectResourceStore) EXPECT() *MockProjectResourceStoreMockRecorder {
+	return m.recorder
+}
+
+// GetRegionalProjectResources mocks base method
+func (m *MockProjectResourceStore) GetRegionalProjectResources(project *archer.Project) ([]*archer.ProjectRegionalResources, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRegionalProjectResources", project)
+	ret0, _ := ret[0].([]*archer.ProjectRegionalResources)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRegionalProjectResources indicates an expected call of GetRegionalProjectResources
+func (mr *MockProjectResourceStoreMockRecorder) GetRegionalProjectResources(project interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalProjectResources", reflect.TypeOf((*MockProjectResourceStore)(nil).GetRegionalProjectResources), project)
+}
+
+// GetProjectResourcesByRegion mocks base method
+func (m *MockProjectResourceStore) GetProjectResourcesByRegion(project *archer.Project, region string) (*archer.ProjectRegionalResources, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetProjectResourcesByRegion", project, region)
+	ret0, _ := ret[0].(*archer.ProjectRegionalResources)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetProjectResourcesByRegion indicates an expected call of GetProjectResourcesByRegion
+func (mr *MockProjectResourceStoreMockRecorder) GetProjectResourcesByRegion(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectResourcesByRegion", reflect.TypeOf((*MockProjectResourceStore)(nil).GetProjectResourcesByRegion), project, region)
+}


Test patch:
diff --git a/internal/pkg/cli/env_init_test.go b/internal/pkg/cli/env_init_test.go
index d454bdee..d010455f 100644
--- a/internal/pkg/cli/env_init_test.go
+++ b/internal/pkg/cli/env_init_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/identity"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation"
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/color"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 	termprogress "github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/progress"
 	"github.com/aws/amazon-ecs-cli-v2/mocks"
 	"github.com/golang/mock/gomock"
@@ -161,7 +161,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				m.EXPECT().Get().Return(identity.Caller{ARN: "some arn"}, nil)
 			},
 			expectProgress: func(m *climocks.Mockprogress) {
-				m.EXPECT().Start("Proposing infrastructure changes for the test environment")
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
 				m.EXPECT().Stop("")
 			},
 			expectEnvDeployer: func(m *climocks.MockenvironmentDeployer) {
@@ -184,8 +184,8 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				m.EXPECT().Get().Return(identity.Caller{ARN: "some arn"}, nil)
 			},
 			expectProgress: func(m *climocks.Mockprogress) {
-				m.EXPECT().Start("Proposing infrastructure changes for the test environment")
-				m.EXPECT().Stop(fmt.Sprintf("%s Failed to accept changes for the test environment", color.ErrorMarker))
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
+				m.EXPECT().Stop(log.Serrorf(fmtDeployEnvFailed, "test"))
 			},
 			expectEnvDeployer: func(m *climocks.MockenvironmentDeployer) {
 				m.EXPECT().DeployEnvironment(gomock.Any()).Return(errors.New("some deploy error"))
@@ -203,8 +203,8 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				m.EXPECT().Get().Return(identity.Caller{ARN: "some arn"}, nil)
 			},
 			expectProgress: func(m *climocks.Mockprogress) {
-				m.EXPECT().Start("Proposing infrastructure changes for the test environment")
-				m.EXPECT().Start("Creating the infrastructure for the test environment")
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
+				m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
 				m.EXPECT().Events([]termprogress.TabRow{
 					termprogress.TabRow(fmt.Sprintf("%s\t[%s]", textVPC, termprogress.StatusFailed)),
 					termprogress.TabRow(fmt.Sprintf("  %s\t", "some reason")),
@@ -216,7 +216,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 					termprogress.TabRow(fmt.Sprintf("%s\t[%s]", textECSCluster, termprogress.StatusInProgress)),
 					termprogress.TabRow(fmt.Sprintf("%s\t[%s]", textALB, termprogress.StatusInProgress)),
 				})
-				m.EXPECT().Stop(fmt.Sprintf("%s Failed to create the infrastructure for the test environment", color.ErrorMarker))
+				m.EXPECT().Stop(log.Serrorf(fmtStreamEnvFailed, "test"))
 			},
 			expectEnvDeployer: func(m *climocks.MockenvironmentDeployer) {
 				m.EXPECT().DeployEnvironment(gomock.Any()).Return(nil)
@@ -252,9 +252,9 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				m.EXPECT().Get().Return(identity.Caller{ARN: "some arn"}, nil)
 			},
 			expectProgress: func(m *climocks.Mockprogress) {
-				m.EXPECT().Start("Proposing infrastructure changes for the test environment")
-				m.EXPECT().Start("Creating the infrastructure for the test environment")
-				m.EXPECT().Stop(fmt.Sprintf("%s Created the infrastructure for the test environment", color.SuccessMarker))
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
+				m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
+				m.EXPECT().Stop(log.Ssuccessf(fmtStreamEnvComplete, "test"))
 			},
 			expectEnvDeployer: func(m *climocks.MockenvironmentDeployer) {
 				m.EXPECT().DeployEnvironment(gomock.Any()).Return(nil)
@@ -290,9 +290,9 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				m.EXPECT().Get().Return(identity.Caller{ARN: "some arn"}, nil)
 			},
 			expectProgress: func(m *climocks.Mockprogress) {
-				m.EXPECT().Start("Proposing infrastructure changes for the test environment")
-				m.EXPECT().Start("Creating the infrastructure for the test environment")
-				m.EXPECT().Stop(fmt.Sprintf("%s Created the infrastructure for the test environment", color.SuccessMarker))
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
+				m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
+				m.EXPECT().Stop(log.Ssuccessf(fmtStreamEnvComplete, "test"))
 			},
 			expectEnvDeployer: func(m *climocks.MockenvironmentDeployer) {
 				m.EXPECT().DeployEnvironment(gomock.Any()).Return(nil)
diff --git a/internal/pkg/cli/project_init_test.go b/internal/pkg/cli/project_init_test.go
index 88239e54..1c787b4d 100644
--- a/internal/pkg/cli/project_init_test.go
+++ b/internal/pkg/cli/project_init_test.go
@@ -11,7 +11,9 @@ import (
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/identity"
 	climocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/mocks"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/term/log"
 	"github.com/aws/amazon-ecs-cli-v2/mocks"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
@@ -176,13 +178,18 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 	testCases := map[string]struct {
 		expectedProject archer.Project
 		expectedError   error
-		mocking         func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace, mockIdentityService *climocks.MockidentityService)
+		mocking         func(t *testing.T,
+			mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+			mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+			mockProgress *climocks.Mockprogress)
 	}{
 		"with a succesfull call to add project": {
 			expectedProject: archer.Project{
 				Name: "project",
 			},
-			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace, mockIdentityService *climocks.MockidentityService) {
+			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+				mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+				mockProgress *climocks.Mockprogress) {
 				mockIdentityService.
 					EXPECT().
 					Get().
@@ -197,15 +204,52 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 					})
 				mockWorkspace.
 					EXPECT().
-					Create(gomock.Eq("project"))
+					Create(gomock.Eq("project")).Return(nil)
+				mockProgress.EXPECT().Start(fmt.Sprintf(fmtDeployProjectStart, "project"))
+				mockDeployer.EXPECT().
+					DeployProject(&deploy.CreateProjectInput{
+						Project:   "project",
+						AccountID: "12345",
+					}).Return(nil)
+				mockProgress.EXPECT().Stop(log.Ssuccessf(fmtDeployProjectComplete, "project"))
+			},
+		},
+		"with an error while deploying project": {
+			expectedError: mockError,
+			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+				mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+				mockProgress *climocks.Mockprogress) {
+				mockIdentityService.
+					EXPECT().
+					Get().
+					Return(identity.Caller{
+						Account: "12345",
+					}, nil)
+				mockProjectStore.
+					EXPECT().
+					CreateProject(gomock.Any()).
+					Do(func(project *archer.Project) {
+						require.Equal(t, project.Name, "project")
+					})
+				mockWorkspace.
+					EXPECT().
+					Create(gomock.Eq("project")).Return(nil)
+				mockProgress.EXPECT().Start(fmt.Sprintf(fmtDeployProjectStart, "project"))
+				mockDeployer.EXPECT().
+					DeployProject(&deploy.CreateProjectInput{
+						Project:   "project",
+						AccountID: "12345",
+					}).Return(mockError)
+				mockProgress.EXPECT().Stop(log.Serrorf(fmtDeployProjectFailed, "project"))
 			},
 		},
-
 		"should ignore ErrProjectAlreadyExists from CreateProject": {
 			expectedProject: archer.Project{
 				Name: "project",
 			},
-			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace, mockIdentityService *climocks.MockidentityService) {
+			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+				mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+				mockProgress *climocks.Mockprogress) {
 				mockIdentityService.
 					EXPECT().
 					Get().
@@ -222,12 +266,21 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 				mockWorkspace.
 					EXPECT().
 					Create(gomock.Any())
+				mockProgress.EXPECT().Start(fmt.Sprintf(fmtDeployProjectStart, "project"))
+				mockDeployer.EXPECT().
+					DeployProject(&deploy.CreateProjectInput{
+						Project:   "project",
+						AccountID: "12345",
+					}).Return(nil)
+				mockProgress.EXPECT().Stop(log.Ssuccessf(fmtDeployProjectComplete, "project"))
 			},
 		},
 
 		"should return error from CreateProject": {
 			expectedError: mockError,
-			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace, mockIdentityService *climocks.MockidentityService) {
+			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+				mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+				mockProgress *climocks.Mockprogress) {
 				mockIdentityService.
 					EXPECT().
 					Get().
@@ -242,12 +295,16 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 					EXPECT().
 					Create(gomock.Any()).
 					Times(0)
+				mockProgress.EXPECT().Start(gomock.Any()).Times(0)
+				mockDeployer.EXPECT().DeployProject(gomock.Any()).Times(0)
 			},
 		},
 
 		"should return error from workspace.Create": {
 			expectedError: mockError,
-			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace, mockIdentityService *climocks.MockidentityService) {
+			mocking: func(t *testing.T, mockProjectStore *mocks.MockProjectStore, mockWorkspace *mocks.MockWorkspace,
+				mockIdentityService *climocks.MockidentityService, mockDeployer *climocks.MockprojectDeployer,
+				mockProgress *climocks.Mockprogress) {
 				mockIdentityService.
 					EXPECT().
 					Get().
@@ -262,6 +319,8 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 					EXPECT().
 					Create(gomock.Eq("project")).
 					Return(mockError)
+				mockProgress.EXPECT().Start(gomock.Any()).Times(0)
+				mockDeployer.EXPECT().DeployProject(gomock.Any()).Times(0)
 			},
 		},
 	}
@@ -273,14 +332,18 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 			mockProjectStore := mocks.NewMockProjectStore(ctrl)
 			mockWorkspace := mocks.NewMockWorkspace(ctrl)
 			mockIdentityService := climocks.NewMockidentityService(ctrl)
+			mockDeployer := climocks.NewMockprojectDeployer(ctrl)
+			mockProgress := climocks.NewMockprogress(ctrl)
 
 			opts := &InitProjectOpts{
 				ProjectName:  "project",
 				projectStore: mockProjectStore,
 				identity:     mockIdentityService,
+				deployer:     mockDeployer,
 				ws:           mockWorkspace,
+				prog:         mockProgress,
 			}
-			tc.mocking(t, mockProjectStore, mockWorkspace, mockIdentityService)
+			tc.mocking(t, mockProjectStore, mockWorkspace, mockIdentityService, mockDeployer, mockProgress)
 
 			// WHEN
 			err := opts.Execute()
diff --git a/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go b/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go
index 2d2258ae..804bcacd 100644
--- a/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go
+++ b/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go
@@ -64,7 +64,10 @@ func Test_Project_Infrastructure(t *testing.T) {
 			})
 		}()
 
-		err = deployer.CreateProjectResources(&project)
+		err = deployer.DeployProject(&deploy.CreateProjectInput{
+			Project:   project.Name,
+			AccountID: project.AccountID,
+		})
 		require.NoError(t, err)
 
 		// We should create the project StackSet
@@ -147,7 +150,10 @@ func Test_Project_Infrastructure(t *testing.T) {
 
 		require.True(t, len(roleStackOutput.Stacks) == 0, "Stack %s should not exist.", projectRoleStackName)
 
-		err = deployer.CreateProjectResources(&project)
+		err = deployer.DeployProject(&deploy.CreateProjectInput{
+			Project:   project.Name,
+			AccountID: project.AccountID,
+		})
 		require.NoError(t, err)
 
 		// Add an application only
diff --git a/internal/pkg/deploy/cloudformation/project_test.go b/internal/pkg/deploy/cloudformation/project_test.go
index 0925ffd3..a62e780d 100644
--- a/internal/pkg/deploy/cloudformation/project_test.go
+++ b/internal/pkg/deploy/cloudformation/project_test.go
@@ -9,6 +9,7 @@ import (
 	"testing"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 	"github.com/aws/amazon-ecs-cli-v2/templates"
 	"github.com/aws/aws-sdk-go/aws"
@@ -20,8 +21,8 @@ import (
 )
 
 func TestCreateProjectResources(t *testing.T) {
-	mockProject := archer.Project{
-		Name:      "testproject",
+	mockProject := &deploy.CreateProjectInput{
+		Project:   "testproject",
 		AccountID: "1234",
 	}
 	testCases := map[string]struct {
@@ -73,7 +74,7 @@ func TestCreateProjectResources(t *testing.T) {
 					require.Equal(t, "arn:aws:iam::1234:role/testproject-adminrole", *in.AdministrationRoleARN)
 					require.True(t, len(in.Tags) == 1, "There should be one tag for the project")
 					require.Equal(t, "ecs-project", *in.Tags[0].Key)
-					require.Equal(t, mockProject.Name, *in.Tags[0].Value)
+					require.Equal(t, mockProject.Project, *in.Tags[0].Value)
 
 					return nil, nil
 				}
@@ -88,7 +89,7 @@ func TestCreateProjectResources(t *testing.T) {
 				client: tc.mockCFClient(),
 				box:    templates.Box(),
 			}
-			got := cf.CreateProjectResources(&mockProject)
+			got := cf.DeployProject(mockProject)
 
 			if tc.want != nil {
 				require.EqualError(t, tc.want, got.Error())
@@ -549,14 +550,14 @@ func TestWaitForStackSetOperation(t *testing.T) {
 }
 
 func TestDeployProjectConfig_ErrWrapping(t *testing.T) {
-	mockProject := archer.Project{Name: "project", AccountID: "12345"}
+	mockProject := &deploy.CreateProjectInput{Project: "project", AccountID: "12345"}
 
 	testCases := map[string]struct {
 		cf   CloudFormation
 		want error
 	}{
 		"ErrCodeOperationIdAlreadyExistsException": {
-			want: &ErrStackSetOutOfDate{projectName: mockProject.Name},
+			want: &ErrStackSetOutOfDate{projectName: mockProject.Project},
 			cf: CloudFormation{
 				client: &mockCloudFormation{
 					t: t,
@@ -568,7 +569,7 @@ func TestDeployProjectConfig_ErrWrapping(t *testing.T) {
 			},
 		},
 		"ErrCodeOperationInProgressException": {
-			want: &ErrStackSetOutOfDate{projectName: mockProject.Name},
+			want: &ErrStackSetOutOfDate{projectName: mockProject.Project},
 			cf: CloudFormation{
 				client: &mockCloudFormation{
 					t: t,
@@ -580,7 +581,7 @@ func TestDeployProjectConfig_ErrWrapping(t *testing.T) {
 			},
 		},
 		"ErrCodeStaleRequestException": {
-			want: &ErrStackSetOutOfDate{projectName: mockProject.Name},
+			want: &ErrStackSetOutOfDate{projectName: mockProject.Project},
 			cf: CloudFormation{
 				client: &mockCloudFormation{
 					t: t,
@@ -596,7 +597,7 @@ func TestDeployProjectConfig_ErrWrapping(t *testing.T) {
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
 			mockProjectResources := stack.ProjectResourcesConfig{}
-			got := tc.cf.deployProjectConfig(stack.NewProjectStackConfig(&mockProject, boxWithProjectTemplate()), &mockProjectResources)
+			got := tc.cf.deployProjectConfig(stack.NewProjectStackConfig(mockProject, boxWithProjectTemplate()), &mockProjectResources)
 			require.NotNil(t, got)
 			require.True(t, errors.Is(tc.want, got), "Got %v but expected %v", got, tc.want)
 		})
diff --git a/internal/pkg/deploy/cloudformation/stack/project_test.go b/internal/pkg/deploy/cloudformation/stack/project_test.go
index ae45db04..d5e6881b 100644
--- a/internal/pkg/deploy/cloudformation/stack/project_test.go
+++ b/internal/pkg/deploy/cloudformation/stack/project_test.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/cloudformation"
 	"github.com/gobuffalo/packd"
@@ -36,7 +36,7 @@ func TestProjTemplate(t *testing.T) {
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			projStack := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, tc.box)
+			projStack := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, tc.box)
 			got, err := projStack.Template()
 
 			if tc.want != nil {
@@ -91,7 +91,7 @@ Outputs:
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			projStack := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, tc.box)
+			projStack := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, tc.box)
 
 			got, err := projStack.ResourceTemplate(tc.given)
 
@@ -106,7 +106,7 @@ Outputs:
 }
 
 func TestProjectParameters(t *testing.T) {
-	proj := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, emptyProjectBox())
+	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
 	expectedParams := []*cloudformation.Parameter{
 		{
 			ParameterKey:   aws.String(projectAdminRoleParamName),
@@ -121,24 +121,24 @@ func TestProjectParameters(t *testing.T) {
 }
 
 func TestProjectTags(t *testing.T) {
-	proj := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, emptyProjectBox())
+	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
 	expectedTags := []*cloudformation.Tag{
 		{
 			Key:   aws.String(projectTagKey),
-			Value: aws.String(proj.Project.Name),
+			Value: aws.String(proj.Project),
 		},
 	}
 	require.ElementsMatch(t, expectedTags, proj.Tags())
 }
 
 func TestProjectStackName(t *testing.T) {
-	proj := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, emptyProjectBox())
-	require.Equal(t, fmt.Sprintf("%s-infrastructure-roles", proj.Project.Name), proj.StackName())
+	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
+	require.Equal(t, fmt.Sprintf("%s-infrastructure-roles", proj.Project), proj.StackName())
 }
 
 func TestProjectStackSetName(t *testing.T) {
-	proj := NewProjectStackConfig(&archer.Project{Name: "testproject", AccountID: "1234"}, emptyProjectBox())
-	require.Equal(t, fmt.Sprintf("%s-infrastructure", proj.Project.Name), proj.StackSetName())
+	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
+	require.Equal(t, fmt.Sprintf("%s-infrastructure", proj.Project), proj.StackSetName())
 }
 
 func TestTemplateToProjectConfig(t *testing.T) {
diff --git a/internal/pkg/term/log/log_test.go b/internal/pkg/term/log/log_test.go
index f900e058..d480eecc 100644
--- a/internal/pkg/term/log/log_test.go
+++ b/internal/pkg/term/log/log_test.go
@@ -49,6 +49,28 @@ func TestSuccessf(t *testing.T) {
 	require.Contains(t, b.String(), "hello world\n")
 }
 
+func TestSsuccess(t *testing.T) {
+	s := Ssuccess("hello", " world")
+
+	require.Contains(t, s, "Success!")
+	require.Contains(t, s, "hello world")
+}
+
+func TestSsuccessln(t *testing.T) {
+	s := Ssuccessln("hello", " world")
+
+	// THEN
+	require.Contains(t, s, "Success!")
+	require.Contains(t, s, "hello world\n")
+}
+
+func TestSsuccessf(t *testing.T) {
+	s := Ssuccessf("%s %s\n", "hello", "world")
+
+	require.Contains(t, s, "Success!")
+	require.Contains(t, s, "hello world\n")
+}
+
 func TestError(t *testing.T) {
 	// GIVEN
 	b := &strings.Builder{}
@@ -88,6 +110,27 @@ func TestErrorf(t *testing.T) {
 	require.Contains(t, b.String(), "hello world\n")
 }
 
+func TestSerror(t *testing.T) {
+	s := Serror("hello", " world")
+
+	require.Contains(t, s, "Error!")
+	require.Contains(t, s, "hello world")
+}
+
+func TestSerrorln(t *testing.T) {
+	s := Serrorln("hello", " world")
+
+	require.Contains(t, s, "Error!")
+	require.Contains(t, s, "hello world\n")
+}
+
+func TestSerrorf(t *testing.T) {
+	s := Serrorf("%s %s\n", "hello", "world")
+
+	require.Contains(t, s, "Error!")
+	require.Contains(t, s, "hello world\n")
+}
+
 func TestWarning(t *testing.T) {
 	// GIVEN
 	b := &strings.Builder{}