Repository: cloud-custodian/cloud-custodian

Issue:
Add custodian_snapshot tag to aws.ebs.actions.snapshot?
I've noticed EBS snapshot created using **aws.ec2.actions.snapshot** automatically gets tagged **custodian_snapshot**. But snapshot created using **aws.ebs.actions.snapshot** doesn't have that tag. That tag is actually quite useful for when making a decision on deleting a snapshot based on whether or not the snapshot was originally created by Cloud Custodian. Can we add that to aws.ebs.actions.snapshot?

This existing logic that adds that tag in ec2.py: https://github.com/cloud-custodian/cloud-custodian/blob/0.8.45.3/c7n/resources/ec2.py#L1379

Happy to contribute if we think this is useful!


Discussion:
ebs snapshot was recently extended to support copy-tag style syntax in trunk. the custodian marker tag was a bit of an accident wrt to value, but yeah game for the addition.
ideally this would be a tags: mapping that the user could also specify their own tags/values.

Resolving patch:
diff --git a/c7n/resources/ebs.py b/c7n/resources/ebs.py
--- a/c7n/resources/ebs.py
+++ b/c7n/resources/ebs.py
@@ -31,7 +31,7 @@
 from c7n.manager import resources
 from c7n.resources.kms import ResourceKmsKeyAlias
 from c7n.query import QueryResourceManager, TypeInfo
