Skip to main content

One post tagged with "deployments"

View All Tags

New Railway Provider Available

· 8 min read
Technologist and Cloud Consultant

We've released a new StackQL provider for Railway:

  • railway - the Railway public API: projects, environments, services, deployments, variables, networking, storage, billing, observability, workspaces, account, templates, integrations, platform and agents (15 services, 151 resources, 396 operations)

The provider reads and writes. Projects, environments, services, variables, domains and volumes can be queried, created, changed and removed, and deployment operations such as redeploy, restart and rollback are available on the resources they act on. The rest of this post is the questions it answers and the tasks it handles.

Connect​

Authentication is a Railway account token or workspace token, read from RAILWAY_TOKEN - the same variable the Terraform Railway provider uses:

export RAILWAY_TOKEN=...
stackql shell

Tokens are created under Account settings -> Tokens. A project token does not work with the provider. Railway limits API requests per hour by plan (100 on Free, 1000 on Hobby, 10000 on Pro), which is worth keeping in mind for queries that cover many projects or environments.

What is deployed where​

The workspaces a token can reach, the projects in one, and the environments of a project:

SELECT id, name, plan
FROM railway.workspaces.workspaces;

SELECT id, name, description, created_at
FROM railway.projects.projects
WHERE workspace_id = '<workspace-id>'
ORDER BY created_at DESC;

SELECT id, name, is_ephemeral
FROM railway.environments.environments
WHERE project_id = '<project-id>';

A service instance is a service as it is configured in one environment. Listing the instances of an environment shows what runs there, from which image or repository, and how the last deployment went:

SELECT service_name, region, num_replicas,
json_extract(source, '$.image') AS image,
json_extract(source, '$.repo') AS repo,
json_extract(latest_deployment, '$.status') AS last_deployment,
json_extract(latest_deployment, '$.created_at') AS deployed_at
FROM railway.services.service_instances
WHERE environment_id = '<environment-id>';

An IN list covers several environments in one query, with one request made per environment. Joining to environments puts names on the rows:

SELECT e.name AS environment, s.service_name, s.region, s.num_replicas,
json_extract(s.latest_deployment, '$.status') AS last_deployment
FROM railway.services.service_instances s
JOIN railway.environments.environments e ON e.id = s.environment_id
WHERE e.project_id = '<project-id>'
AND s.environment_id IN ('<production-environment-id>', '<staging-environment-id>')
ORDER BY environment, service_name;

The same works across projects, here for the services of two projects in a workspace:

SELECT p.name AS project, s.name AS service, s.created_at
FROM railway.services.services s
JOIN railway.projects.projects p ON p.id = s.project_id
WHERE p.workspace_id = '<workspace-id>'
AND s.project_id IN ('<project-id>', '<other-project-id>')
ORDER BY project, service;

Deployment health​

Deployment outcomes for a project, then the ones that failed or crashed, with who triggered them:

SELECT status, count(*) AS deployments
FROM railway.deployments.deployments
WHERE project_id = '<project-id>'
GROUP BY status;

SELECT id, service_id, status, created_at,
json_extract(creator, '$.name') AS deployed_by
FROM railway.deployments.deployments
WHERE project_id = '<project-id>'
AND status = '{in: [FAILED, CRASHED]}'
ORDER BY created_at DESC;

Services in an environment whose latest deployment is not healthy:

SELECT service_name,
json_extract(latest_deployment, '$.id') AS deployment_id,
json_extract(latest_deployment, '$.status') AS status
FROM railway.services.service_instances
WHERE environment_id = '<environment-id>'
AND json_extract(latest_deployment, '$.status') NOT IN ('SUCCESS', 'SLEEPING');

From there, the build and runtime logs of a deployment are tables. LIMIT sets how many lines are fetched:

SELECT timestamp, severity, message
FROM railway.observability.build_logs
WHERE deployment_id = '<deployment-id>'
LIMIT 100;

SELECT timestamp, severity, message
FROM railway.observability.deployment_logs
WHERE deployment_id = '<deployment-id>'
AND filter = 'error'
LIMIT 100;

