Ping Me! (Part 2: Site-to-Site VPN Using CDK)
Tags: aws, typescript, devops, tutorial
Published: 2021-07-25 11:55:48

## Overview

Thanks to [AWS Site-to-Site VPN](https://docs.aws.amazon.com/vpn/latest/s2svpn/VPC_VPN.html) you can establish a secure connection over insecure infrastructure (e.g. the public internet) between a VPC and your on-premises data center. With such a connection in place, you can communicate with your EC2 instances as if they were within your existing corporate network.

It is much cheaper and quicker to establish this option rather than [AWS Direct Connect](https://docs.aws.amazon.com/directconnect/latest/UserGuide/Welcome.html), although it is less reliable in terms of performance and has a lower cap for bandwidth. It's not uncommon to have both in place, with AWS Site-to-Site VPN being the backup option.

Furthermore, if you have multiple data centers, each with an AWS Site-to-Site VPN connection to a central Virtual Private Gateway (that's the logical construct on which the VPN connection is terminated), you can provide secure communications between those in a hub-and-spoke model using something that AWS advertises as [VPN CloudHub](https://docs.aws.amazon.com/vpn/latest/s2svpn/VPN_CloudHub.html#vpn-cloudhub-overview).

As far as billing is concerned, [you pay for each hour during which the VPN connection is in the _available_ state](https://aws.amazon.com/vpn/pricing/) (thus, to avoid unnecessary costs you should terminate it whenever it's not in use), plus [the usual fee for the data out transfer](https://aws.amazon.com/ec2/pricing/on-demand/).

## Implementation and validation

![A rudimentary diagram of the complete solution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sua220xneew0mgc55am0.png)

As mentioned in the overview section, in most cases you'd use AWS Site-to-Site VPN to connect your on-premises data center to AWS. However, for the sake of simplicity, we're going to connect two VPCs together using the [Software VPN-to-AWS Managed VPN approach](https://docs.aws.amazon.com/whitepapers/latest/aws-vpc-connectivity-options/software-vpn-to-aws-managed-vpn.html).

Since we'll reuse the `VpcStack` and `InstanceStack` classes that we've created in [part 1](https://dev.to/aws-builders/ping-me-part-1-vpc-peering-using-cdk-2kpd), the only class we're missing is the one for the [Customer Gateway Device (CGD)](https://docs.aws.amazon.com/vpn/latest/s2svpn/your-cgw.html#example-configuration-files):

```bash
➜  ping-me-cdk-example$ touch lib/cgd.ts
```

+ `ping-me-cdk-example/lib/instance.ts`

```typescript
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam'; // <--- this module is not available from the start; remember to import it: `npm install @aws-cdk/aws-iam`

interface CustomerGatewayDeviceProps extends cdk.StackProps {
  vpc: ec2.Vpc; // <--- the VPC in which the Customer Gateway Device will be created
}

export class CustomerGatewayDeviceStack extends cdk.Stack {

  constructor(scope: cdk.Construct, id: string, props: CustomerGatewayDeviceProps) {
    super(scope, id, props);

    // Prepare the user data to be applied to the Windows EC2 instance on its initial boot-up
    const userData = ec2.UserData.forWindows();
    userData.addCommands(
      // The below PowerShell commands were taken from here: https://github.com/ACloudGuru-Resources/course-aws-certified-advanced-networking-specialty/blob/3a687ba5c70d507a53743037b8f1c5a52d05d357/SteveResources/OnPremNet.yaml#L126
      '<powershell>',
      '# Disable Internet Explorer Enhanced Security Configuration for Administrators',
      'Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" -Name "IsInstalled" -Value 0 -Force',
      'Stop-Process -Name iexplore -ErrorAction SilentlyContinue',
      '# Begin configuration for VPN services',
      'Set-NetAdapterAdvancedProperty -DisplayName "IPv4 Checksum Offload" -DisplayValue "Disabled"',
      'Set-NetAdapterAdvancedProperty -DisplayName "TCP Checksum Offload (IPv4)" -DisplayValue "Disabled"',
      'Set-NetAdapterAdvancedProperty -DisplayName "UDP Checksum Offload (IPv4)" -DisplayValue "Disabled"',
      'Invoke-WebRequest https://steveatacg.s3-us-west-1.amazonaws.com/advnetspec/Win2019VPNServerConfig.xml -OutFile c:\config.xml',
      'Install-WindowsFeature -ConfigurationFilePath c:\config.xml -computername $env:COMPUTERNAME -Restart',
      'Install-RemoteAccess -VpnType VpnS2S',
      '</powershell>',
    )

    // Create an IAM role allowing the instance to be managed by SSM
    const ssmRoleForEc2 = new iam.Role(this, 'SsmRoleForEc2', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')],
    });

    // Create an EC2 instance to serve as the software VPN
    const cgd = new ec2.Instance(this, 'CGW', {
      vpc: props.vpc,
      instanceType: new ec2.InstanceType('t2.micro'),
      machineImage: ec2.MachineImage.fromSSMParameter('/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base', ec2.OperatingSystemType.WINDOWS, userData), // <--- use the latest Amazon Machine Image for Windows Server 2019
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC, // <--- place the instance in a public subnet
      },
      role: ssmRoleForEc2, // <--- use the created earlier IAM role as the instance profile
      sourceDestCheck: false, // <--- make sure the source/destination check is turned off
    });

    // Output the instance's public IP
    new cdk.CfnOutput(this, 'CustomerGatewayDevicePublicIp', {
      value: cgd.instancePublicIp,
    });
  }
}
```

Since the [`@aws-cdk/aws-iam` module](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-iam-readme.html) was not imported during the cdk initialization, let's install it now:

```bash
➜  ping-me-cdk-example$ npm install @aws-cdk/aws-iam@1.73.0  
npm WARN ping-me-cdk-example@0.1.0 No repository field.
npm WARN ping-me-cdk-example@0.1.0 No license field.

+ @aws-cdk/aws-iam@1.73.0
updated 1 package and audited 847 packages in 5.085s

27 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

We explicitly chose the `1.73.0` version (the exact same one as for our `@aws-cdk/core` and `@aws-cdk/aws-ec2` modules) to avoid the possibility of seeing [the `Argument of type 'this' is not assignable to parameter of type 'Construct'` error](https://github.com/aws/aws-cdk/issues/3416).

Since we got all the pieces of the puzzle, let's start putting them together.

First, we'll initialize the VPN source VPC (that's the one that mocks an on-prem network) and place a Customer Gateway Device (a software VPN in our case) in it:

+ `ping-me-cdk-example/bin/ping-me-cdk-example.ts`

```typescript
import * as cdk from '@aws-cdk/core';
import { VpcStack } from '../lib/vpc';
import { InstanceStack } from '../lib/instance';
import { PeeringStack } from '../lib/peering';
import { CustomerGatewayDeviceStack } from '../lib/cgd'; // <--- added in part 2

const app = new cdk.App(); // <--- you can read more about the App construct here: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.App.html

/**
 * CODE FROM "Ping Me! (Part 1: VPC Peering Using CDK)" WAS REMOVED FOR VISIBILITY
 */

// Create a VPN source VPC
const vpcVpnSource = new VpcStack(app, 'VpcVpnSourceStack', {
  vpcSetup: {
    cidrs: ['10.0.2.0/24'],
    maxAzs: 1, // <--- to keep the costs down, we'll stick to 1 availability zone per VPC (obviously, not something you'd want to do in production)
  },
});

// Create a Customer Gateway Device in the VPN source VPC
new CustomerGatewayDeviceStack(app, 'CustomerGatewayDeviceStack', {
  vpc: vpcVpnSource.createdVpcs[0],
});
```

Now, we can deploy these two stacks. And since `VpcVpnSourceStack` is a dependency of `CustomerGatewayDeviceStack` we can just call the latter in our command and still both are going to be built out:

```bash
➜  ping-me-cdk-example$ cdk deploy CustomerGatewayDeviceStack --require-approval never
Including dependency stacks: VpcVpnSourceStack
VpcVpnSourceStack
VpcVpnSourceStack: deploying...
VpcVpnSourceStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (16/16)

 ✅  VpcVpnSourceStack

Outputs:
VpcVpnSourceStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.2.0/24
VpcVpnSourceStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-0d8d8118923b40b85
VpcVpnSourceStack.ExportsOutputRefVpc0publicSubnet1SubnetB977A71E8C9155C7 = subnet-037467ea871b103c1

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcVpnSourceStack/f9d46c70-3005-11eb-8c3a-06fab0925578
CustomerGatewayDeviceStack
CustomerGatewayDeviceStack: deploying...
CustomerGatewayDeviceStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (6/6)

 ✅  CustomerGatewayDeviceStack

Outputs:
CustomerGatewayDeviceStack.CustomerGatewayDevicePublicIp = 52.51.199.29 # <--- COPY THE PUBLIC IP OF THE CUSTOMER GATEWAY DEVICE (CGD)

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/CustomerGatewayDeviceStack/665a19d0-3006-11eb-9492-0a91d0eaa80f
```

Let's grab the public IP of the CGD from the above output, or by using AWS CLI (like so: `aws cloudformation describe-stacks --stack-name CustomerGatewayDeviceStack --query "Stacks[].Outputs[].OutputValue"`) and move on to creating a VPN destination VPC, with a Site-to-Site VPN connection to the VPN source VPC already in place, and an EC2 instance that'll serve as our ping destination.

+ `ping-me-cdk-example/bin/ping-me-cdk-example.ts`

```typescript
/**
 * ALL PRECEDING CODE WAS REMOVED FOR VISIBILITY
 */

const vpcVpnDestination = new VpcStack(app, 'VpcVpnDestinationStack', {
  vpcSetup: {
    cidrs: ['10.0.3.0/24'],
    maxAzs: 1, // <--- to keep the costs down, we'll stick to 1 availability zone per VPC (obviously, not something you'd want to do in production)
    vpnConnections: {
      toOnPrem: {
        ip: '52.51.199.29', // <--- grab this from the outputs of CustomerGatewayDeviceStack, e.g.: aws cloudformation describe-stacks --stack-name CustomerGatewayDeviceStack --query "Stacks[].Outputs[].OutputValue"
        staticRoutes: [
          vpcVpnSource.createdVpcs[0].vpcCidrBlock,
        ]
      }
    }
  },
});

new InstanceStack(app, 'InstanceVpnDestinationStack', {
  vpcs: vpcVpnDestination.createdVpcs,
});
```

```bash
➜  ping-me-cdk-example$ cdk deploy InstanceVpnDestinationStack --require-approval never
Including dependency stacks: VpcVpnDestinationStack, VpcVpnSourceStack
VpcVpnSourceStack
VpcVpnSourceStack: deploying...

 ✅  VpcVpnSourceStack (no changes)

Outputs:
VpcVpnSourceStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.2.0/24
VpcVpnSourceStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-0d8d8118923b40b85
VpcVpnSourceStack.ExportsOutputRefVpc0publicSubnet1SubnetB977A71E8C9155C7 = subnet-037467ea871b103c1

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcVpnSourceStack/f9d46c70-3005-11eb-8c3a-06fab0925578
VpcVpnDestinationStack
VpcVpnDestinationStack: deploying...
VpcVpnDestinationStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (22/22)

 ✅  VpcVpnDestinationStack

Outputs:
VpcVpnDestinationStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0e750a0ff54ed08aa
VpcVpnDestinationStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-0149bc2a09ea2bc34

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcVpnDestinationStack/af2f9e90-3007-11eb-93e5-0a4cff3749d5
InstanceVpnDestinationStack
InstanceVpnDestinationStack: deploying...
InstanceVpnDestinationStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (6/6)

 ✅  InstanceVpnDestinationStack

Outputs:
InstanceVpnDestinationStack.Instance0BastionHostId1959CA92 = i-02e376354bcb4b094
InstanceVpnDestinationStack.Instance0PrivateIp = 10.0.3.49

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/InstanceVpnDestinationStack/c2a92a30-3008-11eb-a26f-0639b3b50e04
```

The necessary infrastructure is standing, but we still need to configure the CGD. To accomplish that we'll head over to the AWS Console and download a config file that was generated for us:

+ AWS Console -> VPC -> Site-to-Site VPN Connections -> select our new connection -> click on "Download Configuration"

![AWS download VPN configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dhor58xxii7f9v5mqswe.png)

![AWS choose the type of the Customer Gateway Device](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6avf8dqaw5yvn3jfw8ks.png)

In this text file we're interested in the lines 111 to 129 containing two scripts we'll need to execute on the CGD. Those should look something like this:

```txt
! Script for Tunnel 1:
netsh advfirewall consec add rule Name="vgw-0e89a3295d6b100c2 Tunnel 1" ^
Enable=Yes Profile=any Type=Static Mode=Tunnel ^
LocalTunnelEndpoint=[Windows_Server_Private_IP_address] ^
RemoteTunnelEndpoint=34.253.60.42 Endpoint1=[Your_Static_Route_IP_Prefix] ^
Endpoint2=[Your_VPC_CIDR_Block] Protocol=Any Action=RequireInClearOut ^
Auth1=ComputerPSK Auth1PSK=iKV4pvEVAxVk6wKeVma38F8ZDxVfPhNA ^
QMSecMethods=ESP:SHA1-AES128+60min+100000kb ^
ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2

! Script for Tunnel 2:
netsh advfirewall consec add rule Name="vgw-0e89a3295d6b100c2 Tunnel 2" ^
Enable=Yes Profile=any Type=Static Mode=Tunnel ^
LocalTunnelEndpoint=[Windows_Server_Private_IP_address] ^
RemoteTunnelEndpoint=52.50.166.48 Endpoint1=[Your_Static_Route_IP_Prefix] ^
Endpoint2=[Your_VPC_CIDR_Block] Protocol=Any Action=RequireInClearOut ^
Auth1=ComputerPSK Auth1PSK=PAoYUDzM_V93hRcJxvVkMCV6VjpWFtKt ^
QMSecMethods=ESP:SHA1-AES128+60min+100000kb ^
ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2
```

Swap `[Windows_Server_Private_IP_address]` for the private IP address of your CGD (you can get it using the AWS CLI: `aws ec2 describe-instances --filters "Name=tag:Name,Values=CustomerGatewayDeviceStack/CGW" --query "Reservations[].Instances[].PrivateIpAddress"`), `[Your_Static_Route_IP_Prefix]` for the VPN source VPC's CIDR (`10.0.2.0/24`), `[Your_VPC_CIDR_Block]` for the VPN destination VPC's CIDR (`10.0.3.0/24`), escape the quotation marks (`"`) by preceding them with backslashes (`\`) and remove all the carets (`^`) and following new lines to make both scripts into one-liners, e.g.:

```bash
netsh advfirewall consec add rule Name=\"vgw-06fab417114297dd1 Tunnel 1\" Enable=Yes Profile=any Type=Static Mode=Tunnel LocalTunnelEndpoint=10.0.2.18 RemoteTunnelEndpoint=34.249.222.228 Endpoint1=10.0.2.0/24 Endpoint2=10.0.3.0/24 Protocol=Any Action=RequireInClearOut Auth1=ComputerPSK Auth1PSK=F_BjSjPg8ctE6XeX183INrtAPxkaktXm QMSecMethods=ESP:SHA1-AES128+60min+100000kb ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2
```

We'll run them on the Windows instance remotely using SSM.

If you're following along, be sure to swap the ID of the Windows instance (`i-03dd90e66a0e6a145`) for the appropriate value before running the below:

```bash
➜  ping-me-cdk-example$ aws ssm send-command --document-name "AWS-RunPowerShellScript" \
--document-version "1" \
--targets '[{"Key":"InstanceIds","Values":["i-03dd90e66a0e6a145"]}]' \
--parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["netsh advfirewall consec add rule Name=\"vgw-06fab417114297dd1 Tunnel 1\" Enable=Yes Profile=any Type=Static Mode=Tunnel LocalTunnelEndpoint=10.0.2.18 RemoteTunnelEndpoint=34.249.222.228 Endpoint1=10.0.2.0/24 Endpoint2=10.0.3.0/24 Protocol=Any Action=RequireInClearOut Auth1=ComputerPSK Auth1PSK=F_BjSjPg8ctE6XeX183INrtAPxkaktXm QMSecMethods=ESP:SHA1-AES128+60min+100000kb ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2"]}' \
--timeout-seconds 600 \
--max-concurrency "50" \
--max-errors "0"
{
    "Command": {
        "CommandId": "ab32f5e8-78c9-4e5b-95d4-944e02814682",
        "DocumentName": "AWS-RunPowerShellScript",
        "DocumentVersion": "1",
        "Comment": "",
        "ExpiresAfter": "2020-11-26T19:27:48.114000+01:00",
        "Parameters": {
            "commands": [
                "netsh advfirewall consec add rule Name=\"vgw-06fab417114297dd1 Tunnel 1\" Enable=Yes Profile=any Type=Static Mode=Tunnel LocalTunnelEndpoint=10.0.2.18 RemoteTunnelEndpoint=34.249.222.228 Endpoint1=10.0.2.0/24 Endpoint2=10.0.3.0/24 Protocol=Any Action=RequireInClearOut Auth1=ComputerPSK Auth1PSK=F_BjSjPg8ctE6XeX183INrtAPxkaktXm QMSecMethods=ESP:SHA1-AES128+60min+100000kb ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2"
            ],
            "executionTimeout": [
                "3600"
            ],
            "workingDirectory": [
                ""
            ]
        },
        "InstanceIds": [],
        "Targets": [
            {
                "Key": "InstanceIds",
                "Values": [
                    "i-03dd90e66a0e6a145"
                ]
            }
        ],
        "RequestedDateTime": "2020-11-26T18:17:48.114000+01:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 0,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 600
    }
}
```

Now, let's check whether that succeeded using AWS CLI's [`aws ssm get-command-invocation` command](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ssm/get-command-invocation.html).

Again, if you're following along, be sure to swap the command ID (`ab32f5e8-78c9-4e5b-95d4-944e02814682`) and the ID of the Windows instance (`i-03dd90e66a0e6a145`) for appropriate values before running the below:

```bash
➜  ping-me-cdk-example$ aws ssm get-command-invocation --command-id ab32f5e8-78c9-4e5b-95d4-944e02814682 --instance-id i-03dd90e66a0e6a145
{
    "CommandId": "ab32f5e8-78c9-4e5b-95d4-944e02814682",
    "InstanceId": "i-03dd90e66a0e6a145",
    "Comment": "",
    "DocumentName": "AWS-RunPowerShellScript",
    "DocumentVersion": "1",
    "PluginName": "aws:runPowerShellScript",
    "ResponseCode": 0,
    "ExecutionStartDateTime": "2020-11-26T17:17:49.795Z",
    "ExecutionElapsedTime": "PT1.96S",
    "ExecutionEndDateTime": "2020-11-26T17:17:50.795Z",
    "Status": "Success",
    "StatusDetails": "Success",
    "StandardOutputContent": "Ok.\r\n\r\n",
    "StandardOutputUrl": "",
    "StandardErrorContent": "",
    "StandardErrorUrl": "",
    "CloudWatchOutputConfig": {
        "CloudWatchLogGroupName": "",
        "CloudWatchOutputEnabled": false
    }
}
```

All's A-OKAY, so let's repeat these two steps for the second script:

```bash
➜  ping-me-cdk-example$ aws ssm send-command --document-name "AWS-RunPowerShellScript" \
--document-version "1" \
--targets '[{"Key":"InstanceIds","Values":["i-03dd90e66a0e6a145"]}]' \
--parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["netsh advfirewall consec add rule Name=\"vgw-06fab417114297dd1 Tunnel 2\" Enable=Yes Profile=any Type=Static Mode=Tunnel LocalTunnelEndpoint=10.0.2.18 RemoteTunnelEndpoint=54.77.85.166 Endpoint1=10.0.2.0/24 Endpoint2=10.0.3.0/24 Protocol=Any Action=RequireInClearOut Auth1=ComputerPSK Auth1PSK=sPGbITqCkW.PSrwuhlycnn6CgFFbjS0w QMSecMethods=ESP:SHA1-AES128+60min+100000kb ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2"]}' \
--timeout-seconds 600 \
--max-concurrency "50" \
--max-errors "0"
{
    "Command": {
        "CommandId": "dd7bb695-51e2-41eb-a898-eec8fc4562f4",
        "DocumentName": "AWS-RunPowerShellScript",
        "DocumentVersion": "1",
        "Comment": "",
        "ExpiresAfter": "2020-11-26T19:30:10.955000+01:00",
        "Parameters": {
            "commands": [
                "netsh advfirewall consec add rule Name=\"vgw-06fab417114297dd1 Tunnel 2\" Enable=Yes Profile=any Type=Static Mode=Tunnel LocalTunnelEndpoint=10.0.2.18 RemoteTunnelEndpoint=54.77.85.166 Endpoint1=10.0.2.0/24 Endpoint2=10.0.3.0/24 Protocol=Any Action=RequireInClearOut Auth1=ComputerPSK Auth1PSK=sPGbITqCkW.PSrwuhlycnn6CgFFbjS0w QMSecMethods=ESP:SHA1-AES128+60min+100000kb ExemptIPsecProtectedConnections=No ApplyAuthz=No QMPFS=dhgroup2"
            ],
            "executionTimeout": [
                "3600"
            ],
            "workingDirectory": [
                ""
            ]
        },
        "InstanceIds": [],
        "Targets": [
            {
                "Key": "InstanceIds",
                "Values": [
                    "i-03dd90e66a0e6a145"
                ]
            }
        ],
        "RequestedDateTime": "2020-11-26T18:20:10.955000+01:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 0,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 600
    }
}
```

```bash
➜  ping-me-cdk-example$ aws ssm get-command-invocation --command-id dd7bb695-51e2-41eb-a898-eec8fc4562f4 --instance-id i-03dd90e66a0e6a145
{
    "CommandId": "dd7bb695-51e2-41eb-a898-eec8fc4562f4",
    "InstanceId": "i-03dd90e66a0e6a145",
    "Comment": "",
    "DocumentName": "AWS-RunPowerShellScript",
    "DocumentVersion": "1",
    "PluginName": "aws:runPowerShellScript",
    "ResponseCode": 0,
    "ExecutionStartDateTime": "2020-11-26T17:20:11.557Z",
    "ExecutionElapsedTime": "PT0.647S",
    "ExecutionEndDateTime": "2020-11-26T17:20:11.557Z",
    "Status": "Success",
    "StatusDetails": "Success",
    "StandardOutputContent": "Ok.\r\n\r\n",
    "StandardOutputUrl": "",
    "StandardErrorContent": "",
    "StandardErrorUrl": "",
    "CloudWatchOutputConfig": {
        "CloudWatchLogGroupName": "",
        "CloudWatchOutputEnabled": false
    }
}
```

Another A-OKAY, so let's ping the EC2 instance in the VPN destination VPC from the Windows instance in the VPN source VPC to make sure the Site-to-Site VPN connection is working as expected.

Again, if you're following along, be sure to swap the ID of the Windows instance (`i-03dd90e66a0e6a145`) and the private IP of the Linux instance for appropriate values before running the below:

```bash
aws ssm send-command --document-name "AWS-RunPowerShellScript" --document-version "1" --targets '[{"Key":"InstanceIds","Values":["i-03dd90e66a0e6a145"]}]' --parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["ping 10.0.3.49"]}' --timeout-seconds 600 --max-concurrency "50" --max-errors "0"
{
    "Command": {
        "CommandId": "09785d32-8b7e-4f24-9581-90aed774aa7d",
        "DocumentName": "AWS-RunPowerShellScript",
        "DocumentVersion": "1",
        "Comment": "",
        "ExpiresAfter": "2020-11-26T20:16:40.640000+01:00",
        "Parameters": {
            "commands": [
                "ping 10.0.3.49"
            ],
            "executionTimeout": [
                "3600"
            ],
            "workingDirectory": [
                ""
            ]
        },
        "InstanceIds": [],
        "Targets": [
            {
                "Key": "InstanceIds",
                "Values": [
                    "i-03dd90e66a0e6a145"
                ]
            }
        ],
        "RequestedDateTime": "2020-11-26T19:06:40.640000+01:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 0,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 600
    }
}
```

Let's see the result's output:

```bash
➜  ping-me-cdk-example$ aws ssm get-command-invocation --command-id 09785d32-8b7e-4f24-9581-90aed774aa7d --instance-id i-03dd90e66a0e6a145
{
    "CommandId": "09785d32-8b7e-4f24-9581-90aed774aa7d",
    "InstanceId": "i-03dd90e66a0e6a145",
    "Comment": "",
    "DocumentName": "AWS-RunPowerShellScript",
    "DocumentVersion": "1",
    "PluginName": "aws:runPowerShellScript",
    "ResponseCode": 0,
    "ExecutionStartDateTime": "2020-11-26T18:06:41.265Z",
    "ExecutionElapsedTime": "PT7.529S",
    "ExecutionEndDateTime": "2020-11-26T18:06:48.265Z",
    "Status": "Success",
    "StatusDetails": "Success",
    "StandardOutputContent": "\r\nPinging 10.0.3.49 with 32 bytes of data:\r\nRequest timed out.\r\nReply from 10.0.3.49: bytes=32 time=1ms TTL=254\r\nReply from 10.0.3.49: bytes=32 time<1ms TTL=254\r\nReply from 10.0.3.49: bytes=32 time<1ms TTL=254\r\n\r\nPing statistics for 10.0.3.49:\r\n    Packets: Sent = 4, Received = 3, Lost = 1 (25% loss),\r\nApproximate round trip times in milli-seconds:\r\n    Minimum = 0ms, Maximum = 1ms, Average = 0ms\r\n",
    "StandardOutputUrl": "",
    "StandardErrorContent": "",
    "StandardErrorUrl": "",
    "CloudWatchOutputConfig": {
        "CloudWatchLogGroupName": "",
        "CloudWatchOutputEnabled": false
    }
}
```

The first ping was lost, the other three came back. [It's a perfectly valid behavior since by default the Customer Gateway Device must bring up the VPN tunnels by generating traffic into AWS](https://docs.aws.amazon.com/vpn/latest/s2svpn/initiate-vpn-tunnels.html).

Wait, tunnels? Yep, AWS Site-to-site VPN creates two tunnels from the Customer Gateway Device and they operate in an active/passive failover configuration. You can check their status in the AWS Console:

+ before the ping

![AWS, the state of the VPN tunnels before the ping](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w2mfsulq2lcj4uptbbr8.png)

+ after the ping

![AWS, the state of the VPN tunnels after the ping](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3tssj3l0r5yr85x0m5xo.png)

## Cleanup

For the sake of our wallets, let's promptly destroy the current infrastructure before moving on. When prompted, type `y` for yes:

```bash
➜  ping-me-cdk-example$ cdk destroy --all
Are you sure you want to delete: InstanceVpnDestinationStack, VpcVpnDestinationStack, PeeringStack, InstancePeersStack, CustomerGatewayDeviceStack, VpcVpnSourceStack, VpcPeersStack (y/n)? y
InstanceVpnDestinationStack: destroying...
19:15:58 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | InstanceVpnDestinationStack
 ✅  InstanceVpnDestinationStack: destroyed
VpcVpnDestinationStack: destroying...
19:16:55 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack            | VpcVpnDestinationStack
19:18:39 | DELETE_IN_PROGRESS   | AWS::EC2::InternetGateway             | Vpc0/IGW
19:18:39 | DELETE_IN_PROGRESS   | AWS::EC2::VPC                         | Vpc0
 ✅  VpcVpnDestinationStack: destroyed
PeeringStack: destroying...
 ✅  PeeringStack: destroyed
InstancePeersStack: destroying...
 ✅  InstancePeersStack: destroyed
CustomerGatewayDeviceStack: destroying...
19:19:00 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | CustomerGatewayDeviceStack
19:19:52 | DELETE_IN_PROGRESS   | AWS::IAM::Role            | SsmRoleForEc2
 ✅  CustomerGatewayDeviceStack: destroyed
VpcVpnSourceStack: destroying...
19:19:58 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack            | VpcVpnSourceStack
 ✅  VpcVpnSourceStack: destroyed
VpcPeersStack: destroying...
 ✅  VpcPeersStack: destroyed
```

Well then, we've already pinged over a VPC Peering and just a moment ago we've seen AWS Site-to-Site VPN in action. Hence, [what's left is taking AWS Transit Gateway for a spin](https://dev.to/aws-builders/ping-me-part-3-transit-gateway-using-cdk-18bf)!