Creating An Amazon API Gateway With aws-cli For Domain Redirect

Ten commands to launch a minimal, functioning API Gateway

As of this publication date, the Amazon API Gateway is pretty new and the aws-cli interface for it is even newer. The API and aws-cli documentation at the moment is a bit rough, but this article outlines steps to create a functioning API Gateway with the aws-cli. Hopefully, this can help others who are trying to get it to work.

Goals

I regularly have a need to redirect browsers from one domain to another, whether it’s a vanity domain, legacy domain, “www” to base domain, misspelling, or other reasons.

I usually do this with an S3 bucket in website mode with a CloudFront distribution in front to support https. This works, performs well, and costs next to nothing.

Now that the Amazon API Gateway has aws-cli support, I was looking for simple projects to test out so I worked to reproduce the domain redirect. I found I can create an API Gateway that will redirect a hostname to a target URL, without any back end for the API (not even a Lambda function).

I’m not saying the API Gateway method is better than using S3 plus CloudFront for simple hostname redirection. In fact, it costs more (though still cheap), takes more commands to set up, and isn’t quite as flexible in what URL paths get redirected from the source domain. It does, however, work and may be useful as an API Gateway aws-cli example.

Assumptions

The following steps assume that you already own and have set up the source domain (to be redirected). Specifically:

  • You have already created a Route53 Hosted Zone for the source domain in your AWS account.

  • You have the source domain SSL key, certificate, chain certificate in local files.

Now here are the steps for setting up the domain to redirect to another URL using the aws-cli to create an API Gateway.

1. Create an Amazon API Gateway with aws-cli

Set up the parameters for your redirection. Adjust values to suit:

base_domain=erichammond.xyz # Replace with your domain
target_url=https://twitter.com/esh # Replace with your URL

api_name=$base_domain
api_description="Redirect $base_domain to $target_url"
resource_path=/
stage_name=prod
region=us-east-1

certificate_name=$base_domain
certificate_body=$base_domain.crt
certificate_private_key=$base_domain.key
certificate_chain=$base_domain-chain.crt

Create a new API Gateway:

api_id=$(aws apigateway create-rest-api \
  --region "$region" \
  --name "$api_name" \
  --description "$api_description" \
  --output text \
  --query 'id')
echo api_id=$api_id

Get the resource id of the root path (/):

resource_id=$(aws apigateway get-resources \
  --region "$region" \
  --rest-api-id "$api_id" \
  --output text \
  --query 'items[?path==`'$resource_path'`].[id]')
echo resource_id=$resource_id

Create a GET method on the root resource:

aws apigateway put-method \
  --region "$region" \
  --rest-api-id "$api_id" \
  --resource-id "$resource_id" \
  --http-method GET \
  --authorization-type NONE \
  --no-api-key-required \
  --request-parameters '{}'

Add a Method Response for status 301 with a required Location HTTP header:

aws apigateway put-method-response \
  --region "$region" \
  --rest-api-id "$api_id" \
  --resource-id "$resource_id" \
  --http-method "GET" \
  --status-code 301 \
  --response-models '{"application/json":"Empty"}' \
  --response-parameters '{"method.response.header.Location":true}'

Set the GET method integration to MOCK with a default 301 status code. By using a mock integration, we don’t need a back end.

aws apigateway put-integration \
  --region "$region" \
  --rest-api-id "$api_id" \
  --resource-id "$resource_id" \
  --http-method GET \
  --type MOCK \
  --request-templates '{"application/json":"{\"statusCode\": 301}"}'

Add an Integration Response for GET method status 301. Set the Location header to the redirect target URL.

aws apigateway put-integration-response \
  --region "$region" \
  --rest-api-id "$api_id" \
  --resource-id "$resource_id" \
  --http-method GET \
  --status-code 301 \
  --response-templates '{"application/json":"redirect"}' \
  --response-parameters \
    '{"method.response.header.Location":"'"'$target_url'"'"}'

2. Create API Gateway Deployment and Stage using aws-cli

The deployment and its first stage are created with one command:

deployment_id=$(aws apigateway create-deployment \
  --region "$region" \
  --rest-api-id "$api_id" \
  --description "$api_name deployment" \
  --stage-name "$stage_name" \
  --stage-description "$api_name $stage_name" \
  --no-cache-cluster-enabled \
  --output text \
  --query 'id')
echo deployment_id=$deployment_id

If you want to add more stages for the deployment, you can do it with the create-stage sub-command.

At this point, we can actually test the redirect using the endpoint URL that is printed by this command:

echo "https://$api_id.execute-api.$region.amazonaws.com/$stage_name$resource_path"

3. Create API Gateway Domain Name using aws-cli

The API Gateway Domain Name seems to be a CloudFront distribution with an SSL Certificate, though it won’t show up in your normal CloudFront queries in the AWS account.

distribution_domain=$(aws apigateway create-domain-name \
  --region "$region" \
  --domain-name "$base_domain" \
  --certificate-name "$certificate_name" \
  --certificate-body "file://$certificate_body" \
  --certificate-private-key "file://$certificate_private_key" \
  --certificate-chain "file://$certificate_chain" \
  --output text \
  --query distributionDomainName)
echo distribution_domain=$distribution_domain

aws apigateway create-base-path-mapping \
  --region "$region" \
  --rest-api-id "$api_id" \
  --domain-name "$base_domain" \
  --stage "$stage_name"

4. Set up DNS

All that’s left is to update Route53 so that we can use our preferred hostname for the CloudFront distribution in front of the API Gateway. You can do this with your own DNS if you aren’t managing the domain’s DNS in Route53.

Get the hosted zone id for the source domain:

hosted_zone_id=$(
  aws route53 list-hosted-zones \
    --region "$region" \
    --output text \
    --query 'HostedZones[?Name==`'$base_domain'.`].Id'
)
hosted_zone_id=${hosted_zone_id#/hostedzone/}
echo hosted_zone_id=$hosted_zone_id

Add an Alias record for the source domain, pointing to the CloudFront distribution associated with the API Gateway Domain Name.

cloudfront_hosted_zone_id=Z2FDTNDATAQYW2
change_id=$(aws route53 change-resource-record-sets \
  --region "$region" \
  --hosted-zone-id $hosted_zone_id \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "'$base_domain'",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "'$cloudfront_hosted_zone_id'",
          "DNSName": "'$distribution_domain'",
          "EvaluateTargetHealth": false
  }}}]}' \
  --output text \
  --query 'ChangeInfo.Id')
echo change_id=$change_id

This could be a CNAME if you are setting up a hostname that is not a bare apex domain, but the Alias approach works in all Route53 cases.

Once this is all done, you may still need to wait 10-20 minutes while the CloudFront distribution is deployed to all edge nodes, and for the Route53 updates to complete.

Eventually, however, hitting the source domain in your browser should automatically redirect to the target URL. Here is my example in action:

EricHammond.xyz

Using the above as a starting point, we can now expand into more advance setups with the API Gateway and the aws-cli.