CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-pulumi-pulumi-aws-sdk-v7

A Pulumi provider SDK for creating and managing Amazon Web Services (AWS) cloud resources in Go, providing strongly-typed resource classes and data sources for all major AWS services.

Overview
Eval results
Files

edge-cases.mddocs/examples/

Edge Cases and Troubleshooting

Advanced scenarios, common pitfalls, and solutions for the Pulumi AWS Provider.

Common Pitfalls

Pitfall 1: Circular Dependencies

Problem: Two resources trying to reference each other create a circular dependency.

Wrong Approach:

// ❌ This creates a circular dependency
role, err := iam.NewRole(ctx, "role", &iam.RoleArgs{
    AssumeRolePolicy: pulumi.String(assumePolicy),
    ManagedPolicyArns: pulumi.StringArray{
        policy.Arn,  // Can't reference policy that needs this role
    },
})

Correct Approach:

// ✅ Break the cycle with separate attachment
role, err := iam.NewRole(ctx, "role", &iam.RoleArgs{
    AssumeRolePolicy: pulumi.String(assumePolicy),
})
if err != nil {
    return err
}

policy, err := iam.NewPolicy(ctx, "policy", &iam.PolicyArgs{
    Policy: pulumi.String(policyDocument),
})
if err != nil {
    return err
}

_, err = iam.NewRolePolicyAttachment(ctx, "attachment", &iam.RolePolicyAttachmentArgs{
    Role:      role.Name,
    PolicyArn: policy.Arn,
})
if err != nil {
    return err
}

Pitfall 2: Resource Naming Collisions

Problem: Hardcoded resource names can conflict globally (S3) or regionally (most services).

Wrong Approach:

// ❌ Hardcoded name might already exist globally
bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
    Bucket: pulumi.String("my-app-bucket"),  // Might conflict
})

Correct Approaches:

// ✅ Option 1: Use BucketPrefix to generate unique name
bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
    BucketPrefix: pulumi.String("my-app-"),  // Creates "my-app-<random>"
})

// ✅ Option 2: Include stack/account for uniqueness
bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
    Bucket: pulumi.Sprintf("%s-%s-bucket", ctx.Project(), ctx.Stack()),
})

// ✅ Option 3: Let Pulumi generate the name
bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
    // Omit Bucket field - Pulumi generates unique name
})

Pitfall 3: Missing Required Dependencies

Problem: Some resources require others to be fully configured before creation.

Wrong Approach:

// ❌ EKS cluster might be created before IAM policy is attached
role, err := iam.NewRole(ctx, "eks-role", &iam.RoleArgs{
    AssumeRolePolicy: pulumi.String(assumeRolePolicy),
})

_, err = iam.NewRolePolicyAttachment(ctx, "eks-policy", &iam.RolePolicyAttachmentArgs{
    Role:      role.Name,
    PolicyArn: pulumi.String("arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"),
})

cluster, err := eks.NewCluster(ctx, "cluster", &eks.ClusterArgs{
    RoleArn: role.Arn,
    // ...
})  // Might fail - policy not attached yet

Correct Approach:

// ✅ Explicit dependency ensures policy is attached first
role, err := iam.NewRole(ctx, "eks-role", &iam.RoleArgs{
    AssumeRolePolicy: pulumi.String(assumeRolePolicy),
})
if err != nil {
    return err
}

attachment, err := iam.NewRolePolicyAttachment(ctx, "eks-policy", &iam.RolePolicyAttachmentArgs{
    Role:      role.Name,
    PolicyArn: pulumi.String("arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"),
})
if err != nil {
    return err
}

cluster, err := eks.NewCluster(ctx, "cluster", &eks.ClusterArgs{
    RoleArn: role.Arn,
    VpcConfig: &eks.ClusterVpcConfigArgs{
        SubnetIds: pulumi.StringArray{subnet1.ID(), subnet2.ID()},
    },
}, pulumi.DependsOn([]pulumi.Resource{attachment}))  // Wait for policy

Pitfall 4: Tags on Sub-Resources

Problem: Not understanding tag inheritance between parent and child resources.

Solution: Some sub-resources inherit parent tags, others need explicit tags.

// Bucket-level tags
bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
    Tags: pulumi.StringMap{
        "Environment": pulumi.String("prod"),
    },
})

// Objects need their own tags (don't inherit from bucket)
_, err = s3.NewBucketObjectv2(ctx, "object", &s3.BucketObjectv2Args{
    Bucket: bucket.ID(),
    Key:    pulumi.String("data.json"),
    Source: pulumi.NewFileAsset("data.json"),
    Tags: pulumi.StringMap{
        "ContentType": pulumi.String("configuration"),
    },  // Max 10 tags for S3 objects
})

