batchbru

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 33 Imported by: 0

README

batchbru

A CLI tool for managing AWS Batch job definitions as code. Think of it as ecspresso for AWS Batch job definitions: write your job definitions in JSON / Jsonnet / YAML, review them with diff, then deploy.

Compute environments and job queues are out of scope. Manage those with Terraform or similar. So are scheduling policies and production job submission; run exists only to smoke-test a definition right after deploying it.

Why batchbru?

When you need it — and when you don't

SubmitJob can override a job's command, environment and resources at submission time, but not its container image. If you use immutable image tags (recommended), every application release therefore needs a new job definition revision — the definition changes on the application's release cadence, not the infrastructure's. batchbru exists for exactly that situation: registering revisions from a release pipeline, with a reviewable diff, without touching anything else.

Conversely, if your job definitions change rarely, or you use a mutable image tag that you repoint on release, managing the definitions in Terraform alongside the compute environment is perfectly fine and you may not need this tool.

Relationship to Terraform

Terraform can manage job definitions too, and batchbru is not a replacement for it. What batchbru changes is the workflow for the part that moves with application releases:

  • Deploy cadence separation. Registering a revision from an application release pipeline with Terraform means running terraform apply — dragging the entire infrastructure state, its lock, and its blast radius into every application deploy. batchbru only ever touches job definitions.
  • No state file. Revisions are immutable and the latest ACTIVE one is authoritative, so batchbru diffs directly against what is actually in AWS. There is no state to lock, drift, or reconcile.
  • Straight mapping to the API. Terraform models the immutable revision history as one updatable resource, which makes the history and rollback awkward. batchbru embraces the API: deploy is RegisterJobDefinition, rollback deregisters the newest revision so the previous one becomes current, and old revisions remain available.

The two compose: compute environments, job queues, IAM roles and ECR repositories stay in Terraform, and batchbru reads their outputs through the tfstate template function.

Design focus

batchbru's emphasis is on making unattended CI deploys safe:

  • The diff is normalized — API-materialised defaults are pruned, member names are canonical — so deploy is idempotent and can run unconditionally from CI without piling up redundant revisions.
  • Typos in a definition are an error, never silently dropped members.
  • Destructive commands have guardrails: rollback and deregister refuse to leave a job definition with zero ACTIVE revisions, and deregister prompts before acting.
  • One configuration file manages many definitions (globs, --name narrowing), in JSON, Jsonnet, or YAML.

Installation

$ go install github.com/ikaro1192/batchbru/cmd/batchbru@latest
GitHub Actions

This repository is itself an action that puts batchbru on PATH, so the commands can be used from a workflow as they are on a terminal:

- uses: ikaro1192/batchbru@main   # pin to a release tag once one exists
  with:
    version: v0.1.0   # or "latest", the default
- uses: aws-actions/configure-aws-credentials@v6
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    aws-region: ap-northeast-1
- run: batchbru diff
Input Default Description
version latest Release tag to install, or latest
token ${{ github.token }} Token used to reach this repository
go-version stable Go version used when batchbru has to be built from source

The action installs a published release archive when there is one, and otherwise builds from source with Go, so it also works for a revision that was never released.

While this repository is private, token needs read access to it: the default ${{ github.token }} only works for workflows in this repository, and using the action from another repository requires a personal access token.

A typical pipeline reviews on pull requests and deploys on merge:

- run: batchbru diff        # on pull requests: show what deploying would change
- run: batchbru deploy      # on the default branch: registers nothing when unchanged
- run: batchbru run --name etl-daily --queue default --wait   # smoke-test the new revision

batchbru diff --exit-code fails when the local definitions and AWS disagree, which makes it a drift check to run on a schedule.

Usage

Import an existing job definition
$ batchbru init --job-definition etl-daily
wrote jobdefs/etl-daily.jsonnet
wrote batchbru.yml
Configuration file
# batchbru.yml
region: ap-northeast-1

