scottwinkler / terraform-provider-shell

Terraform provider for executing shell commands and saving output to state file
Mozilla Public License 2.0
279 stars 60 forks source link

Update command fails, state updated anyway #43

Closed jcarlson closed 4 years ago

jcarlson commented 4 years ago

Another interesting issue I've run into that I can't seem to root-cause. I have a failure in my update command, and this is caught by Terraform during terraform apply. The apply fails and raises the error, but re-running terraform apply shows no changes pending. Inspecting terraform.tfstate reveals that the changes were persisted to state, even though the update command failed.

I think what should happen on failure is that the changes should not be persisted to state, so that the next terraform apply will try again to run the update command.

Similarly, for the create command, if there is a failure in the script, the resource should be marked as tainted.

Notice the plan (below) references a change to the environment variable:

~ "DESCRIPTION"    = "changing things up" -> "foo bar"

After this apply is complete, the terraform state file captures this change and reflects a final value of "foo bar" for this environment variable.

Any suggestions?

Here are some log dumps from a terraform apply attempting to update a resource:

# excerpt from `terraform plan`

  # module.acl.shell_script.web_acl will be updated in-place
  ~ resource "shell_script" "web_acl" {
        dirty             = false
      ~ environment       = {
            "DEFAULT_ACTION" = "Allow"
          ~ "DESCRIPTION"    = "changing things up" -> "foo bar"
            "NAME"           = "test-webacl"
            "RULES"          = jsonencode([])
            "SCOPE"          = "REGIONAL"
            "TAGS"           = jsonencode(
                [
                    {
                        Key   = "baz"
                        Value = "etc"
                    },
                    {
                        Key   = "foo"
                        Value = "bar"
                    },
                ]
            )
        }
        id                = "bqausnv0eb17akv9tm1g"
        output            = {
            "arn"        = "arn:aws:wafv2:us-east-1:999999999999:regional/webacl/test-webacl/bee6287e-0cc5-403f-846b-726dca7e95ad"
            "id"         = "bee6287e-0cc5-403f-846b-726dca7e95ad"
            "lock_token" = "8bc4608b-e337-4d84-b2be-0e529346ec8c"
        }
        triggers          = {
            "name"  = "test-webacl"
            "scope" = "REGIONAL"
        }
        working_directory = "."

        lifecycle_commands {
            create = <omitted>
            delete = <omitted>
            read   = <omitted>
            update = <<~EOT
                #!/bin/bash

                # TODO: Check tags

                set -e
                set -o pipefail

                eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')

                LOCK_TOKEN=$(
                  aws wafv2 get-web-acl \
                    --name "$NAME" \
                    --scope "$SCOPE" \
                    --id "$ID" | jq -r '.LockToken'
                )

                aws wafv2 update-web-acl \
                  --scope "$SCOPE" \
                  --name "$NAME" \
                  --id "$ID" \
                  --lock-token "$LOCK_TOKEN" \
                  --default-action "$DEFAULT_ACTION={}" \
                  --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
                  --rules "$RULES" \
                  --tags "$TAGS" \
                  ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
                    --arg id "$ID" \
                    --arg arn "$ARN" \
                    '{
                    arn: .Summary.ARN,
                    id: .Summary.Id,
                    lock_token: .Summary.LockToken
                  }'
            EOT
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.
# excerpt from Terraform DEBUG log

2020-04-14T11:10:18.722-0600 [INFO]  plugin.terraform-provider-shell_v1.2.0: configuring server automatic mTLS: timestamp=2020-04-14T11:10:18.722-0600
2020-04-14T11:10:18.750-0600 [DEBUG] plugin: using plugin: version=5
2020-04-14T11:10:18.750-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: plugin address: address=/var/folders/hj/6wpp8x5s0lbc1jvtzp9np2rw0000gn/T/plugin469611253 network=unix timestamp=2020-04-14T11:10:18.750-0600
2020-04-14T11:10:18.752-0600 [INFO]  plugin: configuring client automatic mTLS
module.acl.shell_script.web_acl: Modifying... [id=bqausnv0eb17akv9tm1g]
2020/04/14 11:10:18 [DEBUG] module.acl.shell_script.web_acl: applying the planned Update change
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] Updating shell script resource...
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 -------------------------
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] Current stack:
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] -- update
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 -------------------------
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] Locking "shellScriptMutexKey"
2020-04-14T11:10:18.870-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] Locked "shellScriptMutexKey"
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] shell script command old state: "&{[DEFAULT_ACTION=Allow DESCRIPTION=changing things up NAME=test-webacl SCOPE=REGIONAL TAGS=[{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}] RULES=[]] map[lock_token:8bc4608b-e337-4d84-b2be-0e529346ec8c arn:arn:aws:wafv2:us-east-1:999999999999:regional/webacl/test-webacl/bee6287e-0cc5-403f-846b-726dca7e95ad id:bee6287e-0cc5-403f-846b-726dca7e95ad]}"
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] shell script going to execute: /bin/sh -c
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    #!/bin/bash
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    # TODO: Check tags
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    set -e
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    set -o pipefail
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    LOCK_TOKEN=$(
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      aws wafv2 get-web-acl \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        --name "$NAME" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        --scope "$SCOPE" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        --id "$ID" | jq -r '.LockToken'
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    )
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18    aws wafv2 update-web-acl \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --scope "$SCOPE" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --name "$NAME" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --id "$ID" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --lock-token "$LOCK_TOKEN" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --default-action "$DEFAULT_ACTION={}" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --rules "$RULES" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      --tags "$TAGS" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        --arg id "$ID" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        --arg arn "$ARN" \
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        '{
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        arn: .Summary.ARN,
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        id: .Summary.Id,
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18        lock_token: .Summary.LockToken
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18      }'
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 -------------------------
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 [DEBUG] Starting execution...
2020-04-14T11:10:18.871-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:18 -------------------------
2020-04-14T11:10:20.183-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20   usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
2020-04-14T11:10:20.183-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20   To see help text, you can run:
2020-04-14T11:10:20.183-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20
2020-04-14T11:10:20.183-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20     aws help
2020-04-14T11:10:20.184-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20     aws <command> help
2020-04-14T11:10:20.184-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20     aws <command> <subcommand> help
2020-04-14T11:10:20.184-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20
2020-04-14T11:10:20.184-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20   Unknown options: --tags, [{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}]
2020-04-14T11:10:20.265-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20 [DEBUG] Unlocking "shellScriptMutexKey"
2020-04-14T11:10:20.265-0600 [DEBUG] plugin.terraform-provider-shell_v1.2.0: 2020/04/14 11:10:20 [DEBUG] Unlocked "shellScriptMutexKey"
2020/04/14 11:10:20 [DEBUG] module.acl.shell_script.web_acl: apply errored, but we're indicating that via the Error pointer rather than returning it: Error occured in Command: '#!/bin/bash

