Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add optional external_id flag when using iam_role #3025

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 17 additions & 4 deletions aws_helper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ func getSTSCredentialsFromIAMRoleOptions(sess *session.Session, iamRoleOptions o
if iamRoleOptions.AssumeRoleSessionName != "" {
p.RoleSessionName = iamRoleOptions.AssumeRoleSessionName
}
if iamRoleOptions.ExternalId != "" {
p.ExternalID = &iamRoleOptions.ExternalId
}
})
return stscreds.NewCredentials(sess, iamRoleOptions.RoleARN, optFns...)
}
Expand Down Expand Up @@ -188,10 +191,20 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error)
sessionDurationSeconds = iamRoleOpts.AssumeRoleDuration
}

input := sts.AssumeRoleInput{
RoleArn: aws.String(iamRoleOpts.RoleARN),
RoleSessionName: aws.String(sessionName),
DurationSeconds: aws.Int64(sessionDurationSeconds),
var input sts.AssumeRoleInput
if iamRoleOpts.ExternalId != "" {
input = sts.AssumeRoleInput{
RoleArn: aws.String(iamRoleOpts.RoleARN),
ExternalId: aws.String(iamRoleOpts.ExternalId),
RoleSessionName: aws.String(sessionName),
DurationSeconds: aws.Int64(sessionDurationSeconds),
}
} else {
input = sts.AssumeRoleInput{
RoleArn: aws.String(iamRoleOpts.RoleARN),
RoleSessionName: aws.String(sessionName),
DurationSeconds: aws.Int64(sessionDurationSeconds),
}
}

output, err := stsClient.AssumeRole(&input)
Expand Down
14 changes: 14 additions & 0 deletions cli/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ func TestParseTerragruntOptionsFromArgs(t *testing.T) {
nil,
},

{
[]string{doubleDashed(commands.FlagNameTerragruntExternalId), "your-set-value"},
mockOptionsWithExternalId(t, util.JoinPath(workingDir, config.DefaultTerragruntConfigPath), workingDir, []string{}, false, "", false, "your-set-value"),
nil,
},

{
[]string{doubleDashed(commands.FlagNameTerragruntIAMAssumeRoleDuration), "36000"},
mockOptionsWithIamAssumeRoleDuration(t, util.JoinPath(workingDir, config.DefaultTerragruntConfigPath), workingDir, []string{}, false, "", false, 36000),
Expand Down Expand Up @@ -231,6 +237,14 @@ func mockOptionsWithIamRole(t *testing.T, terragruntConfigPath string, workingDi
return opts
}

func mockOptionsWithExternalId(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, externalId string) *options.TerragruntOptions {
opts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)
opts.OriginalIAMRoleOptions.ExternalId = externalId
opts.IAMRoleOptions.ExternalId = externalId

return opts
}

func mockOptionsWithIamAssumeRoleDuration(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, iamAssumeRoleDuration int64) *options.TerragruntOptions {
opts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)
opts.OriginalIAMRoleOptions.AssumeRoleDuration = iamAssumeRoleDuration
Expand Down
7 changes: 7 additions & 0 deletions cli/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
FlagNameTerragruntSourceMap = "terragrunt-source-map"
FlagNameTerragruntSourceUpdate = "terragrunt-source-update"
FlagNameTerragruntIAMRole = "terragrunt-iam-role"
FlagNameTerragruntExternalId = "terragrunt-external-id"
FlagNameTerragruntIAMAssumeRoleDuration = "terragrunt-iam-assume-role-duration"
FlagNameTerragruntIAMAssumeRoleSessionName = "terragrunt-iam-assume-role-session-name"
FlagNameTerragruntIgnoreDependencyErrors = "terragrunt-ignore-dependency-errors"
Expand Down Expand Up @@ -126,6 +127,12 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags {
EnvVar: "TERRAGRUNT_IAM_ROLE",
Usage: "Assume the specified IAM role before executing Terraform. Can also be set via the TERRAGRUNT_IAM_ROLE environment variable.",
},
&cli.GenericFlag[string]{
Name: FlagNameTerragruntExternalId,
Destination: &opts.IAMRoleOptions.ExternalId,
EnvVar: "TERRAGRUNT_EXTERNAL_ID",
Usage: "Assume the specified IAM role, together with the given ExternalId, before executing Terraform. Can also be set via the TERRAGRUNT_EXTERNAL_ID environment variable.",
},
&cli.GenericFlag[int64]{
Name: FlagNameTerragruntIAMAssumeRoleDuration,
Destination: &opts.IAMRoleOptions.AssumeRoleDuration,
Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const (
MetadataPreventDestroy = "prevent_destroy"
MetadataSkip = "skip"
MetadataIamRole = "iam_role"
MetadataExternalId = "external_id"
MetadataIamAssumeRoleDuration = "iam_assume_role_duration"
MetadataIamAssumeRoleSessionName = "iam_assume_role_session_name"
MetadataInputs = "inputs"
Expand Down Expand Up @@ -90,6 +91,7 @@ type TerragruntConfig struct {
PreventDestroy *bool
Skip bool
IamRole string
ExternalId string
IamAssumeRoleDuration *int64
IamAssumeRoleSessionName string
Inputs map[string]interface{}
Expand Down Expand Up @@ -123,6 +125,7 @@ func (conf *TerragruntConfig) String() string {
func (conf *TerragruntConfig) GetIAMRoleOptions() options.IAMRoleOptions {
configIAMRoleOptions := options.IAMRoleOptions{
RoleARN: conf.IamRole,
ExternalId: conf.ExternalId,
AssumeRoleSessionName: conf.IamAssumeRoleSessionName,
}
if conf.IamAssumeRoleDuration != nil {
Expand Down Expand Up @@ -162,6 +165,7 @@ type terragruntConfigFile struct {
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
IamRole *string `hcl:"iam_role,attr"`
ExternalId *string `hcl:"external_id,attr"`
IamAssumeRoleDuration *int64 `hcl:"iam_assume_role_duration,attr"`
IamAssumeRoleSessionName *string `hcl:"iam_assume_role_session_name,attr"`
TerragruntDependencies []Dependency `hcl:"dependency,block"`
Expand Down Expand Up @@ -1046,6 +1050,11 @@ func convertToTerragruntConfig(ctx *ParsingContext, configPath string, terragrun
terragruntConfig.SetFieldMetadata(MetadataIamRole, defaultMetadata)
}

if terragruntConfigFromFile.ExternalId != nil {
terragruntConfig.ExternalId = *terragruntConfigFromFile.ExternalId
terragruntConfig.SetFieldMetadata(MetadataExternalId, defaultMetadata)
}

if terragruntConfigFromFile.IamAssumeRoleDuration != nil {
terragruntConfig.IamAssumeRoleDuration = terragruntConfigFromFile.IamAssumeRoleDuration
terragruntConfig.SetFieldMetadata(MetadataIamAssumeRoleDuration, defaultMetadata)
Expand Down
1 change: 1 addition & 0 deletions config/config_as_cty.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
output[MetadataTerragruntVersionConstraint] = gostringToCty(config.TerragruntVersionConstraint)
output[MetadataDownloadDir] = gostringToCty(config.DownloadDir)
output[MetadataIamRole] = gostringToCty(config.IamRole)
output[MetadataExternalId] = gostringToCty(config.ExternalId)
output[MetadataSkip] = goboolToCty(config.Skip)
output[MetadataIamAssumeRoleSessionName] = gostringToCty(config.IamAssumeRoleSessionName)

Expand Down
4 changes: 4 additions & 0 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type terraformConfigSourceOnly struct {
// terragruntFlags is a struct that can be used to only decode the flag attributes (skip and prevent_destroy)
type terragruntFlags struct {
IamRole *string `hcl:"iam_role,attr"`
ExternalId *string `hcl:"external_id,attr"`
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
Remain hcl.Body `hcl:",remain"`
Expand Down Expand Up @@ -289,6 +290,9 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
if decoded.IamRole != nil {
output.IamRole = *decoded.IamRole
}
if decoded.ExternalId != nil {
output.ExternalId = *decoded.ExternalId
}

case TerragruntInputs:
decoded := terragruntInputs{}
Expand Down
8 changes: 8 additions & 0 deletions config/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ func (targetConfig *TerragruntConfig) Merge(sourceConfig *TerragruntConfig, terr
targetConfig.IamRole = sourceConfig.IamRole
}

if sourceConfig.ExternalId != "" {
targetConfig.ExternalId = sourceConfig.ExternalId
}

if sourceConfig.IamAssumeRoleDuration != nil {
targetConfig.IamAssumeRoleDuration = sourceConfig.IamAssumeRoleDuration
}
Expand Down Expand Up @@ -323,6 +327,10 @@ func (targetConfig *TerragruntConfig) DeepMerge(sourceConfig *TerragruntConfig,
targetConfig.IamRole = sourceConfig.IamRole
}

if sourceConfig.ExternalId != "" {
targetConfig.ExternalId = sourceConfig.ExternalId
}

if sourceConfig.IamAssumeRoleDuration != nil {
targetConfig.IamAssumeRoleDuration = sourceConfig.IamAssumeRoleDuration
}
Expand Down
2 changes: 1 addition & 1 deletion docs/_docs/01_getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Currently terragrunt parses the config in the following order:

2. `locals` block

3. Evaluation of values for `iam_role`, `iam_assume_role_duration`, and `iam_assume_role_session_name` attributes, if defined
3. Evaluation of values for `iam_role`, `external_id`, `iam_assume_role_duration`, and `iam_assume_role_session_name` attributes, if defined

4. `dependencies` block

Expand Down
24 changes: 24 additions & 0 deletions docs/_docs/02_features/work-with-multiple-aws-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,27 @@ iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
Terragrunt will resolve the value of the option by first looking for the cli argument, then looking for the environment variable, then defaulting to the value specified in the config file.

Terragrunt will call the `sts assume-role` API on your behalf and expose the credentials it gets back as environment variables when running Terraform. The advantage of this approach is that you can store your AWS credentials in a secret store and never write them to disk in plaintext, you get fresh credentials on every run of Terragrunt, without the complexity of calling `assume-role` yourself, and you don’t have to modify your Terraform code or backend configuration at all.

### Configuring Terragrunt to assume an IAM role with ExternalId

More: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html

You might be in a situation where you need to use ExternalIds when assuming an IAM role. Besides the IAM role itself, Terragrunt can be configured to attach an ExternalId when assuming said role.

``` bash
terragrunt apply --terragrunt-external-id "your-set-value"
```

Alternatively, you can set the `TERRAGRUNT_EXTERNAL_ID` environment variable:

``` bash
export TERRAGRUNT_EXTERNAL_ID="your-set-value"
terragrunt apply
```

Additionally, you can specify an `external_id` property in the terragrunt config:

``` hcl
iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
external_id = "your-set-value"
```
9 changes: 9 additions & 0 deletions docs/_docs/04_reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,15 @@ When passed in, the `*-all` commands continue processing components even if a de
Assume the specified IAM role ARN before running Terraform or AWS commands. This is a convenient way to use Terragrunt
and Terraform with multiple AWS accounts.

### terragrunt-external-id

**CLI Arg**: `--terragrunt-external-id`<br/>
**Environment Variable**: `TERRAGRUNT_EXTERNAL_ID`<br/>
**Requires an argument**: `--terragrunt-iam-role "your-set-value"`

Assume the specified IAM role ARN, together with ExternalId, before running Terraform or AWS commands.
This is a convenient way to use Terragrunt and Terraform with multiple AWS accounts.


### terragrunt-iam-assume-role-duration

Expand Down
18 changes: 18 additions & 0 deletions docs/_docs/04_reference/config-blocks-and-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ generate = local.common.generate
- [prevent_destroy](#prevent_destroy)
- [skip](#skip)
- [iam_role](#iam_role)
- [external_id](#external_id)
- [iam_assume_role_duration](#iam_assume_role_duration)
- [iam_assume_role_session_name](#iam_assume_role_session_name)
- [terraform_binary](#terraform_binary)
Expand Down Expand Up @@ -1211,6 +1212,23 @@ iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
* Value of `iam_role` can reference local variables
* Definitions of `iam_role` included from other HCL files through `include`

### external_id

The `external_id` attribute can be used to specify an ExternalId that Terragrunt should use when assuming a role, prior to invoking Terraform.

The precedence is as follows: `--terragrunt-external-id` command line option → `TERRAGRUNT_EXTERNAL_ID` env variable →
`external_id` attribute of the `terragrunt.hcl` file in the module directory → `external_id` attribute of the included
`terragrunt.hcl`.

Example:

```hcl
external_id = "your-set-value"
```
**Notes:**
* Value of `external_id` can reference local variables
* Definitions of `external_id` included from other HCL files through `include`

### iam_assume_role_duration

The `iam_assume_role_duration` attribute can be used to specify the STS session duration, in seconds, for the IAM role that Terragrunt should assume prior to invoking Terraform.
Expand Down
8 changes: 8 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ type IAMRoleOptions struct {
// The ARN of an IAM Role to assume. Used when accessing AWS, both internally and through terraform.
RoleARN string

// The ExternalId to use when assuming RoleARN. Used when accessing AWS, both internally and through terraform.
// See: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html
ExternalId string

// Duration of the STS Session when assuming the role.
AssumeRoleDuration int64

Expand All @@ -287,6 +291,10 @@ func MergeIAMRoleOptions(target IAMRoleOptions, source IAMRoleOptions) IAMRoleOp
out.RoleARN = source.RoleARN
}

if source.ExternalId != "" {
out.ExternalId = source.ExternalId
}

if source.AssumeRoleDuration != 0 {
out.AssumeRoleDuration = source.AssumeRoleDuration
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions test/fixture-s3-encryption/custom-key/backend.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "s3" {
bucket = "terragrunt-test-bucket-qh7pvm"
dynamodb_table = "terragrunt-test-locks-jmktrj"
bucket = "terragrunt-test-bucket-av7csi"
dynamodb_table = "terragrunt-test-locks-rdpe2z"
encrypt = true
key = "terraform.tfstate"
region = "us-west-2"
Expand Down
4 changes: 2 additions & 2 deletions test/fixture-s3-encryption/sse-aes/backend.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "s3" {
bucket = "terragrunt-test-bucket-hid5kv"
dynamodb_table = "terragrunt-test-locks-xrkt1b"
bucket = "terragrunt-test-bucket-dxbsru"
dynamodb_table = "terragrunt-test-locks-3fwxqx"
encrypt = true
key = "terraform.tfstate"
region = "us-west-2"
Expand Down