Blog post

Terraform Registry Cache

2022-04-08

12 minute read

I recently came across an r/terraform post about a GitHub project called terraform-registry-proxy.

This inspired me to create a similar project strictly using Nginx.

(Thanks u/jasonwbarnett!)

What is a Terraform Registry Cache?

By default, Terraform will pull down provider artifacts from https://releases.hashicorp.com. For the typical user this behavior is acceptable. For environments that are air gapped or require a local caching server, we can do better.

How does this work exactly?

Nginx will proxy requests for the following endpoints:

Response bodies are rewritten to update where provider artifacts are served from. Terraform will use the rewritten URL to fetch the artifact from the registry cache (Nginx).

Nginx will serve back a response from its local cache if possible and fallback to the official Hashicorp servers.

Requirements

  • An Nginx server with SSL configured. The Terraform CLI must be able to trust the certificate.
  • Two subdomains: 1) terraform-registry.example.com 2) terraform-releases.example.com

Nginx Configuration

This is an example nginx.conf. Modify as you see fit.

1user  nginx;
2worker_processes  auto;
3error_log  /var/log/nginx/error.log notice;
4pid        /var/run/nginx.pid;
5
6events {
7    worker_connections  1024;
8}
9
10http {
11    include       mime.types;
12    default_type  application/octet-stream;
13    sendfile        on;
14    keepalive_timeout  65;
15    gzip  on;
16
17    proxy_headers_hash_bucket_size 128;
18    proxy_cache_path /tmp/terraform-cache levels=1:2 keys_zone=terraform_cache:10m max_size=10g inactive=7d use_temp_path=off;
19
20    # Custom log format so we can validate cache HITS and MISSES
21    log_format custom_cache_log '[$time_local] [Cache:$upstream_cache_status] [$host] [Remote_Addr: $remote_addr] - $remote_user - $server_name to: $upstream_addr: "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" ';
22
23    # terraform-registry.example.com
24    server {
25        listen       443 ssl;
26        server_name  terraform-registry.example.com;
27        resolver 8.8.8.8 ipv6=off;
28        access_log  /var/log/nginx/terraform-registry-cache.log custom_cache_log;
29
30        # SSL
31        ssl_certificate "/etc/letsencrypt/live/terraform-registry.example.com/fullchain.pem";
32        ssl_certificate_key "/etc/letsencrypt/live/terraform-registry.example.com/privkey.pem";
33        ssl_protocols TLSv1.2 TLSv1.3;
34        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
35        ssl_session_cache shared:SSL:1m;
36        ssl_session_timeout 10m;
37        ssl_prefer_server_ciphers on;
38
39        location / {
40            # Pull from the local cache then https://registry.terraform.io/
41            proxy_cache terraform_cache;
42            proxy_cache_use_stale error timeout;
43            proxy_pass https://registry.terraform.io/;
44            proxy_set_header Accept-Encoding "";
45            proxy_set_header X-Forwarded-For $remote_addr;
46            tcp_nodelay on;
47            # Rewrite releases.hashicorp.com payloads so the Terraform CLI pulls artifacts from terraform-releases.example.com
48            sub_filter_types application/json;
49            sub_filter 'releases.hashicorp.com' 'terraform-releases.example.com';
50        }
51    }
52
53    # terraform-releases.example.com
54    server {
55        listen       443 ssl;
56        server_name  terraform-releases.example.com;
57        resolver 8.8.8.8 ipv6=off;
58        access_log  /var/log/nginx/terraform-releases-cache.log custom_cache_log;
59
60        # SSL
61        ssl_certificate "/etc/letsencrypt/live/terraform-releases.example.com/fullchain.pem";
62        ssl_certificate_key "/etc/letsencrypt/live/terraform-releases.example.com/privkey.pem";
63        ssl_protocols TLSv1.2 TLSv1.3;
64        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
65        ssl_session_cache shared:SSL:1m;
66        ssl_session_timeout 10m;
67        ssl_prefer_server_ciphers on;
68
69        location / {
70            # Pull from the local cache then https://releases.hashicorp.com/
71            proxy_cache terraform_cache;
72            proxy_cache_use_stale error timeout;
73            proxy_pass https://releases.hashicorp.com/;
74            proxy_set_header Accept-Encoding "";
75            proxy_set_header X-Forwarded-For $remote_addr;
76            tcp_nodelay on;
77        }
78    }
79}
80

