FROM rust:1.75-alpine AS chef
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
RUN cargo install cargo-chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release
FROM alpine:3.18 AS runtime
RUN apk add --no-cache ca-certificates openssl libgcc
RUN addgroup -g 1001 -S opencrates && \
adduser -S opencrates -u 1001 -G opencrates
COPY --from=builder /app/target/release/opencrates /usr/local/bin/opencrates
COPY --from=builder /app/target/release/opencrates_server /usr/local/bin/opencrates_server
COPY --from=builder /app/target/release/opencrates_cli /usr/local/bin/opencrates_cli
USER opencrates
EXPOSE 8080 8081 8082
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["/usr/local/bin/opencrates_server"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opencrates-deployment
namespace: opencrates
labels:
app: opencrates
version: v3.0.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: opencrates
template:
metadata:
labels:
app: opencrates
version: v3.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: opencrates-service-account
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containers:
- name: opencrates
image: opencrates:v3.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
protocol: TCP
- containerPort: 8081
name: metrics
protocol: TCP
- containerPort: 8082
name: grpc
protocol: TCP
env:
- name: RUST_LOG
value: "info"
- name: OPENCRATES_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: opencrates-secrets
key: database-url
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: opencrates-secrets
key: openai-api-key
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: opencrates-secrets
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
- name: logs
mountPath: /app/logs
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: logs
emptyDir: {}
nodeSelector:
kubernetes.io/arch: amd64
tolerations:
- key: "node-role.kubernetes.io/spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
---
apiVersion: v1
kind: Service
metadata:
name: opencrates-service
namespace: opencrates
labels:
app: opencrates
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
- port: 8081
targetPort: 8081
protocol: TCP
name: metrics
selector:
app: opencrates
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: opencrates-hpa
namespace: opencrates
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: opencrates-deployment
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
---
apiVersion: v1
kind: ConfigMap
metadata:
name: opencrates-config
namespace: opencrates
data:
app.toml: |
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[database]
pool_size = 10
connection_timeout = 30
[cache]
ttl_seconds = 3600
max_entries = 10000
[openai]
timeout_seconds = 30
max_retries = 3
[metrics]
enabled = true
port = 8081
[logging]
level = "info"
format = "json"
---
apiVersion: v1
kind: Secret
metadata:
name: opencrates-secrets
namespace: opencrates
type: Opaque
data:
database-url: <base64-encoded-database-url>
openai-api-key: <base64-encoded-openai-api-key>
redis-url: <base64-encoded-redis-url>
jwt-secret: <base64-encoded-jwt-secret>
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: opencrates-service-account
namespace: opencrates
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: opencrates-cluster-role
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: opencrates-cluster-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: opencrates-cluster-role
subjects:
- kind: ServiceAccount
name: opencrates-service-account
namespace: opencrates
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: opencrates-network-policy
namespace: opencrates
spec:
podSelector:
matchLabels:
app: opencrates
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
- from: []
ports:
- protocol: TCP
port: 8081
egress:
- to: []
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 5432
- protocol: TCP
port: 6379
- to: []
ports:
- protocol: UDP
port: 53
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: opencrates-pdb
namespace: opencrates
spec:
minAvailable: 2
selector:
matchLabels:
app: opencrates
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: opencrates-ingress
namespace: opencrates
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- opencrates.dev
- api.opencrates.dev
secretName: opencrates-tls
rules:
- host: opencrates.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: opencrates-service
port:
number: 80
- host: api.opencrates.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: opencrates-service
port:
number: 80
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: opencrates-metrics
namespace: opencrates
labels:
app: opencrates
spec:
selector:
matchLabels:
app: opencrates
endpoints:
- port: metrics
interval: 30s
path: /metrics
scrapeTimeout: 10s