Repository: aws/amazon-ecs-cli-v2
Pull request #394 (2019-11-17 04:18:15)

DNS delegation part 1
    Attach environment principal to DNS Delegation Policy

    This change makes it so we can do cross-account DNS delegation. It
    grants the environment permission to update the project's hosted zone.

    TODO: the project.yml template is not complete, it just adds a role.

    But this change adds the infrastructure to add an environment to a
    project's DNSDelegation role.


Patch:
diff --git a/internal/pkg/archer/project.go b/internal/pkg/archer/project.go
index b7b01dd5..f5515ad5 100644
--- a/internal/pkg/archer/project.go
+++ b/internal/pkg/archer/project.go
@@ -11,6 +11,11 @@ type Project struct {
 	Version   string `json:"version"` // The version of the project layout in the underlying datastore (e.g. SSM).
 }
 
+// RequiresDNSDelegation returns true if we have to set up DNS Delegation resources
+func (p *Project) RequiresDNSDelegation() bool {
+	return p.Domain != ""
+}
+
 // ProjectRegionalResources represent project resources that are regional.
 type ProjectRegionalResources struct {
 	Region         string            // The region these resources are in.
diff --git a/internal/pkg/cli/deploy.go b/internal/pkg/cli/deploy.go
index 8bc0087f..66be2b68 100644
--- a/internal/pkg/cli/deploy.go
+++ b/internal/pkg/cli/deploy.go
@@ -25,6 +25,7 @@ type projectDeployer interface {
 	DeployProject(in *deploy.CreateProjectInput) error
 	AddAppToProject(project *archer.Project, appName string) error
 	AddEnvToProject(project *archer.Project, env *archer.Environment) error
+	DelegateDNSPermissions(project *archer.Project, accountID string) error
 }
 
 type projectResourcesGetter interface {
diff --git a/internal/pkg/cli/env_init.go b/internal/pkg/cli/env_init.go
index d9181034..c0f0c4bd 100644
--- a/internal/pkg/cli/env_init.go
+++ b/internal/pkg/cli/env_init.go
@@ -23,6 +23,8 @@ import (
 const (
 	fmtDeployEnvStart          = "Proposing infrastructure changes for the %s environment."
 	fmtDeployEnvFailed         = "Failed to accept changes for the %s environment."
+	fmtDNSDelegationStart      = "Sharing DNS permissions for this project to account %s."
+	fmtDNSDelegationFailed     = "Failed to grant DNS permissions to account %s."
 	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."
@@ -44,8 +46,8 @@ type InitEnvOpts struct {
 	envDeployer   deployer
 	projDeployer  deployer
 	identity      identityService
-
-	prog progress
+	envIdentity   identityService
+	prog          progress
 
 	*GlobalOpts // Embed global options.
 }
@@ -99,7 +101,15 @@ func (opts *InitEnvOpts) Execute() error {
 		PublicLoadBalancer:       true, // TODO: configure this based on user input or application Type needs?
 		ToolsAccountPrincipalARN: caller.RootUserARN,
 	}
+
+	if project.RequiresDNSDelegation() {
+		if err := opts.delegateDNSFromProject(project); err != nil {
+			return fmt.Errorf("granting DNS permissions: %w", err)
+		}
+	}
+
 	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) {
@@ -143,6 +153,26 @@ func (opts *InitEnvOpts) Execute() error {
 	return nil
 }
 
+func (opts *InitEnvOpts) delegateDNSFromProject(project *archer.Project) error {
+	envAccount, err := opts.envIdentity.Get()
+	if err != nil {
+		return fmt.Errorf("getting environment account ID for DNS Delegation: %w", err)
+	}
+
+	// By default, our DNS Delegation permits same account delegation.
+	if envAccount.Account == project.AccountID {
+		return nil
+	}
+
+	opts.prog.Start(fmt.Sprintf(fmtDNSDelegationStart, color.HighlightUserInput(envAccount.Account)))
+
+	if err := opts.projDeployer.DelegateDNSPermissions(project, envAccount.Account); err != nil {
+		opts.prog.Stop(log.Serrorf(fmtDNSDelegationFailed, color.HighlightUserInput(envAccount.Account)))
+		return err
+	}
+	return nil
+}
+
 func (opts *InitEnvOpts) humanizeEnvironmentEvents(resourceEvents []deploy.ResourceEvent) []termprogress.TabRow {
 	matcher := map[termprogress.Text]termprogress.ResourceMatcher{
 		textVPC: func(event deploy.Resource) bool {
@@ -231,6 +261,7 @@ func BuildEnvInitCmd() *cobra.Command {
 			opts.envDeployer = cloudformation.New(profileSess)
 			opts.projDeployer = cloudformation.New(defaultSession)
 			opts.identity = identity.New(defaultSession)
+			opts.envIdentity = identity.New(profileSess)
 			return nil
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/internal/pkg/cli/mocks/mock_deploy.go b/internal/pkg/cli/mocks/mock_deploy.go
index 6e72ad5b..6ea5dd4e 100644
--- a/internal/pkg/cli/mocks/mock_deploy.go
+++ b/internal/pkg/cli/mocks/mock_deploy.go
@@ -5,10 +5,11 @@
 package mocks
 
 import (
+	reflect "reflect"
+
 	archer "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	deploy "github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
 	gomock "github.com/golang/mock/gomock"
-	reflect "reflect"
 )
 
 // MockenvironmentDeployer is a mock of environmentDeployer interface
@@ -36,6 +37,7 @@ func (m *MockenvironmentDeployer) EXPECT() *MockenvironmentDeployerMockRecorder
 
 // DeployEnvironment mocks base method
 func (m *MockenvironmentDeployer) DeployEnvironment(env *deploy.CreateEnvironmentInput) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DeployEnvironment", env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -43,11 +45,13 @@ func (m *MockenvironmentDeployer) DeployEnvironment(env *deploy.CreateEnvironmen
 
 // DeployEnvironment indicates an expected call of DeployEnvironment
 func (mr *MockenvironmentDeployerMockRecorder) DeployEnvironment(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployEnvironment", reflect.TypeOf((*MockenvironmentDeployer)(nil).DeployEnvironment), env)
 }
 
 // StreamEnvironmentCreation mocks base method
 func (m *MockenvironmentDeployer) StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse) {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "StreamEnvironmentCreation", env)
 	ret0, _ := ret[0].(<-chan []deploy.ResourceEvent)
 	ret1, _ := ret[1].(<-chan deploy.CreateEnvironmentResponse)
@@ -56,6 +60,7 @@ func (m *MockenvironmentDeployer) StreamEnvironmentCreation(env *deploy.CreateEn
 
 // StreamEnvironmentCreation indicates an expected call of StreamEnvironmentCreation
 func (mr *MockenvironmentDeployerMockRecorder) StreamEnvironmentCreation(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamEnvironmentCreation", reflect.TypeOf((*MockenvironmentDeployer)(nil).StreamEnvironmentCreation), env)
 }
 
@@ -84,6 +89,7 @@ func (m *MockpipelineDeployer) EXPECT() *MockpipelineDeployerMockRecorder {
 
 // DeployPipeline mocks base method
 func (m *MockpipelineDeployer) DeployPipeline(env *deploy.CreatePipelineInput) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DeployPipeline", env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -91,11 +97,13 @@ func (m *MockpipelineDeployer) DeployPipeline(env *deploy.CreatePipelineInput) e
 
 // DeployPipeline indicates an expected call of DeployPipeline
 func (mr *MockpipelineDeployerMockRecorder) DeployPipeline(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployPipeline", reflect.TypeOf((*MockpipelineDeployer)(nil).DeployPipeline), env)
 }
 
 // AddPipelineResourcesToProject mocks base method
 func (m *MockpipelineDeployer) AddPipelineResourcesToProject(project *archer.Project, region string) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddPipelineResourcesToProject", project, region)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -103,11 +111,13 @@ func (m *MockpipelineDeployer) AddPipelineResourcesToProject(project *archer.Pro
 
 // AddPipelineResourcesToProject indicates an expected call of AddPipelineResourcesToProject
 func (mr *MockpipelineDeployerMockRecorder) AddPipelineResourcesToProject(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPipelineResourcesToProject", reflect.TypeOf((*MockpipelineDeployer)(nil).AddPipelineResourcesToProject), project, region)
 }
 
 // GetProjectResourcesByRegion mocks base method
 func (m *MockpipelineDeployer) 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)
@@ -116,11 +126,13 @@ func (m *MockpipelineDeployer) GetProjectResourcesByRegion(project *archer.Proje
 
 // GetProjectResourcesByRegion indicates an expected call of GetProjectResourcesByRegion
 func (mr *MockpipelineDeployerMockRecorder) GetProjectResourcesByRegion(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectResourcesByRegion", reflect.TypeOf((*MockpipelineDeployer)(nil).GetProjectResourcesByRegion), project, region)
 }
 
 // GetRegionalProjectResources mocks base method
 func (m *MockpipelineDeployer) 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)
@@ -129,6 +141,7 @@ func (m *MockpipelineDeployer) GetRegionalProjectResources(project *archer.Proje
 
 // GetRegionalProjectResources indicates an expected call of GetRegionalProjectResources
 func (mr *MockpipelineDeployerMockRecorder) GetRegionalProjectResources(project interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalProjectResources", reflect.TypeOf((*MockpipelineDeployer)(nil).GetRegionalProjectResources), project)
 }
 
@@ -157,6 +170,7 @@ func (m *MockprojectDeployer) EXPECT() *MockprojectDeployerMockRecorder {
 
 // 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
@@ -164,11 +178,13 @@ func (m *MockprojectDeployer) DeployProject(in *deploy.CreateProjectInput) error
 
 // 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)
 }
 
 // AddAppToProject mocks base method
 func (m *MockprojectDeployer) AddAppToProject(project *archer.Project, appName string) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddAppToProject", project, appName)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -176,11 +192,13 @@ func (m *MockprojectDeployer) AddAppToProject(project *archer.Project, appName s
 
 // AddAppToProject indicates an expected call of AddAppToProject
 func (mr *MockprojectDeployerMockRecorder) AddAppToProject(project, appName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAppToProject", reflect.TypeOf((*MockprojectDeployer)(nil).AddAppToProject), project, appName)
 }
 
 // AddEnvToProject mocks base method
 func (m *MockprojectDeployer) AddEnvToProject(project *archer.Project, env *archer.Environment) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddEnvToProject", project, env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -188,9 +206,24 @@ func (m *MockprojectDeployer) AddEnvToProject(project *archer.Project, env *arch
 
 // AddEnvToProject indicates an expected call of AddEnvToProject
 func (mr *MockprojectDeployerMockRecorder) AddEnvToProject(project, env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEnvToProject", reflect.TypeOf((*MockprojectDeployer)(nil).AddEnvToProject), project, env)
 }
 
+// DelegateDNSPermissions mocks base method
+func (m *MockprojectDeployer) DelegateDNSPermissions(project *archer.Project, accountID string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DelegateDNSPermissions", project, accountID)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DelegateDNSPermissions indicates an expected call of DelegateDNSPermissions
+func (mr *MockprojectDeployerMockRecorder) DelegateDNSPermissions(project, accountID interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelegateDNSPermissions", reflect.TypeOf((*MockprojectDeployer)(nil).DelegateDNSPermissions), project, accountID)
+}
+
 // MockprojectResourcesGetter is a mock of projectResourcesGetter interface
 type MockprojectResourcesGetter struct {
 	ctrl     *gomock.Controller
@@ -216,6 +249,7 @@ func (m *MockprojectResourcesGetter) EXPECT() *MockprojectResourcesGetterMockRec
 
 // GetProjectResourcesByRegion mocks base method
 func (m *MockprojectResourcesGetter) 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)
@@ -224,11 +258,13 @@ func (m *MockprojectResourcesGetter) GetProjectResourcesByRegion(project *archer
 
 // GetProjectResourcesByRegion indicates an expected call of GetProjectResourcesByRegion
 func (mr *MockprojectResourcesGetterMockRecorder) GetProjectResourcesByRegion(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectResourcesByRegion", reflect.TypeOf((*MockprojectResourcesGetter)(nil).GetProjectResourcesByRegion), project, region)
 }
 
 // GetRegionalProjectResources mocks base method
 func (m *MockprojectResourcesGetter) 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)
@@ -237,6 +273,7 @@ func (m *MockprojectResourcesGetter) GetRegionalProjectResources(project *archer
 
 // GetRegionalProjectResources indicates an expected call of GetRegionalProjectResources
 func (mr *MockprojectResourcesGetterMockRecorder) GetRegionalProjectResources(project interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalProjectResources", reflect.TypeOf((*MockprojectResourcesGetter)(nil).GetRegionalProjectResources), project)
 }
 
@@ -265,6 +302,7 @@ func (m *Mockdeployer) EXPECT() *MockdeployerMockRecorder {
 
 // DeployEnvironment mocks base method
 func (m *Mockdeployer) DeployEnvironment(env *deploy.CreateEnvironmentInput) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DeployEnvironment", env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -272,11 +310,13 @@ func (m *Mockdeployer) DeployEnvironment(env *deploy.CreateEnvironmentInput) err
 
 // DeployEnvironment indicates an expected call of DeployEnvironment
 func (mr *MockdeployerMockRecorder) DeployEnvironment(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployEnvironment", reflect.TypeOf((*Mockdeployer)(nil).DeployEnvironment), env)
 }
 
 // StreamEnvironmentCreation mocks base method
 func (m *Mockdeployer) StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse) {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "StreamEnvironmentCreation", env)
 	ret0, _ := ret[0].(<-chan []deploy.ResourceEvent)
 	ret1, _ := ret[1].(<-chan deploy.CreateEnvironmentResponse)
@@ -285,11 +325,13 @@ func (m *Mockdeployer) StreamEnvironmentCreation(env *deploy.CreateEnvironmentIn
 
 // StreamEnvironmentCreation indicates an expected call of StreamEnvironmentCreation
 func (mr *MockdeployerMockRecorder) StreamEnvironmentCreation(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamEnvironmentCreation", reflect.TypeOf((*Mockdeployer)(nil).StreamEnvironmentCreation), env)
 }
 
 // DeployProject mocks base method
 func (m *Mockdeployer) DeployProject(in *deploy.CreateProjectInput) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DeployProject", in)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -297,11 +339,13 @@ func (m *Mockdeployer) DeployProject(in *deploy.CreateProjectInput) error {
 
 // DeployProject indicates an expected call of DeployProject
 func (mr *MockdeployerMockRecorder) DeployProject(in interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployProject", reflect.TypeOf((*Mockdeployer)(nil).DeployProject), in)
 }
 
 // AddAppToProject mocks base method
 func (m *Mockdeployer) AddAppToProject(project *archer.Project, appName string) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddAppToProject", project, appName)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -309,11 +353,27 @@ func (m *Mockdeployer) AddAppToProject(project *archer.Project, appName string)
 
 // AddAppToProject indicates an expected call of AddAppToProject
 func (mr *MockdeployerMockRecorder) AddAppToProject(project, appName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAppToProject", reflect.TypeOf((*Mockdeployer)(nil).AddAppToProject), project, appName)
 }
 
+// DelegateDNSPermissions mocks base method
+func (m *Mockdeployer) DelegateDNSPermissions(project *archer.Project, accountID string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DelegateDNSPermissions", project, accountID)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DelegateDNSPermissions indicates an expected call of DelegateDNSPermissions
+func (mr *MockdeployerMockRecorder) DelegateDNSPermissions(project, accountID interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelegateDNSPermissions", reflect.TypeOf((*Mockdeployer)(nil).DelegateDNSPermissions), project, accountID)
+}
+
 // AddEnvToProject mocks base method
 func (m *Mockdeployer) AddEnvToProject(project *archer.Project, env *archer.Environment) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddEnvToProject", project, env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -321,11 +381,13 @@ func (m *Mockdeployer) AddEnvToProject(project *archer.Project, env *archer.Envi
 
 // AddEnvToProject indicates an expected call of AddEnvToProject
 func (mr *MockdeployerMockRecorder) AddEnvToProject(project, env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEnvToProject", reflect.TypeOf((*Mockdeployer)(nil).AddEnvToProject), project, env)
 }
 
 // DeployPipeline mocks base method
 func (m *Mockdeployer) DeployPipeline(env *deploy.CreatePipelineInput) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DeployPipeline", env)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -333,11 +395,13 @@ func (m *Mockdeployer) DeployPipeline(env *deploy.CreatePipelineInput) error {
 
 // DeployPipeline indicates an expected call of DeployPipeline
 func (mr *MockdeployerMockRecorder) DeployPipeline(env interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployPipeline", reflect.TypeOf((*Mockdeployer)(nil).DeployPipeline), env)
 }
 
 // AddPipelineResourcesToProject mocks base method
 func (m *Mockdeployer) AddPipelineResourcesToProject(project *archer.Project, region string) error {
+	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AddPipelineResourcesToProject", project, region)
 	ret0, _ := ret[0].(error)
 	return ret0
@@ -345,11 +409,13 @@ func (m *Mockdeployer) AddPipelineResourcesToProject(project *archer.Project, re
 
 // AddPipelineResourcesToProject indicates an expected call of AddPipelineResourcesToProject
 func (mr *MockdeployerMockRecorder) AddPipelineResourcesToProject(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPipelineResourcesToProject", reflect.TypeOf((*Mockdeployer)(nil).AddPipelineResourcesToProject), project, region)
 }
 
 // GetProjectResourcesByRegion mocks base method
 func (m *Mockdeployer) 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)
@@ -358,11 +424,13 @@ func (m *Mockdeployer) GetProjectResourcesByRegion(project *archer.Project, regi
 
 // GetProjectResourcesByRegion indicates an expected call of GetProjectResourcesByRegion
 func (mr *MockdeployerMockRecorder) GetProjectResourcesByRegion(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectResourcesByRegion", reflect.TypeOf((*Mockdeployer)(nil).GetProjectResourcesByRegion), project, region)
 }
 
 // GetRegionalProjectResources mocks base method
 func (m *Mockdeployer) 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)
@@ -371,5 +439,6 @@ func (m *Mockdeployer) GetRegionalProjectResources(project *archer.Project) ([]*
 
 // GetRegionalProjectResources indicates an expected call of GetRegionalProjectResources
 func (mr *MockdeployerMockRecorder) GetRegionalProjectResources(project interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalProjectResources", reflect.TypeOf((*Mockdeployer)(nil).GetRegionalProjectResources), project)
 }
diff --git a/internal/pkg/cli/mocks/mock_projectservice.go b/internal/pkg/cli/mocks/mock_projectservice.go
index dca7c941..ca305723 100644
--- a/internal/pkg/cli/mocks/mock_projectservice.go
+++ b/internal/pkg/cli/mocks/mock_projectservice.go
@@ -6,6 +6,7 @@ package mocks
 
 import (
 	archer "github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	ecr "github.com/aws/amazon-ecs-cli-v2/internal/pkg/build/ecr"
 	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
 )
@@ -164,3 +165,121 @@ func (mr *MockprojectServiceMockRecorder) CreateApplication(app interface{}) *go
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApplication", reflect.TypeOf((*MockprojectService)(nil).CreateApplication), app)
 }
+
+// MockecrService is a mock of ecrService interface
+type MockecrService struct {
+	ctrl     *gomock.Controller
+	recorder *MockecrServiceMockRecorder
+}
+
+// MockecrServiceMockRecorder is the mock recorder for MockecrService
+type MockecrServiceMockRecorder struct {
+	mock *MockecrService
+}
+
+// NewMockecrService creates a new mock instance
+func NewMockecrService(ctrl *gomock.Controller) *MockecrService {
+	mock := &MockecrService{ctrl: ctrl}
+	mock.recorder = &MockecrServiceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockecrService) EXPECT() *MockecrServiceMockRecorder {
+	return m.recorder
+}
+
+// GetRepository mocks base method
+func (m *MockecrService) GetRepository(name string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRepository", name)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRepository indicates an expected call of GetRepository
+func (mr *MockecrServiceMockRecorder) GetRepository(name interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepository", reflect.TypeOf((*MockecrService)(nil).GetRepository), name)
+}
+
+// GetECRAuth mocks base method
+func (m *MockecrService) GetECRAuth() (ecr.Auth, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetECRAuth")
+	ret0, _ := ret[0].(ecr.Auth)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetECRAuth indicates an expected call of GetECRAuth
+func (mr *MockecrServiceMockRecorder) GetECRAuth() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetECRAuth", reflect.TypeOf((*MockecrService)(nil).GetECRAuth))
+}
+
+// MockdockerService is a mock of dockerService interface
+type MockdockerService struct {
+	ctrl     *gomock.Controller
+	recorder *MockdockerServiceMockRecorder
+}
+
+// MockdockerServiceMockRecorder is the mock recorder for MockdockerService
+type MockdockerServiceMockRecorder struct {
+	mock *MockdockerService
+}
+
+// NewMockdockerService creates a new mock instance
+func NewMockdockerService(ctrl *gomock.Controller) *MockdockerService {
+	mock := &MockdockerService{ctrl: ctrl}
+	mock.recorder = &MockdockerServiceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockdockerService) EXPECT() *MockdockerServiceMockRecorder {
+	return m.recorder
+}
+
+// Build mocks base method
+func (m *MockdockerService) Build(uri, tag, path string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Build", uri, tag, path)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Build indicates an expected call of Build
+func (mr *MockdockerServiceMockRecorder) Build(uri, tag, path interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockdockerService)(nil).Build), uri, tag, path)
+}
+
+// Login mocks base method
+func (m *MockdockerService) Login(uri string, auth ecr.Auth) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Login", uri, auth)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Login indicates an expected call of Login
+func (mr *MockdockerServiceMockRecorder) Login(uri, auth interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockdockerService)(nil).Login), uri, auth)
+}
+
+// Push mocks base method
+func (m *MockdockerService) Push(uri, tag string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Push", uri, tag)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Push indicates an expected call of Push
+func (mr *MockdockerServiceMockRecorder) Push(uri, tag interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockdockerService)(nil).Push), uri, tag)
+}
diff --git a/internal/pkg/cli/project_init.go b/internal/pkg/cli/project_init.go
index 6b1de077..26c9c396 100644
--- a/internal/pkg/cli/project_init.go
+++ b/internal/pkg/cli/project_init.go
@@ -139,8 +139,9 @@ func (opts *InitProjectOpts) Execute() error {
 	}
 	opts.prog.Start(fmt.Sprintf(fmtDeployProjectStart, color.HighlightUserInput(opts.ProjectName)))
 	err = opts.deployer.DeployProject(&deploy.CreateProjectInput{
-		Project:   opts.ProjectName,
-		AccountID: caller.Account,
+		Project:    opts.ProjectName,
+		AccountID:  caller.Account,
+		DomainName: opts.DomainName,
 	})
 	if err != nil {
 		opts.prog.Stop(log.Serrorf(fmtDeployProjectFailed, color.HighlightUserInput(opts.ProjectName)))
diff --git a/internal/pkg/deploy/cloudformation/changeset.go b/internal/pkg/deploy/cloudformation/changeset.go
index b0537ced..2a488811 100644
--- a/internal/pkg/deploy/cloudformation/changeset.go
+++ b/internal/pkg/deploy/cloudformation/changeset.go
@@ -132,9 +132,9 @@ func withParameters(params []*cloudformation.Parameter) createChangeSetOpt {
 	}
 }
 
-func withCreateChangeSetType() createChangeSetOpt {
+func withChangeSetType(csType string) createChangeSetOpt {
 	return func(in *cloudformation.CreateChangeSetInput) {
-		in.ChangeSetType = aws.String(cloudformation.ChangeSetTypeCreate)
+		in.ChangeSetType = aws.String(csType)
 	}
 }
 
diff --git a/internal/pkg/deploy/cloudformation/cloudformation.go b/internal/pkg/deploy/cloudformation/cloudformation.go
index e1158494..66aa48ac 100644
--- a/internal/pkg/deploy/cloudformation/cloudformation.go
+++ b/internal/pkg/deploy/cloudformation/cloudformation.go
@@ -165,14 +165,21 @@ func (cf CloudFormation) describeStackWithClient(describeStackInput *cloudformat
 
 	return describeStackOutput.Stacks[0], nil
 }
+func (cf CloudFormation) create(stackConfig stackConfiguration) error {
+	return cf.deploy(stackConfig, cloudformation.ChangeSetTypeCreate)
+}
+
+func (cf CloudFormation) update(stackConfig stackConfiguration) error {
+	return cf.deploy(stackConfig, cloudformation.ChangeSetTypeUpdate)
+}
 
-func (cf CloudFormation) deploy(stackConfig stackConfiguration) error {
+func (cf CloudFormation) deploy(stackConfig stackConfiguration, createOrUpdate string) error {
 	template, err := stackConfig.Template()
 	if err != nil {
 		return fmt.Errorf("template creation: %w", err)
 	}
 
-	in, err := createChangeSetInput(stackConfig.StackName(), template, withCreateChangeSetType(), withTags(stackConfig.Tags()), withParameters(stackConfig.Parameters()))
+	in, err := createChangeSetInput(stackConfig.StackName(), template, withChangeSetType(createOrUpdate), withTags(stackConfig.Tags()), withParameters(stackConfig.Parameters()))
 	if err != nil {
 		return err
 	}
diff --git a/internal/pkg/deploy/cloudformation/env.go b/internal/pkg/deploy/cloudformation/env.go
index 077d961d..9a847dcc 100644
--- a/internal/pkg/deploy/cloudformation/env.go
+++ b/internal/pkg/deploy/cloudformation/env.go
@@ -16,7 +16,7 @@ import (
 // If the change set to create the stack cannot be executed, returns a ErrNotExecutableChangeSet.
 // Otherwise, returns a wrapped error.
 func (cf CloudFormation) DeployEnvironment(env *deploy.CreateEnvironmentInput) error {
-	return cf.deploy(stack.NewEnvStackConfig(env, cf.box))
+	return cf.create(stack.NewEnvStackConfig(env, cf.box))
 }
 
 // StreamEnvironmentCreation streams resource update events while a deployment is taking place.
diff --git a/internal/pkg/deploy/cloudformation/pipeline.go b/internal/pkg/deploy/cloudformation/pipeline.go
index 6adb4303..264a741f 100644
--- a/internal/pkg/deploy/cloudformation/pipeline.go
+++ b/internal/pkg/deploy/cloudformation/pipeline.go
@@ -17,5 +17,5 @@ import (
 func (cf CloudFormation) DeployPipeline(in *deploy.CreatePipelineInput) error {
 	pipelineConfig := stack.NewPipelineStackConfig(in)
 
-	return cf.deploy(pipelineConfig)
+	return cf.create(pipelineConfig)
 }
diff --git a/internal/pkg/deploy/cloudformation/project.go b/internal/pkg/deploy/cloudformation/project.go
index 19bda160..57a64dc5 100644
--- a/internal/pkg/deploy/cloudformation/project.go
+++ b/internal/pkg/deploy/cloudformation/project.go
@@ -27,7 +27,7 @@ func (cf CloudFormation) DeployProject(in *deploy.CreateProjectInput) error {
 
 	// First deploy the project roles needed by StackSets. These roles
 	// allow the stack set to set up our regional stacks.
-	if err := cf.deploy(projectConfig); err == nil {
+	if err := cf.create(projectConfig); err == nil {
 		_, err := cf.waitForStackCreation(projectConfig)
 		if err != nil {
 			return err
@@ -65,6 +65,37 @@ func (cf CloudFormation) DeployProject(in *deploy.CreateProjectInput) error {
 	return nil
 }
 
+// DelegateDNSPermissions grants the provided account ID the ability to write to this project's
+// DNS HostedZone. This allows us to perform cross account DNS delegation.
+func (cf CloudFormation) DelegateDNSPermissions(project *archer.Project, accountID string) error {
+	deployProject := deploy.CreateProjectInput{
+		Project:    project.Name,
+		AccountID:  project.AccountID,
+		DomainName: project.Domain,
+	}
+
+	projectConfig := stack.NewProjectStackConfig(&deployProject, cf.box)
+
+	describeStack := cloudformation.DescribeStacksInput{
+		StackName: aws.String(projectConfig.StackName()),
+	}
+	projectStack, err := cf.describeStack(&describeStack)
+
+	if err != nil {
+		return fmt.Errorf("getting existing project infrastructure stack: %w", err)
+	}
+
+	dnsDelegatedAccounts := stack.DNSDelegatedAccountsForStack(projectStack)
+	deployProject.DNSDelegationAccounts = append(dnsDelegatedAccounts, accountID)
+	updatedProjectConfig := stack.NewProjectStackConfig(&deployProject, cf.box)
+
+	if err := cf.update(updatedProjectConfig); err != nil {
+		return fmt.Errorf("updating project to allow DNS delegation: %w", err)
+	}
+
+	return cf.client.WaitUntilStackUpdateComplete(&describeStack)
+}
+
 // GetProjectResourcesByRegion fetches all the regional resources for a particular region.
 func (cf CloudFormation) GetProjectResourcesByRegion(project *archer.Project, region string) (*archer.ProjectRegionalResources, error) {
 	resources, err := cf.getResourcesForStackInstances(project, &region)
diff --git a/internal/pkg/deploy/cloudformation/stack/project.go b/internal/pkg/deploy/cloudformation/stack/project.go
index 1279527b..93c8377e 100644
--- a/internal/pkg/deploy/cloudformation/stack/project.go
+++ b/internal/pkg/deploy/cloudformation/stack/project.go
@@ -43,13 +43,15 @@ type ProjectStackConfig struct {
 }
 
 const (
-	projectTemplatePath           = "project/project.yml"
-	projectResourcesTemplatePath  = "project/cf.yml"
-	projectAdminRoleParamName     = "AdminRoleName"
-	projectExecutionRoleParamName = "ExecutionRoleName"
-	projectOutputKMSKey           = "KMSKeyARN"
-	projectOutputS3Bucket         = "PipelineBucket"
-	projectOutputECRRepoPrefix    = "ECRRepo"
+	projectTemplatePath            = "project/project.yml"
+	projectResourcesTemplatePath   = "project/cf.yml"
+	projectAdminRoleParamName      = "AdminRoleName"
+	projectExecutionRoleParamName  = "ExecutionRoleName"
+	projectOutputKMSKey            = "KMSKeyARN"
+	projectOutputS3Bucket          = "PipelineBucket"
+	projectOutputECRRepoPrefix     = "ECRRepo"
+	projectDNSDelegatedAccountsKey = "ProjectDNSDelegatedAccounts"
+	projectDomainNameKey           = "ProjectDomainName"
 )
 
 // ProjectConfigFrom takes a template file and extracts the metadata block,
@@ -114,6 +116,14 @@ func (c *ProjectStackConfig) Parameters() []*cloudformation.Parameter {
 			ParameterKey:   aws.String(projectExecutionRoleParamName),
 			ParameterValue: aws.String(c.StackSetExecutionRoleName()),
 		},
+		{
+			ParameterKey:   aws.String(projectDNSDelegatedAccountsKey),
+			ParameterValue: aws.String(strings.Join(c.dnsDelegationAccounts(), ",")),
+		},
+		{
+			ParameterKey:   aws.String(projectDomainNameKey),
+			ParameterValue: aws.String(c.DomainName),
+		},
 	}
 }
 
@@ -159,6 +169,19 @@ func (c *ProjectStackConfig) StackSetExecutionRoleName() string {
 	return fmt.Sprintf("%s-executionrole", c.Project)
 }
 
+func (c *ProjectStackConfig) dnsDelegationAccounts() []string {
+	accounts := append(c.CreateProjectInput.DNSDelegationAccounts, c.CreateProjectInput.AccountID)
+	accountIDs := make(map[string]bool)
+	var uniqueAccountIDs []string
+	for _, entry := range accounts {
+		if _, value := accountIDs[entry]; !value {
+			accountIDs[entry] = true
+			uniqueAccountIDs = append(uniqueAccountIDs, entry)
+		}
+	}
+	return uniqueAccountIDs
+}
+
 // ToProjectRegionalResources takes a Project Resource Stack Instance stack, reads the output resources
 // and returns a modeled  ProjectRegionalResources.
 func ToProjectRegionalResources(stack *cloudformation.Stack) (*archer.ProjectRegionalResources, error) {
@@ -202,3 +225,16 @@ func ToProjectRegionalResources(stack *cloudformation.Stack) (*archer.ProjectReg
 
 	return &regionalResources, nil
 }
+
+// DNSDelegatedAccountsForStack looks through a stack's parameters for
+// the parameter which stores the comma seperated list of account IDs
+// which are permitted for DNS delegation.
+func DNSDelegatedAccountsForStack(stack *cloudformation.Stack) []string {
+	for _, parameter := range stack.Parameters {
+		if *parameter.ParameterKey == projectDNSDelegatedAccountsKey {
+			return strings.Split(*parameter.ParameterValue, ",")
+		}
+	}
+
+	return []string{}
+}
diff --git a/internal/pkg/deploy/project.go b/internal/pkg/deploy/project.go
index aac55bb8..e59a8c22 100644
--- a/internal/pkg/deploy/project.go
+++ b/internal/pkg/deploy/project.go
@@ -5,8 +5,12 @@
 // This file defines project deployment resources.
 package deploy
 
+//TODO rename this DeployProjectInput
+
 // 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.
+	Project               string   // Name of the project that needs to be created.
+	AccountID             string   // AWS account ID to administrate the project.
+	DNSDelegationAccounts []string // Accounts to grant DNS access to for this project
+	DomainName            string   // DNS Name used for this project
 }
diff --git a/templates/project/project.yml b/templates/project/project.yml
index 3cfeed35..4924afa8 100644
--- a/templates/project/project.yml
+++ b/templates/project/project.yml
@@ -7,6 +7,16 @@ Parameters:
     Type: String
   ExecutionRoleName:
     Type: String
+  ProjectDNSDelegatedAccounts:
+    Type: CommaDelimitedList
+    Default: ""
+  ProjectDomainName:
+    Type: String
+    Default: ""
+Conditions:
+  DelegateDNS:
+    !Not [!Equals [ !Ref ProjectDomainName, "" ]]
+
 Resources:
   AdministrationRole:
     Type: AWS::IAM::Role
@@ -81,6 +91,41 @@ Resources:
                   - ecr:GetRepositoryPolicy
                   - ecr:GetLifecyclePolicy
                 Resource: "*"
+  DNSDelegationRole:
+    Type: AWS::IAM::Role
+    Condition: DelegateDNS
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              AWS:  !Sub arn:aws:iam::${AWS::AccountId}:root
+            Action:
+              - sts:AssumeRole
+          - Effect: Allow
+            Principal:
+              AWS: !Split
+                - ','
+                - !Sub
+                    - 'arn:aws:iam::${inner}:root'
+                    - inner: !Join
+                      - ':root,arn:aws:iam::'
+                      - Ref: "ProjectDNSDelegatedAccounts"
+            Action:
+              - sts:AssumeRole
+      Path: /
+      Policies:
+      - PolicyName: DNSDelegationPolicy
+        PolicyDocument:
+          Version: "2012-10-17"
+          Statement:
+              - Sid: HostedZoneChangeRecord
+                Effect: Allow
+                Action:
+                  - route53:*
+                Resource: "*"
+
 Outputs:
   ExecutionRoleARN:
     Description: ExecutionRole used by this project to set up ECR Repos, KMS Keys and S3 buckets


Test patch:
diff --git a/internal/pkg/cli/env_init_test.go b/internal/pkg/cli/env_init_test.go
index 392791a0..ec8dd245 100644
--- a/internal/pkg/cli/env_init_test.go
+++ b/internal/pkg/cli/env_init_test.go
@@ -373,6 +373,52 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				}).Return(nil)
 			},
 		},
+		"success with DNS Delegation (project has Domain and env and project are different)": {
+			inProjectName: "phonetool",
+			inEnvName:     "test",
+
+			expectProjectGetter: func(m *mocks.MockProjectGetter) {
+				m.EXPECT().GetProject("phonetool").Return(&archer.Project{Name: "phonetool", AccountID: "1234", Domain: "amazon.com"}, nil)
+			},
+			expectIdentity: func(m *climocks.MockidentityService) {
+				m.EXPECT().Get().Return(identity.Caller{RootUserARN: "some arn", Account: "4567"}, nil).Times(2)
+			},
+			expectProgress: func(m *climocks.Mockprogress) {
+				m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
+				m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
+				m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
+				m.EXPECT().Stop(log.Ssuccessf(fmtStreamEnvComplete, "test"))
+				m.EXPECT().Start(fmt.Sprintf(fmtAddEnvToProjectStart, "1234", "mars-1", "phonetool"))
+				m.EXPECT().Stop(log.Ssuccessf(fmtAddEnvToProjectComplete, "1234", "mars-1", "phonetool"))
+			},
+			expectDeployer: func(m *climocks.Mockdeployer) {
+				m.EXPECT().DelegateDNSPermissions(gomock.Any(), "4567").Return(nil)
+				m.EXPECT().DeployEnvironment(gomock.Any()).Return(nil)
+				events := make(chan []deploy.ResourceEvent, 1)
+				responses := make(chan deploy.CreateEnvironmentResponse, 1)
+				m.EXPECT().StreamEnvironmentCreation(gomock.Any()).Return(events, responses)
+				responses <- deploy.CreateEnvironmentResponse{
+					Env: &archer.Environment{
+						Project:   "phonetool",
+						Name:      "test",
+						AccountID: "1234",
+						Region:    "mars-1",
+					},
+					Err: nil,
+				}
+				close(events)
+				close(responses)
+				m.EXPECT().AddEnvToProject(gomock.Any(), gomock.Any()).Return(nil)
+			},
+			expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
+				m.EXPECT().CreateEnvironment(&archer.Environment{
+					Project:   "phonetool",
+					Name:      "test",
+					AccountID: "1234",
+					Region:    "mars-1",
+				}).Return(nil)
+			},
+		},
 	}
 
 	for name, tc := range testCases {
@@ -409,6 +455,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 				envDeployer:   mockDeployer,
 				projDeployer:  mockDeployer,
 				identity:      mockIdentity,
+				envIdentity:   mockIdentity,
 				prog:          mockProgress,
 				GlobalOpts:    &GlobalOpts{projectName: tc.inProjectName},
 			}
@@ -425,3 +472,119 @@ func TestInitEnvOpts_Execute(t *testing.T) {
 		})
 	}
 }
+
+func TestInitEnvOpts_delegateDNSFromProject(t *testing.T) {
+	testCases := map[string]struct {
+		project        *archer.Project
+		expectDeployer func(m *climocks.Mockdeployer)
+		expectIdentity func(m *climocks.MockidentityService)
+		expectProgress func(m *climocks.Mockprogress)
+		wantedErr      string
+	}{
+		"should call DelegateDNSPermissions when project and env are in different accounts": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "crossaccountproject",
+				Domain:    "amazon.com",
+			},
+			expectIdentity: func(m *climocks.MockidentityService) {
+				m.EXPECT().Get().Return(identity.Caller{Account: "4567"}, nil)
+			},
+			expectProgress: func(m *climocks.Mockprogress) {
+				m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
+			},
+			expectDeployer: func(m *climocks.Mockdeployer) {
+				m.EXPECT().DelegateDNSPermissions(gomock.Any(), "4567").Return(nil)
+			},
+		},
+		"should skip updating when project and env are in same account": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "crossaccountproject",
+				Domain:    "amazon.com",
+			},
+			expectIdentity: func(m *climocks.MockidentityService) {
+				m.EXPECT().Get().Return(identity.Caller{Account: "1234"}, nil)
+			},
+			expectProgress: func(m *climocks.Mockprogress) {
+				m.EXPECT().Start(gomock.Any()).Times(0)
+			},
+			expectDeployer: func(m *climocks.Mockdeployer) {
+				m.EXPECT().DelegateDNSPermissions(gomock.Any(), gomock.Any()).Times(0)
+			},
+		},
+		"should return errors from identity": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "crossaccountproject",
+				Domain:    "amazon.com",
+			},
+			expectIdentity: func(m *climocks.MockidentityService) {
+				m.EXPECT().Get().Return(identity.Caller{}, fmt.Errorf("error"))
+			},
+			expectProgress: func(m *climocks.Mockprogress) {
+				m.EXPECT().Start(gomock.Any()).Times(0)
+			},
+			expectDeployer: func(m *climocks.Mockdeployer) {
+				m.EXPECT().DelegateDNSPermissions(gomock.Any(), gomock.Any()).Times(0)
+			},
+			wantedErr: "getting environment account ID for DNS Delegation: error",
+		},
+		"should return errors from DelegateDNSPermissions": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "crossaccountproject",
+				Domain:    "amazon.com",
+			},
+			expectIdentity: func(m *climocks.MockidentityService) {
+				m.EXPECT().Get().Return(identity.Caller{Account: "4567"}, nil)
+			},
+			expectProgress: func(m *climocks.Mockprogress) {
+				m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
+				m.EXPECT().Stop(log.Serrorf(fmtDNSDelegationFailed, "4567"))
+			},
+			expectDeployer: func(m *climocks.Mockdeployer) {
+				m.EXPECT().DelegateDNSPermissions(gomock.Any(), gomock.Any()).Return(fmt.Errorf("error"))
+			},
+			wantedErr: "error",
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+
+			// GIVEN
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			mockDeployer := climocks.NewMockdeployer(ctrl)
+			mockIdentity := climocks.NewMockidentityService(ctrl)
+			mockProgress := climocks.NewMockprogress(ctrl)
+			if tc.expectDeployer != nil {
+				tc.expectDeployer(mockDeployer)
+			}
+			if tc.expectIdentity != nil {
+				tc.expectIdentity(mockIdentity)
+			}
+			if tc.expectProgress != nil {
+				tc.expectProgress(mockProgress)
+			}
+			opts := &InitEnvOpts{
+				envIdentity:  mockIdentity,
+				projDeployer: mockDeployer,
+				prog:         mockProgress,
+				GlobalOpts:   &GlobalOpts{projectName: tc.project.Name},
+			}
+
+			// WHEN
+			err := opts.delegateDNSFromProject(tc.project)
+
+			// THEN
+			if tc.wantedErr != "" {
+				require.EqualError(t, err, tc.wantedErr)
+			} else {
+				require.Nil(t, err)
+			}
+		})
+	}
+}
diff --git a/internal/pkg/cli/project_init_test.go b/internal/pkg/cli/project_init_test.go
index 0388d2e4..d3103d15 100644
--- a/internal/pkg/cli/project_init_test.go
+++ b/internal/pkg/cli/project_init_test.go
@@ -209,8 +209,9 @@ func TestInitProjectOpts_Execute(t *testing.T) {
 				mockProgress.EXPECT().Start(fmt.Sprintf(fmtDeployProjectStart, "project"))
 				mockDeployer.EXPECT().
 					DeployProject(&deploy.CreateProjectInput{
-						Project:   "project",
-						AccountID: "12345",
+						Project:    "project",
+						AccountID:  "12345",
+						DomainName: "amazon.com",
 					}).Return(nil)
 				mockProgress.EXPECT().Stop(log.Ssuccessf(fmtDeployProjectComplete, "project"))
 			},
