Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
yutotakano authored Feb 29, 2024
0 parents commit d215556
Show file tree
Hide file tree
Showing 16 changed files with 1,093 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SUBDOMAIN=example
37 changes: 37 additions & 0 deletions .github/workflows/publish-and-deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Docker

on:
workflow_dispatch:
push:
# Publish `main` as Docker `latest` image.
branches:
- main

jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
push:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Build image
run: |
source .env
docker build . --file Dockerfile --tag service-$SUBDOMAIN
- name: Log into registry
run: echo "${{ secrets.PAT }}" | docker login ghcr.io -u compsoc-service --password-stdin

- name: Push image
run: |
source .env
IMAGE_ID=ghcr.io/compsoc-edinburgh/service-$SUBDOMAIN
docker tag service-$SUBDOMAIN $IMAGE_ID:latest
docker tag service-$SUBDOMAIN $IMAGE_ID:${{ github.sha }}
docker push $IMAGE_ID:${{ github.sha }}
docker push $IMAGE_ID:latest
- name: Trigger update
run: |
curl -H "Token: ${{ secrets.WATCHTOWER_TOKEN }}" https://watchtower.dev.comp-soc.com/v1/update
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
scripts
.secrets
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:15.0.1-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE ${PORT}

CMD [ "npm", "run", "start" ]
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Deploying a CompSoc service

So you want to run a CompSoc service? Wonderful! You'll need to be part of the `SigWeb` team on GitHub for this to work, and will need to have added your SSH keys to GitHub. An implementation detail here is that keys are synced at most every 30 minutes, so you may need to wait that long after being added to the team. If you're not part of the team, but would _like_ to be, file an issue on the template repository.

It's pretty simple to get started with a new service, just follow these steps:

1. Use this template repository to create a template within `compsoc-edinburgh` where your app will live.
2. Clone it
3. Should your app be committee only? That's the default behaviour, but if you want it to be public delete line `28` in the `makefile`
4. Modify `.env` with your app's options

- `SUBDOMAIN=...`: Which subdomain of `dev.comp-soc.com` should this app run on. This should be a valid subdomain string.

5. Add, commit, and push
6. You'll need to enable GitHub Actions for the repository, and you'll need to trigger a manual run of the `Deploy` action
7. Run `make initialise`
8. And you're done! This template presumes a NodeJS app, but you'd be able to use any technologies to build your app, as long as:
1. You can package it in a _single_ docker container
2. It listens on at most _one_ port, specified by the `$PORT` environment variable

Your app is now setup with deployment on push, automatic HTTPS, and (unless you diabled it) is behind CompSoc's GSuite authentication layer.

# FAQ

_I mean, these aren't frequently asked **yet**, but they may be in future_

## Can I use cool stuff like databases?

Absolutely! Postgres is automatically supported (check the `$DATABASE_URL` environment variable for the connection string), and other can be supported as and when needed (just file an issue in the template repository)

## What about secrets? I don't want those in `git`!

You absolutely don't, which is why secrets management is built in. When you run `make initialise`, a directory `.secrets` will be created, which isn't checked in to git. Everything in that directory is available to your running application at the path `/secrets`. To re-sync secrets after you've changed the contents of `.secrets`, just run `make sync-secrets`. You can stick anything in here, from `json` config files to private signing keys.

## Object storage support?

Yep, check the `$FILE_UPLOAD` environment variable. It contains a URL that you can send a `GET` request to, with a `Content-Type` header set to indicate your file's type. The response will be:

```json
{
"upload_url": "https://...", // A signed upload URL that you can use yourself, or pass to a client
"accessible_at": "https://cdn.comp-soc.com/..." // The URL at which your blob will be accessible once uploaded — to be stored in your database
}
```

## What if my app crashes in production?

No worries, you can sort it — CompSoc believes in you! Just run `make tail` to stream the current production logs from your app into your terminal, or `make logs` to get a dump of all your app's logs since initialisation (note, this can be a _lot_ of data, so use `make tail` where you can).

## I have another question?

File an issue in the template repository, and we'll get to it as soon as possible.

## I'd like to develop locally

That's a good shout. You will need to have `docker` and `docker-compose` installed, but after that a simple `make dev` should get you running locally. Well, it will at _some_ point anyway — it doesn't quite yet...
41 changes: 41 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;
90 changes: 90 additions & 0 deletions bin/www
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node

/**
* Module dependencies.
*/

var app = require('../app');
var debug = require('debug')('deploy-bot:server');
var http = require('http');

/**
* Get port from environment and store in Express.
*/

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
* Create HTTP server.
*/

var server = http.createServer(app);

/**
* Listen on provided port, on all network interfaces.
*/

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
* Normalize a port into a number, string, or false.
*/

function normalizePort(val) {
var port = parseInt(val, 10);

if (isNaN(port)) {
// named pipe
return val;
}

if (port >= 0) {
// port number
return port;
}

return false;
}

/**
* Event listener for HTTP server "error" event.
*/

function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}

var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}

/**
* Event listener for HTTP server "listening" event.
*/

function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
46 changes: 46 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
include .env

REMOTE[email protected]

# .SILENT:

tail:
ssh ${REMOTE} 'docker logs -f --tail 0 service-${SUBDOMAIN}'

logs:
ssh ${REMOTE} 'docker logs service-${SUBDOMAIN}'

sync-secrets:
rsync -r ./.secrets/ ${REMOTE}:/secrets/service-${SUBDOMAIN}

generate-port:
ssh ${REMOTE} 'ruby -e "require \"socket\"; puts Addrinfo.tcp(\"\", 0).bind {|s| s.local_address.ip_port }"' | tr -d '[:space:]' > .open-port

PORT = $(shell cat .open-port)

# Only run once, at service initialisation. All other deployment will be through github actions
initialise: generate-port
mkdir -p .secrets
ssh ${REMOTE} "mkdir -p /secrets/service-${SUBDOMAIN}"
# _Definitely_ prone to race conditions, but this won't be called anywhere near frequently enough for that to matter
ssh ${REMOTE} 'docker exec postgres createdb -U postgres service-db-${SUBDOMAIN}'
ssh ${REMOTE} 'docker run -d --name service-${SUBDOMAIN} \
--network traefik-net \
--label "traefik.enable=true" \
-p ${PORT}:${PORT} \
-e PORT=${PORT} \
-v /secrets/service-${SUBDOMAIN}:/secrets \
-e "DATABASE_URL=postgresql://postgres:mysecretpassword@postgres:5432/service-db-${SUBDOMAIN}" \
-e "FILE_UPLOAD=https://service-simple-storage:3456/${SUBDOMAIN}" \
--label "com.centurylinklabs.watchtower.enable=true" \
--label "traefik.http.routers.service-${SUBDOMAIN}.rule=Host(\`${SUBDOMAIN}.dev.comp-soc.com\`)" \
--label "traefik.http.routers.service-${SUBDOMAIN}.middlewares=traefik-forward-auth" \
ghcr.io/compsoc-edinburgh/service-${SUBDOMAIN}'
rm .open-port

teardown-db:
ssh ${REMOTE} 'docker exec postgres dropdb -U postgres service-db-${SUBDOMAIN}'

teardown: teardown-db
ssh ${REMOTE} 'docker stop service-${SUBDOMAIN}'
ssh ${REMOTE} 'docker rm service-${SUBDOMAIN}'
Loading

0 comments on commit d215556

Please sign in to comment.