hashicorp / terraform-provider-aws

The AWS Provider enables Terraform to manage AWS resources.
https://registry.terraform.io/providers/hashicorp/aws
Mozilla Public License 2.0
9.81k stars 9.16k forks source link

Force new resource for aws_ssm_association on s3 file change #9996

Open hryamzik opened 5 years ago

hryamzik commented 5 years ago

I have a aws_ssm_association with a document that downloads script from s3 and executes it. I want to get that association regenerated each time I change the script so that it was re-run on all existing instances.

I was looking for a terraform-wide solution but didn't find one. Here's what I'd expect:

variable scripts {
    default = [
        "myscript.sh"
    ]
}

resource "aws_s3_bucket_object" "scripts" {
  key    = "${var.scripts[count.index]}"
  bucket = "${aws_s3_bucket.scripts.bucket}"
  source = "scripts/${var.scripts[count.index]}"
  etag   = "${md5(file("scripts/${var.scripts[count.index]}"))}"
  acl    = "private"
  count  = "${length(var.scripts)}"
}

resource "aws_ssm_association" "script" {
  name             = "AWS-RunRemoteScript"
  association_name = "s3-script"
  parameters {
      sourceType = "S3"
      sourceInfo = <<EOJ
      {
          "path": "https://s3.amazonaws.com/${aws_s3_bucket.scripts.bucket}/myscript.sh"
      }
      EOJ
      commandLine = "myscript.sh"
  }

  force_update_key = "${element(aws_s3_bucket_object.scripts.*.etag, index(var.scripts, "myscript.sh"))}"
}
hryamzik commented 5 years ago

Relevant to https://github.com/hashicorp/terraform/issues/8099

hryamzik commented 5 years ago

OK, I managed to get this working. Almost... As soon as name parameter for aws_ssm_association forces new resource:

            "name": {
                Type:     schema.TypeString,
                ForceNew: true,
                Required: true,
            },

It's possible to generate new document on every script change and reference it in aws_ssm_association:

variable scripts {
    default = [
        "myscript.sh"
    ]
}

resource "aws_s3_bucket_object" "scripts" {
  key    = "${var.scripts[count.index]}"
  bucket = "${aws_s3_bucket.scripts.bucket}"
  source = "${path.module}/scripts/${var.scripts[count.index]}"
  etag   = "${md5(file("${path.module}/scripts/${var.scripts[count.index]}"))}"
  acl    = "private"
  count  = "${length(var.scripts)}"
}

resource "aws_ssm_document" "run-remote-script" {
  name          = "RunRemoteScript-${element(aws_s3_bucket_object.scripts.*.etag, count.index)}"
  document_type = "Command"
  content = "${file("${path.module}/documents/RunRemoteScript.yaml")}"
  count = "${length(var.scripts)}"
  document_format = "YAML"
}

resource "aws_ssm_association" "script" {
  name             = "AWS-RunRemoteScript-${element(aws_s3_bucket_object.scripts.*.etag, index(var.scripts, "myscript.sh"))}"
  association_name = "s3-script"
  parameters {
      sourceType = "S3"
      sourceInfo = <<EOJ
      {
          "path": "https://s3.amazonaws.com/${aws_s3_bucket.scripts.bucket}/myscript.sh"
      }
      EOJ
      commandLine = "myscript.sh"
  }
}

That configuration seem to be valid but terraform fails to apply changes on the first run, see #10004

Colza5000 commented 5 years ago

👍

hryamzik commented 5 years ago

I'm still thinking of implementing a separate optional parameter to force new resource when it changes.

seanturner026 commented 4 years ago

AWS's syntax for SourceInfo is really bad. Been looking for to deploy my script from s3 while also interpolating the bucket id which is how I ended up here. It does seem odd though that an update to the s3 object that the ssm association reads as source doesn't trigger a change

dinvlad commented 4 years ago

I think the real issue here might be that default_version and latest_version attributes of aws_ssm_document are treated as args by AWS provider, for some reason. So when I use them to set document_version arg of aws_ssm_association, that doesn't result in the update of aws_ssm_association when the document_version gets updated. I have to re-apply TF a 2nd time to get the version propagated..

(This solution avoids the necessity to change document name on every run)

dinvlad commented 4 years ago

Found another potential workaround:

# document.yml
schemaVersion: "2.2"
parameters:
  version:
    type: String
    description: Version tag for the document content, used for updates
...
resource "aws_ssm_association" "test" {
  name = aws_ssm_document.test.name
  targets {
    ...
  }
  parameters = {
    version = md5(aws_ssm_document.test[0].content)
  }
}

This way, whenever the doc content gets updated (including any Etags/hashes stored within), the aws_ssm_association also gets updated, since content does get re-computed by TF on every run.

jjruescas commented 4 years ago

@hryamzik , do you have an example of your "RunRemoteScript.yaml", please? 🙏

abeluck commented 4 years ago

We're trying to do something similar, but were using the AWS provided document AWS-ApplyAnsiblePlaybooks.

We're uploading the zip file containing the ansible playbooks to s3 with terraform, and the goal is to have the association re-apply when the zip file changes.

The AWS-ApplyAnsiblePlaybooks document doesn't contain a version param.

Is there a workaround for this?

edit: Turns out this is rather simple:

data "archive_file" "ansible_playbooks_bundle" {
  type        = "zip"
  source_dir  = "path/to/ansible/dir"
  output_path = "${path.module}/ansible.zip"
}

locals {
  ansible_bundle_md5 = filemd5(data.archive_file.ansible_playbooks_bundle.output_path)
}

resource "aws_s3_bucket_object" "ansible_playbooks_bundle" {
  key    = "ansible-${local.ansible_bundle_md5}.zip"
  bucket = aws_s3_bucket.playbooks_bucket.id
  source = data.archive_file.ansible_playbooks_bundle.output_path
  etag   = local.ansible_bundle_md5
  acl    = "private"
}

output "ansible_playbooks_bundle_s3_object" {
  value = aws_s3_bucket_object.ansible_playbooks_bundle
}

output "ansible_playbooks_s3_bucket" {
  value = module.ssm_setup.playbooks_bucket_id
}

output "ansible_playbooks_bundle_md5" {
  value = local.ansible_bundle_md5
}

I'm still thinking of implementing a separate optional parameter to force new resource when it changes.

That would probably be useful for other use cases.

dinvlad commented 4 years ago

Yes, we've accomplished it very similarly

baronne commented 2 years ago

not sure if this helps anyone, but I managed to force the association using the ExtraVariables to add a "version" by generating an md5 hash of the playbook.yml file so whenever we change the yaml it will update the association: ` resource "aws_ssm_association" "ansible" { name = "AWS-ApplyAnsiblePlaybooks" association_name = "ansible" parameters = { SourceType = "S3" SourceInfo = "{\"path\": \"https://${module.s3_playbooks.s3_bucket_bucket_domain_name}/playbook.yml\"}" PlaybookFile = "playbook.yml" InstallDependencies = "True" Verbose = "-v" ExtraVariables = "Version=${filemd5("ansible/playbook.yml")}" } output_location { s3_bucket_name = module.s3_logging.s3_bucket_id }

targets { key = "tag:Name" values = ["web"] } } `

netseevol commented 7 months ago

well. following trick works:

resource "null_resource" "always_run" { triggers = { timestamp = "${timestamp()}" } }

resource "aws_ssm_document" "document" { .. lifecycle { replace_triggered_by = [ null_resource.always_run ] }

resource "aws_ssm_association" "run" { .. lifecycle { replace_triggered_by = [ null_resource.always_run ] }

in that way. it re creates aws document and association on each run.

Eero