job_definitions:
  - jobdefs/*.jsonnet
  - jobdefs/*.yaml

plugins:
  - name: tfstate
    config:
      url: s3://my-bucket/terraform.tfstate

Both job_definitions and local tfstate paths are resolved relative to the configuration file. A single configuration file can manage multiple job definitions, and every command can narrow the target with --name (wildcards allowed, repeatable).

Job definition files

Job definitions use the same format as the RegisterJobDefinition API input (the same shape as aws batch register-job-definition --cli-input-json). The content is passed to the API as-is, so all types are supported: container, multinode, ECS, and EKS.

// jobdefs/etl-daily.jsonnet
{
  jobDefinitionName: 'etl-daily',
  type: 'container',
  platformCapabilities: ['FARGATE'],
  containerProperties: {
    image: '{{ tfstate `aws_ecr_repository.example.repository_url` }}:{{ must_env `IMAGE_TAG` }}',
    jobRoleArn: '{{ tfstate `aws_iam_role.batch_job.arn` }}',
    resourceRequirements: [
      { type: 'VCPU', value: '1' },
      { type: 'MEMORY', value: '2048' },
    ],
  },
  retryStrategy: { attempts: 3 },
}

Working examples live in example/.

Deploying
$ batchbru diff
$ batchbru deploy
etl-daily: registered revision 12
report: no changes, skipping (use --force to register anyway)

deploy registers a new revision only when there is a difference from the latest ACTIVE revision. It never piles up redundant revisions with identical content, so it is safe to run repeatedly from CI.

Rolling back

AWS Batch has no rollback API, so rollback deregisters the latest ACTIVE revision, effectively making the previous revision current again.

$ batchbru rollback
etl-daily: deregister revision 12, making revision 11 current

If only one ACTIVE revision exists, the command fails to avoid leaving the job definition unusable.

Smoke-testing with run

run submits a one-off job using the latest ACTIVE revision — that is, what deploy just registered — and with --wait follows it until it succeeds or fails, so a broken definition stops the pipeline right there.

$ batchbru run --name etl-daily --queue default --wait
etl-daily: submitted job 4f0f8a9c-... to queue default
etl-daily: RUNNABLE
etl-daily: RUNNING
etl-daily: SUCCEEDED

run targets exactly one job definition and supports no overrides, dependencies, or array jobs. It is a smoke test, not a job runner: production job submission and scheduling are intentionally out of scope, because they belong to whatever orchestrates your jobs (EventBridge Scheduler, Step Functions, your application), not to a deploy tool.

Cleaning up old revisions
$ batchbru deregister --keep-latest 5

--keep-latest must be 1 or greater. Specifying 0 would mark every revision INACTIVE and render the job definition unusable, so it is rejected for the same reason as rollback.

Commands

Command Description
init Import an existing job definition from AWS and start managing it
render Print the JSON produced after template evaluation
diff Show the difference against the latest ACTIVE revision (--exit-code for CI)
deploy Register a new revision for job definitions that have changed
run Submit a one-off job to smoke-test the latest ACTIVE revision (--wait to follow it)
status Show the latest ACTIVE revision of each job definition
revisions List revisions (--all to include INACTIVE ones)
rollback Deregister the latest ACTIVE revision
deregister Deregister revisions (--revision / --keep-latest)
version Print the version

Template functions

Configuration files and job definition files are evaluated as text/template.

Function Description
env "NAME" "default" Read an environment variable
must_env "NAME" Read an environment variable; error if undefined
ssm "/path/to/param" Fetch a value from SSM Parameter Store (SecureString is decrypted)
json_escape Escape a value as a JSON string
tfstate "TYPE.NAME.ATTR" Read a resource attribute from Terraform state (tfstate plugin)

From Jsonnet, the same functions are available as std.native('env') / std.native('must_env') / std.native('ssm') / std.native('tfstate'). Use --ext-str / --ext-code to pass Jsonnet external variables.

When using multiple tfstate sources, set func_prefix. With func_prefix: shared_, reference it as {{ shared_tfstate ... }}.

Required IAM permissions

Command Actions
diff / status / revisions / init batch:DescribeJobDefinitions
deploy The above + batch:RegisterJobDefinition (plus batch:TagResource when using tags)
rollback / deregister The above + batch:DeregisterJobDefinition
run batch:SubmitJob (plus batch:DescribeJobs with --wait)
Template functions (when used) ssm:GetParameter, read access to the tfstate location

Notes and caveats

How AWS Batch shapes the behavior

Job definitions are immutable: RegisterJobDefinition always creates a new revision and existing revisions can never be updated. Revisions are ACTIVE or INACTIVE, and DeregisterJobDefinition makes one INACTIVE (actual deletion happens later on the AWS side). Submitting a job by name without a revision number uses the latest ACTIVE revision. Therefore:

  • deploy means registering a new revision.
  • rollback means deregistering the latest ACTIVE revision so the previous one becomes current.
  • diff compares your local definition against the latest ACTIVE revision.
Configuration
  • Job definition names are not written in the configuration file: the jobDefinitionName inside each job definition file is authoritative. Duplicate names after glob expansion are an error.
  • A glob that matches no file is also an error, so a typo cannot silently reduce the target set to zero.
  • job_definitions paths and tfstate local paths are resolved relative to the configuration file, never to the current working directory.
  • region applies to both ssm lookups inside job definition files and Batch API calls. The one exception is ssm used inside the configuration file itself: region has not been read yet at that point, so the AWS SDK default resolution is used. Set AWS_REGION if you do this.
  • Unknown members (typos) in a job definition are reported as an error rather than silently dropped.
init
  • If the configuration file exists, one line is appended textually to the job_definitions: block list so that comments and template expressions are preserved.
  • If it cannot be edited safely (no list found, flow style such as job_definitions: [a, b], or a Jsonnet configuration file), the configuration file is left completely untouched and the lines to add by hand are printed instead. The job definition file was still written, so this is not an error (exit code 0).
  • Existing job definition files are never overwritten without --overwrite.
diff normalization

Local and remote definitions are normalized through the same path before comparison: both are decoded into RegisterJobDefinitionInput and re-encoded with API member names; the read-only fields (jobDefinitionArn, revision, status, containerOrchestrationType) are stripped from the remote side; and fields absent locally whose remote value is the API default are not reported as differences. If a key is present locally it is always compared, even when its value equals the default.

False diffs

The list of non-empty API defaults is maintained by hand, so when AWS starts materialising a new default that batchbru does not know about, it shows up as a false diff: a member that exists only in the remote definition. This matters because deploy registers whenever diff is non-empty — a false diff would register a redundant revision on every CI run.

diff and deploy therefore print a warning whenever a diff contains members that exist only remotely, naming them. If you removed the member from your file on purpose, the warning is noise — ignore it. If you never wrote that member, it is a default the API filled in:

  • Workaround: add the member with the remote value to your local file; the diff disappears.
  • Please also report it so the default can be added to batchbru and the workaround becomes unnecessary for everyone.
deregister
  • --revision and --keep-latest are mutually exclusive; exactly one must be given.
  • Without --force, the affected revisions are listed and a confirmation prompt is shown.
Exit codes
  • 0: success (including diff finding no changes)
  • 1: runtime error, or diff --exit-code finding changes

When a command targets multiple job definitions and some of them fail, the remaining ones are still processed; errors are reported together at the end with exit code 1.

Development

$ make test
$ make build
$ make lint

make lint and make test are what CI runs (.github/workflows/ci.yml), and .github/workflows/test-action.yml installs batchbru through action.yml on Linux and macOS runners to check that the action itself still works.

Releasing

Pushing a v* tag runs GoReleaser (.goreleaser.yml) and publishes the binaries:

$ git tag v0.1.0 && git push origin v0.1.0

The archives are named batchbru_<os>_<arch>.tar.gz — tar.gz on every platform, and without the version in the file name so that the action can fetch the latest release without resolving it to a tag first. action.yml depends on both of those, so the two files have to be changed together.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Version = "dev"

Version is set at build time.

Functions

func Init

func Init(ctx context.Context, configPath string, opt *InitOption, stdout io.Writer) error

Init imports an existing job definition from AWS and starts tracking it in the configuration file, creating the file if it does not exist yet.

func IsExitCode

func IsExitCode(err error) bool

IsExitCode reports whether err only signals a non-zero exit status.

func MarshalIndent

func MarshalIndent(v any) (string, error)

MarshalIndent renders a job definition as the canonical JSON text used for diffs and for files written by init. Object members are sorted by name.

func Normalize

func Normalize(def map[string]any) (map[string]any, error)

Normalize converts an arbitrary job definition document into the canonical form used for diffing and registration. The document is decoded into the SDK input type (rejecting unknown members, which catches typos) and re-encoded with the member names the Batch API uses.

func PruneDefaults

func PruneDefaults(remote, local map[string]any) map[string]any

PruneDefaults removes members from remote that the API filled in with a default value and that local does not mention, so that a definition which omits optional members does not show a diff against every default the API materialised. Members that local does mention are always kept, even when they hold a default value.

func RemoteOnlyPaths

func RemoteOnlyPaths(remote, local map[string]any) []string

RemoteOnlyPaths lists the dotted paths of members that survived PruneDefaults on the remote side but do not exist in the local definition. Array indexes are elided, the same convention as nonFalsyDefaults. Such a member is either something the user removed on purpose, or a default the API materialised that nonFalsyDefaults does not know about yet -- a false diff.

func RemoteToMap

func RemoteToMap(jd types.JobDefinition) (map[string]any, error)

RemoteToMap converts a job definition read back from the API into the same canonical form as a local definition, so that the two can be compared.

func Run

func Run(ctx context.Context, args []string) error

Run parses the arguments and executes the selected command.

Types

type App

type App struct {
	Config  *Config
	JobDefs []*JobDef
	Batch   BatchAPI

	Debug  bool
	Stdout io.Writer
	Stderr io.Writer
	// WaitInterval is how often run --wait polls the job status. The zero
	// value means the default of 5 seconds.
	WaitInterval time.Duration
}

App holds everything the commands operate on.

func New

func New(ctx context.Context, opts *Options) (*App, error)

New loads the configuration and job definitions and builds the AWS clients.

func (*App) Deploy

func (a *App) Deploy(ctx context.Context, opt *DeployOption) error

Deploy registers a new revision for each job definition that differs from the latest ACTIVE revision.

func (*App) Deregister

func (a *App) Deregister(ctx context.Context, opt *DeregisterOption) error

Deregister makes revisions INACTIVE, either a specific one or everything older than the newest N.

func (*App) Diff

func (a *App) Diff(ctx context.Context, opt *DiffOption) error

Diff compares each local job definition against the latest ACTIVE revision.

func (*App) Render

func (a *App) Render(ctx context.Context, opt *RenderOption) error

Render prints the fully evaluated job definitions as JSON.

func (*App) Revisions

func (a *App) Revisions(ctx context.Context, opt *RevisionsOption) error

Revisions lists the revisions of each job definition, newest first.

func (*App) Rollback

func (a *App) Rollback(ctx context.Context, opt *RollbackOption) error

Rollback deregisters the latest ACTIVE revision so that the one before it becomes the revision jobs resolve to.

func (*App) Run

func (a *App) Run(ctx context.Context, opt *RunOption) error

Run submits a one-off job using the latest ACTIVE revision, meant as a smoke test right after a deploy. Production job submission -- schedules, overrides, dependencies, array jobs -- is out of scope; use SubmitJob directly for that.

func (*App) Status

func (a *App) Status(ctx context.Context, opt *StatusOption) error

Status prints the latest ACTIVE revision of each job definition.

func (*App) Target

func (a *App) Target(names []string) ([]*JobDef, error)

Target returns the job definitions selected by --name.

type BatchAPI

type BatchAPI interface {
	DescribeJobDefinitions(ctx context.Context, params *batch.DescribeJobDefinitionsInput, optFns ...func(*batch.Options)) (*batch.DescribeJobDefinitionsOutput, error)
	RegisterJobDefinition(ctx context.Context, params *batch.RegisterJobDefinitionInput, optFns ...func(*batch.Options)) (*batch.RegisterJobDefinitionOutput, error)
	DeregisterJobDefinition(ctx context.Context, params *batch.DeregisterJobDefinitionInput, optFns ...func(*batch.Options)) (*batch.DeregisterJobDefinitionOutput, error)
	SubmitJob(ctx context.Context, params *batch.SubmitJobInput, optFns ...func(*batch.Options)) (*batch.SubmitJobOutput, error)
	DescribeJobs(ctx context.Context, params *batch.DescribeJobsInput, optFns ...func(*batch.Options)) (*batch.DescribeJobsOutput, error)
}

BatchAPI is the subset of the Batch client batchbru uses.

type CLI

type CLI struct {
	Config  string            `help:"path to the configuration file" short:"c" default:"batchbru.yml" env:"BATCHBRU_CONFIG"`
	Debug   bool              `help:"print debug logs"`
	ExtStr  map[string]string `help:"Jsonnet external variable (string), repeatable as KEY=VALUE" name:"ext-str"`
	ExtCode map[string]string `help:"Jsonnet external variable (code), repeatable as KEY=EXPR" name:"ext-code"`

	Init       InitOption       `cmd:"" help:"import an existing job definition and start tracking it"`
	Render     RenderOption     `cmd:"" help:"print the fully evaluated job definitions"`
	Diff       DiffOption       `cmd:"" help:"show the difference against the latest ACTIVE revision"`
	Deploy     DeployOption     `cmd:"" help:"register a new revision for changed job definitions"`
	Run        RunOption        `cmd:"" help:"submit a one-off job to smoke-test the latest ACTIVE revision"`
	Status     StatusOption     `cmd:"" help:"show the latest ACTIVE revision of each job definition"`
	Revisions  RevisionsOption  `cmd:"" help:"list the revisions of each job definition"`
	Rollback   RollbackOption   `cmd:"" help:"deregister the latest ACTIVE revision"`
	Deregister DeregisterOption `cmd:"" help:"deregister revisions"`
	Version    struct{}         `cmd:"" help:"print the version"`
}

CLI is the command line interface of batchbru.

type Config

type Config struct {
	Region         string   `yaml:"region,omitempty" json:"region,omitempty"`
	JobDefinitions []string `yaml:"job_definitions" json:"job_definitions"`
	Plugins        []Plugin `yaml:"plugins,omitempty" json:"plugins,omitempty"`
	// contains filtered or unexported fields
}

Config is the batchbru configuration file.

func LoadConfig

func LoadConfig(ctx context.Context, path string, funcs *Funcs, opts *JsonnetOptions) (*Config, error)

LoadConfig reads and validates the configuration file at path.

func (*Config) Dir

func (c *Config) Dir() string

Dir is the directory the configuration was loaded from. Job definition paths are resolved relative to it.

func (*Config) ResolveJobDefinitionFiles

func (c *Config) ResolveJobDefinitionFiles() ([]string, error)

ResolveJobDefinitionFiles expands the job_definitions patterns into a sorted, deduplicated list of file paths.

func (*Config) Save

func (c *Config) Save() error

Save writes the configuration back to its file as YAML.

type DeployOption

type DeployOption struct {
	Names  []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
	DryRun bool     `help:"show what would be registered without calling the API"`
	Force  bool     `help:"register a new revision even when there is no difference"`
}

DeployOption configures the deploy command.

type DeregisterOption

type DeregisterOption struct {
	Names      []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
	Revision   int32    `help:"deregister this revision"`
	KeepLatest int32    `help:"deregister every ACTIVE revision except the newest N"`
	DryRun     bool     `help:"show what would be deregistered without calling the API"`
	Force      bool     `help:"do not ask for confirmation"`
}

DeregisterOption configures the deregister command.

type DiffOption

type DiffOption struct {
	Names    []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
	ExitCode bool     `help:"exit with code 1 when there is a difference"`
}

DiffOption configures the diff command.

type Funcs

type Funcs struct {
	// contains filtered or unexported fields
}

Funcs holds the template functions available while reading configuration and job definition files.

func (*Funcs) JsonnetVM

func (f *Funcs) JsonnetVM(extStr, extCode map[string]string) *jsonnet.VM

JsonnetVM builds a Jsonnet VM with the native functions and the external variables given on the command line.

func (*Funcs) Render

func (f *Funcs) Render(name, src string) (string, error)

Render evaluates src as a text/template. name is used in error messages.

type InitOption

type InitOption struct {
	JobDefinition string `help:"name of the existing job definition to import" required:""`
	Format        string `help:"output format of the job definition file" enum:"jsonnet,json,yaml" default:"jsonnet"`
	Path          string `help:"directory to write the job definition file into" default:"jobdefs"`
	Region        string `help:"AWS region (defaults to the SDK resolution)"`
	Overwrite     bool   `help:"overwrite the job definition file if it already exists"`
}

InitOption configures the init command.

type JobDef

type JobDef struct {
	// Name is the jobDefinitionName declared in the file.
	Name string
	// Path is the file the definition was loaded from.
	Path string
	// Def is the definition in canonical form.
	Def map[string]any
}

JobDef is a job definition loaded from a local file.

func Filter

func Filter(defs []*JobDef, patterns []string) ([]*JobDef, error)

Filter returns the definitions whose name matches one of the given patterns. Patterns accept shell-style wildcards. An empty pattern list matches all.

func LoadJobDefFile

func LoadJobDefFile(path string, funcs *Funcs, opts *JsonnetOptions) (*JobDef, error)

LoadJobDefFile reads a single job definition file: template functions are evaluated first, then the result is parsed according to the file extension and normalized.

func LoadJobDefs

func LoadJobDefs(c *Config, funcs *Funcs, opts *JsonnetOptions) ([]*JobDef, error)

LoadJobDefs reads every job definition file listed in the configuration.

type JsonnetOptions

type JsonnetOptions struct {
	ExtStr  map[string]string
	ExtCode map[string]string
}

JsonnetOptions carries the external variables passed on the command line.

type Options

type Options struct {
	ConfigPath string
	Debug      bool
	ExtStr     map[string]string
	ExtCode    map[string]string
}

Options are the global command line options.

type Plugin

type Plugin struct {
	Name       string         `yaml:"name" json:"name"`
	Config     map[string]any `yaml:"config,omitempty" json:"config,omitempty"`
	FuncPrefix string         `yaml:"func_prefix,omitempty" json:"func_prefix,omitempty"`
}

Plugin extends the template functions available to job definition files.

type RenderOption

type RenderOption struct {
	Names []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
}

RenderOption configures the render command.

type RevisionsOption

type RevisionsOption struct {
	Names []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
	All   bool     `help:"include INACTIVE revisions"`
}

RevisionsOption configures the revisions command.

type RollbackOption

type RollbackOption struct {
	Names  []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
	DryRun bool     `help:"show what would be deregistered without calling the API"`
}

RollbackOption configures the rollback command.

type RunOption

type RunOption struct {
	Names []string `help:"job definition name to run (wildcards allowed, must match exactly one)" name:"name" short:"n"`
	Queue string   `help:"job queue to submit to" required:""`
	Wait  bool     `help:"poll until the job reaches SUCCEEDED or FAILED"`
}

RunOption configures the run command.

type SSMAPI

type SSMAPI interface {
	GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
}

SSMAPI is the subset of the SSM client used to resolve the ssm template function.

type StatusOption

type StatusOption struct {
	Names []string `help:"job definition names to target (wildcards allowed, repeatable)" name:"name" short:"n"`
}

StatusOption configures the status command.

Directories

Path Synopsis
cmd
batchbru command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL