Whether you are managing a fleet of machines or sharing a private site from your localhost, Argo Tunnel is here to help. On the Argo Tunnel team we help make origins accessible from the Internet in a secure and seamless manner. We also care deeply about productivity and developer experience for the team, so naturally we want to make sure we have a development environment that is reliable, easy to set up and fast to iterate on.
A brief history of our development environment (dev-stack)
Docker compose
When our development team was still small, we used a docker-compose file to orchestrate the services needed to develop Argo Tunnel. There was no native support for hot reload, so every time an engineer made a change, they had to restart their dev-stack.
We could hack around it to hot reload with docker-compose, but when that failed, we had to waste time debugging the internals of Docker. As the team grew, we realized we needed to invest in improving our dev stack.
At the same time Cloudflare was in the process of migrating from Marathon to kubernetes (k8s). We set out to find a tool that could detect changes in source code and automatically upgrade pods with new images.
Skaffold + Minikube
Initially Skaffold seemed to match the criteria. It watches for change in source code, builds new images and deploys applications onto any k8s. Following Skaffold’s tutorial, we picked minikube as the local k8s, but together they didn’t meet our expectations. Port forwarding wasn’t stable, we got frequent connections refused or timeout.
In addition, iteration time didn’t improve, because spinning up minikube takes a long time and it doesn’t use the host's docker registry and so it can’t take advantage of caching. At this point we considered reverting back to using docker compose, but the k8s ecosystem is booming, so we did some more research.
Tilt + Docker for mac k8s
Eventually we found a great blog post from Tilt comparing different options for local k8s, and they seem to be solving the exact problem we are having. Tilt is a tool that makes local development on k8s easier. It detects changes in local sources and updates your deployment accordingly.
In addition, it supports live updates without having to rebuild containers, a process that used to take around 20 minutes. With live updates, we can copy the newest source into the container, run cargo build
within the container, and restart the service without building a new image. Following Tilt’s blog post, we switched to Docker for Mac’s built-in k8s. Combining Tilt and Docker for Mac k8s, we finally have a development environment that meets our needs.
Rust services that could take 20 minutes to rebuild now take less than a minute.
Collaborating with a distributed team
We reached a much happier state with our dev-stack, but one problem remained: we needed a way to share it. As our teams became distributed with people in Austin, Lisbon and Seattle, we needed better ways to help each other.
One day, I was helping our newest member understand an error observed in cloudflared
, Argo Tunnel’s command line interface (CLI) client. I knew the error could either originate from the backend service or a mock API gateway service, but I couldn’t tell for sure without looking at logs.
To get them, I had to ask our new teammate to manually send me the logs of the two services. By the time I discovered the source of the error, reviewed the deployment manifest, and determined the error was caused by a secret set as an empty string, two full hours had elapsed!
I could have solved this in minutes if I had remote access to her development environment. That’s exactly what Argo Tunnel can do! Argo Tunnel provides remote access to development environments by creating secure outbound-only connections to Cloudflare’s edge network from a resource exposing it to the Internet. That model helps protect servers and resources from being vulnerable to attack by an exposed IP address.
I can use Argo Tunnel to expose a remote dev environment, but the information stored is sensitive. Once exposed, we needed a way to prevent users from reaching it unless they are an authenticated member of my team. Cloudflare Access solves that challenge. Access sits in front of the hostname powered by Argo Tunnel and checks for identity on every request. I can combine both services to share the dev-stack details with the rest of the team in a secure deployment.
The built-in k8s dashboard gives a great overview of the dev-stack, with the list of pods, deployments, services, config maps, secrets, etc. It also allows us to inspect pod logs and exec into a container. By default, it is secured by a token that changes every time the service restarts. To avoid the hassle of distributing the service token to everyone on the team, we wrote a simple reverse proxy that injects the service token in the authorization header before forwarding requests to the dashboard service.
Then we run Argo Tunnel as a sidecar to this reverse proxy, so it is accessible from the Internet. Finally, to make sure no random person can see our dashboard, we put an Access policy that only allows team members to access the hostname.
The request flow is eyeball -> Access -> Argo Tunnel -> reverse proxy -> dashboard service
Working example
Your team can use the same model to develop remotely. Here’s how to get started.
Start a local k8s cluster. https://docs.tilt.dev/choosing_clusters.html offers great advice in choosing a local cluster based on your OS and experience with k8s
2. Enable dashboard service:
3. Create a reverse proxy that will inject the service token of the kubernetes-dashboard service account in the Authorization header before forwarding requests to kubernetes dashboard service
package main
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
func main() {
config, err := loadConfigFromEnv()
if err != nil {
panic(err)
}
reverseProxy := httputil.NewSingleHostReverseProxy(config.proxyURL)
// The default Director builds the request URL. We want our custom Director to add Authorization, in
// addition to building the URL
singleHostDirector := reverseProxy.Director
reverseProxy.Director = func(r *http.Request) {
singleHostDirector(r)
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", config.token))
fmt.Println("request header", r.Header)
fmt.Println("request host", r.Host)
fmt.Println("request ULR", r.URL)
}
reverseProxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
server := http.Server{
Addr: config.listenAddr,
Handler: reverseProxy,
}
server.ListenAndServe()
}
type config struct {
listenAddr string
proxyURL *url.URL
token string
}
func loadConfigFromEnv() (*config, error) {
listenAddr, err := requireEnv("LISTEN_ADDRESS")
if err != nil {
return nil, err
}
proxyURLStr, err := requireEnv("DASHBOARD_PROXY_URL")
if err != nil {
return nil, err
}
proxyURL, err := url.Parse(proxyURLStr)
if err != nil {
return nil, err
}
token, err := requireEnv("DASHBOARD_TOKEN")
if err != nil {
return nil, err
}
return &config{
listenAddr: listenAddr,
proxyURL: proxyURL,
token: token,
}, nil
}
func requireEnv(key string) (string, error) {
result := os.Getenv(key)
if result == "" {
return "", fmt.Errorf("%v not provided", key)
}
return result, nil
}
4. Create an Argo Tunnel sidecar to expose this reverse proxy
apiVersion: apps/v1
kind: Deployment
metadata:
name: dashboard-auth-proxy
namespace: kubernetes-dashboard
labels:
app: dashboard-auth-proxy
spec:
replicas: 1
selector:
matchLabels:
app: dashboard-auth-proxy
template:
metadata:
labels:
app: dashboard-auth-proxy
spec:
containers:
- name: dashboard-tunnel
# Image from https://hub.docker.com/r/cloudflare/cloudflared
image: cloudflare/cloudflared:2020.8.0
command: ["cloudflared", "tunnel"]
ports:
- containerPort: 5000
env:
- name: TUNNEL_URL
value: "http://localhost:8000"
- name: NO_AUTOUPDATE
value: "true"
- name: TUNNEL_METRICS
value: "localhost:5000"
# dashboard-proxy is a proxy that injects the dashboard token into Authorization header before forwarding
# the request to dashboard_proxy service
- name: dashboard-auth-proxy
image: dashboard-auth-proxy
ports:
- containerPort: 8000
env:
- name: LISTEN_ADDRESS
value: localhost:8000
- name: DASHBOARD_PROXY_URL
value: https://kubernetes-dashboard
- name: DASHBOARD_TOKEN
valueFrom:
secretKeyRef:
name: ${TOKEN_NAME}
key: token
5. Find out the URL to access your dashboard from Tilt’s UI
6. Share the URL with your collaborators so they can access your dashboard anywhere they are through the tunnel!
You can find the source code for the example in https://github.com/cloudflare/argo-tunnel-examples/tree/master/sharing-k8s-dashboard
If this sounds like a team you want to be on, we are hiring!