Repository: gruntwork-io/terragrunt

Issue:
session-name for assumed role
Hi, I'm trying use terragrunt and atlantis with some mixed up results - problem being the session name for assume-role. Currently it seems to be generated automatically:
`assumed-role/atlantis-test/a839d6a9-atlantis-test is not authorized to perform: sts:AssumeRole`
I've tried using `AWS_SESSION_TOKEN` and `AWS_ROLE_SESSION_NAME` but to no effect. Is it possible to somehow enforce setting of the session_name during assume_role to a specific string? I need this for principal in an IAM role to be assumed. The general idea works, if I use the above generated session name (`a839d6a9-atlantis-test`) as the principal but it would be changed with atlantis container restart. Any ideas would be more than welcome :)

Hints from issue comments:
@mbolek: Also... If I may try to squeeze in another question -> I've messed up a bit and currently have a single tfstate file (per environment) with all the modules inside and would like to modernize it into a typical tfstate per module. Is there a better/other idea than doing `state mv` to separate files manually and adding `terragrunt.hcl` with module's configuration ?@yorinasub17: We actually don't use atlantis at Gruntwork so will need a bit more info here. Specifically, how are you assuming the initial role `atlantis-test`? Is that done:

- In atlantis? If so, can you point us to the documentation for how you are assuming the role?
- In terragrunt? If so, which mechanism?
- Outside both? If so, what command/mechanism?

---

For the second question, unfortunately there aren't any good refactoring tools available in either terraform or terragrunt. Your options are:

- Recreate the stack. This is typically preferred given the amount of resources you would have to touch. This is usually doable without downtime for applications and servers, but not for databases.
- Using a combination of `import` and `state rm`. The advantage of this approach is that if the `import` fails, you can "roll back" or start over by deleting the new state, since you don't remove from the old state until you explicitly call `state rm`.
- Using `state mv`.@mbolek: hey Yoriyasu, thanks for the state suggestions, I'm sort of used to `state mv` with refactoring already to will probably try with that one.

---

