- Explore creating a custom executor
- Go through an example of how to deploy an API to Fly.io through Nx
-
For this workshop you'll need two CLI tools installed:
-
Let's prepare Fly to deploy our API:
# login first fly auth login # Get an authorization token so we don't have to login everytime fly auth token
-
Let's setup our ENV variables from the beginning now
apps/api/.local.env
FLY_API_TOKEN=<your-fly-token>
-
Create a new file
apps/api/src/fly.toml
Pick a unique app name to include in the
fly.toml
file.👉 This will determine the address where the API will be deployed to:
https://<your-app-name>.fly.dev
app = "<your-unique-app-name>" kill_signal = "SIGINT" kill_timeout = 5 processes = [] [build] builder = "paketobuildpacks/builder:base" buildpacks = ["gcr.io/paketo-buildpacks/nodejs"] [env] PORT = "8080" [experimental] cmd = ["PORT=8080 node main.js"] [[services]] http_checks = [] internal_port = 8080 processes = ["app"] protocol = "tcp" script_checks = [] [services.concurrency] hard_limit = 25 soft_limit = 20 type = "connections" [[services.ports]] force_https = true handlers = ["http"] port = 80 [[services.ports]] handlers = ["tls", "http"] port = 443 [[services.tcp_checks]] grace_period = "1s" interval = "15s" restart_limit = 0 timeout = "2s"
❓ What's our plan here?
Fly will launch a pre-build node Docker image (or you could provide your own) and then run the command you specify to launch the server.
So the plan is:
- define a
fly.toml
with instructions for fly to deploy the server - when we want to deploy, we'll build our app to
dist/apps/api
- as part of the build, we need to make sure that our
fly.toml
file makes it intodist/apps/api
- Fly will copy the bundled code to the remote server and run the node server via
cmd = ["PORT=8080 node main.js"]
- define a
-
If you
nx build api
right now- 👍 Then `cd dist/apps/api && node main.js` It should work. Because it has access to `node_modules` - 👎 If you copy your built sources to some other folder on your file system. And then try to `node main.js` in that folder that doesn't hace access to `node_modules` - it will fail 💡 By default, dependencies of server projects are not bundled together, as opposed to your React apps.
-
Currently the
fly.toml
that we added to ourapi
project is not present if we inspect thedist/apps/api
directory after running a prod build. We'll need this to be present for our fly deployment.Update the the
assets
option in the production build options for the API (targets -> build -> configurations -> production
)"assets": [ "apps/api/src/assets", "apps/api/src/fly.toml" ],
-
Use the
@nx/plugin:executor
generator to generate afly-deploy
executor:- The executor should have options for:
- the target
dist
location - the
name
of your fly app
- the target
- When running, your executor should perform the following tasks, using the
fly
cli:- list the current fly apps:
fly apps list
- if the app doesn't exist, launch it:
fly launch --now --name=<the name of your Fly App> --region=lax
- if the app does exist, deploy it again:
fly deploy
- list the current fly apps:
Fly launch and deploy commands need to be run in the
dist
location of your app.Use the
@nx/plugin:executor
to generator an executor in ourinternal-plugin
project for this:npx nx generate @nx/plugin:executor fly-deploy --project=internal-plugin
- The executor should have options for:
-
Adjust the generated
schema.json
andschema.d.ts
file to match the required options:{ "$schema": "http://json-schema.org/schema", "cli": "nx", "title": "FlyDeploy executor", "description": "", "type": "object", "properties": { "distLocation": { "type": "string" }, "flyAppName": { "type": "string" } }, "required": ["distLocation", "flyAppName"] }
export interface FlyDeployExecutorSchema { distLocation: string; flyAppName: string; }
-
Implement the required fly steps using
execSync
to call thefly
cli inside yourexecutor.ts
file:import { FlyDeployExecutorSchema } from './schema'; import { execSync } from 'child_process'; export default async function runExecutor( options: FlyDeployExecutorSchema ) { const cwd = options.distLocation; const results = execSync(`fly apps list`); if (results.toString().includes(options.flyAppName)) { execSync(`fly deploy`, { cwd }); } else { // consult https://fly.io/docs/reference/regions/ to get best region for you execSync(`fly launch --now --name=${options.flyAppName} --region=lax`, { cwd, }); } return { success: true, }; }
-
Next we'll need to add a
deploy
target to ourapps/api/project.json
file (don't forget to put your apps name inflayAppName
field):{ "deploy": { "executor": "@bg-hoard/internal-plugin:fly-deploy", "outputs": [], "options": { "distLocation": "dist/apps/api", "flyAppName": "my-unique-app-name" }, "dependsOn": [ { "target": "build", "projects": "self", "params": "forward" } ] } }
-
Let's enable CORS on the server so our API can make requests to it (since they'll be deployed in separate places): -
yarn add cors
ornpm i -S cors
- Inapps/api/src/main.ts
- Enable CORS:import * as cors from 'cors'; app.use(cors());
⚠️ Normally, you want to restrict this to just a few origins. But to keep things simple in this workshop we'll enable it for all origins.
-
Now run the command to deploy your api!!
npx nx deploy api --prod
Because of how we set up our
dependsOn
for thedeploy
target, Nx will know that it needs to run (or pull from the cache if you already ran it) the production build of the api before then running the deploy! -
Go to
https://<your-app-name>.fly.dev/api/games
- it should return you a list of games.⚠️ Since we are on a free tier, it might take some time for application to become available
-
BONUS - What would a meaningful test be for your new executor? Add it to
libs/internal-plugin/src/executors/fly-deploy/executors.spec.ts
🎓 If you get stuck, check out the solution