Virtual Appliance CLI Setup

Utilizing the AWS CLI to deploy a Virtual Appliance

AWS Setup

Pre-Reqs

This guide uses the AWS CLI v2 and jq.

To install the AWS CLI v2, use their install guide.
To install JQ, see their download page.

$ brew install aws jq

Any IAM cross account roles require the use of an External ID. GRAX support will supply your external ID during provisioning of your GRAX licenses. This is referred to as EXTERNAL_ID in these docs and will be a UUID, e.g. AEF80FCA-C184-48FA-B432-61B948D0259.

Setup Policy (Optional)

First create an IAM policy with minimal actions to install or uninstall the GRAX Virtual Appliance. Note that while this policy is allowed to create and delete Elasticsearch, S3, and Secrets resources, it is not allowed to access data from the services.

Expand policy.json
$ cat >setup-policy.json <<EOF
{
  "Statement": [
    {
      "Action": [
        "acm:AddTagsToCertificate",
        "acm:DeleteCertificate",
        "acm:DescribeCertificate",
        "acm:RequestCertificate",
        "autoscaling:CreateAutoScalingGroup",
        "autoscaling:CreateLaunchConfiguration",
        "autoscaling:DeleteAutoScalingGroup",
        "autoscaling:DeleteLaunchConfiguration",
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeAutoScalingInstances",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:DescribeScalingActivities",
        "autoscaling:UpdateAutoScalingGroup",
        "cloudformation:CreateChangeSet",
        "cloudformation:CreateStack",
        "cloudformation:DeleteStack",
        "cloudformation:DescribeChangeSet",
        "cloudformation:DescribeStackEvents",
        "cloudformation:DescribeStackResource",
        "cloudformation:DescribeStackResources",
        "cloudformation:DescribeStacks",
        "cloudformation:EstimateTemplateCost",
        "cloudformation:GetStackPolicy",
        "cloudformation:GetTemplateSummary",
        "cloudformation:ListStackResources",
        "cloudformation:ListStacks",
        "cloudformation:SignalResource",
        "cloudformation:UpdateStack",
        "cloudformation:UpdateStackSet",
        "ec2:AllocateAddress",
        "ec2:AssociateRouteTable",
        "ec2:AttachInternetGateway",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CreateInternetGateway",
        "ec2:CreateNatGateway",
        "ec2:CreateNetworkInterface",
        "ec2:CreateRoute",
        "ec2:CreateRouteTable",
        "ec2:CreateSecurityGroup",
        "ec2:CreateSubnet",
        "ec2:CreateTags",
        "ec2:CreateVpc",
        "ec2:CreateVpcEndpoint",
        "ec2:DeleteInternetGateway",
        "ec2:DeleteNatGateway",
        "ec2:DeleteNetworkInterface",
        "ec2:DeleteRoute",
        "ec2:DeleteRouteTable",
        "ec2:DeleteSecurityGroup",
        "ec2:DeleteSubnet",
        "ec2:DeleteVpc",
        "ec2:DeleteVpcEndpoints",
        "ec2:DescribeAccountAttributes",
        "ec2:DescribeAddresses",
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeImages",
        "ec2:DescribeInstanceAttribute",
        "ec2:DescribeInstanceStatus",
        "ec2:DescribeInstances",
        "ec2:DescribeInternetGateways",
        "ec2:DescribeNatGateways",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeTags",
        "ec2:DescribeVpcEndpoints",
        "ec2:DescribeVpcs",
        "ec2:DetachInternetGateway",
        "ec2:DisassociateRouteTable",
        "ec2:ModifySubnetAttribute",
        "ec2:ModifyVpcAttribute",
        "ec2:ReleaseAddress",
        "ec2:RunInstances",
        "ec2:TerminateInstances",
        "elasticloadbalancing:AddListenerCertificates",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:CreateListener",
        "elasticloadbalancing:CreateLoadBalancer",
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeTargetGroupAttributes",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth",
        "elasticloadbalancing:ModifyTargetGroupAttributes",
        "elasticloadbalancing:RemoveListenerCertificates",
        "elasticloadbalancing:SetSecurityGroups",
        "elasticloadbalancing:SetWebAcl",
        "es:AddTags",
        "es:CreateElasticsearchDomain",
        "es:DeleteElasticsearchDomain",
        "es:DescribeElasticsearchDomain",
        "es:UpdateElasticsearchDomainConfig",
        "iam:AddRoleToInstanceProfile",
        "iam:AttachRolePolicy",
        "iam:CreateInstanceProfile",
        "iam:CreateRole",
        "iam:CreateServiceLinkedRole",
        "iam:DeleteInstanceProfile",
        "iam:DeleteRole",
        "iam:DeleteRolePolicy",
        "iam:DetachRolePolicy",
        "iam:GetRole",
        "iam:GetRolePolicy",
        "iam:ListRoles",
        "iam:PassRole",
        "iam:PutRolePolicy",
        "iam:RemoveRoleFromInstanceProfile",
        "iam:TagInstanceProfile",
        "iam:TagRole",
        "kms:CreateKey",
        "kms:DescribeKey",
        "kms:GetKeyPolicy",
        "kms:GetKeyRotationStatus",
        "kms:ListResourceTags",
        "kms:PutKeyPolicy",
        "kms:ScheduleKeyDeletion",
        "kms:TagResource",
        "logs:CreateLogGroup",
        "logs:DeleteLogGroup",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams",
        "logs:DescribeQueryDefinitions",
        "logs:DescribeQueries",
        "logs:FilterLogEvents",
        "logs:GetLogEvents",
        "logs:GetQueryResults",
        "logs:StartQuery",
        "logs:StopQuery",
        "rds:AddTagsToResource",
        "rds:CreateDBCluster",
        "rds:CreateDBClusterSnapshot",
        "rds:CreateDBInstance",
        "rds:CreateDBSubnetGroup",
        "rds:DeleteDBCluster",
        "rds:DeleteDBInstance",
        "rds:DeleteDBSubnetGroup",
        "rds:DescribeDBClusterSnapshots",
        "rds:DescribeDBClusters",
        "rds:DescribeDBInstances",
        "rds:DescribeDBSubnetGroups",
        "rds:ModifyDBCluster",
        "route53:ChangeResourceRecordSets",
        "route53:GetChange",
        "route53:GetHostedZone",
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets",
        "s3:CreateBucket",
        "s3:GetBucketLocation",
        "s3:PutBucketPublicAccessBlock",
        "s3:PutBucketTagging",
        "s3:PutEncryptionConfiguration",
        "secretsmanager:CreateSecret",
        "secretsmanager:DeleteSecret",
        "secretsmanager:GetRandomPassword",
        "secretsmanager:GetSecretValue",
        "secretsmanager:PutSecretValue",
        "secretsmanager:TagResource",
        "secretsmanager:UpdateSecret",
        "ssm:GetParameters",
        "ssm:StartSession",
        "wafv2:AssociateWebACL",
        "wafv2:CreateIPSet",
        "wafv2:CreateRegexPatternSet",
        "wafv2:CreateWebACL",
        "wafv2:DeleteIPSet",
        "wafv2:DeleteRegexPatternSet",
        "wafv2:DeleteWebACL",
        "wafv2:DisassociateWebACL",
        "wafv2:GetIPSet",
        "wafv2:GetRegexPatternSet",
        "wafv2:GetWebACL",
        "wafv2:GetWebACLForResource",
        "wafv2:ListTagsForResource",
        "wafv2:UpdateIPSet",
        "wafv2:UpdateWebACL"
      ],
      "Effect": "Allow",
      "Resource": "*",
      "Sid": "GraxSetup"
    }
  ],
  "Version": "2012-10-17"
}
EOF
$ aws iam create-policy --policy-name GraxSetup --policy-document file://setup-policy.json
{
    "Policy": {
        "PolicyName": "GraxSetup",
        "PolicyId": "ANPAW2EG4TIUBTNVQPECV",
        "Arn": "arn:aws:iam::468434328104:policy/GraxSetup",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2021-04-02T14:02:53+00:00",
        "UpdateDate": "2021-04-02T14:02:53+00:00"
    }
}

