stackql-deploy 2.2.0

Infrastructure-as-code framework for declarative cloud resource management using StackQL
---
id: vpc-and-ec2-instance
title: AWS VPC and EC2 Instance
hide_title: false
hide_table_of_contents: false
description: Deploy a complete AWS VPC networking stack with an EC2 web server using the awscc Cloud Control provider.
tags: []
draft: false
unlisted: false
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

In this example, we'll demonstrate how to set up a complete VPC networking stack with an EC2 web server instance in AWS using `stackql-deploy` and the `awscc` (Cloud Control) provider.  Resources are identified using the `awscc.tagging.tagged_resources` service with a standard tag taxonomy.

<div style={{ display: 'flex', justifyContent: 'center' }}>
  <img src="/img/library/aws/simple-aws-vpc-ec2-stack.png" alt="Simple AWS VPC EC2 Stack" style={{ width: '60%', height: 'auto' }} />
</div>
The EC2 instance is bootstrapped with a web server that serves a simple page using the EC2 instance `UserData` property.

## Deploying the Stack

> Install `stackql-deploy` (see [__Installing stackql-deploy__]/getting-started#installing-stackql-deploy), set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` environment variables, that's it!

Once you have setup your project directory (your "stack"), you can use the `stackql-deploy` cli application to deploy, test or teardown the stack in any given environment.  To deploy the stack to an environment labeled `dev`, run the following:

```bash
stackql-deploy build examples/aws/aws-vpc-webserver dev
```
Use the `--dry-run` flag to view the queries to be run without actually running them:

```bash
stackql-deploy build examples/aws/aws-vpc-webserver dev --dry-run --show-queries
```

## stackql_manifest.yml

The `stackql_manifest.yml` defines the resources in your stack and their property values (for one or more environments).  This stack uses the `awscc` provider with a standard tag taxonomy (`stackql:stack-name`, `stackql:stack-env`, `stackql:resource-name`) for resource identification.

<details>
  <summary>Click to expand the <code>stackql_manifest.yml</code> file</summary>

```yaml
version: 1
name: "aws-vpc-webserver"
description: Provisions a complete AWS networking stack (VPC, subnet, internet gateway, route table, security group) with an Apache web server EC2 instance.
providers:
  - awscc
globals:
  - name: region
    description: aws region
    value: "{{ AWS_REGION }}"
  - name: global_tags
    value:
      - Key: 'stackql:stack-name'
        Value: "{{ stack_name }}"
      - Key: 'stackql:stack-env'
        Value: "{{ stack_env }}"
      - Key: 'stackql:resource-name'
        Value: "{{ resource_name }}"
resources:
  - name: example_vpc
    props:
      - name: vpc_cidr_block
        values:
          prd:
            value: "10.0.0.0/16"
          sit:
            value: "10.1.0.0/16"
          dev:
            value: "10.2.0.0/16"
      - name: vpc_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-vpc"
        merge:
          - global_tags
    exports:
      - vpc_id
      - vpc_cidr_block
  - name: example_subnet
    props:
      - name: subnet_cidr_block
        values:
          prd:
            value: "10.0.1.0/24"
          sit:
            value: "10.1.1.0/24"
          dev:
            value: "10.2.1.0/24"
      - name: subnet_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-subnet"
        merge: ['global_tags']
    exports:
      - subnet_id
      - availability_zone
  - name: example_inet_gateway
    props:
      - name: inet_gateway_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-inet-gateway"
        merge: ['global_tags']
    exports:
      - internet_gateway_id
  - name: example_inet_gw_attachment
    props: []
  - name: example_route_table
    props:
      - name: route_table_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-route-table"
        merge: ['global_tags']
    exports:
      - route_table_id
  - name: example_subnet_rt_assn
    props: []
    exports:
      - subnet_route_table_assn_id
  - name: example_inet_route
    props: []
  - name: example_security_group
    props:
      - name: group_description
        value: "web security group for {{ stack_name }} ({{ stack_env }} environment)"
      - name: group_name
        value: "{{ stack_name }}-{{ stack_env }}-web-sg"
      - name: sg_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-web-sg"
        merge: ['global_tags']
      - name: security_group_ingress
        value:
          - IpProtocol: "tcp"
            CidrIp: "0.0.0.0/0"
            Description: Allow HTTP traffic
            FromPort: 80
            ToPort: 80
          - IpProtocol: "tcp"
            CidrIp: "{{ vpc_cidr_block }}"
            Description: Allow SSH traffic from the internal network
            FromPort: 22
            ToPort: 22
      - name: security_group_egress
        value:
        - CidrIp: "0.0.0.0/0"
          Description: "Allow all outbound traffic"
          FromPort: -1
          ToPort: -1
          IpProtocol: "-1"
    exports:
      - security_group_id
  - name: example_web_server
    props:
      - name: ami_id
        value: ami-05024c2628f651b80
      - name: instance_type
        value: t2.micro
      - name: instance_subnet_id
        value: "{{ subnet_id }}"
      - name: sg_ids
        value:
          - "{{ security_group_id }}"
      - name: user_data
        value: |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo '<!DOCTYPE html>...' > /var/www/html/index.html
      - name: instance_tags
        value:
          - Key: Name
            Value: "{{ stack_name }}-{{ stack_env }}-instance"
        merge: ['global_tags']
    exports:
      - instance_id
  - name: get_web_server_url
    type: query
    props: []
    exports:
      - public_dns_name
```

</details>

## Resource Query Files

Resource query files are templates which are used to create, update, test and delete resources in your stack.  This stack uses the **identifier capture** pattern — the `exists` query discovers the resource via tags and the captured field is used in `statecheck` and `exports` queries via `{{ this.<field> }}`.

<Tabs
  defaultValue="vpc"
  values={[
    { label: 'example_vpc.iql', value: 'vpc', },
    { label: 'example_subnet.iql', value: 'subnet', },
  ]}
>
<TabItem value="vpc">

```sql
/*+ exists */
WITH tagged_resources AS
(
    SELECT split_part(ResourceARN, '/', 2) as vpc_id
    FROM awscc.tagging.tagged_resources
    WHERE region = '{{ region }}'
    AND TagFilters = '{{ global_tags | to_aws_tag_filters }}'
    AND ResourceTypeFilters = '["ec2:vpc"]'
),
vpcs AS
(
    SELECT vpc_id
    FROM awscc.ec2.vpcs_list_only
    WHERE region = '{{ region }}'
)
SELECT r.vpc_id
FROM vpcs r
INNER JOIN tagged_resources tr
ON r.vpc_id = tr.vpc_id;

/*+ statecheck, retries=5, retry_delay=5 */
SELECT COUNT(*) as count FROM
(
SELECT
AWS_POLICY_EQUAL(tags, '{{ vpc_tags }}') as test_tags
FROM awscc.ec2.vpcs
WHERE Identifier = '{{ this.vpc_id }}'
AND region = '{{ region }}'
AND cidr_block = '{{ vpc_cidr_block }}'
) t
WHERE test_tags = 1;

/*+ create */
INSERT INTO awscc.ec2.vpcs (
 CidrBlock, Tags, EnableDnsSupport, EnableDnsHostnames, region
)
SELECT
 '{{ vpc_cidr_block }}', '{{ vpc_tags }}', true, true, '{{ region }}'
RETURNING *;

/*+ exports */
SELECT '{{ this.vpc_id }}' as vpc_id,
'{{ vpc_cidr_block }}' as vpc_cidr_block;

/*+ delete */
DELETE FROM awscc.ec2.vpcs
WHERE Identifier = '{{ vpc_id }}'
AND region = '{{ region }}';
```

</TabItem>
<TabItem value="subnet">

```sql
/*+ exists */
WITH tagged_resources AS
(
    SELECT split_part(ResourceARN, '/', 2) as subnet_id
    FROM awscc.tagging.tagged_resources
    WHERE region = '{{ region }}'
    AND TagFilters = '{{ global_tags | to_aws_tag_filters }}'
    AND ResourceTypeFilters = '["ec2:subnet"]'
),
subnets AS
(
    SELECT subnet_id
    FROM awscc.ec2.subnets_list_only
    WHERE region = '{{ region }}'
)
SELECT r.subnet_id
FROM subnets r
INNER JOIN tagged_resources tr
ON r.subnet_id = tr.subnet_id;

/*+ statecheck, retries=5, retry_delay=5 */
SELECT COUNT(*) as count FROM
(
SELECT
AWS_POLICY_EQUAL(tags, '{{ subnet_tags }}') as test_tags
FROM awscc.ec2.subnets
WHERE Identifier = '{{ this.subnet_id }}'
AND region = '{{ region }}'
AND cidr_block = '{{ subnet_cidr_block }}'
AND vpc_id = '{{ vpc_id }}'
) t
WHERE test_tags = 1;

/*+ create */
INSERT INTO awscc.ec2.subnets (
 VpcId, CidrBlock, MapPublicIpOnLaunch, Tags, region
)
SELECT
 '{{ vpc_id }}', '{{ subnet_cidr_block }}', true,
 '{{ subnet_tags }}', '{{ region }}'
RETURNING *;

/*+ exports, retries=5, retry_delay=5 */
SELECT subnet_id, availability_zone
FROM awscc.ec2.subnets
WHERE Identifier = '{{ this.subnet_id }}'
AND region = '{{ region }}';

/*+ delete */
DELETE FROM awscc.ec2.subnets
WHERE Identifier = '{{ subnet_id }}'
AND region = '{{ region }}';
```

</TabItem>
</Tabs>

## Key Patterns

### Tag-Based Resource Discovery

Resources are identified using `awscc.tagging.tagged_resources` cross-referenced with the provider's `*_list_only` resource via a CTE + `INNER JOIN`.  This ensures the resource both has the expected tags **and** currently exists in the provider (eliminating stale tag records).

### `to_aws_tag_filters` Filter

The `global_tags` variable is converted to AWS TagFilters format using the [`to_aws_tag_filters`](/template-filters#to_aws_tag_filters) custom filter, keeping queries clean:

```sql
AND TagFilters = '{{ global_tags | to_aws_tag_filters }}'
```

### Property Verification with `AWS_POLICY_EQUAL`

Statechecks use [`AWS_POLICY_EQUAL`](https://stackql.io/docs/language-spec/functions/json/aws_policy_equal) for order-independent comparison of tags and security group rules.

## More Information

The complete code for this example stack is available [__here__](https://github.com/stackql/stackql-deploy-rs/tree/main/examples/aws/aws-vpc-webserver). For more information on how to use StackQL and StackQL Deploy, visit:

- [`awscc` provider docs]https://awscc.stackql.io/providers/awscc/
- [`stackql`]https://github.com/stackql/stackql
- [`stackql-deploy` GitHub repo]https://github.com/stackql/stackql-deploy-rs