環境とか。
Terraform v0.13.4 + provider registry.terraform.io/hashicorp/aws v3.9.0
Terraform でデプロイ対象の AWS アカウントが MFA 必須だったので aws-vault
を使う前提で provider aws
にはクレデンシャルの指定なし、一方で tfstate のバックエンドは MFA 無しの別の AWS アカウントの S3 バケットを利用するために profile
を指定していました。
terraform { backend "s3" { profile = "ore" region = "ap-northeast-1" bucket = "ore-no-terraform" key = "are.tfstate" } } provider "aws" { region = "ap-northeast-1" }
こんな変な構成にしていることが圧倒的に悪い気がしますが、これは期待通りにはなりません。次のように aws-vault
で実行すると tfstate のバックエンドの S3 へも are のクレデンシャルでアクセスしてしまいます。
aws-vault exec are -- terraform plan
aws-vault
が AWS STS から取得した一時的なクレデンシャルが AWS_ACCESS_KEY_ID
や AWS_SECRET_ACCESS_KEY
などの環境変数に設定されたうえで Terraform が実行されるのですが、環境変数のクレデンシャルが tf ファイルで指定している profile
よりも優先されるためです。
原因
Terraform の AWS プロバイダは hashicorp/aws-sdk-go-base で AWS に接続します。
- https://github.com/terraform-providers/terraform-provider-aws/blob/0265e891b6b30b5e7d896eae36c8c9cc5ec28820/aws/config.go#L158
- https://github.com/terraform-providers/terraform-provider-aws/blob/0265e891b6b30b5e7d896eae36c8c9cc5ec28820/aws/config.go#L413
import ( // ...snip... awsbase "github.com/hashicorp/aws-sdk-go-base" // ...snip... ) // ...snip... // Client configures and returns a fully initialized AWSClient func (c *Config) Client() (interface{}, error) { // ...snip... sess, accountID, partition, err := awsbase.GetSessionWithAccountIDAndPartition(awsbaseConfig) // ...snip... }
そして hashicorp/aws-sdk-go-base のこの辺りで profile
よりも環境変数が優先されています。
// build a chain provider, lazy-evaluated by aws-sdk
providers := []awsCredentials.Provider{
&awsCredentials.StaticProvider{Value: awsCredentials.Value{
AccessKeyID: c.AccessKey,
SecretAccessKey: c.SecretKey,
SessionToken: c.Token,
}},
&awsCredentials.EnvProvider{},
&awsCredentials.SharedCredentialsProvider{
Filename: sharedCredentialsFilename,
Profile: c.Profile,
},
}
ちなみに環境変数 AWS_PROFILE
は SharedCredentialsProvider
で profile
が指定されていないときのフォールバックになっているので、環境変数 AWS_PROFILE
よりも直接指定された profile
が優先されます。
// profile returns the AWS shared credentials profile. If empty will read // environment variable "AWS_PROFILE". If that is not set profile will // return "default". func (p *SharedCredentialsProvider) profile() string { if p.Profile == "" { p.Profile = os.Getenv("AWS_PROFILE") } if p.Profile == "" { p.Profile = "default" } return p.Profile }
整理すると、次のような優先順になっています。
provider aws
で指定したaccess_key
やsecret_key
- 環境変数
AWS_ACCESS_KEY_ID
やAWS_SECRET_ACCESS_KEY
provider aws
で指定したprofile
- 環境変数
AWS_PROFILE
AWS CLI はそうではありません。次のように実行すると ore のクレデンシャルが使用されていることがわかります。
aws-vault exec are -- aws sts --profile ore get-caller-identity --query Account
AWS SDK for Go も同じです。次のようなコードで確認できます。
package main import ( "flag" "fmt" "io/ioutil" "log" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" awsbase "github.com/hashicorp/aws-sdk-go-base" ) func printAccountIdAndAlias(sess *session.Session) { stsClient := sts.New(sess) identity, err := stsClient.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { panic(err) } iamClient := iam.New(sess) aliases, err := iamClient.ListAccountAliases(&iam.ListAccountAliasesInput{}) if err != nil && len(aliases.AccountAliases) == 0 { fmt.Println(*identity.Account) } else { fmt.Println(*identity.Account, *aliases.AccountAliases[0]) } } func main() { log.SetOutput(ioutil.Discard) profile := flag.String("profile", "default", "profile") flag.Parse() fmt.Print("aws/aws-sdk-go: ") printAccountIdAndAlias(session.Must(session.NewSessionWithOptions(session.Options{ Profile: *profile, }))) fmt.Print("hashicorp/aws-sdk-go-base: ") printAccountIdAndAlias(session.Must(awsbase.GetSession(&awsbase.Config{ Profile: *profile, }))) }
次のように実行すると違いがわかります。
aws-vault exec are -- go run main.go -profile ore
さいごに
下記の記述のよると意図されたもののようです(AWS CLI と異なっているのが意図されたものかどうかはさておき)。