├── .editorconfig ├── .env.example ├── .envrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tflint.hcl ├── Makefile ├── README.md ├── cmd └── todos │ └── main.go ├── docs └── PRESENTATION.md ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── k8s └── deployment.yaml ├── main.tf └── terraform.tfvars /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 100 12 | 13 | [Makefile] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.go] 18 | indent_size = 4 19 | 20 | [VERSION] 21 | insert_final_newline = false 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DOCKER_USERNAME= 2 | DOCKER_ACCESS_TOKEN= 3 | DIGITAL_OCEAN_ACCESS_TOKEN= 4 | TF_VAR_do_token="${DIGITAL_OCEAN_ACCESS_TOKEN}" 5 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake . 2 | dotenv .env 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # The full deployment pipeline 2 | name: Deploy 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest # x86_64-linux 13 | environment: deploy 14 | steps: 15 | - name: git checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Install Nix 19 | uses: DeterminateSystems/nix-installer-action@main 20 | with: 21 | determinate: true 22 | 23 | - name: FlakeHub Cache 24 | uses: DeterminateSystems/flakehub-cache-action@main 25 | 26 | - name: Build TODOs service Docker container using Nix 27 | run: nix build ".#dockerImages.x86_64-linux.default" 28 | # Store the image in ./result 29 | 30 | - name: Docker login 31 | uses: docker/login-action@v2 32 | with: 33 | username: "${{ secrets.DOCKER_USERNAME }}" 34 | password: "${{ secrets.DOCKER_ACCESS_TOKEN }}" 35 | 36 | - name: Load and push image to Docker Hub 37 | run: | 38 | export IMAGE_TAG=$(docker load < result | grep -Po 'Loaded image: \K.*') 39 | docker push "${IMAGE_TAG}" 40 | echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV 41 | 42 | - name: Update the image in our Deployment 43 | uses: actions-hub/kubectl@master 44 | env: 45 | KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} 46 | with: 47 | args: set image deployment.apps/todos-deployment todos="${{ env.IMAGE_TAG }}" 48 | 49 | - name: Restart the Deployment 50 | uses: actions-hub/kubectl@master 51 | env: 52 | KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} 53 | with: 54 | args: rollout restart deployment.apps/todos-deployment 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform* 2 | terraform.lock.hcl 3 | terraform.tfstate* 4 | .env 5 | .tflint.d/ 6 | result 7 | -------------------------------------------------------------------------------- /.tflint.hcl: -------------------------------------------------------------------------------- 1 | config { 2 | format = "compact" 3 | plugin_dir = "~/.tflint.d/plugins" 4 | module = true 5 | } 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | k8s-apply: 2 | kubectl apply -f ./k8s/deployment.yaml 3 | 4 | port-forward: 5 | kubectl port-forward deployments.apps/todos-deployment 8080:8080 6 | 7 | show-image: 8 | kubectl get deployments.apps/todos-deployment --output=json | jq '.spec.template.spec.containers[0].image' 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real world DevOps with Nix 2 | 3 | This example project is meant to demonstrate the power of [Nix] in a DevOps 4 | context. You can see this repo in action in my talk [Real world DevOps with 5 | Nix][video], which was part of the [Summer of Nix][son] video series in 2022. 6 | 7 | ## Moving parts 8 | 9 | * A very simple "TODOs" web service written in [Go] in 10 | [`cmd/todos/main.go`](./cmd/todos/main.go). This service is built to be 11 | deployed on a [Kubernetes] cluster running on [Digital Ocean][do]. 12 | * That cluster is stood up using a [Terraform] configuration in 13 | [`main.tf`](./main.tf) and [`terraform.tfvars`](./terraform.tfvars). 14 | * The Kubernetes configuration in [`k8s/deployment.yaml`](./k8s/deployment.yaml) 15 | defines the Kubernetes [Deployment] for the service. 16 | * The [GitHub Actions][actions] pipeline 17 | * Builds a [Docker] image for the TODOs service using [Nix] 18 | * Pushes the image to [Docker Hub][hub] 19 | * Updates the existing [Deployment] to use the new image 20 | * Restarts the [Deployment] to complete the upgrade 21 | 22 | Some other things to note: 23 | 24 | * The Kubernetes configuration for the Digital Ocean cluster, named 25 | `real-world-devops-with-nix`, is provided under the `KUBE_CONFIG` environment 26 | variable in the CI pipeline. To get that configuration as a base64 string: 27 | 28 | ```shell 29 | doctl kubernetes cluster kubeconfig show real-world-devops-with-nix | base64 30 | ``` 31 | 32 | [actions]: https://github.com/features/actions 33 | [deployment]: https://kubernetes.io/docs/concepts/workloads/controllers/deployment 34 | [docker]: https://docker.com 35 | [do]: https://digitalocean.com 36 | [go]: https://golang.org 37 | [hub]: https://hub.docker.com 38 | [kubernetes]: https://kubernetes.io 39 | [nix]: https://nixos.org 40 | [son]: https://www.youtube.com/playlist?list=PLt4-_lkyRrOMWyp5G-m_d1wtTcbBaOxZk 41 | [terraform]: https://terraform.io 42 | [video]: https://www.youtube.com/watch?v=LjyQ7baj-KM&t=2809s 43 | -------------------------------------------------------------------------------- /cmd/todos/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type todo struct { 10 | ID int64 `json:"id"` 11 | Task string `json:"task"` 12 | Done bool `json:"done"` 13 | } 14 | 15 | func main() { 16 | todos := []todo{ 17 | { 18 | ID: 1, 19 | Task: "Venture forth and continue exploring Nix and DevOps", 20 | Done: false, 21 | }, 22 | } 23 | 24 | r := gin.Default() 25 | r.GET("/todos", func(c *gin.Context) { 26 | c.JSON(http.StatusOK, todos) 27 | }) 28 | r.Run() 29 | } 30 | -------------------------------------------------------------------------------- /docs/PRESENTATION.md: -------------------------------------------------------------------------------- 1 | # Presentation 2 | 3 | Some ready-made commands I may need in case I get lost: 4 | 5 | ```shell 6 | # Spin up the Kubernetes cluster in Digital Ocean 7 | terraform apply 8 | # Get the generated cluster name from the Terraform output 9 | K8S_CLUSTER_NAME=$(terraform output --json | jq .k8s_cluster_name.value | tr -d \") 10 | # Save the cluster config to the local ~/.kube 11 | doctl kubernetes cluster kubeconfig save "${K8S_CLUSTER_NAME}" 12 | # Get the context name 13 | K8S_CONTEXT=$(terraform output --json | jq .k8s_context.value | tr -d \") 14 | # Switch to the Digital Ocean cluster context 15 | kubectx "${K8S_CONTEXT}" 16 | # Get the Kubernetes config from the cluster (and copy into GitHub Actions environment) 17 | doctl kubernetes cluster kubeconfig show "${K8S_CLUSTER_NAME}" 18 | # Set up the deployment 19 | kubectl apply -f ./k8s/deployment.yaml 20 | # Tear down the deployment 21 | kubectl delete -f ./k8s/deployment.yaml 22 | # Port forward 23 | kubectl port-forward deployments.apps/todos-deployment 8080:8080 24 | # Get current image 25 | kubectl get deployments.apps/todos-deployment --output=json | jq '.spec.template.spec.containers[0].image' 26 | ``` 27 | 28 | Steps: 29 | 30 | ```shell 31 | export K8S_CONTEXT="do-nyc1-real-world-devops-with-nix" 32 | 33 | # 1. Make sure I'm in the right k8s context 34 | kubectx "${K8S_CONTEXT}" 35 | # 2. Make sure the cluster is going and ready 36 | kubectl get nodes 37 | # 3. Make sure there are no existing deployments 38 | kubectl get deployments.apps 39 | # 4. Stand up the deployment 40 | kubectl apply -f ./k8s/deployment.yaml 41 | # 5. Check for running deployment 42 | kubectl get deployments.apps 43 | # 6. Port-forward to running pod 44 | kubectl port-forward deployments.apps/todos-deployment 8080:8080 45 | # 7. Run an operation against the service 46 | http :8080 47 | ``` 48 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1743367904, 6 | "narHash": "sha256-sOos1jZGKmT6xxPvxGQyPTApOunXvScV4lNjBCXd/CI=", 7 | "rev": "7ffe0edc685f14b8c635e3d6591b0bbb97365e6c", 8 | "revCount": 716438, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2411.716438%2Brev-7ffe0edc685f14b8c635e3d6591b0bbb97365e6c/0195eb7b-fb36-7030-ad30-51b9576f6073/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/NixOS/nixpkgs/0" 15 | } 16 | }, 17 | "root": { 18 | "inputs": { 19 | "nixpkgs": "nixpkgs" 20 | } 21 | } 22 | }, 23 | "root": "root", 24 | "version": 7 25 | } 26 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Real world DevOps with Nix"; 3 | 4 | inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0"; 5 | 6 | outputs = inputs: 7 | let 8 | name = "todos"; 9 | supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 10 | forEachSupportedSystem = f: inputs.nixpkgs.lib.genAttrs supportedSystems (system: f { 11 | pkgs = import inputs.nixpkgs { 12 | inherit system; 13 | config.allowUnfree = true; 14 | }; 15 | }); 16 | in 17 | { 18 | devShells = forEachSupportedSystem ({ pkgs }: { 19 | default = pkgs.mkShell { 20 | buildInputs = with pkgs; 21 | [ 22 | # Platform-non-specific Go (for local development) 23 | go 24 | 25 | # Docker CLI 26 | docker 27 | 28 | # Kubernetes 29 | kubectl 30 | kubectx 31 | 32 | # Terraform 33 | terraform 34 | tflint 35 | 36 | # Digital Ocean 37 | doctl 38 | ]; 39 | }; 40 | }); 41 | 42 | packages = forEachSupportedSystem ({ pkgs }: rec { 43 | default = todos; 44 | 45 | todos = pkgs.buildGoModule { 46 | name = "todos"; 47 | src = ./.; 48 | subPackages = [ "cmd/todos" ]; 49 | vendorHash = "sha256-fwJTg/HqDAI12mF1u/BlnG52yaAlaIMzsILDDZuETrI="; 50 | }; 51 | }); 52 | 53 | 54 | dockerImages = forEachSupportedSystem ({ pkgs }: { 55 | # A layered image means better caching and less bandwidth 56 | default = pkgs.dockerTools.buildLayeredImage { 57 | name = "lucperkins/todos"; 58 | config = { 59 | Cmd = [ "${inputs.self.packages.x86_64-linux.todos}/bin/todos" ]; 60 | ExposedPorts."8080/tcp" = { }; 61 | }; 62 | maxLayers = 120; 63 | }; 64 | }); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/the-nix-way/real-world-devops-with-nix 2 | 3 | go 1.19 4 | 5 | require github.com/gin-gonic/gin v1.8.1 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.10.0 // indirect 12 | github.com/goccy/go-json v0.9.7 // indirect 13 | github.com/json-iterator/go v1.1.12 // indirect 14 | github.com/leodido/go-urn v1.2.1 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 19 | github.com/ugorji/go/codec v1.2.7 // indirect 20 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 22 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 23 | golang.org/x/text v0.3.6 // indirect 24 | google.golang.org/protobuf v1.28.0 // indirect 25 | gopkg.in/yaml.v2 v2.4.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 8 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 12 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 13 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 14 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 15 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 16 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 17 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 18 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 19 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 20 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 21 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 23 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 24 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 27 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 28 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 34 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 35 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 36 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 39 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 40 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 41 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 42 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 43 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 47 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 48 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 51 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 52 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 53 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 54 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 56 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 57 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 58 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 59 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 60 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 61 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 62 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 66 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 68 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 69 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 70 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 71 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 73 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 75 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 76 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 80 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 81 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 82 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 83 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 84 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 86 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | -------------------------------------------------------------------------------- /k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | # Run 3 copies of our image behind a single load balancer 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: todos-deployment 6 | labels: 7 | app: todos 8 | spec: 9 | replicas: 3 10 | selector: 11 | matchLabels: 12 | app: todos 13 | template: 14 | metadata: 15 | labels: 16 | app: todos 17 | spec: 18 | containers: 19 | - name: todos 20 | image: lucperkins/todos:v1.0.0 # Just a "seed" image that we'll update 21 | ports: 22 | - containerPort: 8080 23 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | // Root manifest 2 | terraform { 3 | required_version = "1.2.8" 4 | required_providers { 5 | digitalocean = { 6 | source = "digitalocean/digitalocean" 7 | version = "2.22.1" 8 | } 9 | kubernetes = { 10 | source = "hashicorp/kubernetes" 11 | version = "2.12.1" 12 | } 13 | } 14 | } 15 | 16 | // Providers 17 | provider "digitalocean" { 18 | token = var.do_token 19 | } 20 | 21 | provider "kubernetes" { 22 | host = data.digitalocean_kubernetes_cluster.devops.endpoint 23 | token = data.digitalocean_kubernetes_cluster.devops.kube_config[0].token 24 | cluster_ca_certificate = base64decode( 25 | data.digitalocean_kubernetes_cluster.devops.kube_config[0].cluster_ca_certificate 26 | ) 27 | } 28 | 29 | // Variables 30 | variable "do_token" { 31 | type = string 32 | } 33 | 34 | variable "k8s_cluster_name" { 35 | type = string 36 | } 37 | 38 | variable "k8s_num_nodes" { 39 | type = number 40 | } 41 | 42 | variable "k8s_region" { 43 | type = string 44 | } 45 | 46 | variable "k8s_worker_size" { 47 | type = string 48 | } 49 | 50 | // Data 51 | data "digitalocean_kubernetes_versions" "current" { 52 | version_prefix = "1.23" 53 | } 54 | 55 | // Output 56 | output "k8s_cluster_name" { 57 | value = digitalocean_kubernetes_cluster.devops.name 58 | } 59 | 60 | output "k8s_context" { 61 | value = "do-${var.k8s_region}-${digitalocean_kubernetes_cluster.devops.name}" 62 | } 63 | 64 | // Resources 65 | resource "digitalocean_kubernetes_cluster" "devops" { 66 | name = var.k8s_cluster_name 67 | region = var.k8s_region 68 | version = data.digitalocean_kubernetes_versions.current.latest_version 69 | 70 | node_pool { 71 | name = "default" 72 | size = var.k8s_worker_size 73 | node_count = var.k8s_num_nodes 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /terraform.tfvars: -------------------------------------------------------------------------------- 1 | k8s_cluster_name = "real-world-devops-with-nix" 2 | k8s_num_nodes = 3 3 | k8s_region = "nyc1" 4 | k8s_worker_size = "s-2vcpu-2gb" 5 | --------------------------------------------------------------------------------