Skip to content

Commit da9791c

Browse files
committed
Add production deployment configuration and CI/CD
- Add deploy/ folder with Dockerfile.prod, nginx, uwsgi configs - Add production.ini (secrets externalized to secrets.ini) - Add entrypoint that merges production.ini + secrets.ini at startup - Add build-deploy.yml GitHub Actions workflow - Add dependabot.yml - Update supervisor config with nginx and uwsgi programs
1 parent 734525c commit da9791c

9 files changed

Lines changed: 759 additions & 0 deletions

File tree

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: github-actions
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
groups:
8+
actions:
9+
patterns:
10+
- "*"

.github/workflows/build-deploy.yml

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
name: Build and Deploy CKAN
2+
3+
on:
4+
push:
5+
branches: [master, ckan211-prod-deploy-pr]
6+
tags: ["v*"]
7+
workflow_dispatch:
8+
inputs:
9+
image_tag:
10+
description: "Image tag to deploy (e.g., sha-abc1234 or v1.0.0)"
11+
required: true
12+
type: string
13+
environment:
14+
description: "Target environment"
15+
required: true
16+
type: choice
17+
options:
18+
- staging
19+
- production
20+
21+
env:
22+
ACR_NAME: adracr
23+
IMAGE_NAME: ckan
24+
25+
jobs:
26+
build:
27+
if: github.event_name != 'workflow_dispatch'
28+
runs-on: ubuntu-latest
29+
outputs:
30+
image_tag: ${{ steps.set-env.outputs.image_tag }}
31+
environment: ${{ steps.set-env.outputs.environment }}
32+
steps:
33+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
34+
with:
35+
submodules: recursive
36+
37+
- name: Determine environment and image tag
38+
id: set-env
39+
run: |
40+
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
41+
echo "environment=production" >> $GITHUB_OUTPUT
42+
echo "image_tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
43+
else
44+
echo "environment=staging" >> $GITHUB_OUTPUT
45+
echo "image_tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
46+
fi
47+
48+
- name: Set up Docker Buildx
49+
uses: docker/setup-buildx-action@v3
50+
51+
- name: Docker meta
52+
id: meta
53+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.6.1
54+
with:
55+
images: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}
56+
tags: |
57+
type=sha
58+
type=ref,event=tag
59+
60+
- name: Login to ACR
61+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
62+
with:
63+
registry: ${{ env.ACR_NAME }}.azurecr.io
64+
username: ${{ secrets.ACR_USERNAME }}
65+
password: ${{ secrets.ACR_PASSWORD }}
66+
67+
- name: Build and push
68+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.10.0
69+
with:
70+
context: .
71+
file: deploy/Dockerfile.prod
72+
push: true
73+
cache-from: type=gha
74+
cache-to: type=gha,mode=max
75+
tags: ${{ steps.meta.outputs.tags }}
76+
labels: ${{ steps.meta.outputs.labels }}
77+
78+
deploy:
79+
if: always() && (needs.build.result == 'success' || github.event_name == 'workflow_dispatch')
80+
needs: build
81+
runs-on: ubuntu-latest
82+
environment:
83+
name: ${{ github.event_name == 'workflow_dispatch' && inputs.environment || needs.build.outputs.environment }}
84+
url: ${{ steps.params.outputs.url }}
85+
steps:
86+
- name: Set deploy params
87+
id: params
88+
run: |
89+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
90+
ENV="${{ inputs.environment }}"
91+
echo "image_tag=${{ inputs.image_tag }}" >> $GITHUB_OUTPUT
92+
else
93+
ENV="${{ needs.build.outputs.environment }}"
94+
echo "image_tag=${{ needs.build.outputs.image_tag }}" >> $GITHUB_OUTPUT
95+
fi
96+
97+
if [[ "$ENV" == "production" ]]; then
98+
echo "namespace=adr-p" >> $GITHUB_OUTPUT
99+
echo "url=https://adr-p.fjelltopp.org" >> $GITHUB_OUTPUT
100+
else
101+
echo "namespace=adr-s" >> $GITHUB_OUTPUT
102+
echo "url=https://adr-s.fjelltopp.org" >> $GITHUB_OUTPUT
103+
fi
104+
105+
- name: Setup kubeconfig
106+
run: |
107+
mkdir -p ~/.kube
108+
echo "${{ secrets.KUBECONFIG_BASE64 }}" | base64 -d > ~/.kube/config
109+
chmod 600 ~/.kube/config
110+
111+
- name: Deploy to AKS
112+
run: |
113+
kubectl set image deployment/ckan \
114+
ckan=${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.params.outputs.image_tag }} \
115+
-n ${{ steps.params.outputs.namespace }}
116+
kubectl rollout status deployment/ckan -n ${{ steps.params.outputs.namespace }} --timeout=5m