Pitfall 5: ForceDestroy Flag Misuse

Problem: Forgetting ForceDestroy prevents deletion of non-empty buckets.

Solution: Use ForceDestroy carefully based on environment.

// ✅ Development/test: Enable force destroy
devBucket, err := s3.NewBucketV2(ctx, "dev-bucket", &s3.BucketV2Args{
    Bucket:       pulumi.String("dev-data-bucket"),
    ForceDestroy: pulumi.Bool(true),  // Allows deletion even if contains objects
})

// ✅ Production: Omit ForceDestroy to prevent accidental data loss
prodBucket, err := s3.NewBucketV2(ctx, "prod-bucket", &s3.BucketV2Args{
    Bucket: pulumi.String("prod-data-bucket"),
    // ForceDestroy defaults to false - safer for production
})

// Add lifecycle protection
_, err = s3.NewBucketVersioning(ctx, "versioning", &s3.BucketVersioningArgs{
    Bucket: prodBucket.ID(),
    VersioningConfiguration: &s3.BucketVersioningVersioningConfigurationArgs{
        Status: pulumi.String("Enabled"),
    },
})

Edge Cases

Edge Case 1: Eventually Consistent Operations

Scenario: IAM role creation is eventually consistent - resources using the role might fail initially.

Solution: Use explicit dependencies and let Pulumi's retry logic handle it.

// IAM role creation is eventually consistent
role, err := iam.NewRole(ctx, "lambda-role", &iam.RoleArgs{
    AssumeRolePolicy: pulumi.String(assumeRolePolicy),
})
if err != nil {
    return err
}

// Explicit dependency + Pulumi's automatic retry handles eventual consistency
fn, err := lambda.NewFunction(ctx, "function", &lambda.FunctionArgs{
    Role:    role.Arn,
    Runtime: pulumi.String("python3.12"),
    Handler: pulumi.String("index.handler"),
    Code:    pulumi.NewFileArchive("function.zip"),
}, pulumi.DependsOn([]pulumi.Resource{role}))
// Pulumi will retry if role not yet available

Edge Case 2: Cross-Region Resource Dependencies

Scenario: CloudFront requires ACM certificates in us-east-1, regardless of your default region.

Solution: Create separate provider for us-east-1.

// Default region
defaultProvider, err := aws.NewProvider(ctx, "default", &aws.ProviderArgs{
    Region: pulumi.StringPtr("eu-west-1"),
})

// CloudFront certificates MUST be in us-east-1
usEast1Provider, err := aws.NewProvider(ctx, "us-east-1", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-east-1"),
})
if err != nil {
    return err
}

// Certificate in us-east-1 (required for CloudFront)
cert, err := acm.NewCertificate(ctx, "cdn-cert", &acm.CertificateArgs{
    DomainName:       pulumi.String("example.com"),
    ValidationMethod: pulumi.String("DNS"),
}, pulumi.Provider(usEast1Provider))
if err != nil {
    return err
}

// CloudFront distribution can be in default region
distribution, err := cloudfront.NewDistribution(ctx, "cdn", &cloudfront.DistributionArgs{
    ViewerCertificate: &cloudfront.DistributionViewerCertificateArgs{
        AcmCertificateArn: cert.Arn,
    },
    // ...other config
}, pulumi.Provider(defaultProvider))

Edge Case 3: Resource Name Immutability

Scenario: Many AWS resources cannot be renamed without replacement.

Solution: Use Pulumi aliases to rename the Pulumi resource without replacing the AWS resource.

// Original code
bucket, err := s3.NewBucketV2(ctx, "old-bucket-name", &s3.BucketV2Args{
    Bucket: pulumi.String("my-actual-bucket"),
})

// To rename Pulumi resource without replacing AWS bucket
bucket, err := s3.NewBucketV2(ctx, "new-bucket-name", &s3.BucketV2Args{
    Bucket: pulumi.String("my-actual-bucket"),  // AWS name stays same
}, pulumi.Aliases([]pulumi.Alias{
    {Name: pulumi.StringInput(pulumi.String("old-bucket-name"))},
}))

Edge Case 4: VPC CIDR Modification

Scenario: Cannot change VPC primary CIDR block after creation.

Solution: Add secondary CIDR blocks instead.

