mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Backend/S3: Custom Service Endpoint Configuration (#794)
Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
This commit is contained in:
parent
9c789368dc
commit
a1e110c679
@ -62,6 +62,7 @@ S3 BACKEND:
|
||||
* Adds support for the `retry_mode` attribute. ([#698](https://github.com/opentofu/opentofu/issues/698))
|
||||
* Adds support for the `http_proxy`, `insecure`, `use_dualstack_endpoint`, and `use_fips_endpoint` attributes. ([#694](https://github.com/opentofu/opentofu/issues/694))
|
||||
* Adds support for the `use_path_style` argument and deprecates the `force_path_style` argument. ([#783](https://github.com/opentofu/opentofu/issues/783))
|
||||
* Adds support for customizing the AWS API endpoints. ([#775](https://github.com/opentofu/opentofu/issues/775))
|
||||
|
||||
## Previous Releases
|
||||
|
||||
|
@ -66,25 +66,56 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
|
||||
Optional: true,
|
||||
Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).",
|
||||
},
|
||||
"endpoints": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"s3": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the S3 API.",
|
||||
},
|
||||
"iam": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the IAM API.",
|
||||
},
|
||||
"sts": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the STS API.",
|
||||
},
|
||||
"dynamodb": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the DynamoDB API.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dynamodb_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the DynamoDB API",
|
||||
Description: "A custom endpoint for the DynamoDB API. Use `endpoints.dynamodb` instead.",
|
||||
Deprecated: true,
|
||||
},
|
||||
"endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the S3 API",
|
||||
Description: "A custom endpoint for the S3 API. Use `endpoints.s3` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"iam_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the IAM API",
|
||||
Description: "A custom endpoint for the IAM API. Use `endpoints.iam` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"sts_endpoint": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "A custom endpoint for the STS API",
|
||||
Description: "A custom endpoint for the STS API. Use `endpoints.sts` instead",
|
||||
Deprecated: true,
|
||||
},
|
||||
"sts_region": {
|
||||
Type: cty.String,
|
||||
@ -568,6 +599,10 @@ func (b *Backend) PrepareConfig(ctx context.Context, obj cty.Value) (cty.Value,
|
||||
}
|
||||
}
|
||||
|
||||
for _, endpoint := range customEndpoints {
|
||||
endpoint.Validate(obj, &diags)
|
||||
}
|
||||
|
||||
return obj, diags
|
||||
}
|
||||
|
||||
@ -651,13 +686,13 @@ func (b *Backend) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnost
|
||||
CallerDocumentationURL: "https://opentofu.org/docs/language/settings/backends/s3",
|
||||
CallerName: "S3 Backend",
|
||||
SuppressDebugLog: logging.IsDebugOrHigher(),
|
||||
IamEndpoint: stringAttrDefaultEnvVar(obj, "iam_endpoint", "AWS_IAM_ENDPOINT"),
|
||||
IamEndpoint: customEndpoints["iam"].String(obj),
|
||||
MaxRetries: intAttrDefault(obj, "max_retries", 5),
|
||||
Profile: stringAttr(obj, "profile"),
|
||||
Region: stringAttr(obj, "region"),
|
||||
SecretKey: stringAttr(obj, "secret_key"),
|
||||
SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"),
|
||||
StsEndpoint: stringAttrDefaultEnvVar(obj, "sts_endpoint", "AWS_STS_ENDPOINT"),
|
||||
StsEndpoint: customEndpoints["sts"].String(obj),
|
||||
StsRegion: stringAttr(obj, "sts_region"),
|
||||
Token: stringAttr(obj, "token"),
|
||||
HTTPProxy: stringAttrDefaultEnvVar(obj, "http_proxy", "HTTP_PROXY", "HTTPS_PROXY"),
|
||||
@ -779,7 +814,7 @@ func verifyAllowedAccountID(ctx context.Context, awsConfig aws.Config, cfg *awsb
|
||||
|
||||
func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) {
|
||||
return func(options *dynamodb.Options) {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(obj, "dynamodb_endpoint", "AWS_DYNAMODB_ENDPOINT", "AWS_ENDPOINT_URL_DYNAMODB"); ok {
|
||||
if v, ok := customEndpoints["dynamodb"].StringOk(obj); ok {
|
||||
options.BaseEndpoint = aws.String(v)
|
||||
}
|
||||
}
|
||||
@ -787,7 +822,7 @@ func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) {
|
||||
|
||||
func getS3Config(obj cty.Value) func(options *s3.Options) {
|
||||
return func(options *s3.Options) {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(obj, "endpoint", "AWS_S3_ENDPOINT", "AWS_ENDPOINT_URL_S3"); ok {
|
||||
if v, ok := customEndpoints["s3"].StringOk(obj); ok {
|
||||
options.BaseEndpoint = aws.String(v)
|
||||
}
|
||||
if v, ok := boolAttrOk(obj, "force_path_style"); ok {
|
||||
@ -1021,6 +1056,15 @@ func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) {
|
||||
return stringMapValueOk(obj.GetAttr(name))
|
||||
}
|
||||
|
||||
func customEndpointAttrDefaultEnvVarOk(obj cty.Value, endpointsKey, deprecatedKey string, envvars ...string) (string, bool) {
|
||||
if val := obj.GetAttr("endpoints"); !val.IsNull() {
|
||||
if v, ok := stringAttrDefaultEnvVarOk(val, endpointsKey, envvars...); ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return stringAttrDefaultEnvVarOk(obj, deprecatedKey, envvars...)
|
||||
}
|
||||
|
||||
func pathString(path cty.Path) string {
|
||||
var buf strings.Builder
|
||||
for i, step := range path {
|
||||
@ -1098,3 +1142,78 @@ const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the envir
|
||||
The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS)
|
||||
while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C).
|
||||
Please choose one or the other.`
|
||||
|
||||
type customEndpoint struct {
|
||||
Paths []cty.Path
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (e customEndpoint) Validate(obj cty.Value, diags *tfdiags.Diagnostics) {
|
||||
validateAttributesConflict(e.Paths...)(obj, cty.Path{}, diags)
|
||||
}
|
||||
|
||||
func (e customEndpoint) String(obj cty.Value) string {
|
||||
v, _ := e.StringOk(obj)
|
||||
return v
|
||||
}
|
||||
|
||||
func (e customEndpoint) StringOk(obj cty.Value) (string, bool) {
|
||||
for _, path := range e.Paths {
|
||||
val, err := path.Apply(obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := stringValueOk(val); ok {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
for _, envVar := range e.EnvVars {
|
||||
if v := os.Getenv(envVar); v != "" {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
var customEndpoints = map[string]customEndpoint{
|
||||
"s3": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("s3"),
|
||||
cty.GetAttrPath("endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_S3",
|
||||
"AWS_S3_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"iam": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("iam"),
|
||||
cty.GetAttrPath("iam_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_IAM",
|
||||
"AWS_IAM_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"sts": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("sts"),
|
||||
cty.GetAttrPath("sts_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_STS",
|
||||
"AWS_STS_ENDPOINT",
|
||||
},
|
||||
},
|
||||
"dynamodb": {
|
||||
Paths: []cty.Path{
|
||||
cty.GetAttrPath("endpoints").GetAttr("dynamodb"),
|
||||
cty.GetAttrPath("dynamodb_endpoint"),
|
||||
},
|
||||
EnvVars: []string{
|
||||
"AWS_ENDPOINT_URL_DYNAMODB",
|
||||
"AWS_DYNAMODB_ENDPOINT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -738,6 +738,54 @@ func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
|
||||
}),
|
||||
expectedErr: `Invalid retry mode: Valid values are "standard" and "adaptive".`,
|
||||
},
|
||||
"s3 endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"s3": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.s3, endpoint can be set.`,
|
||||
},
|
||||
"iam endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"iam_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"iam": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.iam, iam_endpoint can be set.`,
|
||||
},
|
||||
"sts endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"sts_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"sts": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.sts, sts_endpoint can be set.`,
|
||||
},
|
||||
"dynamodb endpoint conflict": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"bucket": cty.StringVal("test"),
|
||||
"key": cty.StringVal("test"),
|
||||
"region": cty.StringVal("us-west-2"),
|
||||
"dynamodb_endpoint": cty.StringVal("x1"),
|
||||
"endpoints": cty.ObjectVal(map[string]cty.Value{
|
||||
"dynamodb": cty.StringVal("x2"),
|
||||
}),
|
||||
}),
|
||||
expectedErr: `Invalid Attribute Combination: Only one of endpoints.dynamodb, dynamodb_endpoint can be set.`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
|
@ -154,10 +154,27 @@ func aliasIdFromARNResource(s string) string {
|
||||
type objectValidator func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics)
|
||||
|
||||
func validateAttributesConflict(paths ...cty.Path) objectValidator {
|
||||
applyPath := func(obj cty.Value, path cty.Path) (cty.Value, error) {
|
||||
if len(path) == 0 {
|
||||
return cty.NilVal, nil
|
||||
}
|
||||
for _, step := range path {
|
||||
val, err := step.Apply(obj)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
if val.IsNull() {
|
||||
return cty.NilVal, nil
|
||||
}
|
||||
obj = val
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) {
|
||||
found := false
|
||||
for _, path := range paths {
|
||||
val, err := path.Apply(obj)
|
||||
val, err := applyPath(obj, path)
|
||||
if err != nil {
|
||||
*diags = diags.Append(attributeErrDiag(
|
||||
"Invalid Path for Schema",
|
||||
|
@ -153,6 +153,7 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
objValues: map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr2": cty.StringVal("value2"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
@ -160,14 +161,43 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
name: "No Conflict",
|
||||
paths: []cty.Path{
|
||||
{cty.GetAttrStep{Name: "attr1"}},
|
||||
{cty.GetAttrStep{Name: "attr3"}},
|
||||
{cty.GetAttrStep{Name: "attr2"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr2": cty.NilVal,
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Nested: Conflict Found",
|
||||
paths: []cty.Path{
|
||||
(cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"),
|
||||
{cty.GetAttrStep{Name: "attr2"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"nested": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr1": cty.StringVal("value1"),
|
||||
}),
|
||||
"attr2": cty.StringVal("value2"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Nested: No Conflict",
|
||||
paths: []cty.Path{
|
||||
(cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"),
|
||||
{cty.GetAttrStep{Name: "attr3"}},
|
||||
},
|
||||
objValues: map[string]cty.Value{
|
||||
"nested": cty.NilVal,
|
||||
"attr1": cty.StringVal("value1"),
|
||||
"attr3": cty.StringVal("value3"),
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -182,7 +212,11 @@ func Test_validateAttributesConflict(t *testing.T) {
|
||||
|
||||
if test.expectErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("Expected validation errors, but got none.")
|
||||
t.Error("Expected validation errors, but got none.")
|
||||
}
|
||||
} else {
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("Expected no errors, but got %s.", diags.Err())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -159,7 +159,7 @@ The following configuration is optional:
|
||||
|
||||
* `access_key` - (Optional) AWS access key. If configured, must also configure `secret_key`. This can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`).
|
||||
* `secret_key` - (Optional) AWS access key. If configured, must also configure `access_key`. This can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`).
|
||||
* `iam_endpoint` - (Optional) Custom endpoint for the AWS Identity and Access Management (IAM) API. This can also be sourced from the `AWS_IAM_ENDPOINT` environment variable.
|
||||
* `iam_endpoint` - (Optional) **Deprecated** Custom endpoint for the AWS Identity and Access Management (IAM) API. This can also be sourced from the `AWS_IAM_ENDPOINT` environment variable.
|
||||
* `max_retries` - (Optional) The maximum number of times an AWS API request is retried on retryable failure. Defaults to 5.
|
||||
* `retry_mode` - (Optional) Specifies how retries are attempted. Valid values are `standard` and `adaptive`. This can also be sourced from the `AWS_RETRY_MODE` environment variable.
|
||||
* `profile` - (Optional) Name of AWS profile in AWS shared credentials file (e.g. `~/.aws/credentials`) or AWS shared configuration file (e.g. `~/.aws/config`) to use for credentials and/or configuration. This can also be sourced from the `AWS_PROFILE` environment variable.
|
||||
@ -169,7 +169,7 @@ The following configuration is optional:
|
||||
* `skip_credentials_validation` - (Optional) Skip credentials validation via the STS API.
|
||||
* `skip_region_validation` - (Optional) Skip validation of provided region name.
|
||||
* `skip_metadata_api_check` - (Optional) Skip usage of EC2 Metadata API.
|
||||
* `sts_endpoint` - (Optional) Custom endpoint for the AWS Security Token Service (STS) API. This can also be sourced from the `AWS_STS_ENDPOINT` environment variable.
|
||||
* `sts_endpoint` - (Optional) **Deprecated** Custom endpoint for the AWS Security Token Service (STS) API. This can also be sourced from the `AWS_STS_ENDPOINT` environment variable.
|
||||
* `sts_region` - (Optional) AWS region for STS. If unset, AWS will use the same region for STS as other non-STS operations.
|
||||
* `token` - (Optional) Multi-Factor Authentication (MFA) token. This can also be sourced from the `AWS_SESSION_TOKEN` environment variable.
|
||||
* `allowed_account_ids` (Optional): A list of permitted AWS account IDs to safeguard against accidental disruption of a live environment. This option conflicts with `forbidden_account_ids`.
|
||||
@ -183,6 +183,26 @@ The following configuration is optional:
|
||||
* `use_dualstack_endpoint` - (Optional) Resolve an endpoint with DualStack capability.
|
||||
* `use_fips_endpoint` - (Optional) Resolve an endpoint with FIPS capability.
|
||||
|
||||
#### Customizing AWS API Endpoints
|
||||
|
||||
The optional `endpoints` argument contains the following options:
|
||||
|
||||
* `s3` - (Optional) Use this to set a custom endpoint URL for the AWS S3 API. This can also be sourced from the `AWS_ENDPOINT_URL_S3` environment variable or the deprecated environment variable `AWS_S3_ENDPOINT`.
|
||||
* `iam` - (Optional) Use this to set a custom endpoint URL for the AWS IAM API. This can also be sourced from the `AWS_ENDPOINT_URL_IAM` environment variable or the deprecated environment variable `AWS_IAM_ENDPOINT`.
|
||||
* `sts` - (Optional) Use this to set a custom endpoint URL for the AWS STS API. This can also be sourced from the `AWS_ENDPOINT_URL_STS` environment variable or the deprecated environment variable `AWS_STS_ENDPOINT`.
|
||||
* `dynamodb` - (Optional) Use this to set a custom endpoint URL for the AWS DynamoDB API. This can also be sourced from the `AWS_ENDPOINT_URL_DYNAMODB` environment variable or the deprecated environment variable `AWS_DYNAMODB_ENDPOINT`.
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
endpoints {
|
||||
dynamodb = "http://localhost:4569"
|
||||
s3 = "http://localhost:4572"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Assume Role Configuration
|
||||
|
||||
Assuming an IAM Role is optional and can be configured in two ways.
|
||||
@ -314,7 +334,7 @@ The following configuration is optional:
|
||||
|
||||
* `acl` - (Optional) [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl) to be applied to the state file.
|
||||
* `encrypt` - (Optional) Enable [server side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html) of the state file.
|
||||
* `endpoint` - (Optional) Custom endpoint for the AWS S3 API. This can also be sourced from the `AWS_S3_ENDPOINT` environment variable.
|
||||
* `endpoint` - (Optional) **Deprecated** Custom endpoint for the AWS S3 API. This can also be sourced from the `AWS_S3_ENDPOINT` environment variable.
|
||||
* `force_path_style` - (Optional) **Deprecated** Enable path-style S3 URLs (`https://<HOST>/<BUCKET>` instead of `https://<BUCKET>.<HOST>`). Use `use_path_style` instead.
|
||||
* `use_path_style` - (Optional) Enable path-style S3 URLs (`https://<HOST>/<BUCKET>` instead of `https://<BUCKET>.<HOST>`).
|
||||
* `kms_key_id` - (Optional) Amazon Resource Name (ARN) of a Key Management Service (KMS) Key to use for encrypting the state. Note that if this value is specified, OpenTofu will need `kms:Encrypt`, `kms:Decrypt` and `kms:GenerateDataKey` permissions on this KMS key.
|
||||
@ -325,7 +345,7 @@ The following configuration is optional:
|
||||
|
||||
The following configuration is optional:
|
||||
|
||||
* `dynamodb_endpoint` - (Optional) Custom endpoint for the AWS DynamoDB API. This can also be sourced from the `AWS_DYNAMODB_ENDPOINT` environment variable.
|
||||
* `dynamodb_endpoint` - (Optional) **Deprecated** Custom endpoint for the AWS DynamoDB API. This can also be sourced from the `AWS_DYNAMODB_ENDPOINT` environment variable.
|
||||
* `dynamodb_table` - (Optional) Name of DynamoDB Table to use for state locking and consistency. The table must have a partition key named `LockID` with type of `String`. If not configured, state locking will be disabled.
|
||||
|
||||
## Multi-account AWS Architecture
|
||||
|
Loading…
Reference in New Issue
Block a user