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 π
Composite Action: Set π
We’ll create a Composite Action with three inputs defined:
key
- the ‘key’, a FQDN (fully qualified domain name)value
- the valuehosted-zone-id
- the Hosted Zone ID assigned by AWS
Then, we’ll execute two steps:
- Create JSON Payload - we create
payload.json
for our request using thekey
andvalue
inputs to the Action. - Set Record - we call
awscli
, specifying thehosted-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 }}