// Original VPC with primary CIDR
vpc, err := ec2.NewVpc(ctx, "vpc", &ec2.VpcArgs{
    CidrBlock: pulumi.String("10.0.0.0/16"),  // Cannot change this
})

// Add secondary CIDR block
secondaryCidr, err := ec2.NewVpcIpv4CidrBlockAssociation(ctx, "secondary", &ec2.VpcIpv4CidrBlockAssociationArgs{
    VpcId:     vpc.ID(),
    CidrBlock: pulumi.String("10.1.0.0/16"),  // Additional address space
})
if err != nil {
    return err
}

// Create subnet in secondary CIDR
subnet, err := ec2.NewSubnet(ctx, "extra-subnet", &ec2.SubnetArgs{
    VpcId:     vpc.ID(),
    CidrBlock: pulumi.String("10.1.1.0/24"),
})

Edge Case 5: Security Group Self-References

Scenario: Security groups need to allow traffic from members of the same security group.

Solution: Create security group first, then add self-referencing rule.

// Create security group
clusterSg, err := ec2.NewSecurityGroup(ctx, "cluster-sg", &ec2.SecurityGroupArgs{
    Name:        pulumi.String("cluster-sg"),
    Description: pulumi.String("Cluster internal communication"),
    VpcId:       vpc.ID(),
})
if err != nil {
    return err
}

// Add self-referencing rule
_, err = ec2.NewSecurityGroupRule(ctx, "cluster-self-ingress", &ec2.SecurityGroupRuleArgs{
    Type:            pulumi.String("ingress"),
    SecurityGroupId: clusterSg.ID(),
    Protocol:        pulumi.String("-1"),
    FromPort:        pulumi.Int(0),
    ToPort:          pulumi.Int(0),
    Self:            pulumi.Bool(true),  // Allow traffic from same SG
    Description:     pulumi.String("Allow all traffic within cluster"),
})

Edge Case 6: NAT Gateway Dependency on Internet Gateway

Scenario: NAT Gateway requires Internet Gateway to exist, even though not directly connected.

Solution: Add explicit dependency on IGW.

igw, err := ec2.NewInternetGateway(ctx, "igw", &ec2.InternetGatewayArgs{
    VpcId: vpc.ID(),
})
if err != nil {
    return err
}

// ✅ Explicit dependency on IGW
eip, err := ec2.NewEip(ctx, "nat-eip", &ec2.EipArgs{
    Domain: pulumi.String("vpc"),
}, pulumi.DependsOn([]pulumi.Resource{igw}))
if err != nil {
    return err
}

natGw, err := ec2.NewNatGateway(ctx, "nat", &ec2.NatGatewayArgs{
    AllocationId: eip.AllocationId,
    SubnetId:     publicSubnet.ID(),
}, pulumi.DependsOn([]pulumi.Resource{igw}))

Edge Case 7: VPC Endpoint DNS Requirements

Scenario: Interface endpoints require specific VPC DNS settings.

Solution: Enable both DNS hostname and DNS support on VPC.

// ✅ Enable required DNS settings
vpc, err := ec2.NewVpc(ctx, "vpc", &ec2.VpcArgs{
    CidrBlock:          pulumi.String("10.0.0.0/16"),
    EnableDnsHostnames: pulumi.Bool(true),  // Required for private DNS
    EnableDnsSupport:   pulumi.Bool(true),  // Required for private DNS
})
if err != nil {
    return err
}

// Now interface endpoint can use private DNS
endpoint, err := ec2.NewVpcEndpoint(ctx, "s3-endpoint", &ec2.VpcEndpointArgs{
    VpcId:             vpc.ID(),
    ServiceName:       pulumi.String("com.amazonaws.us-east-1.s3"),
    VpcEndpointType:   pulumi.String("Interface"),
    PrivateDnsEnabled: pulumi.Bool(true),  // Requires DNS settings above
    SubnetIds:         pulumi.StringArray{subnet.ID()},
})

Edge Case 8: Default Security Group Management

Scenario: AWS automatically creates a default security group - you can't delete it but can manage it.

Solution: Use DefaultSecurityGroup resource to lock it down.

// Lock down the default security group
defaultSg, err := ec2.NewDefaultSecurityGroup(ctx, "default-sg", &ec2.DefaultSecurityGroupArgs{
    VpcId: vpc.ID(),
    // Remove all default rules
    Ingress: ec2.DefaultSecurityGroupIngressArray{},
    Egress:  ec2.DefaultSecurityGroupEgressArray{},
    Tags: pulumi.StringMap{
        "Name": pulumi.String("default-locked-down"),
    },
})
if err != nil {
    return err
}