Note the ARN for later:

$ POLICY_ARN=$(aws iam list-policies | jq -r '.Policies[] | select(.PolicyName == "GraxSetup") | .Arn')

Setup Role (Optional)

Next, create a role that uses this policy and that gives trust to GRAX account ID 956140928604 and the External ID GRAX provided to you:

$ EXTERNAL_ID=<provided by GRAX support>
$ cat >assume-role-policy.json <<EOF
{
 "Version": "2012-10-17",
 "Statement": [{
   "Effect": "Allow",
   "Principal": {
     "AWS": "arn:aws:iam::956140928604:root"
   },
   "Action": "sts:AssumeRole",
   "Condition": {
     "StringEquals":{
       "sts:ExternalId": "$EXTERNAL_ID"
     }
   }
 }]
}
EOF
$ aws iam create-role --assume-role-policy-document file://assume-role-policy.json --role-name GraxSetupRole
$ aws iam attach-role-policy --role-name GraxSetupRole --policy-arn $POLICY_ARN
{
    "Role": {
        "Path": "/",
        "RoleName": "GraxSetupRole",
        "RoleId": "AROAW2EG4TIUABUKU2W5I",
        "Arn": "arn:aws:iam::468434328104:role/GraxSetupRole",
        "CreateDate": "2021-04-22T16:04:18+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "arn:aws:iam::956140928604:root"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {
                        "StringEquals": {
                            "sts:ExternalId": "F3590FF4-E0C7-47CD-B217-D15D2078C035"
                        }
                    }
                }
            ]
        }
    }
}