-from c7n.tags import Tag
+from c7n.tags import Tag, coalesce_copy_user_tags
 from c7n.utils import (
     camelResource,
     chunks,
@@ -1164,7 +1164,20 @@ def _wait_on_resource(
 
 @EBS.action_registry.register('snapshot')
 class CreateSnapshot(BaseAction):
-    """Snapshot an EBS volume
+    """Snapshot an EBS volume.
+
+    Tags may be optionally added to the snapshot during creation.
+
+    - `copy-volume-tags` copies all the tags from the specified
+      volume to the corresponding snapshot.
+    - `copy-tags` copies the listed tags from each volume
+      to the snapshot.  This is mutually exclusive with
+      `copy-volume-tags`.
+    - `tags` allows new tags to be added to each snapshot.  If
+      no tags are specified, then the tag `custodian_snapshot`
+      is added.
+
+    The default behavior is `copy-volume-tags: true`.
 
     :example:
 
@@ -1180,11 +1193,14 @@ class CreateSnapshot(BaseAction):
                   - type: snapshot
                     copy-tags:
                       - Name
+                    tags:
+                        custodian_snapshot: True
     """
     schema = type_schema(
         'snapshot',
         **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}},
-           'copy-volume-tags': {'type': 'boolean'}})
+           'copy-volume-tags': {'type': 'boolean'},
+           'tags': {'type': 'object'}})
     permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',)
 
     def validate(self):
@@ -1212,19 +1228,9 @@ def process_volume(self, client, volume, tags):
             raise
 
     def get_snapshot_tags(self, resource):
-        tags = [
-            {'Key': 'custodian_snapshot', 'Value': ''}]
-        copy_keys = self.data.get('copy-tags', [])
-        copy_tags = []
-        if copy_keys:
-            for t in resource.get('Tags', []):
-                if t['Key'] in copy_keys:
-                    copy_tags.append(t)
-            tags.extend(copy_tags)
-            return tags
-        if self.data.get('copy-volume-tags', True):
-            tags.extend(resource.get('Tags', []))
-        return tags
+        user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''}
+        copy_tags = self.data.get('copy-tags', []) or self.data.get('copy-volume-tags', True)
+        return coalesce_copy_user_tags(resource, copy_tags, user_tags)
 
 
 @EBS.action_registry.register('delete')
diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py
--- a/c7n/resources/ec2.py
+++ b/c7n/resources/ec2.py
@@ -36,6 +36,7 @@
 
 from c7n.manager import resources
 from c7n import query, utils
+from c7n.tags import coalesce_copy_user_tags
 from c7n.utils import type_schema, filter_empty
 
 from c7n.resources.iam import CheckPermissions
@@ -1024,7 +1025,7 @@ def process(self, resources, event=None):
 class MonitorInstances(BaseAction, StateTransitionFilter):
     """Action on EC2 Instances to enable/disable detailed monitoring
 
-    The differents states of detailed monitoring status are :
+    The different states of detailed monitoring status are :
     'disabled'|'disabling'|'enabled'|'pending'
     (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances)
 
@@ -1384,7 +1385,7 @@ def _run_instances_op(self, op, instance_ids, **kwargs):
 
 @actions.register('reboot')
 class Reboot(BaseAction, StateTransitionFilter):
-    """reboots a previously running EC2 instance.
+    """Reboots a previously running EC2 instance.
 
     :Example:
 
@@ -1517,7 +1518,20 @@ def process_instance(i):
 
 @actions.register('snapshot')
 class Snapshot(BaseAction):
-    """Snapshots volumes attached to an EC2 instance
+    """Snapshot the volumes attached to an EC2 instance.
+
+    Tags may be optionally added to the snapshot during creation.
+
+    - `copy-volume-tags` copies all the tags from the specified
+      volume to the corresponding snapshot.
+    - `copy-tags` copies the listed tags from each volume
+      to the snapshot.  This is mutually exclusive with
+      `copy-volume-tags`.
+    - `tags` allows new tags to be added to each snapshot when using
+      'copy-tags`.  If no tags are specified, then the tag
+      `custodian_snapshot` is added.
+
+    The default behavior is `copy-volume-tags: true`.
 
     :Example:
 
@@ -1530,12 +1544,15 @@ class Snapshot(BaseAction):
               - type: snapshot
                 copy-tags:
                   - Name
+                tags:
+                    custodian_snapshot: True
     """
 
     schema = type_schema(
         'snapshot',
         **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}},
            'copy-volume-tags': {'type': 'boolean'},
+           'tags': {'type': 'object'},
            'exclude-boot': {'type': 'boolean', 'default': False}})
     permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',)
 
@@ -1590,16 +1607,9 @@ def process_volume_set(self, client, resource):
                 resource['InstanceId'], err_code)
 
     def get_snapshot_tags(self, resource):
-        tags = [
-            {'Key': 'custodian_snapshot', 'Value': ''}]
-        copy_keys = self.data.get('copy-tags', [])
-        copy_tags = []
-        if copy_keys:
-            for t in resource.get('Tags', []):
-                if t['Key'] in copy_keys:
-                    copy_tags.append(t)
-            tags.extend(copy_tags)
-        return tags
+        user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''}
+        copy_tags = self.data.get('copy-tags', [])
+        return coalesce_copy_user_tags(resource, copy_tags, user_tags)
 
 
 @actions.register('modify-security-groups')
@@ -1954,7 +1964,7 @@ def query(self):
 
 @filters.register('instance-attribute')
 class InstanceAttribute(ValueFilter):
-    """EC2 Instance Value FIlter on a given instance attribute.
+    """EC2 Instance Value Filter on a given instance attribute.
 
     Filters EC2 Instances with the given instance attribute
 


Test patch:
diff --git a/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.CreateSnapshot_1.json b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.CreateSnapshot_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.CreateSnapshot_1.json
@@ -0,0 +1,30 @@
+{
+  "status_code": 200,
+  "data": {
+    "Description": "",
+    "ResponseMetadata": {},
+    "Encrypted": false,
+    "VolumeId": "vol-0252f61378ede9d01",
+    "State": "pending",
+    "VolumeSize": 8,
+    "Progress": "",
+    "StartTime": {
+      "hour": 14,
+      "__class__": "datetime",
+      "month": 5,
+      "second": 17,
+      "microsecond": 0,
+      "year": 2017,
+      "day": 3,
+      "minute": 43
+    },
+    "SnapshotId": "snap-0c8b611065fed033e",
+    "Tags": [
+      {
+        "Key": "test-tag",
+        "Value": "custodian"
+      }
+    ],
+    "OwnerId": "644160558196"
+  }
+}
diff --git a/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeSnapshots_1.json b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeSnapshots_1.json
@@ -0,0 +1,34 @@
+{
+  "status_code": 200,
+  "data": {
+    "ResponseMetadata": {},
+    "Snapshots": [
+      {
+        "Description": "",
+        "Encrypted": false,
+        "VolumeId": "vol-0252f61378ede9d01",
+        "State": "pending",
+        "VolumeSize": 8,
+        "Progress": "99%",
+        "StartTime": {
+          "hour": 14,
+          "__class__": "datetime",
+          "month": 5,
+          "second": 17,
+          "microsecond": 0,
+          "year": 2017,
+          "day": 3,
+          "minute": 43
+        },
+        "SnapshotId": "snap-0c8b611065fed033e",
+        "Tags": [
+          {
+            "Key": "test-tag",
+            "Value": "custodian"
+          }
+        ],
+        "OwnerId": "644160558196"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeVolumes_1.json b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeVolumes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ebs_snapshot_copy_volume_tags/ec2.DescribeVolumes_1.json
@@ -0,0 +1,139 @@
+{
+  "status_code": 200,
+  "data": {
+    "ResponseMetadata": {},
+    "Volumes": [
+      {
+        "AvailabilityZone": "us-east-1b",
+        "Attachments": [
+          {
+            "AttachTime": {
+              "hour": 14,
+              "__class__": "datetime",
+              "month": 3,
+              "second": 28,
+              "microsecond": 0,
+              "year": 2017,
+              "day": 28,
+              "minute": 55
+            },
+            "InstanceId": "i-0a0b51bcf11a8cdfb",
+            "VolumeId": "vol-01adbb6a4f175941d",
+            "State": "attached",
+            "DeleteOnTermination": true,
+            "Device": "/dev/xvda"
+          }
+        ],
+        "Encrypted": false,
+        "VolumeType": "gp2",
+        "VolumeId": "vol-01adbb6a4f175941d",
+        "State": "in-use",
+        "Iops": 100,
+        "SnapshotId": "snap-037f1f9e6c8ea4d65",
+        "CreateTime": {
+          "hour": 14,
+          "__class__": "datetime",
+          "month": 3,
+          "second": 28,
+          "microsecond": 486000,
+          "year": 2017,
+          "day": 28,
+          "minute": 55
+        },
+        "Size": 8
+      },
+      {
+        "AvailabilityZone": "us-east-1b",
+        "Attachments": [
+          {
+            "AttachTime": {
+              "hour": 20,
+              "__class__": "datetime",
+              "month": 4,
+              "second": 21,
+              "microsecond": 0,
+              "year": 2017,
+              "day": 10,
+              "minute": 41
+            },
+            "InstanceId": "i-0be2fdca058278ea3",
+            "VolumeId": "vol-0252f61378ede9d01",
+            "State": "attached",
+            "DeleteOnTermination": true,
+            "Device": "/dev/sda1"
+          }
+        ],
+        "Encrypted": false,
+        "VolumeType": "gp2",
+        "VolumeId": "vol-0252f61378ede9d01",
+        "State": "in-use",
+        "Iops": 100,
+        "SnapshotId": "snap-00762b50998e9113b",
+        "CreateTime": {
+          "hour": 20,
+          "__class__": "datetime",
+          "month": 4,
+          "second": 21,
+          "microsecond": 977000,
+          "year": 2017,
+          "day": 10,
+          "minute": 41
+        },
+        "Size": 8,
+        "Tags": [
+          {
+            "Value": "FancyTestInstancePrivate",
+            "Key": "Name"
+          },
+          {
+            "Key": "Stage",
+            "Value": "Dev"
+          },
+          {
+            "Key": "DoNotCopy",
+            "Value": "ThisTag"
+          }
+        ]
+      },
+      {
+        "AvailabilityZone": "us-east-1d",
+        "Attachments": [
+          {
+            "AttachTime": {
+              "hour": 1,
+              "__class__": "datetime",
+              "month": 4,
+              "second": 40,
+              "microsecond": 0,
+              "year": 2017,
+              "day": 3,
+              "minute": 34
+            },
+            "InstanceId": "i-0aace5764d21d3105",
+            "VolumeId": "vol-0b5bc31549f55bdd2",
+            "State": "attached",
+            "DeleteOnTermination": true,
+            "Device": "/dev/xvda"
+          }
+        ],
+        "Encrypted": false,
+        "VolumeType": "gp2",
+        "VolumeId": "vol-0b5bc31549f55bdd2",
+        "State": "in-use",
+        "Iops": 100,
+        "SnapshotId": "snap-037f1f9e6c8ea4d65",
+        "CreateTime": {
+          "hour": 1,
+          "__class__": "datetime",
+          "month": 4,
+          "second": 40,
+          "microsecond": 677000,
+          "year": 2017,
+          "day": 3,
+          "minute": 34
+        },
+        "Size": 8
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ec2_snapshot_tags/ec2.CreateSnapshots_1.json b/tests/data/placebo/test_ec2_snapshot_tags/ec2.CreateSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ec2_snapshot_tags/ec2.CreateSnapshots_1.json
@@ -0,0 +1,110 @@
+{
+    "status_code": 200,
+    "data": {
+        "Snapshots": [
+            {
+                "Description": "",
+                "Tags": [
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    },
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    }
+                ],
+                "Encrypted": false,
+                "VolumeId": "vol-0caf24c45a8aa4b11",
+                "State": "pending",
+                "VolumeSize": 8,
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 0
+                },
+                "Progress": "",
+                "OwnerId": "644160558196",
+                "SnapshotId": "snap-07187bfeb49818609"
+            },
+            {
+                "Description": "",
+                "Tags": [
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    },
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    }
+                ],
+                "Encrypted": false,
+                "VolumeId": "vol-05117418b3ad30481",
+                "State": "pending",
+                "VolumeSize": 8,
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 0
+                },
+                "Progress": "",
+                "OwnerId": "644160558196",
+                "SnapshotId": "snap-051cf33e858fe7bf7"
+            },
+            {
+                "Description": "",
+                "Tags": [
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    },
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    }
+                ],
+                "Encrypted": false,
+                "VolumeId": "vol-0188a91cfcb95c342",
+                "State": "pending",
+                "VolumeSize": 8,
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 0
+                },
+                "Progress": "",
+                "OwnerId": "644160558196",
+                "SnapshotId": "snap-04589ad9a763d4cfd"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeInstances_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeInstances_1.json
@@ -0,0 +1,201 @@
+{
+  "status_code": 200,
+  "data": {
+    "Reservations": [
+      {
+        "Groups": [],
+        "Instances": [
+          {
+            "AmiLaunchIndex": 0,
+            "ImageId": "ami-07b4156579ea1d7ba",
+            "InstanceId": "i-063c6794763254b51",
+            "InstanceType": "t3.small",
+            "LaunchTime": {
+              "__class__": "datetime",
+              "year": 2019,
+              "month": 6,
+              "day": 11,
+              "hour": 20,
+              "minute": 56,
+              "second": 23,
+              "microsecond": 0
+            },
+            "Monitoring": {
+              "State": "disabled"
+            },
+            "Placement": {
+              "AvailabilityZone": "us-east-1a",
+              "GroupName": "",
+              "Tenancy": "default"
+            },
+            "PrivateDnsName": "ip-172-31-11-37.ec2.internal",
+            "PrivateIpAddress": "172.31.11.37",
+            "ProductCodes": [],
+            "PublicDnsName": "ec2-100-26-36-220.compute-1.amazonaws.com",
+            "PublicIpAddress": "100.26.36.220",
+            "State": {
+              "Code": 16,
+              "Name": "running"
+            },
+            "StateTransitionReason": "",
+            "SubnetId": "subnet-914763e7",
+            "VpcId": "vpc-d2d616b5",
+            "Architecture": "x86_64",
+            "BlockDeviceMappings": [
+              {
+                "DeviceName": "/dev/sda1",
+                "Ebs": {
+                  "AttachTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 20,
+                    "minute": 56,
+                    "second": 23,
+                    "microsecond": 0
+                  },
+                  "DeleteOnTermination": true,
+                  "Status": "attached",
+                  "VolumeId": "vol-0caf24c45a8aa4b11"
+                }
+              },
+              {
+                "DeviceName": "/dev/sdb",
+                "Ebs": {
+                  "AttachTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 20,
+                    "minute": 56,
+                    "second": 23,
+                    "microsecond": 0
+                  },
+                  "DeleteOnTermination": true,
+                  "Status": "attached",
+                  "VolumeId": "vol-0188a91cfcb95c342"
+                }
+              },
+              {
+                "DeviceName": "/dev/sdc",
+                "Ebs": {
+                  "AttachTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 20,
+                    "minute": 56,
+                    "second": 23,
+                    "microsecond": 0
+                  },
+                  "DeleteOnTermination": true,
+                  "Status": "attached",
+                  "VolumeId": "vol-05117418b3ad30481"
+                }
+              }
+            ],
+            "ClientToken": "",
+            "EbsOptimized": true,
+            "EnaSupport": true,
+            "Hypervisor": "xen",
+            "NetworkInterfaces": [
+              {
+                "Association": {
+                  "IpOwnerId": "amazon",
+                  "PublicDnsName": "ec2-100-26-36-220.compute-1.amazonaws.com",
+                  "PublicIp": "100.26.36.220"
+                },
+                "Attachment": {
+                  "AttachTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 20,
+                    "minute": 56,
+                    "second": 23,
+                    "microsecond": 0
+                  },
+                  "AttachmentId": "eni-attach-0125cf6988c1ec962",
+                  "DeleteOnTermination": true,
+                  "DeviceIndex": 0,
+                  "Status": "attached"
+                },
+                "Description": "",
+                "Groups": [
+                  {
+                    "GroupName": "default",
+                    "GroupId": "sg-6c7fa917"
+                  }
+                ],
+                "Ipv6Addresses": [],
+                "MacAddress": "0a:9f:f7:9f:2d:24",
+                "NetworkInterfaceId": "eni-00eb9a1bebb7a0d0c",
+                "OwnerId": "644160558196",
+                "PrivateDnsName": "ip-172-31-11-37.ec2.internal",
+                "PrivateIpAddress": "172.31.11.37",
+                "PrivateIpAddresses": [
+                  {
+                    "Association": {
+                      "IpOwnerId": "amazon",
+                      "PublicDnsName": "ec2-100-26-36-220.compute-1.amazonaws.com",
+                      "PublicIp": "100.26.36.220"
+                    },
+                    "Primary": true,
+                    "PrivateDnsName": "ip-172-31-11-37.ec2.internal",
+                    "PrivateIpAddress": "172.31.11.37"
+                  }
+                ],
+                "SourceDestCheck": true,
+                "Status": "in-use",
+                "SubnetId": "subnet-914763e7",
+                "VpcId": "vpc-d2d616b5",
+                "InterfaceType": "interface"
+              }
+            ],
+            "RootDeviceName": "/dev/sda1",
+            "RootDeviceType": "ebs",
+            "SecurityGroups": [
+              {
+                "GroupName": "default",
+                "GroupId": "sg-6c7fa917"
+              }
+            ],
+            "SourceDestCheck": true,
+            "Tags": [
+              {
+                "Key": "App",
+                "Value": "DevX"
+              },
+              {
+                "Key": "Stage",
+                "Value": "Dev"
+              },
+              {
+                "Key": "Name",
+                "Value": "Foo"
+              }
+            ],
+            "VirtualizationType": "hvm",
+            "CpuOptions": {
+              "CoreCount": 1,
+              "ThreadsPerCore": 2
+            },
+            "CapacityReservationSpecification": {
+              "CapacityReservationPreference": "open"
+            },
+            "HibernationOptions": {
+              "Configured": false
+            }
+          }
+        ],
+        "OwnerId": "644160558196",
+        "ReservationId": "r-02b0823521eeb99df"
+      }
+    ],
+    "ResponseMetadata": {}
+  }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeSnapshots_1.json b/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ec2_snapshot_tags/ec2.DescribeSnapshots_1.json
@@ -0,0 +1,110 @@
+{
+    "status_code": 200,
+    "data": {
+        "Snapshots": [
+            {
+                "Description": "",
+                "Encrypted": false,
+                "OwnerId": "644160558196",
+                "Progress": "99%",
+                "SnapshotId": "snap-051cf33e858fe7bf7",
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 343000
+                },
+                "State": "pending",
+                "VolumeId": "vol-05117418b3ad30481",
+                "VolumeSize": 8,
+                "Tags": [
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    },
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    }
+                ]
+            },
+            {
+                "Description": "",
+                "Encrypted": false,
+                "OwnerId": "644160558196",
+                "Progress": "99%",
+                "SnapshotId": "snap-04589ad9a763d4cfd",
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 343000
+                },
+                "State": "pending",
+                "VolumeId": "vol-0188a91cfcb95c342",
+                "VolumeSize": 8,
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    },
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    },
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    }
+                ]
+            },
+            {
+                "Description": "",
+                "Encrypted": false,
+                "OwnerId": "644160558196",
+                "Progress": "99%",
+                "SnapshotId": "snap-07187bfeb49818609",
+                "StartTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 6,
+                    "day": 11,
+                    "hour": 21,
+                    "minute": 25,
+                    "second": 25,
+                    "microsecond": 343000
+                },
+                "State": "pending",
+                "VolumeId": "vol-0caf24c45a8aa4b11",
+                "VolumeSize": 8,
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "Foo"
+                    },
+                    {
+                        "Key": "test-tag",
+                        "Value": "custodian"
+                    },
+                    {
+                        "Key": "Stage",
+                        "Value": "Dev"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_ebs.py b/tests/test_ebs.py
--- a/tests/test_ebs.py
+++ b/tests/test_ebs.py
@@ -500,6 +500,27 @@ def test_volume_snapshot_copy_tags(self):
         for s in snapshot_data['Snapshots']:
             self.assertEqual(rtags, {t['Key']: t['Value'] for t in s['Tags']})
 
+    def test_volume_snapshot_copy_volume_tags(self):
+        factory = self.replay_flight_data("test_ebs_snapshot_copy_volume_tags")
+        policy = self.load_policy(
+            {
+                "name": "ebs-test-snapshot",
+                "resource": "ebs",
+                "filters": [{"VolumeId": "vol-0252f61378ede9d01"}],
+                "actions": [{"type": "snapshot",
+                             "copy-volume-tags": False,
+                             "tags": {'test-tag': 'custodian'}}]
+            },
+            session_factory=factory,
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        snapshot_data = factory().client("ec2").describe_snapshots(
+            Filters=[{"Name": "volume-id", "Values": ["vol-0252f61378ede9d01"]}]
+        )
+        for s in snapshot_data['Snapshots']:
+            self.assertEqual({'test-tag': 'custodian'}, {t['Key']: t['Value'] for t in s['Tags']})
+
 
 class VolumeDeleteTest(BaseTest):
 
diff --git a/tests/test_ec2.py b/tests/test_ec2.py
--- a/tests/test_ec2.py
+++ b/tests/test_ec2.py
@@ -1148,6 +1148,30 @@ def test_ec2_snapshot_copy_tags(self):
         for s in snaps:
             self.assertEqual(rtags, {t['Key']: t['Value'] for t in s['Tags']})
 
+    def test_ec2_snapshot_tags(self):
+        session_factory = self.replay_flight_data("test_ec2_snapshot_tags")
+        policy = self.load_policy(
+            {
+                "name": "ec2-test-snapshot",
+                "resource": "ec2",
+                "filters": [{"tag:Name": "Foo"}],
+                "actions": [{"type": "snapshot", "copy-tags": ['Name', 'Stage'],
+                             "tags": {"test-tag": 'custodian'}}]
+            },
+            session_factory=session_factory,
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+
+        client = session_factory().client('ec2')
+        snaps = client.describe_snapshots(
+            SnapshotIds=resources[0]['c7n:snapshots']).get('Snapshots')
+        rtags = {t['Key']: t['Value'] for t in resources[0]['Tags']}
+        rtags.pop('App')
+        rtags['test-tag'] = 'custodian'
+        for s in snaps:
+            self.assertEqual(rtags, {t['Key']: t['Value'] for t in s['Tags']})
+
 
 class TestSetInstanceProfile(BaseTest):