I feel like I’m just memeing with the whole “as code” thing at this point - so let’s take it further and make a development environment as code. This will give you an instance of VS Code accessible in a web browser with access to all the buildtime and runtime dependancies I need to survive on this planet.

As I further explore the theme of Infrastructure as Code (IaC), containers, and cloud-native technologies, I wanted to build an environment where I would be able to develop and effectively “control” my infrastructure securely, remotely, and from any device and wondered if it would be possible to work and develop entirely from a Docker container, which itself is defined and built using code checked into version control. Remember, one large benefit of keeping declarative infrastructure in version control is it effectively covers your backout plan for change management, which lowers your impact when deploying changes (rapid revert), and allows you to fail fast. Rolling back the change in your infrastructure merely requires rolling back in your version control system, which is one of its primary purposes.

With these principles, I wanted to reduce the administrative overhead of keeping my development environment updated in sync with my production environment, which is patched very frequently to reduce vulnerabilities. This is huge because it allows me to test if any dependency updates break while I’m working in dev before I even deploy to prod. It also allows me to audit changes to my development environment and keep the file system clean. It’s important to get in the mindset that you don’t make changes in the live environment, but you make changes in the code which builds the environment. This means you don’t make changes on the fly, but you redeploy from scratch (automatically) each time. This keeps things clean and prevents drift over time. Anything which doesn’t require to be persisted is eventually flushed.

My Personal Ideal Development Environment

I primarily develop web-based applications using the Go programming language, which was designed by Ken Thompson, Rob Pike, and Robert Griesemer. My IDE of preference is VS Code, and I use Git as my Version Control System. I also use Hugo as a static site generator, it created the site you’re reading right now.

I also want it to be web-accessible, which provides a few very nice attributes:

  • I can develop on any device which has a web browser, including low powered devices such as a tablet with a keyboard, a raspberry pi, or a Chromebook
  • No matter what device I use, I pick up where I left off, it doesn’t matter what device I last used - it’s one git client.

Dockerfile

I wanted to keep the build steps for the environment contained within the Dockerfile, rather than the CICD pipeline definition. This would keep it more modular to allow moving to other CICD, but keep the infrastructure more locked into the Docker ecosystem. All the CICD system has to do is build the Docker container, by providing a runtime with Docker installed and then calling docker build .

The following Dockerfile is used as of the time of this post:

# https://github.com/cdr/code-server/blob/master/ci/release-image/Dockerfile
FROM codercom/code-server:latest 
LABEL maintainer="himself@stevenpolley.net"

# Update packages
RUN sudo apt update && \
    sudo apt upgrade -y && \
    sudo apt clean autoclean && \
    sudo apt autoremove -y

# Install Go
RUN curl -L -O https://dl.google.com/go/go1.15.2.linux-amd64.tar.gz && \
    tar xvfz go1.15.2.linux-amd64.tar.gz && \
    sudo chown -R root:root ./go && \
    sudo mv go /usr/local && \
    rm -f go1.15.2.linux-amd64.tar.gz

# Install Hugo
RUN curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.75.1/hugo_0.75.1_Linux-64bit.tar.gz && \
    tar xvfz hugo_0.75.1_Linux-64bit.tar.gz && \
    sudo chown -R root:root ./hugo && \
    sudo mv hugo /usr/local/bin && \
    rm -f hugo_0.75.1_Linux-64bit.tar.gz LICENSE README.md

# Start code-server
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]

You could of course include steps to install any additional dependencies your development environment requires. Let’s say I wanted to make a change to update a dependency, such as Go. Currently, it’s using 1.15.2, but I want to upgrade it to 1.15.3. All I need to do is replace each occurrence of 1.15.2 with 1.15.3, and check it into version control in the master branch. My CICD will read the change and deploy it to production. To roll it back would be as easy as reverting that commit in version control on the master branch. CICD would deploy that to production. What the master branch is, is what you have in production. A neat idea.

docker-compose.yml

Finally, it’s deployed using docker-compose using traefik as a reverse-proxy. The following will make a full version of code accessible over the web at https://deac.deadbeef.codes and any running Hugo server for content writing accessible at https://hugo.deac.deadbeef.codes.

Note: These URLs are not actively in use and I do not have an instance of Code accessible over the internet at this time, it is only within my secure network.

version: '3.8'

services:
 
  traefik:
    image: traefik:2.3.1
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./acme.json:/acme.json
      - ./dynamic_conf.toml:/dynamic_conf.toml
    container_name: traefik
    command:
        - "--providers.docker=true"
        - "--providers.docker.exposedbydefault=false"
        - "--entrypoints.web.address=:80"
        - "--entrypoints.web-secure.address=:443"
        - "--certificatesresolvers.le.acme.email=himself@stevenpolley.net"
        - "--certificatesresolvers.le.acme.storage=acme.json"
        - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
        - "--providers.file.filename=dynamic_conf.toml"
        - "--providers.file.watch=true"

  development-environment:
    image: registry.deadbeef.codes/development-environment:latest
    restart: always
    expose:
      - "8080"
      - "1313"
    volumes:
      - /data/deac:/home/coder
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.devenv-web.rule=Host(`deac.deadbeef.codes`)"
      - "traefik.http.routers.devenv-web.entrypoints=web"
      - "traefik.http.routers.devenv-web.middlewares=redirect@file"
      - "traefik.http.routers.devenv-web-secured.rule=Host(`deac.deadbeef.codes`)"
      - "traefik.http.routers.devenv-web-secured.entrypoints=web-secure"
      - "traefik.http.routers.devenv-web-secured.tls=true"
      - "traefik.http.routers.devenv-web-secured.tls.certresolver=le"
      - "traefik.http.routers.devenv-web-secured.service=devenv-service"
      - "traefik.http.services.devenv-service.loadbalancer.server.port=8080"
      - "traefik.http.routers.hugo-devenv-web-secured.rule=Host(`hugo.deac.deadbeef.codes`)"
      - "traefik.http.routers.hugo-devenv-web-secured.entrypoints=web-secure"
      - "traefik.http.routers.hugo-devenv-web-secured.tls=true"
      - "traefik.http.routers.hugo-devenv-web-secured.tls.certresolver=le"
      - "traefik.http.routers.hugo-devenv-web-secured.service=hugo-devenv-service"
      - "traefik.http.services.hugo-devenv-service.loadbalancer.server.port=1313"

Conclusion

Storing these two files alone in version control allows me to completely recover my development environment to any point in time to any host system which has Docker and Docker-compose installed on it.

My familiar text editor, code, running as a secure web application from a Docker container, with my entire development environment configured and accessible.
My familiar text editor, code, running as a secure web application from a Docker container, with my entire development environment configured and accessible.

From here it’s pretty easy to start adding to your development environment as required, for example adding python would be as simple as adding a RUN line to the Dockerfile containing the commands to install Python.

Another possible idea is to mount the host’s Docker socket to the development environment container. Instead of installing directly into the development-environment container any development dependencies, you could instead use specific Docker containers for each dependency. Because my environment is relatively simple, I stuck with the former method and simply installed the development tools into the Docker container. For more complex development environments with many dependencies, the latter may make more sense. There are pros and cons to each method - use the simplest method that supports your needs. We no longer live in a world where we must plan 20 steps ahead when we build our infrastructure. Making changes is now easy.

See my full development environment repository here: https://deadbeef.codes/steven/development-environment