Skip to content

Commit

Permalink
Do not skip metadata API check by default (#3960)
Browse files Browse the repository at this point in the history
This PR explores reverting the default `aws:skipMetadataApiCheck=false`
setting to enable the provider to be able to seamlessly authenticate
against an IMDS(v2) endpoints in the AWS environment. It appears that
doing so no longer slows down the provider startup time perceptibly. The
way I tested the speed delta was by measuring local empty preview of an
AWS s3 Bucket using AWS_PROFILE authentication with local <-> us-east-1
there is no perceptible difference.

Fixes: #1692

An integration test is added that exercises `pulumi preview` on an EC2
instance with IMDSv2 and asserts that the provider can authenticate
successfully.

Background:

- #873
- #1288
  • Loading branch information
t0yv0 committed May 22, 2024
1 parent f07ba4f commit 5a342a1
Show file tree
Hide file tree
Showing 17 changed files with 297 additions and 49 deletions.
6 changes: 2 additions & 4 deletions provider/cmd/pulumi-resource-aws/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,7 @@
},
"skipMetadataApiCheck": {
"type": "boolean",
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n",
"default": true
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n"
},
"skipRegionValidation": {
"type": "boolean",
Expand Down Expand Up @@ -157873,8 +157872,7 @@
},
"skipMetadataApiCheck": {
"type": "boolean",
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n",
"default": true
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n"
},
"skipRegionValidation": {
"type": "boolean",
Expand Down
1 change: 0 additions & 1 deletion provider/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ func TestCheckConfigFastWithCustomEndpoints(t *testing.T) {
"s3UsePathStyle": "true",
"secretKey": "*",
"skipCredentialsValidation": "true",
"skipMetadataApiCheck": "true",
"skipRegionValidation": "true",
"skipRequestingAccountId": "true",
"version": "6.5.0"
Expand Down
56 changes: 55 additions & 1 deletion provider/provider_yaml_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// Copyright 2016-2023, Pulumi Corporation. All rights reserved.
// Copyright 2016-2024, Pulumi Corporation. All rights reserved.

//go:build !go && !nodejs && !python && !dotnet
// +build !go,!nodejs,!python,!dotnet

package provider

import (
"bytes"
"context"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/aws/aws-sdk-go-v2/config"
Expand Down Expand Up @@ -315,6 +319,56 @@ func TestRegress3674(t *testing.T) {
require.NotContainsf(t, string(state), "MyTestTag", "Expected MyTestTag to be removed")
}

// Ensure that pulumi-aws can authenticate using IMDS API when Pulumi is running in a context where that is made
// available such as an EC2 instance.
func TestIMDSAuth(t *testing.T) {
var localProviderBuild string
actual := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
expected := "linux/amd64"
cwd, err := os.Getwd()
require.NoError(t, err)
if actual == expected {
currentBinary, err := filepath.Abs(filepath.Join(cwd, "..", "bin", "pulumi-resource-aws"))
require.NoError(t, err)
t.Logf("Reusing prebuilt binary from %s to test %q", currentBinary, expected)
localProviderBuild = currentBinary
} else {
t.Logf("Cross-compiling provider-resource-aws under test to %q", expected)
localProviderBuild = filepath.Join(os.TempDir(), "pulumi-resource-aws")
ldFlags := []string{
"-X", "github.com/pulumi/pulumi-aws/provider/v6/pkg/version.Version=6.0.0-alpha.0+dev",
"-X", "github.com/hashicorp/terraform-provider-aws/version.ProviderVersion=6.0.0-alpha.0+dev",
}
args := []string{
"build", "-o", localProviderBuild,
"-ldflags", strings.Join(ldFlags, " "),
}
cmd := exec.Command("go", args...)
cmd.Dir = filepath.Join(cwd, "cmd", "pulumi-resource-aws")
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("GOOS=linux"),
fmt.Sprintf("GOARCH=amd64"),
)
var stderr, stdout bytes.Buffer
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
t.Logf("go %s failed\nStdout:\n%s\nStderr:\n%s\n", strings.Join(args, " "),
stdout.String(), stderr.String())
require.NoError(t, err)
}
}
t.Run("IDMSv2", func(t *testing.T) {
ptest := pulumiTest(t, filepath.Join("test-programs", "imds-auth", "imds-v2"), opttest.SkipInstall())
ptest.SetConfig("localProviderBuild", localProviderBuild)
result := ptest.Up()
t.Logf("stdout: %s", result.StdOut)
t.Logf("stderr: %s", result.StdErr)
t.Logf("commandOut: %v", result.Outputs["commandOut"].Value)
})
}

func configureS3() *s3sdk.Client {
loadOpts := []func(*config.LoadOptions) error{}
if p, ok := os.LookupEnv("AWS_PROFILE"); ok {
Expand Down
41 changes: 22 additions & 19 deletions provider/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,22 +522,23 @@ func stringValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []st
}

// boolValue gets a bool value from a property map if present, else false
func boolValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) bool {
func boolValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) (*bool, error) {
val, ok := vars[prop]
if ok && val.IsBool() {
return val.BoolValue()
result := val.BoolValue()
return &result, nil
}
for _, env := range envs {
val, ok := os.LookupEnv(env)
if ok {
boolValue, err := strconv.ParseBool(val)
if err != nil {
return false
return nil, err
}
return boolValue
return &boolValue, nil
}
}
return false
return nil, nil
}

func arrayValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) []string {
Expand Down Expand Up @@ -641,15 +642,16 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error
config.AssumeRoleWithWebIdentity = &assumeRole
}

