Skip to content

Commit 5577bbd

Browse files
Add deployment guide.
1 parent b3f00a1 commit 5577bbd

File tree

9 files changed

+629
-74
lines changed

9 files changed

+629
-74
lines changed

context/deployment.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Deployment
2+
3+
This guide explains how to deploy `async-service` applications using systemd and Kubernetes. We'll use a simple example service to demonstrate deployment configurations.
4+
5+
## Example Service
6+
7+
Let's start with a simple HTTP service that we'll deploy:
8+
9+
```ruby
10+
#!/usr/bin/env async-service
11+
# frozen_string_literal: true
12+
13+
require "async/http"
14+
require "async/service/managed/service"
15+
require "async/service/managed/environment"
16+
17+
class WebService < Async::Service::Managed::Service
18+
def start
19+
super
20+
@endpoint = @evaluator.endpoint
21+
@bound_endpoint = Sync{@endpoint.bound}
22+
end
23+
24+
def stop
25+
@endpoint = nil
26+
@bound_endpoint&.close
27+
super
28+
end
29+
30+
def run(instance, evaluator)
31+
Console.info(self){"Starting web server on #{@endpoint}"}
32+
33+
server = Async::HTTP::Server.for(@bound_endpoint, protocol: @endpoint.protocol, scheme: @endpoint.scheme) do |request|
34+
Protocol::HTTP::Response[200, {}, ["Hello, World!"]]
35+
end
36+
37+
instance.ready!
38+
server.run
39+
end
40+
end
41+
42+
module WebEnvironment
43+
include Async::Service::Managed::Environment
44+
45+
def endpoint
46+
Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
47+
end
48+
end
49+
50+
service "web" do
51+
service_class WebService
52+
include WebEnvironment
53+
end
54+
```
55+
56+
Save this as `web_service.rb` and make it executable:
57+
58+
```bash
59+
$ chmod +x web_service.rb
60+
```
61+
62+
## Systemd Deployment
63+
64+
Systemd can manage your `async-service` application as a system service, providing automatic startup, restart on failure, and integration with system logging.
65+
66+
### Service File
67+
68+
Create a systemd service file at `/etc/systemd/system/my-web-service.service`:
69+
70+
```
71+
[Unit]
72+
Description=My Web Service
73+
After=network.target
74+
75+
[Service]
76+
Type=notify
77+
ExecStart=/usr/local/bin/bundle exec /path/to/web_service.rb
78+
WorkingDirectory=/path/to/application
79+
User=www-data
80+
Group=www-data
81+
Restart=always
82+
RestartSec=5
83+
StandardOutput=journal
84+
StandardError=journal
85+
86+
[Install]
87+
WantedBy=multi-user.target
88+
```
89+
90+
### Key Configuration Points
91+
92+
- **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.
93+
- **ExecStart**: Points to your service script. Use `bundle exec` if you're using Bundler.
94+
- **WorkingDirectory**: Set to your application root directory.
95+
- **User/Group**: Run the service as a non-privileged user.
96+
- **Restart=always**: Automatically restart the service if it fails.
97+
98+
### Installing and Managing the Service
99+
100+
```bash
101+
# Reload systemd to recognize the new service
102+
$ sudo systemctl daemon-reload
103+
104+
# Enable the service to start on boot
105+
$ sudo systemctl enable my-web-service
106+
107+
# Start the service
108+
$ sudo systemctl start my-web-service
109+
110+
# Check service status
111+
$ sudo systemctl status my-web-service
112+
113+
# View service logs
114+
$ sudo journalctl -u my-web-service -f
115+
116+
# Stop the service
117+
$ sudo systemctl stop my-web-service
118+
```
119+
120+
### Verifying Readiness
121+
122+
The service will notify systemd when it's ready. You can verify this by checking the service status:
123+
124+
```bash
125+
$ sudo systemctl status my-web-service
126+
```
127+
128+
The service should show as "active (running)" once it has notified systemd of its readiness.
129+
130+
## Kubernetes Deployment
131+
132+
Kubernetes can manage your `async-service` application as a containerized workload, providing scaling, health checks, and rolling updates.
133+
134+
### Dockerfile
135+
136+
First, create a `Dockerfile` for your application:
137+
138+
```dockerfile
139+
FROM ruby:3.2
140+
141+
WORKDIR /app
142+
143+
# Install dependencies
144+
COPY Gemfile Gemfile.lock ./
145+
RUN bundle install --deployment --without development test
146+
147+
# Copy application files
148+
COPY . .
149+
150+
# Expose the service port
151+
EXPOSE 3000
152+
153+
# Set the notification log path
154+
ENV NOTIFY_LOG=/tmp/notify.log
155+
156+
# Run the service
157+
CMD ["bundle", "exec", "./web_service.rb"]
158+
```
159+
160+
### Deployment Configuration
161+
162+
Create a Kubernetes deployment file `web-service-deployment.yaml`:
163+
164+
```yaml
165+
apiVersion: apps/v1
166+
kind: Deployment
167+
metadata:
168+
name: web-service
169+
spec:
170+
replicas: 2
171+
selector:
172+
matchLabels:
173+
app: web-service
174+
template:
175+
metadata:
176+
labels:
177+
app: web-service
178+
spec:
179+
containers:
180+
- name: web-service
181+
image: my-registry/web-service:latest
182+
ports:
183+
- containerPort: 3000
184+
name: http
185+
env:
186+
- name: NOTIFY_LOG
187+
value: "/tmp/notify.log"
188+
readinessProbe:
189+
exec:
190+
command:
191+
- bundle
192+
- exec
193+
- bake
194+
- async:container:notify:log:ready?
195+
initialDelaySeconds: 5
196+
periodSeconds: 5
197+
timeoutSeconds: 3
198+
failureThreshold: 12
199+
livenessProbe:
200+
httpGet:
201+
path: /
202+
port: 3000
203+
initialDelaySeconds: 30
204+
periodSeconds: 10
205+
timeoutSeconds: 5
206+
failureThreshold: 3
207+
resources:
208+
requests:
209+
memory: "128Mi"
210+
cpu: "100m"
211+
limits:
212+
memory: "256Mi"
213+
cpu: "500m"
214+
---
215+
apiVersion: v1
216+
kind: Service
217+
metadata:
218+
name: web-service
219+
spec:
220+
selector:
221+
app: web-service
222+
ports:
223+
- protocol: TCP
224+
port: 80
225+
targetPort: 3000
226+
type: LoadBalancer
227+
```
228+
229+
### Key Configuration Points
230+
231+
- **readinessProbe**: Uses the `async:container:notify:log:ready?` bake task to check if the service is ready. This reads from the `NOTIFY_LOG` file.
232+
- **livenessProbe**: HTTP health check to ensure the service is responding to requests.
233+
- **NOTIFY_LOG**: Environment variable pointing to the notification log file path.
234+
- **replicas**: Number of pod instances to run.
235+
236+
### Deploying to Kubernetes
237+
238+
```bash
239+
# Build and push the Docker image
240+
$ docker build -t my-registry/web-service:latest .
241+
$ docker push my-registry/web-service:latest
242+
243+
# Apply the deployment
244+
$ kubectl apply -f web-service-deployment.yaml
245+
246+
# Check deployment status
247+
$ kubectl get deployments
248+
$ kubectl get pods
249+
250+
# View pod logs
251+
$ kubectl logs -f deployment/web-service
252+
253+
# Check service endpoints
254+
$ kubectl get svc web-service
255+
256+
# Scale the deployment
257+
$ kubectl scale deployment web-service --replicas=3
258+
259+
# Update the deployment (rolling update)
260+
$ kubectl set image deployment/web-service web-service=my-registry/web-service:v2
261+
```
262+
263+
### Verifying Readiness
264+
265+
Kubernetes will wait for the readiness probe to pass before routing traffic to the pod:
266+
267+
```bash
268+
# Check pod readiness
269+
$ kubectl get pods -l app=web-service
270+
271+
# Describe pod to see readiness probe status
272+
$ kubectl describe pod <pod-name>
273+
```
274+
275+
The pod will show as "Ready" once the readiness probe succeeds, indicating the service has notified that it's ready to accept traffic.
276+
277+
## Notification Mechanism
278+
279+
Both systemd and Kubernetes deployments rely on the notification mechanism provided by `async-container`. The service uses `instance.ready!` to signal readiness:
280+
281+
- **Systemd**: Uses the `sd_notify` protocol via the `$NOTIFY_SOCKET` environment variable (automatically handled by `async-container`).
282+
- **Kubernetes**: Uses a log file (`NOTIFY_LOG`) that the readiness probe checks using the `async:container:notify:log:ready?` bake task.
283+
284+
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.

