Skip to content

Commit

Permalink
feat: CloudFormation Service Role support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mumoshu committed Nov 30, 2018
1 parent eb00133 commit 2f72525
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 1 deletion.
42 changes: 42 additions & 0 deletions pkg/cfn/manager/api.go
Expand Up @@ -3,6 +3,7 @@ package manager
import (
"fmt"
"regexp"
"strings"
"time"

"github.com/pkg/errors"
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -83,13 +91,41 @@ 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)
i.StackId = s.StackId
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 = "<nil role arn. bug?>"
} 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
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions pkg/ctl/cmdutils/cmdutils.go
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion pkg/eks/api.go
Expand Up @@ -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 }

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions pkg/eks/api/api.go
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pkg/testutils/mock_provider.go
Expand Up @@ -13,6 +13,8 @@ import (

// MockProvider stores the mocked APIs
type MockProvider struct {
cfnRoleArn string

cfn *mocks.CloudFormationAPI
eks *mocks.EKSAPI
ec2 *mocks.EC2API
Expand All @@ -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)
Expand Down

0 comments on commit 2f72525

Please sign in to comment.