From 2f72525172a5d76ad7e94f5b6d90e2dff86b71c8 Mon Sep 17 00:00:00 2001 From: Yusuke KUOKA Date: Fri, 30 Nov 2018 01:54:11 -0800 Subject: [PATCH] feat: CloudFormation Service Role support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case your role is too restrictive that `eksctl create cluster` fails due to cloudformation reporting insufficient permissions, specify a service role used by CloudFormation to call AWS API while provisioning stacks on your behalf. ``` eksctl create cluster --cfn-role-arn arn:aws:iam:YOUR_AWS_ACCOUNT_ID:role/eksctl ``` Also note that eksctl now helps you by printing the guidance like below on permission errors: ``` 2018-11-30T01:42:20-08:00 [ℹ] creating cluster stack "eksctl-ferocious-gopher-1543570938-cluster" 2018-11-30T01:42:21-08:00 [✖] ensure that the iam role "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/eksctl" exists, it should also have a trust relationship like the below. { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "cloudformation.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } ``` Resolves #329 --- pkg/cfn/manager/api.go | 42 ++++++++++++++++++++++++++++++++++ pkg/ctl/cmdutils/cmdutils.go | 2 ++ pkg/eks/api.go | 8 ++++++- pkg/eks/api/api.go | 3 +++ pkg/testutils/mock_provider.go | 5 ++++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 2142cdf2b01..16ae7b372e5 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -3,6 +3,7 @@ package manager import ( "fmt" "regexp" + "strings" "time" "github.com/pkg/errors" @@ -35,6 +36,8 @@ type ChangeSet = cloudformation.DescribeChangeSetOutput // StackCollection stores the CloudFormation stack information type StackCollection struct { + cfnSvcRoleArn string + provider api.ClusterProvider spec *api.ClusterConfig tags []*cloudformation.Tag @@ -54,6 +57,7 @@ func NewStackCollection(provider api.ClusterProvider, spec *api.ClusterConfig) * } logger.Debug("tags = %#v", tags) return &StackCollection{ + cfnSvcRoleArn: provider.CloudFormationRoleARN(), provider: provider, spec: spec, tags: tags, @@ -72,6 +76,10 @@ func (c *StackCollection) doCreateStackRequest(i *Stack, templateBody []byte, pa input.SetCapabilities(stackCapabilitiesIAM) } + if c.cfnSvcRoleArn != "" { + input = input.SetRoleARN(c.cfnSvcRoleArn) + } + for k, v := range parameters { p := &cloudformation.Parameter{ ParameterKey: aws.String(k), @@ -83,6 +91,7 @@ func (c *StackCollection) doCreateStackRequest(i *Stack, templateBody []byte, pa logger.Debug("input = %#v", input) s, err := c.provider.CloudFormation().CreateStack(input) if err != nil { + logGuidanceOnAssumeRoleFailure(err, input.RoleARN) return errors.Wrapf(err, "creating CloudFormation stack %q", *i.StackName) } logger.Debug("stack = %#v", s) @@ -90,6 +99,33 @@ func (c *StackCollection) doCreateStackRequest(i *Stack, templateBody []byte, pa return nil } +func logGuidanceOnAssumeRoleFailure(err error, roleArn *string) { + if strings.Contains(err.Error(), "is invalid or cannot be assumed") { + var arn string + if roleArn == nil { + arn = "" + } else { + arn = *roleArn + } + logger.Critical(`ensure that the iam role "%s" exists, it should also have a trust relationship like the below. + +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +`, arn) + } +} + // CreateStack with given name, stack builder instance and parameters; // any errors will be written to errs channel, when nil is written, // assume completion, do not expect more then one error value on the @@ -262,6 +298,7 @@ func (c *StackCollection) doCreateChangeSetRequest(i *Stack, action string, desc StackName: i.StackName, ChangeSetName: &changeSetName, Description: &description, + RoleARN: aws.String(c.cfnSvcRoleArn), } input.SetChangeSetType(cloudformation.ChangeSetTypeUpdate) @@ -273,6 +310,10 @@ func (c *StackCollection) doCreateChangeSetRequest(i *Stack, action string, desc input.SetCapabilities(stackCapabilitiesIAM) } + if c.cfnSvcRoleArn != "" { + input.SetRoleARN(c.cfnSvcRoleArn) + } + for k, v := range parameters { p := &cloudformation.Parameter{ ParameterKey: aws.String(k), @@ -284,6 +325,7 @@ func (c *StackCollection) doCreateChangeSetRequest(i *Stack, action string, desc logger.Debug("creating changeSet, input = %#v", input) s, err := c.provider.CloudFormation().CreateChangeSet(input) if err != nil { + logGuidanceOnAssumeRoleFailure(err, input.RoleARN) return "", errors.Wrap(err, fmt.Sprintf("creating ChangeSet %q for stack %q", changeSetName, *i.StackName)) } logger.Debug("changeSet = %#v", s) diff --git a/pkg/ctl/cmdutils/cmdutils.go b/pkg/ctl/cmdutils/cmdutils.go index 24ea904eb45..b84b2eb940b 100644 --- a/pkg/ctl/cmdutils/cmdutils.go +++ b/pkg/ctl/cmdutils/cmdutils.go @@ -31,6 +31,8 @@ func AddCommonFlagsForAWS(fs *pflag.FlagSet, p *api.ProviderConfig) { fs.StringVarP(&p.Region, "region", "r", "", "AWS region") fs.StringVarP(&p.Profile, "profile", "p", "", "AWS credentials profile to use (overrides the AWS_PROFILE environment variable)") + fs.StringVar(&p.CloudFormationRoleARN, "cfn-role-arn", "", "IAM role used by CloudFormation to call AWS API on your behalf") + fs.DurationVar(&p.WaitTimeout, "aws-api-timeout", api.DefaultWaitTimeout, "") // TODO deprecate in 0.2.0 if err := fs.MarkHidden("aws-api-timeout"); err != nil { diff --git a/pkg/eks/api.go b/pkg/eks/api.go index a63b7af4193..f0e19fb2162 100644 --- a/pkg/eks/api.go +++ b/pkg/eks/api.go @@ -44,11 +44,16 @@ type ProviderServices struct { eks eksiface.EKSAPI ec2 ec2iface.EC2API sts stsiface.STSAPI + + cfnRoleArn string } // CloudFormation returns a representation of the CloudFormation API func (p ProviderServices) CloudFormation() cloudformationiface.CloudFormationAPI { return p.cfn } +// CloudFormationRoleARN returns, if any, a service role used by CloudFormation to call AWS API on your behalf +func (p ProviderServices) CloudFormationRoleARN() string { return p.cfnRoleArn } + // EKS returns a representation of the EKS API func (p ProviderServices) EKS() eksiface.EKSAPI { return p.eks } @@ -76,7 +81,8 @@ type ProviderStatus struct { // New creates a new setup of the used AWS APIs func New(spec *api.ProviderConfig, clusterSpec *api.ClusterConfig) *ClusterProvider { provider := &ProviderServices{ - spec: spec, + spec: spec, + cfnRoleArn: spec.CloudFormationRoleARN, } c := &ClusterProvider{ Provider: provider, diff --git a/pkg/eks/api/api.go b/pkg/eks/api/api.go index 77f70895ec2..758f73752a3 100644 --- a/pkg/eks/api/api.go +++ b/pkg/eks/api/api.go @@ -66,6 +66,7 @@ func (c *ClusterMeta) LogString() string { // ClusterProvider is the interface to AWS APIs type ClusterProvider interface { CloudFormation() cloudformationiface.CloudFormationAPI + CloudFormationRoleARN() string EKS() eksiface.EKSAPI EC2() ec2iface.EC2API STS() stsiface.STSAPI @@ -76,6 +77,8 @@ type ClusterProvider interface { // ProviderConfig holds global parameters for all interactions with AWS APIs type ProviderConfig struct { + CloudFormationRoleARN string + Region string Profile string WaitTimeout time.Duration diff --git a/pkg/testutils/mock_provider.go b/pkg/testutils/mock_provider.go index 652fa1578fa..3f66bb8c01f 100644 --- a/pkg/testutils/mock_provider.go +++ b/pkg/testutils/mock_provider.go @@ -13,6 +13,8 @@ import ( // MockProvider stores the mocked APIs type MockProvider struct { + cfnRoleArn string + cfn *mocks.CloudFormationAPI eks *mocks.EKSAPI ec2 *mocks.EC2API @@ -39,6 +41,9 @@ var ProviderConfig = &api.ProviderConfig{ // CloudFormation returns a representation of the CloudFormation API func (m MockProvider) CloudFormation() cloudformationiface.CloudFormationAPI { return m.cfn } +// CloudFormationRoleARN returns, if any, a service role used by CloudFormation to call AWS API on your behalf +func (m MockProvider) CloudFormationRoleARN() string { return m.cfnRoleArn } + // MockCloudFormation returns a mocked CloudFormation API func (m MockProvider) MockCloudFormation() *mocks.CloudFormationAPI { return m.CloudFormation().(*mocks.CloudFormationAPI)