diff --git a/.circleci/config.yml b/.circleci/config.yml
index 556c512e7..616cc7584 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -761,6 +761,7 @@ workflows:
only: /^v.*/
context:
- AWS__PHXDEVOPS__circle-ci-test
+ - AWS__PHXDEVOPS__terragrunt-oidc-test
- GCP__automated-tests
- GITHUB__PAT__gruntwork-ci
- APPLE__OSX__code-signing
@@ -770,6 +771,7 @@ workflows:
only: /^v.*/
context:
- AWS__PHXDEVOPS__circle-ci-test
+ - AWS__PHXDEVOPS__terragrunt-oidc-test
- GCP__automated-tests
- GITHUB__PAT__gruntwork-ci
- APPLE__OSX__code-signing
@@ -779,6 +781,7 @@ workflows:
only: /^v.*/
context:
- AWS__PHXDEVOPS__circle-ci-test
+ - AWS__PHXDEVOPS__terragrunt-oidc-test
- GCP__automated-tests
- GITHUB__PAT__gruntwork-ci
- APPLE__OSX__code-signing
diff --git a/cli/commands/terraform/creds/providers/externalcmd/provider.go b/cli/commands/terraform/creds/providers/externalcmd/provider.go
index 90f44192a..07e894de1 100644
--- a/cli/commands/terraform/creds/providers/externalcmd/provider.go
+++ b/cli/commands/terraform/creds/providers/externalcmd/provider.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds/providers"
+ "github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds/providers/amazonsts"
"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/shell"
@@ -66,10 +67,21 @@ func (provider *Provider) GetCredentials(ctx context.Context) (*providers.Creden
}
if resp.AWSCredentials != nil {
- if envs := resp.AWSCredentials.Envs(provider.terragruntOptions); envs != nil {
+ if envs := resp.AWSCredentials.Envs(ctx, provider.terragruntOptions); envs != nil {
provider.terragruntOptions.Logger.Debugf("Obtaining AWS credentials from the %s.", provider.Name())
maps.Copy(creds.Envs, envs)
}
+
+ return creds, nil
+ }
+
+ if resp.AWSRole != nil {
+ if envs := resp.AWSRole.Envs(ctx, provider.terragruntOptions); envs != nil {
+ provider.terragruntOptions.Logger.Debugf("Assuming AWS role %s using the %s.", resp.AWSRole.RoleARN, provider.Name())
+ maps.Copy(creds.Envs, envs)
+ }
+
+ return creds, nil
}
return creds, nil
@@ -77,6 +89,7 @@ func (provider *Provider) GetCredentials(ctx context.Context) (*providers.Creden
type Response struct {
AWSCredentials *AWSCredentials `json:"awsCredentials"`
+ AWSRole *AWSRole `json:"awsRole"`
Envs map[string]string `json:"envs"`
}
@@ -86,7 +99,67 @@ type AWSCredentials struct {
SessionToken string `json:"SESSION_TOKEN"`
}
-func (creds *AWSCredentials) Envs(opts *options.TerragruntOptions) map[string]string {
+type AWSRole struct {
+ RoleARN string `json:"roleARN"`
+ RoleSessionName string `json:"roleSessionName"`
+ Duration int64 `json:"duration"`
+ WebIdentityToken string `json:"webIdentityToken"`
+}
+
+func (role *AWSRole) Envs(ctx context.Context, opts *options.TerragruntOptions) map[string]string {
+ if role.RoleARN == "" {
+ opts.Logger.Warnf("The command %s completed successfully, but AWS role assumption contains empty required value: roleARN, nothing is being done.", opts.AuthProviderCmd)
+ return nil
+ }
+
+ sessionName := role.RoleSessionName
+ if sessionName == "" {
+ sessionName = options.GetDefaultIAMAssumeRoleSessionName()
+ }
+
+ duration := role.Duration
+ if duration == 0 {
+ duration = options.DefaultIAMAssumeRoleDuration
+ }
+
+ // Construct minimal TerragruntOptions for role assumption.
+ providerOpts := options.TerragruntOptions{
+ IAMRoleOptions: options.IAMRoleOptions{
+ RoleARN: role.RoleARN,
+ AssumeRoleDuration: duration,
+ AssumeRoleSessionName: sessionName,
+ },
+ Logger: opts.Logger,
+ }
+
+ if role.WebIdentityToken != "" {
+ providerOpts.IAMRoleOptions.WebIdentityToken = role.WebIdentityToken
+ }
+
+ provider := amazonsts.NewProvider(&providerOpts)
+
+ creds, err := provider.GetCredentials(ctx)
+ if err != nil {
+ opts.Logger.Warnf("Failed to assume role %s: %v", role.RoleARN, err)
+ return nil
+ }
+
+ if creds == nil {
+ opts.Logger.Warnf("The command %s completed successfully, but failed to assume role %s, nothing is being done.", opts.AuthProviderCmd, role.RoleARN)
+ return nil
+ }
+
+ envs := map[string]string{
+ "AWS_ACCESS_KEY_ID": creds.Envs["AWS_ACCESS_KEY_ID"],
+ "AWS_SECRET_ACCESS_KEY": creds.Envs["AWS_SECRET_ACCESS_KEY"],
+ "AWS_SESSION_TOKEN": creds.Envs["AWS_SESSION_TOKEN"],
+ "AWS_SECURITY_TOKEN": creds.Envs["AWS_SESSION_TOKEN"],
+ }
+
+ return envs
+}
+
+func (creds *AWSCredentials) Envs(_ context.Context, opts *options.TerragruntOptions) map[string]string {
var emptyFields []string
if creds.AccessKeyID == "" {
diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md
index 2b5a50a1a..70be71637 100644
--- a/docs/_docs/04_reference/cli-options.md
+++ b/docs/_docs/04_reference/cli-options.md
@@ -48,6 +48,7 @@ This page documents the CLI commands and options available with Terragrunt:
- [terragrunt-iam-role](#terragrunt-iam-role)
- [terragrunt-iam-assume-role-duration](#terragrunt-iam-assume-role-duration)
- [terragrunt-iam-assume-role-session-name](#terragrunt-iam-assume-role-session-name)
+ - [terragrunt-iam-web-identity-token](#terragrunt-iam-web-identity-token)
- [terragrunt-excludes-file](#terragrunt-excludes-file)
- [terragrunt-exclude-dir](#terragrunt-exclude-dir)
- [terragrunt-include-dir](#terragrunt-include-dir)
@@ -984,6 +985,14 @@ Uses the specified duration as the session duration (in seconds) for the STS ses
Used as the session name for the STS session which assumes the role defined in `--terragrunt-iam-role`.
+### terragrunt-iam-web-identity-token
+
+**CLI Arg**: `--terragrunt-iam-web-identity-token`
+**Environment Variable**: `TERRAGRUNT_IAM_WEB_IDENTITY_TOKEN`
+**Requires an argument**: `--terragrunt-iam-web-identity-token [/path/to/web-identity-token | web-identity-token-value]`
+
+Used as the web identity token for assuming a role temporarily using the AWS Security Token Service (STS) with the [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) API.
+
### terragrunt-excludes-file
**CLI Arg**: `--terragrunt-excludes-file`
@@ -1529,6 +1538,12 @@ The output must be valid JSON of the following schema:
"SECRET_ACCESS_KEY": "",
"SESSION_TOKEN": ""
},
+ "awsRole": {
+ "roleARN": "",
+ "sessionName": "",
+ "duration": 0,
+ "webIdentityToken": ""
+ },
"envs": {
"ANY_KEY": ""
}
@@ -1553,8 +1568,12 @@ Note that more specific configurations (e.g. `awsCredentials`) take precedence o
If you would like to set credentials for AWS with this method, you are encouraged to use `awsCredentials` instead of `envs`, as these keys will be validated to conform to the officially supported environment variables expected by the AWS SDK.
+Similarly, if you would like Terragrunt to assume an AWS role on your behalf, you are encouraged to use the `awsRole` configuration instead of `envs`.
+
Other credential configurations will be supported in the future, but until then, if your provider authenticates via environment variables, you can use the `envs` field to fetch credentials dynamically from a secret store, etc before Terragrunt executes any IAC.
+**Note**: The `awsRole` configuration is only used when the `awsCredentials` configuration is not present. If both are present, the `awsCredentials` configuration will take precedence.
+
### terragrunt-disable-log-formatting
**CLI Arg**: `--terragrunt-disable-log-formatting`
diff --git a/test/fixtures/assume-role-web-identity/env-var/main.tf b/test/fixtures/assume-role-web-identity/env-var/main.tf
deleted file mode 100644
index ec64be1fd..000000000
--- a/test/fixtures/assume-role-web-identity/env-var/main.tf
+++ /dev/null
@@ -1,4 +0,0 @@
-resource "local_file" "test_file" {
- content = "test_file"
- filename = "${path.module}/test_file.txt"
-}
diff --git a/test/fixtures/assume-role-web-identity/env-var/terragrunt.hcl b/test/fixtures/assume-role-web-identity/env-var/terragrunt.hcl
deleted file mode 100644
index b64cb5d5b..000000000
--- a/test/fixtures/assume-role-web-identity/env-var/terragrunt.hcl
+++ /dev/null
@@ -1,16 +0,0 @@
-iam_role = "__FILL_IN_ASSUME_ROLE__"
-iam_web_identity_token = get_env("__FILL_IN_IDENTITY_TOKEN_ENV_VAR__", "")
-
-remote_state {
- backend = "s3"
- generate = {
- path = "backend.tf"
- if_exists = "overwrite_terragrunt"
- }
- config = {
- bucket = "__FILL_IN_BUCKET_NAME__"
- key = "${path_relative_to_include()}/terraform.tfstate"
- region = "__FILL_IN_REGION__"
- encrypt = true
- }
-}
diff --git a/test/fixtures/auth-provider-cmd/oidc/main.tf b/test/fixtures/auth-provider-cmd/oidc/main.tf
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/fixtures/auth-provider-cmd/oidc/mock-auth-cmd.sh b/test/fixtures/auth-provider-cmd/oidc/mock-auth-cmd.sh
new file mode 100755
index 000000000..408b48419
--- /dev/null
+++ b/test/fixtures/auth-provider-cmd/oidc/mock-auth-cmd.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -o pipefail
+
+: "${AWS_TEST_OIDC_ROLE_ARN:?The AWS_TEST_OIDC_ROLE_ARN environment variable must be set.}"
+: "${CIRCLE_OIDC_TOKEN_V2:?The CIRCLE_OIDC_TOKEN_V2 environment variable must be set.}"
+
+jq -n \
+ --arg role "$AWS_TEST_OIDC_ROLE_ARN" \
+ --arg token "$CIRCLE_OIDC_TOKEN_V2" \
+ '{awsRole: {roleARN: $role, webIdentityToken: $token}}'
diff --git a/test/fixtures/auth-provider-cmd/oidc/terragrunt.hcl b/test/fixtures/auth-provider-cmd/oidc/terragrunt.hcl
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/integration_aws_test.go b/test/integration_aws_test.go
index 065aad92c..d50aab1bb 100644
--- a/test/integration_aws_test.go
+++ b/test/integration_aws_test.go
@@ -759,74 +759,79 @@ func TestAwsAssumeRoleDuration(t *testing.T) {
assert.Contains(t, output, "no changes are needed.")
}
-func TestAwsAssumeRoleWebIdentityEnv(t *testing.T) {
- t.Parallel()
-
- assumeRole := os.Getenv("AWS_TEST_S3_ASSUME_ROLE")
- tokenEnvVar := os.Getenv("AWS_TEST_S3_IDENTITY_TOKEN_VAR")
- if tokenEnvVar == "" {
- t.Skip("Missing required env var AWS_TEST_S3_IDENTITY_TOKEN_VAR")
- return
+func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
+ if os.Getenv("CIRCLECI") != "true" {
+ t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
}
- tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityEnv)
- helpers.CleanupTerraformFolder(t, tmpEnvPath)
- testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWebIdentityEnv)
+ // These tests need to be run without the static key + secret
+ // used by most AWS tests here.
+ t.Setenv("AWS_ACCESS_KEY_ID", "")
+ os.Unsetenv("AWS_ACCESS_KEY_ID")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
+ os.Unsetenv("AWS_SECRET_ACCESS_KEY")
- originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWebIdentityEnv, "terragrunt.hcl")
+ tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityFile)
+ cleanupTerraformFolder(t, tmpEnvPath)
+ testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWebIdentityFile)
+
+ originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWebIdentityFile, "terragrunt.hcl")
tmpTerragruntConfigFile := util.JoinPath(testPath, "terragrunt.hcl")
s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
- defer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName, options.WithIAMRoleARN(assumeRole), options.WithIAMWebIdentityToken(os.Getenv(tokenEnvVar)))
+ role := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
+ require.NotEmpty(t, role)
+ token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
+ require.NotEmpty(t, token)
+
+ tokenFile := t.TempDir() + "/oidc-token"
+ require.NoError(t, os.WriteFile(tokenFile, []byte(token), 0400))
+
+ defer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName, options.WithIAMRoleARN(role), options.WithIAMWebIdentityToken(token))
helpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{
- "__FILL_IN_BUCKET_NAME__": s3BucketName,
- "__FILL_IN_REGION__": helpers.TerraformRemoteStateS3Region,
- "__FILL_IN_ASSUME_ROLE__": assumeRole,
- "__FILL_IN_IDENTITY_TOKEN_ENV_VAR__": tokenEnvVar,
+ "__FILL_IN_BUCKET_NAME__": s3BucketName,
+ "__FILL_IN_REGION__": helpers.TerraformRemoteStateS3Region,
+ "__FILL_IN_ASSUME_ROLE__": role,
+ "__FILL_IN_IDENTITY_TOKEN_FILE_PATH__": tokenFile,
})
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
- err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+ err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir "+testPath, &stdout, &stderr)
require.NoError(t, err)
output := fmt.Sprintf("%s %s", stderr.String(), stdout.String())
assert.Contains(t, output, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
}
-func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
- t.Parallel()
-
- tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityFile)
- helpers.CleanupTerraformFolder(t, tmpEnvPath)
- testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWebIdentityFile)
-
- originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWebIdentityFile, "terragrunt.hcl")
- tmpTerragruntConfigFile := util.JoinPath(testPath, "terragrunt.hcl")
- s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
+func TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {
+ if os.Getenv("CIRCLECI") != "true" {
+ t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
+ }
- assumeRole := os.Getenv("AWS_TEST_S3_ASSUME_ROLE")
- tokenFilePath := os.Getenv("AWS_TEST_S3_IDENTITY_TOKEN_FILE_PATH")
+ // These tests need to be run without the static key + secret
+ // used by most AWS tests here.
+ t.Setenv("AWS_ACCESS_KEY_ID", "")
+ os.Unsetenv("AWS_ACCESS_KEY_ID")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
+ os.Unsetenv("AWS_SECRET_ACCESS_KEY")
- defer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName, options.WithIAMRoleARN(assumeRole), options.WithIAMWebIdentityToken(tokenFilePath))
+ tmp := t.TempDir()
- helpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{
- "__FILL_IN_BUCKET_NAME__": s3BucketName,
- "__FILL_IN_REGION__": helpers.TerraformRemoteStateS3Region,
- "__FILL_IN_ASSUME_ROLE__": assumeRole,
- "__FILL_IN_IDENTITY_TOKEN_FILE_PATH__": tokenFilePath,
- })
+ emptyTerragruntConfigPath := filepath.Join(tmp, "terragrunt.hcl")
+ require.NoError(t, os.WriteFile(emptyTerragruntConfigPath, []byte(""), 0400))
- stdout := bytes.Buffer{}
- stderr := bytes.Buffer{}
+ emptyMainTFPath := filepath.Join(tmp, "main.tf")
+ require.NoError(t, os.WriteFile(emptyMainTFPath, []byte(""), 0400))
- err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
- require.NoError(t, err)
+ roleARN := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
+ require.NotEmpty(t, roleARN)
+ token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
+ require.NotEmpty(t, token)
- output := fmt.Sprintf("%s %s", stderr.String(), stdout.String())
- assert.Contains(t, output, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
+ helpers.RunTerragrunt(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir "+tmp+" --terragrunt-iam-role "+roleARN+" --terragrunt-iam-web-identity-token "+token)
}
// Regression testing for https://github.com/gruntwork-io/terragrunt/issues/906
@@ -1096,6 +1101,21 @@ func TestAwsReadTerragruntAuthProviderCmdWithSops(t *testing.T) {
assert.Equal(t, "Welcome to SOPS! Edit this file as you please!", outputs["hello"].Value)
}
+func TestAwsReadTerragruntAuthProviderCmdWithOIDC(t *testing.T) {
+ t.Parallel()
+
+ if os.Getenv("CIRCLECI") != "true" {
+ t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
+ }
+
+ cleanupTerraformFolder(t, testFixtureAuthProviderCmd)
+ tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)
+ oidcPath := util.JoinPath(tmpEnvPath, testFixtureAuthProviderCmd, "oidc")
+ mockAuthCmd := filepath.Join(oidcPath, "mock-auth-cmd.sh")
+
+ helpers.RunTerragrunt(t, fmt.Sprintf(`terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s --terragrunt-auth-provider-cmd %s`, oidcPath, mockAuthCmd))
+}
+
func TestAwsReadTerragruntConfigIamRole(t *testing.T) {
t.Parallel()