diff --git a/internal/pkg/deploy/cloudformation/cloudformation_test.go b/internal/pkg/deploy/cloudformation/cloudformation_test.go
index 3ab11131..03daa561 100644
--- a/internal/pkg/deploy/cloudformation/cloudformation_test.go
+++ b/internal/pkg/deploy/cloudformation/cloudformation_test.go
@@ -355,7 +355,7 @@ func TestDeploy(t *testing.T) {
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			got := tc.cf.deploy(tc.input)
+			got := tc.cf.deploy(tc.input, cloudformation.ChangeSetTypeCreate)
 
 			if tc.want != nil {
 				require.EqualError(t, got, tc.want.Error())
diff --git a/internal/pkg/deploy/cloudformation/project_test.go b/internal/pkg/deploy/cloudformation/project_test.go
index dbf846b6..ef918c28 100644
--- a/internal/pkg/deploy/cloudformation/project_test.go
+++ b/internal/pkg/deploy/cloudformation/project_test.go
@@ -1004,6 +1004,133 @@ func TestGetProjectResourcesByRegion(t *testing.T) {
 	}
 }
 
+func TestDelegateDNSPermissions(t *testing.T) {
+
+	testCases := map[string]struct {
+		project      *archer.Project
+		accountID    string
+		mockCFClient func() *mockCloudFormation
+		want         error
+	}{
+		"Calls Update Stack": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "project",
+				Domain:    "amazon.com",
+			},
+			mockCFClient: func() *mockCloudFormation {
+				return &mockCloudFormation{
+					t: t,
+					mockCreateChangeSet: func(t *testing.T, in *cloudformation.CreateChangeSetInput) (*cloudformation.CreateChangeSetOutput, error) {
+						require.Equal(t, len(in.Parameters), 4)
+						return &cloudformation.CreateChangeSetOutput{
+							StackId: aws.String("stackname"),
+						}, nil
+					},
+					mockExecuteChangeSet: func(t *testing.T, in *cloudformation.ExecuteChangeSetInput) (*cloudformation.ExecuteChangeSetOutput, error) {
+						return nil, nil
+					},
+					mockDescribeStacks: func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) {
+						stack := mockProjectRolesStack("stackname", map[string]string{
+							"ProjectDNSDelegatedAccounts": "1234",
+						})
+						return &cloudformation.DescribeStacksOutput{Stacks: []*cloudformation.Stack{stack}}, nil
+					},
+					mockDescribeChangeSet: func(t *testing.T, in *cloudformation.DescribeChangeSetInput) (*cloudformation.DescribeChangeSetOutput, error) {
+						return &cloudformation.DescribeChangeSetOutput{
+							ExecutionStatus: aws.String(cloudformation.ExecutionStatusAvailable),
+						}, nil
+					},
+					mockWaitUntilChangeSetCreateComplete: func(t *testing.T, in *cloudformation.DescribeChangeSetInput) error {
+						return nil
+					},
+					mockWaitUntilStackUpdateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
+						return nil
+					},
+				}
+			},
+		},
+
+		"Returns error from Describe Stack": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "project",
+				Domain:    "amazon.com",
+			},
+			want: fmt.Errorf("getting existing project infrastructure stack: error"),
+			mockCFClient: func() *mockCloudFormation {
+				return &mockCloudFormation{
+					t: t,
+					mockCreateChangeSet: func(t *testing.T, in *cloudformation.CreateChangeSetInput) (*cloudformation.CreateChangeSetOutput, error) {
+						require.Equal(t, len(in.Parameters), 4)
+						return &cloudformation.CreateChangeSetOutput{
+							StackId: aws.String("stackname"),
+						}, nil
+					},
+					mockExecuteChangeSet: func(t *testing.T, in *cloudformation.ExecuteChangeSetInput) (*cloudformation.ExecuteChangeSetOutput, error) {
+						return nil, nil
+					},
+					mockDescribeStacks: func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) {
+						return nil, fmt.Errorf("error")
+					},
+				}
+			},
+		},
+		"Returns error from Update Stack": {
+			project: &archer.Project{
+				AccountID: "1234",
+				Name:      "project",
+				Domain:    "amazon.com",
+			},
+			want: fmt.Errorf("updating project to allow DNS delegation: failed to execute changeSet name=, stackID=stackname: error"),
+			mockCFClient: func() *mockCloudFormation {
+				return &mockCloudFormation{
+					t: t,
+					mockCreateChangeSet: func(t *testing.T, in *cloudformation.CreateChangeSetInput) (*cloudformation.CreateChangeSetOutput, error) {
+						require.Equal(t, len(in.Parameters), 4)
+						return &cloudformation.CreateChangeSetOutput{
+							StackId: aws.String("stackname"),
+						}, nil
+					},
+					mockExecuteChangeSet: func(t *testing.T, in *cloudformation.ExecuteChangeSetInput) (*cloudformation.ExecuteChangeSetOutput, error) {
+						return nil, fmt.Errorf("error")
+					},
+					mockDescribeStacks: func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) {
+						stack := mockProjectRolesStack("stackname", map[string]string{
+							"ProjectDNSDelegatedAccounts": "1234",
+						})
+						return &cloudformation.DescribeStacksOutput{Stacks: []*cloudformation.Stack{stack}}, nil
+					},
+					mockDescribeChangeSet: func(t *testing.T, in *cloudformation.DescribeChangeSetInput) (*cloudformation.DescribeChangeSetOutput, error) {
+						return &cloudformation.DescribeChangeSetOutput{
+							ExecutionStatus: aws.String(cloudformation.ExecutionStatusAvailable),
+						}, nil
+					},
+					mockWaitUntilChangeSetCreateComplete: func(t *testing.T, in *cloudformation.DescribeChangeSetInput) error {
+						return nil
+					},
+				}
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cf := CloudFormation{
+				client: tc.mockCFClient(),
+				box:    templates.Box(),
+			}
+			got := cf.DelegateDNSPermissions(tc.project, tc.accountID)
+
+			if tc.want != nil {
+				require.EqualError(t, tc.want, got.Error())
+			} else {
+				require.NoError(t, got)
+			}
+		})
+	}
+}
+
 // Useful for mocking a successfully deployed stack
 func getMockSuccessfulDeployCFClient(t *testing.T, stackName string) *mockCloudFormation {
 	return &mockCloudFormation{
@@ -1071,6 +1198,21 @@ func mockProjectResourceStack(stackArn string, outputs map[string]string) *cloud
 	}
 }
 
