Enforcing a resource tagging strategy on AWS with Pulumi

Temps de lecture : 3 minutes

What if you could easily and painlessly enforce a tagging policy through your infrastructure code without paying any additional fees?

Enforcing a resource tagging policy makes your AWS resource tracking much easier. One might wants to apply tags to track which components this specific resource belongs to, who needs to be billed for a service usage or who own that resource in the organization.

With Terraform, there is no easy way to enforce such policies.

You can rely on linters that will make sure you’re resources are tagged correctly. These linters parse the code and check every resource block declarations. It’s very cumbersome to setup. You have to run your linter in your CI/CD pipeline which is not ideal. We would prefer to have it working on our workstation. I addition, today, this kind of tools does not exist. One has write it using HCL parsing libraries, such as PyHCL.

An other way is to create a Terraform module whose interface enforce the tagging strategy. It works very well for simple resources such as IAM policies, where the interface is simple. However, due to the very limited capabilities of the Hashicorp Configuration Language (HCL), it’s very hard to reproduce complex structures, such as embedded object definition. The S3 Bucket website block is a good example. Moreover, some resources have a very large interface. Reproducing the complete interface inside a module is very painful and tends to make the code unreadable. Try with the S3 Bucket resource if your not convinced.

Fortunately nowadays, there are alternatives to Terraform. One of them, Pulumi, is very promising. Pulumi is a cloud infrastructure management software. Fundamentally it does the same thing as Terraform. As a frontend language, Terraform uses HCL, whereas Pulumi developed an abstract interface. This allows to use any programming language as a frontend, provided that the execution environment implements the “pulumi protocol”. This protocol is used to communicate with the Pulumi engine allowing to register new cloud resources and declare dependency relationships. See the “How Pulumi works” documentation page for a more detailed explanation.

Because Pulumi uses expressive programming language, such has TypeScript, it far easier to express complex structures concisely.

As an example, we are going to apply a tagging strategy to a S3 bucket. We will use the “module” approach.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// We define our organization tagging policy here
interface OrganizationTags {
  department: string,
  application: string,
  classification: "public" | "private",
}
  
// We define the TaggedS3Bucket resource can be given any standard S3 Bucket argument
// and MUST contains a organizationTags field containing the organization specifics tags.
type TaggedS3BucketArgs = aws.s3.BucketArgs & { organizationTags: OrganizationTags };
  
class TaggedS3Bucket extends aws.s3.Bucket {
  constructor(name: string, args: TaggedS3BucketArgs, opts?: pulumi.ResourceOptions) {
    // We create our definitive tag object by merging the user defined tags and the 
    // organization tags.
    let tags = { ...args.tags, ...args.organizationTags};
    
    // We create the bucket args by merging the provided arguments and the newly 
    // created tag object.
    let bucketArgs = {...args, tags: tags}
    
    // We use the new bucket args to create our bucket.
    super(name, bucketArgs, opts)
  }
}

// As an example
let bucket = new TaggedS3Bucket("test", {
  bucketPrefix: "test-tagged-bucket",
  // Try removing this block
  organizationTags: {
    application: "mywebservice",
    department: "R&D",
    classification: "public"
  }
  // ======
});

// Then we can use our bucket in the same way we would do with any other bucket. 
export let bucket_name = bucket.id;

It’s straightforward and easy to use at the same time. With this code, you can’t omit the organization specific tags, otherwise your code won’t compile.

If the S3 Bucket argument list is evolving, you don’t need to modify this code to make it work with the new interface.

Because TaggedS3Bucket derives Bucket you can use TaggedS3Bucket and Bucket interchangeably in your code. Therefore, if an other component requires a Bucket object, you can pass a TaggedS3Bucket instead without any issue.

This technique is generic enough so that you can use it to enforce other resource properties, such as naming conventions.

Note that, even though it forces developers to enforce the tagging policy in the code, if you need to strictly enforce your naming or tagging policies globally, then it is necessary to use other services such as AWS Config and / or CloudWatch and CloudTrail events to catch any non-compliant resource.

Commentaires :

A lire également sur le sujet :