GRAX uses this for creating virtual appliances.

Management Policies

Next create two IAM policies with minimal actions to manage the GRAX Virtual Appliance. Note that these policies are not allowed to create or delete any of the core services that the GRAX Virtual Appliance runs on. These roles are intended for log monitoring, stack inspection, and interactive instance access. The first allows no data access or visibility.

The second policy allows interactive access to the instance running GRAX, which enables debugging of networking and connectivity from inside the VPC. This is created upon implementation and only connected to the GRAX role by the customer when a support issue requires escalation. You can regard this much like SFDC LMA - we only request it and thus have the access when it is critical to solving an issue. It is always within your control.

These policies are both entirely within your control, and can be revoked from the role at any time.

Expand general policy.json
$ cat >management-policy.json <<EOF
{
  "Statement": [
    {
      "Action": [
        "cloudformation:CreateChangeSet",
        "cloudformation:DescribeChangeSet",
        "cloudformation:DescribeStackEvents",
        "cloudformation:DescribeStackResource",
        "cloudformation:DescribeStackResources",
        "cloudformation:DescribeStacks",
        "cloudformation:EstimateTemplateCost",
        "cloudformation:GetStackPolicy",
        "cloudformation:GetTemplateSummary",
        "cloudformation:ListStackResources",
        "cloudformation:ListStacks",
        "cloudformation:SignalResource",
        "cloudformation:UpdateStack",
        "cloudformation:UpdateStackSet",
        "cloudwatch:GetMetricStatistics",
        "cloudwatch:ListMetrics",
        "ec2:DescribeImages",
        "ec2:DescribeInstances",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVpcs",
        "es:DescribeElasticsearchDomain",
        "iam:GetRole",
        "iam:GetRolePolicy",
        "iam:ListRoles",
        "iam:PassRole",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams",
        "logs:DescribeQueryDefinitions",
        "logs:DescribeQueries",
        "logs:FilterLogEvents",
        "logs:GetLogEvents",
        "logs:GetQueryResults",
        "logs:StartQuery",
        "logs:StopQuery"
      ],
      "Effect": "Allow",
      "Resource": "*",
      "Sid": "GraxManagement"
    }
  ],
  "Version": "2012-10-17"
}
EOF
$ aws iam create-policy --policy-name GraxManagement --policy-document file://management-policy.json
{
    "Policy": {
        "PolicyName": "GraxManagement",
        "PolicyId": "ANPAW2EG4TIUBTNVQPECV",
        "Arn": "arn:aws:iam::468434328104:policy/GraxManagement",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2021-04-02T14:02:53+00:00",
        "UpdateDate": "2021-04-02T14:02:53+00:00"
    }
}