Drift between environments​

Variables are one row per variable, so comparing two environments is a join. Variables a service has in production that are missing in staging:

SELECT prod.name
FROM railway.variables.variables prod
LEFT JOIN railway.variables.variables stg
ON stg.name = prod.name
WHERE prod.project_id = '<project-id>'
AND prod.environment_id = '<production-environment-id>'
AND prod.service_id = '<service-id>'
AND stg.project_id = '<project-id>'
AND stg.environment_id = '<staging-environment-id>'
AND stg.service_id = '<service-id>'
AND stg.name IS NULL
AND prod.name NOT LIKE 'RAILWAY_%';

Variables set in both environments with different values:

SELECT prod.name
FROM railway.variables.variables prod
JOIN railway.variables.variables stg
ON stg.name = prod.name
WHERE prod.project_id = '<project-id>'
AND prod.environment_id = '<production-environment-id>'
AND prod.service_id = '<service-id>'
AND stg.project_id = '<project-id>'
AND stg.environment_id = '<staging-environment-id>'
AND stg.service_id = '<service-id>'
AND prod.value <> stg.value
AND prod.name NOT LIKE 'RAILWAY_%';

The variables Railway provides itself are prefixed RAILWAY_ and differ between environments by design, so they are left out. Both queries return names only. Selecting value returns the values in clear, so treat that output as a secret.

The same approach compares how a service is configured in each environment:

SELECT prod.service_name,
prod.num_replicas AS prod_replicas, stg.num_replicas AS staging_replicas,
prod.region AS prod_region, stg.region AS staging_region,
prod.start_command AS prod_start_command, stg.start_command AS staging_start_command
FROM railway.services.service_instances prod
JOIN railway.services.service_instances stg
ON stg.service_id = prod.service_id
WHERE prod.environment_id = '<production-environment-id>'
AND stg.environment_id = '<staging-environment-id>';

Domains and certificates​

The generated domains of every service in an environment, without going service by service. The custom_domains key of the same column holds the custom ones:

SELECT s.service_name,
json_extract(d.value, '$.domain') AS domain,
json_extract(d.value, '$.target_port') AS target_port
FROM railway.services.service_instances s,
json_each(json_extract(s.domains, '$.service_domains')) d
WHERE s.environment_id = '<environment-id>';

Custom domains carry their verification and certificate state, and the DNS records Railway expects to find:

SELECT domain,
json_extract(status, '$.verified') AS verified,
json_extract(status, '$.certificate_status') AS certificate_status,
json_extract(status, '$.dns_records') AS dns_records
FROM railway.networking.custom_domains
WHERE project_id = '<project-id>'
AND environment_id = '<environment-id>'
AND service_id = '<service-id>';

Usage and cost​

Usage for a workspace by measurement, grouped by project, with project names joined in:

SELECT p.name AS project, u.measurement, u.value
FROM railway.billing.usage u
JOIN railway.projects.projects p
ON p.id = json_extract(u.tags, '$.project_id')
WHERE u.workspace_id = '<workspace-id>'
AND u.measurements = '[CPU_USAGE, MEMORY_USAGE_GB, NETWORK_TX_GB, DISK_USAGE_GB]'
AND u.group_by = '[PROJECT_ID]'
AND p.workspace_id = '<workspace-id>'
ORDER BY project, measurement;

start_date and end_date narrow the window. The projected usage for the current billing period, and the billing position of the workspace:

SELECT project_id, measurement, estimated_value
FROM railway.billing.estimated_usage
WHERE workspace_id = '<workspace-id>'
AND measurements = '[CPU_USAGE, MEMORY_USAGE_GB]';

SELECT state, current_usage, credit_balance,
json_extract(billing_period, '$.end') AS period_ends,
json_extract(usage_limit, '$.hard_limit') AS hard_limit
FROM railway.billing.customers
WHERE workspace_id = '<workspace-id>';

Volumes report their allocated and used size per environment:

SELECT json_extract(volume, '$.name') AS volume, mount_path, region, state,
size_mb, current_size_mb,
ROUND(100.0 * current_size_mb / size_mb, 1) AS used_pct
FROM railway.storage.volume_instances
WHERE environment_id = '<environment-id>';

Provisioning​

A project is created with one environment. RETURNING gives back the identifiers the next statements need:

INSERT INTO railway.projects.projects (name, description, workspace_id)
SELECT 'orders', 'order processing', '<workspace-id>'
RETURNING id, name, primary_environment_id;

INSERT INTO railway.environments.environments (project_id, name)
SELECT '<project-id>', 'staging'
RETURNING id, name;

A service from a container image or a repository. A service with a source is deployed when it is created, and a running deployment is billed:

INSERT INTO railway.services.services (project_id, name, source)
SELECT '<project-id>', 'cache', '{"image": "redis:7-alpine"}'
RETURNING id, name;

INSERT INTO railway.services.services (project_id, name, source, branch)
SELECT '<project-id>', 'api', '{"repo": "my-org/orders-api"}', 'main'
RETURNING id, name;

Variables are written one at a time or as a set. Writing a variable that exists replaces its value, and skip_deploys keeps the change from triggering a deployment:

INSERT INTO railway.variables.variables
(project_id, environment_id, service_id, name, value, skip_deploys)
SELECT '<project-id>', '<environment-id>', '<service-id>', 'LOG_LEVEL', 'info', true;

INSERT INTO railway.variables.variables
(project_id, environment_id, service_id, variables, skip_deploys)
SELECT '<project-id>', '<environment-id>', '<service-id>',
'{"SENTRY_DSN": "https://example.ingest.sentry.io/1", "FEATURE_FLAGS": "checkout-v2"}', true;

A generated domain and a volume for the service:

INSERT INTO railway.networking.service_domains (environment_id, service_id)
SELECT '<environment-id>', '<service-id>'
RETURNING id, domain;

INSERT INTO railway.storage.volumes (project_id, environment_id, service_id, mount_path)
SELECT '<project-id>', '<environment-id>', '<service-id>', '/data'
RETURNING id, name;

Day-two operations​

Changing how a service runs in an environment is an UPDATE. Values in SET are written as quoted strings:

UPDATE railway.services.service_instances
SET num_replicas = '2',
start_command = 'npm run start',
healthcheck_path = '/healthz'
WHERE service_id = '<service-id>'
AND environment_id = '<environment-id>';

Deployment operations are EXEC methods. The SHOWRESULTS hint returns the API's answer, including a refusal:

EXEC /*+ SHOWRESULTS */ railway.services.service_instances.redeploy
@service_id = '<service-id>',
@environment_id = '<environment-id>';

EXEC /*+ SHOWRESULTS */ railway.deployments.deployments.restart
@id = '<deployment-id>';

EXEC /*+ SHOWRESULTS */ railway.deployments.deployments.rollback
@id = '<deployment-id>';

Preview environment cleanup​

Pull request environments accumulate. The ephemeral environments of a project, with the pull request each one belongs to:

SELECT id, name, created_at,
json_extract(meta, '$.pr_number') AS pr_number,
json_extract(meta, '$.branch') AS branch
FROM railway.environments.environments
WHERE project_id = '<project-id>'
AND is_ephemeral = true
ORDER BY created_at;

Removing one is a DELETE. Running the listing again confirms it is gone:

DELETE FROM railway.environments.environments
WHERE id = '<environment-id>';

A join across providers​

Services deployed from a GitHub repository can be matched to the repository itself, for example to find services still deploying from a repository that has been archived or has not been pushed to in some time. This uses the github provider alongside railway:

SELECT s.service_name,
r.full_name AS repo, r.archived, r.pushed_at
FROM railway.services.service_instances s
JOIN github.repos.repos r
ON r.full_name = json_extract(s.source, '$.repo')
WHERE s.environment_id = '<environment-id>'
AND r.org = 'my-org'
ORDER BY r.pushed_at;

Get started​

Pull the provider from the public registry:

registry pull railway;

Provider docs are at railway-provider.stackql.io. Let us know what you build. Star us on GitHub.