Edge Case 9: S3 Bucket Policy Overwriting

Scenario: Multiple BucketPolicy resources on same bucket overwrite each other silently.

Solution: Only create ONE BucketPolicy per bucket with all statements.

// ❌ WRONG: Second policy overwrites the first silently
policy1, err := s3.NewBucketPolicy(ctx, "policy1", &s3.BucketPolicyArgs{
    Bucket: bucket.ID(),
    Policy: pulumi.String(policy1JSON),
})

policy2, err := s3.NewBucketPolicy(ctx, "policy2", &s3.BucketPolicyArgs{
    Bucket: bucket.ID(),  // Overwrites policy1!
    Policy: pulumi.String(policy2JSON),
})

// ✅ CORRECT: Single policy with multiple statements
combinedPolicy, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
    Statements: []iam.GetPolicyDocumentStatement{
        {/* Statement 1 */},
        {/* Statement 2 */},
    },
})

policy, err := s3.NewBucketPolicy(ctx, "policy", &s3.BucketPolicyArgs{
    Bucket: bucket.ID(),
    Policy: pulumi.String(combinedPolicy.Json),
})

Edge Case 10: Rate Limiting with Bulk Operations

Scenario: Creating many resources quickly can hit AWS API rate limits.

Solution: Pulumi handles retries automatically, but batch operations strategically.

// Pulumi automatically handles rate limiting with retries
// For very large operations, consider batching by service

// ✅ Create resources - Pulumi handles throttling
for i := 0; i < 100; i++ {
    _, err := s3.NewBucketObjectv2(ctx, fmt.Sprintf("object-%d", i), &s3.BucketObjectv2Args{
        Bucket:  bucket.ID(),
        Key:     pulumi.Sprintf("objects/%d.json", i),
        Content: pulumi.Sprintf(`{"index": %d}`, i),
    })
    if err != nil {
        return err
    }
    // Pulumi's engine manages parallelism and retries
}

Troubleshooting

Issue 1: "No valid credential sources"

Error:

error configuring Pulumi AWS Provider: no valid credential sources found

Diagnosis: AWS credentials not configured.

Solutions:

# Solution A: Environment variables
export AWS_ACCESS_KEY_ID=your_access_key
export AWS_SECRET_ACCESS_KEY=your_secret_key
export AWS_REGION=us-east-1

# Solution B: AWS profile
aws configure --profile myprofile
export AWS_PROFILE=myprofile

# Solution C: Provider configuration
provider, err := aws.NewProvider(ctx, "aws", &aws.ProviderArgs{
    Region:  pulumi.StringPtr("us-east-1"),
    Profile: pulumi.StringPtr("myprofile"),
})

Issue 2: "Access Denied" When Assuming Role

Error:

operation error STS: AssumeRole, https response error StatusCode: 403

Diagnosis: Trust policy on target role doesn't allow assumption.

Solution: Update role's trust policy.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::SOURCE_ACCOUNT:root"
    },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": {
        "sts:ExternalId": "your-external-id"
      }
    }
  }]
}
// Use matching ExternalId in provider
provider, err := aws.NewProvider(ctx, "assumed", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-east-1"),
    AssumeRoles: aws.ProviderAssumeRoleArray{
        &aws.ProviderAssumeRoleArgs{
            RoleArn:    pulumi.StringPtr("arn:aws:iam::123456789012:role/MyRole"),
            ExternalId: pulumi.StringPtr("your-external-id"),  // Must match trust policy
        },
    },
})

Issue 3: "DependencyViolation" on VPC Deletion

Error:

DependencyViolation: The vpc 'vpc-xxxxx' has dependencies and cannot be deleted

Diagnosis: Resources still exist in VPC.

Solution: Ensure proper dependency tracking and deletion order.

// Pulumi automatically tracks dependencies when using Output references
// If manual deletion needed, delete in this order:
// 1. Instances, Fargate tasks
// 2. NAT Gateways
// 3. EIPs (if not associated)
// 4. Load Balancers, Network Interfaces
// 5. VPC Endpoints
// 6. Security Groups
// 7. Subnets
// 8. Route Tables
// 9. Internet Gateway
// 10. VPC

// This happens automatically with: pulumi destroy

Issue 4: "InvalidSubnetID.NotFound"

Error:

InvalidSubnetID.NotFound: The subnet ID 'subnet-xxxxx' does not exist

Diagnosis: Subnet not created yet or wrong subnet ID.

Solution: Use Output references to ensure subnet exists.

