Deploying Server with GitHub Actions and Fly.io

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

  1. The Fly Deploy workflow starts on push to main when changes occur in paths associated with the server.
  2. GitHub authenticates with AWS using OIDC.
  3. It invokes a Lambda that returns a short-lived Fly deploy token.
  4. It sets this token as an environment variable for use in the deployment step.
  5. 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

  1. A personal access token (PAT) is created via browser login and stored locally in ~/.fly/config.yml.
  2. This PAT can then be used to generate short-lived deploy tokens via the flyctl CLI.
  3. 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