Terraform Configuration

Inside your main.tf, set the global source address against the provider:

1terraform {
2  required_providers {
3    aws = {
4      source = "terraform-registry.example.com/hashicorp/aws"
5    }
6  }
7}
8

Example Usage

Inside the directory with your main.tf changes:

1$ terraform init
2$ terraform init
3
1josh@ringo:~/$ tail -f /var/log/nginx/terraform-registry-cache.log
2[08/Apr/2022:12:44:39 +0000] [Cache:MISS] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: 151.101.86.49:443: "GET /.well-known/terraform.json HTTP/1.1" 200 73 "-" "HashiCorp Terraform/1.1.7 (+https://www.terraform.io)" 
3[08/Apr/2022:12:44:39 +0000] [Cache:MISS] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: 151.101.86.49:443: "GET /v1/providers/hashicorp/aws/versions HTTP/1.1" 200 96922 "-" "Terraform/1.1.7" 
4[08/Apr/2022:12:44:39 +0000] [Cache:MISS] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: 151.101.86.49:443: "GET /v1/providers/hashicorp/aws/4.9.0/download/linux/amd64 HTTP/1.1" 200 8598 "-" "Terraform/1.1.7" 
5[08/Apr/2022:12:44:55 +0000] [Cache:HIT] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: -: "GET /.well-known/terraform.json HTTP/1.1" 200 73 "-" "HashiCorp Terraform/1.1.7 (+https://www.terraform.io)" 
6[08/Apr/2022:12:44:55 +0000] [Cache:HIT] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: -: "GET /v1/providers/hashicorp/aws/versions HTTP/1.1" 200 96882 "-" "Terraform/1.1.7" 
7[08/Apr/2022:12:44:55 +0000] [Cache:HIT] [registry.terrateam.io] [Remote_Addr: 213.93.7.141] - - - registry.terrateam.io to: -: "GET /v1/providers/hashicorp/aws/4.9.0/download/linux/amd64 HTTP/1.1" 200 8591 "-" "Terraform/1.1.7" 
8josh@ringo:~/terraform-registry-cache$ 
9
1josh@ringo:~/$ tail -f /var/log/nginx/terraform-releases-cache.log
2[08/Apr/2022:12:44:42 +0000] [Cache:MISS] [releases.terrateam.io] [Remote_Addr: 213.93.7.141] - - - releases.terrateam.io to: 151.101.85.183:443: "GET /terraform-provider-aws/4.9.0/terraform-provider-aws_4.9.0_linux_amd64.zip HTTP/1.1" 200 53259564 "-" "Terraform/1.1.7" 
3[08/Apr/2022:12:44:58 +0000] [Cache:HIT] [releases.terrateam.io] [Remote_Addr: 213.93.7.141] - - - releases.terrateam.io to: -: "GET /terraform-provider-aws/4.9.0/terraform-provider-aws_4.9.0_linux_amd64.zip HTTP/1.1" 200 53259564 "-" "Terraform/1.1.7" 
4josh@ringo:~/terraform-registry-cache$ 
5

You'll notice I ran the init command two times.

This is to show that the first time Nginx served the request produced a Cache:MISS. The second time Nginx was able to pull from the local cache resulting in a Cache:HIT.

Conclusion

This Nginx setup could easily be integrated against environments that don't have a direct connection to the Internet or company policy requiring the use of proxy servers.

Sign up for Terrateam here!

Share this article

GitHub App Install to Terraform Apply 🚀