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.
Advanced scenarios, common pitfalls, and solutions for the Pulumi AWS Provider.
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
}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
})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 yetCorrect 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 policyProblem: 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
})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"),
},
})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 availableScenario: 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))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"))},
}))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"),
})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"),
})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}))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()},
})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
}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),
})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
}Error:
error configuring Pulumi AWS Provider: no valid credential sources foundDiagnosis: 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 configurationprovider, err := aws.NewProvider(ctx, "aws", &aws.ProviderArgs{
Region: pulumi.StringPtr("us-east-1"),
Profile: pulumi.StringPtr("myprofile"),
})Error:
operation error STS: AssumeRole, https response error StatusCode: 403Diagnosis: 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
},
},
})Error:
DependencyViolation: The vpc 'vpc-xxxxx' has dependencies and cannot be deletedDiagnosis: 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 destroyError:
InvalidSubnetID.NotFound: The subnet ID 'subnet-xxxxx' does not existDiagnosis: 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
// ...
})Error:
InvalidGroup.NotFound: The security group 'sg-xxxxx' does not existDiagnosis: 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
// ...
})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 PulumiError:
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
}))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"),
},
// ...
})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
})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))// 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
},
},
})// 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))// 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))// 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),
})// 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
// ...
})// ❌ 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
}
}// 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
}// 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// 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),
})instance, err := ec2.NewInstance(ctx, "instance", &ec2.InstanceArgs{
MetadataOptions: &ec2.InstanceMetadataOptionsArgs{
HttpTokens: pulumi.String("required"), // IMDSv2 only
HttpPutResponseHopLimit: pulumi.Int(1),
},
// ...
})_, 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),
})_, 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
})// ✅ 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
},
},
})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))// 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)For detailed API specifications and type definitions:
For more production patterns:
Install with Tessl CLI
npx tessl i tessl/golang-github-com-pulumi-pulumi-aws-sdk-v7docs