context/getting-started.md

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class WebServerService < Async::Service::Generic
9292
end
9393
```
9494

95+
The evaluator is a memoized instance of the service's configuration, allowing for efficient access to configuration values throughout the service's lifecycle. If a service worker is restarted, it will create a new evaluator and a fresh environment.
96+
9597
### Multiple Services
9698

9799
You can define multiple services in a single configuration file:
@@ -151,40 +153,3 @@ end
151153

152154
Async::Service::Controller.run(configuration)
153155
```
154-
155-
### Accessing Configuration Values
156-
157-
Services have access to their configuration through the environment and evaluator:
158-
159-
```ruby
160-
class ConfigurableService < Async::Service::Generic
161-
def setup(container)
162-
super
163-
164-
container.run(count: 1, restart: true) do |instance|
165-
# Clone the evaluator for thread safety
166-
evaluator = self.environment.evaluator
167-
database_url = evaluator.database_url
168-
max_connections = evaluator.max_connections
169-
debug_mode = evaluator.debug_mode
170-
171-
puts "Database URL: #{database_url}"
172-
puts "Max connections: #{max_connections}"
173-
puts "Debug mode: #{debug_mode}"
174-
175-
instance.ready!
176-
177-
# Your service implementation using these values
178-
end
179-
end
180-
end
181-
182-
service "configurable" do
183-
service_class ConfigurableService
184-
database_url "postgresql://localhost/myapp"
185-
max_connections 10
186-
debug_mode true
187-
end
188-
```
189-
190-
The evaluator is a memoized instance of the service's configuration, allowing for efficient access to configuration values throughout the service's lifecycle.