// ✅ CORRECT: Output reference creates implicit dependency
subnet, err := ec2.NewSubnet(ctx, "subnet", &ec2.SubnetArgs{
    VpcId:     vpc.ID(),
    CidrBlock: pulumi.String("10.0.1.0/24"),
})
if err != nil {
    return err
}

instance, err := ec2.NewInstance(ctx, "instance", &ec2.InstanceArgs{
    SubnetId: subnet.ID(),  // Implicit dependency - subnet created first
    // ...
})

Issue 5: "InvalidGroup.NotFound"

Error:

InvalidGroup.NotFound: The security group 'sg-xxxxx' does not exist

Diagnosis: Security group not created yet.

Solution: Use Output references.

// ✅ CORRECT: SG created before instance
sg, err := ec2.NewSecurityGroup(ctx, "sg", &ec2.SecurityGroupArgs{
    VpcId: vpc.ID(),
})
if err != nil {
    return err
}

instance, err := ec2.NewInstance(ctx, "instance", &ec2.InstanceArgs{
    VpcSecurityGroupIds: pulumi.StringArray{sg.ID()},  // Implicit dependency
    // ...
})

Issue 6: "ResourceAlreadyExists"

Error:

ResourceAlreadyExists: Resource already exists with name 'my-bucket'

Diagnosis: Resource already exists in AWS.

Solution: Import existing resource.

// Import existing bucket
bucket, err := s3.GetBucketV2(ctx, "existing-bucket",
    pulumi.ID("existing-bucket-name"),
    &s3.BucketV2State{},
    pulumi.Import(pulumi.ID("existing-bucket-name")),
)
if err != nil {
    return err
}

// Now managed by Pulumi

Issue 7: NAT Gateway Creation Timeout

Error:

Error: timeout while waiting for state to become 'available'

Diagnosis: NAT Gateway can take 3-5 minutes to become available.

Solution: Be patient - this is normal. Increase timeout if needed.

natGw, err := ec2.NewNatGateway(ctx, "nat", &ec2.NatGatewayArgs{
    AllocationId: eip.AllocationId,
    SubnetId:     subnet.ID(),
}, pulumi.Timeouts(&pulumi.CustomTimeouts{
    Create: "10m",  // Increase from default 5m if needed
}))

Issue 8: IMDSv1 Deprecation Warning

Warning: AWS recommends using IMDSv2 for EC2 instance metadata.

Solution: Explicitly configure IMDSv2.

instance, err := ec2.NewInstance(ctx, "secure-instance", &ec2.InstanceArgs{
    Ami:          pulumi.String(ami.Id),
    InstanceType: pulumi.String("t3.micro"),
    MetadataOptions: &ec2.InstanceMetadataOptionsArgs{
        HttpTokens:              pulumi.String("required"),  // IMDSv2 only
        HttpPutResponseHopLimit: pulumi.Int(1),              // Prevent IP forwarding
        HttpEndpoint:            pulumi.String("enabled"),
    },
    // ...
})

Issue 9: Lambda Function Code Changes Not Detected

Scenario: Changes to Lambda code not triggering updates.

Solution: Use SourceCodeHash to track changes.

import (
    "crypto/sha256"
    "encoding/base64"
    "io/ioutil"
)

// Calculate hash of function code
codeBytes, err := ioutil.ReadFile("function.zip")
if err != nil {
    return err
}
hash := sha256.Sum256(codeBytes)
codeHash := base64.StdEncoding.EncodeToString(hash[:])

fn, err := lambda.NewFunction(ctx, "function", &lambda.FunctionArgs{
    Runtime:        pulumi.String("python3.12"),
    Handler:        pulumi.String("index.handler"),
    Role:           role.Arn,
    Code:           pulumi.NewFileArchive("function.zip"),
    SourceCodeHash: pulumi.String(codeHash),  // Triggers update on code change
})

Issue 10: Provider Configuration Inheritance

Scenario: Resources not using expected provider configuration.

Solution: Explicitly pass provider to resources.

// Custom provider
customProvider, err := aws.NewProvider(ctx, "custom", &aws.ProviderArgs{
    Region:  pulumi.StringPtr("eu-west-1"),
    Profile: pulumi.StringPtr("production"),
})
if err != nil {
    return err
}

// ❌ Uses default provider (not customProvider)
bucket1, err := s3.NewBucketV2(ctx, "bucket1", &s3.BucketV2Args{
    Bucket: pulumi.String("bucket-default-region"),
})

