Deploying Server with GitHub Actions and Fly.io

I am setting up a CICD pipeline in continuation of my Dallas Police Active Calls project. Terraform infrastructure, the data pipeline and the application server all reside in the same project, but I want to use GitHub actions to keep the infrastructure and application deployments loosely coupled.
CICD Workflow
- The Fly Deploy workflow starts on push to main when changes occur in paths associated with the server.
- GitHub authenticates with AWS using OIDC.
- It invokes a Lambda that returns a short-lived Fly deploy token.
- It sets this token as an environment variable for use in the deployment step.
- Finally, it runs
flyctl deploy
with the token to publish the Docker image and deploy the app.

Project Layout
All server-side code is located in the ./server
directory, and the entire service is containerized. The Dockerfile sits at the root of the repository.
I am hosting the server on Fly.io. To deploy via the Fly CLI (flyctl
), an app deploy token is required. According to Fly's documentation, this deploy token is short-lived and can only be created by a user who is already authenticated via a browser-based login flow.
The Fly.io Authentication Flow
- A personal access token (PAT) is created via browser login and stored locally in
~/.fly/config.yml
. - This PAT can then be used to generate short-lived deploy tokens via the
flyctl
CLI. - GitHub Actions needs the deploy token in order to authenticate with Fly.io and push the Docker image.
Avoid Storing Long-lived Tokens in GitHub
I want to avoid having long-lived credentials stored in GitHub. Rather than saving the PAT directly in GitHub Secrets, I stored it in AWS using the Systems Manager Parameter Store SecureString.
aws ssm put-parameter \
--name "/test-app/key" \
--value "$(cat ~/.fly/config.yml | grep access_token)" \
--type "SecureString" \
--description "PAT for fly.io"
I wrote a Lambda that reads the PAT from the Parameter Store, uses the flyctl cli to generate a deploy token and returns the deploy token in its response. I added a flyctl lambda layer in order to make the flyctl cli available to the lambda.
Creating the flyctl lambda layer:
set -e
mkdir -p ./bin
curl -L https://fly.io/install.sh | sh
cp ~/.fly/bin/flyctl ./bin
zip -r9 flyctl-layer.zip .
resource "aws_lambda_layer_version" "flyctl_layer" {
filename = var.fly_zip_path
layer_name = "flyctl_layer"
}
The Lambda function:
import subprocess
import os
import boto3
def lambda_handler(event, context):
"""
Get PAT stored in ssm and create a short-live deploy token for fly.io
"""
FLY_TOKEN_PARAM = os.getenv("FLY_TOKEN_PARAM")
APPLICATION_NAME = os.getenv("APPLICATION_NAME")
ssm = boto3.client("ssm")
pat = ssm.get_parameter(Name=FLY_TOKEN_PARAM, WithDecryption=True)["Parameter"]["Value"]
os.environ["HOME"] = "/tmp"
result = subprocess.run(
["flyctl", "tokens", "create", "deploy", "--name", "github-deploy-token", "--app", APPLICATION_NAME, "--expiry", "15m", "-t", pat],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if result.returncode != 0:
raise Exception(result.stderr.decode())
output = result.stdout.decode().strip()
return {
'statusCode': 200,
'body': output
}
To enable GitHub Actions to invoke this Lambda securely, I set up GitHub OIDC integration with AWS and created a role that trusts the GitHub OIDC provider and grants permission to invoke the Lambda.
The github workflow: https://github.com/mwgolden/dallas-police-active-calls/blob/main/.github/workflows/fly-deploy.yml
References:
https://fly.io/docs/security/tokens/
https://docs.github.com/en/actions/concepts/security/openid-connect
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html