diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx
index c7806e649e7..a46ec3d642f 100644
--- a/packages/grafana-ui/src/components/Forms/InlineField.tsx
+++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx
@@ -23,6 +23,8 @@ export interface Props extends Omit<FieldProps, 'css' | 'horizontal' | 'descript
   /** Error message to display */
   error?: string | null;
   htmlFor?: string;
+  /** Make tooltip interactive */
+  interactive?: boolean;
 }
 
 export const InlineField: FC<Props> = ({
@@ -38,6 +40,7 @@ export const InlineField: FC<Props> = ({
   grow,
   error,
   transparent,
+  interactive,
   ...htmlProps
 }) => {
   const theme = useTheme2();
@@ -46,7 +49,13 @@ export const InlineField: FC<Props> = ({
 
   const labelElement =
     typeof label === 'string' ? (
-      <InlineLabel width={labelWidth} tooltip={tooltip} htmlFor={inputId} transparent={transparent}>
+      <InlineLabel
+        interactive={interactive}
+        width={labelWidth}
+        tooltip={tooltip}
+        htmlFor={inputId}
+        transparent={transparent}
+      >
         {label}
       </InlineLabel>
     ) : (
diff --git a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx
index 03f1045c109..32e21247969 100644
--- a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx
+++ b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx
@@ -23,6 +23,8 @@ export interface Props extends Omit<LabelProps, 'css' | 'description' | 'categor
   /** @deprecated */
   /** This prop is deprecated and is not used anymore */
   isInvalid?: boolean;
+  /** Make tooltip interactive */
+  interactive?: boolean;
   /** @beta */
   /** Controls which element the InlineLabel should be rendered into */
   as?: React.ElementType;
@@ -34,6 +36,7 @@ export const InlineLabel: FunctionComponent<Props> = ({
   tooltip,
   width,
   transparent,
+  interactive,
   as: Component = 'label',
   ...rest
 }) => {
@@ -43,7 +46,7 @@ export const InlineLabel: FunctionComponent<Props> = ({
     <Component className={cx(styles.label, className)} {...rest}>
       {children}
       {tooltip && (
-        <Tooltip placement="top" content={tooltip} theme="info">
+        <Tooltip interactive={interactive} placement="top" content={tooltip} theme="info">
           <Icon tabIndex={0} name="info-circle" size="sm" className={styles.icon} />
         </Tooltip>
       )}
diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
index 7cc2ab389e1..5ce973cca66 100644
--- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
+++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
@@ -9,7 +9,11 @@ import { CustomVariableModel } from 'app/features/variables/types';
 import { TemplateSrvMock } from '../../../../features/templating/template_srv.mock';
 import { CloudWatchDatasource } from '../datasource';
 
-export function setupMockedDataSource({ data = [], variables }: { data?: any; variables?: any } = {}) {
+export function setupMockedDataSource({
+  data = [],
+  variables,
+  mockGetVariableName = true,
+}: { data?: any; variables?: any; mockGetVariableName?: boolean } = {}) {
   let templateService = new TemplateSrvMock({
     region: 'templatedRegion',
     fields: 'templatedField',
@@ -19,7 +23,9 @@ export function setupMockedDataSource({ data = [], variables }: { data?: any; va
     templateService = new TemplateSrv();
     templateService.init(variables);
     templateService.getVariables = jest.fn().mockReturnValue(variables);
-    templateService.getVariableName = (name: string) => name;
+    if (mockGetVariableName) {
+      templateService.getVariableName = (name: string) => name;
+    }
   }
 
   const datasource = new CloudWatchDatasource(
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.test.tsx
new file mode 100644
index 00000000000..b1050541aec
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.test.tsx
@@ -0,0 +1,109 @@
+import { fireEvent, render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import { MultiFilter } from './MultiFilter';
+
+describe('MultiFilters', () => {
+  describe('when rendered with two existing multifilters', () => {
+    it('should render two filter items', async () => {
+      const filters = {
+        InstanceId: ['a', 'b'],
+        InstanceGroup: ['Group1'],
+      };
+      const onChange = jest.fn();
+      render(<MultiFilter filters={filters} onChange={onChange} />);
+      const filterItems = screen.getAllByTestId('cloudwatch-multifilter-item');
+      expect(filterItems.length).toBe(2);
+
+      expect(within(filterItems[0]).getByDisplayValue('InstanceId')).toBeInTheDocument();
+      expect(within(filterItems[0]).getByDisplayValue('a, b')).toBeInTheDocument();
+
+      expect(within(filterItems[1]).getByDisplayValue('InstanceGroup')).toBeInTheDocument();
+      expect(within(filterItems[1]).getByDisplayValue('Group1')).toBeInTheDocument();
+    });
+  });
+
+  describe('when adding a new filter item', () => {
+    it('it should add the new item but not call onChange', async () => {
+      const filters = {};
+      const onChange = jest.fn();
+      render(<MultiFilter filters={filters} onChange={onChange} />);
+
+      await userEvent.click(screen.getByLabelText('Add'));
+      expect(screen.getByTestId('cloudwatch-multifilter-item')).toBeInTheDocument();
+      expect(onChange).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('when adding a new filter item with key', () => {
+    it('it should add the new item but not call onChange', async () => {
+      const filters = {};
+      const onChange = jest.fn();
+      render(<MultiFilter filters={filters} onChange={onChange} />);
+
+      await userEvent.click(screen.getByLabelText('Add'));
+      const filterItemElement = screen.getByTestId('cloudwatch-multifilter-item');
+      expect(filterItemElement).toBeInTheDocument();
+
+      const keyElement = screen.getByTestId('cloudwatch-multifilter-item-key');
+      expect(keyElement).toBeInTheDocument();
+      await userEvent.type(keyElement!, 'my-key');
+      fireEvent.blur(keyElement!);
+
+      expect(within(filterItemElement).getByDisplayValue('my-key')).toBeInTheDocument();
+      expect(onChange).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('when adding a new filter item with key and value', () => {
+    it('it should add the new item and trigger onChange', async () => {
+      const filters = {};
+      const onChange = jest.fn();
+      render(<MultiFilter filters={filters} onChange={onChange} />);
+
+      const label = await screen.findByLabelText('Add');
+      await userEvent.click(label);
+      const filterItemElement = screen.getByTestId('cloudwatch-multifilter-item');
+      expect(filterItemElement).toBeInTheDocument();
+
+      const keyElement = screen.getByTestId('cloudwatch-multifilter-item-key');
+      expect(keyElement).toBeInTheDocument();
+      await userEvent.type(keyElement!, 'my-key');
+      fireEvent.blur(keyElement!);
+      expect(within(filterItemElement).getByDisplayValue('my-key')).toBeInTheDocument();
+      expect(onChange).not.toHaveBeenCalled();
+
+      const valueElement = screen.getByTestId('cloudwatch-multifilter-item-value');
+      expect(valueElement).toBeInTheDocument();
+      await userEvent.type(valueElement!, 'my-value1,my-value2');
+      fireEvent.blur(valueElement!);
+      expect(within(filterItemElement).getByDisplayValue('my-value1, my-value2')).toBeInTheDocument();
+      expect(onChange).toHaveBeenCalledWith({
+        'my-key': ['my-value1', 'my-value2'],
+      });
+    });
+  });
+  describe('when editing an existing filter item key', () => {
+    it('it should change the key and call onChange', async () => {
+      const filters = { 'my-key': ['my-value'] };
+      const onChange = jest.fn();
+      render(<MultiFilter filters={filters} onChange={onChange} />);
+
+      const filterItemElement = screen.getByTestId('cloudwatch-multifilter-item');
+      expect(filterItemElement).toBeInTheDocument();
+      expect(within(filterItemElement).getByDisplayValue('my-key')).toBeInTheDocument();
+      expect(within(filterItemElement).getByDisplayValue('my-value')).toBeInTheDocument();
+
+      const keyElement = screen.getByTestId('cloudwatch-multifilter-item-key');
+      expect(keyElement).toBeInTheDocument();
+      await userEvent.type(keyElement!, '2');
+      fireEvent.blur(keyElement!);
+
+      expect(within(filterItemElement).getByDisplayValue('my-key2')).toBeInTheDocument();
+      expect(onChange).toHaveBeenCalledWith({
+        'my-key2': ['my-value'],
+      });
+    });
+  });
+});
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.tsx
new file mode 100644
index 00000000000..eddb3d2aedf
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilter.tsx
@@ -0,0 +1,68 @@
+import { isEqual } from 'lodash';
+import React, { useEffect, useState } from 'react';
+
+import { EditorList } from '@grafana/experimental';
+
+import { MultiFilters } from '../../types';
+
+import { MultiFilterItem } from './MultiFilterItem';
+
+export interface Props {
+  filters?: MultiFilters;
+  onChange: (filters: MultiFilters) => void;
+  keyPlaceholder?: string;
+}
+
+export interface MultiFilterCondition {
+  key?: string;
+  operator?: string;
+  value?: string[];
+}
+
+const multiFiltersToFilterConditions = (filters: MultiFilters) =>
+  Object.keys(filters).map((key) => ({ key, value: filters[key], operator: '=' }));
+
+const filterConditionsToMultiFilters = (filters: MultiFilterCondition[]) => {
+  const res: MultiFilters = {};
+  filters.forEach(({ key, value }) => {
+    if (key && value) {
+      res[key] = value;
+    }
+  });
+  return res;
+};
+
+export const MultiFilter: React.FC<Props> = ({ filters, onChange, keyPlaceholder }) => {
+  const [items, setItems] = useState<MultiFilterCondition[]>([]);
+  useEffect(() => setItems(filters ? multiFiltersToFilterConditions(filters) : []), [filters]);
+  const onFiltersChange = (newItems: Array<Partial<MultiFilterCondition>>) => {
+    setItems(newItems);
+
+    // The onChange event should only be triggered in the case there is a complete dimension object.
+    // So when a new key is added that does not yet have a value, it should not trigger an onChange event.
+    const newMultifilters = filterConditionsToMultiFilters(newItems);
+    if (!isEqual(newMultifilters, filters)) {
+      onChange(newMultifilters);
+    }
+  };
+
+  return <EditorList items={items} onChange={onFiltersChange} renderItem={makeRenderFilter(keyPlaceholder)} />;
+};
+
+function makeRenderFilter(keyPlaceholder?: string) {
+  function renderFilter(
+    item: MultiFilterCondition,
+    onChange: (item: MultiFilterCondition) => void,
+    onDelete: () => void
+  ) {
+    return (
+      <MultiFilterItem
+        filter={item}
+        onChange={(item) => onChange(item)}
+        onDelete={onDelete}
+        keyPlaceholder={keyPlaceholder}
+      />
+    );
+  }
+  return renderFilter;
+}
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilterItem.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilterItem.tsx
new file mode 100644
index 00000000000..a4ccf64492c
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/MultiFilterItem.tsx
@@ -0,0 +1,67 @@
+import { css, cx } from '@emotion/css';
+import React, { FunctionComponent, useState } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { AccessoryButton, InputGroup } from '@grafana/experimental';
+import { Input, stylesFactory, useTheme2 } from '@grafana/ui';
+
+import { MultiFilterCondition } from './MultiFilter';
+
+export interface Props {
+  filter: MultiFilterCondition;
+  onChange: (value: MultiFilterCondition) => void;
+  onDelete: () => void;
+  keyPlaceholder?: string;
+}
+
+export const MultiFilterItem: FunctionComponent<Props> = ({ filter, onChange, onDelete, keyPlaceholder }) => {
+  const [localKey, setLocalKey] = useState(filter.key || '');
+  const [localValue, setLocalValue] = useState(filter.value?.join(', ') || '');
+  const theme = useTheme2();
+  const styles = getOperatorStyles(theme);
+
+  return (
+    <div data-testid="cloudwatch-multifilter-item">
+      <InputGroup>
+        <Input
+          data-testid="cloudwatch-multifilter-item-key"
+          aria-label="Filter key"
+          value={localKey}
+          placeholder={keyPlaceholder ?? 'key'}
+          onChange={(e) => setLocalKey(e.currentTarget.value)}
+          onBlur={() => {
+            if (localKey && localKey !== filter.key) {
+              onChange({ ...filter, key: localKey });
+            }
+          }}
+        />
+
+        <span className={cx(styles.root)}>=</span>
+
+        <Input
+          data-testid="cloudwatch-multifilter-item-value"
+          aria-label="Filter value"
+          value={localValue}
+          placeholder="value1, value2,..."
+          onChange={(e) => setLocalValue(e.currentTarget.value)}
+          onBlur={() => {
+            const newValues = localValue.split(',').map((v) => v.trim());
+            if (localValue && newValues !== filter.value) {
+              onChange({ ...filter, value: newValues });
+            }
+            setLocalValue(newValues.join(', '));
+          }}
+        />
+
+        <AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
+      </InputGroup>
+    </div>
+  );
+};
+
+const getOperatorStyles = stylesFactory((theme: GrafanaTheme2) => ({
+  root: css({
+    padding: theme.spacing(0, 1),
+    alignSelf: 'center',
+  }),
+}));
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx
index fffb71a724a..838c719fc1e 100644
--- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx
@@ -1,4 +1,5 @@
-import { render, screen, waitFor, within } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { select } from 'react-select-event';
 
@@ -13,11 +14,9 @@ const defaultQuery = {
   region: '',
   metricName: '',
   dimensionKey: '',
-  ec2Filters: '',
   instanceID: '',
   attributeName: '',
   resourceType: '',
-  tags: '',
   refId: '',
 };
 
@@ -40,7 +39,7 @@ ds.datasource.getMetrics = jest.fn().mockResolvedValue([
 ]);
 ds.datasource.getDimensionKeys = jest
   .fn()
-  .mockImplementation((namespace: string, region: string, dimensionFilters?: Dimensions) => {
+  .mockImplementation((_namespace: string, region: string, dimensionFilters?: Dimensions) => {
     if (!!dimensionFilters) {
       return Promise.resolve([
         { label: 's4', value: 's4' },
@@ -61,6 +60,7 @@ ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([
   { label: 'bar', value: 'bar' },
 ]);
 ds.datasource.getVariables = jest.fn().mockReturnValue([]);
+ds.datasource.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
 
 const onChange = jest.fn();
 const defaultProps: Props = {
@@ -167,6 +167,40 @@ describe('VariableEditor', () => {
         dimensionFilters: { v4: 'bar' },
       });
     });
+    it('should parse multiFilters correctly', async () => {
+      const props = defaultProps;
+      props.query = {
+        ...defaultQuery,
+        queryType: VariableQueryType.EC2InstanceAttributes,
+        region: 'a1',
+        attributeName: 'Tags.blah',
+        ec2Filters: { s4: ['foo', 'bar'] },
+      };
+      render(<VariableQueryEditor {...props} />);
+
+      await waitFor(() => {
+        expect(screen.getByDisplayValue('Tags.blah')).toBeInTheDocument();
+      });
+
+      const filterItem = screen.getByTestId('cloudwatch-multifilter-item');
+      expect(filterItem).toBeInTheDocument();
+      expect(within(filterItem).getByDisplayValue('foo, bar')).toBeInTheDocument();
+
+      // set filter value
+      const valueElement = screen.getByTestId('cloudwatch-multifilter-item-value');
+      expect(valueElement).toBeInTheDocument();
+      await userEvent.type(valueElement!, ',baz');
+      fireEvent.blur(valueElement!);
+
+      expect(screen.getByDisplayValue('foo, bar, baz')).toBeInTheDocument();
+      expect(onChange).toHaveBeenCalledWith({
+        ...defaultQuery,
+        queryType: VariableQueryType.EC2InstanceAttributes,
+        region: 'a1',
+        attributeName: 'Tags.blah',
+        ec2Filters: { s4: ['foo', 'bar', 'baz'] },
+      });
+    });
   });
   describe('and a different region is selected', () => {
     it('should clear invalid fields', async () => {
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx
index b7440e78ca9..0ccc64b7781 100644
--- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx
@@ -9,6 +9,7 @@ import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../h
 import { migrateVariableQuery } from '../../migrations';
 import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
 
+import { MultiFilter } from './MultiFilter';
 import { VariableQueryField } from './VariableQueryField';
 import { VariableTextField } from './VariableTextField';
 
@@ -165,14 +166,44 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
             placeholder="attribute name"
             onBlur={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
             label="Attribute Name"
+            interactive={true}
+            tooltip={
+              <>
+                {'Attribute or tag to query on. Tags should be formatted "Tags.<name>". '}
+                <a
+                  href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
+                  target="_blank"
+                  rel="noreferrer"
+                >
+                  See the documentation for more details
+                </a>
+              </>
+            }
           />
-          <VariableTextField
-            value={parsedQuery.ec2Filters}
-            tooltip='A JSON object representing dimensions/tags and the values to filter on. Ex. { "filter_name": [ "filter_value" ], "tag:name": [ "*" ] }'
-            placeholder='{"key":["value"]}'
-            onBlur={(value: string) => onQueryChange({ ...parsedQuery, ec2Filters: value })}
+          <InlineField
             label="Filters"
-          />
+            labelWidth={20}
+            tooltip={
+              <>
+                <a
+                  href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
+                  target="_blank"
+                  rel="noreferrer"
+                >
+                  Pre-defined ec2:DescribeInstances filters/tags
+                </a>
+                {' and the values to filter on. Tags should be formatted tag:<name>.'}
+              </>
+            }
+          >
+            <MultiFilter
+              filters={parsedQuery.ec2Filters}
+              onChange={(filters) => {
+                onChange({ ...parsedQuery, ec2Filters: filters });
+              }}
+              keyPlaceholder="filter/tag"
+            />
+          </InlineField>
         </>
       )}
       {parsedQuery.queryType === VariableQueryType.ResourceArns && (
@@ -183,12 +214,15 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
             onBlur={(value: string) => onQueryChange({ ...parsedQuery, resourceType: value })}
             label="Resource Type"
           />
-          <VariableTextField
-            value={parsedQuery.tags}
-            placeholder='{"tag":["value"]}'
-            onBlur={(value: string) => onQueryChange({ ...parsedQuery, tags: value })}
-            label="Tags"
-          />
+          <InlineField label="Tags" labelWidth={20} tooltip="Tags to filter the returned values on.">
+            <MultiFilter
+              filters={parsedQuery.tags}
+              onChange={(filters) => {
+                onChange({ ...parsedQuery, tags: filters });
+              }}
+              keyPlaceholder="tag"
+            />
+          </InlineField>
         </>
       )}
     </>
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx
index 2e095bb4f00..693c9776e8b 100644
--- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx
@@ -1,6 +1,6 @@
 import React, { FC, useState } from 'react';
 
-import { InlineField, Input } from '@grafana/ui';
+import { InlineField, Input, PopoverContent } from '@grafana/ui';
 
 const LABEL_WIDTH = 20;
 
@@ -9,13 +9,21 @@ interface VariableTextFieldProps {
   placeholder: string;
   value: string;
   label: string;
-  tooltip?: string;
+  tooltip?: PopoverContent;
+  interactive?: boolean;
 }
 
-export const VariableTextField: FC<VariableTextFieldProps> = ({ label, onBlur, placeholder, value, tooltip }) => {
+export const VariableTextField: FC<VariableTextFieldProps> = ({
+  interactive,
+  label,
+  onBlur,
+  placeholder,
+  value,
+  tooltip,
+}) => {
   const [localValue, setLocalValue] = useState(value);
   return (
-    <InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip} grow>
+    <InlineField interactive={interactive} label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip} grow>
       <Input
         aria-label={label}
         placeholder={placeholder}
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts
index c4baf75a911..e8f8619fd95 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts
@@ -5,6 +5,7 @@ import { ArrayVector, DataFrame, dataFrameToJSON, dateTime, Field, MutableDataFr
 import { setDataSourceSrv } from '@grafana/runtime';
 
 import {
+  dimensionVariable,
   labelsVariable,
   limitVariable,
   metricVariable,
@@ -398,6 +399,19 @@ describe('datasource', () => {
     });
   });
 
+  describe('convertMultiFiltersFormat', () => {
+    const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false });
+    it('converts keys and values correctly', () => {
+      // the json in this line doesn't matter, but it makes sure that old queries will be parsed
+      const filters = { $dimension: ['b'], a: ['${labels:json}', 'bar'] };
+      const result = ds.datasource.convertMultiFilterFormat(filters);
+      expect(result).toStrictEqual({
+        env: ['b'],
+        a: ['InstanceId', 'InstanceType', 'bar'],
+      });
+    });
+  });
+
   describe('getLogGroupFields', () => {
     it('passes region correctly', async () => {
       const { datasource, fetchMock } = setupMockedDataSource();
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index 76e25387841..38eb2226efc 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -54,6 +54,7 @@ import {
   MetricQuery,
   MetricQueryType,
   MetricRequest,
+  MultiFilters,
   StartQueryRequest,
   TSDBResponse,
 } from './types';
@@ -125,7 +126,7 @@ export class CloudWatchDatasource
     this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
     this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
     this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
-    this.variables = new CloudWatchVariableSupport(this, this.templateSrv);
+    this.variables = new CloudWatchVariableSupport(this);
     this.annotations = CloudWatchAnnotationSupport;
   }
 
@@ -756,7 +757,7 @@ export class CloudWatchDatasource
     return this.doMetricResourceRequest('ec2-instance-attribute', {
       region: this.templateSrv.replace(this.getActualRegion(region)),
       attributeName: this.templateSrv.replace(attributeName),
-      filters: JSON.stringify(filters),
+      filters: JSON.stringify(this.convertMultiFilterFormat(filters, 'filter key')),
     });
   }
 
@@ -764,7 +765,7 @@ export class CloudWatchDatasource
     return this.doMetricResourceRequest('resource-arns', {
       region: this.templateSrv.replace(this.getActualRegion(region)),
       resourceType: this.templateSrv.replace(resourceType),
-      tags: JSON.stringify(tags),
+      tags: JSON.stringify(this.convertMultiFilterFormat(tags, 'tag name')),
     });
   }
 
@@ -826,18 +827,40 @@ export class CloudWatchDatasource
         return { ...result, [key]: null };
       }
 
-      const valueVar = this.templateSrv
-        .getVariables()
-        .find(({ name }) => name === this.templateSrv.getVariableName(value));
-      if (valueVar) {
-        if ((valueVar as unknown as VariableWithMultiSupport).multi) {
-          const values = this.templateSrv.replace(value, scopedVars, 'pipe').split('|');
-          return { ...result, [key]: values };
-        }
-        return { ...result, [key]: [this.templateSrv.replace(value, scopedVars)] };
-      }
+      const newValues = this.getVariableValue(value, scopedVars);
+      return { ...result, [key]: newValues };
+    }, {});
+  }
 
-      return { ...result, [key]: [value] };
+  // get the value for a given template variable
+  getVariableValue(value: string, scopedVars: ScopedVars): string[] {
+    const variableName = this.templateSrv.getVariableName(value);
+    const valueVar = this.templateSrv.getVariables().find(({ name }) => {
+      return name === variableName;
+    });
+    if (variableName && valueVar) {
+      if ((valueVar as unknown as VariableWithMultiSupport).multi) {
+        // rebuild the variable name to handle old migrated queries
+        const values = this.templateSrv.replace('$' + variableName, scopedVars, 'pipe').split('|');
+        return values;
+      }
+      return [this.templateSrv.replace(value, scopedVars)];
+    }
+    return [value];
+  }
+
+  convertMultiFilterFormat(multiFilters: MultiFilters, fieldName?: string) {
+    return Object.entries(multiFilters).reduce((result, [key, values]) => {
+      key = this.replace(key, {}, true, fieldName);
+      if (!values) {
+        return { ...result, [key]: null };
+      }
+      const initialVal: string[] = [];
+      const newValues = values.reduce((result, value) => {
+        const vals = this.getVariableValue(value, {});
+        return [...result, ...vals];
+      }, initialVal);
+      return { ...result, [key]: newValues };
     }, {});
   }
 
diff --git a/public/app/plugins/datasource/cloudwatch/migration.test.ts b/public/app/plugins/datasource/cloudwatch/migration.test.ts
index 3d6ce76cea8..271fc54ca90 100644
--- a/public/app/plugins/datasource/cloudwatch/migration.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/migration.test.ts
@@ -12,6 +12,7 @@ import {
   MetricEditorMode,
   MetricQueryType,
   VariableQueryType,
+  OldVariableQuery,
 } from './types';
 
 describe('migration', () => {
@@ -231,11 +232,45 @@ describe('migration', () => {
   });
   describe('when resource_arns query is used', () => {
     it('should parse the query', () => {
-      const query = migrateVariableQuery('resource_arns(us-east-1,rds:db,{"environment":["$environment"]})');
+      const query = migrateVariableQuery(
+        'resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})'
+      );
       expect(query.queryType).toBe(VariableQueryType.ResourceArns);
+      expect(query.region).toBe('eu-west-1');
+      expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
+      expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
+    });
+  });
+  describe('when ec2_instance_attribute query is used', () => {
+    it('should parse the query', () => {
+      const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,{"environment":["$environment"]})');
+      expect(query.queryType).toBe(VariableQueryType.EC2InstanceAttributes);
       expect(query.region).toBe('us-east-1');
-      expect(query.resourceType).toBe('rds:db');
-      expect(query.tags).toBe('{"environment":["$environment"]}');
+      expect(query.attributeName).toBe('rds:db');
+      expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
+    });
+  });
+  describe('when OldVariableQuery is used', () => {
+    it('should parse the query', () => {
+      const oldQuery: OldVariableQuery = {
+        queryType: VariableQueryType.EC2InstanceAttributes,
+        namespace: '',
+        region: 'us-east-1',
+        metricName: '',
+        dimensionKey: '',
+        ec2Filters: '{"environment":["$environment"]}',
+        instanceID: '',
+        attributeName: 'rds:db',
+        resourceType: 'elasticloadbalancing:loadbalancer',
+        tags: '{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]}',
+        refId: '',
+      };
+      const query = migrateVariableQuery(oldQuery);
+      expect(query.region).toBe('us-east-1');
+      expect(query.attributeName).toBe('rds:db');
+      expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
+      expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
+      expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
     });
   });
 });
diff --git a/public/app/plugins/datasource/cloudwatch/migrations.ts b/public/app/plugins/datasource/cloudwatch/migrations.ts
index 1c13479fc94..88650a2246d 100644
--- a/public/app/plugins/datasource/cloudwatch/migrations.ts
+++ b/public/app/plugins/datasource/cloudwatch/migrations.ts
@@ -1,3 +1,5 @@
+import { omit } from 'lodash';
+
 import { AnnotationQuery, DataQuery } from '@grafana/data';
 import { getNextRefIdChar } from 'app/core/utils/query';
 
@@ -8,6 +10,7 @@ import {
   MetricQueryType,
   VariableQuery,
   VariableQueryType,
+  OldVariableQuery,
 } from './types';
 
 // Migrates a metric query that use more than one statistic into multiple queries
@@ -70,10 +73,38 @@ export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
   }
 }
 
-export function migrateVariableQuery(rawQuery: string | VariableQuery): VariableQuery {
-  if (typeof rawQuery !== 'string') {
+function isVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): rawQuery is VariableQuery {
+  return typeof rawQuery !== 'string' && typeof rawQuery.ec2Filters !== 'string' && typeof rawQuery.tags !== 'string';
+}
+
+export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): VariableQuery {
+  if (isVariableQuery(rawQuery)) {
     return rawQuery;
   }
+
+  // rawQuery is OldVariableQuery
+  if (typeof rawQuery !== 'string') {
+    const newQuery: VariableQuery = omit(rawQuery, ['ec2Filters', 'tags']);
+    newQuery.ec2Filters = {};
+    newQuery.tags = {};
+
+    if (rawQuery.ec2Filters !== '') {
+      try {
+        newQuery.ec2Filters = JSON.parse(rawQuery.ec2Filters);
+      } catch {
+        throw new Error(`unable to migrate poorly formed filters: ${rawQuery.ec2Filters}`);
+      }
+    }
+    if (rawQuery.tags !== '') {
+      try {
+        newQuery.tags = JSON.parse(rawQuery.tags);
+      } catch {
+        throw new Error(`unable to migrate poorly formed filters: ${rawQuery.tags}`);
+      }
+    }
+    return newQuery;
+  }
+
   const newQuery: VariableQuery = {
     refId: 'CloudWatchVariableQueryEditor-VariableQuery',
     queryType: VariableQueryType.Regions,
@@ -82,12 +113,13 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
     metricName: '',
     dimensionKey: '',
     dimensionFilters: {},
-    ec2Filters: '',
+    ec2Filters: {},
     instanceID: '',
     attributeName: '',
     resourceType: '',
-    tags: '',
+    tags: {},
   };
+
   if (rawQuery === '') {
     return newQuery;
   }
@@ -147,7 +179,13 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
     newQuery.queryType = VariableQueryType.EC2InstanceAttributes;
     newQuery.region = ec2InstanceAttributeQuery[1];
     newQuery.attributeName = ec2InstanceAttributeQuery[2];
-    newQuery.ec2Filters = ec2InstanceAttributeQuery[3] || '';
+    if (ec2InstanceAttributeQuery[3]) {
+      try {
+        newQuery.ec2Filters = JSON.parse(ec2InstanceAttributeQuery[3]);
+      } catch {
+        throw new Error(`unable to migrate poorly formed filters: ${ec2InstanceAttributeQuery[3]}`);
+      }
+    }
     return newQuery;
   }
 
@@ -156,7 +194,13 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
     newQuery.queryType = VariableQueryType.ResourceArns;
     newQuery.region = resourceARNsQuery[1];
     newQuery.resourceType = resourceARNsQuery[2];
-    newQuery.tags = resourceARNsQuery[3] || '';
+    if (resourceARNsQuery[3]) {
+      try {
+        newQuery.tags = JSON.parse(resourceARNsQuery[3]);
+      } catch {
+        throw new Error(`unable to migrate poorly formed filters: ${resourceARNsQuery[3]}`);
+      }
+    }
     return newQuery;
   }
 
diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts
index 38462070b93..e169e3fb329 100644
--- a/public/app/plugins/datasource/cloudwatch/types.ts
+++ b/public/app/plugins/datasource/cloudwatch/types.ts
@@ -11,6 +11,10 @@ export interface Dimensions {
   [key: string]: string | string[];
 }
 
+export interface MultiFilters {
+  [key: string]: string[];
+}
+
 export type CloudWatchQueryMode = 'Metrics' | 'Logs' | 'Annotations';
 
 export enum MetricQueryType {
@@ -372,7 +376,7 @@ export enum VariableQueryType {
   Statistics = 'statistics',
 }
 
-export interface VariableQuery extends DataQuery {
+export interface OldVariableQuery extends DataQuery {
   queryType: VariableQueryType;
   namespace: string;
   region: string;
@@ -386,6 +390,20 @@ export interface VariableQuery extends DataQuery {
   tags: string;
 }
 
+export interface VariableQuery extends DataQuery {
+  queryType: VariableQueryType;
+  namespace: string;
+  region: string;
+  metricName: string;
+  dimensionKey: string;
+  dimensionFilters?: Dimensions;
+  ec2Filters?: MultiFilters;
+  instanceID: string;
+  attributeName: string;
+  resourceType: string;
+  tags?: MultiFilters;
+}
+
 export interface LegacyAnnotationQuery extends MetricStat, DataQuery {
   actionPrefix: string;
   alarmNamePrefix: string;
diff --git a/public/app/plugins/datasource/cloudwatch/variables.test.ts b/public/app/plugins/datasource/cloudwatch/variables.test.ts
index a6e48356153..57589b526dc 100644
--- a/public/app/plugins/datasource/cloudwatch/variables.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/variables.test.ts
@@ -8,11 +8,9 @@ const defaultQuery: VariableQuery = {
   region: 'bar',
   metricName: '',
   dimensionKey: '',
-  ec2Filters: '',
   instanceID: '',
   attributeName: '',
   resourceType: '',
-  tags: '',
   refId: '',
 };
 
@@ -26,7 +24,7 @@ const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]
 const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
 const getResourceARNs = jest.fn().mockResolvedValue([{ label: 'h', value: 'h' }]);
 
-const variables = new CloudWatchVariableSupport(ds.datasource, ds.templateService);
+const variables = new CloudWatchVariableSupport(ds.datasource);
 
 describe('variables', () => {
   it('should run regions', async () => {
@@ -114,7 +112,7 @@ describe('variables', () => {
       ...defaultQuery,
       queryType: VariableQueryType.EC2InstanceAttributes,
       attributeName: 'abc',
-      ec2Filters: '{"$dimension":["b"]}',
+      ec2Filters: { a: ['b'] },
     };
     beforeEach(() => {
       ds.datasource.getEc2InstanceAttribute = getEc2InstanceAttribute;
@@ -129,7 +127,7 @@ describe('variables', () => {
 
     it('should run if instance id set', async () => {
       const result = await variables.execute(query);
-      expect(getEc2InstanceAttribute).toBeCalledWith(query.region, query.attributeName, { env: ['b'] });
+      expect(getEc2InstanceAttribute).toBeCalledWith(query.region, query.attributeName, { a: ['b'] });
       expect(result).toEqual([{ text: 'g', value: 'g', expandable: true }]);
     });
   });
@@ -139,7 +137,7 @@ describe('variables', () => {
       ...defaultQuery,
       queryType: VariableQueryType.ResourceArns,
       resourceType: 'abc',
-      tags: '{"a":${labels:json}}',
+      tags: { a: ['b'] },
     };
     beforeEach(() => {
       ds.datasource.getResourceARNs = getResourceARNs;
@@ -154,7 +152,7 @@ describe('variables', () => {
 
     it('should run if instance id set', async () => {
       const result = await variables.execute(query);
-      expect(getResourceARNs).toBeCalledWith(query.region, query.resourceType, { a: ['InstanceId', 'InstanceType'] });
+      expect(getResourceARNs).toBeCalledWith(query.region, query.resourceType, { a: ['b'] });
       expect(result).toEqual([{ text: 'h', value: 'h', expandable: true }]);
     });
   });
diff --git a/public/app/plugins/datasource/cloudwatch/variables.ts b/public/app/plugins/datasource/cloudwatch/variables.ts
index a4eb2ea5397..82733bb0861 100644
--- a/public/app/plugins/datasource/cloudwatch/variables.ts
+++ b/public/app/plugins/datasource/cloudwatch/variables.ts
@@ -2,7 +2,6 @@ import { from, Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
 import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data';
-import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
 
 import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
 import { CloudWatchDatasource } from './datasource';
@@ -11,12 +10,10 @@ import { VariableQuery, VariableQueryType } from './types';
 
 export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
   private readonly datasource: CloudWatchDatasource;
-  private readonly templateSrv: TemplateSrv;
 
-  constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
+  constructor(datasource: CloudWatchDatasource) {
     super();
     this.datasource = datasource;
-    this.templateSrv = templateSrv;
     this.query = this.query.bind(this);
   }
 
@@ -125,11 +122,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
     if (!attributeName) {
       return [];
     }
-    let filterJson = {};
-    if (ec2Filters) {
-      filterJson = JSON.parse(this.templateSrv.replace(ec2Filters));
-    }
-    const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, filterJson);
+    const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {});
     return values.map((s: { label: string; value: string }) => ({
       text: s.label,
       value: s.value,
@@ -141,11 +134,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
     if (!resourceType) {
       return [];
     }
-    let tagJson = {};
-    if (tags) {
-      tagJson = JSON.parse(this.templateSrv.replace(tags));
-    }
-    const keys = await this.datasource.getResourceARNs(region, resourceType, tagJson);
+    const keys = await this.datasource.getResourceARNs(region, resourceType, tags ?? {});
     return keys.map((s: { label: string; value: string }) => ({
       text: s.label,
       value: s.value,