# TODO: Check tags

set -e
set -o pipefail

eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')

LOCK_TOKEN=$(
  aws wafv2 get-web-acl \
    --name "$NAME" \
    --scope "$SCOPE" \
    --id "$ID" | jq -r '.LockToken'
)

aws wafv2 update-web-acl \
  --scope "$SCOPE" \
  --name "$NAME" \
  --id "$ID" \
  --lock-token "$LOCK_TOKEN" \
  --default-action "$DEFAULT_ACTION={}" \
  --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
  --rules "$RULES" \
  --tags "$TAGS" \
  ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
    --arg id "$ID" \
    --arg arn "$ARN" \
    '{
    arn: .Summary.ARN,
    id: .Summary.Id,
    lock_token: .Summary.LockToken
  }'
' Error: 'exit status 252'
 StdOut:

 StdErr:
 usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]To see help text, you can run:  aws help  aws <command> help  aws <command> <subcommand> helpUnknown options: --tags, [{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}]
2020/04/14 11:10:20 [ERROR] module.acl: eval: *terraform.EvalApplyPost, err: Error occured in Command: '#!/bin/bash

# TODO: Check tags

set -e
set -o pipefail

eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')

LOCK_TOKEN=$(
  aws wafv2 get-web-acl \
    --name "$NAME" \
    --scope "$SCOPE" \
    --id "$ID" | jq -r '.LockToken'
)