+func mockProjectRolesStack(stackArn string, parameters map[string]string) *cloudformation.Stack {
+	parametersList := []*cloudformation.Parameter{}
+	for key, val := range parameters {
+		parametersList = append(parametersList, &cloudformation.Parameter{
+			ParameterKey:   aws.String(key),
+			ParameterValue: aws.String(val),
+		})
+	}
+
+	return &cloudformation.Stack{
+		StackId:    aws.String(stackArn),
+		Parameters: parametersList,
+	}
+}
+
 func boxWithProjectTemplate() packd.Box {
 	box := packd.NewMemoryBox()
 
diff --git a/internal/pkg/deploy/cloudformation/stack/project_test.go b/internal/pkg/deploy/cloudformation/stack/project_test.go
index 2caadabf..a1b1dcbb 100644
--- a/internal/pkg/deploy/cloudformation/stack/project_test.go
+++ b/internal/pkg/deploy/cloudformation/stack/project_test.go
@@ -50,6 +50,42 @@ func TestProjTemplate(t *testing.T) {
 	}
 }
 
+func TestDNSDelegationAccounts(t *testing.T) {
+	testCases := map[string]struct {
+		given *deploy.CreateProjectInput
+		want  []string
+	}{
+		"should append project account": {
+			given: &deploy.CreateProjectInput{
+				AccountID: "1234",
+			},
+			want: []string{"1234"},
+		},
+		"should ignore duplicates": {
+			given: &deploy.CreateProjectInput{
+				AccountID:             "1234",
+				DNSDelegationAccounts: []string{"1234"},
+			},
+			want: []string{"1234"},
+		},
+		"should return a set": {
+			given: &deploy.CreateProjectInput{
+				AccountID:             "1234",
+				DNSDelegationAccounts: []string{"4567"},
+			},
+			want: []string{"1234", "4567"},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			projStack := NewProjectStackConfig(tc.given, emptyProjectBox())
+			got := projStack.dnsDelegationAccounts()
+			require.ElementsMatch(t, tc.want, got)
+		})
+	}
+}
+
 func TestProjResourceTemplate(t *testing.T) {
 	properlyEscapedTemplate := `AWSTemplateFormatVersion: '2010-09-09'
 Outputs:
@@ -107,7 +143,7 @@ Outputs:
 }
 
 func TestProjectParameters(t *testing.T) {
-	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
+	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234", DomainName: "amazon.com"}, emptyProjectBox())
 	expectedParams := []*cloudformation.Parameter{
 		{
 			ParameterKey:   aws.String(projectAdminRoleParamName),
@@ -117,6 +153,14 @@ func TestProjectParameters(t *testing.T) {
 			ParameterKey:   aws.String(projectExecutionRoleParamName),
 			ParameterValue: aws.String("testproject-executionrole"),
 		},
+		{
+			ParameterKey:   aws.String(projectDNSDelegatedAccountsKey),
+			ParameterValue: aws.String("1234"),
+		},
+		{
+			ParameterKey:   aws.String(projectDomainNameKey),
+			ParameterValue: aws.String("amazon.com"),
+		},
 	}
 	require.ElementsMatch(t, expectedParams, proj.Parameters())
 }
@@ -186,6 +230,33 @@ func TestToRegionalResources(t *testing.T) {
 	}
 }
 
+func TestDNSDelegatedAccountsForStack(t *testing.T) {
+	testCases := map[string]struct {
+		given map[string]string
+		want  []string
+	}{
+		"should read from parameter and parse comma seperated list": {
+			given: map[string]string{
+				projectDNSDelegatedAccountsKey: "1234,5678",
+			},
+			want: []string{"1234", "5678"},
+		},
+		"should return empty when no field is found": {
+			given: map[string]string{
+				"not a real field": "ok",
+			},
+			want: []string{},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			got := DNSDelegatedAccountsForStack(mockProjectRolesStack("stack", tc.given))
+			require.ElementsMatch(t, tc.want, got)
+		})
+	}
+}
+
 func mockProjectResourceStack(stackArn string, outputs map[string]string) *cloudformation.Stack {
 	outputList := []*cloudformation.Output{}
 	for key, val := range outputs {
@@ -201,6 +272,21 @@ func mockProjectResourceStack(stackArn string, outputs map[string]string) *cloud
 	}
 }
 
+func mockProjectRolesStack(stackArn string, parameters map[string]string) *cloudformation.Stack {
+	parameterList := []*cloudformation.Parameter{}
+	for key, val := range parameters {
+		parameterList = append(parameterList, &cloudformation.Parameter{
+			ParameterKey:   aws.String(key),
+			ParameterValue: aws.String(val),
+		})
+	}
+
+	return &cloudformation.Stack{
+		StackId:    aws.String(stackArn),
+		Parameters: parameterList,
+	}
+}
+
 func TestProjectStackName(t *testing.T) {
 	proj := NewProjectStackConfig(&deploy.CreateProjectInput{Project: "testproject", AccountID: "1234"}, emptyProjectBox())
 	require.Equal(t, fmt.Sprintf("%s-infrastructure-roles", proj.Project), proj.StackName())