// By default `skipMetadataApiCheck` is true for Pulumi to speed operations
// if we want to authenticate against the AWS API Metadata Service then the user
// will specify that skipMetadataApiCheck: false
// therefore, if we have skipMetadataApiCheck false, then we are enabling the imds client
config.EC2MetadataServiceEnableState = imds.ClientDisabled
skipMetadataApiCheck := boolValue(vars, "skipMetadataApiCheck",
// Only set non-default EC2MetadataServiceEnableState if requested by skipMetadataApiCheck.
skipMetadataApiCheck, err := boolValue(vars, "skipMetadataApiCheck",
[]string{"AWS_SKIP_METADATA_API_CHECK"})
if !skipMetadataApiCheck {
config.EC2MetadataServiceEnableState = imds.ClientEnabled
contract.AssertNoErrorf(err, "Failed to parse skipMetadataApiCheck configuration")
if skipMetadataApiCheck != nil {
if !*skipMetadataApiCheck {
config.EC2MetadataServiceEnableState = imds.ClientEnabled
} else {
config.EC2MetadataServiceEnableState = imds.ClientDisabled
}
}

// lastly let's set the sharedCreds and sharedConfig file. If these are not found then let's default to the
Expand Down Expand Up @@ -751,17 +753,21 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error
// before passing control to the TF provider to ensure we can report actionable errors.
func preConfigureCallback(alreadyRun *atomic.Bool) func(vars resource.PropertyMap, c shim.ResourceConfig) error {
return func(vars resource.PropertyMap, c shim.ResourceConfig) error {
skipCredentialsValidation := boolValue(vars, "skipCredentialsValidation",
var err error
skipCredentialsValidation, err := boolValue(vars, "skipCredentialsValidation",
[]string{"AWS_SKIP_CREDENTIALS_VALIDATION"})

if err != nil {
return err
}

// if we skipCredentialsValidation then we don't need to do anything in
// preConfigureCallback as this is an explicit operation
if skipCredentialsValidation {
if skipCredentialsValidation != nil && *skipCredentialsValidation {
log.Printf("[INFO] pulumi-aws: skip credentials validation")
return nil
}

var err error
if alreadyRun.CompareAndSwap(false, true) {
log.Printf("[INFO] pulumi-aws: starting to validate credentials. " +
"Disable this by AWS_SKIP_CREDENTIALS_VALIDATION or " +
Expand Down Expand Up @@ -866,9 +872,6 @@ func ProviderFromMeta(metaInfo *tfbridge.MetadataInfo) *tfbridge.ProviderInfo {
},
"skip_metadata_api_check": {
Type: "boolean",
Default: &tfbridge.DefaultInfo{
Value: true,
},
},
"access_key": {
Secret: tfbridge.True(),
Expand Down
193 changes: 193 additions & 0 deletions provider/test-programs/imds-auth/imds-v2/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
name: imds-v2
runtime: yaml
description: Test the ability of pulumi-aws to authenticate on an EC2 instance with IMDSv2 enabled

backend:
url: file://./pulumi-state

config:
localProviderBuild:
type: string

pulumi:tags:
value:
pulumi:template: aws-yaml

variables:
ec2ami:
fn::invoke:
function: aws:ec2:getAmi
arguments:
filters:
- name: name
values: ["al2023*x86_64*"]
owners:
- amazon
mostRecent: true
return: id

instanceType: t2.medium
policyArn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" # example policy

resources:

segroup:
type: aws:ec2:SecurityGroup
properties:
ingress:
- protocol: tcp
fromPort: 80
toPort: 80
cidrBlocks: ["0.0.0.0/0"]
- protocol: tcp
fromPort: 22
toPort: 22
cidrBlocks: ["0.0.0.0/0"]
egress:
- fromPort: 0
toPort: 0
protocol: '-1'
cidrBlocks:
- 0.0.0.0/0
ipv6CidrBlocks:
- ::/0

priv-key:
type: tls:PrivateKey
properties:
algorithm: RSA
rsaBits: 2048

key-pair:
type: aws:ec2/keyPair:KeyPair
properties:
publicKey: ${priv-key.publicKeyOpenssh}

my-role:
type: aws:iam/role:Role
properties:
assumeRolePolicy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {"Service": "ec2.amazonaws.com"},
"Effect": "Allow",
"Sid": ""
}
]
}
my-role-policy-attachment:
type: aws:iam/rolePolicyAttachment:RolePolicyAttachment
properties:
role: ${my-role.name}
policyArn: ${policyArn}

my-instance-profile:
type: aws:iam/instanceProfile:InstanceProfile
properties:
role: ${my-role.name}

inst:
type: aws:ec2/instance:Instance
properties:
ami: ${ec2ami}
instanceType: ${instanceType}
iamInstanceProfile: ${my-instance-profile.name}
keyName: ${key-pair.keyName}
# Enable and enforce IMDSv2
metadataOptions:
httpTokens: required
httpEndpoint: enabled
httpPutResponseHopLimit: 1
vpcSecurityGroupIds:
- ${segroup}
userData: |
#!/bin/bash
# Reconfigure SSHD - workaround for pulumi Command issues
cat /etc/ssh/ssh_config >/tmp/sshd_config
echo "AcceptEnv PULUMI_COMMAND_STDOUT" >> /tmp/sshd_config
echo "AcceptEnv PULUMI_COMMAND_STDERR" >> /tmp/sshd_config
sudo cp /tmp/sshd_config /etc/ssh/sshd_config || echo "FAILED to set sshd_config"
rm /tmp/sshd_config
file-copy:
type: command:remote:CopyFile
properties:
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
localPath: remote-program/Pulumi.yaml
remotePath: "/tmp/Pulumi.yaml"
options:
ignoreChanges:
- connection

provider-copy:
type: command:remote:CopyFile
properties:
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
localPath: ${localProviderBuild}
remotePath: "/tmp/pulumi-resource-aws"
options:
ignoreChanges:
- connection

install-cmd:
type: command:remote:Command
properties:
create: |
echo "========"
curl -fsSL https://get.pulumi.com | sh
export PATH="/home/ec2-user/.pulumi/bin:$PATH"
echo "========"
pulumi version
echo "========"
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
options:
ignoreChanges:
- connection
dependsOn:
- ${file-copy}

init-cmd:
type: command:remote:Command
properties:
create: |
cd /tmp
mkdir ./pulumi-state
export PULUMI_CONFIG_PASSPHRASE=123456
export PATH="/tmp:$PATH"
export PATH="/home/ec2-user/.pulumi/bin:$PATH"
chmod a+x /tmp/pulumi-resource-aws
pulumi version # ensure in PATH
pulumi-resource-aws --help # ensure in PATH
pulumi stack init dev
pulumi stack select dev
pulumi config
pulumi preview
# SSH connection details to the remote machine
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
options:
ignoreChanges:
- connection
dependsOn:
- ${install-cmd}
- ${provider-copy}

outputs:
instanceId: ${inst.id}
publicIp: ${inst.publicIp}
commandOut: ${init-cmd.stdout}
Loading

0 comments on commit 5a342a1

Please sign in to comment.