ckan/ckan_supervisor.conf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,31 @@ startsecs=10
9595
; Need to wait for currently executing tasks to finish at shutdown.
9696
; Increase this if you have very long running tasks.
9797
stopwaitsecs = 600
98+
99+
; ===============================
100+
; nginx reverse proxy
101+
; ===============================
102+
[program:nginx]
103+
command=/usr/sbin/nginx -g "daemon off;"
104+
user=root
105+
stdout_logfile=/dev/fd/1
106+
stdout_logfile_maxbytes=0
107+
stderr_logfile=/dev/fd/2
108+
stderr_logfile_maxbytes=0
109+
autostart=true
110+
autorestart=true
111+
priority=10
112+
113+
; ===============================
114+
; uWSGI CKAN application
115+
; ===============================
116+
[program:uwsgi]
117+
command=/usr/local/bin/uwsgi --ini /usr/lib/adx/uwsgi.ini
118+
user=root
119+
stdout_logfile=/dev/fd/1
120+
stdout_logfile_maxbytes=0
121+
stderr_logfile=/dev/fd/2
122+
stderr_logfile_maxbytes=0
123+
autostart=true
124+
autorestart=true
125+
priority=20

deploy/Dockerfile.prod

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
FROM python:3.10
2+
3+
# OCI Annotations
4+
LABEL org.opencontainers.image.maintainer="support@fjelltopp.org"
5+
LABEL org.opencontainers.image.title="CKAN-ADX"
6+
LABEL org.opencontainers.image.description="Fjelltopp's ADX CKAN production image"
7+
8+
ARG CKAN_SITE_URL
9+
10+
# Set timezone
11+
ENV TZ=UTC
12+
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
13+
14+
# Setting the locale
15+
ENV LC_ALL=en_US.UTF-8
16+
RUN apt-get update && \
17+
apt-get install --no-install-recommends -y locales && \
18+
sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \
19+
dpkg-reconfigure --frontend=noninteractive locales && \
20+
update-locale LANG=${LC_ALL}
21+
22+
# Define environment variables
23+
ENV CKAN_HOME /usr/lib/adx
24+
ENV CKAN_VENV $CKAN_HOME/venv
25+
ENV CKAN_CONFIG /etc/ckan
26+
ENV CKAN_STORAGE_PATH=/var/lib/ckan
27+
ENV PATH=${CKAN_VENV}/bin:${PATH}
28+
29+
# Install required system packages
30+
RUN apt-get -q -y update \
31+
&& apt-get -q -y install \
32+
libpq-dev \
33+
libmagic-dev \
34+
libxml2-dev \
35+
libxslt-dev \
36+
libgeos-dev \
37+
libssl-dev \
38+
libffi-dev \
39+
postgresql-client \
40+
build-essential \
41+
git-core \
42+
vim \
43+
wget \
44+
curl \
45+
xmlsec1 \
46+
jq \
47+
supervisor \
48+
nginx \
49+
&& apt-get -q clean \
50+
&& rm -rf /var/lib/apt/lists/*
51+
52+
# Add CKAN user
53+
RUN useradd -r -u 900 -m -c "ckan account" -d $CKAN_HOME -s /bin/false ckan
54+
55+
# Install pipenv and uwsgi
56+
RUN pip3 install pipenv uwsgi
57+
58+
# Copy submodules and Pipfile
59+
COPY submodules /usr/lib/adx/submodules
60+
COPY Pipfile Pipfile.lock /usr/lib/adx/
61+
62+
# Install Python dependencies
63+
WORKDIR /usr/lib/adx
64+
RUN mkdir .venv && pipenv sync -v && ln -s .venv venv
65+
66+
# Install Node.js and yarn for React build
67+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
68+
apt-get install -y nodejs && \
69+
npm install --global yarn
70+
71+
# Build React components
72+
RUN yarn --cwd /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/react/ && \
73+
yarn --cwd /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/react/ build && \
74+
chmod -R 777 /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/assets/build
75+
76+
RUN chown -R ckan:ckan /usr/lib/adx/
77+
RUN mkdir -p /var/lib/ckan/resources && chmod 777 -R /var/lib/ckan
78+
79+
# Create symlink for backward compatibility with configs that reference /usr/lib/ckan
80+
RUN ln -s /usr/lib/adx /usr/lib/ckan
81+
82+
# Copy entrypoint and config files
83+
COPY deploy/ckan-entrypoint-prod.sh /ckan-entrypoint.sh
84+
COPY ckan/adx_config.ini $CKAN_CONFIG/ckan.ini
85+
COPY ckan/adx_who.ini $CKAN_CONFIG/who.ini
86+
COPY ckan/ckan_supervisor.conf /etc/supervisor/conf.d/ckan_supervisor.conf
87+
COPY deploy/uwsgi.ini /usr/lib/adx/uwsgi.ini
88+
COPY deploy/nginx.conf /etc/nginx/nginx.conf
89+
90+
RUN chmod +x /*.sh
91+
92+
USER root
93+
EXPOSE 5000
94+
95+
ENTRYPOINT ["/ckan-entrypoint.sh"]
96+
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

deploy/ckan-entrypoint-prod.sh

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# URL for the primary database, in the format expected by sqlalchemy
5+
: "${CKAN_SQLALCHEMY_URL:=}"
6+
: "${CKAN_SOLR_URL:=}"
7+
: "${CKAN_REDIS_URL:=}"
8+
: "${CKAN_DATAPUSHER_URL:=}"
9+
10+
export CKAN_HOME=/usr/lib/adx
11+
export CKAN_VENV=$CKAN_HOME/venv
12+
export PATH=${CKAN_VENV}/bin:${PATH}
13+
14+
# Combine base config with secrets using Python ConfigParser
15+
# secrets.ini values override production.ini
16+
echo "Combining configuration files..."
17+
python3 << 'PYEOF'
18+
import configparser
19+
import sys
20+
21+
config = configparser.ConfigParser()
22+
config.read('/etc/ckan/production.ini')
23+
config.read('/etc/ckan/secrets.ini') # Later values override earlier
24+
25+
with open('/tmp/ckan.ini', 'w') as f:
26+
config.write(f)
27+
PYEOF
28+
export CONFIG="/tmp/ckan.ini"
29+
export CKAN_INI="/tmp/ckan.ini"
30+
31+
abort () {
32+
echo "$@" >&2
33+
exit 1
34+
}
35+
36+
set_environment () {
37+
export CKAN_SITE_ID=${CKAN_SITE_ID}
38+
export CKAN_SITE_URL=${CKAN_SITE_URL}
39+
export CKAN_SQLALCHEMY_URL=${CKAN_SQLALCHEMY_URL}
40+
export CKAN_SOLR_URL=${CKAN_SOLR_URL}
41+
export CKAN_REDIS_URL=${CKAN_REDIS_URL}
42+
export CKAN_STORAGE_PATH=/var/lib/ckan
43+
export CKAN_DATAPUSHER_URL=${CKAN_DATAPUSHER_URL}
44+
export CKAN_DATASTORE_WRITE_URL=${CKAN_DATASTORE_WRITE_URL}
45+
export CKAN_DATASTORE_READ_URL=${CKAN_DATASTORE_READ_URL}
46+
export CKAN_SMTP_SERVER=${CKAN_SMTP_SERVER}
47+
export CKAN_SMTP_STARTTLS=${CKAN_SMTP_STARTTLS}
48+
export CKAN_SMTP_USER=${CKAN_SMTP_USER}
49+
export CKAN_SMTP_PASSWORD=${CKAN_SMTP_PASSWORD}
50+
export CKAN_SMTP_MAIL_FROM=${CKAN_SMTP_MAIL_FROM}
51+
export CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB}
52+
if [ -n "${ADR_CKAN_SAML_IDP_CERT}" ]; then
53+
echo "${ADR_CKAN_SAML_IDP_CERT}" > /tmp/saml_idp.crt || echo "Warning: Could not write SAML IDP cert"
54+
fi
55+
}
56+
57+
# Validate required environment variables
58+
if [ -z "$CKAN_SQLALCHEMY_URL" ]; then
59+
abort "ERROR: no CKAN_SQLALCHEMY_URL specified"
60+
fi
61+
62+
if [ -z "$CKAN_SOLR_URL" ]; then
63+
abort "ERROR: no CKAN_SOLR_URL specified"
64+
fi
65+
66+
if [ -z "$CKAN_REDIS_URL" ]; then
67+
abort "ERROR: no CKAN_REDIS_URL specified"
68+
fi
69+
70+
if [ -z "$CKAN_DATAPUSHER_URL" ]; then
71+
abort "ERROR: no CKAN_DATAPUSHER_URL specified"
72+
fi
73+
74+
set_environment
75+
echo "CKAN production environment ready"
76+
77+
# Initialize CKAN database and run plugin migrations
78+
echo "Initializing CKAN database..."
79+
ckan --config="$CONFIG" db init || echo "CKAN database already initialized"
80+
81+
echo "Setting up DataStore permissions..."
82+
ckan --config="$CONFIG" datastore set-permissions | psql "${CKAN_DATASTORE_WRITE_URL}" || echo "Warning: DataStore set-permissions failed or already applied"
83+
84+
echo "Running database migrations for plugins..."
85+
ckan --config="$CONFIG" db upgrade -p pages || echo "Warning: ckanext-pages migration failed or already applied"
86+
# ckan --config="$CONFIG" versions initdb || echo "Warning: ckanext-versions initdb failed or already applied"
87+
# ckan --config="$CONFIG" validation init-db || echo "Warning: ckanext-validation init-db failed or already applied"
88+
ckan --config="$CONFIG" unaids initdb || echo "Warning: ckanext-unaids initdb failed or already applied"
89+
90+
exec "$@"

0 commit comments

Comments
 (0)