aws wafv2 update-web-acl \
  --scope "$SCOPE" \
  --name "$NAME" \
  --id "$ID" \
  --lock-token "$LOCK_TOKEN" \
  --default-action "$DEFAULT_ACTION={}" \
  --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
  --rules "$RULES" \
  --tags "$TAGS" \
  ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
    --arg id "$ID" \
    --arg arn "$ARN" \
    '{
    arn: .Summary.ARN,
    id: .Summary.Id,
    lock_token: .Summary.LockToken
  }'
' Error: 'exit status 252'
 StdOut:

 StdErr:
 usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]To see help text, you can run:  aws help  aws <command> help  aws <command> <subcommand> helpUnknown options: --tags, [{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}]
2020/04/14 11:10:20 [ERROR] module.acl: eval: *terraform.EvalSequence, err: Error occured in Command: '#!/bin/bash

# TODO: Check tags

set -e
set -o pipefail

eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')

LOCK_TOKEN=$(
  aws wafv2 get-web-acl \
    --name "$NAME" \
    --scope "$SCOPE" \
    --id "$ID" | jq -r '.LockToken'
)

aws wafv2 update-web-acl \
  --scope "$SCOPE" \
  --name "$NAME" \
  --id "$ID" \
  --lock-token "$LOCK_TOKEN" \
  --default-action "$DEFAULT_ACTION={}" \
  --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
  --rules "$RULES" \
  --tags "$TAGS" \
  ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
    --arg id "$ID" \
    --arg arn "$ARN" \
    '{
    arn: .Summary.ARN,
    id: .Summary.Id,
    lock_token: .Summary.LockToken
  }'
' Error: 'exit status 252'
 StdOut:

 StdErr:
 usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]To see help text, you can run:  aws help  aws <command> help  aws <command> <subcommand> helpUnknown options: --tags, [{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}]
2020-04-14T11:10:20.574-0600 [DEBUG] plugin: plugin process exited: path=/Users/jcarlson/.terraform.d/plugins/darwin_amd64/terraform-provider-aws_v2.50.0_x4 pid=30463
2020-04-14T11:10:20.574-0600 [DEBUG] plugin: plugin exited

Error: Error occured in Command: '#!/bin/bash

# TODO: Check tags

set -e
set -o pipefail

eval $(jq -r '@sh "ID=\(.id) ARN=\(.arn)"')

LOCK_TOKEN=$(
  aws wafv2 get-web-acl \
    --name "$NAME" \
    --scope "$SCOPE" \
    --id "$ID" | jq -r '.LockToken'
)

aws wafv2 update-web-acl \
  --scope "$SCOPE" \
  --name "$NAME" \
  --id "$ID" \
  --lock-token "$LOCK_TOKEN" \
  --default-action "$DEFAULT_ACTION={}" \
  --visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=$NAME" \
  --rules "$RULES" \
  --tags "$TAGS" \
  ${DESCRIPTION:+ --description "$DESCRIPTION"} | jq \
    --arg id "$ID" \
    --arg arn "$ARN" \
    '{
    arn: .Summary.ARN,
    id: .Summary.Id,
    lock_token: .Summary.LockToken
  }'
' Error: 'exit status 252'
 StdOut:

 StdErr:
 usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]To see help text, you can run:  aws help  aws <command> help  aws <command> <subcommand> helpUnknown options: --tags, [{"Key":"baz","Value":"etc"},{"Key":"foo","Value":"bar"}]

