Skip to content

Commit 3bd2b7d

Browse files
author
Andreas Kull
committed
Init
0 parents  commit 3bd2b7d

File tree

11 files changed

+351
-0
lines changed

11 files changed

+351
-0
lines changed

Makefile

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.ONESHELL:
2+
3+
.DEFAULT_GOAL := install
4+
5+
BIN = .venv/bin
6+
7+
.PHONY: clean
8+
clean:
9+
rm -f requirements.txt
10+
rm -rf .venv
11+
12+
.PHONY: install
13+
install: requirements.in
14+
python3 -m venv .venv
15+
chmod +x .venv/bin/activate
16+
. .venv/bin/activate
17+
$(BIN)/pip install pip-tools
18+
$(BIN)/pip-compile --strip-extras -q -o requirements.txt requirements.in
19+
$(BIN)/pip-sync requirements.txt
20+
21+
.PHONY: lint
22+
lint: .venv/bin/ruff
23+
$(BIN)/ruff check .
24+
25+
.PHONY: test
26+
test: .venv/bin/pytest
27+
$(BIN)/pytest app/

README.md

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Deployment for Python
2+
3+
Opinionated shell scripts for deployment, a lot is based on the repository name and thus the `SLUG` environment variable.
4+
5+
Assume we have a repository named `foobar-service` and we didn't set `SLUG` by hand.
6+
7+
Here's the explanation in order of recommended execution:
8+
9+
## Requirements
10+
11+
- 3.12 <= [Python](https://docs.python.org) < 3.13
12+
13+
- [Docker](https://docs.docker.com/)
14+
15+
- [Kubernetes](https://kubernetes.io/docs/home/)
16+
17+
- [AWS](https://docs.aws.amazon.com/)
18+
19+
- [pip-tools](https://github.com/jazzband/pip-tools)
20+
21+
- [ruff](https://docs.astral.sh/ruff/)
22+
23+
## slug.sh
24+
25+
A lot of things are derived from the repository's name, i.e. you either set the `SLUG` environment variable manually or you replace `<REPLACE_WITH_REPOSITORY_NAME_ENV_VAR>` with something provided by your CI/CD like `GITHUB_REPOSITORY_NAME`, `BITBUCKET_REPO_SLUG`, ...
26+
27+
Example: `BITBUCKET_REPO_SLUG=foobar-service` will result in `SLUG=foobar`
28+
29+
## master.sh
30+
31+
Sets up all the environment variables for a specific branch/environment, i.e. rename this file to `production.sh` or duplicate it for whatever environment you build for. It requires some replacements:
32+
33+
- `<REPLACE_WITH_AWS_ACCOUNT_ID>`, e.g. 123456789012 or environment variable
34+
35+
- `<REPLACE_WIHT_AWS_REGION>`, e.g. us-east-1 or environment variable
36+
37+
- `<REPLACE_WITH_API_URL_NAME>`, e.g. api.foobar.com or environment variable
38+
39+
Then we have the `CONTEXT` for our API which I like to base on the `SLUG`, e.g. `api.company.com/foobar` but you can also set it manually.
40+
41+
## build.sh
42+
43+
Triggers `make`, see `Makefile`.
44+
45+
## lint.sh
46+
47+
Triggers `make lint`, see `Makefile`.
48+
49+
## docker.sh
50+
51+
Here we build the docker image and push it to the ECR repository.
52+
53+
It also requires some environment_variables to be set:
54+
55+
- `SLUG`, set by `slug.sh` or manually, e.g. in `master.sh`
56+
57+
- `<REPLACE_WITH_BRANCH_NAME_ENV_VAR>`, e.g. `GITHUB_REF##*/`, `BITBUCKET_BRANCH`, ...
58+
59+
- `<REPLACE_WITH_COMMIT_HASH_ENV_VAR>`, e.g. `GITHUB_SHA`, `BITBUCKET_COMMIT`, ...
60+
61+
- `AWS_ECR_URL`
62+
63+
- `AWS_REGION`
64+
65+
- `AWS_ACCESS_KEY_ID`
66+
67+
- `AWS_SECRET_ACCESS_KEY`
68+
69+
A multi-staged build follows that can use a specific `CONTEXT` (set in `master.sh`) environment variable for the server.
70+
71+
The images pushed assume an existing repository with `SLUG` as it's name, e.g. `foobar`.
72+
73+
It will push the image with the tags `latest` and branch name with shortened commit hash, e.g. `master-1234567`.
74+
75+
## k8s.sh
76+
77+
So one assumption here is that `STAGE` is basically also your k8s namespace, e.g. `(STAGE=PRODUCTION) == (NAMESPACE=production)`.
78+
79+
Yet another assumption is that you've created a secret in the secrets-manager with name `$SLUG-secrets`, e.g. `foobar-secrets`.
80+
81+
I use [envsubst](https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html) to replace variables in the `*.yml` files with environment variables.
82+
83+
Have a look at the YAML files yourself, they provide a basic skeleton.

build.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
make

config.yml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: $SLUG-config
5+
data:
6+
STAGE: $STAGE
7+
NAME: $SLUG

docker.sh

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
3+
[[ -z $SLUG ]] && echo "Missing SLUG" && exit 1
4+
[[ -z $<REPLACE_WITH_BRANCH_NAME_ENV_VAR> ]] && echo "Missing BRANCH_NAME" && exit 1
5+
[[ -z $<REPLACE_WITH_COMMIT_HASH_ENV_VAR> ]] && echo "Missing COMMIT_HASH" && exit 1
6+
[[ -z $AWS_ECR_URL ]] && echo "Missing AWS_ECR_URL" && exit 1
7+
[[ -z $AWS_REGION ]] && echo "Missing AWS_REGION" && exit 1
8+
[[ -z $AWS_ACCESS_KEY_ID ]] && echo "Missing AWS_ACCESS_KEY_ID" && exit 1
9+
[[ -z $AWS_SECRET_ACCESS_KEY ]] && echo "Missing AWS_SECRET_ACCESS_KEY" && exit 1
10+
11+
# Create Dockerfile
12+
cat >Dockerfile <<EOF
13+
FROM python:3.12-slim as build
14+
15+
ENV PIP_DEFAULT_TIMEOUT=100 \
16+
PYTHONUNBUFFERED=1 \
17+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
18+
PIP_NO_CACHE_DIR=1
19+
20+
WORKDIR /app
21+
22+
COPY requirements.in ./
23+
24+
RUN pip install pip-tools \
25+
&& pip-compile --strip-extras -q -o requirements.txt requirements.in
26+
27+
28+
FROM python:3.12-slim as runtime
29+
30+
ENV CONTEXT=$CONTEXT
31+
32+
WORKDIR /app
33+
34+
COPY --from=build /app/requirements.txt .
35+
36+
RUN apt-get update \
37+
&& apt-get upgrade -y \
38+
&& apt-get install build-essential -y \
39+
&& pip install -r requirements.txt \
40+
&& apt-get autoremove -y \
41+
&& apt-get clean -y \
42+
&& rm -rf /var/lib/apt/lists/*
43+
44+
COPY ./app app
45+
46+
EXPOSE 8000
47+
48+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--root-path", "/$CONTEXT"]
49+
EOF
50+
51+
# Create vars
52+
IMAGE="${AWS_ECR_URL}/${SLUG}"
53+
TAG="${BRANCH_NAME}-${COMMIT_HASH::7}"
54+
55+
echo -e "\n--- docker ---"
56+
echo "IMAGE=$IMAGE"
57+
echo "TAGS=$TAG,latest"
58+
echo -e "--- docker ---\n"
59+
60+
aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${AWS_ECR_URL}" &&
61+
# Build and push
62+
docker build -t "$IMAGE":"$TAG" -t "$IMAGE":latest . &&
63+
docker push "$IMAGE":latest &&
64+
docker push "$IMAGE":"$TAG"

k8s.sh

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/bash
2+
3+
[[ -z $STAGE ]] && echo "Missing STAGE" && exit 1
4+
[[ -z $SLUG ]] && echo "Missing SLUG" && exit 1
5+
6+
7+
# Create vars
8+
BASEDIR=$(dirname "$0")
9+
NAME=$(echo "${STAGE}" | tr '[:upper:]' '[:lower:]')
10+
NAMESPACE="${NAME}"
11+
12+
# Update config
13+
aws eks update-kubeconfig --name "$NAME"
14+
15+
secrets () {
16+
# Create secrets
17+
kubectl -n "$NAMESPACE" create secret generic "${SLUG}"-secrets \
18+
--from-env-file=<(aws secretsmanager get-secret-value --secret-id "${SLUG}"-secrets | jq -r .SecretString | jq -r 'to_entries | .[] | .key + "=" + .value') \
19+
--dry-run=client -o yaml |
20+
kubectl apply -f -
21+
}
22+
23+
config () {
24+
# Apply config map
25+
envsubst <"$BASEDIR"/config.yml | kubectl apply -n "$NAMESPACE" -f -
26+
}
27+
28+
deploy () {
29+
# Required to substitute variables in k8s.yml
30+
[[ -z $HOST ]] && echo "Missing HOST" && exit 1
31+
[[ -z $PORT ]] && echo "Missing PORT" && exit 1
32+
[[ -z $MEMORY_LIMIT ]] && echo "Missing MEMORY_LIMIT" && exit 1
33+
[[ -z $MEMORY_REQUESTS ]] && echo "Missing MEMORY_REQUESTS" && exit 1
34+
[[ -z $AWS_ECR_URL ]] && echo "Missing AWS_ECR_URL" && exit 1
35+
36+
envsubst <"$BASEDIR"/k8s.yml | kubectl apply -n "$NAMESPACE" -f -
37+
}
38+
39+
restart () {
40+
kubectl rollout restart deployment -n "$NAMESPACE" "$SLUG"
41+
}
42+
43+
case $1 in
44+
"secrets")
45+
secrets
46+
;;
47+
"config")
48+
config
49+
;;
50+
"deploy")
51+
deploy
52+
;;
53+
"restart")
54+
restart
55+
;;
56+
*)
57+
exit 1;
58+
;;
59+
esac

k8s.yml

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
kind: Deployment
2+
apiVersion: apps/v1
3+
metadata:
4+
name: $SLUG
5+
labels:
6+
app: $SLUG
7+
spec:
8+
replicas: 1
9+
selector:
10+
matchLabels:
11+
app: $SLUG
12+
template:
13+
metadata:
14+
annotations:
15+
linkerd.io/inject: enabled
16+
labels:
17+
app: $SLUG
18+
spec:
19+
containers:
20+
- name: $SLUG
21+
image: $AWS_ECR_URL/$SLUG:latest
22+
imagePullPolicy: Always
23+
livenessProbe:
24+
tcpSocket:
25+
port: $PORT
26+
initialDelaySeconds: 30
27+
readinessProbe:
28+
tcpSocket:
29+
port: $PORT
30+
initialDelaySeconds: 30
31+
resources:
32+
limits:
33+
memory: $MEMORY_LIMIT
34+
requests:
35+
memory: $MEMORY_REQUESTS
36+
ports:
37+
- containerPort: $PORT
38+
name: $SLUG
39+
envFrom:
40+
- configMapRef:
41+
name: $SLUG-config
42+
- secretRef:
43+
name: $SLUG-secrets
44+
---
45+
kind: Ingress
46+
apiVersion: networking.k8s.io/v1
47+
metadata:
48+
name: $SLUG-ingress
49+
namespace: $NAMESPACE
50+
annotations:
51+
nginx.ingress.kubernetes.io/rewrite-target: /$2
52+
nginx.ingress.kubernetes.io/limit-connections: "500"
53+
nginx.ingress.kubernetes.io/limit-rps: "500"
54+
nginx.ingress.kubernetes.io/load-balance: ewma
55+
nginx.ingress.kubernetes.io/use-regex: "true"
56+
spec:
57+
ingressClassName: nginx
58+
rules:
59+
- host: $HOST
60+
http:
61+
paths:
62+
- backend:
63+
service:
64+
name: $SLUG
65+
port:
66+
number: $PORT
67+
path: /$CONTEXT(/*)(.*)
68+
pathType: ImplementationSpecific
69+
---
70+
kind: Service
71+
apiVersion: v1
72+
metadata:
73+
name: $SLUG
74+
spec:
75+
selector:
76+
app: $SLUG
77+
ports:
78+
- protocol: TCP
79+
port: $PORT
80+
targetPort: $PORT

lint.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
make lint

master.sh

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
export AWS_ECR_URL=<REPLACE_WITH_AWS_ACCOUNT_ID>.dkr.ecr.<REPLACE_WIHT_AWS_REGION>.amazonaws.com
4+
export AWS_REGION=<REPLACE_WIHT_AWS_REGION>
5+
6+
export STAGE=PRODUCTION
7+
export HOST=<REPLACE_WITH_API_URL_NAME>
8+
export PORT=8000
9+
export CONTEXT=$SLUG
10+
export MEMORY_LIMIT=512Mi
11+
export MEMORY_REQUESTS=256Mi
12+
13+
printenv

slug.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
# If SLUG is not set, build it from the repository name
4+
if [[ -z ${SLUG} ]]; then
5+
SLUG=$(echo "$<REPLACE_WITH_REPOSITORY_NAME_ENV_VAR>" | cut -f1 -d"-" | head -c 15)
6+
fi
7+
8+
export SLUG=$SLUG
9+
echo "SLUG: $SLUG"

test.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
STAGE=LOCAL make test

0 commit comments

Comments
 (0)