// ✅ Explicitly uses customProvider
bucket2, err := s3.NewBucketV2(ctx, "bucket2", &s3.BucketV2Args{
    Bucket: pulumi.String("bucket-eu-west-1"),
}, pulumi.Provider(customProvider))

Advanced Scenarios

Scenario 1: Blue-Green Deployment with Lambda Aliases

// Current version (blue)
blueFunction, err := lambda.NewFunction(ctx, "function-blue", &lambda.FunctionArgs{
    Runtime: pulumi.String("python3.12"),
    Handler: pulumi.String("index.handler"),
    Role:    role.Arn,
    Code:    pulumi.NewFileArchive("./function-v1"),
    Publish: pulumi.Bool(true),  // Create version
})
if err != nil {
    return err
}

// Create alias with traffic splitting
alias, err := lambda.NewAlias(ctx, "production", &lambda.AliasArgs{
    Name:            pulumi.String("production"),
    FunctionName:    blueFunction.Name,
    FunctionVersion: pulumi.String("5"),  // 95% traffic
    RoutingConfig: &lambda.AliasRoutingConfigArgs{
        AdditionalVersionWeights: pulumi.Float64Map{
            "6": pulumi.Float64(0.05),  // 5% canary to version 6
        },
    },
})

Scenario 2: Cross-Region RDS Replication

// Primary region
primaryProvider, err := aws.NewProvider(ctx, "primary", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-east-1"),
})

primary, err := rds.NewInstance(ctx, "primary", &rds.InstanceArgs{
    Engine:           pulumi.String("postgres"),
    InstanceClass:    pulumi.String("db.t3.micro"),
    AllocatedStorage: pulumi.Int(20),
    BackupRetentionPeriod: pulumi.Int(7),  // Required for cross-region replica
}, pulumi.Provider(primaryProvider))
if err != nil {
    return err
}

// Replica in different region
replicaProvider, err := aws.NewProvider(ctx, "replica", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-west-2"),
})

replica, err := rds.NewInstance(ctx, "replica", &rds.InstanceArgs{
    ReplicateSourceDb: primary.Identifier,
    InstanceClass:     pulumi.String("db.t3.micro"),
}, pulumi.Provider(replicaProvider))

Scenario 3: VPC Peering Between Accounts

// Requester VPC (Account A)
requesterProvider, err := aws.NewProvider(ctx, "requester", &aws.ProviderArgs{
    Region:  pulumi.StringPtr("us-east-1"),
    Profile: pulumi.StringPtr("account-a"),
})

// Accepter VPC (Account B)
accepterProvider, err := aws.NewProvider(ctx, "accepter", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-east-1"),
    AssumeRoles: aws.ProviderAssumeRoleArray{
        &aws.ProviderAssumeRoleArgs{
            RoleArn: pulumi.StringPtr("arn:aws:iam::ACCOUNT_B:role/VPCPeeringRole"),
        },
    },
})

// Create peering connection from Account A
peering, err := ec2.NewVpcPeeringConnection(ctx, "peering", &ec2.VpcPeeringConnectionArgs{
    VpcId:      pulumi.String("vpc-requester"),
    PeerVpcId:  pulumi.String("vpc-accepter"),
    PeerOwnerId: pulumi.String("ACCOUNT_B"),
    PeerRegion: pulumi.String("us-east-1"),
}, pulumi.Provider(requesterProvider))
if err != nil {
    return err
}

// Accept from Account B
_, err = ec2.NewVpcPeeringConnectionAccepter(ctx, "accepter", &ec2.VpcPeeringConnectionAccepterArgs{
    VpcPeeringConnectionId: peering.ID(),
}, pulumi.Provider(accepterProvider))

Scenario 4: Lambda Container Images with ECR

// 1. Create ECR repository
repo, err := ecr.NewRepository(ctx, "lambda-repo", &ecr.RepositoryArgs{
    Name: pulumi.String("my-lambda-functions"),
})
if err != nil {
    return err
}

// 2. Build and push image (external process)
// docker build -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-functions:latest .
// aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ...
// docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-functions:latest

// 3. Create Lambda from container image
fn, err := lambda.NewFunction(ctx, "container-function", &lambda.FunctionArgs{
    PackageType: pulumi.String("Image"),
    ImageUri:    repo.RepositoryUrl.ApplyT(func(url string) string {
        return fmt.Sprintf("%s:latest", url)
    }).(pulumi.StringOutput),
    Role:       role.Arn,
    Timeout:    pulumi.IntPtr(60),
    MemorySize: pulumi.IntPtr(512),
})

Scenario 5: ECS with Secrets from Secrets Manager

