Repository: getmoto/moto

Issue:
False positive: AMI ID missing warning when using a LaunchTemplate
I am using an AMI LaunchTemplate that contains an AMI ID from the `ec2_backend.describe_images()` helper:

```
for ami in ec2_backend.describe_images():
    if ami.description.startswith("Canonical, Ubuntu, 16.04 LTS") and ami.architecture == "x86_64":
        break

ec2_client = boto3.client("ec2")
ec2_resource = boto3.resource("ec2")

template = ec2_client.create_launch_template(
    LaunchTemplateName="test",
    LaunchTemplateData=dict(ImageId=ami.id, InstanceType="m3.medium"),
)["LaunchTemplate"]

instances = ec2_resource.create_instances(
    LaunchTemplate=template,
    MaxCount=1,
    MinCount=1
)
```

Here, the "create_instances" call will trigger a warning about the `Image Id` being None:

```
PendingDeprecationWarning: Could not find AMI with image-id:None, in the near future this will cause an error.
```

Expected result: Moto should not complain since the AMI ID in the template matches the one in the "ec2_backend".


Discussion:
After some tracing I can see that [this piece of code](https://github.com/spulec/moto/blob/45b2684eb6024d2b2bfa901afe651d565b25767b/moto/ec2/responses/instances.py#L48) seems to only care about the `ImageId` param passed to the `create_instances` call, and doesn't try to see if a template was specified and if it specifies the ami id.

However, the template definition passed to the call doesn't include the AMI ID specified inside the template. So the code would have to pull the template using its template id and check the AMI ID there; I'm not sure how to do that, but with some guidance I can probably do a PR.


Hi @jonapich, thanks for raising this! A PR would be very welcome if you want to fix this.

From what I can tell, this would be the best way of approaching this:

1. Retrieve the `LaunchTemplate`-parameter here: https://github.com/spulec/moto/blob/45b2684eb6024d2b2bfa901afe651d565b25767b/moto/ec2/responses/instances.py#L48 (either using `self._get_param(..)` or `self._get_dict_param(..)`, depending on the format if the input)
2. Pass the launch template into `add_instances` (https://github.com/spulec/moto/blob/45b2684eb6024d2b2bfa901afe651d565b25767b/moto/ec2/models.py), and into the `Instance`-class (https://github.com/spulec/moto/blob/45b2684eb6024d2b2bfa901afe651d565b25767b/moto/ec2/models.py#L511)
3. In the Instance-constructor, it should be possible to then retrieve the necessary AMI from the launch template:
```
self.launch_template_args = kwargs.get("launch_template")
self.launch_template = self.ec2_backend.get_launch_template(template_id=self.launch_template_args[".."])
if not self.image_id:
  self.image_id = self.launch_template.get("ImageId")
```
Note: this is partial pseudocode, just to give an idea - hopefully it's enough to get you started. Let me know if you have any other questions, I'd be happy to dive a little deeper into this.

Resolving patch:
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -572,7 +572,24 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
         self.ec2_backend = ec2_backend
         self.id = random_instance_id()
         self.lifecycle = kwargs.get("lifecycle")
-        self.image_id = image_id
+
+        launch_template_arg = kwargs.get("launch_template", {})
+        if launch_template_arg and not image_id:
+            # the image id from the template should be used
+            template = (
+                ec2_backend.describe_launch_templates(
+                    template_ids=[launch_template_arg["LaunchTemplateId"]]
+                )[0]
+                if "LaunchTemplateId" in launch_template_arg
+                else ec2_backend.describe_launch_templates(
+                    template_names=[launch_template_arg["LaunchTemplateName"]]
+                )[0]
+            )
+            version = launch_template_arg.get("Version", template.latest_version_number)
+            self.image_id = template.get_version(int(version)).image_id
+        else:
+            self.image_id = image_id
+
         self._state = InstanceState("running", 16)
         self._reason = ""
         self._state_reason = StateReason()
@@ -600,7 +617,7 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
             # If we are in EC2-Classic, autoassign a public IP
             self.associate_public_ip = True
 
-        amis = self.ec2_backend.describe_images(filters={"image-id": image_id})
+        amis = self.ec2_backend.describe_images(filters={"image-id": self.image_id})
         ami = amis[0] if amis else None
         if ami is None:
             warnings.warn(
@@ -608,7 +625,7 @@ def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
                 "in the near future this will "
                 "cause an error.\n"
                 "Use ec2_backend.describe_images() to "
-                "find suitable image for your test".format(image_id),
+                "find suitable image for your test".format(self.image_id),
                 PendingDeprecationWarning,
             )
 
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -69,6 +69,7 @@ def run_instances(self):
             "instance_initiated_shutdown_behavior": self._get_param(
                 "InstanceInitiatedShutdownBehavior"
             ),
+            "launch_template": self._get_multi_param_dict("LaunchTemplate"),
         }
 
         mappings = self._parse_block_device_mapping()


Test patch:
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -2931,3 +2931,30 @@ def test_instance_lifecycle():
     instance = result[0]
 
     assert instance.instance_lifecycle is None
+
+
+@mock_ec2
+@pytest.mark.parametrize(
+    "launch_template_kind", ("LaunchTemplateId", "LaunchTemplateName")
+)
+def test_create_instance_with_launch_template_id_produces_no_warning(
+    launch_template_kind,
+):
+    client, resource = (
+        boto3.client("ec2", region_name="us-west-1"),
+        boto3.resource("ec2", region_name="us-west-1"),
+    )
+
+    template = client.create_launch_template(
+        LaunchTemplateName="test-template",
+        LaunchTemplateData={"ImageId": EXAMPLE_AMI_ID},
+    )["LaunchTemplate"]
+
+    with pytest.warns(None) as captured_warnings:
+        resource.create_instances(
+            MinCount=1,
+            MaxCount=1,
+            LaunchTemplate={launch_template_kind: template[launch_template_kind]},
+        )
+
+    assert len(captured_warnings) == 0