As for atlantis  - it's running as a container inside kubernetes and gets its role with kube2iam and then I was hoping it could do another 'assume-role' for the correct credentials in VPC B. I am passing `iam_role = "arn:aws:iam:0123456789012:role/atlantis-testing` inside `terragrunt.hcl`. This role is created in VPC B but I would like to make it 'securer' by specifying only single principal that is able to assume it. AWS docs state it should look like this `"Principal": { "AWS": "arn:aws:sts::AWS-account-ID:assumed-role/role-name/role-session-name" }`. The last part seems to be generated somewhere. I have tried configuring provider in module's `main.tf`:
```
provider "aws" {                                                                                                        
  region  = var.aws_region                                                                                              
  version = "~> 2.0"                                                                                                    
  assume_role = {                                                                                                       
    session_name = "DEV_test_atlantis"                                                                                  
  }                                                                                                                     
}    
```
but it doesn't seem to be taken into account. I have also tried with env variable `AWS_ROLE_SESSION_NAME` which should work for aws-cli (https://docs.amazonaws.cn/en_us/cli/latest/userguide/cli-configure-role.html) but this one also doesn't change the session name I see in error message.
Additionally, from what I was told by atlantis team, atlantis is generally a wrapper so it should honor any setup (https://www.runatlantis.io/docs/provider-credentials.html). I've just thought of trying to create a `~/.aws/config` with `role_session_name` set, but it doesn't seem to affect anything.
Generally I think it is possible to achieve this double assume-role since if I add the autogenerated ID as a principal it works. The part that eludes me is the ID. Since terragrunt is the one that invokes the role assumption, is it maybe possible to set session_name directly?
Also, if I get into the container and exec terragrunt manually I get the same error with the same ID@heitorlessa: Came here wondering if you found a solution @mbolek to this? 

I thought Terragrunt also supported AWS_ROLE_SESSION_NAME which makes it easier for auditing access later.@mbolek: @heitorlessa sadly I did not. Tried for some time and postponed the idea for 'the near future' which unfortunately hasn't come yet :(
I guess you're also facing it today ? So either the tools haven't changed/caught up or it's not the correct way to do it@yorinasub17: Sorry for the delay in responding here. Since this is using `iam_role` to assume the role, this is an issue in how terragrunt is assuming the role. I think we can solve this by specifying a new parameter `iam_role_session_name` and `--terragrunt-iam-role-session-name` to specify the session name when terragrunt assumes the role.

A PR to add this feature would be most appreciated! As far as pointers for implementation goes, the idea would be to expose those settings and plumb them through to https://github.com/gruntwork-io/terragrunt/blob/master/aws_helper/config.go#L142-L146@github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for raising this issue.

Solution patch:
diff --git a/aws_helper/config.go b/aws_helper/config.go
index a1389f12..aa3f3796 100644
--- a/aws_helper/config.go
+++ b/aws_helper/config.go
@@ -2,7 +2,6 @@ package aws_helper
 
 import (
 	"fmt"
-	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/arn"
@@ -125,7 +124,7 @@ func CreateAwsSession(config *AwsSessionConfig, terragruntOptions *options.Terra
 }
 
 // Make API calls to AWS to assume the IAM role specified and return the temporary AWS credentials to use that role
-func AssumeIamRole(iamRoleArn string, sessionDurationSeconds int64) (*sts.Credentials, error) {
+func AssumeIamRole(iamRoleArn, sessionName string, sessionDurationSeconds int64) (*sts.Credentials, error) {
 	sessionOptions := session.Options{SharedConfigState: session.SharedConfigEnable}
 	sess, err := session.NewSessionWithOptions(sessionOptions)
 	if err != nil {
@@ -141,7 +140,7 @@ func AssumeIamRole(iamRoleArn string, sessionDurationSeconds int64) (*sts.Creden
 
 	input := sts.AssumeRoleInput{
 		RoleArn:         aws.String(iamRoleArn),
-		RoleSessionName: aws.String(fmt.Sprintf("terragrunt-%d", time.Now().UTC().UnixNano())),
+		RoleSessionName: aws.String(sessionName),
 		DurationSeconds: aws.Int64(sessionDurationSeconds),
 	}
 
@@ -220,7 +219,7 @@ func AssumeRoleAndUpdateEnvIfNecessary(terragruntOptions *options.TerragruntOpti
 	}
 
 	terragruntOptions.Logger.Debugf("Assuming IAM role %s with a session duration of %d seconds.", terragruntOptions.IamRole, terragruntOptions.IamAssumeRoleDuration)
-	creds, err := AssumeIamRole(terragruntOptions.IamRole, terragruntOptions.IamAssumeRoleDuration)
+	creds, err := AssumeIamRole(terragruntOptions.IamRole, terragruntOptions.IamAssumeRoleSessionName, terragruntOptions.IamAssumeRoleDuration)
 	if err != nil {
 		return err
 	}
diff --git a/cli/args.go b/cli/args.go
index 319628b0..1b6288b3 100644
--- a/cli/args.go
+++ b/cli/args.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/gruntwork-io/terragrunt/config"
 	"github.com/gruntwork-io/terragrunt/errors"
@@ -125,6 +126,15 @@ func parseTerragruntOptionsFromArgs(terragruntVersion string, args []string, wri
 		return nil, err
 	}
 
+	defaultIamAssumeRoleSessionName := os.Getenv("TERRAGRUNT_IAM_ASSUME_ROLE_SESSION_NAME")
+	if defaultIamAssumeRoleSessionName == "" {
+		defaultIamAssumeRoleSessionName = fmt.Sprintf("terragrunt-%d", time.Now().UTC().UnixNano())
+	}
+	iamAssumeRoleSessionName, err := parseStringArg(args, optTerragruntIAMAssumeRoleSessionName, defaultIamAssumeRoleSessionName)
+	if err != nil {
+		return nil, err
+	}
+
 	excludeDirs, err := parseMultiStringArg(args, optTerragruntExcludeDir, []string{})
 	if err != nil {
 		return nil, err
@@ -202,7 +212,9 @@ func parseTerragruntOptionsFromArgs(terragruntVersion string, args []string, wri
 	opts.ErrWriter = errWriter
 	opts.Env = parseEnvironmentVariables(os.Environ())
 	opts.IamRole = iamRole
+	opts.OriginalIamRole = iamRole
 	opts.IamAssumeRoleDuration = int64(IamAssumeRoleDuration)
+	opts.IamAssumeRoleSessionName = iamAssumeRoleSessionName
 	opts.ExcludeDirs = excludeDirs
 	opts.IncludeDirs = includeDirs
 	opts.StrictInclude = strictInclude
diff --git a/cli/cli_app.go b/cli/cli_app.go
index f6e504ea..15766eba 100644
--- a/cli/cli_app.go
+++ b/cli/cli_app.go
@@ -38,6 +38,7 @@ const (
 	optTerragruntSourceUpdate                = "terragrunt-source-update"
 	optTerragruntIAMRole                     = "terragrunt-iam-role"
 	optTerragruntIAMAssumeRoleDuration       = "terragrunt-iam-assume-role-duration"
+	optTerragruntIAMAssumeRoleSessionName    = "terragrunt-iam-assume-role-session-name"
 	optTerragruntIgnoreDependencyErrors      = "terragrunt-ignore-dependency-errors"
 	optTerragruntIgnoreDependencyOrder       = "terragrunt-ignore-dependency-order"
 	optTerragruntIgnoreExternalDependencies  = "terragrunt-ignore-external-dependencies"
@@ -77,6 +78,7 @@ var allTerragruntStringOpts = []string{
 	optTerragruntSourceMap,
 	optTerragruntIAMRole,
 	optTerragruntIAMAssumeRoleDuration,
+	optTerragruntIAMAssumeRoleSessionName,
 	optTerragruntExcludeDir,
 	optTerragruntIncludeDir,
 	optTerragruntParallelism,
@@ -231,6 +233,7 @@ GLOBAL OPTIONS:
    terragrunt-source-update                     Delete the contents of the temporary folder to clear out any old, cached source code before downloading new source code into it.
    terragrunt-iam-role                          Assume the specified IAM role before executing Terraform. Can also be set via the TERRAGRUNT_IAM_ROLE environment variable.
    terragrunt-iam-assume-role-duration          Session duration for IAM Assume Role session. Can also be set via the TERRAGRUNT_IAM_ASSUME_ROLE_DURATION environment variable.
+   terragrunt-iam-assume-role-session-name      Name for the IAM Assummed Role session. Can also be set via TERRAGRUNT_IAM_ASSUME_ROLE_SESSION_NAME environment variable.
    terragrunt-ignore-dependency-errors          *-all commands continue processing components even if a dependency fails.
    terragrunt-ignore-dependency-order           *-all commands will be run disregarding the dependencies
    terragrunt-ignore-external-dependencies      *-all commands will not attempt to include external dependencies
@@ -398,6 +401,10 @@ func RunTerragrunt(terragruntOptions *options.TerragruntOptions) error {
 		terragruntOptions.IamAssumeRoleDuration = *terragruntConfig.IamAssumeRoleDuration
 	}
 
+	if terragruntOptions.IamAssumeRoleSessionName == "" {
+		terragruntOptions.IamAssumeRoleSessionName = terragruntConfig.IamAssumeRoleSessionName
+	}
+
 	if err := aws_helper.AssumeRoleAndUpdateEnvIfNecessary(terragruntOptions); err != nil {
 		return err
 	}
diff --git a/cli/hclfmt.go b/cli/hclfmt.go
index 6977dfe5..e5cc0b39 100644
--- a/cli/hclfmt.go
+++ b/cli/hclfmt.go
@@ -29,11 +29,11 @@ func runHCLFmt(terragruntOptions *options.TerragruntOptions) error {
 		if !filepath.IsAbs(targetFile) {
 			targetFile = util.JoinPath(workingDir, targetFile)
 		}
-		terragruntOptions.Logger.Infof("Formatting hcl file at: %s.", targetFile)
+		terragruntOptions.Logger.Debugf("Formatting hcl file at: %s.", targetFile)
 		return formatTgHCL(terragruntOptions, targetFile)
 	}
 
-	terragruntOptions.Logger.Infof("Formatting hcl files from the directory tree %s.", terragruntOptions.WorkingDir)
+	terragruntOptions.Logger.Debugf("Formatting hcl files from the directory tree %s.", terragruntOptions.WorkingDir)
 	// zglob normalizes paths to "/"
 	tgHclFiles, err := zglob.Glob(util.JoinPath(workingDir, "**", "*.hcl"))
 	if err != nil {
@@ -66,7 +66,7 @@ func runHCLFmt(terragruntOptions *options.TerragruntOptions) error {
 // formatTgHCL uses the hcl2 library to format the hcl file. This will attempt to parse the HCL file first to
 // ensure that there are no syntax errors, before attempting to format it.
 func formatTgHCL(terragruntOptions *options.TerragruntOptions, tgHclFile string) error {
-	terragruntOptions.Logger.Infof("Formatting %s", tgHclFile)
+	terragruntOptions.Logger.Debugf("Formatting %s", tgHclFile)
 
 	info, err := os.Stat(tgHclFile)
 	if err != nil {
@@ -89,14 +89,18 @@ func formatTgHCL(terragruntOptions *options.TerragruntOptions, tgHclFile string)
 
 	newContents := hclwrite.Format(contents)
 
-	if terragruntOptions.Check {
-		if !bytes.Equal(newContents, contents) {
-			return fmt.Errorf("Invalid file format %s", tgHclFile)
-		}
-		return nil
+	fileUpdated := !bytes.Equal(newContents, contents)
+
+	if terragruntOptions.Check && fileUpdated {
+		return fmt.Errorf("Invalid file format %s", tgHclFile)
 	}
 
-	return ioutil.WriteFile(tgHclFile, newContents, info.Mode())
+	if fileUpdated {
+		terragruntOptions.Logger.Infof("%s was updated", tgHclFile)
+		return ioutil.WriteFile(tgHclFile, newContents, info.Mode())
+	}
+
+	return nil
 }
 
 // checkErrors takes in the contents of a hcl file and looks for syntax errors.
diff --git a/config/config.go b/config/config.go
index 3dc87c28..a77df381 100644
--- a/config/config.go
+++ b/config/config.go
@@ -42,6 +42,7 @@ type TerragruntConfig struct {
 	Skip                        bool
 	IamRole                     string
 	IamAssumeRoleDuration       *int64
+	IamAssumeRoleSessionName    string
 	Inputs                      map[string]interface{}
 	Locals                      map[string]interface{}
 	TerragruntDependencies      []Dependency
@@ -87,13 +88,14 @@ type terragruntConfigFile struct {
 	RemoteState     *remoteStateConfigFile `hcl:"remote_state,block"`
 	RemoteStateAttr *cty.Value             `hcl:"remote_state,optional"`
 
-	Dependencies           *ModuleDependencies `hcl:"dependencies,block"`
-	DownloadDir            *string             `hcl:"download_dir,attr"`
-	PreventDestroy         *bool               `hcl:"prevent_destroy,attr"`
-	Skip                   *bool               `hcl:"skip,attr"`
-	IamRole                *string             `hcl:"iam_role,attr"`
-	IamAssumeRoleDuration  *int64              `hcl:"iam_assume_role_duration,attr"`
-	TerragruntDependencies []Dependency        `hcl:"dependency,block"`
+	Dependencies             *ModuleDependencies `hcl:"dependencies,block"`
+	DownloadDir              *string             `hcl:"download_dir,attr"`
+	PreventDestroy           *bool               `hcl:"prevent_destroy,attr"`
+	Skip                     *bool               `hcl:"skip,attr"`
+	IamRole                  *string             `hcl:"iam_role,attr"`
+	IamAssumeRoleDuration    *int64              `hcl:"iam_assume_role_duration,attr"`
+	IamAssumeRoleSessionName *string             `hcl:"iam_assume_role_session_name,attr"`
+	TerragruntDependencies   []Dependency        `hcl:"dependency,block"`
 
 	// We allow users to configure code generation via blocks:
 	//
@@ -601,7 +603,7 @@ func ParseConfigString(
 
 	// Initial evaluation of configuration to load flags like IamRole which will be used for final parsing
 	// https://github.com/gruntwork-io/terragrunt/issues/667
-	if err := setIAMRole(configString, terragruntOptions, filename); err != nil {
+	if err := setIAMRole(configString, terragruntOptions, includeFromChild, filename); err != nil {
 		return nil, err
 	}
 
@@ -654,9 +656,9 @@ func ParseConfigString(
 }
 
 // setIAMRole - extract IAM role details from Terragrunt flags block
-func setIAMRole(configString string, terragruntOptions *options.TerragruntOptions, filename string) error {
+func setIAMRole(configString string, terragruntOptions *options.TerragruntOptions, includeFromChild *IncludeConfig, filename string) error {
 	if terragruntOptions.IamRole == "" {
-		iamConfig, err := PartialParseConfigString(configString, terragruntOptions, nil, filename, []PartialDecodeSectionType{TerragruntFlags})
+		iamConfig, err := PartialParseConfigString(configString, terragruntOptions, includeFromChild, filename, []PartialDecodeSectionType{TerragruntFlags})
 		if err != nil {
 			return err
 		}
@@ -666,6 +668,9 @@ func setIAMRole(configString string, terragruntOptions *options.TerragruntOption
 		if iamConfig.IamAssumeRoleDuration != nil {
 			terragruntOptions.IamAssumeRoleDuration = *iamConfig.IamAssumeRoleDuration
 		}
+		if iamConfig.IamAssumeRoleSessionName != "" {
+			terragruntOptions.IamAssumeRoleSessionName = iamConfig.IamAssumeRoleSessionName
+		}
 	}
 	return nil
 }
@@ -804,6 +809,10 @@ func convertToTerragruntConfig(
 		terragruntConfig.IamAssumeRoleDuration = terragruntConfigFromFile.IamAssumeRoleDuration
 	}
 
+	if terragruntConfigFromFile.IamAssumeRoleSessionName != nil {
+		terragruntConfig.IamAssumeRoleSessionName = *terragruntConfigFromFile.IamAssumeRoleSessionName
+	}
+
 	generateBlocks := []terragruntGenerateBlock{}
 	generateBlocks = append(generateBlocks, terragruntConfigFromFile.GenerateBlocks...)
 
diff --git a/config/config_as_cty.go b/config/config_as_cty.go
index 14822259..dfb78d95 100644
--- a/config/config_as_cty.go
+++ b/config/config_as_cty.go
@@ -26,6 +26,7 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
 	output["download_dir"] = gostringToCty(config.DownloadDir)
 	output["iam_role"] = gostringToCty(config.IamRole)
 	output["skip"] = goboolToCty(config.Skip)
+	output["iam_assume_role_session_name"] = gostringToCty(config.IamAssumeRoleSessionName)
 
 	terraformConfigCty, err := terraformConfigAsCty(config.Terraform)
 	if err != nil {
diff --git a/config/dependency.go b/config/dependency.go
index f4c6baef..93e56a8a 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -435,6 +435,11 @@ func getOutputJsonWithCaching(targetConfig string, terragruntOptions *options.Te
 func cloneTerragruntOptionsForDependency(terragruntOptions *options.TerragruntOptions, targetConfig string) *options.TerragruntOptions {
 	targetOptions := terragruntOptions.Clone(targetConfig)
 	targetOptions.OriginalTerragruntConfigPath = targetConfig
+	// clear IamRole in case if it is different from one passed through CLI to allow dependencies to define own iam roles
+	// https://github.com/gruntwork-io/terragrunt/issues/1853#issuecomment-940102676
+	if targetOptions.IamRole != targetOptions.OriginalIamRole {
+		targetOptions.IamRole = ""
+	}
 	return targetOptions
 }
 
diff --git a/config/locals.go b/config/locals.go
index 393fa2a5..a643ac1b 100644
--- a/config/locals.go
+++ b/config/locals.go
@@ -1,6 +1,7 @@
 package config
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/hashicorp/hcl/v2"
@@ -100,7 +101,8 @@ func evaluateLocalsBlock(
 		// This is an error because we couldn't evaluate all locals
 		terragruntOptions.Logger.Errorf("Not all locals could be evaluated:")
 		for _, local := range locals {
-			terragruntOptions.Logger.Errorf("\t- %s", local.Name)
+			_, reason := canEvaluate(terragruntOptions, local.Expr, evaluatedLocals)
+			terragruntOptions.Logger.Errorf("\t- %s [REASON: %s]", local.Name, reason)
 		}
 		return nil, errors.WithStackTrace(CouldNotEvaluateAllLocalsError{})
 	}
@@ -165,7 +167,8 @@ func attemptEvaluateLocals(
 		newEvaluatedLocals[key] = val
 	}
 	for _, local := range locals {
-		if canEvaluate(terragruntOptions, local.Expr, evaluatedLocals) {
+		localEvaluated, _ := canEvaluate(terragruntOptions, local.Expr, evaluatedLocals)
+		if localEvaluated {
 			evaluatedVal, diags := local.Expr.Value(evalCtx)
 			if diags.HasErrors() {
 				diagsWriter.WriteDiagnostics(diags)
@@ -192,49 +195,68 @@ func attemptEvaluateLocals(
 // following is true:
 // - It has no references to other locals.
 // - It has references to other locals that have already been evaluated.
+// Note that the second return value is a human friendly reason for why the expression can not be evaluated, and is
+// useful for error reporting.
 func canEvaluate(
 	terragruntOptions *options.TerragruntOptions,
 	expression hcl.Expression,
 	evaluatedLocals map[string]cty.Value,
-) bool {
+) (bool, string) {
 	vars := expression.Variables()
 	if len(vars) == 0 {
 		// If there are no local variable references, we can evaluate this expression.
-		return true
+		return true, ""
 	}
 
 	for _, var_ := range vars {
 		// This should never happen, but if it does, we can't evaluate this expression.
 		if var_.IsRelative() {
-			return false
+			reason := "You've reached an impossible condition and is almost certainly a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl file that caused this."
+			return false, reason
 		}
 
+		rootName := var_.RootName()
+
 		// If the variable is `include`, then we can evaluate it now
-		if var_.RootName() == "include" {
+		if rootName == "include" {
 			continue
 		}
 
 		// We can't evaluate any variable other than `local`
-		if var_.RootName() != "local" {
-			return false
+		if rootName != "local" {
+			reason := fmt.Sprintf(
+				"Can't evaluate expression at %s: you can only reference other local variables here, but it looks like you're referencing something else (%s is not defined)",
+				expression.Range(),
+				rootName,
+			)
+			return false, reason
 		}
 
 		// If we can't get any local name, we can't evaluate it.
 		localName := getLocalName(terragruntOptions, var_)
 		if localName == "" {
-			return false
+			reason := fmt.Sprintf(
+				"Can't evaluate expression at %s because local var name can not be determined.",
+				expression.Range(),
+			)
+			return false, reason
 		}
 
 		// If the referenced local isn't evaluated, we can't evaluate this expression.
 		_, hasEvaluated := evaluatedLocals[localName]
 		if !hasEvaluated {
-			return false
+			reason := fmt.Sprintf(
+				"Can't evaluate expression at %s because local reference '%s' is not evaluated. Either it is not ready yet in the current pass, or there was an error evaluating it in an earlier stage.",
+				expression.Range(),
+				localName,
+			)
+			return false, reason
 		}
 	}
 
 	// If we made it this far, this means all the variables referenced are accounted for and we can evaluate this
 	// expression.
-	return true
+	return true, ""
 }
 
 // getLocalName takes a variable reference encoded as a HCL tree traversal that is rooted at the name `local` and
diff --git a/configstack/stack.go b/configstack/stack.go
index d0dc86b6..c0f2359b 100644
--- a/configstack/stack.go
+++ b/configstack/stack.go
@@ -3,6 +3,7 @@ package configstack
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"sort"
 	"strings"
 
@@ -61,7 +62,11 @@ func (stack *Stack) Run(terragruntOptions *options.TerragruntOptions) error {
 		// We capture the out stream for each module
 		errorStreams := make([]bytes.Buffer, len(stack.Modules))
 		for n, module := range stack.Modules {
-			module.TerragruntOptions.ErrWriter = &errorStreams[n]
+			if !terragruntOptions.NonInteractive { // redirect output to ErrWriter in case of not NonInteractive mode
+				module.TerragruntOptions.ErrWriter = io.MultiWriter(&errorStreams[n], module.TerragruntOptions.ErrWriter)
+			} else {
+				module.TerragruntOptions.ErrWriter = &errorStreams[n]
+			}
 		}
 		defer stack.summarizePlanAllErrors(terragruntOptions, errorStreams)
 	}
diff --git a/docs/_docs/01_getting-started/configuration.md b/docs/_docs/01_getting-started/configuration.md
index ca0ca963..7fc1ef7c 100644
--- a/docs/_docs/01_getting-started/configuration.md
+++ b/docs/_docs/01_getting-started/configuration.md
@@ -55,7 +55,7 @@ Currently terragrunt parses the config in the following order:
 
 2.  `locals` block
 
-3.  Evaluation of values for `iam_role` and `iam_assume_role_duration` attributes, if defined
+3.  Evaluation of values for `iam_role`, `iam_assume_role_duration`, and `iam_assume_role_session_name` attributes, if defined
 
 4.  `dependencies` block
 
diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md
index 83741a2f..46e1da64 100644
--- a/docs/_docs/04_reference/cli-options.md
+++ b/docs/_docs/04_reference/cli-options.md
@@ -619,6 +619,15 @@ and Terraform with multiple AWS accounts.
 Uses the specified duration as the session duration (in seconds) for the STS session which assumes the role defined in `--terragrunt-iam-role`.
 
 
+### terragrunt-iam-assume-role-session-name
+
+**CLI Arg**: `--terragrunt-iam-assume-role-session-name`<br/>
+**Environment Variable**: `TERRAGRUNT_IAM_ASSUME_ROLE_SESSION_NAME`<br/>
+**Requires an argument**: `--terragrunt-iam-assume-role-session-name "terragrunt-iam-role-session-name"`
+
+Used as the session name for the STS session which assumes the role defined in `--terragrunt-iam-role`.
+
+
 ### terragrunt-exclude-dir
 
 **CLI Arg**: `--terragrunt-exclude-dir`<br/>
diff --git a/docs/_docs/04_reference/config-blocks-and-attributes.md b/docs/_docs/04_reference/config-blocks-and-attributes.md
index e03679b1..ae519bf2 100644
--- a/docs/_docs/04_reference/config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/config-blocks-and-attributes.md
@@ -998,6 +998,7 @@ generate = local.common.generate
 - [skip](#skip)
 - [iam_role](#iam_role)
 - [iam_assume_role_duration](#iam_assume_role_duration)
+- [iam_assume_role_session_name](#iam_assume_role_session_name)
 - [terraform_binary](#terraform_binary)
 - [terraform_version_constraint](#terraform_version_constraint)
 - [terragrunt_version_constraint](#terragrunt_version_constraint)
@@ -1146,6 +1147,14 @@ Example:
 iam_assume_role_duration = 14400
 ```
 
+### iam_assume_role_session_name
+
+The `iam_assume_role_session_name` attribute can be used to specify the STS session name, for the IAM role that Terragrunt should assume prior to invoking Terraform.
+
+The precedence is as follows: `--terragrunt-iam-assume-role-session-name` command line option → `TERRAGRUNT_IAM_ASSUME_ROLE_SESSION_NAME` env variable →
+`iam_assume_role_session_name` attribute of the `terragrunt.hcl` file in the module directory → `iam_assume_role_session_name` attribute of the included
+`terragrunt.hcl`.
+
 
 ### terraform_binary
 
diff --git a/options/options.go b/options/options.go
index ec2a82e8..f88230b2 100644
--- a/options/options.go
+++ b/options/options.go
@@ -103,12 +103,18 @@ type TerragruntOptions struct {
 	// Download Terraform configurations specified in the Source parameter into this folder
 	DownloadDir string
 
+	// ARN of an IAM Role set from command line. This is used to differentiate between the IAM role set from the config and CLI.
+	OriginalIamRole string
+
 	// The ARN of an IAM Role to assume before running Terraform
 	IamRole string
 
 	// Duration of the STS Session
 	IamAssumeRoleDuration int64
 
+	// STS Session name
+	IamAssumeRoleSessionName string
+
 	// If set to true, continue running *-all commands even if a dependency has errors. This is mostly useful for 'output-all <some_variable>'. See https://github.com/gruntwork-io/terragrunt/issues/193
 	IgnoreDependencyErrors bool
 
@@ -211,6 +217,7 @@ func NewTerragruntOptions(terragruntConfigPath string) (*TerragruntOptions, erro
 		SourceUpdate:                false,
 		DownloadDir:                 downloadDir,
 		IamAssumeRoleDuration:       DEFAULT_IAM_ASSUME_ROLE_DURATION,
+		IamAssumeRoleSessionName:    "",
 		IgnoreDependencyErrors:      false,
 		IgnoreDependencyOrder:       false,
 		IgnoreExternalDependencies:  false,
@@ -256,6 +263,8 @@ func NewTerragruntOptionsForTest(terragruntConfigPath string) (*TerragruntOption
 	}
 
 	opts.NonInteractive = true
+	opts.Logger = util.CreateLogEntry("", logrus.DebugLevel)
+	opts.LogLevel = logrus.DebugLevel
 
 	return opts, nil
 }
@@ -288,8 +297,10 @@ func (terragruntOptions *TerragruntOptions) Clone(terragruntConfigPath string) *
 		SourceUpdate:                 terragruntOptions.SourceUpdate,
 		DownloadDir:                  terragruntOptions.DownloadDir,
 		Debug:                        terragruntOptions.Debug,
+		OriginalIamRole:              terragruntOptions.OriginalIamRole,
 		IamRole:                      terragruntOptions.IamRole,
 		IamAssumeRoleDuration:        terragruntOptions.IamAssumeRoleDuration,
+		IamAssumeRoleSessionName:     terragruntOptions.IamAssumeRoleSessionName,
 		IgnoreDependencyErrors:       terragruntOptions.IgnoreDependencyErrors,
 		IgnoreDependencyOrder:        terragruntOptions.IgnoreDependencyOrder,
 		IgnoreExternalDependencies:   terragruntOptions.IgnoreExternalDependencies,



Test patch:
diff --git a/cli/args_test.go b/cli/args_test.go
index 60e57371..bbb1e8e4 100644
--- a/cli/args_test.go
+++ b/cli/args_test.go
@@ -121,6 +121,12 @@ func TestParseTerragruntOptionsFromArgs(t *testing.T) {
 			nil,
 		},
 
+		{
+			[]string{"--terragrunt-iam-assume-role-session-name", "terragrunt-iam-role-session-name"},
+			mockOptionsWithIamAssumeRoleSessionName(t, util.JoinPath(workingDir, config.DefaultTerragruntConfigPath), workingDir, []string{}, false, "", false, "terragrunt-iam-role-session-name"),
+			nil,
+		},
+
 		{
 			[]string{"--terragrunt-config", fmt.Sprintf("/some/path/%s", config.DefaultTerragruntConfigPath), "--terragrunt-non-interactive"},
 			mockOptions(t, fmt.Sprintf("/some/path/%s", config.DefaultTerragruntConfigPath), workingDir, []string{}, true, "", false, false, defaultLogLevel, false),
@@ -188,6 +194,7 @@ func assertOptionsEqual(t *testing.T, expected options.TerragruntOptions, actual
 	assert.Equal(t, expected.Source, actual.Source, msgAndArgs...)
 	assert.Equal(t, expected.IgnoreDependencyErrors, actual.IgnoreDependencyErrors, msgAndArgs...)
 	assert.Equal(t, expected.IamRole, actual.IamRole, msgAndArgs...)
+	assert.Equal(t, expected.IamRole, actual.OriginalIamRole, msgAndArgs...)
 	assert.Equal(t, expected.Debug, actual.Debug, msgAndArgs...)
 	assert.Equal(t, expected.AwsProviderPatchOverrides, actual.AwsProviderPatchOverrides, msgAndArgs...)
 }
@@ -223,6 +230,14 @@ func mockOptionsWithIamAssumeRoleDuration(t *testing.T, terragruntConfigPath str
 
 	return opts
 }
+
+func mockOptionsWithIamAssumeRoleSessionName(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, IamAssumeRoleSessionName string) *options.TerragruntOptions {
+	opts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)
+	opts.IamAssumeRoleSessionName = IamAssumeRoleSessionName
+
+	return opts
+}
+
 func mockOptionsWithOverrideAttrs(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, overrideAttrs map[string]string) *options.TerragruntOptions {
 	opts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, false, "", false, false, defaultLogLevel, false)
 	opts.AwsProviderPatchOverrides = overrideAttrs
diff --git a/config/config_as_cty_test.go b/config/config_as_cty_test.go
index 9c4b52d4..322215ce 100644
--- a/config/config_as_cty_test.go
+++ b/config/config_as_cty_test.go
@@ -191,6 +191,8 @@ func terragruntConfigStructFieldToMapKey(t *testing.T, fieldName string) (string
 		return "iam_role", true
 	case "IamAssumeRoleDuration":
 		return "iam_assume_role_duration", true
+	case "IamAssumeRoleSessionName":
+		return "iam_assume_role_session_name", true
 	case "Inputs":
 		return "inputs", true
 	case "Locals":
diff --git a/config/config_test.go b/config/config_test.go
index 29f591e7..0c3826c2 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -254,6 +254,24 @@ func TestParseIamAssumeRoleDuration(t *testing.T) {
 	assert.Equal(t, int64(36000), *terragruntConfig.IamAssumeRoleDuration)
 }
 
+func TestParseIamAssumeRoleSessionName(t *testing.T) {
+	t.Parallel()
+
+	config := `iam_assume_role_session_name = "terragrunt-iam-assume-role-session-name"`
+
+	terragruntConfig, err := ParseConfigString(config, mockOptionsForTest(t), nil, DefaultTerragruntConfigPath, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Nil(t, terragruntConfig.RemoteState)
+	assert.Nil(t, terragruntConfig.Terraform)
+	assert.Nil(t, terragruntConfig.Dependencies)
+	assert.Nil(t, terragruntConfig.RetryableErrors)
+
+	assert.Equal(t, "terragrunt-iam-assume-role-session-name", terragruntConfig.IamAssumeRoleSessionName)
+}
+
 func TestParseTerragruntConfigDependenciesOnePath(t *testing.T) {
 	t.Parallel()
 
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/component1/main.tf b/test/fixture-read-config/iam_roles_multiple_modules/component1/main.tf
new file mode 100644
index 00000000..b7db2541
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/component1/main.tf
@@ -0,0 +1 @@
+# Empty
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/component1/terragrunt.hcl b/test/fixture-read-config/iam_roles_multiple_modules/component1/terragrunt.hcl
new file mode 100644
index 00000000..ea29817a
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/component1/terragrunt.hcl
@@ -0,0 +1,5 @@
+iam_role = "arn:aws:iam::${local.aws_id_b}:role/terragrunt"
+
+locals {
+  aws_id_b = "component1"
+}
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/component2/main.tf b/test/fixture-read-config/iam_roles_multiple_modules/component2/main.tf
new file mode 100644
index 00000000..b7db2541
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/component2/main.tf
@@ -0,0 +1 @@
+# Empty
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/component2/terragrunt.hcl b/test/fixture-read-config/iam_roles_multiple_modules/component2/terragrunt.hcl
new file mode 100644
index 00000000..58fe55b9
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/component2/terragrunt.hcl
@@ -0,0 +1,5 @@
+iam_role = "arn:aws:iam::${local.aws_id_b}:role/terragrunt"
+
+locals {
+  aws_id_b = "component2"
+}
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/main.tf b/test/fixture-read-config/iam_roles_multiple_modules/main.tf
new file mode 100644
index 00000000..b7db2541
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/main.tf
@@ -0,0 +1 @@
+# Empty
diff --git a/test/fixture-read-config/iam_roles_multiple_modules/terragrunt.hcl b/test/fixture-read-config/iam_roles_multiple_modules/terragrunt.hcl
new file mode 100644
index 00000000..3d143704
--- /dev/null
+++ b/test/fixture-read-config/iam_roles_multiple_modules/terragrunt.hcl
@@ -0,0 +1,8 @@
+
+dependency "component1" {
+  config_path = "${get_parent_terragrunt_dir()}/component1"
+}
+
+dependency "component2" {
+  config_path = "${get_parent_terragrunt_dir()}/component2"
+}
diff --git a/test/fixture-relative-include-cmd/app/app.tf b/test/fixture-relative-include-cmd/app/app.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixture-relative-include-cmd/app/terragrunt.hcl b/test/fixture-relative-include-cmd/app/terragrunt.hcl
new file mode 100644
index 00000000..97991cde
--- /dev/null
+++ b/test/fixture-relative-include-cmd/app/terragrunt.hcl
@@ -0,0 +1,4 @@
+include {
+  path = find_in_parent_folders("terragrunt-test.hcl")
+}
+
diff --git a/test/fixture-relative-include-cmd/terragrunt-test.hcl b/test/fixture-relative-include-cmd/terragrunt-test.hcl
new file mode 100644
index 00000000..cf920006
--- /dev/null
+++ b/test/fixture-relative-include-cmd/terragrunt-test.hcl
@@ -0,0 +1,3 @@
+locals {
+  path = run_cmd("echo", "path_relative_to_inclue:", path_relative_to_include())
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index 78ceae65..c8c60b8d 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -119,6 +119,8 @@ const (
 	TEST_FIXTURE_LOCALS_IN_INCLUDE_CHILD_REL_PATH           = "qa/my-app"
 	TEST_FIXTURE_READ_CONFIG                                = "fixture-read-config"
 	TEST_FIXTURE_READ_IAM_ROLE                              = "fixture-read-config/iam_role_in_file"
+	TEST_FIXTURE_IAM_ROLES_MULTIPLE_MODULES                 = "fixture-read-config/iam_roles_multiple_modules"
+	TEST_FIXTURE_RELATIVE_INCLUDE_CMD                       = "fixture-relative-include-cmd"
 	TEST_FIXTURE_AWS_GET_CALLER_IDENTITY                    = "fixture-get-aws-caller-identity"
 	TEST_FIXTURE_GET_PLATFORM                               = "fixture-get-platform"
 	TEST_FIXTURE_GET_TERRAGRUNT_SOURCE_HCL                  = "fixture-get-terragrunt-source-hcl"
@@ -3649,6 +3651,42 @@ func TestReadTerragruntConfigIamRole(t *testing.T) {
 	assert.True(t, util.FileNotExists(util.JoinPath(TEST_FIXTURE_READ_IAM_ROLE, identityArn+".txt")))
 }
 
+func TestIamRolesLoadingFromDifferentModules(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_IAM_ROLES_MULTIPLE_MODULES)
+
+	// Execution outputs to be verified
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	// Invoke terragrunt and verify used IAM roles for each dependency
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt init --terragrunt-log-level debug --terragrunt-debugreset --terragrunt-working-dir %s", TEST_FIXTURE_IAM_ROLES_MULTIPLE_MODULES), &stdout, &stderr)
+
+	// Taking all outputs in one string
+	output := fmt.Sprintf("%v %v %v", string(stderr.Bytes()), string(stdout.Bytes()), err.Error())
+
+	component1 := ""
+	component2 := ""
+
+	// scan each output line and get lines for component1 and component2
+	for _, line := range strings.Split(output, "\n") {
+		if strings.Contains(line, "Assuming IAM role arn:aws:iam::component1:role/terragrunt") {
+			component1 = line
+			continue
+		}
+		if strings.Contains(line, "Assuming IAM role arn:aws:iam::component2:role/terragrunt") {
+			component2 = line
+			continue
+		}
+	}
+	assert.NotEmptyf(t, component1, "Missing role for component 1")
+	assert.NotEmptyf(t, component2, "Missing role for component 2")
+
+	assert.Contains(t, component1, "iam_roles_multiple_modules/component")
+	assert.Contains(t, component2, "iam_roles_multiple_modules/component2")
+}
+
 func TestTerragruntVersionConstraintsPartialParse(t *testing.T) {
 	fixturePath := "fixture-partial-parse/terragrunt-version-constraint"
 	cleanupTerragruntFolder(t, fixturePath)
@@ -4377,3 +4415,35 @@ func TestShowErrorWhenRunAllInvokedWithoutArguments(t *testing.T) {
 	_, ok := errors.Unwrap(err).(cli.MissingCommand)
 	assert.True(t, ok)
 }
+
+func TestPathRelativeToIncludeInvokedInCorrectPathFromChild(t *testing.T) {
+	t.Parallel()
+
+	appPath := path.Join(TEST_FIXTURE_RELATIVE_INCLUDE_CMD, "app")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt version --terragrunt-log-level trace --terragrunt-non-interactive --terragrunt-working-dir %s", appPath), &stdout, &stderr)
+	require.NoError(t, err)
+	errout := string(stderr.Bytes())
+	assert.Equal(t, 1, strings.Count(errout, "\npath_relative_to_inclue: app\n"))
+	assert.Equal(t, 0, strings.Count(errout, "\npath_relative_to_inclue: .\n"))
+}
+
+func TestTerragruntInitConfirmation(t *testing.T) {
+	t.Parallel()
+
+	s3BucketName := fmt.Sprintf("terragrunt-test-bucket-%s", strings.ToLower(uniqueId()))
+
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_OUTPUT_ALL)
+
+	rootTerragruntConfigPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_OUTPUT_ALL, config.DefaultTerragruntConfigPath)
+	copyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, "not-used", "not-used")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt run-all init --terragrunt-working-dir %s", tmpEnvPath), &stdout, &stderr)
+	require.Error(t, err)
+	errout := string(stderr.Bytes())
+	assert.Equal(t, 1, strings.Count(errout, "does not exist or you don't have permissions to access it. Would you like Terragrunt to create it? (y/n)"))
+}