// Create secret
secret, err := secretsmanager.NewSecret(ctx, "db-password", &secretsmanager.SecretArgs{
    Name: pulumi.String("prod/db/password"),
})
if err != nil {
    return err
}

_, err = secretsmanager.NewSecretVersion(ctx, "password-value", &secretsmanager.SecretVersionArgs{
    SecretId:     secret.ID(),
    SecretString: pulumi.String("MySecurePassword123!"),
})
if err != nil {
    return err
}

// Reference in ECS task definition
containerDefs, _ := json.Marshal([]map[string]interface{}{{
    "name":  "app",
    "image": "myapp:latest",
    "secrets": []map[string]interface{}{{
        "name":      "DB_PASSWORD",
        "valueFrom": secret.Arn.ApplyT(func(arn string) string { return arn }).(pulumi.StringOutput),
    }},
}})

taskDef, err := ecs.NewTaskDefinition(ctx, "task", &ecs.TaskDefinitionArgs{
    Family:                  pulumi.String("my-app"),
    ContainerDefinitions:    pulumi.String(string(containerDefs)),
    ExecutionRoleArn:        taskRole.Arn,  // Role needs secretsmanager:GetSecretValue permission
    // ...
})

Performance Optimization

Optimization 1: Minimize Provider Instances

// ❌ LESS EFFICIENT: New provider for each resource
for i := 0; i < 100; i++ {
    provider, _ := aws.NewProvider(ctx, fmt.Sprintf("p-%d", i), &aws.ProviderArgs{
        Region: pulumi.StringPtr("us-east-1"),
    })
    bucket, _ := s3.NewBucketV2(ctx, fmt.Sprintf("b-%d", i), &s3.BucketV2Args{}, 
        pulumi.Provider(provider))
}

// ✅ MORE EFFICIENT: Reuse single provider
provider, err := aws.NewProvider(ctx, "shared", &aws.ProviderArgs{
    Region: pulumi.StringPtr("us-east-1"),
})
if err != nil {
    return err
}

for i := 0; i < 100; i++ {
    bucket, err := s3.NewBucketV2(ctx, fmt.Sprintf("b-%d", i), &s3.BucketV2Args{
        Bucket: pulumi.Sprintf("bucket-%d", i),
    }, pulumi.Provider(provider))
    if err != nil {
        return err
    }
}

Optimization 2: Parallel Resource Creation

// Pulumi automatically creates independent resources in parallel
// Group related resources to maximize parallelism

// These create in parallel automatically
bucket1, err1 := s3.NewBucketV2(ctx, "bucket1", &s3.BucketV2Args{})
bucket2, err2 := s3.NewBucketV2(ctx, "bucket2", &s3.BucketV2Args{})
bucket3, err3 := s3.NewBucketV2(ctx, "bucket3", &s3.BucketV2Args{})

// Check all errors
if err1 != nil {
    return err1
}
if err2 != nil {
    return err2
}
if err3 != nil {
    return err3
}

Optimization 3: Use VPC Endpoints to Reduce NAT Costs

// Gateway endpoints are free
s3Endpoint, err := ec2.NewVpcEndpoint(ctx, "s3", &ec2.VpcEndpointArgs{
    VpcId:           vpc.ID(),
    ServiceName:     pulumi.String("com.amazonaws.us-east-1.s3"),
    VpcEndpointType: pulumi.String("Gateway"),
    RouteTableIds:   pulumi.StringArray{privateRouteTable.ID()},
})

dynamoEndpoint, err := ec2.NewVpcEndpoint(ctx, "dynamodb", &ec2.VpcEndpointArgs{
    VpcId:           vpc.ID(),
    ServiceName:     pulumi.String("com.amazonaws.us-east-1.dynamodb"),
    VpcEndpointType: pulumi.String("Gateway"),
    RouteTableIds:   pulumi.StringArray{privateRouteTable.ID()},
})
// Saves NAT Gateway data transfer costs for S3/DynamoDB traffic

Security Best Practices

1. Enable Encryption Everywhere

// S3 bucket encryption
_, err = s3.NewBucketServerSideEncryptionConfiguration(ctx, "encryption", &s3.BucketServerSideEncryptionConfigurationArgs{
    Bucket: bucket.ID(),
    Rules: s3.BucketServerSideEncryptionConfigurationRuleArray{
        &s3.BucketServerSideEncryptionConfigurationRuleArgs{
            ApplyServerSideEncryptionByDefault: &s3.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs{
                KmsMasterKeyId: key.Arn,
                SseAlgorithm:   pulumi.String("aws:kms"),
            },
        },
    },
})