Note the ARN for later:

$ POLICY_ARN=$(aws iam list-policies | jq -r '.Policies[] | select(.PolicyName == "GraxManagement") | .Arn')
Expand interactive access policy.json
$ cat >sessions-policy.json <<EOF
{
  "Statement": [
    {
      "Action": [
        "ssm:GetParameters",
        "ssm:StartSession"
      ],
      "Effect": "Allow",
      "Resource": "*",
      "Sid": "GraxSessions"
    }
  ],
  "Version": "2012-10-17"
}
EOF
$ aws iam create-policy --policy-name GraxSessions --policy-document file://sessions-policy.json
{
    "Policy": {
        "PolicyName": "GraxSessions",
        "PolicyId": "ANPAW2EG4TIUBTNVQPECV",
        "Arn": "arn:aws:iam::468434328104:policy/GraxSessions",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2021-04-02T14:02:53+00:00",
        "UpdateDate": "2021-04-02T14:02:53+00:00"
    }
}

Management Role

Next, create a role that uses the first policy and that gives trust to GRAX account ID 956140928604 and the External ID GRAX provided to you:

$ EXTERNAL_ID=<provided by GRAX support>
$ cat >assume-role-policy.json <<EOF
{
 "Version": "2012-10-17",
 "Statement": [{
   "Effect": "Allow",
   "Principal": {
     "AWS": "arn:aws:iam::956140928604:root"
   },
   "Action": "sts:AssumeRole",
   "Condition": {
     "StringEquals":{
       "sts:ExternalId": "$EXTERNAL_ID"
     }
   }
 }]
}
EOF
$ aws iam create-role --assume-role-policy-document file://assume-role-policy.json --role-name GraxManagementRole
{
    "Role": {
        "Path": "/",
        "RoleName": "GraxManagementRole",
        "RoleId": "AROAW2EG4TIUABUKU2W5I",
        "Arn": "arn:aws:iam::468434328104:role/GraxManagementRole",
        "CreateDate": "2021-04-22T16:04:18+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "arn:aws:iam::956140928604:root"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {
                        "StringEquals": {
                            "sts:ExternalId": "F3590FF4-E0C7-47CD-B217-D15D2078C035"
                        }
                    }
                }
            ]
        }
    }
}

GRAX uses this for monitoring and automated service and infrastructure updates.

Granting Management Access to the GRAX Role

We require mininal ongoing access to the AWS resources for the virtual appliance, so we require the GraxManagement policy be added to the role granted to our AWS account.

Find the ARN:

$ POLICY_ARN=$(aws iam list-policies | jq -r '.Policies[] | select(.PolicyName == "GraxManagement") | .Arn')
$ aws iam attach-role-policy --role-name GraxManagementRole --policy-arn $POLICY_ARN

Granting Interactive Access to the GRAX Role

When a support case or other issue requires interactive access to the instance running the GRAX application, we will request the GraxSessions policy be added to the role granted to our AWS account.

Find the ARN:

$ POLICY_ARN=$(aws iam list-policies | jq -r '.Policies[] | select(.PolicyName == "GraxSessions") | .Arn')
$ aws iam attach-role-policy --role-name GraxManagementRole --policy-arn $POLICY_ARN

Revoking Interactive Access

When support is finished, or the agreed access interval ends, revoke the policy from the role:

$ aws iam detach-role-policy --role-name GraxManagementRole --policy-arn $POLICY_ARN

Standard VPC Setup

GRAX Can Do This For You!

The rest of this documentation is going to describe the steps you would take to create the GRAX environment yourself. If you have granted the GRAX Team access via the above Management Role, then you don't need to worry about this.

The following steps require at least the GRAX Setup permissions declared above

Next create your VPC.

The "Standard VPC" creates a VPC with 2 subnets, 2 NAT gateways and an Internet Gateway. You can create this with the reference CloudFormation template.

Now create the CloudFormation stack:

$ aws cloudformation create-stack    \
  --stack-name production-vpc        \
  --tags                             \
    Key=application,Value=grax       \
    Key=environment,Value=production \
  --template-body file://virtual-appliance/vpc-standard.yml
{
    "StackId": "arn:aws:cloudformation:us-east-1:468434328104:stack/production-vpc/2c00f930-93bf-11eb-8348-12c9c271ca70"
}

Note the outputs for later:

$ aws cloudformation describe-stacks --stack-name production-vpc | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]'

Tier1Subnet0=subnet-0dd8f4020fe19949a
Tier1Subnet1=subnet-037971166a544a08b
Tier2Subnet0=subnet-00a9398222522b21f
Tier2Subnet1=subnet-069b61f99026c483b
Tier3Subnet0=subnet-075d2fdc5896ff7d8
Tier3Subnet1=subnet-0692ab3b87a65e3d1
VPC=vpc-0454a454bb0dcbd9a

You can "export" these for later steps:

$ export $(aws cloudformation describe-stacks --stack-name production-vpc | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]')

If you intend to use private and isolated VPCs for your production and sandbox environments, use different VPC CIDRs to retain VPC peering optionality.

$ aws cloudformation create-stack                                     \
  --parameters                                                        \
    ParameterKey=Tier1Subnet0CidrBlock,ParameterValue=10.50.51.160/27 \
    ParameterKey=Tier1Subnet1CidrBlock,ParameterValue=10.50.51.128/27 \
    ParameterKey=Tier2Subnet0CidrBlock,ParameterValue=10.50.51.96/27  \
    ParameterKey=Tier2Subnet1CidrBlock,ParameterValue=10.50.51.64/27  \
    ParameterKey=Tier3Subnet0CidrBlock,ParameterValue=10.50.51.32/27  \
    ParameterKey=Tier3Subnet1CidrBlock,ParameterValue=10.50.51.0/27   \
    ParameterKey=VPCCidrBlock,ParameterValue=10.50.51.0/24            \
  --stack-name production-vpc                                         \
  --tags                                                              \
    Key=application,Value=grax                                        \
    Key=environment,Value=production                                  \
  --template-body file://virtual-appliance/vpc-standard.yml

Custom VPC Setup (Optional)

A "Custom VPC" gives you full control over the network. A common use case here is a high security VPC with no internet or NAT gateways, and security groups that only allow ingress / egress to a firewall. GRAX can provide a reference "Custom VPC" CloudFormation template on request.

Domain Name Setup

$ DOMAIN_NAME=myappliance.com
$ aws route53domains check-domain-availability  \
    --region us-east-1                          \
    --domain-name $DOMAIN_NAME

$ cat >register-domain.json <<EOF
{
    "DomainName": "$DOMAIN_NAME",
    "DurationInYears": 1,
    "AutoRenew": true,
    "AdminContact": {
        "FirstName": "Martha",
        "LastName": "Rivera",
        "ContactType": "PERSON",
        "OrganizationName": "Example",
        "AddressLine1": "1 Main Street",
        "City": "Anytown",
        "State": "WA",
        "CountryCode": "US",
        "ZipCode": "98101",
        "PhoneNumber": "+1.8005551212",
        "Email": "[email protected]"
    },
    "RegistrantContact": {
        "FirstName": "Li",
        "LastName": "Juan",
        "ContactType": "PERSON",
        "OrganizationName": "Example",
        "AddressLine1": "1 Main Street",
        "City": "Anytown",
        "State": "WA",
        "CountryCode": "US",
        "ZipCode": "98101",
        "PhoneNumber": "+1.8005551212",
        "Email": "[email protected]"
    },
    "TechContact": {
        "FirstName": "Mateo",
        "LastName": "Jackson",
        "ContactType": "PERSON",
        "OrganizationName": "Example",
        "AddressLine1": "1 Main Street",
        "City": "Anytown",
        "State": "WA",
        "CountryCode": "US",
        "ZipCode": "98101",
        "PhoneNumber": "+1.8005551212",
        "Email": "[email protected]"
    },
    "PrivacyProtectAdminContact": true,
    "PrivacyProtectRegistrantContact": true,
    "PrivacyProtectTechContact": true
}
EOF

