How to Use Make and Makefiles for DevOps Tasks
GNU Make is traditionally a build tool for compiling software, but it excels as a task runner for DevOps workflows. A Makefile provides a single entry point for common operations on your Breeze instances, replacing scattered shell scripts with a documented, tab-complete interface.
Makefile Basics
A Makefile consists of targets, prerequisites, and recipes. Create a Makefile in your project root:
# Variables
APP_NAME := myapp
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
DEPLOY_HOST := breeze-web-01
DEPLOY_USER := deploy
DEPLOY_PATH := /var/www/$(APP_NAME)
.PHONY: help build test deploy clean
# Default target
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
DevOps Task Targets
Define targets for common operations:
build: ## Build the application
@echo "Building $(APP_NAME) v$(VERSION)..."
docker build -t $(APP_NAME):$(VERSION) .
docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest
test: ## Run the test suite
@echo "Running tests..."
docker run --rm $(APP_NAME):$(VERSION) npm test
lint: ## Run linters
shellcheck scripts/*.sh
hadolint Dockerfile
deploy: build test ## Deploy to production Breeze instance
@echo "Deploying $(APP_NAME) v$(VERSION) to $(DEPLOY_HOST)..."
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
./ $(DEPLOY_USER)@$(DEPLOY_HOST):$(DEPLOY_PATH)/
ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'cd $(DEPLOY_PATH) && make install'
install: ## Install on server (run on Breeze instance)
npm ci --production
sudo systemctl restart $(APP_NAME)
rollback: ## Rollback to previous version
ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'cd $(DEPLOY_PATH) && git checkout HEAD~1 && make install'
Infrastructure Management Targets
provision: ## Provision a new Breeze instance
terraform init
terraform plan -out=tfplan
terraform apply tfplan
configure: ## Run Ansible configuration
ansible-playbook -i inventory/production site.yml
configure-dry: ## Dry-run Ansible configuration
ansible-playbook -i inventory/production site.yml --check --diff
backup: ## Create a database backup
@echo "Creating backup..."
ssh $(DEPLOY_USER)@$(DEPLOY_HOST) '/usr/local/bin/backup-db.sh'
@echo "Backup complete"
logs: ## Tail production logs
ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'journalctl -u $(APP_NAME) -f'
status: ## Check service status
@ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'systemctl status $(APP_NAME) --no-pager'
@ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'df -h / | tail -1'
@ssh $(DEPLOY_USER)@$(DEPLOY_HOST) 'free -h | grep Mem'
ssh: ## SSH into the Breeze instance
ssh $(DEPLOY_USER)@$(DEPLOY_HOST)
Environment-Specific Targets
# Use environment variables for different targets
ENV ?= production
deploy-staging: ENV=staging
deploy-staging: deploy ## Deploy to staging
deploy-prod: ENV=production
deploy-prod: deploy ## Deploy to production
config-file:
@echo "Using config for $(ENV)"
cp config/$(ENV).env .env
Conditional Logic and Checks
check-env: ## Verify required environment variables
ifndef API_KEY
$(error API_KEY is not set)
endif
ifndef DB_PASSWORD
$(error DB_PASSWORD is not set)
endif
@echo "Environment check passed"
clean: ## Remove build artifacts
rm -rf dist/ build/ *.tar.gz
docker rmi $(APP_NAME):$(VERSION) 2>/dev/null || true
@echo "Cleaned up"
Best Practices
- Use .PHONY — declare non-file targets as phony to avoid conflicts with filenames
- Add help target — use the
##comment pattern for self-documenting targets - Use variables — centralize hostnames, paths, and versions at the top
- Chain dependencies —
deploy: build testensures build and test run before deploy - Use
@prefix — suppress recipe echoing for cleaner output - Use tabs, not spaces — Makefile recipes require tab indentation
- Keep it simple — if a target grows beyond 10 lines, move logic to a shell script and call it from Make
A well-crafted Makefile gives your team a consistent, discoverable interface for managing Breeze infrastructure and deployments.