top of page
  • Akmod

Building Resource States with Idem: A Deep Dive

Idem, the declarative cloud management tool, is designed to be extensible and consistent in managing cloud resources. One of the core features of Idem is its ability to describe existing infrastructure and manage it with states. This post delves into creating resource states in Idem and making the most of its describe functionality.

Implementing the Resource Contract

For a plugin in Idem to utilize the describe functionality, its states must adhere to the resource contract. This contract dictates that three asynchronous functions must be present in the state file: present, absent, and describe.

To include the resource contract in your state file, add the following line of code at the top:

__contracts__ = ["resource"]

Let's dissect each function required by the resource contract.

The `present` Function

The present function plays a vital role in ensuring that the desired resource exists and has parameters that match the ones passed to it. If the resource does not exist, it creates it; if the resource exists but differs in configuration, it updates it. This is where the concept of idempotence comes in. Idempotence is a fundamental concept in infrastructure as code (and is where idem's name is derived from!). An idempotent operation is one that has the same effect whether you run it once or multiple times. For the present function, this means that no matter how many times you run the state with the same parameters, the final state of the resource will always be the same.

Idempotence ensures that your infrastructure is consistent and predictable. Instead of writing scripts with complex logic to handle different scenarios, you can define the desired state, and Idem will take care of the rest.

Here’s an example of what the `present` function can look like:

async def present(hub, ctx, name: str, resource_id: str = None, **kwargs):
    result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
    # Initially check the state of the resource
    attributes = await hub.exec.my_cloud.my_resource.get(ctx, resource_id)
    result["old_state"] = attributes
    # Ensure the resource exists, create if necessary
    if not resource_id:
        if ctx.test:
            result["comment"].append("Would create the resource")
            result["comment"].append("Creating the resource")
            await hub.exec.my_cloud.my_resource.create(ctx, resource_id)

    attributes = await hub.func_call_to_create_resource(ctx)
    # Update the resource attributes
    if attributes != kwargs:
        if ctx.test:
            result["comment"].append("Would update resource attributes")
            result["comment"].append("Updating resource attributes")
             await hub.exec.my_cloud.my_resource.update(ctx, **kwargs)
    # Get the final state of the resource after changes
    attributes = await hub.exec.my_cloud.my_resource.get(ctx, resource_id)
    result["new_state"] = attributes  
    return result

The absent Function

The absent function ensures that the resource no longer exists. It usually takes only hub, ctx, name, and resource_id as parameters. Sometimes, it may contain additional kwargs for extra identifiers or options like "force", but generally, it's used to delete or remove a resource.

Here's an example of an absent function:

async def absent(hub, ctx, name: str, resource_id: str, **kwargs):
    result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
    # Check if the resource exists before trying to delete it
    attributes = await hub.exec.my_cloud.my_resource.get(ctx, resource_id)
    result["old_state"] = attributes
    if attributes:
        # The resource exists, delete it
        if ctx.test:
            result["comment"].append("Would delete the resource")
            result["comment"].append("Deleting the resource")
            success = await hub.exec.my_cloud.my_resource.delete(ctx, resource_id)
            if success:
                result["comment"].append("Resource deleted successfully")
                result["comment"].append("Failed to delete the resource")
                result["result"] = False
        # The resource does not exist
        result["comment"].append("Resource not found, nothing to do")

    return result

Parameters of the present and absent Functions

Let's break down the parameters that the present and absent functions accepts:

  • hub: This is the global namespace that is passed to all functions in POP. It can be used to call other functions and access shared data.

  • ctx: The ctx parameter holds the context during the execution. It contains useful data such as credentials which are accessed using ctx.acct. This is essential for calling the cloud's API via the exec layer. Another important part of ctx is ctx.test, a flag that indicates whether the --test flag was set on the CLI. If ctx.test is True, the function should not make any actual changes and should only indicate what changes would have been made.

  • name: This parameter is the state's unique identifier. It is used to keep track of the specific instance of the resource you are managing in ESM.

  • resource_id: The resource_id is a unique identifier for the resource in the cloud. It is used to ensure idempotence and can be especially helpful with resources that are not natively idempotent, such as AWS EC2 instances.

  • kwargs: This stands for keyword arguments and represents any other attributes of the resource that should be managed by the present state, such as tags or other attributes of the resource being managed.

Return Value

The present and absent functions return a dictionary containing details of the operation. This dictionary includes:

  • comment: A list of comments that provides information about the process of managing the resource.

  • old_state: The state of the resource at the very beginning of the function. This represents how the resource was before any operations were performed.

  • new_state: The state of the resource after all operations have been completed. This represents the final state of the resource.

  • name: The unmodified name parameter passed to the function.

  • result: A boolean, True if all operations were completed successfully, otherwise False.

The old_state and new_state are particularly important as they can be used to calculate what changes were made to the resource. There is a post contract for the resource contract that fires an event with the result of present and absent states.

The describe Function

The describe function is used to scan the resource in the cloud and generate valid present states for existing infrastructure. This function only takes hub and ctx as parameters. The credentials from ctx.acct are passed to exec modules that enumerate a resource, and the describe function then formats them as valid present states.

The output of the describe function looks like this:

    "resource_id": {
        "cloud.resource.ref.present": [
            {"resource_attribute_key": "resource_attribute_value"},

Here's an example of what the full describe function can look like:

async def describe(hub, ctx):
    resources = await hub.exec.my_cloud.my_resource.list(ctx)
    result = {}
    for resource in resources:
        resource_id = resource.get("id")
        resource_attributes = resource.get("attributes")
        result[resource_id] = {"cloud.resource.ref.present": [{"attrbitue": "value"}, ...]}
    return result

Wrapping Up

By following the resource contract, you can build robust and idempotent states in Idem. This not only ensures consistency but also makes infrastructure as code more declarative and manageable. The present, absent, and describe functions play a vital role in enforcing the state of resources in the cloud.

Remember that idempotence is key in infrastructure management. Through this, we can ensure that our infrastructure reaches the desired state without unintended side-effects.

Happy coding!


bottom of page