2020-04-14T11:10:20.604-0600 [DEBUG] plugin: plugin process exited: path=/Users/jcarlson/.terraform.d/plugins/terraform-provider-shell_v1.2.0 pid=30462
2020-04-14T11:10:20.604-0600 [DEBUG] plugin: plugin exited
  on ../../modules/waf-acl/main.tf line 1, in resource "shell_script" "web_acl":
   1: resource "shell_script" "web_acl" {
scottwinkler commented 4 years ago

First of all, I am extremely impressed with what you were able to accomplish. It is good to see that people are actually using this provider to solve real business needs.

Update() has been something I have been neglecting recently. I know that there was some weird stuff about it. As far as the issue you are dealing with, that is because although the script failed to run, the environment block was still set via partial state. There is a way to override partial state, but i will need to look in the best way to do this.

In the meantime, you could choose to use a template file for your create/read/update/delete scripts and not use the environment block at all. Changing environment variables in the template file will thus change the command, and trigger a force new (taint) on the resource .

Another problem you may not have considered is that if there is an error in the script, it could prevent the resource from being deleted altogether. Consider the trivial case where i have a resource that does nothing but return an exit condition of 0 for create and 1 (error) for delete. I update delete to return 0, "fixing" the problem in the script, and also tainting the resource and causing it to be destroyed and recreated - except the delete always fails because it keeps returning 1 and so the update never succeeds. What is unique about this provider compared to all other providers is that is allows hooks into the Terraform runtime, and if there is an error in there then you don't have a way of proceeding without manually removing the resource from state file.

To solve the above problem I am thinking to have a boolean "force_destroy". That will force the destruction of a resource and ignore any errors in the script. Usually errors are common during development, but not very common after you have been running the code in production for a while. So you could set force_destroy to true when developing for faster iteration, and force_destroy to false when running in production to capture the error

jcarlson commented 4 years ago

I noticed that I had, on accident, referred to my lifecycle commands by reference, rather than inline, as in create = "${path.module}/create.sh" vs create = file("${path.module}/create.sh").

The nice thing about "by reference" is that it allows me to tune and refine my script without causing the resource to be needlessly torn down and recreated. I'm not sure I agree that tinkering with the delete command should cause an otherwise unchanged resource to be destroyed and recreated.

A side effect of this that I noticed, though, is that when a resource does need to be destroyed and recreated, the environment variables passed to the script are that of their current value.

Consider the following example:

resource "shell_script" "immutable" {
  environment = {
    IMMUTABLE_ATTR = "abc"
  }

  lifecycle_commands {
    create = <<-CMD
      echo "PUT https://api.example.com/foos/$IMMUTABLE_ATTR"
      jq -n --arg immutable_attr $IMMUTABLE_ATTR '{ immutable_attr: $immutable_attr }'
    CMD

    delete = <<-CMD
      echo "DELETE https://api.example.com/foos/$IMMUTABLE_ATTR"
    CMD
  }
}

Since I have defined only a create and delete command, changing the [immutable] environment variable should necessitate a destroy/create pairing. Unfortunately, the delete command won't be able to properly delete the resource, since it will receive the new value of IMMUTABLE_ATTR, rather than the stored value that it needs.

This kind of pattern is necessary for cases where I have multiple resources that depend on each other; in the case of AWS WAFv2 resources, I must first create the web-acl, and then associate it with a load balancer. The ARN of the WebACL has to be an input to the ALB-WAF association resource, so an environment variable is the only way to get that value into the dependent resource.

As I write this up, I suppose a reasonable work around would be to store any value necessary for proper deletion as state on the dependent resource; in other words, in the example above, the delete command should be reading IMMUTABLE_ATTR from stored state, not from an environment variable. 🤔

scottwinkler commented 4 years ago

I just released a new build that specifically was designed to make update() easier to understand and work with. Please test it out and let me know what you think. I also added a better example for update in the examples folder ("complete-with-update"). This might help answer some of your questions regarding state. I do not use environment variables for state, i use stdin which contains the information of any previous output map. I also removed ForceNew from delete() and update() because i didnt think they added any value.

If you have any problems with the latest version please open a new issue, I will close this one for now.