$ aws route53domains register-domain \
    --cli-input-json file://register-domain.json \
    --region us-east-1

HTTPS Load Balancer Setup

Next create a certificate, load balancer, and DNS records for the secure endpoint for your GRAX Virtual Appliance. This is the address for your web management console and the API that Salesforce will connect to.

It's very common to create an SSL certificate manually:

  • Create a certificate request in ACM for your GRAX domain, e.g. production.us-east-1.myappliance.com
  • Work with your IT department to complete the DNS or Email certificate validation
  • Create an ALB with your certificate
  • Work with your IT department to add the DNS CNAME to the ALB

This is fully automated if you already have a domain name and hosted zone managed in Route 53.

$ aws route53 list-hosted-zones
{
    "HostedZones": [
        {
            "Id": "/hostedzone/Z04896123RW84RM04X6V7",
            "Name": "myappliance.com.",
            "Config": {
                "PrivateZone": false
            },
            "ResourceRecordSetCount": 1
        },
    ]
}
$ HostedZoneId=$(aws route53 list-hosted-zones | jq -r '.HostedZones[0] | .Id | sub("/hostedzone/"; "")')
$ Subdomain=$(aws route53 list-hosted-zones | jq -r '.HostedZones[0] | .Name | .[:-1]')
$ Region=$(aws configure get region)
$ DomainName=prod.$Region.$Subdomain

You can create this with the reference CloudFormation template:

$ DomainName=production.us-east-1.myappliance.com
$ HostedZoneId=Z04896123RW84RM04X6V7
$ Tier1Subnet0=subnet-0dd8f4020fe19949a
$ Tier1Subnet1=subnet-037971166a544a08b
$ VPC=vpc-0454a454bb0dcbd9a

$ aws cloudformation create-stack                          \
  --parameters                                             \
    ParameterKey=DomainName,ParameterValue=$DomainName     \
    ParameterKey=HostedZoneId,ParameterValue=$HostedZoneId \
    ParameterKey=Tier1Subnet0,ParameterValue=$Tier1Subnet0 \
    ParameterKey=Tier1Subnet1,ParameterValue=$Tier1Subnet1 \
    ParameterKey=VPC,ParameterValue=$VPC                   \
  --stack-name production-alb                              \
  --tags                                                   \
    Key=application,Value=grax                             \
    Key=environment,Value=production                       \
  --template-body file://virtual-appliance/alb-dns.yml

Or update it:

$ DomainName=new.us-east-1.myappliance.com

$ aws cloudformation update-stack                       \
  --parameters                                          \
    ParameterKey=DomainName,ParameterValue=$DomainName \
    ParameterKey=HostedZoneId,UsePreviousValue=true     \
    ParameterKey=Tier1Subnet0,UsePreviousValue=true     \
    ParameterKey=Tier1Subnet1,UsePreviousValue=true     \
    ParameterKey=VPC,UsePreviousValue=true              \
  --stack-name production-alb                           \
  --template-body file://virtual-appliance/alb-dns.yml

Note the outputs for later:

$ aws cloudformation describe-stacks --stack-name production-alb | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]'

DomainName=production.us-east-1.grax.mycompany.com
LoadBalancer=arn:aws:elasticloadbalancing:us-east-2:468434328104:loadbalancer/app/produ-LoadB-1R8KUD10M6OHC/e8dbad3953d0346e
SecurityGroup=sg-095b53cceef32f799
TargetGroup=arn:aws:elasticloadbalancing:us-east-2:468434328104:targetgroup/produ-Targe-ZX4Y6AJYJ6PB/aa81f5bd42666fe0

You can "export" these for later steps:

$ export $(aws cloudformation describe-stacks --stack-name production-alb | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]')

Firewall Setup (Optional)

Next create a Web Application Firewall (WAF) for your GRAX Virtual Appliance. This limits your API to only accept connections from Salesforce IP addresses.

You can create this with the reference CloudFormation template:

$ LoadBalancer=arn:aws:elasticloadbalancing:us-east-2:468434328104:loadbalancer/app/produ-LoadB-1R8KUD10M6OHC/e8dbad3953d0346e