// EBS encryption by default
_, err = ebs.NewEncryptionByDefault(ctx, "ebs-encryption", &ebs.EncryptionByDefaultArgs{
    Enabled: pulumi.Bool(true),
})

2. IMDSv2 Enforcement

instance, err := ec2.NewInstance(ctx, "instance", &ec2.InstanceArgs{
    MetadataOptions: &ec2.InstanceMetadataOptionsArgs{
        HttpTokens:              pulumi.String("required"),  // IMDSv2 only
        HttpPutResponseHopLimit: pulumi.Int(1),
    },
    // ...
})

3. Block Public Access on S3

_, err = s3.NewBucketPublicAccessBlock(ctx, "block", &s3.BucketPublicAccessBlockArgs{
    Bucket:                bucket.ID(),
    BlockPublicAcls:       pulumi.Bool(true),
    BlockPublicPolicy:     pulumi.Bool(true),
    IgnorePublicAcls:      pulumi.Bool(true),
    RestrictPublicBuckets: pulumi.Bool(true),
})

4. Enable Multi-Factor Delete on S3

_, err = s3.NewBucketVersioning(ctx, "versioning", &s3.BucketVersioningArgs{
    Bucket: bucket.ID(),
    VersioningConfiguration: &s3.BucketVersioningVersioningConfigurationArgs{
        Status:    pulumi.String("Enabled"),
        MfaDelete: pulumi.String("Enabled"),  // Requires MFA to delete
    },
    Mfa: pulumi.String("arn:aws:iam::123456789012:mfa/user TOKEN"),  // Device ARN + token
})

5. Use Least Privilege IAM Policies

// ✅ Specific permissions
policy, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
    Statements: []iam.GetPolicyDocumentStatement{
        {
            Effect:    pulumi.StringRef("Allow"),
            Actions:   []string{"s3:GetObject", "s3:PutObject"},  // Specific actions
            Resources: []string{"arn:aws:s3:::my-bucket/app/*"},  // Specific resources
        },
    },
})

// ❌ Avoid wildcards
badPolicy, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
    Statements: []iam.GetPolicyDocumentStatement{
        {
            Effect:    pulumi.StringRef("Allow"),
            Actions:   []string{"s3:*"},     // Too broad
            Resources: []string{"*"},         // Too broad
        },
    },
})

Testing and Validation

Local Testing with LocalStack

localstackEndpoint := "http://localhost:4566"
testProvider, err := aws.NewProvider(ctx, "localstack", &aws.ProviderArgs{
    Region:                    pulumi.StringPtr("us-east-1"),
    AccessKey:                 pulumi.StringPtr("test"),
    SecretKey:                 pulumi.StringPtr("test"),
    SkipCredentialsValidation: pulumi.BoolPtr(true),
    SkipMetadataApiCheck:      pulumi.BoolPtr(true),
    SkipRequestingAccountId:   pulumi.BoolPtr(true),
    Endpoints: aws.ProviderEndpointArray{
        &aws.ProviderEndpointArgs{
            S3:        pulumi.StringPtr(localstackEndpoint),
            Dynamodb:  pulumi.StringPtr(localstackEndpoint),
            Lambda:    pulumi.StringPtr(localstackEndpoint),
            Sns:       pulumi.StringPtr(localstackEndpoint),
            Sqs:       pulumi.StringPtr(localstackEndpoint),
        },
    },
})

// Use testProvider for all resources in tests
bucket, err := s3.NewBucketV2(ctx, "test-bucket", &s3.BucketV2Args{
    Bucket: pulumi.String("test-bucket"),
}, pulumi.Provider(testProvider))

Verify Account Before Deployment

// Always verify you're deploying to the correct account
current, err := aws.GetCallerIdentity(ctx, &aws.GetCallerIdentityArgs{}, nil)
if err != nil {
    return err
}

expectedAccount := "123456789012"
if current.AccountId != expectedAccount {
    return fmt.Errorf("wrong account: got %s, expected %s", current.AccountId, expectedAccount)
}

ctx.Log.Info(fmt.Sprintf("Deploying to account %s (%s)", current.AccountId, current.Arn), nil)

Reference Documentation

For detailed API specifications and type definitions:

  • Provider Configuration
  • Compute Services
  • Storage Services
  • Database Services
  • Networking
  • Security (IAM, KMS)

For more production patterns:

Install with Tessl CLI

npx tessl i tessl/golang-github-com-pulumi-pulumi-aws-sdk-v7@7.16.1

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json