Using Route53 as a Key Value Store in GitHub Actions

Dec 13, 2021 00:00 Β· 828 words Β· 4 minute read

The Need πŸ”—

GitHub Actions is great, but one thing it’s missing is a form of persistence, or simple key-value store. Sure, you could persist to a git repo - and that might be the right move, depending on the use case. But, what if you just want to store a string or two that you’ll need later?

AWS Route53? πŸ”—

Route53 is a highly-available DNS service offered by AWS. It’s automatically replicated across the globe. It’s cheap, too. Each hosted zone costs $0.50USD/month, and a million queries is $0.40USD.

Since it’s a name server, you can store a variety of records, one of which is TXT - or a text record. The DNS TXT record was originally intended for human-readable text, or notes, about a DNS record. It’s the perfect candidate to hold a string for us.

DNS zones and their records, by their nature, are publicly accessible to the Internet. However, Route53 also provides Private Hosted Zones, which are DNS zones meant only to be used inside of a VPC (Virtual Private Cloud). We can use these Private Hosted Zones to store records that are only retrievable in a VPC or via the AWS API with credentials.

Game Plan πŸ”—

We can use Route53 as our key value store, but how do we store or fetch data? Since we’re using GitHub Actions, we’ll use a reusable Composite Action. We’ll create two Actions – one that sets data, and one that gets data.

We’ll use the AWS CLI utility known as awscli to access the AWS API.

Our data setting Action will use awscli to UPSERT the value of the key as the TXT value of the hostname record using the change-resource-record-sets command and providing a JSON payload we’ll construct.

Our data getting Action will also use awscli, but it will fetch the TXT value of the hostname record using the list-resource-record-sets command.

We don’t want our data to be publicly available, so we’re using Private Hosted Zones. This means we’ll need to use the AWS API to fetch our data, in this case using awscli. But, if you were to use a publicly available zone, you could just use nslookup --type=txt <key/hostname>.

Below, we’ll look out how each Action works.

Creating a Private Hosted Zone πŸ”—

You create a Private Hosted Zone the same way as any other Route53 zone, but you need to specify a few other options like the region and VPC. You can do this via the AWS Console GUI or using awscli at a terminal.

Since this zone is completely isolated to our account and VPCs, we can name it anything. The zone name is a namespace, and you can have many. In this case, we just name it kvstore.

It doesn’t matter which VPCs we associate with the zone, as we’ll just be using the API to get/set the values.

Terminal πŸ”—

aws route53 create-hosted-zone --name kvstore \
--caller-reference 2021-12-13-00:47 \
--hosted-zone-config PrivateZone=true \
-β€”vpc VPCRegion=us-east-1,VPCId=vpc-ccab2fb4

AWS Console πŸ”—

AWS Console

Composite Action: Set πŸ”—

We’ll create a Composite Action with three inputs defined:

  • key - the ‘key’, a FQDN (fully qualified domain name)
  • value - the value
  • hosted-zone-id - the Hosted Zone ID assigned by AWS

Then, we’ll execute two steps:

  1. Create JSON Payload - we create payload.json for our request using the key and value inputs to the Action.
  2. Set Record - we call awscli, specifying the hosted-zone-id we pass in as an input, as well as the path to our payload JSON.
name: 'Set Route53 TXT Record'
inputs:
  key:
    description: 'FQDN of Key'
    required: true
  value:
    description: 'Value to set'
    required: true
  hosted-zone-id:
    description: 'Route53 Hosted Zone ID'
    required: true
runs:
  using: "composite"
  steps:
  
    - name: Create JSON Payload
      run: |
        cat << EOF >> payload.json
        {
          "Changes": [{
          "Action": "UPSERT",
          "ResourceRecordSet": {
            "Name": "${{ inputs.key }}",
            "Type": "TXT",
            "TTL": 30,
            "ResourceRecords": [{ "Value": "\"${{ inputs.value }}\""}]
          }
         }]
        }
        EOF
        cat payload.json
      shell: bash

    - name: Set Record
      run: |
        aws route53 change-resource-record-sets \
        --hosted-zone-id ${{ inputs.hosted-zone-id }} \
        --change-batch file://payload.json
      shell: bash

Composite Action: Get πŸ”—

Our Get Composite Action will also need inputs defined:

  • key - the β€˜key’ to fetch, a FQDN (fully qualified domain name)
  • hosted-zone-id - the Hosted Zone ID assigned by AWS

Then, we’ll execute a single step that fetches the value for the key, strips quotations, and sets the value of the value output to the value of the key.


name: 'Get Route53 TXT Record'

inputs:
  key:
    description: 'FQDN of Key'
    required: true
  hosted-zone-id:
    description: 'Route53 Hosted Zone ID'
    required: true
    
outputs:
  value:
    description: 'Value of key'
    value: ${{ steps.get-record.outputs.value }}

runs:
  using: "composite"
  steps:
    - name: Get Record
      id: get-record
      run: |
        VALUE=`aws route53 list-resource-record-sets \
        --hosted-zone-id ${{ inputs.hosted-zone-id }} \
        --query "ResourceRecordSets[?Name == '${{ inputs.key }}.']" \
        | jq '.[0].ResourceRecords[0].Value' \
        | tr -d '"' | tr -d '\'`

        echo "::set-output name=value::$VALUE"
      shell: bash

Using the Actions πŸ”—

Now that we have our two Actions, we can put them to use in a Workflow.

We’ll need to specify three environment variables for AWS, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_DEFAULT_REGION.

When using the get action, the value of the key will be available as the value output of the step. You can access it’s value using ${{ steps.<step-id>.outputs.value }}

name: Route53 KV Store Actions Example
on: [workflow_dispatch]
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: us-east-1

   steps:
      - uses: actions/checkout@v2

      - uses: ./actions/set
        with: 
          key: testkey.kvstore
          value: "hello world"
          hosted-zone-id: Z0750343I14B1A1HAP9
        
      - uses: ./actions/get
        id: get-testkey
        with:
          key: testkey.kvstore
          hosted-zone-id: Z0750343I14B1A1HAP9

      - run: echo ${{ steps.get-testkey.outputs.value }}