$ aws cloudformation create-stack                          \
  --parameters                                             \
    ParameterKey=LoadBalancer,ParameterValue=$LoadBalancer \
  --stack-name production-waf                              \
  --tags                                                   \
    Key=application,Value=grax                             \
    Key=environment,Value=production                       \
  --template-body file://virtual-appliance/waf.yml

Runtime Setup

Finally create the runtime with the reference CloudFormation template:

$ DomainName=production.us-east-1.grax.mycompany.com
$ SecurityGroup=sg-095b53cceef32f799
$ TargetGroup=arn:aws:elasticloadbalancing:us-east-2:468434328104:targetgroup/produ-Targe-ZX4Y6AJYJ6PB/aa81f5bd42666fe0
$ Tier1Subnet0=subnet-0dd8f4020fe19949a
$ Tier1Subnet1=subnet-037971166a544a08b
$ Tier2Subnet0=subnet-00a9398222522b21f
$ Tier2Subnet1=subnet-069b61f99026c483b
$ Tier3Subnet0=subnet-075d2fdc5896ff7d8
$ Tier3Subnet1=subnet-0692ab3b87a65e3d1
$ VPC=vpc-0454a454bb0dcbd9a

$ aws cloudformation create-stack                                        \
  --capabilities CAPABILITY_NAMED_IAM                                    \
  --parameters                                                           \
    ParameterKey=DomainName,ParameterValue=$DomainName                   \
    ParameterKey=LoadBalancerSecurityGroup,ParameterValue=$SecurityGroup \
    ParameterKey=TargetGroup,ParameterValue=$TargetGroup                 \
    ParameterKey=Tier1Subnet0,ParameterValue=$Tier1Subnet0               \
    ParameterKey=Tier1Subnet1,ParameterValue=$Tier1Subnet1               \
    ParameterKey=Tier2Subnet0,ParameterValue=$Tier2Subnet0               \
    ParameterKey=Tier2Subnet1,ParameterValue=$Tier2Subnet1               \
    ParameterKey=Tier3Subnet0,ParameterValue=$Tier3Subnet0               \
    ParameterKey=Tier3Subnet1,ParameterValue=$Tier3Subnet1               \
    ParameterKey=VPC,ParameterValue=$VPC                                 \
  --stack-name production-runtime                                        \
  --tags                                                                 \
    Key=application,Value=grax                                           \
    Key=environment,Value=production                                     \
  --template-body file://virtual-appliance/runtime.yml
$ aws cloudformation describe-stacks --stack-name production-runtime | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]'

EngagementgraphSecret=arn:aws:secretsmanager:us-east-2:468434328104:secret:EngagementgraphSecret-4FERvvkEhIw7-ClMLZR

You can "export" these for later steps:

$ export $(aws cloudformation describe-stacks --stack-name production-runtime | jq -r '.Stacks[].Outputs | sort_by(.OutputKey) | map("\(.OutputKey)=\(.OutputValue)")|.[]')

Salesforce Setup

The settings to "Connect" to AWS are in the stacks EngagementgraphSecret:

$ EngagementgraphSecret=arn:aws:secretsmanager:us-east-2:468434328104:secret:EngagementgraphSecret-4FERvvkEhIw7-ClMLZR

$ aws secretsmanager get-secret-value --secret-id $EngagementgraphSecret | jq '.SecretString | fromjson'
{
  "api_url": "https://production.us-east-1.grax.mycompany.com",
  "api_token": "<REDACTED>",
  "gateway_token": "DC7FDE32-335C-4897-B55E-197416674636",
  "license_token": "468434328104"
}

Then follow the standard GRAX configuration guide.

In the SFDC Grax package, go to Configuration -> Connect -> Unlock -> AWS and enter the values.

Next, go to Configuration -> Setup and connect to SFDC.

Note that Elastic and S3 config is already connected.

Management and Updates

To update to the latest AMI and GRAX release:

$ ImageId=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
$ Redeploy=$RANDOM
$ aws cloudformation update-stack                                   \
  --capabilities CAPABILITY_NAMED_IAM                               \
  --parameters                                                      \
    ParameterKey=AdminUsername,UsePreviousValue=true                \
    ParameterKey=AutoScalingGroupSize,UsePreviousValue=true         \
    ParameterKey=DBEngineVersion,UsePreviousValue=true              \
    ParameterKey=DBInstanceClass,UsePreviousValue=true              \
    ParameterKey=DBPassword,UsePreviousValue=true                   \
    ParameterKey=DBPort,UsePreviousValue=true                       \
    ParameterKey=DBSnapshotIdentifier,UsePreviousValue=true         \
    ParameterKey=DomainName,UsePreviousValue=true                   \
    ParameterKey=ElasticsearchInstanceCount,UsePreviousValue=true   \
    ParameterKey=ElasticsearchInstanceType,UsePreviousValue=true    \
    ParameterKey=ElasticsearchVersion,UsePreviousValue=true         \
    ParameterKey=ElasticsearchVolumeSize,UsePreviousValue=true      \
    ParameterKey=EngagementgraphGatewayToken,UsePreviousValue=true  \
    ParameterKey=EnvVars,UsePreviousValue=true                      \
    ParameterKey=GoRelease,UsePreviousValue=true                    \
    ParameterKey=ImageId,ParameterValue=$ImageId                    \
    ParameterKey=IngressEgressSecurityGroup,UsePreviousValue=true   \
    ParameterKey=InstanceType,UsePreviousValue=true                 \
    ParameterKey=JsRelease,UsePreviousValue=true                    \
    ParameterKey=LoadBalancerSecurityGroup,UsePreviousValue=true    \
    ParameterKey=Redeploy,ParameterValue=$Redeploy                  \
    ParameterKey=S3BucketName,UsePreviousValue=true                 \
    ParameterKey=SFDCAnalyticsFrequency,UsePreviousValue=true       \
    ParameterKey=SFDCMetadataFrequency,UsePreviousValue=true        \
    ParameterKey=Services,UsePreviousValue=true                     \
    ParameterKey=SyslogUrl,UsePreviousValue=true                    \
    ParameterKey=TargetGroup,UsePreviousValue=true                  \
    ParameterKey=TemplateVersion,UsePreviousValue=true              \
    ParameterKey=Tier1Subnet0,UsePreviousValue=true                 \
    ParameterKey=Tier1Subnet1,UsePreviousValue=true                 \
    ParameterKey=Tier2Subnet0,UsePreviousValue=true                 \
    ParameterKey=Tier2Subnet1,UsePreviousValue=true                 \
    ParameterKey=Tier3Subnet0,UsePreviousValue=true                 \
    ParameterKey=Tier3Subnet1,UsePreviousValue=true                 \
    ParameterKey=VPC,UsePreviousValue=true                          \
    ParameterKey=VolumeSize,UsePreviousValue=true                   \
  --stack-name production-runtime                                   \
  --use-previous-template

To review logs:

$ LOG_GROUP=$(aws cloudformation describe-stack-resource --stack-name production-runtime --logical-resource-id LogGroup | jq -r '.StackResourceDetail.PhysicalResourceId')
$ aws logs tail $LOG_GROUP --follow

To connect to the instance:

$ INSTANCE_ID=$(aws ec2 describe-instances | jq -r '.Reservations[].Instances[] | select(.State.Name == "running") | select(any(.Tags[]; .Key == "aws:cloudformation:stack-name" and .Value =="production-runtime")) | .InstanceId')
$ aws ssm start-session --target $INSTANCE_ID

To review metrics, look at CloudWatch. An example elasticsearch metrics lookup is:

$ StartTime="$(date -v -5M '+%Y-%m-%dT%T')"
$ EndTime="$(date '+%Y-%m-%dT%T')"
$ aws cloudwatch get-metric-statistics  \
    --metric-name FreeStorageSpace        \
    --start-time $StartTime               \
    --end-time $EndTime                   \
    --period 300                         \
    --namespace AWS/ES                    \
    --statistics Sum                      \
    --dimensions Name=DomainName,Value=product-elasti-nygpcy5xr5y7 Name=ClientId,Value=468434328104

EC2, RDS, etc. are also in CloudWatch.