|
| 1 | +--- |
| 2 | +title_tag: Building a Component Resource | Learn Pulumi |
| 3 | +title: "Building a Component Resource" |
| 4 | +layout: topic |
| 5 | +date: 2021-11-17 |
| 6 | +draft: false |
| 7 | +description: | |
| 8 | + Try creating a component resource, or a logical grouping of code that Pulumi |
| 9 | + recognizes as a resource. |
| 10 | +meta_desc: Learn how to build a component resource, or a logical grouping of code that Pulumi recognizes as a resource, in this tutorial. |
| 11 | +index: 3 |
| 12 | +estimated_time: 10 |
| 13 | +meta_image: meta.png |
| 14 | +authors: |
| 15 | + - laura-santamaria |
| 16 | +tags: |
| 17 | + - learn |
| 18 | +links: |
| 19 | + - text: Component Resources |
| 20 | + url: https://www.pulumi.com/docs/concepts/resources/#components |
| 21 | +--- |
| 22 | + |
| 23 | +Now that we have a better understanding of why abstraction and encapsulation are valuable concepts for Pulumi programs, let's explore what happens when we start working in larger teams at scale. |
| 24 | + |
| 25 | +A resource in Pulumi terms is basically a building block. That building block could be a base component like an `s3.Bucket` object that's managed by external providers like your favorite cloud provider. Alternatively, a resource could be a group of other resources that implements abstraction and encapsulation for reasons like defining a common pattern. This latter type of resource is called a [_Component Resource_](/docs/concepts/resources#components) in Pulumi's terminology. |
| 26 | + |
| 27 | +## Deciding to create a component resource |
| 28 | + |
| 29 | +Why would you make a component resource instead of just a "simple" logical grouping that you can call and use like a generic library full of classes and functions? A component resource shows up in the Pulumi ecosystem just like any other resource. That means it has a trackable state, appears in diffs, and has a name field you can reference that refers to the entire grouping. |
| 30 | + |
| 31 | +We've actually already started creating a component resource in the encapsulation part when we made a new class that built up an `s3.Bucket` and an `s3.BucketPolicy`. Let's now turn that logical grouping into an actual component resource. |
| 32 | + |
| 33 | +## Converting to a component resource |
| 34 | + |
| 35 | +When we're converting to a component resource, we're subclassing the `ComponentResource` so that our new component resource can get all of the lovely benefits of a resource (state tracking, diffs, name fields, etc.) that other resources have. |
| 36 | + |
| 37 | +In Python, we subclass by using `super()` in the initialization of the class. This call ensures that Pulumi registers the component resource as a resource properly. |
| 38 | + |
| 39 | +{{< chooser language "typescript,python" />}} |
| 40 | + |
| 41 | +{{% choosable language typescript %}} |
| 42 | + |
| 43 | +```typescript |
| 44 | +// ... |
| 45 | + |
| 46 | +// Create a class that encapsulates the functionality by subclassing |
| 47 | +// pulumi.ComponentResource. |
| 48 | +class OurBucketComponent extends pulumi.ComponentResource { |
| 49 | + public bucket: aws.s3.Bucket; |
| 50 | + private bucketPolicy: aws.s3.BucketPolicy; |
| 51 | + |
| 52 | + private policies: { [K in PolicyType]: aws.iam.PolicyStatement } = { |
| 53 | + default: { |
| 54 | + Effect: "Allow", |
| 55 | + Principal: "*", |
| 56 | + Action: [ |
| 57 | + "s3:GetObject" |
| 58 | + ], |
| 59 | + }, |
| 60 | + locked: { |
| 61 | + /* ... */ |
| 62 | + }, |
| 63 | + permissive: { |
| 64 | + /* ... */ |
| 65 | + }, |
| 66 | + }; |
| 67 | + |
| 68 | + private getBucketPolicy(policyType: PolicyType): aws.iam.PolicyDocument { |
| 69 | + return { |
| 70 | + Version: "2012-10-17", |
| 71 | + Statement: [{ |
| 72 | + ...this.policies[policyType], |
| 73 | + Resource: [ |
| 74 | + pulumi.interpolate`${this.bucket.arn}/*`, |
| 75 | + ], |
| 76 | + }], |
| 77 | + } |
| 78 | + }; |
| 79 | + |
| 80 | + constructor(name: string, args: { policyType: PolicyType }, opts?: pulumi.ComponentResourceOptions) { |
| 81 | + |
| 82 | + // By calling super(), we ensure any instantiation of this class |
| 83 | + // inherits from the ComponentResource class so we don't have to |
| 84 | + // declare all the same things all over again. |
| 85 | + super("pkg:index:OurBucketComponent", name, args, opts); |
| 86 | + |
| 87 | + this.bucket = new aws.s3.Bucket(name, {}, { parent: this }); |
| 88 | + |
| 89 | + this.bucketPolicy = new aws.s3.BucketPolicy(`${name}-policy`, { |
| 90 | + bucket: this.bucket.id, |
| 91 | + policy: this.getBucketPolicy(args.policyType), |
| 92 | + }, { parent: this }); |
| 93 | + |
| 94 | + // We also need to register all the expected outputs for this |
| 95 | + // component resource that will get returned by default. |
| 96 | + this.registerOutputs({ |
| 97 | + bucketName: this.bucket.id, |
| 98 | + }); |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +const bucket = new OurBucketComponent("laura-bucket-1", { |
| 103 | + policyType: "permissive", |
| 104 | +}); |
| 105 | + |
| 106 | +export const bucketName = bucket.bucket.id; |
| 107 | +``` |
| 108 | + |
| 109 | +{{% /choosable %}} |
| 110 | + |
| 111 | +{{% choosable language python %}} |
| 112 | + |
| 113 | +```python |
| 114 | +import ... |
| 115 | + |
| 116 | +# Create a class that encapsulates the functionality while subclassing the ComponentResource class (using the ComponentResource class as a template). |
| 117 | +class OurBucketComponent(pulumi.ComponentResource): |
| 118 | + def __init__(self, name_me, policy_name='default', opts=None): |
| 119 | + # By calling super(), we ensure any instantiation of this class inherits from the ComponentResource class so we don't have to declare all the same things all over again. |
| 120 | + super().__init__('pkg:index:OurBucketComponent', name, None, opts) |
| 121 | + # This definition ensures the new component resource acts like anything else in the Pulumi ecosystem when being called in code. |
| 122 | + child_opts = pulumi.ResourceOptions(parent=self) |
| 123 | + self.name_me = name_me |
| 124 | + self.policy_name = policy_name |
| 125 | + self.bucket = aws_native.s3.Bucket(f"{self.name_me}") |
| 126 | + self.policy_list = { |
| 127 | + 'default': default, |
| 128 | + 'locked': '{...}', |
| 129 | + 'permissive': '{...}' |
| 130 | + } |
| 131 | + # We also need to register all the expected outputs for this component resource that will get returned by default. |
| 132 | + self.register_outputs({ |
| 133 | + "bucket_name": self.bucket.bucket_name |
| 134 | + }) |
| 135 | + |
| 136 | + def define_policy(self): |
| 137 | + policy_name = self.policy_name |
| 138 | + try: |
| 139 | + json_data = self.policy_list[f"{policy_name}"] |
| 140 | + policy = self.bucket.arn.apply(lambda arn: json.dumps(json_data).replace('fakeobjectresourcething', arn)) |
| 141 | + return policy |
| 142 | + except KeyError as err: |
| 143 | + add_note = "Policy name needs to be 'default', 'locked', or 'permissive'" |
| 144 | + print(f"Error: {add_note}. You used {policy_name}.") |
| 145 | + raise |
| 146 | + |
| 147 | + def set_policy(self): |
| 148 | + bucket_policy = aws_classic.s3.BucketPolicy( |
| 149 | + f"{self.name_me}-policy", |
| 150 | + bucket=self.bucket.id, |
| 151 | + policy=self.define_policy(), |
| 152 | + opts=pulumi.ResourceOptions(parent=self.bucket) |
| 153 | + ) |
| 154 | + return bucket_policy |
| 155 | + |
| 156 | +bucket1 = OurBucketClass('laura-bucket-1', 'default') |
| 157 | +bucket1.set_policy() |
| 158 | + |
| 159 | +pulumi.export("bucket_name", bucket1.bucket.id) |
| 160 | +``` |
| 161 | + |
| 162 | +{{% /choosable %}} |
| 163 | + |
| 164 | +With the call to `super()`, we pass in a name for the resource, which [we recommend](/docs/concepts/resources/components#authoring-a-new-component-resource) being of the form `<package>:<module>:<type>` to avoid type conflicts since it's being registered alongside other resources like the Bucket resource we're calling (`aws:s3:Bucket`). |
| 165 | + |
| 166 | +{{% choosable language python %}} |
| 167 | + |
| 168 | +That last call in the init, `self.register_outputs({})`, passes Pulumi the expected outputs so Pulumi can read the results of the creation or update of a component resource just like any other resource, so don't forget that call! You can [register default outputs using this call](/docs/concepts/resources/components#registering-component-outputs), as well. It's not hard to imagine we will always want the bucket name for our use case, so we pass that in as an always-given output for our component resource. |
| 169 | + |
| 170 | +{{% /choosable %}} |
| 171 | + |
| 172 | +{{% choosable language typescript %}} |
| 173 | + |
| 174 | +That last call in the init, `this.registerOutputs({})`, passes Pulumi the expected outputs so Pulumi can read the results of the creation or update of a component resource just like any other resource, so don't forget that call! You can [register default outputs using this call](/docs/concepts/resources/components#registering-component-outputs), as well. It's not hard to imagine we will always want the bucket name for our use case, so we pass that in as an always-given output for our component resource. |
| 175 | + |
| 176 | +{{% /choosable %}} |
| 177 | + |
| 178 | +From here, you can deploy it and get your custom resource appearing in the resource tree in your terminal! You can also share it with others so they can import the resource and use it without ever needing to understand all of the underlying needs of a standard storage system on AWS. |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +Congratulations! You've now finished this pathway on abstraction and encapsulation in Pulumi programs! In this pathway, you've learned about thinking of code in abstract forms, wrapping up logical groupings of code to make reuse easier, and building with component resources to make those logical groupings something that Pulumi recognizes. |
| 183 | + |
| 184 | +There's a lot more to explore regarding this topic in Pulumi. We're working on more pathways, but for now, check out some more resources: |
| 185 | + |
| 186 | +* [An AWS/Python example](https://github.com/pulumi/examples/tree/master/aws-py-wordpress-fargate-rds) |
| 187 | +* [An Azure/Python example](https://github.com/pulumi/examples/tree/master/classic-azure-py-webserver-component) |
| 188 | +* [A Google Cloud/Python example](https://github.com/pulumi/examples/tree/master/gcp-py-network-component) |
| 189 | + |
| 190 | +Go build new things, and watch this space for more learning experiences with Pulumi! |
0 commit comments