Async::ServiceGuidesDeployment

Deployment

This guide explains how to deploy async-service applications using systemd and Kubernetes. We'll use a simple example service to demonstrate deployment configurations.

Example Service

Let's start with a simple HTTP service that we'll deploy:

#!/usr/bin/env async-service
# frozen_string_literal: true

require "async/http"
require "async/service/managed/service"
require "async/service/managed/environment"

class WebService < Async::Service::Managed::Service
	def start
		super
		@endpoint = @evaluator.endpoint
		@bound_endpoint = Sync{@endpoint.bound}
	end
	
	def stop
		@endpoint = nil
		@bound_endpoint&.close
		super
	end
	
	def run(instance, evaluator)
		Console.info(self){"Starting web server on #{@endpoint}"}
		
		server = Async::HTTP::Server.for(@bound_endpoint, protocol: @endpoint.protocol, scheme: @endpoint.scheme) do |request|
			Protocol::HTTP::Response[200, {}, ["Hello, World!"]]
		end
		
		instance.ready!
		server.run
	end
end

module WebEnvironment
	include Async::Service::Managed::Environment
	
	def endpoint
		Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
	end
end

service "web" do
	service_class WebService
	include WebEnvironment
end

Save this as web_service.rb and make it executable:

$ chmod +x web_service.rb

Systemd Deployment

Systemd can manage your async-service application as a system service, providing automatic startup, restart on failure, and integration with system logging.

Service File

Create a systemd service file at /etc/systemd/system/my-web-service.service:

[Unit]
Description=My Web Service
After=network.target

[Service]
Type=notify
ExecStart=/usr/local/bin/bundle exec /path/to/web_service.rb
WorkingDirectory=/path/to/application
User=www-data
Group=www-data
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Key Configuration Points

  • Type=notify: This is essential for async-service to notify systemd when the service is ready. The service uses the sd_notify protocol via the $NOTIFY_SOCKET environment variable.
  • ExecStart: Points to your service script. Use bundle exec if you're using Bundler.
  • WorkingDirectory: Set to your application root directory.
  • User/Group: Run the service as a non-privileged user.
  • Restart=always: Automatically restart the service if it fails.

Installing and Managing the Service

# Reload systemd to recognize the new service
$ sudo systemctl daemon-reload

# Enable the service to start on boot
$ sudo systemctl enable my-web-service

# Start the service
$ sudo systemctl start my-web-service

# Check service status
$ sudo systemctl status my-web-service

# View service logs
$ sudo journalctl -u my-web-service -f

# Stop the service
$ sudo systemctl stop my-web-service

Verifying Readiness

The service will notify systemd when it's ready. You can verify this by checking the service status:

$ sudo systemctl status my-web-service

The service should show as "active (running)" once it has notified systemd of its readiness.

Kubernetes Deployment

Kubernetes can manage your async-service application as a containerized workload, providing scaling, health checks, and rolling updates.

Dockerfile

First, create a Dockerfile for your application:

FROM ruby:3.2

WORKDIR /app

# Install dependencies
COPY Gemfile Gemfile.lock ./
RUN bundle install --deployment --without development test

# Copy application files
COPY . .

# Expose the service port
EXPOSE 3000

# Set the notification log path
ENV NOTIFY_LOG=/tmp/notify.log

# Run the service
CMD ["bundle", "exec", "./web_service.rb"]

Deployment Configuration

Create a Kubernetes deployment file web-service-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-service
  template:
    metadata:
      labels:
        app: web-service
    spec:
      containers:
        - name: web-service
          image: my-registry/web-service:latest
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: NOTIFY_LOG
              value: "/tmp/notify.log"
          readinessProbe:
            exec:
              command:
                - bundle
                - exec
                - bake
                - async:container:notify:log:ready?
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 12
          livenessProbe:
            httpGet:
              path: /
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  selector:
    app: web-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer

Key Configuration Points

  • readinessProbe: Uses the async:container:notify:log:ready? bake task to check if the service is ready. This reads from the NOTIFY_LOG file.
  • livenessProbe: HTTP health check to ensure the service is responding to requests.
  • NOTIFY_LOG: Environment variable pointing to the notification log file path.
  • replicas: Number of pod instances to run.

Deploying to Kubernetes

# Build and push the Docker image
$ docker build -t my-registry/web-service:latest .
$ docker push my-registry/web-service:latest

# Apply the deployment
$ kubectl apply -f web-service-deployment.yaml

# Check deployment status
$ kubectl get deployments
$ kubectl get pods

# View pod logs
$ kubectl logs -f deployment/web-service

# Check service endpoints
$ kubectl get svc web-service

# Scale the deployment
$ kubectl scale deployment web-service --replicas=3

# Update the deployment (rolling update)
$ kubectl set image deployment/web-service web-service=my-registry/web-service:v2

Verifying Readiness

Kubernetes will wait for the readiness probe to pass before routing traffic to the pod:

# Check pod readiness
$ kubectl get pods -l app=web-service

# Describe pod to see readiness probe status
$ kubectl describe pod <pod-name>

The pod will show as "Ready" once the readiness probe succeeds, indicating the service has notified that it's ready to accept traffic.

Notification Mechanism

Both systemd and Kubernetes deployments rely on the notification mechanism provided by async-container. The service uses instance.ready! to signal readiness:

  • Systemd: Uses the sd_notify protocol via the $NOTIFY_SOCKET environment variable (automatically handled by async-container).
  • Kubernetes: Uses a log file (NOTIFY_LOG) that the readiness probe checks using the async:container:notify:log:ready? bake task.

This ensures that your service is only considered ready when it has actually started and is prepared to handle requests, preventing premature traffic routing and improving reliability.