context/index.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ files:
1818
title: Best Practices
1919
description: This guide outlines recommended patterns and practices for building
2020
robust, maintainable services with `async-service`.
21+
- path: deployment.md
22+
title: Deployment
23+
description: This guide explains how to deploy `async-service` applications using
24+
systemd and Kubernetes. We'll use a simple example service to demonstrate deployment
25+
configurations.

examples/deployment/server.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env async-service
2+
# frozen_string_literal: true
3+
4+
require "async/http"
5+
require "async/service/managed/service"
6+
require "async/service/managed/environment"
7+
8+
class WebService < Async::Service::Managed::Service
9+
def start
10+
super
11+
@endpoint = @evaluator.endpoint
12+
@bound_endpoint = Sync{@endpoint.bound}
13+
end
14+
15+
def stop
16+
@endpoint = nil
17+
@bound_endpoint&.close
18+
super
19+
end
20+
21+
def run(instance, evaluator)
22+
Console.info(self){"Starting web server on #{evaluator.endpoint}"}
23+
24+
server = Async::HTTP::Server.for(@bound_endpoint, protocol: @endpoint.protocol, scheme: @endpoint.scheme) do |request|
25+
Protocol::HTTP::Response[200, {}, ["Hello, World!"]]
26+
end
27+
28+
instance.ready!
29+
server.run
30+
end
31+
end
32+
33+
module WebEnvironment
34+
include Async::Service::Managed::Environment
35+
36+
def endpoint
37+
Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
38+
end
39+
end
40+
41+
service "web" do
42+
service_class WebService
43+
include WebEnvironment
44+
end

0 commit comments

Comments
 (0)