Search code examples
amazon-web-servicesgoamazon-s3pulumi

Pulumi - S3 - How can I attach a AccesspointPolicy to a Accesspoint when the policy depends on it


I know I can create a Accesspoint and attach a policy like this:

exampleAccessPoint, err := pulumiS3.NewAccessPoint(ctx, accessPointName, &pulumiS3.AccessPointArgs{
        AccountId: pulumi.String(exampleAcc),
        Bucket:    pulumi.String(exampleBucket),
        Name:      pulumi.String(exampleName),
        Policy: pulumi.String(policy)
})

but what do I do when my policy looks like this:

policyDoc := policyDocument{
        Version: "2012-10-17",
        Statement: []statementEntry{
            {
                Effect: "Allow",
                Action: []string{
                    "s3:GetObject",
                },
                Resource:  []string{exampleAccesspointArn},
                Principal: struct{ AWS []string }{exampleValue},
            },
            {
                Effect:    "Allow",
                Action:    []string{"s3:ListBucket"},
                Resource:  []string{accessPointArn},
                Principal: struct{ AWS []string }{exampleValue},
                Condition: &condition{
                    StringLike: &stringLike{
                        S3Prefix: examplePrefix,
                    },
                },
            },
        },
    }

It depends on the accessPointArn so I can't attach it on creation, so what should I do?


Solution

  • Pulumi has a special type called Output that's made for managing resource relationships like these. Since your policy depends on the resource ARN, and the ARN won't be available until after the AccessPoint is created, you'll need to construct the policy as a string output (rather than just a plain string) by "waiting" for the ARN to be available using .ApplyT().

    Here's a somewhat simplified version of your example showing how you might do that:

    package main
    
    import (
        "encoding/json"
    
        "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
    
            bucket, err := s3.NewBucket(ctx, "bucket", nil)
            if err != nil {
                return err
            }
    
            accessPoint, err := s3.NewAccessPoint(ctx, "access-point", &s3.AccessPointArgs{
                Bucket: bucket.ID(),
            })
            if err != nil {
                return err
            }
    
            policy, err := s3.NewBucketPolicy(ctx, "policy", &s3.BucketPolicyArgs{
                Bucket: bucket.ID(),
    
                // 👇 Use .ApplyT() to wait for the ARN to be available, then compose a new string output with it.
                Policy: accessPoint.Arn.ApplyT(func(arn string) (pulumi.StringOutput, error) {
                    p, err := json.Marshal(map[string]interface{}{
                        "Version": "2012-10-17",
                        "Statement": []map[string]interface{}{
                            {
                                "Effect": "Allow",
                                "Action": "s3:ListBucket",
                                "Principal": map[string]string{
                                    "AWS": "exampleValue",
                                },
    
                                // 👇 Use the unwrapped string.
                                "Resource": arn + "/*", 
                            },
                        },
                    })
    
                    if err != nil {
                        return pulumi.StringOutput{}, err
                    }
    
                    // 👇 Return a new, transformed string output.
                    return pulumi.String(p).ToStringOutput(), nil 
                }).(pulumi.StringOutput),
            })
            if err != nil {
                return err
            }
    
            ctx.Export("policy", policy.ID())
            return nil
        })
    }
    

    This tells Pulumi to create the AccessPoint first, then the BucketPolicy. If the policy happened to call for multiple values from multiple resources, you could use .All() instead.

    That's the general pattern, though: use .ApplyT() to receive and transform a plain value into a new output, then pass the transformed output as an input to another resource. The docs go into this in much more detail:

    Hope that helps!